aboutsummaryrefslogtreecommitdiffstats
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/TrialActivationInfoBar.js94
-rw-r--r--src/components/auth/AuthLayout.js28
-rw-r--r--src/components/auth/ChangeServer.js4
-rw-r--r--src/components/auth/Locked.js2
-rw-r--r--src/components/auth/Login.js2
-rw-r--r--src/components/auth/Password.js2
-rw-r--r--src/components/auth/Pricing.js270
-rw-r--r--src/components/auth/SetupAssistant.js104
-rw-r--r--src/components/auth/Signup.js5
-rw-r--r--src/components/auth/Welcome.js2
-rw-r--r--src/components/layout/AppLayout.js94
-rw-r--r--src/components/layout/Sidebar.js35
-rw-r--r--src/components/services/content/ConnectionLostBanner.js34
-rw-r--r--src/components/services/content/ErrorHandlers/styles.js2
-rw-r--r--src/components/services/content/ServiceRestricted.js78
-rw-r--r--src/components/services/content/ServiceView.js16
-rw-r--r--src/components/services/content/ServiceWebview.js6
-rw-r--r--src/components/services/content/Services.js9
-rw-r--r--src/components/services/content/WebviewCrashHandler.js2
-rw-r--r--src/components/services/tabs/TabBarSortableList.js18
-rw-r--r--src/components/services/tabs/TabItem.js221
-rw-r--r--src/components/services/tabs/Tabbar.js25
-rw-r--r--src/components/settings/account/AccountDashboard.js159
-rw-r--r--src/components/settings/navigation/SettingsNavigation.js16
-rw-r--r--src/components/settings/recipes/RecipeItem.js5
-rw-r--r--src/components/settings/recipes/RecipesDashboard.js77
-rw-r--r--src/components/settings/services/EditServiceForm.js142
-rw-r--r--src/components/settings/services/ServicesDashboard.js6
-rw-r--r--src/components/settings/settings/EditSettingsForm.js91
-rw-r--r--src/components/settings/supportFerdi/SupportFerdiDashboard.js18
-rw-r--r--src/components/settings/team/TeamDashboard.js39
-rw-r--r--src/components/settings/user/EditUserForm.js2
-rw-r--r--src/components/subscription/SubscriptionForm.js78
-rw-r--r--src/components/subscription/SubscriptionPopup.js84
-rw-r--r--src/components/subscription/TrialForm.js115
-rw-r--r--src/components/ui/ActivateTrialButton/index.js107
-rw-r--r--src/components/ui/AppLoader/index.js2
-rw-r--r--src/components/ui/AppLoader/styles.js8
-rw-r--r--src/components/ui/FeatureItem.js2
-rw-r--r--src/components/ui/FeatureList.js83
-rw-r--r--src/components/ui/ImageUpload.js7
-rw-r--r--src/components/ui/Link.js2
-rw-r--r--src/components/ui/Modal/styles.js2
-rw-r--r--src/components/ui/PremiumFeatureContainer/index.js101
-rw-r--r--src/components/ui/PremiumFeatureContainer/styles.js34
-rw-r--r--src/components/ui/Radio.js2
-rw-r--r--src/components/ui/SearchInput.js2
-rw-r--r--src/components/ui/Select.js4
-rw-r--r--src/components/ui/ServiceIcon.js2
-rw-r--r--src/components/ui/Slider.js2
-rw-r--r--src/components/ui/Tabs/TabItem.js8
-rw-r--r--src/components/ui/Tabs/Tabs.js2
-rw-r--r--src/components/ui/Toggle.js2
-rw-r--r--src/components/ui/ToggleRaw.js2
-rw-r--r--src/components/ui/UpgradeButton/index.js83
-rw-r--r--src/components/ui/WebviewLoader/styles.js2
-rw-r--r--src/components/util/ErrorBoundary/styles.js2
57 files changed, 566 insertions, 1780 deletions
diff --git a/src/components/TrialActivationInfoBar.js b/src/components/TrialActivationInfoBar.js
deleted file mode 100644
index 77ab97565..000000000
--- a/src/components/TrialActivationInfoBar.js
+++ /dev/null
@@ -1,94 +0,0 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { defineMessages, intlShape } from 'react-intl';
4import ms from 'ms';
5import injectSheet from 'react-jss';
6import classnames from 'classnames';
7
8import InfoBar from './ui/InfoBar';
9
10const messages = defineMessages({
11 message: {
12 id: 'infobar.trialActivated',
13 defaultMessage: '!!!Your trial was successfully activated. Happy messaging!',
14 },
15});
16
17const styles = {
18 notification: {
19 height: 'auto',
20 position: 'absolute',
21 top: -50,
22 transition: 'top 0.3s',
23 zIndex: 500,
24 width: 'calc(100% - 300px)',
25 },
26 show: {
27 top: 0,
28 },
29};
30
31@injectSheet(styles)
32class TrialActivationInfoBar extends Component {
33 static propTypes = {
34 // eslint-disable-next-line
35 classes: PropTypes.object.isRequired,
36 };
37
38 static contextTypes = {
39 intl: intlShape,
40 };
41
42 state = {
43 showing: false,
44 removed: false,
45 }
46
47 componentDidMount() {
48 setTimeout(() => {
49 this.setState({
50 showing: true,
51 });
52 }, 0);
53
54 setTimeout(() => {
55 this.setState({
56 showing: false,
57 });
58 }, ms('6s'));
59
60 setTimeout(() => {
61 this.setState({
62 removed: true,
63 });
64 }, ms('7s'));
65 }
66
67 render() {
68 const { classes } = this.props;
69 const { showing, removed } = this.state;
70 const { intl } = this.context;
71
72 if (removed) return null;
73
74 return (
75 <div
76 className={classnames({
77 [classes.notification]: true,
78 [classes.show]: showing,
79 })}
80 >
81 <InfoBar
82 type="primary"
83 position="top"
84 sticky
85 >
86 <span className="mdi mdi-information" />
87 {intl.formatMessage(messages.message)}
88 </InfoBar>
89 </div>
90 );
91 }
92}
93
94export default TrialActivationInfoBar;
diff --git a/src/components/auth/AuthLayout.js b/src/components/auth/AuthLayout.js
index 6fa3adf92..c0e6b982b 100644
--- a/src/components/auth/AuthLayout.js
+++ b/src/components/auth/AuthLayout.js
@@ -7,14 +7,19 @@ import { TitleBar } from 'electron-react-titlebar';
7import Link from '../ui/Link'; 7import Link from '../ui/Link';
8import InfoBar from '../ui/InfoBar'; 8import InfoBar from '../ui/InfoBar';
9 9
10import { oneOrManyChildElements, globalError as globalErrorPropType } from '../../prop-types'; 10import {
11 oneOrManyChildElements,
12 globalError as globalErrorPropType,
13} from '../../prop-types';
11import globalMessages from '../../i18n/globalMessages'; 14import globalMessages from '../../i18n/globalMessages';
12 15
13import { isWindows } from '../../environment'; 16import { isWindows } from '../../environment';
14import AppUpdateInfoBar from '../AppUpdateInfoBar'; 17import AppUpdateInfoBar from '../AppUpdateInfoBar';
15import { GITHUB_FERDI_URL } from '../../config'; 18import { GITHUB_FERDI_URL } from '../../config';
16 19
17export default @observer class AuthLayout extends Component { 20export default
21@observer
22class AuthLayout extends Component {
18 static propTypes = { 23 static propTypes = {
19 children: oneOrManyChildElements.isRequired, 24 children: oneOrManyChildElements.isRequired,
20 error: globalErrorPropType.isRequired, 25 error: globalErrorPropType.isRequired,
@@ -30,7 +35,7 @@ export default @observer class AuthLayout extends Component {
30 35
31 state = { 36 state = {
32 shouldShowAppUpdateInfoBar: true, 37 shouldShowAppUpdateInfoBar: true,
33 } 38 };
34 39
35 static defaultProps = { 40 static defaultProps = {
36 nextAppReleaseVersion: null, 41 nextAppReleaseVersion: null,
@@ -57,12 +62,15 @@ export default @observer class AuthLayout extends Component {
57 62
58 return ( 63 return (
59 <> 64 <>
60 {isWindows && !isFullScreen && <TitleBar menu={window.ferdi.menu.template} icon="assets/images/logo.svg" />} 65 {isWindows && !isFullScreen && (
66 <TitleBar
67 menu={window.ferdi.menu.template}
68 icon="assets/images/logo.svg"
69 />
70 )}
61 <div className="auth"> 71 <div className="auth">
62 {!isOnline && ( 72 {!isOnline && (
63 <InfoBar 73 <InfoBar type="warning">
64 type="warning"
65 >
66 <span className="mdi mdi-flash" /> 74 <span className="mdi mdi-flash" />
67 {intl.formatMessage(globalMessages.notConnectedToTheInternet)} 75 {intl.formatMessage(globalMessages.notConnectedToTheInternet)}
68 </InfoBar> 76 </InfoBar>
@@ -95,7 +103,11 @@ export default @observer class AuthLayout extends Component {
95 })} 103 })}
96 </div> 104 </div>
97 {/* </div> */} 105 {/* </div> */}
98 <Link to={`${GITHUB_FERDI_URL}/ferdi`} className="auth__adlk" target="_blank"> 106 <Link
107 to={`${GITHUB_FERDI_URL}/ferdi`}
108 className="auth__adlk"
109 target="_blank"
110 >
99 <img src="./assets/images/adlk.svg" alt="" /> 111 <img src="./assets/images/adlk.svg" alt="" />
100 </Link> 112 </Link>
101 </div> 113 </div>
diff --git a/src/components/auth/ChangeServer.js b/src/components/auth/ChangeServer.js
index 0dedd825a..7bab80280 100644
--- a/src/components/auth/ChangeServer.js
+++ b/src/components/auth/ChangeServer.js
@@ -100,7 +100,7 @@ export default @observer class ChangeServer extends Component {
100 const { intl } = this.context; 100 const { intl } = this.context;
101 return ( 101 return (
102 <div className="auth__container"> 102 <div className="auth__container">
103 <form className="franz-form auth__form" onSubmit={e => this.submit(e)}> 103 <form className="franz-form auth__form" onSubmit={(e) => this.submit(e)}>
104 <h1>{intl.formatMessage(messages.headline)}</h1> 104 <h1>{intl.formatMessage(messages.headline)}</h1>
105 {form.$('server').value === this.franzServer 105 {form.$('server').value === this.franzServer
106 && ( 106 && (
@@ -113,7 +113,7 @@ export default @observer class ChangeServer extends Component {
113 && ( 113 && (
114 <Input 114 <Input
115 placeholder="Custom Server" 115 placeholder="Custom Server"
116 onChange={e => this.submit(e)} 116 onChange={(e) => this.submit(e)}
117 field={form.$('customServer')} 117 field={form.$('customServer')}
118 /> 118 />
119 )} 119 )}
diff --git a/src/components/auth/Locked.js b/src/components/auth/Locked.js
index 1fdbea595..2ad8a2409 100644
--- a/src/components/auth/Locked.js
+++ b/src/components/auth/Locked.js
@@ -102,7 +102,7 @@ export default @observer class Locked extends Component {
102 102
103 return ( 103 return (
104 <div className="auth__container"> 104 <div className="auth__container">
105 <form className="franz-form auth__form" onSubmit={e => this.submit(e)}> 105 <form className="franz-form auth__form" onSubmit={(e) => this.submit(e)}>
106 <img 106 <img
107 src="./assets/images/logo.svg" 107 src="./assets/images/logo.svg"
108 className="auth__logo" 108 className="auth__logo"
diff --git a/src/components/auth/Login.js b/src/components/auth/Login.js
index 52b09eab6..9e6a8d046 100644
--- a/src/components/auth/Login.js
+++ b/src/components/auth/Login.js
@@ -135,7 +135,7 @@ export default @inject('actions') @observer class Login extends Component {
135 135
136 return ( 136 return (
137 <div className="auth__container"> 137 <div className="auth__container">
138 <form className="franz-form auth__form" onSubmit={e => this.submit(e)}> 138 <form className="franz-form auth__form" onSubmit={(e) => this.submit(e)}>
139 <img 139 <img
140 src="./assets/images/logo.svg" 140 src="./assets/images/logo.svg"
141 className="auth__logo" 141 className="auth__logo"
diff --git a/src/components/auth/Password.js b/src/components/auth/Password.js
index ad34e39af..eafc48502 100644
--- a/src/components/auth/Password.js
+++ b/src/components/auth/Password.js
@@ -86,7 +86,7 @@ export default @observer class Password extends Component {
86 86
87 return ( 87 return (
88 <div className="auth__container"> 88 <div className="auth__container">
89 <form className="franz-form auth__form" onSubmit={e => this.submit(e)}> 89 <form className="franz-form auth__form" onSubmit={(e) => this.submit(e)}>
90 <img 90 <img
91 src="./assets/images/logo.svg" 91 src="./assets/images/logo.svg"
92 className="auth__logo" 92 className="auth__logo"
diff --git a/src/components/auth/Pricing.js b/src/components/auth/Pricing.js
deleted file mode 100644
index 2fcabe54d..000000000
--- a/src/components/auth/Pricing.js
+++ /dev/null
@@ -1,270 +0,0 @@
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 { H2, Loader } from '@meetfranz/ui';
7import classnames from 'classnames';
8
9import { Button } from '@meetfranz/forms';
10import { FeatureItem } from '../ui/FeatureItem';
11import { FeatureList } from '../ui/FeatureList';
12
13const messages = defineMessages({
14 headline: {
15 id: 'pricing.trial.headline.pro',
16 defaultMessage: '!!!Hi {name}, welcome to Franz',
17 },
18 specialTreat: {
19 id: 'pricing.trial.intro.specialTreat',
20 defaultMessage: '!!!We have a special treat for you.',
21 },
22 tryPro: {
23 id: 'pricing.trial.intro.tryPro',
24 defaultMessage: '!!!Enjoy the full Franz Professional experience completely free for 14 days.',
25 },
26 happyMessaging: {
27 id: 'pricing.trial.intro.happyMessaging',
28 defaultMessage: '!!!Happy messaging,',
29 },
30 noStringsAttachedHeadline: {
31 id: 'pricing.trial.terms.headline',
32 defaultMessage: '!!!No strings attached',
33 },
34 noCreditCard: {
35 id: 'pricing.trial.terms.noCreditCard',
36 defaultMessage: '!!!No credit card required',
37 },
38 automaticTrialEnd: {
39 id: 'pricing.trial.terms.automaticTrialEnd',
40 defaultMessage: '!!!Your free trial ends automatically after 14 days',
41 },
42 trialWorth: {
43 id: 'pricing.trial.terms.trialWorth',
44 defaultMessage: '!!!Free trial (normally {currency}{price} per month)',
45 },
46 activationError: {
47 id: 'pricing.trial.error',
48 defaultMessage: '!!!Sorry, we could not activate your trial!',
49 },
50 ctaAccept: {
51 id: 'pricing.trial.cta.accept',
52 defaultMessage: '!!!Start my 14-day Franz Professional Trial ',
53 },
54 ctaStart: {
55 id: 'pricing.trial.cta.start',
56 defaultMessage: '!!!Start using Franz',
57 },
58 ctaSkip: {
59 id: 'pricing.trial.cta.skip',
60 defaultMessage: '!!!Continue to Ferdi',
61 },
62 featuresHeadline: {
63 id: 'pricing.trial.features.headline',
64 defaultMessage: '!!!Franz Professional includes:',
65 },
66});
67
68const styles = theme => ({
69 root: {
70 width: '500px !important',
71 textAlign: 'center',
72 padding: 20,
73 zIndex: 100,
74
75 '& h1': {
76 },
77 },
78 container: {
79 position: 'relative',
80 marginLeft: -150,
81 },
82 welcomeOffer: {
83 textAlign: 'center',
84 fontWeight: 'bold',
85 marginBottom: '6 !important',
86 },
87 keyTerms: {
88 textAlign: 'center',
89 },
90 content: {
91 position: 'relative',
92 zIndex: 20,
93 },
94 featureContainer: {
95 width: 300,
96 position: 'absolute',
97 left: 'calc(100% / 2 + 250px)',
98 marginTop: 20,
99 background: theme.signup.pricing.feature.background,
100 height: 'auto',
101 padding: 20,
102 borderTopRightRadius: theme.borderRadius,
103 borderBottomRightRadius: theme.borderRadius,
104 zIndex: 10,
105 },
106 featureItem: {
107 borderBottom: [1, 'solid', theme.signup.pricing.feature.border],
108 },
109 cta: {
110 marginTop: 40,
111 width: '100%',
112 },
113 skipLink: {
114 textAlign: 'center',
115 marginTop: 10,
116 },
117 error: {
118 margin: [20, 0, 0],
119 color: theme.styleTypes.danger.accent,
120 },
121 priceContainer: {
122 display: 'flex',
123 justifyContent: 'space-evenly',
124 margin: [10, 0, 15],
125 },
126 price: {
127 '& sup': {
128 verticalAlign: 14,
129 fontSize: 20,
130 },
131 },
132 figure: {
133 fontSize: 40,
134 },
135 regularPrice: {
136 position: 'relative',
137
138 '&:before': {
139 content: '" "',
140 position: 'absolute',
141 width: '130%',
142 height: 1,
143 top: 14,
144 left: -12,
145 borderBottom: [3, 'solid', 'red'],
146 transform: 'rotateZ(-20deg)',
147 },
148 },
149});
150
151export default @injectSheet(styles) @observer class Signup extends Component {
152 static propTypes = {
153 onSubmit: PropTypes.func.isRequired,
154 isLoadingRequiredData: PropTypes.bool.isRequired,
155 isActivatingTrial: PropTypes.bool.isRequired,
156 trialActivationError: PropTypes.bool.isRequired,
157 canSkipTrial: PropTypes.bool.isRequired,
158 classes: PropTypes.object.isRequired,
159 currency: PropTypes.string.isRequired,
160 price: PropTypes.number.isRequired,
161 name: PropTypes.string.isRequired,
162 };
163
164 static contextTypes = {
165 intl: intlShape,
166 };
167
168 render() {
169 const {
170 onSubmit,
171 isLoadingRequiredData,
172 isActivatingTrial,
173 trialActivationError,
174 canSkipTrial,
175 classes,
176 currency,
177 price,
178 name,
179 } = this.props;
180 const { intl } = this.context;
181
182 const [intPart, fractionPart] = (price).toString().split('.');
183
184 return (
185 <>
186 <div className={classnames('auth__container', classes.root, classes.container)}>
187 <form className="franz-form auth__form">
188 {isLoadingRequiredData ? <Loader /> : (
189 <img
190 src="./assets/images/sm.png"
191 className="auth__logo auth__logo--sm"
192 alt=""
193 />
194 )}
195 <h1>{intl.formatMessage(messages.headline, { name })}</h1>
196 <div className="auth__letter">
197 <p>
198 {intl.formatMessage(messages.specialTreat)}
199 <br />
200 </p>
201 <p>
202 {intl.formatMessage(messages.tryPro)}
203 <br />
204 </p>
205 <p>
206 {intl.formatMessage(messages.happyMessaging)}
207 </p>
208 <p>
209 <strong>Stefan Malzner</strong>
210 </p>
211 </div>
212 <div className={classes.priceContainer}>
213 <p className={classnames(classes.price, classes.regularPrice)}>
214 <span className={classes.figure}>
215 {currency}
216 {intPart}
217 </span>
218 <sup>{fractionPart}</sup>
219 </p>
220 <p className={classnames(classes.price, classes.trialPrice)}>
221 <span className={classes.figure}>
222 {currency}
223 0
224 </span>
225 <sup>00</sup>
226 </p>
227 </div>
228 <div className={classes.keyTerms}>
229 <H2>
230 {intl.formatMessage(messages.noStringsAttachedHeadline)}
231 </H2>
232 <ul className={classes.keyTermsList}>
233 <FeatureItem
234 icon="👉"
235 name={intl.formatMessage(messages.trialWorth, {
236 currency,
237 price,
238 })}
239 />
240 <FeatureItem icon="👉" name={intl.formatMessage(messages.noCreditCard)} />
241 <FeatureItem icon="👉" name={intl.formatMessage(messages.automaticTrialEnd)} />
242 </ul>
243 </div>
244 {trialActivationError && (
245 <p className={classes.error}>{intl.formatMessage(messages.activationError)}</p>
246 )}
247 <Button
248 label={intl.formatMessage(!canSkipTrial ? messages.ctaStart : messages.ctaAccept)}
249 className={classes.cta}
250 onClick={onSubmit}
251 busy={isActivatingTrial}
252 disabled={isLoadingRequiredData || isActivatingTrial}
253 />
254 {canSkipTrial && (
255 <p className={classes.skipLink}>
256 <a href="#/">{intl.formatMessage(messages.ctaSkip)}</a>
257 </p>
258 )}
259 </form>
260 </div>
261 <div className={classes.featureContainer}>
262 <H2>
263 {intl.formatMessage(messages.featuresHeadline)}
264 </H2>
265 <FeatureList />
266 </div>
267 </>
268 );
269 }
270}
diff --git a/src/components/auth/SetupAssistant.js b/src/components/auth/SetupAssistant.js
index e03cf9101..0f821d4fc 100644
--- a/src/components/auth/SetupAssistant.js
+++ b/src/components/auth/SetupAssistant.js
@@ -18,15 +18,16 @@ const SLACK_ID = 'slack';
18const messages = defineMessages({ 18const messages = defineMessages({
19 headline: { 19 headline: {
20 id: 'setupAssistant.headline', 20 id: 'setupAssistant.headline',
21 defaultMessage: '!!!Let\'s get started', 21 defaultMessage: "!!!Let's get started",
22 }, 22 },
23 subHeadline: { 23 subHeadline: {
24 id: 'setupAssistant.subheadline', 24 id: 'setupAssistant.subheadline',
25 defaultMessage: '!!!Choose from our most used services and get back on top of your messaging now.', 25 defaultMessage:
26 '!!!Choose from our most used services and get back on top of your messaging now.',
26 }, 27 },
27 submitButtonLabel: { 28 submitButtonLabel: {
28 id: 'setupAssistant.submit.label', 29 id: 'setupAssistant.submit.label',
29 defaultMessage: '!!!Let\'s go', 30 defaultMessage: "!!!Let's go",
30 }, 31 },
31 inviteSuccessInfo: { 32 inviteSuccessInfo: {
32 id: 'invite.successInfo', 33 id: 'invite.successInfo',
@@ -34,14 +35,19 @@ const messages = defineMessages({
34 }, 35 },
35}); 36});
36 37
38let transition = 'none';
39
40if (window && window.matchMedia('(prefers-reduced-motion: no-preference)')) {
41 transition = 'all 0.25s';
42}
43
37const styles = theme => ({ 44const styles = theme => ({
38 root: { 45 root: {
39 width: '500px !important', 46 width: '500px !important',
40 textAlign: 'center', 47 textAlign: 'center',
41 padding: 20, 48 padding: 20,
42 49
43 '& h1': { 50 '& h1': {},
44 },
45 }, 51 },
46 servicesGrid: { 52 servicesGrid: {
47 display: 'flex', 53 display: 'flex',
@@ -60,7 +66,7 @@ const styles = theme => ({
60 borderRadius: theme.borderRadius, 66 borderRadius: theme.borderRadius,
61 marginBottom: 10, 67 marginBottom: 10,
62 opacity: 0.5, 68 opacity: 0.5,
63 transition: 'all 0.25s', 69 transition,
64 border: [3, 'solid', 'transparent'], 70 border: [3, 'solid', 'transparent'],
65 71
66 '& h2': { 72 '& h2': {
@@ -70,10 +76,8 @@ const styles = theme => ({
70 76
71 '&:hover': { 77 '&:hover': {
72 border: [3, 'solid', theme.brandPrimary], 78 border: [3, 'solid', theme.brandPrimary],
73 '& $serviceIcon': { 79 '& $serviceIcon': {},
74 },
75 }, 80 },
76
77 }, 81 },
78 selected: { 82 selected: {
79 border: [3, 'solid', theme.brandPrimary], 83 border: [3, 'solid', theme.brandPrimary],
@@ -82,7 +86,7 @@ const styles = theme => ({
82 }, 86 },
83 serviceIcon: { 87 serviceIcon: {
84 width: 50, 88 width: 50,
85 transition: 'all 0.25s', 89 transition,
86 }, 90 },
87 91
88 slackModalContent: { 92 slackModalContent: {
@@ -125,7 +129,8 @@ const styles = theme => ({
125 }, 129 },
126}); 130});
127 131
128@injectSheet(styles) @observer 132@injectSheet(styles)
133@observer
129class SetupAssistant extends Component { 134class SetupAssistant extends Component {
130 static propTypes = { 135 static propTypes = {
131 classes: PropTypes.object.isRequired, 136 classes: PropTypes.object.isRequired,
@@ -144,13 +149,17 @@ class SetupAssistant extends Component {
144 }; 149 };
145 150
146 state = { 151 state = {
147 services: [{ 152 services: [
148 id: 'whatsapp', 153 {
149 }, { 154 id: 'whatsapp',
150 id: 'messenger', 155 },
151 }, { 156 {
152 id: 'gmail', 157 id: 'messenger',
153 }], 158 },
159 {
160 id: 'gmail',
161 },
162 ],
154 isSlackModalOpen: false, 163 isSlackModalOpen: false,
155 slackWorkspace: '', 164 slackWorkspace: '',
156 }; 165 };
@@ -158,7 +167,9 @@ class SetupAssistant extends Component {
158 slackWorkspaceHandler() { 167 slackWorkspaceHandler() {
159 const { slackWorkspace = '', services } = this.state; 168 const { slackWorkspace = '', services } = this.state;
160 169
161 const sanitizedWorkspace = slackWorkspace.trim().replace(/^https?:\/\//, ''); 170 const sanitizedWorkspace = slackWorkspace
171 .trim()
172 .replace(/^https?:\/\//, '');
162 173
163 if (sanitizedWorkspace) { 174 if (sanitizedWorkspace) {
164 const index = services.findIndex(s => s.id === SLACK_ID); 175 const index = services.findIndex(s => s.id === SLACK_ID);
@@ -179,9 +190,17 @@ class SetupAssistant extends Component {
179 render() { 190 render() {
180 const { intl } = this.context; 191 const { intl } = this.context;
181 const { 192 const {
182 classes, isInviteSuccessful, onSubmit, services, isSettingUpServices, 193 classes,
194 isInviteSuccessful,
195 onSubmit,
196 services,
197 isSettingUpServices,
183 } = this.props; 198 } = this.props;
184 const { isSlackModalOpen, slackWorkspace, services: addedServices } = this.state; 199 const {
200 isSlackModalOpen,
201 slackWorkspace,
202 services: addedServices,
203 } = this.state;
185 204
186 return ( 205 return (
187 <div className={`auth__container ${classes.root}`}> 206 <div className={`auth__container ${classes.root}`}>
@@ -197,25 +216,18 @@ class SetupAssistant extends Component {
197 </Appear> 216 </Appear>
198 )} 217 )}
199 218
200 <img 219 <img src="./assets/images/logo.svg" className="auth__logo" alt="" />
201 src="./assets/images/logo.svg" 220 <h1>{intl.formatMessage(messages.headline)}</h1>
202 className="auth__logo" 221 <h2>{intl.formatMessage(messages.subHeadline)}</h2>
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)}> 222 <div className={classnames('grid', classes.servicesGrid)}>
212 {Object.keys(services).map((id) => { 223 {Object.keys(services).map(id => {
213 const service = services[id]; 224 const service = services[id];
214 return ( 225 return (
215 <button 226 <button
216 className={classnames({ 227 className={classnames({
217 [classes.serviceContainer]: true, 228 [classes.serviceContainer]: true,
218 [classes.selected]: this.state.services.findIndex(s => s.id === id) !== -1, 229 [classes.selected]:
230 this.state.services.findIndex(s => s.id === id) !== -1,
219 })} 231 })}
220 key={id} 232 key={id}
221 onClick={() => { 233 onClick={() => {
@@ -244,9 +256,7 @@ class SetupAssistant extends Component {
244 className={classes.serviceIcon} 256 className={classes.serviceIcon}
245 alt="" 257 alt=""
246 /> 258 />
247 <h2> 259 <h2>{service.name}</h2>
248 {service.name}
249 </h2>
250 {id === SLACK_ID && slackWorkspace && ( 260 {id === SLACK_ID && slackWorkspace && (
251 <Badge type="secondary" className={classes.slackBadge}> 261 <Badge type="secondary" className={classes.slackBadge}>
252 {slackWorkspace} 262 {slackWorkspace}
@@ -275,22 +285,22 @@ class SetupAssistant extends Component {
275 <div className={classes.slackModalContent}> 285 <div className={classes.slackModalContent}>
276 <img src={`${CDN_URL}/recipes/dist/slack/src/icon.svg`} alt="" /> 286 <img src={`${CDN_URL}/recipes/dist/slack/src/icon.svg`} alt="" />
277 <h1>Create your first Slack workspace</h1> 287 <h1>Create your first Slack workspace</h1>
278 <form onSubmit={(e) => { 288 <form
279 e.preventDefault(); 289 onSubmit={e => {
280 this.slackWorkspaceHandler(); 290 e.preventDefault();
281 }} 291 this.slackWorkspaceHandler();
292 }}
282 > 293 >
283 <Input 294 <Input
284 suffix=".slack.com" 295 suffix=".slack.com"
285 placeholder="workspace-url" 296 placeholder="workspace-url"
286 onChange={e => this.setState({ slackWorkspace: e.target.value })} 297 onChange={e =>
298 this.setState({ slackWorkspace: e.target.value })
299 }
287 value={slackWorkspace} 300 value={slackWorkspace}
288 /> 301 />
289 <div className={classes.modalActionContainer}> 302 <div className={classes.modalActionContainer}>
290 <Button 303 <Button type="submit" label="Save" />
291 type="submit"
292 label="Save"
293 />
294 <Button 304 <Button
295 type="link" 305 type="link"
296 buttonType="secondary" 306 buttonType="secondary"
@@ -305,7 +315,7 @@ class SetupAssistant extends Component {
305 <Button 315 <Button
306 type="button" 316 type="button"
307 className="auth__button" 317 className="auth__button"
308 // disabled={!atLeastOneEmailAddress} 318 // disabled={!atLeastOneEmailAddress}
309 label={intl.formatMessage(messages.submitButtonLabel)} 319 label={intl.formatMessage(messages.submitButtonLabel)}
310 onClick={() => onSubmit(this.state.services)} 320 onClick={() => onSubmit(this.state.services)}
311 busy={isSettingUpServices} 321 busy={isSettingUpServices}
diff --git a/src/components/auth/Signup.js b/src/components/auth/Signup.js
index 140867ea1..6fb41a164 100644
--- a/src/components/auth/Signup.js
+++ b/src/components/auth/Signup.js
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
4import { observer, inject } from 'mobx-react'; 4import { observer, inject } from 'mobx-react';
5import { defineMessages, intlShape } from 'react-intl'; 5import { defineMessages, intlShape } from 'react-intl';
6 6
7import { isDevMode, termsBase, useLiveAPI } from '../../environment'; 7import { isDevMode, useLiveAPI } from '../../environment';
8import Form from '../../lib/Form'; 8import Form from '../../lib/Form';
9import { required, email, minLength } from '../../helpers/validation-helpers'; 9import { required, email, minLength } from '../../helpers/validation-helpers';
10import serverlessLogin from '../../helpers/serverless-helpers'; 10import serverlessLogin from '../../helpers/serverless-helpers';
@@ -14,6 +14,7 @@ import Link from '../ui/Link';
14import Infobox from '../ui/Infobox'; 14import Infobox from '../ui/Infobox';
15 15
16import { globalError as globalErrorPropType } from '../../prop-types'; 16import { globalError as globalErrorPropType } from '../../prop-types';
17import { termsBase } from '../../api/apiBase';
17 18
18const messages = defineMessages({ 19const messages = defineMessages({
19 headline: { 20 headline: {
@@ -138,7 +139,7 @@ export default @inject('actions') @observer class Signup extends Component {
138 return ( 139 return (
139 <div className="auth__scroll-container"> 140 <div className="auth__scroll-container">
140 <div className="auth__container auth__container--signup"> 141 <div className="auth__container auth__container--signup">
141 <form className="franz-form auth__form" onSubmit={e => this.submit(e)}> 142 <form className="franz-form auth__form" onSubmit={(e) => this.submit(e)}>
142 <img 143 <img
143 src="./assets/images/logo.svg" 144 src="./assets/images/logo.svg"
144 className="auth__logo" 145 className="auth__logo"
diff --git a/src/components/auth/Welcome.js b/src/components/auth/Welcome.js
index 5f2fac64b..cb522e26e 100644
--- a/src/components/auth/Welcome.js
+++ b/src/components/auth/Welcome.js
@@ -84,7 +84,7 @@ export default @inject('actions') @observer class Login extends Component {
84 </Link> 84 </Link>
85 </div> 85 </div>
86 <div className="welcome__featured-services"> 86 <div className="welcome__featured-services">
87 {recipes.map(recipe => ( 87 {recipes.map((recipe) => (
88 <div 88 <div
89 key={recipe.id} 89 key={recipe.id}
90 className="welcome__featured-service" 90 className="welcome__featured-service"
diff --git a/src/components/layout/AppLayout.js b/src/components/layout/AppLayout.js
index a60270a6f..d5e1deb39 100644
--- a/src/components/layout/AppLayout.js
+++ b/src/components/layout/AppLayout.js
@@ -19,10 +19,7 @@ import { isWindows } from '../../environment';
19import WorkspaceSwitchingIndicator from '../../features/workspaces/components/WorkspaceSwitchingIndicator'; 19import WorkspaceSwitchingIndicator from '../../features/workspaces/components/WorkspaceSwitchingIndicator';
20import { workspaceStore } from '../../features/workspaces'; 20import { workspaceStore } from '../../features/workspaces';
21import AppUpdateInfoBar from '../AppUpdateInfoBar'; 21import AppUpdateInfoBar from '../AppUpdateInfoBar';
22import TrialActivationInfoBar from '../TrialActivationInfoBar';
23import Todos from '../../features/todos/containers/TodosScreen'; 22import Todos from '../../features/todos/containers/TodosScreen';
24import PlanSelection from '../../features/planSelection/containers/PlanSelectionScreen';
25import TrialStatusBar from '../../features/trialStatusBar/containers/TrialStatusBarScreen';
26 23
27function createMarkup(HTMLString) { 24function createMarkup(HTMLString) {
28 return { __html: HTMLString }; 25 return { __html: HTMLString };
@@ -43,22 +40,32 @@ const messages = defineMessages({
43 }, 40 },
44 authRequestFailed: { 41 authRequestFailed: {
45 id: 'infobar.authRequestFailed', 42 id: 'infobar.authRequestFailed',
46 defaultMessage: '!!!There were errors while trying to perform an authenticated request. Please try logging out and back in if this error persists.', 43 defaultMessage:
44 '!!!There were errors while trying to perform an authenticated request. Please try logging out and back in if this error persists.',
47 }, 45 },
48}); 46});
49 47
48let transition = 'none';
49
50if (window && window.matchMedia('(prefers-reduced-motion: no-preference)')) {
51 transition = 'transform 0.5s ease';
52}
53
50const styles = theme => ({ 54const styles = theme => ({
51 appContent: { 55 appContent: {
52 // width: `calc(100% + ${theme.workspaces.drawer.width}px)`, 56 // width: `calc(100% + ${theme.workspaces.drawer.width}px)`,
53 width: '100%', 57 width: '100%',
54 transition: 'transform 0.5s ease', 58 transition,
55 transform() { 59 transform() {
56 return workspaceStore.isWorkspaceDrawerOpen ? 'translateX(0)' : `translateX(-${theme.workspaces.drawer.width}px)`; 60 return workspaceStore.isWorkspaceDrawerOpen
61 ? 'translateX(0)'
62 : `translateX(-${theme.workspaces.drawer.width}px)`;
57 }, 63 },
58 }, 64 },
59}); 65});
60 66
61@injectSheet(styles) @observer 67@injectSheet(styles)
68@observer
62class AppLayout extends Component { 69class AppLayout extends Component {
63 static propTypes = { 70 static propTypes = {
64 classes: PropTypes.object.isRequired, 71 classes: PropTypes.object.isRequired,
@@ -79,12 +86,11 @@ class AppLayout extends Component {
79 areRequiredRequestsSuccessful: PropTypes.bool.isRequired, 86 areRequiredRequestsSuccessful: PropTypes.bool.isRequired,
80 retryRequiredRequests: PropTypes.func.isRequired, 87 retryRequiredRequests: PropTypes.func.isRequired,
81 areRequiredRequestsLoading: PropTypes.bool.isRequired, 88 areRequiredRequestsLoading: PropTypes.bool.isRequired,
82 hasActivatedTrial: PropTypes.bool.isRequired,
83 }; 89 };
84 90
85 state = { 91 state = {
86 shouldShowAppUpdateInfoBar: true, 92 shouldShowAppUpdateInfoBar: true,
87 } 93 };
88 94
89 static defaultProps = { 95 static defaultProps = {
90 children: [], 96 children: [],
@@ -115,7 +121,6 @@ class AppLayout extends Component {
115 areRequiredRequestsSuccessful, 121 areRequiredRequestsSuccessful,
116 retryRequiredRequests, 122 retryRequiredRequests,
117 areRequiredRequestsLoading, 123 areRequiredRequestsLoading,
118 hasActivatedTrial,
119 } = this.props; 124 } = this.props;
120 125
121 const { intl } = this.context; 126 const { intl } = this.context;
@@ -123,45 +128,48 @@ class AppLayout extends Component {
123 return ( 128 return (
124 <ErrorBoundary> 129 <ErrorBoundary>
125 <div className="app"> 130 <div className="app">
126 {isWindows && !isFullScreen && <TitleBar menu={window.ferdi.menu.template} icon="assets/images/logo.svg" />} 131 {isWindows && !isFullScreen && (
132 <TitleBar
133 menu={window.ferdi.menu.template}
134 icon="assets/images/logo.svg"
135 />
136 )}
127 <div className={`app__content ${classes.appContent}`}> 137 <div className={`app__content ${classes.appContent}`}>
128 {workspacesDrawer} 138 {workspacesDrawer}
129 {sidebar} 139 {sidebar}
130 <div className="app__service"> 140 <div className="app__service">
131 <WorkspaceSwitchingIndicator /> 141 <WorkspaceSwitchingIndicator />
132 {news.length > 0 && news.map(item => ( 142 {news.length > 0 &&
143 news.map(item => (
144 <InfoBar
145 key={item.id}
146 position="top"
147 type={item.type}
148 sticky={item.sticky}
149 onHide={() => removeNewsItem({ newsId: item.id })}
150 >
151 <span
152 dangerouslySetInnerHTML={createMarkup(item.message)}
153 onClick={event => {
154 const { target } = event;
155 if (target && target.hasAttribute('data-is-news-cta')) {
156 removeNewsItem({ newsId: item.id });
157 }
158 }}
159 />
160 </InfoBar>
161 ))}
162 {!areRequiredRequestsSuccessful && showRequiredRequestsError && (
133 <InfoBar 163 <InfoBar
134 key={item.id} 164 type="danger"
135 position="top" 165 ctaLabel="Try again"
136 type={item.type} 166 ctaLoading={areRequiredRequestsLoading}
137 sticky={item.sticky} 167 sticky
138 onHide={() => removeNewsItem({ newsId: item.id })} 168 onClick={retryRequiredRequests}
139 > 169 >
140 <span 170 <span className="mdi mdi-flash" />
141 dangerouslySetInnerHTML={createMarkup(item.message)} 171 {intl.formatMessage(messages.requiredRequestsFailed)}
142 onClick={(event) => {
143 const { target } = event;
144 if (target && target.hasAttribute('data-is-news-cta')) {
145 removeNewsItem({ newsId: item.id });
146 }
147 }}
148 />
149 </InfoBar> 172 </InfoBar>
150 ))}
151 {hasActivatedTrial && (
152 <TrialActivationInfoBar />
153 )}
154 {!areRequiredRequestsSuccessful && showRequiredRequestsError && (
155 <InfoBar
156 type="danger"
157 ctaLabel="Try again"
158 ctaLoading={areRequiredRequestsLoading}
159 sticky
160 onClick={retryRequiredRequests}
161 >
162 <span className="mdi mdi-flash" />
163 {intl.formatMessage(messages.requiredRequestsFailed)}
164 </InfoBar>
165 )} 173 )}
166 {authRequestFailed && ( 174 {authRequestFailed && (
167 <InfoBar 175 <InfoBar
@@ -186,7 +194,7 @@ class AppLayout extends Component {
186 {intl.formatMessage(messages.servicesUpdated)} 194 {intl.formatMessage(messages.servicesUpdated)}
187 </InfoBar> 195 </InfoBar>
188 )} 196 )}
189 { appUpdateIsDownloaded && this.state.shouldShowAppUpdateInfoBar && ( 197 {appUpdateIsDownloaded && this.state.shouldShowAppUpdateInfoBar && (
190 <AppUpdateInfoBar 198 <AppUpdateInfoBar
191 nextAppReleaseVersion={nextAppReleaseVersion} 199 nextAppReleaseVersion={nextAppReleaseVersion}
192 onInstallUpdate={installAppUpdate} 200 onInstallUpdate={installAppUpdate}
@@ -202,11 +210,9 @@ class AppLayout extends Component {
202 <PublishDebugInfo /> 210 <PublishDebugInfo />
203 {services} 211 {services}
204 {children} 212 {children}
205 <TrialStatusBar />
206 </div> 213 </div>
207 <Todos /> 214 <Todos />
208 </div> 215 </div>
209 <PlanSelection />
210 </div> 216 </div>
211 </ErrorBoundary> 217 </ErrorBoundary>
212 ); 218 );
diff --git a/src/components/layout/Sidebar.js b/src/components/layout/Sidebar.js
index 802538eba..daa5642c3 100644
--- a/src/components/layout/Sidebar.js
+++ b/src/components/layout/Sidebar.js
@@ -6,16 +6,13 @@ import { inject, observer } from 'mobx-react';
6import { Link } from 'react-router'; 6import { Link } from 'react-router';
7 7
8import Tabbar from '../services/tabs/Tabbar'; 8import Tabbar from '../services/tabs/Tabbar';
9import { ctrlKey, isMac } from '../../environment'; 9import { settingsShortcutKey, lockFerdiShortcutKey, todosToggleShortcutKey, workspaceToggleShortcutKey, addNewServiceShortcutKey, muteFerdiShortcutKey } from '../../environment';
10import { workspaceStore } from '../../features/workspaces'; 10import { workspaceStore } from '../../features/workspaces';
11import { todosStore } from '../../features/todos'; 11import { todosStore } from '../../features/todos';
12import { todoActions } from '../../features/todos/actions'; 12import { todoActions } from '../../features/todos/actions';
13import AppStore from '../../stores/AppStore'; 13import AppStore from '../../stores/AppStore';
14import SettingsStore from '../../stores/SettingsStore'; 14import SettingsStore from '../../stores/SettingsStore';
15 15
16// Platform specific shortcut keys
17const settingsShortcutKey = isMac ? ',' : 'P';
18
19const messages = defineMessages({ 16const messages = defineMessages({
20 settings: { 17 settings: {
21 id: 'sidebar.settings', 18 id: 'sidebar.settings',
@@ -58,6 +55,19 @@ const messages = defineMessages({
58export default @inject('stores', 'actions') @observer class Sidebar extends Component { 55export default @inject('stores', 'actions') @observer class Sidebar extends Component {
59 static propTypes = { 56 static propTypes = {
60 openSettings: PropTypes.func.isRequired, 57 openSettings: PropTypes.func.isRequired,
58 closeSettings: PropTypes.func.isRequired,
59 setActive: PropTypes.func.isRequired,
60 reorder: PropTypes.func.isRequired,
61 reload: PropTypes.func.isRequired,
62 toggleNotifications: PropTypes.func.isRequired,
63 toggleAudio: PropTypes.func.isRequired,
64 toggleDarkMode: PropTypes.func.isRequired,
65 showMessageBadgeWhenMutedSetting: PropTypes.bool.isRequired,
66 showMessageBadgesEvenWhenMuted: PropTypes.bool.isRequired,
67 deleteService: PropTypes.func.isRequired,
68 updateService: PropTypes.func.isRequired,
69 hibernateService: PropTypes.func.isRequired,
70 wakeUpService: PropTypes.func.isRequired,
61 toggleMuteApp: PropTypes.func.isRequired, 71 toggleMuteApp: PropTypes.func.isRequired,
62 isAppMuted: PropTypes.bool.isRequired, 72 isAppMuted: PropTypes.bool.isRequired,
63 isWorkspaceDrawerOpen: PropTypes.bool.isRequired, 73 isWorkspaceDrawerOpen: PropTypes.bool.isRequired,
@@ -139,7 +149,7 @@ export default @inject('stores', 'actions') @observer class Sidebar extends Comp
139 }, 149 },
140 }); 150 });
141 }} 151 }}
142 data-tip={`${intl.formatMessage(messages.lockFerdi)} (${ctrlKey}+Shift+L)`} 152 data-tip={`${intl.formatMessage(messages.lockFerdi)} (${lockFerdiShortcutKey(false)})`}
143 > 153 >
144 <i className="mdi mdi-lock" /> 154 <i className="mdi mdi-lock" />
145 </button> 155 </button>
@@ -152,8 +162,8 @@ export default @inject('stores', 'actions') @observer class Sidebar extends Comp
152 this.updateToolTip(); 162 this.updateToolTip();
153 }} 163 }}
154 disabled={isTodosServiceActive} 164 disabled={isTodosServiceActive}
155 className={`sidebar__button sidebar__button--todos ${todosStore.isTodosPanelVisible ? 'is-active' : ''}`} 165 className={`sidebar__button sidebar__button--todos ${todosStore.isTodosPanelVisible ? 'is-active' : ''}`}
156 data-tip={`${intl.formatMessage(todosToggleMessage)} (${ctrlKey}+T)`} 166 data-tip={`${intl.formatMessage(todosToggleMessage)} (${todosToggleShortcutKey(false)})`}
157 > 167 >
158 <i className="mdi mdi-check-all" /> 168 <i className="mdi mdi-check-all" />
159 </button> 169 </button>
@@ -166,7 +176,7 @@ export default @inject('stores', 'actions') @observer class Sidebar extends Comp
166 this.updateToolTip(); 176 this.updateToolTip();
167 }} 177 }}
168 className={`sidebar__button sidebar__button--workspaces ${isWorkspaceDrawerOpen ? 'is-active' : ''}`} 178 className={`sidebar__button sidebar__button--workspaces ${isWorkspaceDrawerOpen ? 'is-active' : ''}`}
169 data-tip={`${intl.formatMessage(workspaceToggleMessage)} (${ctrlKey}+D)`} 179 data-tip={`${intl.formatMessage(workspaceToggleMessage)} (${workspaceToggleShortcutKey(false)})`}
170 > 180 >
171 <i className="mdi mdi-view-grid" /> 181 <i className="mdi mdi-view-grid" />
172 </button> 182 </button>
@@ -178,7 +188,7 @@ export default @inject('stores', 'actions') @observer class Sidebar extends Comp
178 this.updateToolTip(); 188 this.updateToolTip();
179 }} 189 }}
180 className={`sidebar__button sidebar__button--audio ${isAppMuted ? 'is-muted' : ''}`} 190 className={`sidebar__button sidebar__button--audio ${isAppMuted ? 'is-muted' : ''}`}
181 data-tip={`${intl.formatMessage(isAppMuted ? messages.unmute : messages.mute)} (${ctrlKey}+Shift+M)`} 191 data-tip={`${intl.formatMessage(isAppMuted ? messages.unmute : messages.mute)} (${muteFerdiShortcutKey(false)})`}
182 > 192 >
183 <i className={`mdi mdi-bell${isAppMuted ? '-off' : ''}`} /> 193 <i className={`mdi mdi-bell${isAppMuted ? '-off' : ''}`} />
184 </button> 194 </button>
@@ -186,7 +196,7 @@ export default @inject('stores', 'actions') @observer class Sidebar extends Comp
186 type="button" 196 type="button"
187 onClick={() => openSettings({ path: 'recipes' })} 197 onClick={() => openSettings({ path: 'recipes' })}
188 className="sidebar__button sidebar__button--new-service" 198 className="sidebar__button sidebar__button--new-service"
189 data-tip={`${intl.formatMessage(messages.addNewService)} (${ctrlKey}+N)`} 199 data-tip={`${intl.formatMessage(messages.addNewService)} (${addNewServiceShortcutKey(false)})`}
190 > 200 >
191 <i className="mdi mdi-plus-box" /> 201 <i className="mdi mdi-plus-box" />
192 </button> 202 </button>
@@ -204,9 +214,10 @@ export default @inject('stores', 'actions') @observer class Sidebar extends Comp
204 type="button" 214 type="button"
205 onClick={() => openSettings({ path: 'app' })} 215 onClick={() => openSettings({ path: 'app' })}
206 className="sidebar__button sidebar__button--settings" 216 className="sidebar__button sidebar__button--settings"
207 data-tip={`${intl.formatMessage(messages.settings)} (${ctrlKey}+${settingsShortcutKey})`} 217 data-tip={`${intl.formatMessage(messages.settings)} (${settingsShortcutKey(false)})`}
208 > 218 >
209 <i className="mdi mdi-settings" /> 219 {/* TODO: Because of https://github.com/Templarian/MaterialDesign-Webfont/issues/81 bug in @mdi/font in 5.9.55, added `mdi-memory` as a fallback */}
220 <i className="mdi mdi-settings mdi-memory" />
210 { (this.props.stores.app.updateStatus === this.props.stores.app.updateStatusTypes.AVAILABLE 221 { (this.props.stores.app.updateStatus === this.props.stores.app.updateStatusTypes.AVAILABLE
211 || this.props.stores.app.updateStatus === this.props.stores.app.updateStatusTypes.DOWNLOADED) && ( 222 || this.props.stores.app.updateStatus === this.props.stores.app.updateStatusTypes.DOWNLOADED) && (
212 <span className="update-available"> 223 <span className="update-available">
diff --git a/src/components/services/content/ConnectionLostBanner.js b/src/components/services/content/ConnectionLostBanner.js
index e54a88faa..ebe863333 100644
--- a/src/components/services/content/ConnectionLostBanner.js
+++ b/src/components/services/content/ConnectionLostBanner.js
@@ -5,9 +5,7 @@ import injectSheet from 'react-jss';
5import { Icon } from '@meetfranz/ui'; 5import { Icon } from '@meetfranz/ui';
6import { intlShape, defineMessages } from 'react-intl'; 6import { intlShape, defineMessages } from 'react-intl';
7 7
8import { 8import { mdiAlert } from '@mdi/js';
9 mdiAlert,
10} from '@mdi/js';
11import { LIVE_API_FERDI_WEBSITE } from '../../../config'; 9import { LIVE_API_FERDI_WEBSITE } from '../../../config';
12// import { Button } from '@meetfranz/forms'; 10// import { Button } from '@meetfranz/forms';
13 11
@@ -26,6 +24,12 @@ const messages = defineMessages({
26 }, 24 },
27}); 25});
28 26
27let buttonTransition = 'none';
28
29if (window && window.matchMedia('(prefers-reduced-motion: no-preference)')) {
30 buttonTransition = 'opacity 0.25s';
31}
32
29const styles = theme => ({ 33const styles = theme => ({
30 root: { 34 root: {
31 background: theme.colorBackground, 35 background: theme.colorBackground,
@@ -47,7 +51,7 @@ const styles = theme => ({
47 opacity: 0.7, 51 opacity: 0.7,
48 }, 52 },
49 button: { 53 button: {
50 transition: 'opacity 0.25s', 54 transition: buttonTransition,
51 color: theme.colorText, 55 color: theme.colorText,
52 border: [1, 'solid', theme.colorText], 56 border: [1, 'solid', theme.colorText],
53 borderRadius: theme.borderRadiusSmall, 57 borderRadius: theme.borderRadiusSmall,
@@ -65,13 +69,14 @@ const styles = theme => ({
65 }, 69 },
66}); 70});
67 71
68@injectSheet(styles) @observer 72@injectSheet(styles)
73@observer
69class ConnectionLostBanner extends Component { 74class ConnectionLostBanner extends Component {
70 static propTypes = { 75 static propTypes = {
71 classes: PropTypes.object.isRequired, 76 classes: PropTypes.object.isRequired,
72 name: PropTypes.string.isRequired, 77 name: PropTypes.string.isRequired,
73 reload: PropTypes.func.isRequired, 78 reload: PropTypes.func.isRequired,
74 } 79 };
75 80
76 static contextTypes = { 81 static contextTypes = {
77 intl: intlShape, 82 intl: intlShape,
@@ -80,20 +85,13 @@ class ConnectionLostBanner extends Component {
80 inputRef = React.createRef(); 85 inputRef = React.createRef();
81 86
82 render() { 87 render() {
83 const { 88 const { classes, name, reload } = this.props;
84 classes,
85 name,
86 reload,
87 } = this.props;
88 89
89 const { intl } = this.context; 90 const { intl } = this.context;
90 91
91 return ( 92 return (
92 <div className={classes.root}> 93 <div className={classes.root}>
93 <Icon 94 <Icon icon={mdiAlert} className={classes.icon} />
94 icon={mdiAlert}
95 className={classes.icon}
96 />
97 <p> 95 <p>
98 {intl.formatMessage(messages.text, { name })} 96 {intl.formatMessage(messages.text, { name })}
99 <br /> 97 <br />
@@ -104,11 +102,7 @@ class ConnectionLostBanner extends Component {
104 {intl.formatMessage(messages.moreInformation)} 102 {intl.formatMessage(messages.moreInformation)}
105 </a> 103 </a>
106 </p> 104 </p>
107 <button 105 <button type="button" className={classes.button} onClick={reload}>
108 type="button"
109 className={classes.button}
110 onClick={reload}
111 >
112 {intl.formatMessage(messages.cta)} 106 {intl.formatMessage(messages.cta)}
113 </button> 107 </button>
114 </div> 108 </div>
diff --git a/src/components/services/content/ErrorHandlers/styles.js b/src/components/services/content/ErrorHandlers/styles.js
index 9e2509ee5..72d62f5e3 100644
--- a/src/components/services/content/ErrorHandlers/styles.js
+++ b/src/components/services/content/ErrorHandlers/styles.js
@@ -1,4 +1,4 @@
1export default theme => ({ 1export default (theme) => ({
2 component: { 2 component: {
3 left: 0, 3 left: 0,
4 position: 'absolute', 4 position: 'absolute',
diff --git a/src/components/services/content/ServiceRestricted.js b/src/components/services/content/ServiceRestricted.js
deleted file mode 100644
index 4b8d926aa..000000000
--- a/src/components/services/content/ServiceRestricted.js
+++ /dev/null
@@ -1,78 +0,0 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl';
5
6import { serviceLimitStore } from '../../../features/serviceLimit';
7import Button from '../../ui/Button';
8import { RESTRICTION_TYPES } from '../../../models/Service';
9
10const messages = defineMessages({
11 headlineServiceLimit: {
12 id: 'service.restrictedHandler.serviceLimit.headline',
13 defaultMessage: '!!!You have reached your service limit.',
14 },
15 textServiceLimit: {
16 id: 'service.restrictedHandler.serviceLimit.text',
17 defaultMessage: '!!!Please upgrade your account to use more than {count} services.',
18 },
19 headlineCustomUrl: {
20 id: 'service.restrictedHandler.customUrl.headline',
21 defaultMessage: '!!!Franz Professional Plan required',
22 },
23 textCustomUrl: {
24 id: 'service.restrictedHandler.customUrl.text',
25 defaultMessage: '!!!Please upgrade to the Franz Professional plan to use custom urls & self hosted services.',
26 },
27 action: {
28 id: 'service.restrictedHandler.action',
29 defaultMessage: '!!!Upgrade Account',
30 },
31});
32
33export default @observer class ServiceRestricted extends Component {
34 static propTypes = {
35 name: PropTypes.string.isRequired,
36 upgrade: PropTypes.func.isRequired,
37 type: PropTypes.number.isRequired,
38 };
39
40 static contextTypes = {
41 intl: intlShape,
42 };
43
44 countdownInterval = null;
45
46 countdownIntervalTimeout = 1000;
47
48 render() {
49 const {
50 name,
51 upgrade,
52 type,
53 } = this.props;
54 const { intl } = this.context;
55
56 return (
57 <div className="services__info-layer">
58 {type === RESTRICTION_TYPES.SERVICE_LIMIT && (
59 <>
60 <h1>{intl.formatMessage(messages.headlineServiceLimit)}</h1>
61 <p>{intl.formatMessage(messages.textServiceLimit, { count: serviceLimitStore.serviceLimit })}</p>
62 </>
63 )}
64 {type === RESTRICTION_TYPES.CUSTOM_URL && (
65 <>
66 <h1>{intl.formatMessage(messages.headlineCustomUrl)}</h1>
67 <p>{intl.formatMessage(messages.textCustomUrl)}</p>
68 </>
69 )}
70 <Button
71 label={intl.formatMessage(messages.action, { name })}
72 buttonType="inverted"
73 onClick={() => upgrade()}
74 />
75 </div>
76 );
77 }
78}
diff --git a/src/components/services/content/ServiceView.js b/src/components/services/content/ServiceView.js
index 17d2db5a0..3fc084ff0 100644
--- a/src/components/services/content/ServiceView.js
+++ b/src/components/services/content/ServiceView.js
@@ -145,19 +145,17 @@ export default @inject('stores', 'actions') @observer class ServiceView extends
145 </> 145 </>
146 ) : ( 146 ) : (
147 <> 147 <>
148 {(!service.isHibernating || service.isHibernationEnabled) ? ( 148 {!service.isHibernating ? (
149 <> 149 <>
150 {showNavBar && ( 150 {showNavBar && (
151 <WebControlsScreen service={service} /> 151 <WebControlsScreen service={service} />
152 )} 152 )}
153 {!service.isHibernating && ( 153 <ServiceWebview
154 <ServiceWebview 154 service={service}
155 service={service} 155 setWebviewReference={setWebviewReference}
156 setWebviewReference={setWebviewReference} 156 detachService={detachService}
157 detachService={detachService} 157 isSpellcheckerEnabled={isSpellcheckerEnabled}
158 isSpellcheckerEnabled={isSpellcheckerEnabled} 158 />
159 />
160 )}
161 </> 159 </>
162 ) : ( 160 ) : (
163 <div> 161 <div>
diff --git a/src/components/services/content/ServiceWebview.js b/src/components/services/content/ServiceWebview.js
index 9e5fed996..3b499a5db 100644
--- a/src/components/services/content/ServiceWebview.js
+++ b/src/components/services/content/ServiceWebview.js
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import { observable, reaction } from 'mobx'; 4import { observable, reaction } from 'mobx';
5import ElectronWebView from 'react-electron-web-view'; 5import ElectronWebView from 'react-electron-web-view';
6import path from 'path'; 6import { join } from 'path';
7 7
8import ServiceModel from '../../../models/Service'; 8import ServiceModel from '../../../models/Service';
9 9
@@ -59,7 +59,7 @@ class ServiceWebview extends Component {
59 isSpellcheckerEnabled, 59 isSpellcheckerEnabled,
60 } = this.props; 60 } = this.props;
61 61
62 const preloadScript = path.join(__dirname, '../../../', 'webview', 'recipe.js'); 62 const preloadScript = join(__dirname, '..', '..', '..', 'webview', 'recipe.js');
63 63
64 return ( 64 return (
65 <ElectronWebView 65 <ElectronWebView
@@ -83,7 +83,7 @@ class ServiceWebview extends Component {
83 useragent={service.userAgent} 83 useragent={service.userAgent}
84 disablewebsecurity={service.recipe.disablewebsecurity ? true : undefined} 84 disablewebsecurity={service.recipe.disablewebsecurity ? true : undefined}
85 allowpopups 85 allowpopups
86 webpreferences={`spellcheck=${isSpellcheckerEnabled ? 1 : 0}, contextIsolation=false`} 86 webpreferences={`spellcheck=${isSpellcheckerEnabled ? 1 : 0}`}
87 /> 87 />
88 ); 88 );
89 } 89 }
diff --git a/src/components/services/content/Services.js b/src/components/services/content/Services.js
index caa3cf9aa..bb93ff7d4 100644
--- a/src/components/services/content/Services.js
+++ b/src/components/services/content/Services.js
@@ -30,7 +30,7 @@ const messages = defineMessages({
30 }, 30 },
31 serverInfo: { 31 serverInfo: {
32 id: 'services.serverInfo', 32 id: 'services.serverInfo',
33 defaultMessage: '!!!Optionally, you can change your Ferdi server by clicking the cog in the bottom left corner.', 33 defaultMessage: '!!!Optionally, you can change your Ferdi server by clicking the cog in the bottom left corner. If you are switching over (from one of the hosted servers) to using Ferdi without an account, please be informed that you can export your data from that server and subsequently import it using the Help menu to resurrect all your workspaces and configured services!',
34 }, 34 },
35}); 35});
36 36
@@ -54,7 +54,6 @@ export default @injectSheet(styles) @inject('actions') @observer class Services
54 openSettings: PropTypes.func.isRequired, 54 openSettings: PropTypes.func.isRequired,
55 update: PropTypes.func.isRequired, 55 update: PropTypes.func.isRequired,
56 userHasCompletedSignup: PropTypes.bool.isRequired, 56 userHasCompletedSignup: PropTypes.bool.isRequired,
57 hasActivatedTrial: PropTypes.bool.isRequired,
58 classes: PropTypes.object.isRequired, 57 classes: PropTypes.object.isRequired,
59 actions: PropTypes.object.isRequired, 58 actions: PropTypes.object.isRequired,
60 isSpellcheckerEnabled: PropTypes.bool.isRequired, 59 isSpellcheckerEnabled: PropTypes.bool.isRequired,
@@ -109,7 +108,6 @@ export default @injectSheet(styles) @inject('actions') @observer class Services
109 openSettings, 108 openSettings,
110 update, 109 update,
111 userHasCompletedSignup, 110 userHasCompletedSignup,
112 hasActivatedTrial,
113 classes, 111 classes,
114 isSpellcheckerEnabled, 112 isSpellcheckerEnabled,
115 } = this.props; 113 } = this.props;
@@ -123,7 +121,7 @@ export default @injectSheet(styles) @inject('actions') @observer class Services
123 121
124 return ( 122 return (
125 <div className="services"> 123 <div className="services">
126 {(userHasCompletedSignup || hasActivatedTrial) && ( 124 {userHasCompletedSignup && (
127 <div className={classes.confettiContainer}> 125 <div className={classes.confettiContainer}>
128 <Confetti 126 <Confetti
129 width={window.width} 127 width={window.width}
@@ -169,7 +167,7 @@ export default @injectSheet(styles) @inject('actions') @observer class Services
169 </div> 167 </div>
170 </Appear> 168 </Appear>
171 )} 169 )}
172 {services.filter(service => !service.isTodosService).map(service => ( 170 {services.filter((service) => !service.isTodosService).map((service) => (
173 <ServiceView 171 <ServiceView
174 key={service.id} 172 key={service.id}
175 service={service} 173 service={service}
@@ -186,7 +184,6 @@ export default @injectSheet(styles) @inject('actions') @observer class Services
186 }, 184 },
187 redirect: false, 185 redirect: false,
188 })} 186 })}
189 upgrade={() => openSettings({ path: 'user' })}
190 isSpellcheckerEnabled={isSpellcheckerEnabled} 187 isSpellcheckerEnabled={isSpellcheckerEnabled}
191 /> 188 />
192 ))} 189 ))}
diff --git a/src/components/services/content/WebviewCrashHandler.js b/src/components/services/content/WebviewCrashHandler.js
index b62940c06..10ff0bbbb 100644
--- a/src/components/services/content/WebviewCrashHandler.js
+++ b/src/components/services/content/WebviewCrashHandler.js
@@ -47,7 +47,7 @@ export default @observer class WebviewCrashHandler extends Component {
47 const { reload } = this.props; 47 const { reload } = this.props;
48 48
49 this.countdownInterval = setInterval(() => { 49 this.countdownInterval = setInterval(() => {
50 this.setState(prevState => ({ 50 this.setState((prevState) => ({
51 countdown: prevState.countdown - this.countdownIntervalTimeout, 51 countdown: prevState.countdown - this.countdownIntervalTimeout,
52 })); 52 }));
53 53
diff --git a/src/components/services/tabs/TabBarSortableList.js b/src/components/services/tabs/TabBarSortableList.js
index 489027d57..1a389991d 100644
--- a/src/components/services/tabs/TabBarSortableList.js
+++ b/src/components/services/tabs/TabBarSortableList.js
@@ -14,9 +14,12 @@ class TabBarSortableList extends Component {
14 reload: PropTypes.func.isRequired, 14 reload: PropTypes.func.isRequired,
15 toggleNotifications: PropTypes.func.isRequired, 15 toggleNotifications: PropTypes.func.isRequired,
16 toggleAudio: PropTypes.func.isRequired, 16 toggleAudio: PropTypes.func.isRequired,
17 toggleDarkMode: PropTypes.func.isRequired,
17 deleteService: PropTypes.func.isRequired, 18 deleteService: PropTypes.func.isRequired,
18 disableService: PropTypes.func.isRequired, 19 disableService: PropTypes.func.isRequired,
19 enableService: PropTypes.func.isRequired, 20 enableService: PropTypes.func.isRequired,
21 hibernateService: PropTypes.func.isRequired,
22 wakeUpService: PropTypes.func.isRequired,
20 showMessageBadgeWhenMutedSetting: PropTypes.bool.isRequired, 23 showMessageBadgeWhenMutedSetting: PropTypes.bool.isRequired,
21 showMessageBadgesEvenWhenMuted: PropTypes.bool.isRequired, 24 showMessageBadgesEvenWhenMuted: PropTypes.bool.isRequired,
22 } 25 }
@@ -28,9 +31,12 @@ class TabBarSortableList extends Component {
28 reload, 31 reload,
29 toggleNotifications, 32 toggleNotifications,
30 toggleAudio, 33 toggleAudio,
34 toggleDarkMode,
31 deleteService, 35 deleteService,
32 disableService, 36 disableService,
33 enableService, 37 enableService,
38 hibernateService,
39 wakeUpService,
34 openSettings, 40 openSettings,
35 showMessageBadgeWhenMutedSetting, 41 showMessageBadgeWhenMutedSetting,
36 showMessageBadgesEvenWhenMuted, 42 showMessageBadgesEvenWhenMuted,
@@ -50,23 +56,17 @@ class TabBarSortableList extends Component {
50 reload={() => reload({ serviceId: service.id })} 56 reload={() => reload({ serviceId: service.id })}
51 toggleNotifications={() => toggleNotifications({ serviceId: service.id })} 57 toggleNotifications={() => toggleNotifications({ serviceId: service.id })}
52 toggleAudio={() => toggleAudio({ serviceId: service.id })} 58 toggleAudio={() => toggleAudio({ serviceId: service.id })}
59 toggleDarkMode={() => toggleDarkMode({ serviceId: service.id })}
53 deleteService={() => deleteService({ serviceId: service.id })} 60 deleteService={() => deleteService({ serviceId: service.id })}
54 disableService={() => disableService({ serviceId: service.id })} 61 disableService={() => disableService({ serviceId: service.id })}
55 enableService={() => enableService({ serviceId: service.id })} 62 enableService={() => enableService({ serviceId: service.id })}
63 hibernateService={() => hibernateService({ serviceId: service.id })}
64 wakeUpService={() => wakeUpService({ serviceId: service.id })}
56 openSettings={openSettings} 65 openSettings={openSettings}
57 showMessageBadgeWhenMutedSetting={showMessageBadgeWhenMutedSetting} 66 showMessageBadgeWhenMutedSetting={showMessageBadgeWhenMutedSetting}
58 showMessageBadgesEvenWhenMuted={showMessageBadgesEvenWhenMuted} 67 showMessageBadgesEvenWhenMuted={showMessageBadgesEvenWhenMuted}
59 /> 68 />
60 ))} 69 ))}
61 {/* <li>
62 <button
63 className="sidebar__add-service"
64 onClick={() => openSettings({ path: 'recipes' })}
65 data-tip={`${intl.formatMessage(messages.addNewService)} (${ctrlKey}+N)`}
66 >
67 <span className="mdi mdi-plus" />
68 </button>
69 </li> */}
70 </ul> 70 </ul>
71 ); 71 );
72 } 72 }
diff --git a/src/components/services/tabs/TabItem.js b/src/components/services/tabs/TabItem.js
index 5c3149a11..6a6d2c8c5 100644
--- a/src/components/services/tabs/TabItem.js
+++ b/src/components/services/tabs/TabItem.js
@@ -1,6 +1,4 @@
1import { 1import { Menu, dialog, app, getCurrentWindow } from '@electron/remote';
2 Menu, dialog, app, getCurrentWindow,
3} from '@electron/remote';
4import React, { Component } from 'react'; 2import React, { Component } from 'react';
5import { defineMessages, intlShape } from 'react-intl'; 3import { defineMessages, intlShape } from 'react-intl';
6import PropTypes from 'prop-types'; 4import PropTypes from 'prop-types';
@@ -12,9 +10,11 @@ import ms from 'ms';
12 10
13import { observable, autorun } from 'mobx'; 11import { observable, autorun } from 'mobx';
14import ServiceModel from '../../../models/Service'; 12import ServiceModel from '../../../models/Service';
15import { ctrlKey, cmdKey } from '../../../environment'; 13import { shortcutKey } from '../../../environment';
16 14
17const IS_SERVICE_DEBUGGING_ENABLED = (localStorage.getItem('debug') || '').includes('Ferdi:Service'); 15const IS_SERVICE_DEBUGGING_ENABLED = (
16 localStorage.getItem('debug') || ''
17).includes('Ferdi:Service');
18 18
19const messages = defineMessages({ 19const messages = defineMessages({
20 reload: { 20 reload: {
@@ -41,6 +41,14 @@ const messages = defineMessages({
41 id: 'tabs.item.enableAudio', 41 id: 'tabs.item.enableAudio',
42 defaultMessage: '!!!Enable audio', 42 defaultMessage: '!!!Enable audio',
43 }, 43 },
44 enableDarkMode: {
45 id: 'tabs.item.enableDarkMode',
46 defaultMessage: '!!!Enable Dark mode',
47 },
48 disableDarkMode: {
49 id: 'tabs.item.disableDarkMode',
50 defaultMessage: '!!!Disable Dark mode',
51 },
44 disableService: { 52 disableService: {
45 id: 'tabs.item.disableService', 53 id: 'tabs.item.disableService',
46 defaultMessage: '!!!Disable Service', 54 defaultMessage: '!!!Disable Service',
@@ -49,16 +57,35 @@ const messages = defineMessages({
49 id: 'tabs.item.enableService', 57 id: 'tabs.item.enableService',
50 defaultMessage: '!!!Enable Service', 58 defaultMessage: '!!!Enable Service',
51 }, 59 },
60 hibernateService: {
61 id: 'tabs.item.hibernateService',
62 defaultMessage: '!!!Hibernate Service',
63 },
64 wakeUpService: {
65 id: 'tabs.item.wakeUpService',
66 defaultMessage: '!!!Wake Up Service',
67 },
52 deleteService: { 68 deleteService: {
53 id: 'tabs.item.deleteService', 69 id: 'tabs.item.deleteService',
54 defaultMessage: '!!!Delete Service', 70 defaultMessage: '!!!Delete Service',
55 }, 71 },
56 confirmDeleteService: { 72 confirmDeleteService: {
57 id: 'tabs.item.confirmDeleteService', 73 id: 'tabs.item.confirmDeleteService',
58 defaultMessage: '!!!Do you really want to delete the {serviceName} service?', 74 defaultMessage:
75 '!!!Do you really want to delete the {serviceName} service?',
59 }, 76 },
60}); 77});
61 78
79let pollIndicatorTransition = 'none';
80let polledTransition = 'none';
81let pollAnsweredTransition = 'none';
82
83if (window && window.matchMedia('(prefers-reduced-motion: no-preference)')) {
84 pollIndicatorTransition = 'background 0.5s';
85 polledTransition = 'background 0.1s';
86 pollAnsweredTransition = 'background 0.1s';
87}
88
62const styles = { 89const styles = {
63 pollIndicator: { 90 pollIndicator: {
64 position: 'absolute', 91 position: 'absolute',
@@ -67,7 +94,7 @@ const styles = {
67 height: 10, 94 height: 10,
68 borderRadius: 5, 95 borderRadius: 5,
69 background: 'gray', 96 background: 'gray',
70 transition: 'background 0.5s', 97 transition: pollIndicatorTransition,
71 }, 98 },
72 pollIndicatorPoll: { 99 pollIndicatorPoll: {
73 left: 2, 100 left: 2,
@@ -77,18 +104,20 @@ const styles = {
77 }, 104 },
78 polled: { 105 polled: {
79 background: 'yellow !important', 106 background: 'yellow !important',
80 transition: 'background 0.1s', 107 transition: polledTransition,
81 }, 108 },
82 pollAnswered: { 109 pollAnswered: {
83 background: 'green !important', 110 background: 'green !important',
84 transition: 'background 0.1s', 111 transition: pollAnsweredTransition,
85 }, 112 },
86 stale: { 113 stale: {
87 background: 'red !important', 114 background: 'red !important',
88 }, 115 },
89}; 116};
90 117
91@injectSheet(styles) @observer class TabItem extends Component { 118@injectSheet(styles)
119@observer
120class TabItem extends Component {
92 static propTypes = { 121 static propTypes = {
93 classes: PropTypes.object.isRequired, 122 classes: PropTypes.object.isRequired,
94 service: PropTypes.instanceOf(ServiceModel).isRequired, 123 service: PropTypes.instanceOf(ServiceModel).isRequired,
@@ -97,10 +126,13 @@ const styles = {
97 reload: PropTypes.func.isRequired, 126 reload: PropTypes.func.isRequired,
98 toggleNotifications: PropTypes.func.isRequired, 127 toggleNotifications: PropTypes.func.isRequired,
99 toggleAudio: PropTypes.func.isRequired, 128 toggleAudio: PropTypes.func.isRequired,
129 toggleDarkMode: PropTypes.func.isRequired,
100 openSettings: PropTypes.func.isRequired, 130 openSettings: PropTypes.func.isRequired,
101 deleteService: PropTypes.func.isRequired, 131 deleteService: PropTypes.func.isRequired,
102 disableService: PropTypes.func.isRequired, 132 disableService: PropTypes.func.isRequired,
103 enableService: PropTypes.func.isRequired, 133 enableService: PropTypes.func.isRequired,
134 hibernateService: PropTypes.func.isRequired,
135 wakeUpService: PropTypes.func.isRequired,
104 showMessageBadgeWhenMutedSetting: PropTypes.bool.isRequired, 136 showMessageBadgeWhenMutedSetting: PropTypes.bool.isRequired,
105 showMessageBadgesEvenWhenMuted: PropTypes.bool.isRequired, 137 showMessageBadgesEvenWhenMuted: PropTypes.bool.isRequired,
106 }; 138 };
@@ -121,13 +153,17 @@ const styles = {
121 if (Date.now() - service.lastPoll < ms('0.2s')) { 153 if (Date.now() - service.lastPoll < ms('0.2s')) {
122 this.isPolled = true; 154 this.isPolled = true;
123 155
124 setTimeout(() => { this.isPolled = false; }, ms('1s')); 156 setTimeout(() => {
157 this.isPolled = false;
158 }, ms('1s'));
125 } 159 }
126 160
127 if (Date.now() - service.lastPollAnswer < ms('0.2s')) { 161 if (Date.now() - service.lastPollAnswer < ms('0.2s')) {
128 this.isPollAnswered = true; 162 this.isPollAnswered = true;
129 163
130 setTimeout(() => { this.isPollAnswered = false; }, ms('1s')); 164 setTimeout(() => {
165 this.isPollAnswered = false;
166 }, ms('1s'));
131 } 167 }
132 }); 168 });
133 } 169 }
@@ -142,67 +178,103 @@ const styles = {
142 reload, 178 reload,
143 toggleNotifications, 179 toggleNotifications,
144 toggleAudio, 180 toggleAudio,
181 toggleDarkMode,
145 deleteService, 182 deleteService,
146 disableService, 183 disableService,
147 enableService, 184 enableService,
185 hibernateService,
186 wakeUpService,
148 openSettings, 187 openSettings,
149 showMessageBadgeWhenMutedSetting, 188 showMessageBadgeWhenMutedSetting,
150 showMessageBadgesEvenWhenMuted, 189 showMessageBadgesEvenWhenMuted,
151 } = this.props; 190 } = this.props;
152 const { intl } = this.context; 191 const { intl } = this.context;
153 192
154 const menuTemplate = [{ 193 const menuTemplate = [
155 label: service.name || service.recipe.name, 194 {
156 enabled: false, 195 label: service.name || service.recipe.name,
157 }, { 196 enabled: false,
158 type: 'separator', 197 },
159 }, { 198 {
160 label: intl.formatMessage(messages.reload), 199 type: 'separator',
161 click: reload, 200 },
162 accelerator: `${cmdKey}+R`, 201 {
163 }, { 202 label: intl.formatMessage(messages.reload),
164 label: intl.formatMessage(messages.edit), 203 click: reload,
165 click: () => openSettings({ 204 accelerator: `${shortcutKey()}+R`,
166 path: `services/edit/${service.id}`, 205 },
167 }), 206 {
168 }, { 207 label: intl.formatMessage(messages.edit),
169 type: 'separator', 208 click: () =>
170 }, { 209 openSettings({
171 label: service.isNotificationEnabled 210 path: `services/edit/${service.id}`,
172 ? intl.formatMessage(messages.disableNotifications) 211 }),
173 : intl.formatMessage(messages.enableNotifications),
174 click: () => toggleNotifications(),
175 }, {
176 label: service.isMuted
177 ? intl.formatMessage(messages.enableAudio)
178 : intl.formatMessage(messages.disableAudio),
179 click: () => toggleAudio(),
180 }, {
181 label: intl.formatMessage(service.isEnabled ? messages.disableService : messages.enableService),
182 click: () => (service.isEnabled ? disableService() : enableService()),
183 }, {
184 type: 'separator',
185 }, {
186 label: intl.formatMessage(messages.deleteService),
187 click: () => {
188 const selection = dialog.showMessageBoxSync(app.mainWindow, {
189 type: 'question',
190 message: intl.formatMessage(messages.deleteService),
191 detail: intl.formatMessage(messages.confirmDeleteService, { serviceName: service.name || service.recipe.name }),
192 buttons: [
193 'Yes',
194 'No',
195 ],
196 });
197 if (selection === 0) {
198 deleteService();
199 }
200 }, 212 },
201 }]; 213 {
214 type: 'separator',
215 },
216 {
217 label: service.isNotificationEnabled
218 ? intl.formatMessage(messages.disableNotifications)
219 : intl.formatMessage(messages.enableNotifications),
220 click: () => toggleNotifications(),
221 },
222 {
223 label: service.isMuted
224 ? intl.formatMessage(messages.enableAudio)
225 : intl.formatMessage(messages.disableAudio),
226 click: () => toggleAudio(),
227 },
228 {
229 label: service.isDarkModeEnabled
230 ? intl.formatMessage(messages.enableDarkMode)
231 : intl.formatMessage(messages.disableDarkMode),
232 click: () => toggleDarkMode(),
233 },
234 {
235 label: intl.formatMessage(
236 service.isEnabled ? messages.disableService : messages.enableService,
237 ),
238 click: () => (service.isEnabled ? disableService() : enableService()),
239 },
240 {
241 label: intl.formatMessage(
242 service.isHibernating
243 ? messages.wakeUpService
244 : messages.hibernateService,
245 ),
246 click: () =>
247 (service.isHibernating ? wakeUpService() : hibernateService()),
248 enabled: service.canHibernate,
249 },
250 {
251 type: 'separator',
252 },
253 {
254 label: intl.formatMessage(messages.deleteService),
255 click: () => {
256 const selection = dialog.showMessageBoxSync(app.mainWindow, {
257 type: 'question',
258 message: intl.formatMessage(messages.deleteService),
259 detail: intl.formatMessage(messages.confirmDeleteService, {
260 serviceName: service.name || service.recipe.name,
261 }),
262 buttons: ['Yes', 'No'],
263 });
264 if (selection === 0) {
265 deleteService();
266 }
267 },
268 },
269 ];
202 const menu = Menu.buildFromTemplate(menuTemplate); 270 const menu = Menu.buildFromTemplate(menuTemplate);
203 271
204 let notificationBadge = null; 272 let notificationBadge = null;
205 if ((showMessageBadgeWhenMutedSetting || service.isNotificationEnabled) && showMessageBadgesEvenWhenMuted && service.isBadgeEnabled) { 273 if (
274 (showMessageBadgeWhenMutedSetting || service.isNotificationEnabled) &&
275 showMessageBadgesEvenWhenMuted &&
276 service.isBadgeEnabled
277 ) {
206 notificationBadge = ( 278 notificationBadge = (
207 <span> 279 <span>
208 {service.unreadDirectMessageCount > 0 && ( 280 {service.unreadDirectMessageCount > 0 && (
@@ -210,17 +282,13 @@ const styles = {
210 {service.unreadDirectMessageCount} 282 {service.unreadDirectMessageCount}
211 </span> 283 </span>
212 )} 284 )}
213 {service.unreadIndirectMessageCount > 0 285 {service.unreadIndirectMessageCount > 0 &&
214 && service.unreadDirectMessageCount === 0 286 service.unreadDirectMessageCount === 0 &&
215 && service.isIndirectMessageBadgeEnabled && ( 287 service.isIndirectMessageBadgeEnabled && (
216 <span className="tab-item__message-count is-indirect"> 288 <span className="tab-item__message-count is-indirect">•</span>
217 •
218 </span>
219 )} 289 )}
220 {service.isHibernating && !service.isHibernationEnabled && ( 290 {service.isHibernating && (
221 <span className="tab-item__message-count hibernating"> 291 <span className="tab-item__message-count hibernating">•</span>
222 •
223 </span>
224 )} 292 )}
225 </span> 293 </span>
226 ); 294 );
@@ -229,7 +297,8 @@ const styles = {
229 return ( 297 return (
230 <li 298 <li
231 className={classnames({ 299 className={classnames({
232 [classes.stale]: IS_SERVICE_DEBUGGING_ENABLED && service.lostRecipeConnection, 300 [classes.stale]:
301 IS_SERVICE_DEBUGGING_ENABLED && service.lostRecipeConnection,
233 'tab-item': true, 302 'tab-item': true,
234 'is-active': service.isActive, 303 'is-active': service.isActive,
235 'has-custom-icon': service.hasCustomIcon, 304 'has-custom-icon': service.hasCustomIcon,
@@ -237,13 +306,11 @@ const styles = {
237 })} 306 })}
238 onClick={clickHandler} 307 onClick={clickHandler}
239 onContextMenu={() => menu.popup(getCurrentWindow())} 308 onContextMenu={() => menu.popup(getCurrentWindow())}
240 data-tip={`${service.name} ${shortcutIndex <= 9 ? `(${ctrlKey}+${shortcutIndex})` : ''}`} 309 data-tip={`${service.name} ${
310 shortcutIndex <= 9 ? `(${shortcutKey(false)}+${shortcutIndex})` : ''
311 }`}
241 > 312 >
242 <img 313 <img src={service.icon} className="tab-item__icon" alt="" />
243 src={service.icon}
244 className="tab-item__icon"
245 alt=""
246 />
247 {notificationBadge} 314 {notificationBadge}
248 {IS_SERVICE_DEBUGGING_ENABLED && ( 315 {IS_SERVICE_DEBUGGING_ENABLED && (
249 <> 316 <>
diff --git a/src/components/services/tabs/Tabbar.js b/src/components/services/tabs/Tabbar.js
index 5e8260ad0..ab1e46c9f 100644
--- a/src/components/services/tabs/Tabbar.js
+++ b/src/components/services/tabs/Tabbar.js
@@ -15,8 +15,11 @@ export default @observer class TabBar extends Component {
15 reload: PropTypes.func.isRequired, 15 reload: PropTypes.func.isRequired,
16 toggleNotifications: PropTypes.func.isRequired, 16 toggleNotifications: PropTypes.func.isRequired,
17 toggleAudio: PropTypes.func.isRequired, 17 toggleAudio: PropTypes.func.isRequired,
18 toggleDarkMode: PropTypes.func.isRequired,
18 deleteService: PropTypes.func.isRequired, 19 deleteService: PropTypes.func.isRequired,
19 updateService: PropTypes.func.isRequired, 20 updateService: PropTypes.func.isRequired,
21 hibernateService: PropTypes.func.isRequired,
22 wakeUpService: PropTypes.func.isRequired,
20 showMessageBadgeWhenMutedSetting: PropTypes.bool.isRequired, 23 showMessageBadgeWhenMutedSetting: PropTypes.bool.isRequired,
21 showMessageBadgesEvenWhenMuted: PropTypes.bool.isRequired, 24 showMessageBadgesEvenWhenMuted: PropTypes.bool.isRequired,
22 }; 25 };
@@ -31,7 +34,7 @@ export default @observer class TabBar extends Component {
31 reorder({ oldIndex, newIndex }); 34 reorder({ oldIndex, newIndex });
32 }; 35 };
33 36
34 shouldPreventSorting = event => event.target.tagName !== 'LI'; 37 shouldPreventSorting = (event) => event.target.tagName !== 'LI';
35 38
36 toggleService = ({ serviceId, isEnabled }) => { 39 toggleService = ({ serviceId, isEnabled }) => {
37 const { updateService } = this.props; 40 const { updateService } = this.props;
@@ -55,6 +58,18 @@ export default @observer class TabBar extends Component {
55 this.toggleService({ serviceId, isEnabled: true }); 58 this.toggleService({ serviceId, isEnabled: true });
56 } 59 }
57 60
61 hibernateService({ serviceId }) {
62 if (serviceId) {
63 this.props.hibernateService({ serviceId });
64 }
65 }
66
67 wakeUpService({ serviceId }) {
68 if (serviceId) {
69 this.props.wakeUpService({ serviceId });
70 }
71 }
72
58 render() { 73 render() {
59 const { 74 const {
60 services, 75 services,
@@ -64,6 +79,7 @@ export default @observer class TabBar extends Component {
64 reload, 79 reload,
65 toggleNotifications, 80 toggleNotifications,
66 toggleAudio, 81 toggleAudio,
82 toggleDarkMode,
67 deleteService, 83 deleteService,
68 showMessageBadgeWhenMutedSetting, 84 showMessageBadgeWhenMutedSetting,
69 showMessageBadgesEvenWhenMuted, 85 showMessageBadgesEvenWhenMuted,
@@ -80,9 +96,12 @@ export default @observer class TabBar extends Component {
80 reload={reload} 96 reload={reload}
81 toggleNotifications={toggleNotifications} 97 toggleNotifications={toggleNotifications}
82 toggleAudio={toggleAudio} 98 toggleAudio={toggleAudio}
99 toggleDarkMode={toggleDarkMode}
83 deleteService={deleteService} 100 deleteService={deleteService}
84 disableService={args => this.disableService(args)} 101 disableService={(args) => this.disableService(args)}
85 enableService={args => this.enableService(args)} 102 enableService={(args) => this.enableService(args)}
103 hibernateService={(args) => this.hibernateService(args)}
104 wakeUpService={(args) => this.wakeUpService(args)}
86 openSettings={openSettings} 105 openSettings={openSettings}
87 distance={20} 106 distance={20}
88 axis="y" 107 axis="y"
diff --git a/src/components/settings/account/AccountDashboard.js b/src/components/settings/account/AccountDashboard.js
index 68d88e218..ef7748343 100644
--- a/src/components/settings/account/AccountDashboard.js
+++ b/src/components/settings/account/AccountDashboard.js
@@ -3,14 +3,11 @@ import PropTypes from 'prop-types';
3import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; 3import { observer, PropTypes as MobxPropTypes } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl'; 4import { defineMessages, intlShape } from 'react-intl';
5import ReactTooltip from 'react-tooltip'; 5import ReactTooltip from 'react-tooltip';
6import { ProBadge, H1, H2 } from '@meetfranz/ui'; 6import { H1, H2 } from '@meetfranz/ui';
7import moment from 'moment';
8 7
9import Loader from '../../ui/Loader'; 8import Loader from '../../ui/Loader';
10import Button from '../../ui/Button'; 9import Button from '../../ui/Button';
11import Infobox from '../../ui/Infobox'; 10import Infobox from '../../ui/Infobox';
12import SubscriptionForm from '../../../containers/subscription/SubscriptionFormScreen';
13import { i18nPlanName } from '../../../helpers/plan-helpers';
14import { LOCAL_SERVER, LIVE_FRANZ_API } from '../../../config'; 11import { LOCAL_SERVER, LIVE_FRANZ_API } from '../../../config';
15 12
16const messages = defineMessages({ 13const messages = defineMessages({
@@ -18,30 +15,10 @@ const messages = defineMessages({
18 id: 'settings.account.headline', 15 id: 'settings.account.headline',
19 defaultMessage: '!!!Account', 16 defaultMessage: '!!!Account',
20 }, 17 },
21 headlineSubscription: {
22 id: 'settings.account.headlineSubscription',
23 defaultMessage: '!!!Your Subscription',
24 },
25 headlineDangerZone: { 18 headlineDangerZone: {
26 id: 'settings.account.headlineDangerZone', 19 id: 'settings.account.headlineDangerZone',
27 defaultMessage: '!!Danger Zone', 20 defaultMessage: '!!Danger Zone',
28 }, 21 },
29 manageSubscriptionButtonLabel: {
30 id: 'settings.account.manageSubscription.label',
31 defaultMessage: '!!!Manage your subscription',
32 },
33 upgradeAccountToPro: {
34 id: 'settings.account.upgradeToPro.label',
35 defaultMessage: '!!!Upgrade to Franz Professional',
36 },
37 accountTypeBasic: {
38 id: 'settings.account.accountType.basic',
39 defaultMessage: '!!!Basic Account',
40 },
41 accountTypePremium: {
42 id: 'settings.account.accountType.premium',
43 defaultMessage: '!!!Premium Supporter Account',
44 },
45 accountEditButton: { 22 accountEditButton: {
46 id: 'settings.account.account.editButton', 23 id: 'settings.account.account.editButton',
47 defaultMessage: '!!!Edit Account', 24 defaultMessage: '!!!Edit Account',
@@ -50,10 +27,6 @@ const messages = defineMessages({
50 id: 'settings.account.headlineInvoices', 27 id: 'settings.account.headlineInvoices',
51 defaultMessage: '!!Invoices', 28 defaultMessage: '!!Invoices',
52 }, 29 },
53 invoiceDownload: {
54 id: 'settings.account.invoiceDownload',
55 defaultMessage: '!!!Download',
56 },
57 userInfoRequestFailed: { 30 userInfoRequestFailed: {
58 id: 'settings.account.userInfoRequestFailed', 31 id: 'settings.account.userInfoRequestFailed',
59 defaultMessage: '!!!Could not load user information', 32 defaultMessage: '!!!Could not load user information',
@@ -76,23 +49,10 @@ const messages = defineMessages({
76 defaultMessage: 49 defaultMessage:
77 '!!!You have received an email with a link to confirm your account deletion. Your account and data cannot be restored!', 50 '!!!You have received an email with a link to confirm your account deletion. Your account and data cannot be restored!',
78 }, 51 },
79 trial: {
80 id: 'settings.account.trial',
81 defaultMessage: '!!!Free Trial',
82 },
83 yourLicense: { 52 yourLicense: {
84 id: 'settings.account.yourLicense', 53 id: 'settings.account.yourLicense',
85 defaultMessage: '!!!Your Franz License:', 54 defaultMessage: '!!!Your Franz License:',
86 }, 55 },
87 trialEndsIn: {
88 id: 'settings.account.trialEndsIn',
89 defaultMessage: '!!!Your free trial ends in {duration}.',
90 },
91 trialUpdateBillingInformation: {
92 id: 'settings.account.trialUpdateBillingInfo',
93 defaultMessage:
94 '!!!Please update your billing info to continue using {license} after your trial period.',
95 },
96 accountUnavailable: { 56 accountUnavailable: {
97 id: 'settings.account.accountUnavailable', 57 id: 'settings.account.accountUnavailable',
98 defaultMessage: 'Account is unavailable', 58 defaultMessage: 'Account is unavailable',
@@ -107,8 +67,6 @@ const messages = defineMessages({
107class AccountDashboard extends Component { 67class AccountDashboard extends Component {
108 static propTypes = { 68 static propTypes = {
109 user: MobxPropTypes.observableObject.isRequired, 69 user: MobxPropTypes.observableObject.isRequired,
110 isPremiumOverrideUser: PropTypes.bool.isRequired,
111 isProUser: PropTypes.bool.isRequired,
112 isLoading: PropTypes.bool.isRequired, 70 isLoading: PropTypes.bool.isRequired,
113 userInfoRequestFailed: PropTypes.bool.isRequired, 71 userInfoRequestFailed: PropTypes.bool.isRequired,
114 retryUserInfoRequest: PropTypes.func.isRequired, 72 retryUserInfoRequest: PropTypes.func.isRequired,
@@ -116,10 +74,7 @@ class AccountDashboard extends Component {
116 isLoadingDeleteAccount: PropTypes.bool.isRequired, 74 isLoadingDeleteAccount: PropTypes.bool.isRequired,
117 isDeleteAccountSuccessful: PropTypes.bool.isRequired, 75 isDeleteAccountSuccessful: PropTypes.bool.isRequired,
118 openEditAccount: PropTypes.func.isRequired, 76 openEditAccount: PropTypes.func.isRequired,
119 openBilling: PropTypes.func.isRequired,
120 upgradeToPro: PropTypes.func.isRequired,
121 openInvoices: PropTypes.func.isRequired, 77 openInvoices: PropTypes.func.isRequired,
122 onCloseSubscriptionWindow: PropTypes.func.isRequired,
123 server: PropTypes.string.isRequired, 78 server: PropTypes.string.isRequired,
124 }; 79 };
125 80
@@ -130,8 +85,6 @@ class AccountDashboard extends Component {
130 render() { 85 render() {
131 const { 86 const {
132 user, 87 user,
133 isPremiumOverrideUser,
134 isProUser,
135 isLoading, 88 isLoading,
136 userInfoRequestFailed, 89 userInfoRequestFailed,
137 retryUserInfoRequest, 90 retryUserInfoRequest,
@@ -139,20 +92,11 @@ class AccountDashboard extends Component {
139 isLoadingDeleteAccount, 92 isLoadingDeleteAccount,
140 isDeleteAccountSuccessful, 93 isDeleteAccountSuccessful,
141 openEditAccount, 94 openEditAccount,
142 openBilling,
143 upgradeToPro,
144 openInvoices, 95 openInvoices,
145 onCloseSubscriptionWindow,
146 server, 96 server,
147 } = this.props; 97 } = this.props;
148 const { intl } = this.context; 98 const { intl } = this.context;
149 99
150 let planName = '';
151
152 if (user.team && user.team.plan) {
153 planName = i18nPlanName(user.team.plan, intl);
154 }
155
156 const isUsingWithoutAccount = server === LOCAL_SERVER; 100 const isUsingWithoutAccount = server === LOCAL_SERVER;
157 const isUsingFranzServer = server === LIVE_FRANZ_API; 101 const isUsingFranzServer = server === LIVE_FRANZ_API;
158 102
@@ -210,98 +154,40 @@ class AccountDashboard extends Component {
210 <div className="account__info"> 154 <div className="account__info">
211 <H1> 155 <H1>
212 <span className="username">{`${user.firstname} ${user.lastname}`}</span> 156 <span className="username">{`${user.firstname} ${user.lastname}`}</span>
213 {user.isPremium && (
214 <>
215 {' '}
216 <ProBadge />
217 </>
218 )}
219 </H1> 157 </H1>
220 <p> 158 <p>
221 {user.organization && `${user.organization}, `} 159 {user.organization && `${user.organization}, `}
222 {user.email} 160 {user.email}
223 </p> 161 </p>
224 {user.isPremium && ( 162 <div className="manage-user-links">
225 <div className="manage-user-links"> 163 <Button
226 <Button 164 label={intl.formatMessage(
227 label={intl.formatMessage( 165 messages.accountEditButton,
228 messages.accountEditButton, 166 )}
229 )} 167 className="franz-form__button--inverted"
230 className="franz-form__button--inverted" 168 onClick={openEditAccount}
231 onClick={openEditAccount} 169 />
232 /> 170 </div>
233 </div>
234 )}
235 </div> 171 </div>
236 {!user.isPremium && ( 172 <Button
237 <Button 173 label={intl.formatMessage(
238 label={intl.formatMessage( 174 messages.accountEditButton,
239 messages.accountEditButton, 175 )}
240 )} 176 className="franz-form__button--inverted"
241 className="franz-form__button--inverted" 177 onClick={openEditAccount}
242 onClick={openEditAccount} 178 />
243 />
244 )}
245 </div> 179 </div>
246 </div> 180 </div>
247 {user.isPremium && user.isSubscriptionOwner && isUsingFranzServer && ( 181 {user.isSubscriptionOwner && isUsingFranzServer && (
248 <div className="account"> 182 <div className="account">
249 <div className="account__box"> 183 <div className="account__box">
250 <H2>{intl.formatMessage(messages.yourLicense)}</H2> 184 <H2>{intl.formatMessage(messages.yourLicense)}</H2>
251 <p> 185 <p>
252 Franz 186 Franz
253 {' '}
254 {isPremiumOverrideUser ? 'Premium' : planName}
255 {user.team.isTrial && (
256 <>
257 {' – '}
258 {intl.formatMessage(messages.trial)}
259 </>
260 )}
261 </p> 187 </p>
262 {user.team.isTrial && (
263 <>
264 <br />
265 <p>
266 {intl.formatMessage(messages.trialEndsIn, {
267 duration: moment
268 .duration(
269 moment().diff(user.team.trialEnd),
270 )
271 .humanize(),
272 })}
273 </p>
274 <p>
275 {intl.formatMessage(
276 messages.trialUpdateBillingInformation,
277 {
278 license: planName,
279 },
280 )}
281 </p>
282 </>
283 )}
284 {!isProUser && (
285 <div className="manage-user-links">
286 <Button
287 label={intl.formatMessage(
288 messages.upgradeAccountToPro,
289 )}
290 className="franz-form__button--primary"
291 onClick={upgradeToPro}
292 />
293 </div>
294 )}
295 <div className="manage-user-links"> 188 <div className="manage-user-links">
296 <Button 189 <Button
297 label={intl.formatMessage( 190 label={intl.formatMessage(
298 messages.manageSubscriptionButtonLabel,
299 )}
300 className="franz-form__button--inverted"
301 onClick={openBilling}
302 />
303 <Button
304 label={intl.formatMessage(
305 messages.invoicesButton, 191 messages.invoicesButton,
306 )} 192 )}
307 className="franz-form__button--inverted" 193 className="franz-form__button--inverted"
@@ -311,15 +197,6 @@ class AccountDashboard extends Component {
311 </div> 197 </div>
312 </div> 198 </div>
313 )} 199 )}
314 {!user.isPremium && (
315 <div className="account franz-form">
316 <div className="account__box">
317 <SubscriptionForm
318 onCloseWindow={onCloseSubscriptionWindow}
319 />
320 </div>
321 </div>
322 )}
323 </> 200 </>
324 )} 201 )}
325 202
diff --git a/src/components/settings/navigation/SettingsNavigation.js b/src/components/settings/navigation/SettingsNavigation.js
index cebab2f12..02cae6b69 100644
--- a/src/components/settings/navigation/SettingsNavigation.js
+++ b/src/components/settings/navigation/SettingsNavigation.js
@@ -2,7 +2,6 @@ import React, { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { defineMessages, intlShape } from 'react-intl'; 3import { defineMessages, intlShape } from 'react-intl';
4import { inject, observer } from 'mobx-react'; 4import { inject, observer } from 'mobx-react';
5import { ProBadge } from '@meetfranz/ui';
6import { RouterStore } from 'mobx-react-router'; 5import { RouterStore } from 'mobx-react-router';
7 6
8import { LOCAL_SERVER, LIVE_FERDI_API, LIVE_FRANZ_API } from '../../../config'; 7import { LOCAL_SERVER, LIVE_FERDI_API, LIVE_FRANZ_API } from '../../../config';
@@ -11,7 +10,6 @@ import { workspaceStore } from '../../../features/workspaces';
11import UIStore from '../../../stores/UIStore'; 10import UIStore from '../../../stores/UIStore';
12import SettingsStore from '../../../stores/SettingsStore'; 11import SettingsStore from '../../../stores/SettingsStore';
13import UserStore from '../../../stores/UserStore'; 12import UserStore from '../../../stores/UserStore';
14import { serviceLimitStore } from '../../../features/serviceLimit';
15 13
16const messages = defineMessages({ 14const messages = defineMessages({
17 availableServices: { 15 availableServices: {
@@ -98,8 +96,6 @@ export default @inject('stores', 'actions') @observer class SettingsNavigation e
98 96
99 render() { 97 render() {
100 const { serviceCount, workspaceCount, stores } = this.props; 98 const { serviceCount, workspaceCount, stores } = this.props;
101 const { isDarkThemeActive } = stores.ui;
102 const { router, user } = stores;
103 const { intl } = this.context; 99 const { intl } = this.context;
104 const isLoggedIn = Boolean(localStorage.getItem('authToken')); 100 const isLoggedIn = Boolean(localStorage.getItem('authToken'));
105 const isUsingWithoutAccount = stores.settings.app.server === LOCAL_SERVER; 101 const isUsingWithoutAccount = stores.settings.app.server === LOCAL_SERVER;
@@ -124,9 +120,6 @@ export default @inject('stores', 'actions') @observer class SettingsNavigation e
124 {' '} 120 {' '}
125 <span className="badge"> 121 <span className="badge">
126 {serviceCount} 122 {serviceCount}
127 {serviceLimitStore.serviceLimit !== 0 && (
128 `/${serviceLimitStore.serviceLimit}`
129 )}
130 </span> 123 </span>
131 </Link> 124 </Link>
132 {workspaceStore.isFeatureEnabled ? ( 125 {workspaceStore.isFeatureEnabled ? (
@@ -138,11 +131,7 @@ export default @inject('stores', 'actions') @observer class SettingsNavigation e
138 > 131 >
139 {intl.formatMessage(messages.yourWorkspaces)} 132 {intl.formatMessage(messages.yourWorkspaces)}
140 {' '} 133 {' '}
141 {workspaceStore.isPremiumUpgradeRequired ? ( 134 <span className="badge">{workspaceCount}</span>
142 <ProBadge inverted={!isDarkThemeActive && workspaceStore.isSettingsRouteActive} />
143 ) : (
144 <span className="badge">{workspaceCount}</span>
145 )}
146 </Link> 135 </Link>
147 ) : null} 136 ) : null}
148 {!isUsingWithoutAccount && ( 137 {!isUsingWithoutAccount && (
@@ -163,9 +152,6 @@ export default @inject('stores', 'actions') @observer class SettingsNavigation e
163 disabled={!isLoggedIn} 152 disabled={!isLoggedIn}
164 > 153 >
165 {intl.formatMessage(messages.team)} 154 {intl.formatMessage(messages.team)}
166 {!user.data.isPremium && (
167 <ProBadge inverted={!isDarkThemeActive && router.location.pathname === '/settings/team'} />
168 )}
169 </Link> 155 </Link>
170 )} 156 )}
171 <Link 157 <Link
diff --git a/src/components/settings/recipes/RecipeItem.js b/src/components/settings/recipes/RecipeItem.js
index 12e3775f6..55f415bd5 100644
--- a/src/components/settings/recipes/RecipeItem.js
+++ b/src/components/settings/recipes/RecipeItem.js
@@ -28,6 +28,11 @@ export default @observer class RecipeItem extends Component {
28 alt="" 28 alt=""
29 /> 29 />
30 <span className="recipe-teaser__label">{recipe.name}</span> 30 <span className="recipe-teaser__label">{recipe.name}</span>
31 {recipe.aliases && recipe.aliases.length > 0 && (
32 <span className="recipe-teaser__alias_label">
33 {`Aliases: ${recipe.aliases.join(', ')}`}
34 </span>
35 )}
31 </button> 36 </button>
32 ); 37 );
33 } 38 }
diff --git a/src/components/settings/recipes/RecipesDashboard.js b/src/components/settings/recipes/RecipesDashboard.js
index b4e2fc05c..44ff2d0d7 100644
--- a/src/components/settings/recipes/RecipesDashboard.js
+++ b/src/components/settings/recipes/RecipesDashboard.js
@@ -6,15 +6,13 @@ import { Link } from 'react-router';
6 6
7import { Button, Input } from '@meetfranz/forms'; 7import { Button, Input } from '@meetfranz/forms';
8import injectSheet from 'react-jss'; 8import injectSheet from 'react-jss';
9import { H3, H2, ProBadge } from '@meetfranz/ui'; 9import { H3, H2 } from '@meetfranz/ui';
10import SearchInput from '../../ui/SearchInput'; 10import SearchInput from '../../ui/SearchInput';
11import Infobox from '../../ui/Infobox'; 11import Infobox from '../../ui/Infobox';
12import RecipeItem from './RecipeItem'; 12import RecipeItem from './RecipeItem';
13import Loader from '../../ui/Loader'; 13import Loader from '../../ui/Loader';
14import Appear from '../../ui/effects/Appear'; 14import Appear from '../../ui/effects/Appear';
15import { FRANZ_SERVICE_REQUEST } from '../../../config'; 15import { FRANZ_SERVICE_REQUEST } from '../../../config';
16import LimitReachedInfobox from '../../../features/serviceLimit/components/LimitReachedInfobox';
17import PremiumFeatureContainer from '../../ui/PremiumFeatureContainer';
18import RecipePreview from '../../../models/RecipePreview'; 16import RecipePreview from '../../../models/RecipePreview';
19 17
20const messages = defineMessages({ 18const messages = defineMessages({
@@ -26,10 +24,6 @@ const messages = defineMessages({
26 id: 'settings.searchService', 24 id: 'settings.searchService',
27 defaultMessage: '!!!Search service', 25 defaultMessage: '!!!Search service',
28 }, 26 },
29 mostPopularRecipes: {
30 id: 'settings.recipes.mostPopular',
31 defaultMessage: '!!!Most popular',
32 },
33 allRecipes: { 27 allRecipes: {
34 id: 'settings.recipes.all', 28 id: 'settings.recipes.all',
35 defaultMessage: '!!!All services', 29 defaultMessage: '!!!All services',
@@ -109,7 +103,6 @@ export default @injectSheet(styles) @observer class RecipesDashboard extends Com
109 recipes: MobxPropTypes.arrayOrObservableArray.isRequired, 103 recipes: MobxPropTypes.arrayOrObservableArray.isRequired,
110 customWebsiteRecipe: PropTypes.instanceOf(RecipePreview).isRequired, 104 customWebsiteRecipe: PropTypes.instanceOf(RecipePreview).isRequired,
111 isLoading: PropTypes.bool.isRequired, 105 isLoading: PropTypes.bool.isRequired,
112 hasLoadedRecipes: PropTypes.bool.isRequired,
113 showAddServiceInterface: PropTypes.func.isRequired, 106 showAddServiceInterface: PropTypes.func.isRequired,
114 searchRecipes: PropTypes.func.isRequired, 107 searchRecipes: PropTypes.func.isRequired,
115 resetSearch: PropTypes.func.isRequired, 108 resetSearch: PropTypes.func.isRequired,
@@ -120,7 +113,6 @@ export default @injectSheet(styles) @observer class RecipesDashboard extends Com
120 openRecipeDirectory: PropTypes.func.isRequired, 113 openRecipeDirectory: PropTypes.func.isRequired,
121 openDevDocs: PropTypes.func.isRequired, 114 openDevDocs: PropTypes.func.isRequired,
122 classes: PropTypes.object.isRequired, 115 classes: PropTypes.object.isRequired,
123 isCommunityRecipesIncludedInCurrentPlan: PropTypes.bool.isRequired,
124 }; 116 };
125 117
126 static defaultProps = { 118 static defaultProps = {
@@ -137,7 +129,6 @@ export default @injectSheet(styles) @observer class RecipesDashboard extends Com
137 recipes, 129 recipes,
138 customWebsiteRecipe, 130 customWebsiteRecipe,
139 isLoading, 131 isLoading,
140 hasLoadedRecipes,
141 showAddServiceInterface, 132 showAddServiceInterface,
142 searchRecipes, 133 searchRecipes,
143 resetSearch, 134 resetSearch,
@@ -148,11 +139,9 @@ export default @injectSheet(styles) @observer class RecipesDashboard extends Com
148 openRecipeDirectory, 139 openRecipeDirectory,
149 openDevDocs, 140 openDevDocs,
150 classes, 141 classes,
151 isCommunityRecipesIncludedInCurrentPlan,
152 } = this.props; 142 } = this.props;
153 const { intl } = this.context; 143 const { intl } = this.context;
154 144
155
156 const communityRecipes = recipes.filter(r => !r.isDevRecipe); 145 const communityRecipes = recipes.filter(r => !r.isDevRecipe);
157 const devRecipes = recipes.filter(r => r.isDevRecipe); 146 const devRecipes = recipes.filter(r => r.isDevRecipe);
158 147
@@ -163,7 +152,6 @@ export default @injectSheet(styles) @observer class RecipesDashboard extends Com
163 <div className="settings__header"> 152 <div className="settings__header">
164 <h1>{intl.formatMessage(messages.headline)}</h1> 153 <h1>{intl.formatMessage(messages.headline)}</h1>
165 </div> 154 </div>
166 <LimitReachedInfobox />
167 <div className="settings__body recipes"> 155 <div className="settings__body recipes">
168 {serviceStatus.length > 0 && serviceStatus.includes('created') && ( 156 {serviceStatus.length > 0 && serviceStatus.includes('created') && (
169 <Appear> 157 <Appear>
@@ -190,14 +178,6 @@ export default @injectSheet(styles) @observer class RecipesDashboard extends Com
190 activeClassName={`${!searchNeedle ? 'badge--primary' : ''}`} 178 activeClassName={`${!searchNeedle ? 'badge--primary' : ''}`}
191 onClick={() => resetSearch()} 179 onClick={() => resetSearch()}
192 > 180 >
193 {intl.formatMessage(messages.mostPopularRecipes)}
194 </Link>
195 <Link
196 to="/settings/recipes/all"
197 className="badge"
198 activeClassName={`${!searchNeedle ? 'badge--primary' : ''}`}
199 onClick={() => resetSearch()}
200 >
201 {intl.formatMessage(messages.allRecipes)} 181 {intl.formatMessage(messages.allRecipes)}
202 </Link> 182 </Link>
203 <Link 183 <Link
@@ -208,7 +188,7 @@ export default @injectSheet(styles) @observer class RecipesDashboard extends Com
208 > 188 >
209 {intl.formatMessage(messages.customRecipes)} 189 {intl.formatMessage(messages.customRecipes)}
210 </Link> 190 </Link>
211 <a href={FRANZ_SERVICE_REQUEST} target="_blank" className="link recipes__service-request"> 191 <a href={FRANZ_SERVICE_REQUEST} target="_blank" className="link recipes__service-request" rel="noreferrer">
212 {intl.formatMessage(messages.missingService)} 192 {intl.formatMessage(messages.missingService)}
213 {' '} 193 {' '}
214 <i className="mdi mdi-open-in-new" /> 194 <i className="mdi mdi-open-in-new" />
@@ -223,9 +203,6 @@ export default @injectSheet(styles) @observer class RecipesDashboard extends Com
223 <> 203 <>
224 <H2> 204 <H2>
225 {intl.formatMessage(messages.headlineCustomRecipes)} 205 {intl.formatMessage(messages.headlineCustomRecipes)}
226 {!isCommunityRecipesIncludedInCurrentPlan && (
227 <ProBadge className={classes.proBadge} />
228 )}
229 </H2> 206 </H2>
230 <div className={classes.devRecipeIntroContainer}> 207 <div className={classes.devRecipeIntroContainer}>
231 <p> 208 <p>
@@ -251,37 +228,33 @@ export default @injectSheet(styles) @observer class RecipesDashboard extends Com
251 </div> 228 </div>
252 </> 229 </>
253 )} 230 )}
254 <PremiumFeatureContainer 231 {recipeFilter === 'dev' && communityRecipes.length > 0 && (
255 condition={(recipeFilter === 'dev' && communityRecipes.length > 0) && !isCommunityRecipesIncludedInCurrentPlan} 232 <H3>{intl.formatMessage(messages.headlineCommunityRecipes)}</H3>
256 > 233 )}
257 {recipeFilter === 'dev' && communityRecipes.length > 0 && ( 234 <div className="recipes__list">
258 <H3>{intl.formatMessage(messages.headlineCommunityRecipes)}</H3> 235 {recipes.length === 0 && recipeFilter !== 'dev' && (
259 )} 236 <div className="align-middle settings__empty-state">
260 <div className="recipes__list"> 237 <span className="emoji">
261 {hasLoadedRecipes && recipes.length === 0 && recipeFilter !== 'dev' && ( 238 <img src="./assets/images/emoji/dontknow.png" alt="" />
262 <div className="align-middle settings__empty-state"> 239 </span>
263 <span className="emoji">
264 <img src="./assets/images/emoji/dontknow.png" alt="" />
265 </span>
266 240
267 <p className="settings__empty-state-text">{intl.formatMessage(messages.nothingFound)}</p> 241 <p className="settings__empty-state-text">{intl.formatMessage(messages.nothingFound)}</p>
268 242
269 <RecipeItem
270 key={customWebsiteRecipe.id}
271 recipe={customWebsiteRecipe}
272 onClick={() => isLoggedIn && showAddServiceInterface({ recipeId: customWebsiteRecipe.id })}
273 />
274 </div>
275 )}
276 {communityRecipes.map(recipe => (
277 <RecipeItem 243 <RecipeItem
278 key={recipe.id} 244 key={customWebsiteRecipe.id}
279 recipe={recipe} 245 recipe={customWebsiteRecipe}
280 onClick={() => isLoggedIn && showAddServiceInterface({ recipeId: recipe.id })} 246 onClick={() => isLoggedIn && showAddServiceInterface({ recipeId: customWebsiteRecipe.id })}
281 /> 247 />
282 ))} 248 </div>
283 </div> 249 )}
284 </PremiumFeatureContainer> 250 {communityRecipes.map(recipe => (
251 <RecipeItem
252 key={recipe.id}
253 recipe={recipe}
254 onClick={() => isLoggedIn && showAddServiceInterface({ recipeId: recipe.id })}
255 />
256 ))}
257 </div>
285 {recipeFilter === 'dev' && devRecipes.length > 0 && ( 258 {recipeFilter === 'dev' && devRecipes.length > 0 && (
286 <div className={classes.devRecipeList}> 259 <div className={classes.devRecipeList}>
287 <H3>{intl.formatMessage(messages.headlineDevRecipes)}</H3> 260 <H3>{intl.formatMessage(messages.headlineDevRecipes)}</H3>
diff --git a/src/components/settings/services/EditServiceForm.js b/src/components/settings/services/EditServiceForm.js
index 513c75eed..c41cdd56a 100644
--- a/src/components/settings/services/EditServiceForm.js
+++ b/src/components/settings/services/EditServiceForm.js
@@ -1,4 +1,4 @@
1import React, { Component, Fragment } from 'react'; 1import React, { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import { Link } from 'react-router'; 4import { Link } from 'react-router';
@@ -6,7 +6,6 @@ import { defineMessages, intlShape } from 'react-intl';
6import normalizeUrl from 'normalize-url'; 6import normalizeUrl from 'normalize-url';
7 7
8import Form from '../../../lib/Form'; 8import Form from '../../../lib/Form';
9import User from '../../../models/User';
10import Recipe from '../../../models/Recipe'; 9import Recipe from '../../../models/Recipe';
11import Service from '../../../models/Service'; 10import Service from '../../../models/Service';
12import Tabs, { TabItem } from '../../ui/Tabs'; 11import Tabs, { TabItem } from '../../ui/Tabs';
@@ -17,9 +16,6 @@ import Button from '../../ui/Button';
17import ImageUpload from '../../ui/ImageUpload'; 16import ImageUpload from '../../ui/ImageUpload';
18import Select from '../../ui/Select'; 17import Select from '../../ui/Select';
19 18
20import PremiumFeatureContainer from '../../ui/PremiumFeatureContainer';
21import LimitReachedInfobox from '../../../features/serviceLimit/components/LimitReachedInfobox';
22import { serviceLimitStore } from '../../../features/serviceLimit';
23import { isMac } from '../../../environment'; 19import { isMac } from '../../../environment';
24import globalMessages from '../../../i18n/globalMessages'; 20import globalMessages from '../../../i18n/globalMessages';
25 21
@@ -80,14 +76,6 @@ const messages = defineMessages({
80 id: 'settings.service.form.customUrlValidationError', 76 id: 'settings.service.form.customUrlValidationError',
81 defaultMessage: '!!!Could not validate custom {name} server.', 77 defaultMessage: '!!!Could not validate custom {name} server.',
82 }, 78 },
83 customUrlPremiumInfo: {
84 id: 'settings.service.form.customUrlPremiumInfo',
85 defaultMessage: '!!!To add self hosted services, you need a Ferdi Premium Supporter Account.',
86 },
87 customUrlUpgradeAccount: {
88 id: 'settings.service.form.customUrlUpgradeAccount',
89 defaultMessage: '!!!Upgrade your account',
90 },
91 indirectMessageInfo: { 79 indirectMessageInfo: {
92 id: 'settings.service.form.indirectMessageInfo', 80 id: 'settings.service.form.indirectMessageInfo',
93 defaultMessage: '!!!You will be notified about all new messages in a channel, not just @username, @channel, @here, ...', 81 defaultMessage: '!!!You will be notified about all new messages in a channel, not just @username, @channel, @here, ...',
@@ -149,7 +137,6 @@ export default @observer class EditServiceForm extends Component {
149 137
150 return null; 138 return null;
151 }, 139 },
152 user: PropTypes.instanceOf(User).isRequired,
153 action: PropTypes.string.isRequired, 140 action: PropTypes.string.isRequired,
154 form: PropTypes.instanceOf(Form).isRequired, 141 form: PropTypes.instanceOf(Form).isRequired,
155 onSubmit: PropTypes.func.isRequired, 142 onSubmit: PropTypes.func.isRequired,
@@ -158,9 +145,6 @@ export default @observer class EditServiceForm extends Component {
158 isSaving: PropTypes.bool.isRequired, 145 isSaving: PropTypes.bool.isRequired,
159 isDeleting: PropTypes.bool.isRequired, 146 isDeleting: PropTypes.bool.isRequired,
160 isProxyFeatureEnabled: PropTypes.bool.isRequired, 147 isProxyFeatureEnabled: PropTypes.bool.isRequired,
161 isServiceProxyIncludedInCurrentPlan: PropTypes.bool.isRequired,
162 isSpellcheckerIncludedInCurrentPlan: PropTypes.bool.isRequired,
163 isHibernationFeatureActive: PropTypes.bool.isRequired,
164 }; 148 };
165 149
166 static defaultProps = { 150 static defaultProps = {
@@ -217,16 +201,12 @@ export default @observer class EditServiceForm extends Component {
217 recipe, 201 recipe,
218 service, 202 service,
219 action, 203 action,
220 user,
221 form, 204 form,
222 isSaving, 205 isSaving,
223 isDeleting, 206 isDeleting,
224 onDelete, 207 onDelete,
225 openRecipeFile, 208 openRecipeFile,
226 isProxyFeatureEnabled, 209 isProxyFeatureEnabled,
227 isServiceProxyIncludedInCurrentPlan,
228 isSpellcheckerIncludedInCurrentPlan,
229 isHibernationFeatureActive,
230 } = this.props; 210 } = this.props;
231 const { intl } = this.context; 211 const { intl } = this.context;
232 212
@@ -285,9 +265,8 @@ export default @observer class EditServiceForm extends Component {
285 )} 265 )}
286 </span> 266 </span>
287 </div> 267 </div>
288 <LimitReachedInfobox />
289 <div className="settings__body"> 268 <div className="settings__body">
290 <form onSubmit={e => this.submit(e)} id="form"> 269 <form onSubmit={(e) => this.submit(e)} id="form">
291 <div className="service-name"> 270 <div className="service-name">
292 <Input field={form.$('name')} focus /> 271 <Input field={form.$('name')} focus />
293 </div> 272 </div>
@@ -311,24 +290,11 @@ export default @observer class EditServiceForm extends Component {
311 )} 290 )}
312 {recipe.hasCustomUrl && ( 291 {recipe.hasCustomUrl && (
313 <TabItem title={intl.formatMessage(messages.tabOnPremise)}> 292 <TabItem title={intl.formatMessage(messages.tabOnPremise)}>
314 {user.isPremium || recipe.author.find(a => a.email === user.email) ? ( 293 <Input field={form.$('customUrl')} />
315 <> 294 {form.error === 'url-validation-error' && (
316 <Input field={form.$('customUrl')} /> 295 <p className="franz-form__error">
317 {form.error === 'url-validation-error' && ( 296 {intl.formatMessage(messages.customUrlValidationError, { name: recipe.name })}
318 <p className="franz-form__error"> 297 </p>
319 {intl.formatMessage(messages.customUrlValidationError, { name: recipe.name })}
320 </p>
321 )}
322 </>
323 ) : (
324 <div className="center premium-info">
325 <p>{intl.formatMessage(messages.customUrlPremiumInfo)}</p>
326 <p>
327 <Link to="/settings/user" className="button">
328 {intl.formatMessage(messages.customUrlUpgradeAccount)}
329 </Link>
330 </p>
331 </div>
332 )} 298 )}
333 </TabItem> 299 </TabItem>
334 )} 300 )}
@@ -373,14 +339,10 @@ export default @observer class EditServiceForm extends Component {
373 <div className="settings__settings-group"> 339 <div className="settings__settings-group">
374 <h3>{intl.formatMessage(messages.headlineGeneral)}</h3> 340 <h3>{intl.formatMessage(messages.headlineGeneral)}</h3>
375 <Toggle field={form.$('isEnabled')} /> 341 <Toggle field={form.$('isEnabled')} />
376 {isHibernationFeatureActive && ( 342 <Toggle field={form.$('isHibernationEnabled')} />
377 <> 343 <p className="settings__help indented__help">
378 <Toggle field={form.$('isHibernationEnabled')} /> 344 {intl.formatMessage(messages.isHibernationEnabledInfo)}
379 <p className="settings__help indented__help"> 345 </p>
380 {intl.formatMessage(messages.isHibernationEnabledInfo)}
381 </p>
382 </>
383 )}
384 <Toggle field={form.$('isDarkModeEnabled')} /> 346 <Toggle field={form.$('isDarkModeEnabled')} />
385 {form.$('isDarkModeEnabled').value 347 {form.$('isDarkModeEnabled').value
386 && ( 348 && (
@@ -403,56 +365,46 @@ export default @observer class EditServiceForm extends Component {
403 </div> 365 </div>
404 366
405 {!isMac && ( 367 {!isMac && (
406 <PremiumFeatureContainer 368 <div className="settings__settings-group">
407 condition={!isSpellcheckerIncludedInCurrentPlan} 369 <Select field={form.$('spellcheckerLanguage')} />
408 gaEventInfo={{ category: 'User', event: 'upgrade', label: 'spellchecker' }} 370 </div>
409 >
410 <div className="settings__settings-group">
411 <Select field={form.$('spellcheckerLanguage')} />
412 </div>
413 </PremiumFeatureContainer>
414 )} 371 )}
415 372
416 {isProxyFeatureEnabled && ( 373 {isProxyFeatureEnabled && (
417 <PremiumFeatureContainer 374 <div className="settings__settings-group">
418 condition={!isServiceProxyIncludedInCurrentPlan} 375 <h3>
419 gaEventInfo={{ category: 'User', event: 'upgrade', label: 'proxy' }} 376 {intl.formatMessage(messages.headlineProxy)}
420 > 377 <span className="badge badge--success">beta</span>
421 <div className="settings__settings-group"> 378 </h3>
422 <h3> 379 <Toggle field={form.$('proxy.isEnabled')} />
423 {intl.formatMessage(messages.headlineProxy)} 380 {form.$('proxy.isEnabled').value && (
424 <span className="badge badge--success">beta</span> 381 <>
425 </h3> 382 <div className="grid">
426 <Toggle field={form.$('proxy.isEnabled')} /> 383 <div className="grid__row">
427 {form.$('proxy.isEnabled').value && ( 384 <Input field={form.$('proxy.host')} className="proxyHost" />
428 <> 385 <Input field={form.$('proxy.port')} />
429 <div className="grid">
430 <div className="grid__row">
431 <Input field={form.$('proxy.host')} className="proxyHost" />
432 <Input field={form.$('proxy.port')} />
433 </div>
434 </div> 386 </div>
435 <div className="grid"> 387 </div>
436 <div className="grid__row"> 388 <div className="grid">
437 <Input field={form.$('proxy.user')} /> 389 <div className="grid__row">
438 <Input 390 <Input field={form.$('proxy.user')} />
439 field={form.$('proxy.password')} 391 <Input
440 showPasswordToggle 392 field={form.$('proxy.password')}
441 /> 393 showPasswordToggle
442 </div> 394 />
443 </div> 395 </div>
444 <p> 396 </div>
445 <span className="mdi mdi-information" /> 397 <p>
446 {intl.formatMessage(messages.proxyRestartInfo)} 398 <span className="mdi mdi-information" />
447 </p> 399 {intl.formatMessage(messages.proxyRestartInfo)}
448 <p> 400 </p>
449 <span className="mdi mdi-information" /> 401 <p>
450 {intl.formatMessage(messages.proxyInfo)} 402 <span className="mdi mdi-information" />
451 </p> 403 {intl.formatMessage(messages.proxyInfo)}
452 </> 404 </p>
453 )} 405 </>
454 </div> 406 )}
455 </PremiumFeatureContainer> 407 </div>
456 )} 408 )}
457 409
458 <div className="user-agent"> 410 <div className="user-agent">
@@ -512,7 +464,7 @@ export default @observer class EditServiceForm extends Component {
512 type="submit" 464 type="submit"
513 label={intl.formatMessage(messages.saveService)} 465 label={intl.formatMessage(messages.saveService)}
514 htmlForm="form" 466 htmlForm="form"
515 disabled={action !== 'edit' && ((form.isPristine && requiresUserInput) || serviceLimitStore.userHasReachedServiceLimit)} 467 disabled={action !== 'edit' && (form.isPristine && requiresUserInput)}
516 /> 468 />
517 )} 469 )}
518 </div> 470 </div>
diff --git a/src/components/settings/services/ServicesDashboard.js b/src/components/settings/services/ServicesDashboard.js
index a0f05fd20..11d3eaa79 100644
--- a/src/components/settings/services/ServicesDashboard.js
+++ b/src/components/settings/services/ServicesDashboard.js
@@ -10,7 +10,6 @@ import Loader from '../../ui/Loader';
10import FAB from '../../ui/FAB'; 10import FAB from '../../ui/FAB';
11import ServiceItem from './ServiceItem'; 11import ServiceItem from './ServiceItem';
12import Appear from '../../ui/effects/Appear'; 12import Appear from '../../ui/effects/Appear';
13import LimitReachedInfobox from '../../../features/serviceLimit/components/LimitReachedInfobox';
14 13
15const messages = defineMessages({ 14const messages = defineMessages({
16 headline: { 15 headline: {
@@ -93,12 +92,11 @@ export default @observer class ServicesDashboard extends Component {
93 <div className="settings__header"> 92 <div className="settings__header">
94 <h1>{intl.formatMessage(messages.headline)}</h1> 93 <h1>{intl.formatMessage(messages.headline)}</h1>
95 </div> 94 </div>
96 <LimitReachedInfobox />
97 <div className="settings__body"> 95 <div className="settings__body">
98 {(services.length !== 0 || searchNeedle) && !isLoading && ( 96 {(services.length !== 0 || searchNeedle) && !isLoading && (
99 <SearchInput 97 <SearchInput
100 placeholder={intl.formatMessage(messages.searchService)} 98 placeholder={intl.formatMessage(messages.searchService)}
101 onChange={needle => filterServices({ needle })} 99 onChange={(needle) => filterServices({ needle })}
102 onReset={() => resetFilter()} 100 onReset={() => resetFilter()}
103 autoFocus 101 autoFocus
104 /> 102 />
@@ -165,7 +163,7 @@ export default @observer class ServicesDashboard extends Component {
165 ) : ( 163 ) : (
166 <table className="service-table"> 164 <table className="service-table">
167 <tbody> 165 <tbody>
168 {services.map(service => ( 166 {services.map((service) => (
169 <ServiceItem 167 <ServiceItem
170 key={service.id} 168 key={service.id}
171 service={service} 169 service={service}
diff --git a/src/components/settings/settings/EditSettingsForm.js b/src/components/settings/settings/EditSettingsForm.js
index 52b26d65b..87a4ada27 100644
--- a/src/components/settings/settings/EditSettingsForm.js
+++ b/src/components/settings/settings/EditSettingsForm.js
@@ -1,5 +1,5 @@
1import { app, systemPreferences } from '@electron/remote'; 1import { app, systemPreferences } from '@electron/remote';
2import React, { Component, Fragment } from 'react'; 2import React, { Component } from 'react';
3import PropTypes from 'prop-types'; 3import PropTypes from 'prop-types';
4import { observer } from 'mobx-react'; 4import { observer } from 'mobx-react';
5import prettyBytes from 'pretty-bytes'; 5import prettyBytes from 'pretty-bytes';
@@ -10,14 +10,13 @@ import Button from '../../ui/Button';
10import Toggle from '../../ui/Toggle'; 10import Toggle from '../../ui/Toggle';
11import ToggleRaw from '../../ui/ToggleRaw'; 11import ToggleRaw from '../../ui/ToggleRaw';
12import Select from '../../ui/Select'; 12import Select from '../../ui/Select';
13import PremiumFeatureContainer from '../../ui/PremiumFeatureContainer';
14import Input from '../../ui/Input'; 13import Input from '../../ui/Input';
15 14
16import { 15import {
17 FRANZ_TRANSLATION, 16 FRANZ_TRANSLATION,
18 GITHUB_FRANZ_URL, 17 GITHUB_FRANZ_URL,
19} from '../../../config'; 18} from '../../../config';
20import { DEFAULT_APP_SETTINGS, isMac, isWindows } from '../../../environment'; 19import { DEFAULT_APP_SETTINGS, isMac, isWindows, lockFerdiShortcutKey } from '../../../environment';
21import globalMessages from '../../../i18n/globalMessages'; 20import globalMessages from '../../../i18n/globalMessages';
22 21
23const messages = defineMessages({ 22const messages = defineMessages({
@@ -31,7 +30,7 @@ const messages = defineMessages({
31 }, 30 },
32 sentryInfo: { 31 sentryInfo: {
33 id: 'settings.app.sentryInfo', 32 id: 'settings.app.sentryInfo',
34 defaultMessage: '!!!Sending telemetry data allows us to find errors in Ferdi - we will not send any personal information like your message data! Changing this option requires you to restart Ferdi.', 33 defaultMessage: '!!!Sending telemetry data allows us to find errors in Ferdi - we will not send any personal information like your message data!',
35 }, 34 },
36 hibernateInfo: { 35 hibernateInfo: {
37 id: 'settings.app.hibernateInfo', 36 id: 'settings.app.hibernateInfo',
@@ -55,7 +54,7 @@ const messages = defineMessages({
55 }, 54 },
56 lockInfo: { 55 lockInfo: {
57 id: 'settings.app.lockInfo', 56 id: 'settings.app.lockInfo',
58 defaultMessage: '!!!Password Lock allows you to keep your messages protected.\nUsing Password Lock, you will be prompted to enter your password everytime you start Ferdi or lock Ferdi yourself using the lock symbol in the bottom left corner or the shortcut CMD/CTRL+Shift+L.', 57 defaultMessage: '!!!Password Lock allows you to keep your messages protected.\nUsing Password Lock, you will be prompted to enter your password everytime you start Ferdi or lock Ferdi yourself using the lock symbol in the bottom left corner or the shortcut {lockShortcut}.',
59 }, 58 },
60 scheduledDNDTimeInfo: { 59 scheduledDNDTimeInfo: {
61 id: 'settings.app.scheduledDNDTimeInfo', 60 id: 'settings.app.scheduledDNDTimeInfo',
@@ -168,12 +167,10 @@ export default @observer class EditSettingsForm extends Component {
168 isClearingAllCache: PropTypes.bool.isRequired, 167 isClearingAllCache: PropTypes.bool.isRequired,
169 onClearAllCache: PropTypes.func.isRequired, 168 onClearAllCache: PropTypes.func.isRequired,
170 getCacheSize: PropTypes.func.isRequired, 169 getCacheSize: PropTypes.func.isRequired,
171 isSpellcheckerIncludedInCurrentPlan: PropTypes.bool.isRequired,
172 isTodosEnabled: PropTypes.bool.isRequired, 170 isTodosEnabled: PropTypes.bool.isRequired,
173 isTodosActivated: PropTypes.bool.isRequired, 171 isTodosActivated: PropTypes.bool.isRequired,
174 isWorkspaceEnabled: PropTypes.bool.isRequired, 172 isWorkspaceEnabled: PropTypes.bool.isRequired,
175 automaticUpdates: PropTypes.bool.isRequired, 173 automaticUpdates: PropTypes.bool.isRequired,
176 hibernationEnabled: PropTypes.bool.isRequired,
177 isDarkmodeEnabled: PropTypes.bool.isRequired, 174 isDarkmodeEnabled: PropTypes.bool.isRequired,
178 isAdaptableDarkModeEnabled: PropTypes.bool.isRequired, 175 isAdaptableDarkModeEnabled: PropTypes.bool.isRequired,
179 isNightlyEnabled: PropTypes.bool.isRequired, 176 isNightlyEnabled: PropTypes.bool.isRequired,
@@ -224,11 +221,9 @@ export default @observer class EditSettingsForm extends Component {
224 isClearingAllCache, 221 isClearingAllCache,
225 onClearAllCache, 222 onClearAllCache,
226 getCacheSize, 223 getCacheSize,
227 isSpellcheckerIncludedInCurrentPlan,
228 isTodosEnabled, 224 isTodosEnabled,
229 isWorkspaceEnabled, 225 isWorkspaceEnabled,
230 automaticUpdates, 226 automaticUpdates,
231 hibernationEnabled,
232 isDarkmodeEnabled, 227 isDarkmodeEnabled,
233 isTodosActivated, 228 isTodosActivated,
234 isNightlyEnabled, 229 isNightlyEnabled,
@@ -271,8 +266,8 @@ export default @observer class EditSettingsForm extends Component {
271 </div> 266 </div>
272 <div className="settings__body"> 267 <div className="settings__body">
273 <form 268 <form
274 onSubmit={e => this.submit(e)} 269 onSubmit={(e) => this.submit(e)}
275 onChange={e => this.submit(e)} 270 onChange={(e) => this.submit(e)}
276 id="form" 271 id="form"
277 > 272 >
278 {/* Titles */} 273 {/* Titles */}
@@ -339,13 +334,8 @@ export default @observer class EditSettingsForm extends Component {
339 334
340 <Hr /> 335 <Hr />
341 336
342 <Toggle field={form.$('hibernate')} /> 337 <Select field={form.$('hibernationStrategy')} />
343 {hibernationEnabled && ( 338 <Toggle field={form.$('hibernateOnStartup')} />
344 <>
345 <Select field={form.$('hibernationStrategy')} />
346 <Toggle field={form.$('hibernateOnStartup')} />
347 </>
348 )}
349 <p 339 <p
350 className="settings__message" 340 className="settings__message"
351 style={{ 341 style={{
@@ -357,14 +347,17 @@ export default @observer class EditSettingsForm extends Component {
357 </span> 347 </span>
358 </p> 348 </p>
359 349
350 <Select field={form.$('wakeUpStrategy')} />
351
360 <Hr /> 352 <Hr />
361 353
362 {isWorkspaceEnabled && ( 354 {isWorkspaceEnabled && (
363 <Toggle field={form.$('keepAllWorkspacesLoaded')} /> 355 <>
356 <Toggle field={form.$('keepAllWorkspacesLoaded')} />
357 <Hr />
358 </>
364 )} 359 )}
365 360
366 <Hr />
367
368 {isTodosEnabled && !hasAddedTodosAsService && ( 361 {isTodosEnabled && !hasAddedTodosAsService && (
369 <> 362 <>
370 <Toggle field={form.$('enableTodos')} /> 363 <Toggle field={form.$('enableTodos')} />
@@ -375,7 +368,7 @@ export default @observer class EditSettingsForm extends Component {
375 <div> 368 <div>
376 <Input 369 <Input
377 placeholder="Todo Server" 370 placeholder="Todo Server"
378 onChange={e => this.submit(e)} 371 onChange={(e) => this.submit(e)}
379 field={form.$('customTodoServer')} 372 field={form.$('customTodoServer')}
380 /> 373 />
381 <p 374 <p
@@ -409,7 +402,7 @@ export default @observer class EditSettingsForm extends Component {
409 > 402 >
410 <Input 403 <Input
411 placeholder="17:00" 404 placeholder="17:00"
412 onChange={e => this.submit(e)} 405 onChange={(e) => this.submit(e)}
413 field={form.$('scheduledDNDStart')} 406 field={form.$('scheduledDNDStart')}
414 type="time" 407 type="time"
415 /> 408 />
@@ -421,7 +414,7 @@ export default @observer class EditSettingsForm extends Component {
421 > 414 >
422 <Input 415 <Input
423 placeholder="09:00" 416 placeholder="09:00"
424 onChange={e => this.submit(e)} 417 onChange={(e) => this.submit(e)}
425 field={form.$('scheduledDNDEnd')} 418 field={form.$('scheduledDNDEnd')}
426 type="time" 419 type="time"
427 /> 420 />
@@ -488,7 +481,7 @@ export default @observer class EditSettingsForm extends Component {
488 481
489 <Input 482 <Input
490 placeholder="Accent Color" 483 placeholder="Accent Color"
491 onChange={e => this.submit(e)} 484 onChange={(e) => this.submit(e)}
492 field={form.$('accentColor')} 485 field={form.$('accentColor')}
493 /> 486 />
494 <p> 487 <p>
@@ -509,8 +502,12 @@ export default @observer class EditSettingsForm extends Component {
509 <Hr /> 502 <Hr />
510 503
511 <Select field={form.$('searchEngine')} /> 504 <Select field={form.$('searchEngine')} />
505
506 <Hr />
507
512 <Toggle field={form.$('sentry')} /> 508 <Toggle field={form.$('sentry')} />
513 <p>{intl.formatMessage(messages.sentryInfo)}</p> 509 <p className="settings__help">{intl.formatMessage(messages.sentryInfo)}</p>
510 <p className="settings__help">{intl.formatMessage(messages.appRestartRequired)}</p>
514 511
515 <Hr /> 512 <Hr />
516 513
@@ -523,7 +520,7 @@ export default @observer class EditSettingsForm extends Component {
523 520
524 <Input 521 <Input
525 placeholder={intl.formatMessage(messages.lockedPassword)} 522 placeholder={intl.formatMessage(messages.lockedPassword)}
526 onChange={e => this.submit(e)} 523 onChange={(e) => this.submit(e)}
527 field={form.$('lockedPassword')} 524 field={form.$('lockedPassword')}
528 type="password" 525 type="password"
529 scorePassword 526 scorePassword
@@ -535,7 +532,7 @@ export default @observer class EditSettingsForm extends Component {
535 532
536 <Input 533 <Input
537 placeholder="Lock after inactivity" 534 placeholder="Lock after inactivity"
538 onChange={e => this.submit(e)} 535 onChange={(e) => this.submit(e)}
539 field={form.$('inactivityLock')} 536 field={form.$('inactivityLock')}
540 autoFocus 537 autoFocus
541 /> 538 />
@@ -551,7 +548,7 @@ export default @observer class EditSettingsForm extends Component {
551 }} 548 }}
552 > 549 >
553 <span> 550 <span>
554 { intl.formatMessage(messages.lockInfo) } 551 { intl.formatMessage(messages.lockInfo, { lockShortcut: `${lockFerdiShortcutKey(false)}` }) }
555 </span> 552 </span>
556 </p> 553 </p>
557 </div> 554 </div>
@@ -564,26 +561,24 @@ export default @observer class EditSettingsForm extends Component {
564 561
565 <Hr /> 562 <Hr />
566 563
567 <PremiumFeatureContainer 564 <Toggle
568 condition={!isSpellcheckerIncludedInCurrentPlan} 565 field={form.$('enableSpellchecking')}
569 gaEventInfo={{ category: 'User', event: 'upgrade', label: 'spellchecker' }} 566 />
570 > 567 {!isMac && form.$('enableSpellchecking').value && (
571 <> 568 <Select field={form.$('spellcheckerLanguage')} />
572 <Toggle 569 )}
573 field={form.$('enableSpellchecking')} 570 {isMac && form.$('enableSpellchecking').value && (
574 /> 571 <p className="settings__help">{intl.formatMessage(messages.spellCheckerLanguageInfo)}</p>
575 {!isMac && form.$('enableSpellchecking').value && ( 572 )}
576 <Select field={form.$('spellcheckerLanguage')} /> 573 <p className="settings__help">{intl.formatMessage(messages.appRestartRequired)}</p>
577 )} 574
578 {isMac && form.$('enableSpellchecking').value && ( 575 <Hr />
579 <p>{intl.formatMessage(messages.spellCheckerLanguageInfo)}</p> 576
580 )}
581 </>
582 </PremiumFeatureContainer>
583 <a 577 <a
584 href={FRANZ_TRANSLATION} 578 href={FRANZ_TRANSLATION}
585 target="_blank" 579 target="_blank"
586 className="link" 580 className="link"
581 rel="noreferrer"
587 > 582 >
588 {intl.formatMessage(messages.translationHelp)} 583 {intl.formatMessage(messages.translationHelp)}
589 {' '} 584 {' '}
@@ -602,7 +597,7 @@ export default @observer class EditSettingsForm extends Component {
602 597
603 <Input 598 <Input
604 placeholder="User Agent" 599 placeholder="User Agent"
605 onChange={e => this.submit(e)} 600 onChange={(e) => this.submit(e)}
606 field={form.$('userAgentPref')} 601 field={form.$('userAgentPref')}
607 /> 602 />
608 <p className="settings__help">{intl.formatMessage(globalMessages.userAgentHelp)}</p> 603 <p className="settings__help">{intl.formatMessage(globalMessages.userAgentHelp)}</p>
@@ -688,12 +683,12 @@ export default @observer class EditSettingsForm extends Component {
688 683
689 Ferdi is based on 684 Ferdi is based on
690 {' '} 685 {' '}
691 <a href={`${GITHUB_FRANZ_URL}/franz`} target="_blank">Franz</a> 686 <a href={`${GITHUB_FRANZ_URL}/franz`} target="_blank" rel="noreferrer">Franz</a>
692 687
693 , a project published 688 , a project published
694 under the 689 under the
695 {' '} 690 {' '}
696 <a href={`${GITHUB_FRANZ_URL}/franz/blob/master/LICENSE`} target="_blank">Apache-2.0 License</a> 691 <a href={`${GITHUB_FRANZ_URL}/franz/blob/master/LICENSE`} target="_blank" rel="noreferrer">Apache-2.0 License</a>
697 </span> 692 </span>
698 <br /> 693 <br />
699 <span className="mdi mdi-information" /> 694 <span className="mdi mdi-information" />
diff --git a/src/components/settings/supportFerdi/SupportFerdiDashboard.js b/src/components/settings/supportFerdi/SupportFerdiDashboard.js
index 36c126565..b84e06739 100644
--- a/src/components/settings/supportFerdi/SupportFerdiDashboard.js
+++ b/src/components/settings/supportFerdi/SupportFerdiDashboard.js
@@ -94,10 +94,10 @@ class SupportFerdiDashboard extends Component {
94 <h1>{intl.formatMessage(messages.title)}</h1> 94 <h1>{intl.formatMessage(messages.title)}</h1>
95 <div> 95 <div>
96 <p className="settings__support-badges"> 96 <p className="settings__support-badges">
97 <a href="https://github.com/getferdi/ferdi" target="_blank"><img alt="GitHub Stars" src="https://img.shields.io/github/stars/getferdi/ferdi?style=social" /></a> 97 <a href="https://github.com/getferdi/ferdi" target="_blank" rel="noreferrer"><img alt="GitHub Stars" src="https://img.shields.io/github/stars/getferdi/ferdi?style=social" /></a>
98 <a href="https://twitter.com/getferdi/" target="_blank"><img alt="Twitter Follow" src="https://img.shields.io/twitter/follow/getferdi?label=Follow&style=social" /></a> 98 <a href="https://twitter.com/getferdi/" target="_blank" rel="noreferrer"><img alt="Twitter Follow" src="https://img.shields.io/twitter/follow/getferdi?label=Follow&style=social" /></a>
99 <a href="https://opencollective.com/getferdi#section-contributors" target="_blank"><img alt="Open Collective backers" src="https://img.shields.io/opencollective/backers/getferdi?logo=open-collective" /></a> 99 <a href="https://opencollective.com/getferdi#section-contributors" target="_blank" rel="noreferrer"><img alt="Open Collective backers" src="https://img.shields.io/opencollective/backers/getferdi?logo=open-collective" /></a>
100 <a href="https://opencollective.com/getferdi#section-contributors" target="_blank"><img alt="Open Collective sponsors" src="https://img.shields.io/opencollective/sponsors/getferdi?logo=open-collective" /></a> 100 <a href="https://opencollective.com/getferdi#section-contributors" target="_blank" rel="noreferrer"><img alt="Open Collective sponsors" src="https://img.shields.io/opencollective/sponsors/getferdi?logo=open-collective" /></a>
101 </p> 101 </p>
102 <FormattedHTMLMessage {...messages.aboutIntro} /> 102 <FormattedHTMLMessage {...messages.aboutIntro} />
103 <br /> 103 <br />
@@ -109,7 +109,7 @@ class SupportFerdiDashboard extends Component {
109 </p> 109 </p>
110 <p> 110 <p>
111 {intl.formatMessage(messages.textListContributors)} 111 {intl.formatMessage(messages.textListContributors)}
112 <a href="https://github.com/getferdi/ferdi#contributors-" target="_blank" className="link"> 112 <a href="https://github.com/getferdi/ferdi#contributors-" target="_blank" className="link" rel="noreferrer">
113 {' '} 113 {' '}
114 {intl.formatMessage(messages.textListContributorsHere)} 114 {intl.formatMessage(messages.textListContributorsHere)}
115 <i className="mdi mdi-open-in-new" /> 115 <i className="mdi mdi-open-in-new" />
@@ -122,7 +122,7 @@ class SupportFerdiDashboard extends Component {
122 </p> 122 </p>
123 <p> 123 <p>
124 {intl.formatMessage(messages.textSupportWelcome)} 124 {intl.formatMessage(messages.textSupportWelcome)}
125 <a href="https://help.getferdi.com/general/support" target="_blank" className="link"> 125 <a href="https://help.getferdi.com/general/support" target="_blank" className="link" rel="noreferrer">
126 {' '} 126 {' '}
127 {intl.formatMessage(messages.textSupportWelcomeHere)} 127 {intl.formatMessage(messages.textSupportWelcomeHere)}
128 <i className="mdi mdi-open-in-new" /> 128 <i className="mdi mdi-open-in-new" />
@@ -130,7 +130,7 @@ class SupportFerdiDashboard extends Component {
130 </p> 130 </p>
131 <p> 131 <p>
132 {intl.formatMessage(messages.textExpenses)} 132 {intl.formatMessage(messages.textExpenses)}
133 <a href="https://opencollective.com/getferdi#section-budget" target="_blank" className="link"> 133 <a href="https://opencollective.com/getferdi#section-budget" target="_blank" className="link" rel="noreferrer">
134 {' '} 134 {' '}
135 {intl.formatMessage(messages.textOpenCollective)} 135 {intl.formatMessage(messages.textOpenCollective)}
136 <i className="mdi mdi-open-in-new" /> 136 <i className="mdi mdi-open-in-new" />
@@ -138,14 +138,14 @@ class SupportFerdiDashboard extends Component {
138 </p> 138 </p>
139 <p> 139 <p>
140 {intl.formatMessage(messages.textDonation)} 140 {intl.formatMessage(messages.textDonation)}
141 <a href="https://opencollective.com/getferdi#section-contribute" target="_blank" className="link"> 141 <a href="https://opencollective.com/getferdi#section-contribute" target="_blank" className="link" rel="noreferrer">
142 {' '} 142 {' '}
143 {intl.formatMessage(messages.textOpenCollective)} 143 {intl.formatMessage(messages.textOpenCollective)}
144 <i className="mdi mdi-open-in-new" /> 144 <i className="mdi mdi-open-in-new" />
145 </a> 145 </a>
146 {' '} 146 {' '}
147 {intl.formatMessage(messages.textDonationAnd)} 147 {intl.formatMessage(messages.textDonationAnd)}
148 <a href="https://github.com/sponsors/getferdi" target="_blank" className="link"> 148 <a href="https://github.com/sponsors/getferdi" target="_blank" className="link" rel="noreferrer">
149 {' '} 149 {' '}
150 {intl.formatMessage(messages.textGitHubSponsors)} 150 {intl.formatMessage(messages.textGitHubSponsors)}
151 <i className="mdi mdi-open-in-new" /> 151 <i className="mdi mdi-open-in-new" />
diff --git a/src/components/settings/team/TeamDashboard.js b/src/components/settings/team/TeamDashboard.js
index 602d6e490..437225058 100644
--- a/src/components/settings/team/TeamDashboard.js
+++ b/src/components/settings/team/TeamDashboard.js
@@ -6,12 +6,9 @@ import ReactTooltip from 'react-tooltip';
6import injectSheet from 'react-jss'; 6import injectSheet from 'react-jss';
7import classnames from 'classnames'; 7import classnames from 'classnames';
8 8
9import { Badge } from '@meetfranz/ui';
10import Loader from '../../ui/Loader'; 9import Loader from '../../ui/Loader';
11import Button from '../../ui/Button'; 10import Button from '../../ui/Button';
12import Infobox from '../../ui/Infobox'; 11import Infobox from '../../ui/Infobox';
13import globalMessages from '../../../i18n/globalMessages';
14import UpgradeButton from '../../ui/UpgradeButton';
15import { LIVE_FRANZ_API } from '../../../config'; 12import { LIVE_FRANZ_API } from '../../../config';
16 13
17const messages = defineMessages({ 14const messages = defineMessages({
@@ -35,10 +32,6 @@ const messages = defineMessages({
35 id: 'settings.team.manageAction', 32 id: 'settings.team.manageAction',
36 defaultMessage: '!!!Manage your Team on meetfranz.com', 33 defaultMessage: '!!!Manage your Team on meetfranz.com',
37 }, 34 },
38 upgradeButton: {
39 id: 'settings.team.upgradeAction',
40 defaultMessage: '!!!Upgrade your Account',
41 },
42 teamsUnavailable: { 35 teamsUnavailable: {
43 id: 'settings.team.teamsUnavailable', 36 id: 'settings.team.teamsUnavailable',
44 defaultMessage: '!!!Teams are unavailable', 37 defaultMessage: '!!!Teams are unavailable',
@@ -88,10 +81,6 @@ const styles = {
88 headlineWithSpacing: { 81 headlineWithSpacing: {
89 marginBottom: 'inherit', 82 marginBottom: 'inherit',
90 }, 83 },
91 proRequired: {
92 margin: [10, 0, 40],
93 height: 'auto',
94 },
95 buttonContainer: { 84 buttonContainer: {
96 display: 'flex', 85 display: 'flex',
97 height: 'auto', 86 height: 'auto',
@@ -105,7 +94,6 @@ export default @injectSheet(styles) @observer class TeamDashboard extends Compon
105 retryUserInfoRequest: PropTypes.func.isRequired, 94 retryUserInfoRequest: PropTypes.func.isRequired,
106 openTeamManagement: PropTypes.func.isRequired, 95 openTeamManagement: PropTypes.func.isRequired,
107 classes: PropTypes.object.isRequired, 96 classes: PropTypes.object.isRequired,
108 isProUser: PropTypes.bool.isRequired,
109 server: PropTypes.string.isRequired, 97 server: PropTypes.string.isRequired,
110 }; 98 };
111 99
@@ -119,7 +107,6 @@ export default @injectSheet(styles) @observer class TeamDashboard extends Compon
119 userInfoRequestFailed, 107 userInfoRequestFailed,
120 retryUserInfoRequest, 108 retryUserInfoRequest,
121 openTeamManagement, 109 openTeamManagement,
122 isProUser,
123 classes, 110 classes,
124 server, 111 server,
125 } = this.props; 112 } = this.props;
@@ -157,37 +144,25 @@ export default @injectSheet(styles) @observer class TeamDashboard extends Compon
157 <> 144 <>
158 <h1 className={classnames({ 145 <h1 className={classnames({
159 [classes.headline]: true, 146 [classes.headline]: true,
160 [classes.headlineWithSpacing]: isProUser, 147 [classes.headlineWithSpacing]: true,
161 })} 148 })}
162 > 149 >
163 {intl.formatMessage(messages.contentHeadline)} 150 {intl.formatMessage(messages.contentHeadline)}
164 151
165 </h1> 152 </h1>
166 {!isProUser && (
167 <Badge className={classes.proRequired}>{intl.formatMessage(globalMessages.proRequired)}</Badge>
168 )}
169 <div className={classes.container}> 153 <div className={classes.container}>
170 <div className={classes.content}> 154 <div className={classes.content}>
171 <p>{intl.formatMessage(messages.intro)}</p> 155 <p>{intl.formatMessage(messages.intro)}</p>
172 <p>{intl.formatMessage(messages.copy)}</p> 156 <p>{intl.formatMessage(messages.copy)}</p>
173 </div> 157 </div>
174 <img className={classes.image} src="https://cdn.franzinfra.com/announcements/assets/teams.png" alt="Franz for Teams" /> 158 <img className={classes.image} src="https://cdn.franzinfra.com/announcements/assets/teams.png" alt="Ferdi for Teams" />
175 </div> 159 </div>
176 <div className={classes.buttonContainer}> 160 <div className={classes.buttonContainer}>
177 {!isProUser ? ( 161 <Button
178 <UpgradeButton 162 label={intl.formatMessage(messages.manageButton)}
179 className={classes.cta} 163 onClick={openTeamManagement}
180 gaEventInfo={{ category: 'Todos', event: 'upgrade' }} 164 className={classes.cta}
181 requiresPro 165 />
182 short
183 />
184 ) : (
185 <Button
186 label={intl.formatMessage(messages.manageButton)}
187 onClick={openTeamManagement}
188 className={classes.cta}
189 />
190 )}
191 </div> 166 </div>
192 </> 167 </>
193 </> 168 </>
diff --git a/src/components/settings/user/EditUserForm.js b/src/components/settings/user/EditUserForm.js
index a1a353e57..db78acb69 100644
--- a/src/components/settings/user/EditUserForm.js
+++ b/src/components/settings/user/EditUserForm.js
@@ -84,7 +84,7 @@ export default @observer class EditUserForm extends Component {
84 </span> 84 </span>
85 </div> 85 </div>
86 <div className="settings__body"> 86 <div className="settings__body">
87 <form onSubmit={e => this.submit(e)} id="form"> 87 <form onSubmit={(e) => this.submit(e)} id="form">
88 {status.length > 0 && status.includes('data-updated') && ( 88 {status.length > 0 && status.includes('data-updated') && (
89 <Infobox 89 <Infobox
90 type="success" 90 type="success"
diff --git a/src/components/subscription/SubscriptionForm.js b/src/components/subscription/SubscriptionForm.js
deleted file mode 100644
index ec486e5d0..000000000
--- a/src/components/subscription/SubscriptionForm.js
+++ /dev/null
@@ -1,78 +0,0 @@
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';
6
7import { H3, H2 } from '@meetfranz/ui';
8
9import { Button } from '@meetfranz/forms';
10import { FeatureList } from '../ui/FeatureList';
11
12const messages = defineMessages({
13 submitButtonLabel: {
14 id: 'subscription.cta.choosePlan',
15 defaultMessage: '!!!Choose your plan',
16 },
17 teaserHeadline: {
18 id: 'settings.account.headlineUpgradeAccount',
19 defaultMessage: '!!!Upgrade your account and get the full Franz experience',
20 },
21 teaserText: {
22 id: 'subscription.teaser.intro',
23 defaultMessage: '!!!Franz 5 comes with a wide range of new features to boost up your everyday communication - batteries included. Check out our new plans and find out which one suits you most!',
24 },
25 includedFeatures: {
26 id: 'subscription.teaser.includedFeatures',
27 defaultMessage: '!!!Paid Franz Plans include:',
28 },
29});
30
31const styles = () => ({
32 activateTrialButton: {
33 margin: [40, 'auto', 50],
34 display: 'flex',
35 },
36});
37
38export default @injectSheet(styles) @observer class SubscriptionForm extends Component {
39 static propTypes = {
40 selectPlan: PropTypes.func.isRequired,
41 isActivatingTrial: PropTypes.bool.isRequired,
42 classes: PropTypes.object.isRequired,
43 };
44
45 static contextTypes = {
46 intl: intlShape,
47 };
48
49 render() {
50 const {
51 isActivatingTrial,
52 selectPlan,
53 classes,
54 } = this.props;
55 const { intl } = this.context;
56
57 return (
58 <>
59 <H2>{intl.formatMessage(messages.teaserHeadline)}</H2>
60 <p>{intl.formatMessage(messages.teaserText)}</p>
61 <Button
62 label={intl.formatMessage(messages.submitButtonLabel)}
63 className={classes.activateTrialButton}
64 busy={isActivatingTrial}
65 onClick={selectPlan}
66 />
67 <div className="subscription__premium-info">
68 <H3>
69 {intl.formatMessage(messages.includedFeatures)}
70 </H3>
71 <div className="subscription">
72 <FeatureList />
73 </div>
74 </div>
75 </>
76 );
77 }
78}
diff --git a/src/components/subscription/SubscriptionPopup.js b/src/components/subscription/SubscriptionPopup.js
deleted file mode 100644
index 0df43fd4b..000000000
--- a/src/components/subscription/SubscriptionPopup.js
+++ /dev/null
@@ -1,84 +0,0 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl';
5import Webview from 'react-electron-web-view';
6import ms from 'ms';
7
8import Button from '../ui/Button';
9
10const messages = defineMessages({
11 buttonCancel: {
12 id: 'subscriptionPopup.buttonCancel',
13 defaultMessage: '!!!Cancel',
14 },
15 buttonDone: {
16 id: 'subscriptionPopup.buttonDone',
17 defaultMessage: '!!!Done',
18 },
19});
20
21export default @observer class SubscriptionPopup extends Component {
22 static propTypes = {
23 url: PropTypes.string.isRequired,
24 closeWindow: PropTypes.func.isRequired,
25 completeCheck: PropTypes.func.isRequired,
26 isCompleted: PropTypes.bool.isRequired,
27 };
28
29 static contextTypes = {
30 intl: intlShape,
31 };
32
33 state = {
34 isFakeLoading: false,
35 };
36
37 // We delay the window closing a bit in order to give
38 // the Recurly webhook a few seconds to do it's magic
39 delayedCloseWindow() {
40 this.setState({
41 isFakeLoading: true,
42 });
43
44 setTimeout(() => {
45 this.props.closeWindow();
46 }, ms('1s'));
47 }
48
49 render() {
50 const {
51 url, closeWindow, completeCheck, isCompleted,
52 } = this.props;
53 const { intl } = this.context;
54
55 return (
56 <div className="subscription-popup">
57 <div className="subscription-popup__content">
58 <Webview
59 className="subscription-popup__webview"
60 autosize
61 allowpopups
62 src={encodeURI(url)}
63 onDidNavigate={completeCheck}
64 onDidNavigateInPage={completeCheck}
65 />
66 </div>
67 <div className="subscription-popup__toolbar franz-form">
68 <Button
69 label={intl.formatMessage(messages.buttonCancel)}
70 buttonType="secondary"
71 onClick={closeWindow}
72 disabled={isCompleted}
73 />
74 <Button
75 label={intl.formatMessage(messages.buttonDone)}
76 onClick={() => this.delayedCloseWindow()}
77 disabled={!isCompleted}
78 loaded={!this.state.isFakeLoading}
79 />
80 </div>
81 </div>
82 );
83 }
84}
diff --git a/src/components/subscription/TrialForm.js b/src/components/subscription/TrialForm.js
deleted file mode 100644
index d61b779ed..000000000
--- a/src/components/subscription/TrialForm.js
+++ /dev/null
@@ -1,115 +0,0 @@
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';
6
7import { H3, H2 } from '@meetfranz/ui';
8
9import { Button } from '@meetfranz/forms';
10import { FeatureList } from '../ui/FeatureList';
11import { FeatureItem } from '../ui/FeatureItem';
12
13const messages = defineMessages({
14 submitButtonLabel: {
15 id: 'subscription.cta.activateTrial',
16 defaultMessage: '!!!Yes, start the free Franz Professional trial',
17 },
18 allOptionsButton: {
19 id: 'subscription.cta.allOptions',
20 defaultMessage: '!!!See all options',
21 },
22 teaserHeadline: {
23 id: 'settings.account.headlineTrialUpgrade',
24 defaultMessage: '!!!Get the free 14 day Franz Professional Trial',
25 },
26 includedFeatures: {
27 id: 'subscription.includedProFeatures',
28 defaultMessage: '!!!The Franz Professional Plan includes:',
29 },
30 noStringsAttachedHeadline: {
31 id: 'pricing.trial.terms.headline',
32 defaultMessage: '!!!No strings attached',
33 },
34 noCreditCard: {
35 id: 'pricing.trial.terms.noCreditCard',
36 defaultMessage: '!!!No credit card required',
37 },
38 automaticTrialEnd: {
39 id: 'pricing.trial.terms.automaticTrialEnd',
40 defaultMessage: '!!!Your free trial ends automatically after 14 days',
41 },
42});
43
44const styles = theme => ({
45 activateTrialButton: {
46 margin: [40, 'auto', 10],
47 display: 'flex',
48 },
49 allOptionsButton: {
50 margin: [0, 0, 40],
51 background: 'none',
52 border: 'none',
53 color: theme.colorText,
54 },
55 keyTerms: {
56 marginTop: 20,
57 },
58});
59
60export default @injectSheet(styles) @observer class TrialForm extends Component {
61 static propTypes = {
62 activateTrial: PropTypes.func.isRequired,
63 isActivatingTrial: PropTypes.bool.isRequired,
64 showAllOptions: PropTypes.func.isRequired,
65 classes: PropTypes.object.isRequired,
66 };
67
68 static contextTypes = {
69 intl: intlShape,
70 };
71
72 render() {
73 const {
74 isActivatingTrial,
75 activateTrial,
76 showAllOptions,
77 classes,
78 } = this.props;
79 const { intl } = this.context;
80
81 return (
82 <>
83 <H2>{intl.formatMessage(messages.teaserHeadline)}</H2>
84 <H3 className={classes.keyTerms}>
85 {intl.formatMessage(messages.noStringsAttachedHeadline)}
86 </H3>
87 <ul>
88 <FeatureItem icon="👉" name={intl.formatMessage(messages.noCreditCard)} />
89 <FeatureItem icon="👉" name={intl.formatMessage(messages.automaticTrialEnd)} />
90 </ul>
91
92 <Button
93 label={intl.formatMessage(messages.submitButtonLabel)}
94 className={classes.activateTrialButton}
95 busy={isActivatingTrial}
96 onClick={activateTrial}
97 />
98 <Button
99 label={intl.formatMessage(messages.allOptionsButton)}
100 className={classes.allOptionsButton}
101 onClick={showAllOptions}
102 stretch
103 />
104 <div className="subscription__premium-info">
105 <H3>
106 {intl.formatMessage(messages.includedFeatures)}
107 </H3>
108 <div className="subscription">
109 <FeatureList />
110 </div>
111 </div>
112 </>
113 );
114 }
115}
diff --git a/src/components/ui/ActivateTrialButton/index.js b/src/components/ui/ActivateTrialButton/index.js
deleted file mode 100644
index 8f4d21f64..000000000
--- a/src/components/ui/ActivateTrialButton/index.js
+++ /dev/null
@@ -1,107 +0,0 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { inject, observer } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl';
5import classnames from 'classnames';
6
7import { Button } from '@meetfranz/forms';
8
9import UserStore from '../../../stores/UserStore';
10import UIStore from '../../../stores/UIStore';
11
12const messages = defineMessages({
13 action: {
14 id: 'feature.delayApp.upgrade.action',
15 defaultMessage: '!!!Get a Franz Supporter License',
16 },
17 actionTrial: {
18 id: 'feature.delayApp.trial.action',
19 defaultMessage: '!!!Yes, I want the free 14 day trial of Franz Professional',
20 },
21 shortAction: {
22 id: 'feature.delayApp.upgrade.actionShort',
23 defaultMessage: '!!!Upgrade account',
24 },
25 shortActionTrial: {
26 id: 'feature.delayApp.trial.actionShort',
27 defaultMessage: '!!!Activate the free Franz Professional trial',
28 },
29 noStringsAttachedHeadline: {
30 id: 'pricing.trial.terms.headline',
31 defaultMessage: '!!!No strings attached',
32 },
33 noCreditCard: {
34 id: 'pricing.trial.terms.noCreditCard',
35 defaultMessage: '!!!No credit card required',
36 },
37 automaticTrialEnd: {
38 id: 'pricing.trial.terms.automaticTrialEnd',
39 defaultMessage: '!!!Your free trial ends automatically after 14 days',
40 },
41});
42
43@inject('stores', 'actions') @observer
44class ActivateTrialButton extends Component {
45 static propTypes = {
46 className: PropTypes.string,
47 short: PropTypes.bool,
48 gaEventInfo: PropTypes.shape({
49 category: PropTypes.string.isRequired,
50 event: PropTypes.string.isRequired,
51 label: PropTypes.string,
52 }),
53 };
54
55 static defaultProps = {
56 className: '',
57 short: false,
58 gaEventInfo: null,
59 }
60
61 static contextTypes = {
62 intl: intlShape,
63 };
64
65 handleCTAClick() {
66 const { actions } = this.props;
67
68 actions.ui.openSettings({ path: 'user' });
69 }
70
71 render() {
72 const { stores, className, short } = this.props;
73 const { intl } = this.context;
74
75 const { hadSubscription } = stores.user.data;
76
77 let label;
78 if (hadSubscription) {
79 label = short ? messages.shortAction : messages.action;
80 } else {
81 label = short ? messages.shortActionTrial : messages.actionTrial;
82 }
83
84 return (
85 <Button
86 label={intl.formatMessage(label)}
87 className={classnames({
88 [className]: className,
89 })}
90 buttonType="inverted"
91 onClick={this.handleCTAClick.bind(this)}
92 busy={stores.user.activateTrialRequest.isExecuting}
93 />
94 );
95 }
96}
97
98export default ActivateTrialButton;
99
100ActivateTrialButton.wrappedComponent.propTypes = {
101 stores: PropTypes.shape({
102 user: PropTypes.instanceOf(UserStore).isRequired,
103 }).isRequired,
104 actions: PropTypes.shape({
105 ui: PropTypes.instanceOf(UIStore).isRequired,
106 }).isRequired,
107};
diff --git a/src/components/ui/AppLoader/index.js b/src/components/ui/AppLoader/index.js
index a7f6f4545..31d7ca249 100644
--- a/src/components/ui/AppLoader/index.js
+++ b/src/components/ui/AppLoader/index.js
@@ -37,7 +37,7 @@ export default @injectSheet(styles) @withTheme class AppLoader extends Component
37 37
38 componentDidMount() { 38 componentDidMount() {
39 this.interval = setInterval(() => { 39 this.interval = setInterval(() => {
40 this.setState(prevState => ({ 40 this.setState((prevState) => ({
41 step: prevState.step === textList.length - 1 ? 0 : prevState.step + 1, 41 step: prevState.step === textList.length - 1 ? 0 : prevState.step + 1,
42 })); 42 }));
43 }, 2500); 43 }, 2500);
diff --git a/src/components/ui/AppLoader/styles.js b/src/components/ui/AppLoader/styles.js
index 755a56b40..9891e0387 100644
--- a/src/components/ui/AppLoader/styles.js
+++ b/src/components/ui/AppLoader/styles.js
@@ -1,3 +1,9 @@
1let sloganTransition = 'none';
2
3if (window && window.matchMedia('(prefers-reduced-motion: no-preference)')) {
4 sloganTransition = 'opacity 1s ease';
5}
6
1export default { 7export default {
2 component: { 8 component: {
3 color: '#FFF', 9 color: '#FFF',
@@ -5,7 +11,7 @@ export default {
5 slogan: { 11 slogan: {
6 display: 'block', 12 display: 'block',
7 opacity: 0, 13 opacity: 0,
8 transition: 'opacity 1s ease', 14 transition: sloganTransition,
9 position: 'absolute', 15 position: 'absolute',
10 textAlign: 'center', 16 textAlign: 'center',
11 width: '100%', 17 width: '100%',
diff --git a/src/components/ui/FeatureItem.js b/src/components/ui/FeatureItem.js
index 4926df470..646cf56ca 100644
--- a/src/components/ui/FeatureItem.js
+++ b/src/components/ui/FeatureItem.js
@@ -4,7 +4,7 @@ import { Icon } from '@meetfranz/ui';
4import classnames from 'classnames'; 4import classnames from 'classnames';
5import { mdiCheckCircle } from '@mdi/js'; 5import { mdiCheckCircle } from '@mdi/js';
6 6
7const styles = theme => ({ 7const styles = (theme) => ({
8 featureItem: { 8 featureItem: {
9 borderBottom: [1, 'solid', theme.defaultContentBorder], 9 borderBottom: [1, 'solid', theme.defaultContentBorder],
10 padding: [8, 0], 10 padding: [8, 0],
diff --git a/src/components/ui/FeatureList.js b/src/components/ui/FeatureList.js
index 72c799819..cf2664830 100644
--- a/src/components/ui/FeatureList.js
+++ b/src/components/ui/FeatureList.js
@@ -3,12 +3,11 @@ import PropTypes from 'prop-types';
3import { defineMessages, intlShape } from 'react-intl'; 3import { defineMessages, intlShape } from 'react-intl';
4 4
5import { FeatureItem } from './FeatureItem'; 5import { FeatureItem } from './FeatureItem';
6import { PLANS } from '../../config';
7 6
8const messages = defineMessages({ 7const messages = defineMessages({
9 availableRecipes: { 8 availableRecipes: {
10 id: 'pricing.features.recipes', 9 id: 'pricing.features.recipes',
11 defaultMessage: '!!!Choose from more than 70 Services', 10 defaultMessage: '!!!Choose from more than 70 Services', // TODO: Make this dynamic
12 }, 11 },
13 accountSync: { 12 accountSync: {
14 id: 'pricing.features.accountSync', 13 id: 'pricing.features.accountSync',
@@ -22,14 +21,6 @@ const messages = defineMessages({
22 id: 'pricing.features.unlimitedServices', 21 id: 'pricing.features.unlimitedServices',
23 defaultMessage: '!!!Add unlimited services', 22 defaultMessage: '!!!Add unlimited services',
24 }, 23 },
25 upToThreeServices: {
26 id: 'pricing.features.upToThreeServices',
27 defaultMessage: '!!!Add up to 3 services',
28 },
29 upToSixServices: {
30 id: 'pricing.features.upToSixServices',
31 defaultMessage: '!!!Add up to 6 services',
32 },
33 spellchecker: { 24 spellchecker: {
34 id: 'pricing.features.spellchecker', 25 id: 'pricing.features.spellchecker',
35 defaultMessage: '!!!Spellchecker support', 26 defaultMessage: '!!!Spellchecker support',
@@ -58,31 +49,17 @@ const messages = defineMessages({
58 id: 'pricing.features.teamManagement', 49 id: 'pricing.features.teamManagement',
59 defaultMessage: '!!!Team Management', 50 defaultMessage: '!!!Team Management',
60 }, 51 },
61 appDelays: {
62 id: 'pricing.features.appDelays',
63 defaultMessage: '!!!No Waiting Screens',
64 },
65 adFree: {
66 id: 'pricing.features.adFree',
67 defaultMessage: '!!!Forever ad-free',
68 },
69 appDelayEnabled: {
70 id: 'pricing.features.appDelaysEnabled',
71 defaultMessage: '!!!Occasional Waiting Screens',
72 },
73}); 52});
74 53
75export class FeatureList extends Component { 54export class FeatureList extends Component {
76 static propTypes = { 55 static propTypes = {
77 className: PropTypes.string, 56 className: PropTypes.string,
78 featureClassName: PropTypes.string, 57 featureClassName: PropTypes.string,
79 plan: PropTypes.oneOf(Object.keys(PLANS)),
80 }; 58 };
81 59
82 static defaultProps = { 60 static defaultProps = {
83 className: '', 61 className: '',
84 featureClassName: '', 62 featureClassName: '',
85 plan: false,
86 } 63 }
87 64
88 static contextTypes = { 65 static contextTypes = {
@@ -93,53 +70,29 @@ export class FeatureList extends Component {
93 const { 70 const {
94 className, 71 className,
95 featureClassName, 72 featureClassName,
96 plan,
97 } = this.props; 73 } = this.props;
98 const { intl } = this.context; 74 const { intl } = this.context;
99 75
100 const features = []; 76 const features = [
101 if (plan === PLANS.FREE) { 77 messages.availableRecipes,
102 features.push( 78 messages.accountSync,
103 messages.appDelayEnabled, 79 messages.desktopNotifications,
104 messages.upToThreeServices, 80
105 messages.availableRecipes, 81 messages.spellchecker,
106 messages.accountSync, 82
107 messages.desktopNotifications, 83 messages.workspaces,
108 ); 84 messages.customWebsites,
109 } else if (plan === PLANS.PERSONAL) { 85 messages.thirdPartyServices,
110 features.push( 86
111 messages.upToSixServices, 87 messages.unlimitedServices,
112 messages.spellchecker, 88 messages.onPremise,
113 messages.appDelays, 89 messages.serviceProxies,
114 messages.adFree, 90 messages.teamManagement,
115 ); 91 ];
116 } else if (plan === PLANS.PRO) {
117 features.push(
118 messages.unlimitedServices,
119 messages.workspaces,
120 messages.customWebsites,
121 // messages.onPremise,
122 messages.thirdPartyServices,
123 // messages.serviceProxies,
124 );
125 } else {
126 features.push(
127 messages.unlimitedServices,
128 messages.spellchecker,
129 messages.workspaces,
130 messages.customWebsites,
131 messages.onPremise,
132 messages.thirdPartyServices,
133 messages.serviceProxies,
134 messages.teamManagement,
135 messages.appDelays,
136 messages.adFree,
137 );
138 }
139 92
140 return ( 93 return (
141 <ul className={className}> 94 <ul className={className}>
142 {features.map(feature => <FeatureItem name={intl.formatMessage(feature)} className={featureClassName} />)} 95 {features.map((feature) => <FeatureItem name={intl.formatMessage(feature)} className={featureClassName} />)}
143 </ul> 96 </ul>
144 ); 97 );
145 } 98 }
diff --git a/src/components/ui/ImageUpload.js b/src/components/ui/ImageUpload.js
index 059610aec..bb4ea0565 100644
--- a/src/components/ui/ImageUpload.js
+++ b/src/components/ui/ImageUpload.js
@@ -26,16 +26,13 @@ export default @observer class ImageUpload extends Component {
26 26
27 dropzoneRef = null; 27 dropzoneRef = null;
28 28
29 imgPath = null;
30
31 onDrop(acceptedFiles) { 29 onDrop(acceptedFiles) {
32 const { field } = this.props; 30 const { field } = this.props;
33 31
34 acceptedFiles.forEach((file) => { 32 acceptedFiles.forEach((file) => {
35 this.imgPath = isWindows ? file.path.replace(/\\/g, '/') : file.path; 33 const imgPath = isWindows ? file.path.replace(/\\/g, '/') : file.path;
36
37 this.setState({ 34 this.setState({
38 path: this.imgPath, 35 path: imgPath,
39 }); 36 });
40 37
41 this.props.field.onDrop(file); 38 this.props.field.onDrop(file);
diff --git a/src/components/ui/Link.js b/src/components/ui/Link.js
index 7930d98b4..fd14b7018 100644
--- a/src/components/ui/Link.js
+++ b/src/components/ui/Link.js
@@ -48,7 +48,7 @@ export default @inject('stores') @observer class Link extends Component {
48 href={router.history.createHref(to)} 48 href={router.history.createHref(to)}
49 className={linkClasses} 49 className={linkClasses}
50 style={style} 50 style={style}
51 onClick={e => this.onClick(e)} 51 onClick={(e) => this.onClick(e)}
52 > 52 >
53 {children} 53 {children}
54 </a> 54 </a>
diff --git a/src/components/ui/Modal/styles.js b/src/components/ui/Modal/styles.js
index c2bebf9bb..f32c075ce 100644
--- a/src/components/ui/Modal/styles.js
+++ b/src/components/ui/Modal/styles.js
@@ -1,4 +1,4 @@
1export default theme => ({ 1export default (theme) => ({
2 component: { 2 component: {
3 zIndex: 500, 3 zIndex: 500,
4 position: 'absolute', 4 position: 'absolute',
diff --git a/src/components/ui/PremiumFeatureContainer/index.js b/src/components/ui/PremiumFeatureContainer/index.js
deleted file mode 100644
index 1e100f9d8..000000000
--- a/src/components/ui/PremiumFeatureContainer/index.js
+++ /dev/null
@@ -1,101 +0,0 @@
1import React, { Component } from 'react';
2import { inject, observer } from 'mobx-react';
3import PropTypes from 'prop-types';
4import { defineMessages, intlShape } from 'react-intl';
5import injectSheet from 'react-jss';
6
7import { oneOrManyChildElements } from '../../../prop-types';
8
9import UserStore from '../../../stores/UserStore';
10
11import styles from './styles';
12import FeaturesStore from '../../../stores/FeaturesStore';
13import UIStore from '../../../stores/UIStore';
14
15const messages = defineMessages({
16 action: {
17 id: 'premiumFeature.button.upgradeAccount',
18 defaultMessage: '!!!Upgrade account',
19 },
20});
21
22@inject('stores', 'actions') @injectSheet(styles) @observer
23class PremiumFeatureContainer extends Component {
24 static propTypes = {
25 classes: PropTypes.object.isRequired,
26 condition: PropTypes.oneOfType([
27 PropTypes.bool,
28 PropTypes.func,
29 ]),
30 gaEventInfo: PropTypes.shape({
31 category: PropTypes.string.isRequired,
32 event: PropTypes.string.isRequired,
33 label: PropTypes.string,
34 }),
35 };
36
37 static defaultProps = {
38 condition: null,
39 gaEventInfo: null,
40 };
41
42 static contextTypes = {
43 intl: intlShape,
44 };
45
46 render() {
47 const {
48 classes,
49 children,
50 actions,
51 condition,
52 stores,
53 } = this.props;
54
55 const { intl } = this.context;
56
57 let showWrapper = !!condition;
58
59 if (condition === null) {
60 showWrapper = !stores.user.data.isPremium;
61 } else if (typeof condition === 'function') {
62 showWrapper = condition({
63 isPremium: stores.user.data.isPremium,
64 features: stores.features.features,
65 });
66 }
67
68 return showWrapper ? (
69 <div className={classes.container}>
70 <div className={classes.titleContainer}>
71 <p className={classes.title}>Premium Feature</p>
72 <button
73 className={classes.actionButton}
74 type="button"
75 onClick={() => {
76 actions.ui.openSettings({ path: 'user' });
77 }}
78 >
79 {intl.formatMessage(messages.action)}
80 </button>
81 </div>
82 <div className={classes.content}>
83 {children}
84 </div>
85 </div>
86 ) : children;
87 }
88}
89
90PremiumFeatureContainer.wrappedComponent.propTypes = {
91 children: oneOrManyChildElements.isRequired,
92 stores: PropTypes.shape({
93 user: PropTypes.instanceOf(UserStore).isRequired,
94 features: PropTypes.instanceOf(FeaturesStore).isRequired,
95 }).isRequired,
96 actions: PropTypes.shape({
97 ui: PropTypes.instanceOf(UIStore).isRequired,
98 }).isRequired,
99};
100
101export default PremiumFeatureContainer;
diff --git a/src/components/ui/PremiumFeatureContainer/styles.js b/src/components/ui/PremiumFeatureContainer/styles.js
deleted file mode 100644
index 41881e044..000000000
--- a/src/components/ui/PremiumFeatureContainer/styles.js
+++ /dev/null
@@ -1,34 +0,0 @@
1export default theme => ({
2 container: {
3 background: theme.colorSubscriptionContainerBackground,
4 border: theme.colorSubscriptionContainerBorder,
5 margin: [0, 0, 20, -20],
6 padding: 20,
7 'border-radius': theme.borderRadius,
8 pointerEvents: 'none',
9 height: 'auto',
10 },
11 titleContainer: {
12 display: 'flex',
13 },
14 title: {
15 'font-weight': 'bold',
16 color: theme.colorSubscriptionContainerTitle,
17 },
18 actionButton: {
19 background: theme.colorSubscriptionContainerActionButtonBackground,
20 color: theme.colorSubscriptionContainerActionButtonColor,
21 'margin-left': 'auto',
22 'border-radius': theme.borderRadiusSmall,
23 padding: [4, 8],
24 'font-size': 12,
25 pointerEvents: 'initial',
26 },
27 content: {
28 opacity: 0.5,
29 'margin-top': 20,
30 '& > :last-child': {
31 'margin-bottom': 0,
32 },
33 },
34});
diff --git a/src/components/ui/Radio.js b/src/components/ui/Radio.js
index ba13aca63..e77714eb7 100644
--- a/src/components/ui/Radio.js
+++ b/src/components/ui/Radio.js
@@ -54,7 +54,7 @@ export default @observer class Radio extends Component {
54 </label> 54 </label>
55 )} 55 )}
56 <div className="franz-form__radio-wrapper"> 56 <div className="franz-form__radio-wrapper">
57 {field.options.map(type => ( 57 {field.options.map((type) => (
58 <label 58 <label
59 key={type.value} 59 key={type.value}
60 htmlFor={`${field.id}-${type.value}`} 60 htmlFor={`${field.id}-${type.value}`}
diff --git a/src/components/ui/SearchInput.js b/src/components/ui/SearchInput.js
index 78d6aae8b..0b25734dd 100644
--- a/src/components/ui/SearchInput.js
+++ b/src/components/ui/SearchInput.js
@@ -96,7 +96,7 @@ export default @observer class SearchInput extends Component {
96 type="text" 96 type="text"
97 placeholder={placeholder} 97 placeholder={placeholder}
98 value={value} 98 value={value}
99 onChange={e => this.onChange(e)} 99 onChange={(e) => this.onChange(e)}
100 ref={(ref) => { this.input = ref; }} 100 ref={(ref) => { this.input = ref; }}
101 /> 101 />
102 </label> 102 </label>
diff --git a/src/components/ui/Select.js b/src/components/ui/Select.js
index b4511433c..e7a5eafa8 100644
--- a/src/components/ui/Select.js
+++ b/src/components/ui/Select.js
@@ -82,7 +82,7 @@ export default @observer class Select extends Component {
82 </label> 82 </label>
83 )} 83 )}
84 <select 84 <select
85 onChange={multiple ? e => this.multipleChange(e) : field.onChange} 85 onChange={multiple ? (e) => this.multipleChange(e) : field.onChange}
86 id={field.id} 86 id={field.id}
87 defaultValue={selected} 87 defaultValue={selected}
88 className="franz-form__select" 88 className="franz-form__select"
@@ -90,7 +90,7 @@ export default @observer class Select extends Component {
90 multiple={multiple} 90 multiple={multiple}
91 ref={this.element} 91 ref={this.element}
92 > 92 >
93 {field.options.map(type => ( 93 {field.options.map((type) => (
94 <option 94 <option
95 key={type.value} 95 key={type.value}
96 value={type.value} 96 value={type.value}
diff --git a/src/components/ui/ServiceIcon.js b/src/components/ui/ServiceIcon.js
index 0b9155a4e..b2dadeac3 100644
--- a/src/components/ui/ServiceIcon.js
+++ b/src/components/ui/ServiceIcon.js
@@ -6,7 +6,7 @@ import classnames from 'classnames';
6 6
7import ServiceModel from '../../models/Service'; 7import ServiceModel from '../../models/Service';
8 8
9const styles = theme => ({ 9const styles = (theme) => ({
10 root: { 10 root: {
11 height: 'auto', 11 height: 'auto',
12 }, 12 },
diff --git a/src/components/ui/Slider.js b/src/components/ui/Slider.js
index b00a6a3f8..f344449a0 100644
--- a/src/components/ui/Slider.js
+++ b/src/components/ui/Slider.js
@@ -53,7 +53,7 @@ export default @observer class Slider extends Component {
53 value={field.value} 53 value={field.value}
54 min="1" 54 min="1"
55 max="100" 55 max="100"
56 onChange={e => (!disabled ? this.onChange(e) : null)} 56 onChange={(e) => (!disabled ? this.onChange(e) : null)}
57 /> 57 />
58 </div> 58 </div>
59 59
diff --git a/src/components/ui/Tabs/TabItem.js b/src/components/ui/Tabs/TabItem.js
index 546b05a4e..d0ef4e798 100644
--- a/src/components/ui/Tabs/TabItem.js
+++ b/src/components/ui/Tabs/TabItem.js
@@ -1,17 +1,15 @@
1import React, { Component, Fragment } from 'react'; 1import React, { Component } from 'react';
2 2
3import { oneOrManyChildElements } from '../../../prop-types'; 3import { oneOrManyChildElements } from '../../../prop-types';
4 4
5export default class TabItem extends Component { 5export default class TabItem extends Component {
6 static propTypes = { 6 static propTypes = {
7 children: oneOrManyChildElements.isRequired, 7 children: oneOrManyChildElements.isRequired,
8 } 8 };
9 9
10 render() { 10 render() {
11 const { children } = this.props; 11 const { children } = this.props;
12 12
13 return ( 13 return <>{children}</>;
14 <>{children}</>
15 );
16 } 14 }
17} 15}
diff --git a/src/components/ui/Tabs/Tabs.js b/src/components/ui/Tabs/Tabs.js
index e68dc3a1d..56c76f215 100644
--- a/src/components/ui/Tabs/Tabs.js
+++ b/src/components/ui/Tabs/Tabs.js
@@ -28,7 +28,7 @@ class Tab extends Component {
28 28
29 render() { 29 render() {
30 const { children: childElements } = this.props; 30 const { children: childElements } = this.props;
31 const children = childElements.filter(c => !!c); 31 const children = childElements.filter((c) => !!c);
32 32
33 if (children.length === 1) { 33 if (children.length === 1) {
34 return <div>{children}</div>; 34 return <div>{children}</div>;
diff --git a/src/components/ui/Toggle.js b/src/components/ui/Toggle.js
index 78fb77cbe..14330e5c7 100644
--- a/src/components/ui/Toggle.js
+++ b/src/components/ui/Toggle.js
@@ -59,7 +59,7 @@ export default @observer class Toggle extends Component {
59 name={field.name} 59 name={field.name}
60 value={field.name} 60 value={field.name}
61 checked={field.value} 61 checked={field.value}
62 onChange={e => (!disabled ? this.onChange(e) : null)} 62 onChange={(e) => (!disabled ? this.onChange(e) : null)}
63 /> 63 />
64 </label> 64 </label>
65 {field.error && <div className={field.error}>{field.error}</div>} 65 {field.error && <div className={field.error}>{field.error}</div>}
diff --git a/src/components/ui/ToggleRaw.js b/src/components/ui/ToggleRaw.js
index dcaf664e1..4700127d4 100644
--- a/src/components/ui/ToggleRaw.js
+++ b/src/components/ui/ToggleRaw.js
@@ -64,7 +64,7 @@ export default @observer class ToggleRaw extends Component {
64 name={field.name} 64 name={field.name}
65 value={field.name} 65 value={field.name}
66 checked={field.value} 66 checked={field.value}
67 onChange={e => (!disabled ? this.onChange(e) : null)} 67 onChange={(e) => (!disabled ? this.onChange(e) : null)}
68 /> 68 />
69 </label> 69 </label>
70 {field.error && <div className={field.error}>{field.error}</div>} 70 {field.error && <div className={field.error}>{field.error}</div>}
diff --git a/src/components/ui/UpgradeButton/index.js b/src/components/ui/UpgradeButton/index.js
deleted file mode 100644
index eade46cfd..000000000
--- a/src/components/ui/UpgradeButton/index.js
+++ /dev/null
@@ -1,83 +0,0 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { inject, observer } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl';
5
6import { Button } from '@meetfranz/forms';
7
8import UserStore from '../../../stores/UserStore';
9import ActivateTrialButton from '../ActivateTrialButton';
10import UIStore from '../../../stores/UIStore';
11
12const messages = defineMessages({
13 upgradeToPro: {
14 id: 'global.upgradeButton.upgradeToPro',
15 defaultMessage: '!!!Upgrade to Franz Professional',
16 },
17});
18
19@inject('stores', 'actions') @observer
20class UpgradeButton extends Component {
21 static propTypes = {
22 // eslint-disable-next-line
23 classes: PropTypes.object.isRequired,
24 className: PropTypes.string,
25 gaEventInfo: PropTypes.shape({
26 category: PropTypes.string.isRequired,
27 event: PropTypes.string.isRequired,
28 label: PropTypes.string,
29 }),
30 requiresPro: PropTypes.bool,
31 };
32
33 static defaultProps = {
34 className: '',
35 gaEventInfo: null,
36 requiresPro: false,
37 }
38
39 static contextTypes = {
40 intl: intlShape,
41 };
42
43 handleCTAClick() {
44 const { actions } = this.props;
45
46 actions.ui.openSettings({ path: 'user' });
47 }
48
49 render() {
50 const { stores, requiresPro } = this.props;
51 const { intl } = this.context;
52
53 const { isPremium, isPersonal } = stores.user;
54
55 if (isPremium && isPersonal && requiresPro) {
56 return (
57 <Button
58 label={intl.formatMessage(messages.upgradeToPro)}
59 onClick={this.handleCTAClick.bind(this)}
60 className={this.props.className}
61 buttonType="inverted"
62 />
63 );
64 }
65
66 if (!isPremium) {
67 return <ActivateTrialButton {...this.props} />;
68 }
69
70 return null;
71 }
72}
73
74export default UpgradeButton;
75
76UpgradeButton.wrappedComponent.propTypes = {
77 stores: PropTypes.shape({
78 user: PropTypes.instanceOf(UserStore).isRequired,
79 }).isRequired,
80 actions: PropTypes.shape({
81 ui: PropTypes.instanceOf(UIStore).isRequired,
82 }).isRequired,
83};
diff --git a/src/components/ui/WebviewLoader/styles.js b/src/components/ui/WebviewLoader/styles.js
index dbd75db8a..5d58011fe 100644
--- a/src/components/ui/WebviewLoader/styles.js
+++ b/src/components/ui/WebviewLoader/styles.js
@@ -1,4 +1,4 @@
1export default theme => ({ 1export default (theme) => ({
2 component: { 2 component: {
3 background: theme.colorWebviewLoaderBackground, 3 background: theme.colorWebviewLoaderBackground,
4 padding: 20, 4 padding: 20,
diff --git a/src/components/util/ErrorBoundary/styles.js b/src/components/util/ErrorBoundary/styles.js
index 0960546ff..51b36fdf3 100644
--- a/src/components/util/ErrorBoundary/styles.js
+++ b/src/components/util/ErrorBoundary/styles.js
@@ -1,4 +1,4 @@
1export default theme => ({ 1export default (theme) => ({
2 component: { 2 component: {
3 display: 'flex', 3 display: 'flex',
4 width: '100%', 4 width: '100%',