aboutsummaryrefslogtreecommitdiffstats
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/AppUpdateInfoBar.js2
-rw-r--r--src/components/auth/AuthLayout.js4
-rw-r--r--src/components/auth/Import.js4
-rw-r--r--src/components/auth/Locked.js115
-rw-r--r--src/components/auth/Login.js46
-rw-r--r--src/components/auth/Pricing.js4
-rw-r--r--src/components/auth/Signup.js29
-rw-r--r--src/components/auth/Welcome.js36
-rw-r--r--src/components/layout/AppLayout.js26
-rw-r--r--src/components/layout/Sidebar.js151
-rw-r--r--src/components/services/content/ServiceView.js104
-rw-r--r--src/components/services/content/ServiceWebview.js20
-rw-r--r--src/components/services/content/Services.js55
-rw-r--r--src/components/services/tabs/TabItem.js5
-rw-r--r--src/components/settings/account/AccountDashboard.js2
-rw-r--r--src/components/settings/navigation/SettingsNavigation.js63
-rw-r--r--src/components/settings/services/EditServiceForm.js57
-rw-r--r--src/components/settings/settings/EditSettingsForm.js260
-rw-r--r--src/components/settings/supportFerdi/SupportFerdiDashboard.js73
-rw-r--r--src/components/settings/team/TeamDashboard.js153
-rw-r--r--src/components/subscription/SubscriptionForm.js2
-rw-r--r--src/components/subscription/TrialForm.js2
-rw-r--r--src/components/ui/ActivateTrialButton/index.js19
-rw-r--r--src/components/ui/AppLoader/index.js26
-rw-r--r--src/components/ui/Button.js15
-rw-r--r--src/components/ui/FullscreenLoader/index.js19
-rw-r--r--src/components/ui/Input.js2
-rw-r--r--src/components/ui/Link.js4
-rw-r--r--src/components/ui/Loader.js15
-rw-r--r--src/components/ui/Modal/index.js3
-rw-r--r--src/components/ui/PremiumFeatureContainer/index.js8
-rw-r--r--src/components/ui/UpgradeButton/index.js7
-rw-r--r--src/components/ui/WebviewLoader/index.js2
33 files changed, 1089 insertions, 244 deletions
diff --git a/src/components/AppUpdateInfoBar.js b/src/components/AppUpdateInfoBar.js
index 4fb3a8b71..4108fdf12 100644
--- a/src/components/AppUpdateInfoBar.js
+++ b/src/components/AppUpdateInfoBar.js
@@ -8,7 +8,7 @@ import InfoBar from './ui/InfoBar';
8const messages = defineMessages({ 8const messages = defineMessages({
9 updateAvailable: { 9 updateAvailable: {
10 id: 'infobar.updateAvailable', 10 id: 'infobar.updateAvailable',
11 defaultMessage: '!!!A new update for Franz is available.', 11 defaultMessage: '!!!A new update for Ferdi is available.',
12 }, 12 },
13 changelog: { 13 changelog: {
14 id: 'infobar.buttonChangelog', 14 id: 'infobar.buttonChangelog',
diff --git a/src/components/auth/AuthLayout.js b/src/components/auth/AuthLayout.js
index 75a8cfc61..0c5198583 100644
--- a/src/components/auth/AuthLayout.js
+++ b/src/components/auth/AuthLayout.js
@@ -52,7 +52,7 @@ export default @observer class AuthLayout extends Component {
52 52
53 return ( 53 return (
54 <> 54 <>
55 {isWindows && !isFullScreen && <TitleBar menu={window.franz.menu.template} icon="assets/images/logo.svg" />} 55 {isWindows && !isFullScreen && <TitleBar menu={window.ferdi.menu.template} icon="assets/images/logo.svg" />}
56 <div className="auth"> 56 <div className="auth">
57 {!isOnline && ( 57 {!isOnline && (
58 <InfoBar 58 <InfoBar
@@ -87,7 +87,7 @@ export default @observer class AuthLayout extends Component {
87 })} 87 })}
88 </div> 88 </div>
89 {/* </div> */} 89 {/* </div> */}
90 <Link to="https://adlk.io" className="auth__adlk" target="_blank"> 90 <Link to="https://github.com/getferdi/ferdi" className="auth__adlk" target="_blank">
91 <img src="./assets/images/adlk.svg" alt="" /> 91 <img src="./assets/images/adlk.svg" alt="" />
92 </Link> 92 </Link>
93 </div> 93 </div>
diff --git a/src/components/auth/Import.js b/src/components/auth/Import.js
index 0d5feb274..3e34c3162 100644
--- a/src/components/auth/Import.js
+++ b/src/components/auth/Import.js
@@ -12,11 +12,11 @@ import Button from '../ui/Button';
12const messages = defineMessages({ 12const messages = defineMessages({
13 headline: { 13 headline: {
14 id: 'import.headline', 14 id: 'import.headline',
15 defaultMessage: '!!!Import your Franz 4 services', 15 defaultMessage: '!!!Import your Ferdi 4 services',
16 }, 16 },
17 notSupportedHeadline: { 17 notSupportedHeadline: {
18 id: 'import.notSupportedHeadline', 18 id: 'import.notSupportedHeadline',
19 defaultMessage: '!!!Services not yet supported in Franz 5', 19 defaultMessage: '!!!Services not yet supported in Ferdi 5',
20 }, 20 },
21 submitButtonLabel: { 21 submitButtonLabel: {
22 id: 'import.submit.label', 22 id: 'import.submit.label',
diff --git a/src/components/auth/Locked.js b/src/components/auth/Locked.js
new file mode 100644
index 000000000..045621d0a
--- /dev/null
+++ b/src/components/auth/Locked.js
@@ -0,0 +1,115 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl';
5
6import Form from '../../lib/Form';
7import { required } from '../../helpers/validation-helpers';
8import Input from '../ui/Input';
9import Button from '../ui/Button';
10import Infobox from '../ui/Infobox';
11
12import { globalError as globalErrorPropType } from '../../prop-types';
13
14const messages = defineMessages({
15 headline: {
16 id: 'locked.headline',
17 defaultMessage: '!!!Locked',
18 },
19 info: {
20 id: 'locked.info',
21 defaultMessage: '!!!Ferdi is currently locked. Please unlock Ferdi with your password to see your messages.',
22 },
23 passwordLabel: {
24 id: 'locked.password.label',
25 defaultMessage: '!!!Password',
26 },
27 submitButtonLabel: {
28 id: 'locked.submit.label',
29 defaultMessage: '!!!Unlock',
30 },
31 invalidCredentials: {
32 id: 'locked.invalidCredentials',
33 defaultMessage: '!!!Password invalid',
34 },
35});
36
37export default @observer class Locked extends Component {
38 static propTypes = {
39 onSubmit: PropTypes.func.isRequired,
40 isSubmitting: PropTypes.bool.isRequired,
41 error: globalErrorPropType.isRequired,
42 };
43
44 static contextTypes = {
45 intl: intlShape,
46 };
47
48 form = new Form({
49 fields: {
50 password: {
51 label: this.context.intl.formatMessage(messages.passwordLabel),
52 value: '',
53 validators: [required],
54 type: 'password',
55 },
56 },
57 }, this.context.intl);
58
59 submit(e) {
60 e.preventDefault();
61 this.form.submit({
62 onSuccess: (form) => {
63 this.props.onSubmit(form.values());
64 },
65 onError: () => { },
66 });
67 }
68
69 render() {
70 const { form } = this;
71 const { intl } = this.context;
72 const {
73 isSubmitting,
74 error,
75 } = this.props;
76
77 return (
78 <div className="auth__container">
79 <form className="franz-form auth__form" onSubmit={e => this.submit(e)}>
80 <img
81 src="./assets/images/logo.svg"
82 className="auth__logo"
83 alt=""
84 />
85 <h1>{intl.formatMessage(messages.headline)}</h1>
86 <Infobox type="warning">
87 {intl.formatMessage(messages.info)}
88 </Infobox>
89 <Input
90 field={form.$('password')}
91 showPasswordToggle
92 />
93 {error.code === 'invalid-credentials' && (
94 <p className="error-message center">{intl.formatMessage(messages.invalidCredentials)}</p>
95 )}
96 {isSubmitting ? (
97 <Button
98 className="auth__button is-loading"
99 buttonType="secondary"
100 label={`${intl.formatMessage(messages.submitButtonLabel)} ...`}
101 loaded={false}
102 disabled
103 />
104 ) : (
105 <Button
106 type="submit"
107 className="auth__button"
108 label={intl.formatMessage(messages.submitButtonLabel)}
109 />
110 )}
111 </form>
112 </div>
113 );
114 }
115}
diff --git a/src/components/auth/Login.js b/src/components/auth/Login.js
index 5d21f8b60..e25121de0 100644
--- a/src/components/auth/Login.js
+++ b/src/components/auth/Login.js
@@ -1,11 +1,13 @@
1/* eslint jsx-a11y/anchor-is-valid: 0 */
1import React, { Component } from 'react'; 2import React, { Component } from 'react';
2import PropTypes from 'prop-types'; 3import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 4import { observer, inject } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl'; 5import { defineMessages, intlShape } from 'react-intl';
5 6
6import { isDevMode, useLiveAPI } from '../../environment'; 7import { isDevMode, useLiveAPI } from '../../environment';
7import Form from '../../lib/Form'; 8import Form from '../../lib/Form';
8import { required, email } from '../../helpers/validation-helpers'; 9import { required, email } from '../../helpers/validation-helpers';
10import serverlessLogin from '../../helpers/serverless-helpers';
9import Input from '../ui/Input'; 11import Input from '../ui/Input';
10import Button from '../ui/Button'; 12import Button from '../ui/Button';
11import Link from '../ui/Link'; 13import Link from '../ui/Link';
@@ -34,6 +36,14 @@ const messages = defineMessages({
34 id: 'login.invalidCredentials', 36 id: 'login.invalidCredentials',
35 defaultMessage: '!!!Email or password not valid', 37 defaultMessage: '!!!Email or password not valid',
36 }, 38 },
39 customServerQuestion: {
40 id: 'login.customServerQuestion',
41 defaultMessage: '!!!Using a Franz account to log in?',
42 },
43 customServerSuggestion: {
44 id: 'login.customServerSuggestion',
45 defaultMessage: '!!!Try importing your Franz account into Ferdi',
46 },
37 tokenExpired: { 47 tokenExpired: {
38 id: 'login.tokenExpired', 48 id: 'login.tokenExpired',
39 defaultMessage: '!!!Your session expired, please login again.', 49 defaultMessage: '!!!Your session expired, please login again.',
@@ -46,13 +56,21 @@ const messages = defineMessages({
46 id: 'login.link.signup', 56 id: 'login.link.signup',
47 defaultMessage: '!!!Create a free account', 57 defaultMessage: '!!!Create a free account',
48 }, 58 },
59 changeServer: {
60 id: 'login.changeServer',
61 defaultMessage: '!!!Change server',
62 },
63 serverless: {
64 id: 'services.serverless',
65 defaultMessage: '!!!Use Ferdi without an Account',
66 },
49 passwordLink: { 67 passwordLink: {
50 id: 'login.link.password', 68 id: 'login.link.password',
51 defaultMessage: '!!!Forgot password', 69 defaultMessage: '!!!Forgot password',
52 }, 70 },
53}); 71});
54 72
55export default @observer class Login extends Component { 73export default @inject('actions') @observer class Login extends Component {
56 static propTypes = { 74 static propTypes = {
57 onSubmit: PropTypes.func.isRequired, 75 onSubmit: PropTypes.func.isRequired,
58 isSubmitting: PropTypes.bool.isRequired, 76 isSubmitting: PropTypes.bool.isRequired,
@@ -61,6 +79,7 @@ export default @observer class Login extends Component {
61 signupRoute: PropTypes.string.isRequired, 79 signupRoute: PropTypes.string.isRequired,
62 passwordRoute: PropTypes.string.isRequired, 80 passwordRoute: PropTypes.string.isRequired,
63 error: globalErrorPropType.isRequired, 81 error: globalErrorPropType.isRequired,
82 actions: PropTypes.object.isRequired,
64 }; 83 };
65 84
66 static contextTypes = { 85 static contextTypes = {
@@ -95,6 +114,10 @@ export default @observer class Login extends Component {
95 }); 114 });
96 } 115 }
97 116
117 useLocalServer() {
118 serverlessLogin(this.props.actions);
119 }
120
98 render() { 121 render() {
99 const { form } = this; 122 const { form } = this;
100 const { intl } = this.context; 123 const { intl } = this.context;
@@ -137,7 +160,22 @@ export default @observer class Login extends Component {
137 showPasswordToggle 160 showPasswordToggle
138 /> 161 />
139 {error.code === 'invalid-credentials' && ( 162 {error.code === 'invalid-credentials' && (
140 <p className="error-message center">{intl.formatMessage(messages.invalidCredentials)}</p> 163 <>
164 <p className="error-message center">{intl.formatMessage(messages.invalidCredentials)}</p>
165 { window.ferdi.stores.settings.all.app.server !== 'https://api.franzinfra.com' && (
166 <p className="error-message center">
167 {intl.formatMessage(messages.customServerQuestion)}
168 {' '}
169 <Link
170 to={`${window.ferdi.stores.settings.all.app.server.replace('v1', '')}/import`}
171 target="_blank"
172 style={{ cursor: 'pointer', textDecoration: 'underline' }}
173 >
174 {intl.formatMessage(messages.customServerSuggestion)}
175 </Link>
176 </p>
177 )}
178 </>
141 )} 179 )}
142 {isSubmitting ? ( 180 {isSubmitting ? (
143 <Button 181 <Button
@@ -156,6 +194,8 @@ export default @observer class Login extends Component {
156 )} 194 )}
157 </form> 195 </form>
158 <div className="auth__links"> 196 <div className="auth__links">
197 <Link to="/settings/app">{intl.formatMessage(messages.changeServer)}</Link>
198 <a onClick={this.useLocalServer.bind(this)}>{intl.formatMessage(messages.serverless)}</a>
159 <Link to={signupRoute}>{intl.formatMessage(messages.signupLink)}</Link> 199 <Link to={signupRoute}>{intl.formatMessage(messages.signupLink)}</Link>
160 <Link to={passwordRoute}>{intl.formatMessage(messages.passwordLink)}</Link> 200 <Link to={passwordRoute}>{intl.formatMessage(messages.passwordLink)}</Link>
161 </div> 201 </div>
diff --git a/src/components/auth/Pricing.js b/src/components/auth/Pricing.js
index 53ae046a0..593cb9c4b 100644
--- a/src/components/auth/Pricing.js
+++ b/src/components/auth/Pricing.js
@@ -58,7 +58,7 @@ const messages = defineMessages({
58 }, 58 },
59 ctaSkip: { 59 ctaSkip: {
60 id: 'pricing.trial.cta.skip', 60 id: 'pricing.trial.cta.skip',
61 defaultMessage: '!!!Continue to Franz', 61 defaultMessage: '!!!Continue to Ferdi',
62 }, 62 },
63 featuresHeadline: { 63 featuresHeadline: {
64 id: 'pricing.trial.features.headline', 64 id: 'pricing.trial.features.headline',
@@ -140,7 +140,7 @@ const styles = theme => ({
140 }, 140 },
141}); 141});
142 142
143export default @observer @injectSheet(styles) class Signup extends Component { 143export default @injectSheet(styles) @observer class Signup extends Component {
144 static propTypes = { 144 static propTypes = {
145 onSubmit: PropTypes.func.isRequired, 145 onSubmit: PropTypes.func.isRequired,
146 isLoadingRequiredData: PropTypes.bool.isRequired, 146 isLoadingRequiredData: PropTypes.bool.isRequired,
diff --git a/src/components/auth/Signup.js b/src/components/auth/Signup.js
index 0499d764b..a166155a7 100644
--- a/src/components/auth/Signup.js
+++ b/src/components/auth/Signup.js
@@ -1,11 +1,13 @@
1/* eslint jsx-a11y/anchor-is-valid: 0 */
1import React, { Component } from 'react'; 2import React, { Component } from 'react';
2import PropTypes from 'prop-types'; 3import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 4import { observer, inject } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl'; 5import { defineMessages, intlShape } from 'react-intl';
5 6
6import { isDevMode, useLiveAPI } from '../../environment'; 7import { isDevMode, useLiveAPI } from '../../environment';
7import Form from '../../lib/Form'; 8import Form from '../../lib/Form';
8import { required, email, minLength } from '../../helpers/validation-helpers'; 9import { required, email, minLength } from '../../helpers/validation-helpers';
10import serverlessLogin from '../../helpers/serverless-helpers';
9import Input from '../ui/Input'; 11import Input from '../ui/Input';
10import Button from '../ui/Button'; 12import Button from '../ui/Button';
11import Link from '../ui/Link'; 13import Link from '../ui/Link';
@@ -40,7 +42,7 @@ const messages = defineMessages({
40 }, 42 },
41 legalInfo: { 43 legalInfo: {
42 id: 'signup.legal.info', 44 id: 'signup.legal.info',
43 defaultMessage: '!!!By creating a Franz account you accept the', 45 defaultMessage: '!!!By creating a Ferdi account you accept the',
44 }, 46 },
45 terms: { 47 terms: {
46 id: 'signup.legal.terms', 48 id: 'signup.legal.terms',
@@ -58,18 +60,27 @@ const messages = defineMessages({
58 id: 'signup.link.login', 60 id: 'signup.link.login',
59 defaultMessage: '!!!Already have an account, sign in?', 61 defaultMessage: '!!!Already have an account, sign in?',
60 }, 62 },
63 changeServer: {
64 id: 'login.changeServer',
65 defaultMessage: '!!!Change server',
66 },
67 serverless: {
68 id: 'services.serverless',
69 defaultMessage: '!!!Use Ferdi without an Account',
70 },
61 emailDuplicate: { 71 emailDuplicate: {
62 id: 'signup.emailDuplicate', 72 id: 'signup.emailDuplicate',
63 defaultMessage: '!!!A user with that email address already exists', 73 defaultMessage: '!!!A user with that email address already exists',
64 }, 74 },
65}); 75});
66 76
67export default @observer class Signup extends Component { 77export default @inject('actions') @observer class Signup extends Component {
68 static propTypes = { 78 static propTypes = {
69 onSubmit: PropTypes.func.isRequired, 79 onSubmit: PropTypes.func.isRequired,
70 isSubmitting: PropTypes.bool.isRequired, 80 isSubmitting: PropTypes.bool.isRequired,
71 loginRoute: PropTypes.string.isRequired, 81 loginRoute: PropTypes.string.isRequired,
72 error: globalErrorPropType.isRequired, 82 error: globalErrorPropType.isRequired,
83 actions: PropTypes.object.isRequired,
73 }; 84 };
74 85
75 static contextTypes = { 86 static contextTypes = {
@@ -112,11 +123,17 @@ export default @observer class Signup extends Component {
112 }); 123 });
113 } 124 }
114 125
126 useLocalServer() {
127 serverlessLogin(this.props.actions);
128 }
129
115 render() { 130 render() {
116 const { form } = this; 131 const { form } = this;
117 const { intl } = this.context; 132 const { intl } = this.context;
118 const { isSubmitting, loginRoute, error } = this.props; 133 const { isSubmitting, loginRoute, error } = this.props;
119 134
135 const termsBase = window.ferdi.stores.settings.all.app.server !== 'https://api.franzinfra.com' ? window.ferdi.stores.settings.all.app.server : 'https://meetfranz.com';
136
120 return ( 137 return (
121 <div className="auth__scroll-container"> 138 <div className="auth__scroll-container">
122 <div className="auth__container auth__container--signup"> 139 <div className="auth__container auth__container--signup">
@@ -163,7 +180,7 @@ export default @observer class Signup extends Component {
163 {intl.formatMessage(messages.legalInfo)} 180 {intl.formatMessage(messages.legalInfo)}
164 <br /> 181 <br />
165 <Link 182 <Link
166 to="https://meetfranz.com/terms" 183 to={`${termsBase}/terms`}
167 target="_blank" 184 target="_blank"
168 className="link" 185 className="link"
169 > 186 >
@@ -171,7 +188,7 @@ export default @observer class Signup extends Component {
171 </Link> 188 </Link>
172 &nbsp;&amp;&nbsp; 189 &nbsp;&amp;&nbsp;
173 <Link 190 <Link
174 to="https://meetfranz.com/privacy" 191 to={`${termsBase}/privacy`}
175 target="_blank" 192 target="_blank"
176 className="link" 193 className="link"
177 > 194 >
@@ -181,6 +198,8 @@ export default @observer class Signup extends Component {
181 </p> 198 </p>
182 </form> 199 </form>
183 <div className="auth__links"> 200 <div className="auth__links">
201 <Link to="/settings/app">{intl.formatMessage(messages.changeServer)}</Link>
202 <a onClick={this.useLocalServer.bind(this)}>{intl.formatMessage(messages.serverless)}</a>
184 <Link to={loginRoute}>{intl.formatMessage(messages.loginLink)}</Link> 203 <Link to={loginRoute}>{intl.formatMessage(messages.loginLink)}</Link>
185 </div> 204 </div>
186 </div> 205 </div>
diff --git a/src/components/auth/Welcome.js b/src/components/auth/Welcome.js
index f6d77f70f..1453c1d7c 100644
--- a/src/components/auth/Welcome.js
+++ b/src/components/auth/Welcome.js
@@ -1,7 +1,9 @@
1/* eslint jsx-a11y/anchor-is-valid: 0 */
1import React, { Component } from 'react'; 2import React, { Component } from 'react';
2import PropTypes from 'prop-types'; 3import PropTypes from 'prop-types';
3import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; 4import { observer, PropTypes as MobxPropTypes, inject } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl'; 5import { defineMessages, intlShape } from 'react-intl';
6import serverlessLogin from '../../helpers/serverless-helpers';
5 7
6import Link from '../ui/Link'; 8import Link from '../ui/Link';
7 9
@@ -14,19 +16,28 @@ const messages = defineMessages({
14 id: 'welcome.loginButton', 16 id: 'welcome.loginButton',
15 defaultMessage: '!!!Login to your account', 17 defaultMessage: '!!!Login to your account',
16 }, 18 },
19 serverless: {
20 id: 'services.serverless',
21 defaultMessage: '!!!Use Ferdi without an Account',
22 },
17}); 23});
18 24
19export default @observer class Login extends Component { 25export default @inject('actions') @observer class Login extends Component {
20 static propTypes = { 26 static propTypes = {
21 loginRoute: PropTypes.string.isRequired, 27 loginRoute: PropTypes.string.isRequired,
22 signupRoute: PropTypes.string.isRequired, 28 signupRoute: PropTypes.string.isRequired,
23 recipes: MobxPropTypes.arrayOrObservableArray.isRequired, 29 recipes: MobxPropTypes.arrayOrObservableArray.isRequired,
30 actions: PropTypes.object.isRequired,
24 }; 31 };
25 32
26 static contextTypes = { 33 static contextTypes = {
27 intl: intlShape, 34 intl: intlShape,
28 }; 35 };
29 36
37 useLocalServer() {
38 serverlessLogin(this.props.actions);
39 }
40
30 render() { 41 render() {
31 const { intl } = this.context; 42 const { intl } = this.context;
32 const { 43 const {
@@ -41,7 +52,7 @@ export default @observer class Login extends Component {
41 <img src="./assets/images/logo.svg" className="welcome__logo" alt="" /> 52 <img src="./assets/images/logo.svg" className="welcome__logo" alt="" />
42 {/* <img src="./assets/images/welcome.png" className="welcome__services" alt="" /> */} 53 {/* <img src="./assets/images/welcome.png" className="welcome__services" alt="" /> */}
43 <div className="welcome__text"> 54 <div className="welcome__text">
44 <h1>Franz</h1> 55 <h1>Ferdi</h1>
45 </div> 56 </div>
46 </div> 57 </div>
47 <div className="welcome__buttons"> 58 <div className="welcome__buttons">
@@ -51,6 +62,25 @@ export default @observer class Login extends Component {
51 <Link to={loginRoute} className="button"> 62 <Link to={loginRoute} className="button">
52 {intl.formatMessage(messages.loginButton)} 63 {intl.formatMessage(messages.loginButton)}
53 </Link> 64 </Link>
65 <br />
66 <br />
67 <a className="button" onClick={this.useLocalServer.bind(this)}>
68 {intl.formatMessage(messages.serverless)}
69 </a>
70 <br />
71 <br />
72
73
74 <Link to="settings/app">
75 <span style={{
76 textAlign: 'center',
77 width: '100%',
78 cursor: 'pointer',
79 }}
80 >
81 Change server
82 </span>
83 </Link>
54 </div> 84 </div>
55 <div className="welcome__featured-services"> 85 <div className="welcome__featured-services">
56 {recipes.map(recipe => ( 86 {recipes.map(recipe => (
diff --git a/src/components/layout/AppLayout.js b/src/components/layout/AppLayout.js
index 9b110262a..80e6daf19 100644
--- a/src/components/layout/AppLayout.js
+++ b/src/components/layout/AppLayout.js
@@ -6,9 +6,9 @@ import { TitleBar } from 'electron-react-titlebar';
6import injectSheet from 'react-jss'; 6import injectSheet from 'react-jss';
7 7
8import InfoBar from '../ui/InfoBar'; 8import InfoBar from '../ui/InfoBar';
9import { Component as DelayApp } from '../../features/delayApp';
10import { Component as BasicAuth } from '../../features/basicAuth'; 9import { Component as BasicAuth } from '../../features/basicAuth';
11import { Component as ShareFranz } from '../../features/shareFranz'; 10import { Component as ShareFranz } from '../../features/shareFranz';
11import { Component as QuickSwitch } from '../../features/quickSwitch';
12import ErrorBoundary from '../util/ErrorBoundary'; 12import ErrorBoundary from '../util/ErrorBoundary';
13 13
14// import globalMessages from '../../i18n/globalMessages'; 14// import globalMessages from '../../i18n/globalMessages';
@@ -39,6 +39,10 @@ const messages = defineMessages({
39 id: 'infobar.requiredRequestsFailed', 39 id: 'infobar.requiredRequestsFailed',
40 defaultMessage: '!!!Could not load services and user information', 40 defaultMessage: '!!!Could not load services and user information',
41 }, 41 },
42 authRequestFailed: {
43 id: 'infobar.authRequestFailed',
44 defaultMessage: '!!!There were errors while trying to perform an authenticated request. Please try logging out and back in if this error persists.',
45 },
42}); 46});
43 47
44const styles = theme => ({ 48const styles = theme => ({
@@ -65,6 +69,7 @@ class AppLayout extends Component {
65 showServicesUpdatedInfoBar: PropTypes.bool.isRequired, 69 showServicesUpdatedInfoBar: PropTypes.bool.isRequired,
66 appUpdateIsDownloaded: PropTypes.bool.isRequired, 70 appUpdateIsDownloaded: PropTypes.bool.isRequired,
67 nextAppReleaseVersion: PropTypes.string, 71 nextAppReleaseVersion: PropTypes.string,
72 authRequestFailed: PropTypes.bool.isRequired,
68 removeNewsItem: PropTypes.func.isRequired, 73 removeNewsItem: PropTypes.func.isRequired,
69 reloadServicesAfterUpdate: PropTypes.func.isRequired, 74 reloadServicesAfterUpdate: PropTypes.func.isRequired,
70 installAppUpdate: PropTypes.func.isRequired, 75 installAppUpdate: PropTypes.func.isRequired,
@@ -72,7 +77,6 @@ class AppLayout extends Component {
72 areRequiredRequestsSuccessful: PropTypes.bool.isRequired, 77 areRequiredRequestsSuccessful: PropTypes.bool.isRequired,
73 retryRequiredRequests: PropTypes.func.isRequired, 78 retryRequiredRequests: PropTypes.func.isRequired,
74 areRequiredRequestsLoading: PropTypes.bool.isRequired, 79 areRequiredRequestsLoading: PropTypes.bool.isRequired,
75 isDelayAppScreenVisible: PropTypes.bool.isRequired,
76 hasActivatedTrial: PropTypes.bool.isRequired, 80 hasActivatedTrial: PropTypes.bool.isRequired,
77 }; 81 };
78 82
@@ -97,6 +101,7 @@ class AppLayout extends Component {
97 showServicesUpdatedInfoBar, 101 showServicesUpdatedInfoBar,
98 appUpdateIsDownloaded, 102 appUpdateIsDownloaded,
99 nextAppReleaseVersion, 103 nextAppReleaseVersion,
104 authRequestFailed,
100 removeNewsItem, 105 removeNewsItem,
101 reloadServicesAfterUpdate, 106 reloadServicesAfterUpdate,
102 installAppUpdate, 107 installAppUpdate,
@@ -104,7 +109,6 @@ class AppLayout extends Component {
104 areRequiredRequestsSuccessful, 109 areRequiredRequestsSuccessful,
105 retryRequiredRequests, 110 retryRequiredRequests,
106 areRequiredRequestsLoading, 111 areRequiredRequestsLoading,
107 isDelayAppScreenVisible,
108 hasActivatedTrial, 112 hasActivatedTrial,
109 } = this.props; 113 } = this.props;
110 114
@@ -113,7 +117,7 @@ class AppLayout extends Component {
113 return ( 117 return (
114 <ErrorBoundary> 118 <ErrorBoundary>
115 <div className="app"> 119 <div className="app">
116 {isWindows && !isFullScreen && <TitleBar menu={window.franz.menu.template} icon="assets/images/logo.svg" />} 120 {isWindows && !isFullScreen && <TitleBar menu={window.ferdi.menu.template} icon="assets/images/logo.svg" />}
117 <div className={`app__content ${classes.appContent}`}> 121 <div className={`app__content ${classes.appContent}`}>
118 {workspacesDrawer} 122 {workspacesDrawer}
119 {sidebar} 123 {sidebar}
@@ -153,6 +157,18 @@ class AppLayout extends Component {
153 {intl.formatMessage(messages.requiredRequestsFailed)} 157 {intl.formatMessage(messages.requiredRequestsFailed)}
154 </InfoBar> 158 </InfoBar>
155 )} 159 )}
160 {authRequestFailed && (
161 <InfoBar
162 type="danger"
163 ctaLabel="Try again"
164 ctaLoading={areRequiredRequestsLoading}
165 sticky
166 onClick={retryRequiredRequests}
167 >
168 <span className="mdi mdi-flash" />
169 {intl.formatMessage(messages.authRequestFailed)}
170 </InfoBar>
171 )}
156 {showServicesUpdatedInfoBar && ( 172 {showServicesUpdatedInfoBar && (
157 <InfoBar 173 <InfoBar
158 type="primary" 174 type="primary"
@@ -170,9 +186,9 @@ class AppLayout extends Component {
170 onInstallUpdate={installAppUpdate} 186 onInstallUpdate={installAppUpdate}
171 /> 187 />
172 )} 188 )}
173 {isDelayAppScreenVisible && (<DelayApp />)}
174 <BasicAuth /> 189 <BasicAuth />
175 <ShareFranz /> 190 <ShareFranz />
191 <QuickSwitch />
176 {services} 192 {services}
177 {children} 193 {children}
178 <TrialStatusBar /> 194 <TrialStatusBar />
diff --git a/src/components/layout/Sidebar.js b/src/components/layout/Sidebar.js
index 918298011..48a83c5a1 100644
--- a/src/components/layout/Sidebar.js
+++ b/src/components/layout/Sidebar.js
@@ -2,13 +2,13 @@ import React, { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import ReactTooltip from 'react-tooltip'; 3import ReactTooltip from 'react-tooltip';
4import { defineMessages, intlShape } from 'react-intl'; 4import { defineMessages, intlShape } from 'react-intl';
5import { observer } from 'mobx-react'; 5import { inject, observer } from 'mobx-react';
6import { Link } from 'react-router';
6 7
7import Tabbar from '../services/tabs/Tabbar'; 8import Tabbar from '../services/tabs/Tabbar';
8import { ctrlKey } from '../../environment'; 9import { ctrlKey } from '../../environment';
9import { GA_CATEGORY_WORKSPACES, workspaceStore } from '../../features/workspaces'; 10import { workspaceStore } from '../../features/workspaces';
10import { gaEvent } from '../../lib/analytics'; 11import { todosStore } from '../../features/todos';
11import { todosStore, GA_CATEGORY_TODOS } from '../../features/todos';
12import { todoActions } from '../../features/todos/actions'; 12import { todoActions } from '../../features/todos/actions';
13 13
14const messages = defineMessages({ 14const messages = defineMessages({
@@ -44,9 +44,13 @@ const messages = defineMessages({
44 id: 'sidebar.closeTodosDrawer', 44 id: 'sidebar.closeTodosDrawer',
45 defaultMessage: '!!!Close Franz Todos', 45 defaultMessage: '!!!Close Franz Todos',
46 }, 46 },
47 lockFerdi: {
48 id: 'sidebar.lockFerdi',
49 defaultMessage: '!!!Lock Ferdi',
50 },
47}); 51});
48 52
49export default @observer class Sidebar extends Component { 53export default @inject('stores', 'actions') @observer class Sidebar extends Component {
50 static propTypes = { 54 static propTypes = {
51 openSettings: PropTypes.func.isRequired, 55 openSettings: PropTypes.func.isRequired,
52 toggleMuteApp: PropTypes.func.isRequired, 56 toggleMuteApp: PropTypes.func.isRequired,
@@ -87,6 +91,8 @@ export default @observer class Sidebar extends Component {
87 isAppMuted, 91 isAppMuted,
88 isWorkspaceDrawerOpen, 92 isWorkspaceDrawerOpen,
89 toggleWorkspaceDrawer, 93 toggleWorkspaceDrawer,
94 stores,
95 actions,
90 } = this.props; 96 } = this.props;
91 const { intl } = this.context; 97 const { intl } = this.context;
92 const todosToggleMessage = ( 98 const todosToggleMessage = (
@@ -96,6 +102,7 @@ export default @observer class Sidebar extends Component {
96 const workspaceToggleMessage = ( 102 const workspaceToggleMessage = (
97 isWorkspaceDrawerOpen ? messages.closeWorkspaceDrawer : messages.openWorkspaceDrawer 103 isWorkspaceDrawerOpen ? messages.closeWorkspaceDrawer : messages.openWorkspaceDrawer
98 ); 104 );
105 const isLoggedIn = Boolean(localStorage.getItem('authToken'));
99 106
100 return ( 107 return (
101 <div className="sidebar"> 108 <div className="sidebar">
@@ -104,53 +111,89 @@ export default @observer class Sidebar extends Component {
104 enableToolTip={() => this.enableToolTip()} 111 enableToolTip={() => this.enableToolTip()}
105 disableToolTip={() => this.disableToolTip()} 112 disableToolTip={() => this.disableToolTip()}
106 /> 113 />
107 {todosStore.isFeatureEnabled && todosStore.isFeatureEnabledByUser ? ( 114 { isLoggedIn ? (
108 <button 115 <>
109 type="button" 116 { stores.settings.all.app.lockingFeatureEnabled ? (
110 onClick={() => { 117 <button
111 todoActions.toggleTodosPanel(); 118 type="button"
112 this.updateToolTip(); 119 className="sidebar__button"
113 gaEvent(GA_CATEGORY_TODOS, 'toggleDrawer', 'sidebar'); 120 onClick={() => {
114 }} 121 // Disable lock first - otherwise the application might not update correctly
115 className={`sidebar__button sidebar__button--todos ${todosStore.isTodosPanelVisible ? 'is-active' : ''}`} 122 actions.settings.update({
116 data-tip={`${intl.formatMessage(todosToggleMessage)} (${ctrlKey}+T)`} 123 type: 'app',
117 > 124 data: {
118 <i className="mdi mdi-check-all" /> 125 locked: false,
119 </button> 126 },
120 ) : null} 127 });
121 {workspaceStore.isFeatureEnabled ? ( 128 setTimeout(() => {
122 <button 129 actions.settings.update({
123 type="button" 130 type: 'app',
124 onClick={() => { 131 data: {
125 toggleWorkspaceDrawer(); 132 locked: true,
126 this.updateToolTip(); 133 },
127 gaEvent(GA_CATEGORY_WORKSPACES, 'toggleDrawer', 'sidebar'); 134 });
128 }} 135 }, 0);
129 className={`sidebar__button sidebar__button--workspaces ${isWorkspaceDrawerOpen ? 'is-active' : ''}`} 136 }}
130 data-tip={`${intl.formatMessage(workspaceToggleMessage)} (${ctrlKey}+D)`} 137 data-tip={`${intl.formatMessage(messages.lockFerdi)} (${ctrlKey}+Shift+L)`}
138 >
139 <i className="mdi mdi-lock" />
140 </button>
141 ) : null}
142 {todosStore.isFeatureEnabled && todosStore.isFeatureEnabledByUser ? (
143 <button
144 type="button"
145 onClick={() => {
146 todoActions.toggleTodosPanel();
147 this.updateToolTip();
148 }}
149 className={`sidebar__button sidebar__button--todos ${todosStore.isTodosPanelVisible ? 'is-active' : ''}`}
150 data-tip={`${intl.formatMessage(todosToggleMessage)} (${ctrlKey}+T)`}
151 >
152 <i className="mdi mdi-check-all" />
153 </button>
154 ) : null}
155 {workspaceStore.isFeatureEnabled ? (
156 <button
157 type="button"
158 onClick={() => {
159 toggleWorkspaceDrawer();
160 this.updateToolTip();
161 }}
162 className={`sidebar__button sidebar__button--workspaces ${isWorkspaceDrawerOpen ? 'is-active' : ''}`}
163 data-tip={`${intl.formatMessage(workspaceToggleMessage)} (${ctrlKey}+D)`}
164 >
165 <i className="mdi mdi-view-grid" />
166 </button>
167 ) : null}
168 <button
169 type="button"
170 onClick={() => {
171 toggleMuteApp();
172 this.updateToolTip();
173 }}
174 className={`sidebar__button sidebar__button--audio ${isAppMuted ? 'is-muted' : ''}`}
175 data-tip={`${intl.formatMessage(isAppMuted ? messages.unmute : messages.mute)} (${ctrlKey}+Shift+M)`}
176 >
177 <i className={`mdi mdi-bell${isAppMuted ? '-off' : ''}`} />
178 </button>
179 <button
180 type="button"
181 onClick={() => openSettings({ path: 'recipes' })}
182 className="sidebar__button sidebar__button--new-service"
183 data-tip={`${intl.formatMessage(messages.addNewService)} (${ctrlKey}+N)`}
184 >
185 <i className="mdi mdi-plus-box" />
186 </button>
187 </>
188 ) : (
189 <Link
190 to="/auth/welcome"
191 className="sidebar__button sidebar__button--new-service"
192 data-tip="Login"
131 > 193 >
132 <i className="mdi mdi-view-grid" /> 194 <i className="mdi mdi-login-variant" />
133 </button> 195 </Link>
134 ) : null} 196 )}
135 <button
136 type="button"
137 onClick={() => {
138 toggleMuteApp();
139 this.updateToolTip();
140 }}
141 className={`sidebar__button sidebar__button--audio ${isAppMuted ? 'is-muted' : ''}`}
142 data-tip={`${intl.formatMessage(isAppMuted ? messages.unmute : messages.mute)} (${ctrlKey}+Shift+M)`}
143 >
144 <i className={`mdi mdi-bell${isAppMuted ? '-off' : ''}`} />
145 </button>
146 <button
147 type="button"
148 onClick={() => openSettings({ path: 'recipes' })}
149 className="sidebar__button sidebar__button--new-service"
150 data-tip={`${intl.formatMessage(messages.addNewService)} (${ctrlKey}+N)`}
151 >
152 <i className="mdi mdi-plus-box" />
153 </button>
154 <button 197 <button
155 type="button" 198 type="button"
156 onClick={() => openSettings({ path: 'app' })} 199 onClick={() => openSettings({ path: 'app' })}
@@ -158,6 +201,12 @@ export default @observer class Sidebar extends Component {
158 data-tip={`${intl.formatMessage(messages.settings)} (${ctrlKey}+,)`} 201 data-tip={`${intl.formatMessage(messages.settings)} (${ctrlKey}+,)`}
159 > 202 >
160 <i className="mdi mdi-settings" /> 203 <i className="mdi mdi-settings" />
204 { (this.props.stores.app.updateStatus === this.props.stores.app.updateStatusTypes.AVAILABLE
205 || this.props.stores.app.updateStatus === this.props.stores.app.updateStatusTypes.DOWNLOADED) && (
206 <span className="update-available">
207 •
208 </span>
209 ) }
161 </button> 210 </button>
162 {this.state.tooltipEnabled && ( 211 {this.state.tooltipEnabled && (
163 <ReactTooltip place="right" type="dark" effect="solid" /> 212 <ReactTooltip place="right" type="dark" effect="solid" />
diff --git a/src/components/services/content/ServiceView.js b/src/components/services/content/ServiceView.js
index 3b09518c5..49ee24361 100644
--- a/src/components/services/content/ServiceView.js
+++ b/src/components/services/content/ServiceView.js
@@ -1,7 +1,7 @@
1import React, { Component, Fragment } from 'react'; 1import React, { Component, Fragment } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { autorun } from 'mobx'; 3import { autorun, reaction } from 'mobx';
4import { observer } from 'mobx-react'; 4import { observer, inject } from 'mobx-react';
5import classnames from 'classnames'; 5import classnames from 'classnames';
6 6
7import ServiceModel from '../../../models/Service'; 7import ServiceModel from '../../../models/Service';
@@ -10,12 +10,12 @@ import WebviewLoader from '../../ui/WebviewLoader';
10import WebviewCrashHandler from './WebviewCrashHandler'; 10import WebviewCrashHandler from './WebviewCrashHandler';
11import WebviewErrorHandler from './ErrorHandlers/WebviewErrorHandler'; 11import WebviewErrorHandler from './ErrorHandlers/WebviewErrorHandler';
12import ServiceDisabled from './ServiceDisabled'; 12import ServiceDisabled from './ServiceDisabled';
13import ServiceRestricted from './ServiceRestricted';
14import ServiceWebview from './ServiceWebview'; 13import ServiceWebview from './ServiceWebview';
14import SettingsStore from '../../../stores/SettingsStore';
15import WebControlsScreen from '../../../features/webControls/containers/WebControlsScreen'; 15import WebControlsScreen from '../../../features/webControls/containers/WebControlsScreen';
16import { CUSTOM_WEBSITE_ID } from '../../../features/webControls/constants'; 16import { CUSTOM_WEBSITE_ID } from '../../../features/webControls/constants';
17 17
18export default @observer class ServiceView extends Component { 18export default @inject('stores', 'actions') @observer class ServiceView extends Component {
19 static propTypes = { 19 static propTypes = {
20 service: PropTypes.instanceOf(ServiceModel).isRequired, 20 service: PropTypes.instanceOf(ServiceModel).isRequired,
21 setWebviewReference: PropTypes.func.isRequired, 21 setWebviewReference: PropTypes.func.isRequired,
@@ -24,7 +24,14 @@ export default @observer class ServiceView extends Component {
24 edit: PropTypes.func.isRequired, 24 edit: PropTypes.func.isRequired,
25 enable: PropTypes.func.isRequired, 25 enable: PropTypes.func.isRequired,
26 isActive: PropTypes.bool, 26 isActive: PropTypes.bool,
27 upgrade: PropTypes.func.isRequired, 27 stores: PropTypes.shape({
28 settings: PropTypes.instanceOf(SettingsStore).isRequired,
29 }).isRequired,
30 actions: PropTypes.shape({
31 service: PropTypes.shape({
32 setHibernation: PropTypes.func.isRequired,
33 }).isRequired,
34 }).isRequired,
28 }; 35 };
29 36
30 static defaultProps = { 37 static defaultProps = {
@@ -35,12 +42,20 @@ export default @observer class ServiceView extends Component {
35 forceRepaint: false, 42 forceRepaint: false,
36 targetUrl: '', 43 targetUrl: '',
37 statusBarVisible: false, 44 statusBarVisible: false,
45 hibernate: false,
46 hibernationTimer: null,
38 }; 47 };
39 48
40 autorunDisposer = null; 49 autorunDisposer = null;
41 50
42 forceRepaintTimeout = null; 51 forceRepaintTimeout = null;
43 52
53 constructor(props) {
54 super(props);
55
56 this.startHibernationTimer = this.startHibernationTimer.bind(this);
57 }
58
44 componentDidMount() { 59 componentDidMount() {
45 this.autorunDisposer = autorun(() => { 60 this.autorunDisposer = autorun(() => {
46 if (this.props.service.isActive) { 61 if (this.props.service.isActive) {
@@ -50,6 +65,45 @@ export default @observer class ServiceView extends Component {
50 }, 100); 65 }, 100);
51 } 66 }
52 }); 67 });
68
69 reaction(
70 () => this.props.service.isActive,
71 () => {
72 if (!this.props.service.isActive && this.props.stores.settings.all.app.hibernate) {
73 // Service is inactive - start hibernation countdown
74 this.startHibernationTimer();
75 } else {
76 if (this.state.hibernationTimer) {
77 // Service is active but we have an active hibernation timer: Clear timeout
78 clearTimeout(this.state.hibernationTimer);
79 }
80
81 // Service is active, wake up service from hibernation
82 this.setState({
83 hibernate: false,
84 });
85 this.props.actions.service.setHibernation({
86 serviceId: this.props.service.id,
87 hibernating: false,
88 });
89 }
90 },
91 );
92
93 // Store hibernation status to state, otherwise the webview won't get unloaded correctly
94 reaction(
95 () => this.props.service.isHibernating,
96 () => {
97 this.setState({
98 hibernate: this.props.service.isHibernating,
99 });
100 },
101 );
102
103 // Start hibernation counter if we are in background
104 if (!this.props.service.isActive && this.props.stores.settings.all.app.hibernate) {
105 this.startHibernationTimer();
106 }
53 } 107 }
54 108
55 componentWillUnmount() { 109 componentWillUnmount() {
@@ -68,6 +122,24 @@ export default @observer class ServiceView extends Component {
68 }); 122 });
69 }; 123 };
70 124
125 startHibernationTimer() {
126 const timerDuration = (Number(this.props.stores.settings.all.app.hibernationStrategy) || 300) * 1000;
127
128 const hibernationTimer = setTimeout(() => {
129 this.setState({
130 hibernate: true,
131 });
132 this.props.actions.service.setHibernation({
133 serviceId: this.props.service.id,
134 hibernating: true,
135 });
136 }, timerDuration);
137
138 this.setState({
139 hibernationTimer,
140 });
141 }
142
71 render() { 143 render() {
72 const { 144 const {
73 detachService, 145 detachService,
@@ -76,9 +148,13 @@ export default @observer class ServiceView extends Component {
76 reload, 148 reload,
77 edit, 149 edit,
78 enable, 150 enable,
79 upgrade, 151 stores,
80 } = this.props; 152 } = this.props;
81 153
154 const {
155 showServiceNavigationBar,
156 } = stores.settings.app;
157
82 const webviewClasses = classnames({ 158 const webviewClasses = classnames({
83 services__webview: true, 159 services__webview: true,
84 'services__webview-wrapper': true, 160 'services__webview-wrapper': true,
@@ -132,15 +208,9 @@ export default @observer class ServiceView extends Component {
132 </Fragment> 208 </Fragment>
133 ) : ( 209 ) : (
134 <> 210 <>
135 {service.isServiceAccessRestricted ? ( 211 {!this.state.hibernate ? (
136 <ServiceRestricted
137 name={service.recipe.name}
138 upgrade={upgrade}
139 type={service.restrictionType}
140 />
141 ) : (
142 <> 212 <>
143 {service.recipe.id === CUSTOM_WEBSITE_ID && ( 213 {(service.recipe.id === CUSTOM_WEBSITE_ID || showServiceNavigationBar) && (
144 <WebControlsScreen service={service} /> 214 <WebControlsScreen service={service} />
145 )} 215 )}
146 <ServiceWebview 216 <ServiceWebview
@@ -149,6 +219,12 @@ export default @observer class ServiceView extends Component {
149 detachService={detachService} 219 detachService={detachService}
150 /> 220 />
151 </> 221 </>
222 ) : (
223 <div>
224 <span role="img" aria-label="Sleeping Emoji">😴</span>
225 {' '}
226 This service is currently hibernating. If this page doesn&#x27;t close soon, please try reloading Ferdi.
227 </div>
152 )} 228 )}
153 </> 229 </>
154 )} 230 )}
diff --git a/src/components/services/content/ServiceWebview.js b/src/components/services/content/ServiceWebview.js
index 4bab4a964..e6ebb6afb 100644
--- a/src/components/services/content/ServiceWebview.js
+++ b/src/components/services/content/ServiceWebview.js
@@ -1,10 +1,13 @@
1import React, { Component } 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 { observable, reaction } from 'mobx';
4import ElectronWebView from 'react-electron-web-view'; 5import ElectronWebView from 'react-electron-web-view';
5 6
6import ServiceModel from '../../../models/Service'; 7import ServiceModel from '../../../models/Service';
7 8
9const debug = require('debug')('Ferdi:Services');
10
8@observer 11@observer
9class ServiceWebview extends Component { 12class ServiceWebview extends Component {
10 static propTypes = { 13 static propTypes = {
@@ -13,7 +16,22 @@ class ServiceWebview extends Component {
13 detachService: PropTypes.func.isRequired, 16 detachService: PropTypes.func.isRequired,
14 }; 17 };
15 18
16 webview = null; 19 @observable webview = null;
20
21 constructor(props) {
22 super(props);
23
24 reaction(
25 () => this.webview,
26 () => {
27 if (this.webview && this.webview.view) {
28 this.webview.view.addEventListener('console-message', (e) => {
29 debug('Service logged a message:', e.message);
30 });
31 }
32 },
33 );
34 }
17 35
18 componentWillUnmount() { 36 componentWillUnmount() {
19 const { service, detachService } = this.props; 37 const { service, detachService } = this.props;
diff --git a/src/components/services/content/Services.js b/src/components/services/content/Services.js
index b6291666b..80f17d8f2 100644
--- a/src/components/services/content/Services.js
+++ b/src/components/services/content/Services.js
@@ -1,6 +1,6 @@
1import React, { Component } from 'react'; 1import React, { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; 3import { observer, PropTypes as MobxPropTypes, inject } from 'mobx-react';
4import { Link } from 'react-router'; 4import { Link } from 'react-router';
5import { defineMessages, intlShape } from 'react-intl'; 5import { defineMessages, intlShape } from 'react-intl';
6import Confetti from 'react-confetti'; 6import Confetti from 'react-confetti';
@@ -9,16 +9,29 @@ import injectSheet from 'react-jss';
9 9
10import ServiceView from './ServiceView'; 10import ServiceView from './ServiceView';
11import Appear from '../../ui/effects/Appear'; 11import Appear from '../../ui/effects/Appear';
12import serverlessLogin from '../../../helpers/serverless-helpers';
12 13
13const messages = defineMessages({ 14const messages = defineMessages({
14 welcome: { 15 welcome: {
15 id: 'services.welcome', 16 id: 'services.welcome',
16 defaultMessage: '!!!Welcome to Franz', 17 defaultMessage: '!!!Welcome to Ferdi',
17 }, 18 },
18 getStarted: { 19 getStarted: {
19 id: 'services.getStarted', 20 id: 'services.getStarted',
20 defaultMessage: '!!!Get started', 21 defaultMessage: '!!!Get started',
21 }, 22 },
23 login: {
24 id: 'services.login',
25 defaultMessage: '!!!Please login to use Ferdi.',
26 },
27 serverless: {
28 id: 'services.serverless',
29 defaultMessage: '!!!Use Ferdi without an Account',
30 },
31 serverInfo: {
32 id: 'services.serverInfo',
33 defaultMessage: '!!!Optionally, you can change your Ferdi server by clicking the cog in the bottom left corner.',
34 },
22}); 35});
23 36
24 37
@@ -31,7 +44,7 @@ const styles = {
31 }, 44 },
32}; 45};
33 46
34export default @observer @injectSheet(styles) class Services extends Component { 47export default @injectSheet(styles) @inject('actions') @observer class Services extends Component {
35 static propTypes = { 48 static propTypes = {
36 services: MobxPropTypes.arrayOrObservableArray, 49 services: MobxPropTypes.arrayOrObservableArray,
37 setWebviewReference: PropTypes.func.isRequired, 50 setWebviewReference: PropTypes.func.isRequired,
@@ -44,6 +57,7 @@ export default @observer @injectSheet(styles) class Services extends Component {
44 userHasCompletedSignup: PropTypes.bool.isRequired, 57 userHasCompletedSignup: PropTypes.bool.isRequired,
45 hasActivatedTrial: PropTypes.bool.isRequired, 58 hasActivatedTrial: PropTypes.bool.isRequired,
46 classes: PropTypes.object.isRequired, 59 classes: PropTypes.object.isRequired,
60 actions: PropTypes.object.isRequired,
47 }; 61 };
48 62
49 static defaultProps = { 63 static defaultProps = {
@@ -60,6 +74,12 @@ export default @observer @injectSheet(styles) class Services extends Component {
60 74
61 _confettiTimeout = null; 75 _confettiTimeout = null;
62 76
77 constructor(props) {
78 super(props);
79
80 this.useLocalServer = this.useLocalServer.bind(this);
81 }
82
63 componentDidMount() { 83 componentDidMount() {
64 this._confettiTimeout = window.setTimeout(() => { 84 this._confettiTimeout = window.setTimeout(() => {
65 this.setState({ 85 this.setState({
@@ -74,6 +94,10 @@ export default @observer @injectSheet(styles) class Services extends Component {
74 } 94 }
75 } 95 }
76 96
97 useLocalServer() {
98 serverlessLogin(this.props.actions);
99 }
100
77 render() { 101 render() {
78 const { 102 const {
79 services, 103 services,
@@ -94,6 +118,7 @@ export default @observer @injectSheet(styles) class Services extends Component {
94 } = this.state; 118 } = this.state;
95 119
96 const { intl } = this.context; 120 const { intl } = this.context;
121 const isLoggedIn = Boolean(localStorage.getItem('authToken'));
97 122
98 return ( 123 return (
99 <div className="services"> 124 <div className="services">
@@ -112,15 +137,33 @@ export default @observer @injectSheet(styles) class Services extends Component {
112 transitionName="slideUp" 137 transitionName="slideUp"
113 > 138 >
114 <div className="services__no-service"> 139 <div className="services__no-service">
115 <img src="./assets/images/logo.svg" alt="" /> 140 <img src="./assets/images/logo.svg" alt="Logo" style={{ maxHeight: '50vh' }} />
116 <h1>{intl.formatMessage(messages.welcome)}</h1> 141 <h1>{intl.formatMessage(messages.welcome)}</h1>
142 { !isLoggedIn && (
143 <>
144 <p>{intl.formatMessage(messages.login)}</p>
145 <p>{intl.formatMessage(messages.serverInfo)}</p>
146 </>
147 ) }
117 <Appear 148 <Appear
118 timeout={300} 149 timeout={300}
119 transitionName="slideUp" 150 transitionName="slideUp"
120 > 151 >
121 <Link to="/settings/recipes" className="button"> 152 <Link to={isLoggedIn ? '/settings/services' : '/auth/welcome'} className="button">
122 {intl.formatMessage(messages.getStarted)} 153 { isLoggedIn ? intl.formatMessage(messages.getStarted) : 'Login' }
123 </Link> 154 </Link>
155 {!isLoggedIn && (
156 <button
157 type="button"
158 className="button"
159 style={{
160 marginLeft: 10,
161 }}
162 onClick={this.useLocalServer}
163 >
164 {intl.formatMessage(messages.serverless)}
165 </button>
166 )}
124 </Appear> 167 </Appear>
125 </div> 168 </div>
126 </Appear> 169 </Appear>
diff --git a/src/components/services/tabs/TabItem.js b/src/components/services/tabs/TabItem.js
index 8de7dc438..36338a910 100644
--- a/src/components/services/tabs/TabItem.js
+++ b/src/components/services/tabs/TabItem.js
@@ -145,6 +145,11 @@ class TabItem extends Component {
145 • 145 •
146 </span> 146 </span>
147 )} 147 )}
148 {service.isHibernating && (
149 <span className="tab-item__message-count hibernating">
150 •
151 </span>
152 )}
148 </span> 153 </span>
149 ); 154 );
150 } 155 }
diff --git a/src/components/settings/account/AccountDashboard.js b/src/components/settings/account/AccountDashboard.js
index b4ff072ab..83dc34a52 100644
--- a/src/components/settings/account/AccountDashboard.js
+++ b/src/components/settings/account/AccountDashboard.js
@@ -69,7 +69,7 @@ const messages = defineMessages({
69 }, 69 },
70 deleteInfo: { 70 deleteInfo: {
71 id: 'settings.account.deleteInfo', 71 id: 'settings.account.deleteInfo',
72 defaultMessage: '!!!If you don\'t need your Franz account any longer, you can delete your account and all related data here.', 72 defaultMessage: '!!!If you don\'t need your Ferdi account any longer, you can delete your account and all related data here.',
73 }, 73 },
74 deleteEmailSent: { 74 deleteEmailSent: {
75 id: 'settings.account.deleteEmailSent', 75 id: 'settings.account.deleteEmailSent',
diff --git a/src/components/settings/navigation/SettingsNavigation.js b/src/components/settings/navigation/SettingsNavigation.js
index 4696b82eb..192cfde7a 100644
--- a/src/components/settings/navigation/SettingsNavigation.js
+++ b/src/components/settings/navigation/SettingsNavigation.js
@@ -3,10 +3,13 @@ import 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'; 5import { ProBadge } from '@meetfranz/ui';
6import { RouterStore } from 'mobx-react-router';
6 7
8import { LOCAL_SERVER, LIVE_API } from '../../../config';
7import Link from '../../ui/Link'; 9import Link from '../../ui/Link';
8import { workspaceStore } from '../../../features/workspaces'; 10import { workspaceStore } from '../../../features/workspaces';
9import UIStore from '../../../stores/UIStore'; 11import UIStore from '../../../stores/UIStore';
12import SettingsStore from '../../../stores/SettingsStore';
10import UserStore from '../../../stores/UserStore'; 13import UserStore from '../../../stores/UserStore';
11import { serviceLimitStore } from '../../../features/serviceLimit'; 14import { serviceLimitStore } from '../../../features/serviceLimit';
12 15
@@ -35,9 +38,9 @@ const messages = defineMessages({
35 id: 'settings.navigation.settings', 38 id: 'settings.navigation.settings',
36 defaultMessage: '!!!Settings', 39 defaultMessage: '!!!Settings',
37 }, 40 },
38 inviteFriends: { 41 supportFerdi: {
39 id: 'settings.navigation.inviteFriends', 42 id: 'settings.navigation.supportFerdi',
40 defaultMessage: '!!!Invite Friends', 43 defaultMessage: '!!!Support Ferdi',
41 }, 44 },
42 logout: { 45 logout: {
43 id: 'settings.navigation.logout', 46 id: 'settings.navigation.logout',
@@ -45,11 +48,18 @@ const messages = defineMessages({
45 }, 48 },
46}); 49});
47 50
48export default @inject('stores') @observer class SettingsNavigation extends Component { 51export default @inject('stores', 'actions') @observer class SettingsNavigation extends Component {
49 static propTypes = { 52 static propTypes = {
50 stores: PropTypes.shape({ 53 stores: PropTypes.shape({
51 ui: PropTypes.instanceOf(UIStore).isRequired, 54 ui: PropTypes.instanceOf(UIStore).isRequired,
52 user: PropTypes.instanceOf(UserStore).isRequired, 55 user: PropTypes.instanceOf(UserStore).isRequired,
56 settings: PropTypes.instanceOf(SettingsStore).isRequired,
57 router: PropTypes.instanceOf(RouterStore).isRequired,
58 }).isRequired,
59 actions: PropTypes.shape({
60 settings: PropTypes.shape({
61 update: PropTypes.func.isRequired,
62 }).isRequired,
53 }).isRequired, 63 }).isRequired,
54 serviceCount: PropTypes.number.isRequired, 64 serviceCount: PropTypes.number.isRequired,
55 workspaceCount: PropTypes.number.isRequired, 65 workspaceCount: PropTypes.number.isRequired,
@@ -59,11 +69,42 @@ export default @inject('stores') @observer class SettingsNavigation extends Comp
59 intl: intlShape, 69 intl: intlShape,
60 }; 70 };
61 71
72 handleLoginLogout() {
73 const isLoggedIn = Boolean(localStorage.getItem('authToken'));
74 const isUsingWithoutAccount = this.props.stores.settings.app.server === LOCAL_SERVER;
75
76 if (isLoggedIn) {
77 // Remove current auth token
78 localStorage.removeItem('authToken');
79
80 if (isUsingWithoutAccount) {
81 // Reset server back to Ferdi API
82 this.props.actions.settings.update({
83 type: 'app',
84 data: {
85 server: LIVE_API,
86 },
87 });
88 }
89 this.props.stores.user.isLoggingOut = true;
90 }
91
92 this.props.stores.router.push(isLoggedIn ? '/auth/logout' : '/auth/welcome');
93
94 if (isLoggedIn) {
95 // Reload Ferdi, otherwise many settings won't sync correctly with the server
96 // after logging into another account
97 window.location.reload();
98 }
99 }
100
62 render() { 101 render() {
63 const { serviceCount, workspaceCount, stores } = this.props; 102 const { serviceCount, workspaceCount, stores } = this.props;
64 const { isDarkThemeActive } = stores.ui; 103 const { isDarkThemeActive } = stores.ui;
65 const { router, user } = stores; 104 const { router, user } = stores;
66 const { intl } = this.context; 105 const { intl } = this.context;
106 const isLoggedIn = Boolean(localStorage.getItem('authToken'));
107 const isUsingWithoutAccount = stores.settings.app.server === LOCAL_SERVER;
67 108
68 return ( 109 return (
69 <div className="settings-navigation"> 110 <div className="settings-navigation">
@@ -128,19 +169,21 @@ export default @inject('stores') @observer class SettingsNavigation extends Comp
128 {intl.formatMessage(messages.settings)} 169 {intl.formatMessage(messages.settings)}
129 </Link> 170 </Link>
130 <Link 171 <Link
131 to="/settings/invite" 172 to="/settings/support"
132 className="settings-navigation__link" 173 className="settings-navigation__link"
133 activeClassName="is-active" 174 activeClassName="is-active"
134 > 175 >
135 {intl.formatMessage(messages.inviteFriends)} 176 {intl.formatMessage(messages.supportFerdi)}
136 </Link> 177 </Link>
137 <span className="settings-navigation__expander" /> 178 <span className="settings-navigation__expander" />
138 <Link 179 <button
139 to="/auth/logout" 180 type="button"
181 to={isLoggedIn ? '/auth/logout' : '/auth/welcome'}
140 className="settings-navigation__link" 182 className="settings-navigation__link"
183 onClick={this.handleLoginLogout.bind(this)}
141 > 184 >
142 {intl.formatMessage(messages.logout)} 185 { isLoggedIn && !isUsingWithoutAccount ? intl.formatMessage(messages.logout) : 'Login'}
143 </Link> 186 </button>
144 </div> 187 </div>
145 ); 188 );
146 } 189 }
diff --git a/src/components/settings/services/EditServiceForm.js b/src/components/settings/services/EditServiceForm.js
index 5cde0db8e..fa34ac60b 100644
--- a/src/components/settings/services/EditServiceForm.js
+++ b/src/components/settings/services/EditServiceForm.js
@@ -29,6 +29,10 @@ const messages = defineMessages({
29 id: 'settings.service.form.deleteButton', 29 id: 'settings.service.form.deleteButton',
30 defaultMessage: '!!!Delete Service', 30 defaultMessage: '!!!Delete Service',
31 }, 31 },
32 openDarkmodeCss: {
33 id: 'settings.service.form.openDarkmodeCss',
34 defaultMessage: '!!!Open darkmode.css',
35 },
32 availableServices: { 36 availableServices: {
33 id: 'settings.service.form.availableServices', 37 id: 'settings.service.form.availableServices',
34 defaultMessage: '!!!Available services', 38 defaultMessage: '!!!Available services',
@@ -63,7 +67,7 @@ const messages = defineMessages({
63 }, 67 },
64 customUrlPremiumInfo: { 68 customUrlPremiumInfo: {
65 id: 'settings.service.form.customUrlPremiumInfo', 69 id: 'settings.service.form.customUrlPremiumInfo',
66 defaultMessage: '!!!To add self hosted services, you need a Franz Premium Supporter Account.', 70 defaultMessage: '!!!To add self hosted services, you need a Ferdi Premium Supporter Account.',
67 }, 71 },
68 customUrlUpgradeAccount: { 72 customUrlUpgradeAccount: {
69 id: 'settings.service.form.customUrlUpgradeAccount', 73 id: 'settings.service.form.customUrlUpgradeAccount',
@@ -103,11 +107,11 @@ const messages = defineMessages({
103 }, 107 },
104 proxyRestartInfo: { 108 proxyRestartInfo: {
105 id: 'settings.service.form.proxy.restartInfo', 109 id: 'settings.service.form.proxy.restartInfo',
106 defaultMessage: '!!!Please restart Franz after changing proxy Settings.', 110 defaultMessage: '!!!Please restart Ferdi after changing proxy Settings.',
107 }, 111 },
108 proxyInfo: { 112 proxyInfo: {
109 id: 'settings.service.form.proxy.info', 113 id: 'settings.service.form.proxy.info',
110 defaultMessage: '!!!Proxy settings will not be synchronized with the Franz servers.', 114 defaultMessage: '!!!Proxy settings will not be synchronized with the Ferdi servers.',
111 }, 115 },
112}); 116});
113 117
@@ -127,6 +131,8 @@ export default @observer class EditServiceForm extends Component {
127 form: PropTypes.instanceOf(Form).isRequired, 131 form: PropTypes.instanceOf(Form).isRequired,
128 onSubmit: PropTypes.func.isRequired, 132 onSubmit: PropTypes.func.isRequired,
129 onDelete: PropTypes.func.isRequired, 133 onDelete: PropTypes.func.isRequired,
134 openDarkmodeCss: PropTypes.func.isRequired,
135 isOpeningDarkModeCss: PropTypes.bool.isRequired,
130 isSaving: PropTypes.bool.isRequired, 136 isSaving: PropTypes.bool.isRequired,
131 isDeleting: PropTypes.bool.isRequired, 137 isDeleting: PropTypes.bool.isRequired,
132 isProxyFeatureEnabled: PropTypes.bool.isRequired, 138 isProxyFeatureEnabled: PropTypes.bool.isRequired,
@@ -155,7 +161,7 @@ export default @observer class EditServiceForm extends Component {
155 const values = form.values(); 161 const values = form.values();
156 let isValid = true; 162 let isValid = true;
157 163
158 const files = form.$('customIcon').files; 164 const { files } = form.$('customIcon');
159 if (files) { 165 if (files) {
160 values.iconFile = files[0]; 166 values.iconFile = files[0];
161 } 167 }
@@ -193,6 +199,8 @@ export default @observer class EditServiceForm extends Component {
193 isSaving, 199 isSaving,
194 isDeleting, 200 isDeleting,
195 onDelete, 201 onDelete,
202 openDarkmodeCss,
203 isOpeningDarkModeCss,
196 isProxyFeatureEnabled, 204 isProxyFeatureEnabled,
197 isServiceProxyIncludedInCurrentPlan, 205 isServiceProxyIncludedInCurrentPlan,
198 isSpellcheckerIncludedInCurrentPlan, 206 isSpellcheckerIncludedInCurrentPlan,
@@ -218,6 +226,23 @@ export default @observer class EditServiceForm extends Component {
218 /> 226 />
219 ); 227 );
220 228
229 const openDarkmodeCssButton = isOpeningDarkModeCss ? (
230 <Button
231 label={intl.formatMessage(messages.openDarkmodeCss)}
232 loaded={false}
233 buttonType="secondary"
234 className="settings__open-dark-mode-button"
235 disabled
236 />
237 ) : (
238 <Button
239 buttonType="secondary"
240 label={intl.formatMessage(messages.openDarkmodeCss)}
241 className="settings__open-dark-mode-button"
242 onClick={openDarkmodeCss}
243 />
244 );
245
221 let activeTabIndex = 0; 246 let activeTabIndex = 0;
222 if (recipe.hasHostedOption && service.team) { 247 if (recipe.hasHostedOption && service.team) {
223 activeTabIndex = 1; 248 activeTabIndex = 1;
@@ -303,6 +328,18 @@ export default @observer class EditServiceForm extends Component {
303 )} 328 )}
304 </Tabs> 329 </Tabs>
305 )} 330 )}
331
332 {recipe.message && (
333 <p
334 className="settings__message"
335 style={{
336 marginTop: 0,
337 }}
338 >
339 <span className="mdi mdi-information" />
340 {recipe.message}
341 </p>
342 )}
306 <div className="service-flex-grid"> 343 <div className="service-flex-grid">
307 <div className="settings__options"> 344 <div className="settings__options">
308 <div className="settings__settings-group"> 345 <div className="settings__settings-group">
@@ -329,9 +366,7 @@ export default @observer class EditServiceForm extends Component {
329 366
330 <div className="settings__settings-group"> 367 <div className="settings__settings-group">
331 <h3>{intl.formatMessage(messages.headlineGeneral)}</h3> 368 <h3>{intl.formatMessage(messages.headlineGeneral)}</h3>
332 {recipe.hasDarkMode && ( 369 <Toggle field={form.$('isDarkModeEnabled')} />
333 <Toggle field={form.$('isDarkModeEnabled')} />
334 )}
335 <Toggle field={form.$('isEnabled')} /> 370 <Toggle field={form.$('isEnabled')} />
336 </div> 371 </div>
337 </div> 372 </div>
@@ -394,18 +429,12 @@ export default @observer class EditServiceForm extends Component {
394 </div> 429 </div>
395 </PremiumFeatureContainer> 430 </PremiumFeatureContainer>
396 )} 431 )}
397
398 {recipe.message && (
399 <p className="settings__message">
400 <span className="mdi mdi-information" />
401 {recipe.message}
402 </p>
403 )}
404 </form> 432 </form>
405 </div> 433 </div>
406 <div className="settings__controls"> 434 <div className="settings__controls">
407 {/* Delete Button */} 435 {/* Delete Button */}
408 {action === 'edit' && deleteButton} 436 {action === 'edit' && deleteButton}
437 {action === 'edit' && openDarkmodeCssButton}
409 438
410 {/* Save Button */} 439 {/* Save Button */}
411 {isSaving || isValidatingCustomUrl ? ( 440 {isSaving || isValidatingCustomUrl ? (
diff --git a/src/components/settings/settings/EditSettingsForm.js b/src/components/settings/settings/EditSettingsForm.js
index 0b69f7514..2be5c4ed7 100644
--- a/src/components/settings/settings/EditSettingsForm.js
+++ b/src/components/settings/settings/EditSettingsForm.js
@@ -9,9 +9,19 @@ import Button from '../../ui/Button';
9import Toggle from '../../ui/Toggle'; 9import Toggle from '../../ui/Toggle';
10import Select from '../../ui/Select'; 10import Select from '../../ui/Select';
11import PremiumFeatureContainer from '../../ui/PremiumFeatureContainer'; 11import PremiumFeatureContainer from '../../ui/PremiumFeatureContainer';
12import Input from '../../ui/Input';
12 13
13import { FRANZ_TRANSLATION } from '../../../config'; 14import { FRANZ_TRANSLATION } from '../../../config';
14 15
16function escapeHtml(unsafe) {
17 return unsafe
18 .replace(/&/g, '&amp;')
19 .replace(/</g, '&lt;')
20 .replace(/>/g, '&gt;')
21 .replace(/"/g, '&quot;')
22 .replace(/'/g, '&#039;');
23}
24
15const messages = defineMessages({ 25const messages = defineMessages({
16 headline: { 26 headline: {
17 id: 'settings.app.headline', 27 id: 'settings.app.headline',
@@ -21,6 +31,42 @@ const messages = defineMessages({
21 id: 'settings.app.headlineGeneral', 31 id: 'settings.app.headlineGeneral',
22 defaultMessage: '!!!General', 32 defaultMessage: '!!!General',
23 }, 33 },
34 hibernateInfo: {
35 id: 'settings.app.hibernateInfo',
36 defaultMessage: '!!!By default, Ferdi will keep all your services open and loaded in the background so they are ready when you want to use them. Service Hibernation will unload your services after a specified amount. This is useful to save RAM or keeping services from slowing down your computer.',
37 },
38 serverInfo: {
39 id: 'settings.app.serverInfo',
40 defaultMessage: '!!!We advice you to logout after changing your server as your settings might not be saved otherwise.',
41 },
42 serverMoneyInfo: {
43 id: 'settings.app.serverMoneyInfo',
44 defaultMessage: '!!!You are using the official Franz Server for Ferdi.\nWe know that Ferdi allows you to use all its features for free but you are still using Franz\'s server resources - which Franz\'s creator has to pay for.\nPlease still consider [Link 1]paying for a Franz account[/Link] or [Link 2]using a self-hosted ferdi-server[/Link] (if you have the knowledge and resources to do so). \nBy using Ferdi, you still profit greatly from Franz\'s recipe store, server resources and its development.',
45 },
46 todoServerInfo: {
47 id: 'settings.app.todoServerInfo',
48 defaultMessage: '!!!This server will be used for the "Franz Todo" feature. (default: https://app.franztodos.com)',
49 },
50 lockedPassword: {
51 id: 'settings.app.lockedPassword',
52 defaultMessage: '!!!Ferdi Lock Password',
53 },
54 lockedPasswordInfo: {
55 id: 'settings.app.lockedPasswordInfo',
56 defaultMessage: '!!!Please make sure to set a password you\'ll remember.\nIf you loose this password, you will have to reinstall Ferdi.',
57 },
58 lockInfo: {
59 id: 'settings.app.lockInfo',
60 defaultMessage: '!!!Ferdi password lock allows you to keep your messages protected.\nUsing Ferdi 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.',
61 },
62 scheduledDNDTimeInfo: {
63 id: 'settings.app.scheduledDNDTimeInfo',
64 defaultMessage: '!!!Times in 24-Hour-Format. End time can be before start time (e.g. start 17:00, end 09:00) to enable Do-not-Disturb overnight.',
65 },
66 scheduledDNDInfo: {
67 id: 'settings.app.scheduledDNDInfo',
68 defaultMessage: '!!!Scheduled Do-not-Disturb allows you to define a period of time in which you do not want to get Notifications from Ferdi.',
69 },
24 headlineLanguage: { 70 headlineLanguage: {
25 id: 'settings.app.headlineLanguage', 71 id: 'settings.app.headlineLanguage',
26 defaultMessage: '!!!Language', 72 defaultMessage: '!!!Language',
@@ -33,13 +79,21 @@ const messages = defineMessages({
33 id: 'settings.app.headlineAppearance', 79 id: 'settings.app.headlineAppearance',
34 defaultMessage: '!!!Appearance', 80 defaultMessage: '!!!Appearance',
35 }, 81 },
82 universalDarkModeInfo: {
83 id: 'settings.app.universalDarkModeInfo',
84 defaultMessage: '!!!Universal Dark Mode tries to dynamically generate dark mode styles for services that are otherwise not currently supported.',
85 },
86 accentColorInfo: {
87 id: 'settings.app.accentColorInfo',
88 defaultMessage: '!!!Write your accent color in a CSS-compatible format. (Default: #7367f0)',
89 },
36 headlineAdvanced: { 90 headlineAdvanced: {
37 id: 'settings.app.headlineAdvanced', 91 id: 'settings.app.headlineAdvanced',
38 defaultMessage: '!!!Advanced', 92 defaultMessage: '!!!Advanced',
39 }, 93 },
40 translationHelp: { 94 translationHelp: {
41 id: 'settings.app.translationHelp', 95 id: 'settings.app.translationHelp',
42 defaultMessage: '!!!Help us to translate Franz into your language.', 96 defaultMessage: '!!!Help us to translate Ferdi into your language.',
43 }, 97 },
44 subheadlineCache: { 98 subheadlineCache: {
45 id: 'settings.app.subheadlineCache', 99 id: 'settings.app.subheadlineCache',
@@ -47,7 +101,7 @@ const messages = defineMessages({
47 }, 101 },
48 cacheInfo: { 102 cacheInfo: {
49 id: 'settings.app.cacheInfo', 103 id: 'settings.app.cacheInfo',
50 defaultMessage: '!!!Franz cache is currently using {size} of disk space.', 104 defaultMessage: '!!!Ferdi cache is currently using {size} of disk space.',
51 }, 105 },
52 buttonClearAllCache: { 106 buttonClearAllCache: {
53 id: 'settings.app.buttonClearAllCache', 107 id: 'settings.app.buttonClearAllCache',
@@ -71,7 +125,7 @@ const messages = defineMessages({
71 }, 125 },
72 updateStatusUpToDate: { 126 updateStatusUpToDate: {
73 id: 'settings.app.updateStatusUpToDate', 127 id: 'settings.app.updateStatusUpToDate',
74 defaultMessage: '!!!You are using the latest version of Franz', 128 defaultMessage: '!!!You are using the latest version of Ferdi',
75 }, 129 },
76 currentVersion: { 130 currentVersion: {
77 id: 'settings.app.currentVersion', 131 id: 'settings.app.currentVersion',
@@ -103,6 +157,11 @@ export default @observer class EditSettingsForm extends Component {
103 isSpellcheckerIncludedInCurrentPlan: PropTypes.bool.isRequired, 157 isSpellcheckerIncludedInCurrentPlan: PropTypes.bool.isRequired,
104 isTodosEnabled: PropTypes.bool.isRequired, 158 isTodosEnabled: PropTypes.bool.isRequired,
105 isWorkspaceEnabled: PropTypes.bool.isRequired, 159 isWorkspaceEnabled: PropTypes.bool.isRequired,
160 server: PropTypes.string.isRequired,
161 noUpdates: PropTypes.bool.isRequired,
162 hibernationEnabled: PropTypes.bool.isRequired,
163 isDarkmodeEnabled: PropTypes.bool.isRequired,
164 openProcessManager: PropTypes.func.isRequired,
106 }; 165 };
107 166
108 static contextTypes = { 167 static contextTypes = {
@@ -135,6 +194,11 @@ export default @observer class EditSettingsForm extends Component {
135 isSpellcheckerIncludedInCurrentPlan, 194 isSpellcheckerIncludedInCurrentPlan,
136 isTodosEnabled, 195 isTodosEnabled,
137 isWorkspaceEnabled, 196 isWorkspaceEnabled,
197 server,
198 noUpdates,
199 hibernationEnabled,
200 isDarkmodeEnabled,
201 openProcessManager,
138 } = this.props; 202 } = this.props;
139 const { intl } = this.context; 203 const { intl } = this.context;
140 204
@@ -147,6 +211,13 @@ export default @observer class EditSettingsForm extends Component {
147 updateButtonLabelMessage = messages.buttonSearchForUpdate; 211 updateButtonLabelMessage = messages.buttonSearchForUpdate;
148 } 212 }
149 213
214 const isLoggedIn = Boolean(localStorage.getItem('authToken'));
215
216 const {
217 lockingFeatureEnabled,
218 scheduledDNDEnabled,
219 } = window.ferdi.stores.settings.all.app;
220
150 return ( 221 return (
151 <div className="settings__main"> 222 <div className="settings__main">
152 <div className="settings__header"> 223 <div className="settings__header">
@@ -163,21 +234,177 @@ export default @observer class EditSettingsForm extends Component {
163 <Toggle field={form.$('autoLaunchOnStart')} /> 234 <Toggle field={form.$('autoLaunchOnStart')} />
164 <Toggle field={form.$('runInBackground')} /> 235 <Toggle field={form.$('runInBackground')} />
165 <Toggle field={form.$('enableSystemTray')} /> 236 <Toggle field={form.$('enableSystemTray')} />
237 <Toggle field={form.$('privateNotifications')} />
238 <Toggle field={form.$('showServiceNavigationBar')} />
239 <Toggle field={form.$('hibernate')} />
240 {hibernationEnabled && (
241 <Select field={form.$('hibernationStrategy')} />
242 )}
243 <p
244 className="settings__message"
245 style={{
246 borderTop: 0, marginTop: 0, paddingTop: 0, marginBottom: '2rem',
247 }}
248 >
249 <span>
250 { intl.formatMessage(messages.hibernateInfo) }
251 </span>
252 </p>
166 {process.platform === 'win32' && ( 253 {process.platform === 'win32' && (
167 <Toggle field={form.$('minimizeToSystemTray')} /> 254 <Toggle field={form.$('minimizeToSystemTray')} />
168 )} 255 )}
256 <Input
257 placeholder="Server"
258 onChange={e => this.submit(e)}
259 field={form.$('server')}
260 autoFocus
261 />
262 {isLoggedIn && (
263 <p>{ intl.formatMessage(messages.serverInfo) }</p>
264 )}
265 {server === 'https://api.franzinfra.com' && (
266 <p
267 className="settings__message"
268 style={{
269 borderTop: 0, marginTop: 0, paddingTop: 0, marginBottom: '2rem',
270 }}
271 >
272 <span
273 dangerouslySetInnerHTML={{
274 __html:
275 // Needed to make links work
276 escapeHtml(
277 intl.formatMessage(messages.serverMoneyInfo),
278 ).replace('[Link 1]', '<a href="https://www.meetfranz.com/pricing" target="_blank">')
279 .replace('[Link 2]', '<a href="https://github.com/getferdi/server" target="_blank">')
280 .replace(/\[\/Link]/g, '</a>'),
281 }}
282 style={{
283 whiteSpace: 'pre-wrap',
284 }}
285 />
286 </p>
287 )}
169 {isWorkspaceEnabled && ( 288 {isWorkspaceEnabled && (
170 <Toggle field={form.$('keepAllWorkspacesLoaded')} /> 289 <Toggle field={form.$('keepAllWorkspacesLoaded')} />
171 )} 290 )}
172 {isTodosEnabled && ( 291 {isTodosEnabled && (
173 <Toggle field={form.$('enableTodos')} /> 292 <>
293 <Toggle field={form.$('enableTodos')} />
294 <Input
295 placeholder="Todo Server"
296 onChange={e => this.submit(e)}
297 field={form.$('todoServer')}
298 />
299 <p>{ intl.formatMessage(messages.todoServerInfo) }</p>
300 </>
174 )} 301 )}
175 302
303 <Toggle field={form.$('lockingFeatureEnabled')} />
304 {lockingFeatureEnabled && (
305 <>
306 <Input
307 placeholder={intl.formatMessage(messages.lockedPassword)}
308 onChange={e => this.submit(e)}
309 field={form.$('lockedPassword')}
310 type="password"
311 scorePassword
312 showPasswordToggle
313 />
314 <p>
315 { intl.formatMessage(messages.lockedPasswordInfo) }
316 </p>
317 </>
318 )}
319 <p
320 className="settings__message"
321 style={{
322 borderTop: 0, marginTop: 0, paddingTop: 0, marginBottom: '2rem',
323 }}
324 >
325 <span>
326 { intl.formatMessage(messages.lockInfo) }
327 </span>
328 </p>
329
330
331 <Toggle field={form.$('scheduledDNDEnabled')} />
332 {scheduledDNDEnabled && (
333 <>
334 <div style={{
335 display: 'flex',
336 justifyContent: 'center',
337 }}
338 >
339 <div style={{
340 padding: '0 1rem',
341 width: '100%',
342 }}
343 >
344 <Input
345 placeholder="17:00"
346 onChange={e => this.submit(e)}
347 field={form.$('scheduledDNDStart')}
348 type="time"
349 />
350 </div>
351 <div style={{
352 padding: '0 1rem',
353 width: '100%',
354 }}
355 >
356 <Input
357 placeholder="09:00"
358 onChange={e => this.submit(e)}
359 field={form.$('scheduledDNDEnd')}
360 type="time"
361 />
362 </div>
363 </div>
364 <p>
365 { intl.formatMessage(messages.scheduledDNDTimeInfo) }
366 </p>
367 </>
368 )}
369 <p
370 className="settings__message"
371 style={{
372 borderTop: 0, marginTop: 0, paddingTop: 0, marginBottom: '2rem',
373 }}
374 >
375 <span>
376 { intl.formatMessage(messages.scheduledDNDInfo) }
377 </span>
378 </p>
379
380
176 {/* Appearance */} 381 {/* Appearance */}
177 <h2 id="apperance">{intl.formatMessage(messages.headlineAppearance)}</h2> 382 <h2 id="apperance">{intl.formatMessage(messages.headlineAppearance)}</h2>
178 <Toggle field={form.$('showDisabledServices')} /> 383 <Toggle field={form.$('showDisabledServices')} />
179 <Toggle field={form.$('showMessageBadgeWhenMuted')} /> 384 <Toggle field={form.$('showMessageBadgeWhenMuted')} />
180 <Toggle field={form.$('darkMode')} /> 385 <Toggle field={form.$('darkMode')} />
386 {isDarkmodeEnabled && (
387 <>
388 <Toggle field={form.$('universalDarkMode')} />
389 <p
390 className="settings__message"
391 style={{
392 borderTop: 0, marginTop: 0, paddingTop: 0, marginBottom: '2rem',
393 }}
394 >
395 <span>
396 { intl.formatMessage(messages.universalDarkModeInfo) }
397 </span>
398 </p>
399 </>
400 )}
401
402 <Input
403 placeholder="Accent Color"
404 onChange={e => this.submit(e)}
405 field={form.$('accentColor')}
406 />
407 <p>{intl.formatMessage(messages.accentColorInfo)}</p>
181 408
182 {/* Language */} 409 {/* Language */}
183 <h2 id="language">{intl.formatMessage(messages.headlineLanguage)}</h2> 410 <h2 id="language">{intl.formatMessage(messages.headlineLanguage)}</h2>
@@ -227,6 +454,16 @@ export default @observer class EditSettingsForm extends Component {
227 loaded={!isClearingAllCache} 454 loaded={!isClearingAllCache}
228 /> 455 />
229 </p> 456 </p>
457 <div style={{
458 marginTop: 20,
459 }}
460 >
461 <Button
462 buttonType="secondary"
463 label="Open Process Manager"
464 onClick={openProcessManager}
465 />
466 </div>
230 </div> 467 </div>
231 468
232 {/* Updates */} 469 {/* Updates */}
@@ -241,7 +478,7 @@ export default @observer class EditSettingsForm extends Component {
241 buttonType="secondary" 478 buttonType="secondary"
242 label={intl.formatMessage(updateButtonLabelMessage)} 479 label={intl.formatMessage(updateButtonLabelMessage)}
243 onClick={checkForUpdates} 480 onClick={checkForUpdates}
244 disabled={isCheckingForUpdates || isUpdateAvailable} 481 disabled={noUpdates || isCheckingForUpdates || isUpdateAvailable}
245 loaded={!isCheckingForUpdates || !isUpdateAvailable} 482 loaded={!isCheckingForUpdates || !isUpdateAvailable}
246 /> 483 />
247 )} 484 )}
@@ -250,6 +487,7 @@ export default @observer class EditSettingsForm extends Component {
250 )} 487 )}
251 <br /> 488 <br />
252 <Toggle field={form.$('beta')} /> 489 <Toggle field={form.$('beta')} />
490 <Toggle field={form.$('noUpdates')} />
253 {intl.formatMessage(messages.currentVersion)} 491 {intl.formatMessage(messages.currentVersion)}
254 {' '} 492 {' '}
255 {remote.app.getVersion()} 493 {remote.app.getVersion()}
@@ -257,6 +495,18 @@ export default @observer class EditSettingsForm extends Component {
257 <span className="mdi mdi-information" /> 495 <span className="mdi mdi-information" />
258 {intl.formatMessage(messages.languageDisclaimer)} 496 {intl.formatMessage(messages.languageDisclaimer)}
259 </p> 497 </p>
498 <p className="settings__message">
499 <span className="mdi mdi-github-face" />
500 <span>
501 Ferdi is based on
502 {' '}
503 <a href="https://github.com/meetfranz/franz" target="_blank">Franz</a>
504 , a project published
505 under the
506 {' '}
507 <a href="https://github.com/meetfranz/franz/blob/master/LICENSE" target="_blank">Apache-2.0 License</a>
508 </span>
509 </p>
260 </form> 510 </form>
261 </div> 511 </div>
262 </div> 512 </div>
diff --git a/src/components/settings/supportFerdi/SupportFerdiDashboard.js b/src/components/settings/supportFerdi/SupportFerdiDashboard.js
new file mode 100644
index 000000000..57920a4a2
--- /dev/null
+++ b/src/components/settings/supportFerdi/SupportFerdiDashboard.js
@@ -0,0 +1,73 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { defineMessages, intlShape } from 'react-intl';
4
5import Button from '../../ui/Button';
6
7const messages = defineMessages({
8 headline: {
9 id: 'settings.supportFerdi.headline',
10 defaultMessage: '!!!Support Ferdi',
11 },
12 title: {
13 id: 'settings.supportFerdi.title',
14 defaultMessage: '!!!Do you like Ferdi? Spread the love!',
15 },
16 github: {
17 id: 'settings.supportFerdi.github',
18 defaultMessage: '!!!Star on GitHub',
19 },
20 share: {
21 id: 'settings.supportFerdi.share',
22 defaultMessage: '!!!Tell your Friends',
23 },
24 openCollective: {
25 id: 'settings.supportFerdi.openCollective',
26 defaultMessage: '!!!Support our Open Collective',
27 },
28});
29
30class SupportFerdiDashboard extends Component {
31 static contextTypes = {
32 intl: intlShape,
33 };
34
35 static propTypes = {
36 openLink: PropTypes.func.isRequired,
37 };
38
39 render() {
40 const { openLink } = this.props;
41 const { intl } = this.context;
42
43 return (
44 <div className="settings__main">
45 <div className="settings__header">
46 <span className="settings__header-item">
47 {intl.formatMessage(messages.headline)}
48 </span>
49 </div>
50 <div className="settings__body">
51 <h1>{intl.formatMessage(messages.title)}</h1>
52 <Button
53 label={intl.formatMessage(messages.github)}
54 className="franz-form__button--inverted franz-form__button--large"
55 onClick={() => openLink('https://github.com/getferdi/ferdi')}
56 />
57 <Button
58 label={intl.formatMessage(messages.share)}
59 className="franz-form__button--inverted franz-form__button--large"
60 onClick={() => openLink('https://twitter.com/intent/tweet?text=Ferdi%3A%20A%20messaging%20browser%20that%20allows%20you%20to%20combine%20your%20favourite%20messaging%20services%20into%20one%20application.%0A%0ACheck%20out%20Ferdi%20at%20https%3A//getferdi.com')}
61 />
62 <Button
63 label={intl.formatMessage(messages.openCollective)}
64 className="franz-form__button--inverted franz-form__button--large"
65 onClick={() => openLink('https://opencollective.com/getferdi')}
66 />
67 </div>
68 </div>
69 );
70 }
71}
72
73export default SupportFerdiDashboard;
diff --git a/src/components/settings/team/TeamDashboard.js b/src/components/settings/team/TeamDashboard.js
index 366b0113a..7e6d93997 100644
--- a/src/components/settings/team/TeamDashboard.js
+++ b/src/components/settings/team/TeamDashboard.js
@@ -20,7 +20,7 @@ const messages = defineMessages({
20 }, 20 },
21 contentHeadline: { 21 contentHeadline: {
22 id: 'settings.team.contentHeadline', 22 id: 'settings.team.contentHeadline',
23 defaultMessage: '!!!Franz for Teams', 23 defaultMessage: '!!!Ferdi for Teams',
24 }, 24 },
25 intro: { 25 intro: {
26 id: 'settings.team.intro', 26 id: 'settings.team.intro',
@@ -28,7 +28,7 @@ const messages = defineMessages({
28 }, 28 },
29 copy: { 29 copy: {
30 id: 'settings.team.copy', 30 id: 'settings.team.copy',
31 defaultMessage: '!!!Franz for Teams gives you the option to invite co-workers to your team by sending them email invitations and manage their subscriptions in your account’s preferences. Don’t waste time setting up subscriptions for every team member individually, forget about multiple invoices and different billing cycles - one team to rule them all!', 31 defaultMessage: '!!!Ferdi for Teams gives you the option to invite co-workers to your team by sending them email invitations and manage their subscriptions in your account’s preferences. Don’t waste time setting up subscriptions for every team member individually, forget about multiple invoices and different billing cycles - one team to rule them all!',
32 }, 32 },
33 manageButton: { 33 manageButton: {
34 id: 'settings.team.manageAction', 34 id: 'settings.team.manageAction',
@@ -38,6 +38,14 @@ const messages = defineMessages({
38 id: 'settings.team.upgradeAction', 38 id: 'settings.team.upgradeAction',
39 defaultMessage: '!!!Upgrade your Account', 39 defaultMessage: '!!!Upgrade your Account',
40 }, 40 },
41 teamsUnavailable: {
42 id: 'settings.team.teamsUnavailable',
43 defaultMessage: '!!!Teams are unavailable',
44 },
45 teamsUnavailableInfo: {
46 id: 'settings.team.teamsUnavailableInfo',
47 defaultMessage: '!!!Teams are currently only available when using the Franz Server and after paying for Franz Professional. Please change your server to https://api.franzinfra.com to use teams.',
48 },
41}); 49});
42 50
43const styles = { 51const styles = {
@@ -98,6 +106,7 @@ export default @injectSheet(styles) @observer class TeamDashboard extends Compon
98 openTeamManagement: PropTypes.func.isRequired, 106 openTeamManagement: PropTypes.func.isRequired,
99 classes: PropTypes.object.isRequired, 107 classes: PropTypes.object.isRequired,
100 isProUser: PropTypes.bool.isRequired, 108 isProUser: PropTypes.bool.isRequired,
109 server: PropTypes.string.isRequired,
101 }; 110 };
102 111
103 static contextTypes = { 112 static contextTypes = {
@@ -112,9 +121,84 @@ export default @injectSheet(styles) @observer class TeamDashboard extends Compon
112 openTeamManagement, 121 openTeamManagement,
113 isProUser, 122 isProUser,
114 classes, 123 classes,
124 server,
115 } = this.props; 125 } = this.props;
116 const { intl } = this.context; 126 const { intl } = this.context;
117 127
128 if (server === 'https://api.franzinfra.com') {
129 return (
130 <div className="settings__main">
131 <div className="settings__header">
132 <span className="settings__header-item">
133 {intl.formatMessage(messages.headline)}
134 </span>
135 </div>
136 <div className="settings__body">
137 {isLoading && (
138 <Loader />
139 )}
140
141 {!isLoading && userInfoRequestFailed && (
142 <Infobox
143 icon="alert"
144 type="danger"
145 ctaLabel={intl.formatMessage(messages.tryReloadUserInfoRequest)}
146 ctaLoading={isLoading}
147 ctaOnClick={retryUserInfoRequest}
148 >
149 {intl.formatMessage(messages.userInfoRequestFailed)}
150 </Infobox>
151 )}
152
153 {!userInfoRequestFailed && (
154 <>
155 {!isLoading && (
156 <>
157 <>
158 <h1 className={classnames({
159 [classes.headline]: true,
160 [classes.headlineWithSpacing]: isProUser,
161 })}
162 >
163 {intl.formatMessage(messages.contentHeadline)}
164
165 </h1>
166 {!isProUser && (
167 <Badge className={classes.proRequired}>{intl.formatMessage(globalMessages.proRequired)}</Badge>
168 )}
169 <div className={classes.container}>
170 <div className={classes.content}>
171 <p>{intl.formatMessage(messages.intro)}</p>
172 <p>{intl.formatMessage(messages.copy)}</p>
173 </div>
174 <img className={classes.image} src="https://cdn.franzinfra.com/announcements/assets/teams.png" alt="Franz for Teams" />
175 </div>
176 <div className={classes.buttonContainer}>
177 {!isProUser ? (
178 <UpgradeButton
179 className={classes.cta}
180 gaEventInfo={{ category: 'Todos', event: 'upgrade' }}
181 requiresPro
182 short
183 />
184 ) : (
185 <Button
186 label={intl.formatMessage(messages.manageButton)}
187 onClick={openTeamManagement}
188 className={classes.cta}
189 />
190 )}
191 </div>
192 </>
193 </>
194 )}
195 </>
196 )}
197 </div>
198 <ReactTooltip place="right" type="dark" effect="solid" />
199 </div>
200 );
201 }
118 return ( 202 return (
119 <div className="settings__main"> 203 <div className="settings__main">
120 <div className="settings__header"> 204 <div className="settings__header">
@@ -123,68 +207,11 @@ export default @injectSheet(styles) @observer class TeamDashboard extends Compon
123 </span> 207 </span>
124 </div> 208 </div>
125 <div className="settings__body"> 209 <div className="settings__body">
126 {isLoading && ( 210 <h1 className={classes.headline}>
127 <Loader /> 211 {intl.formatMessage(messages.teamsUnavailable)}
128 )} 212 </h1>
129 213 {intl.formatMessage(messages.teamsUnavailableInfo)}
130 {!isLoading && userInfoRequestFailed && (
131 <Infobox
132 icon="alert"
133 type="danger"
134 ctaLabel={intl.formatMessage(messages.tryReloadUserInfoRequest)}
135 ctaLoading={isLoading}
136 ctaOnClick={retryUserInfoRequest}
137 >
138 {intl.formatMessage(messages.userInfoRequestFailed)}
139 </Infobox>
140 )}
141
142 {!userInfoRequestFailed && (
143 <>
144 {!isLoading && (
145 <>
146 <>
147 <h1 className={classnames({
148 [classes.headline]: true,
149 [classes.headlineWithSpacing]: isProUser,
150 })}
151 >
152 {intl.formatMessage(messages.contentHeadline)}
153
154 </h1>
155 {!isProUser && (
156 <Badge className={classes.proRequired}>{intl.formatMessage(globalMessages.proRequired)}</Badge>
157 )}
158 <div className={classes.container}>
159 <div className={classes.content}>
160 <p>{intl.formatMessage(messages.intro)}</p>
161 <p>{intl.formatMessage(messages.copy)}</p>
162 </div>
163 <img className={classes.image} src="https://cdn.franzinfra.com/announcements/assets/teams.png" alt="Franz for Teams" />
164 </div>
165 <div className={classes.buttonContainer}>
166 {!isProUser ? (
167 <UpgradeButton
168 className={classes.cta}
169 gaEventInfo={{ category: 'Todos', event: 'upgrade' }}
170 requiresPro
171 short
172 />
173 ) : (
174 <Button
175 label={intl.formatMessage(messages.manageButton)}
176 onClick={openTeamManagement}
177 className={classes.cta}
178 />
179 )}
180 </div>
181 </>
182 </>
183 )}
184 </>
185 )}
186 </div> 214 </div>
187 <ReactTooltip place="right" type="dark" effect="solid" />
188 </div> 215 </div>
189 ); 216 );
190 } 217 }
diff --git a/src/components/subscription/SubscriptionForm.js b/src/components/subscription/SubscriptionForm.js
index 5f268a322..ec486e5d0 100644
--- a/src/components/subscription/SubscriptionForm.js
+++ b/src/components/subscription/SubscriptionForm.js
@@ -35,7 +35,7 @@ const styles = () => ({
35 }, 35 },
36}); 36});
37 37
38export default @observer @injectSheet(styles) class SubscriptionForm extends Component { 38export default @injectSheet(styles) @observer class SubscriptionForm extends Component {
39 static propTypes = { 39 static propTypes = {
40 selectPlan: PropTypes.func.isRequired, 40 selectPlan: PropTypes.func.isRequired,
41 isActivatingTrial: PropTypes.bool.isRequired, 41 isActivatingTrial: PropTypes.bool.isRequired,
diff --git a/src/components/subscription/TrialForm.js b/src/components/subscription/TrialForm.js
index f3f3458f3..d61b779ed 100644
--- a/src/components/subscription/TrialForm.js
+++ b/src/components/subscription/TrialForm.js
@@ -57,7 +57,7 @@ const styles = theme => ({
57 }, 57 },
58}); 58});
59 59
60export default @observer @injectSheet(styles) class TrialForm extends Component { 60export default @injectSheet(styles) @observer class TrialForm extends Component {
61 static propTypes = { 61 static propTypes = {
62 activateTrial: PropTypes.func.isRequired, 62 activateTrial: PropTypes.func.isRequired,
63 isActivatingTrial: PropTypes.bool.isRequired, 63 isActivatingTrial: PropTypes.bool.isRequired,
diff --git a/src/components/ui/ActivateTrialButton/index.js b/src/components/ui/ActivateTrialButton/index.js
index e0637da90..340123c2f 100644
--- a/src/components/ui/ActivateTrialButton/index.js
+++ b/src/components/ui/ActivateTrialButton/index.js
@@ -5,7 +5,6 @@ import { defineMessages, intlShape } from 'react-intl';
5import classnames from 'classnames'; 5import classnames from 'classnames';
6 6
7import { Button } from '@meetfranz/forms'; 7import { Button } from '@meetfranz/forms';
8import { gaEvent } from '../../../lib/analytics';
9 8
10import UserStore from '../../../stores/UserStore'; 9import UserStore from '../../../stores/UserStore';
11 10
@@ -63,25 +62,9 @@ class ActivateTrialButton extends Component {
63 }; 62 };
64 63
65 handleCTAClick() { 64 handleCTAClick() {
66 const { actions, stores, gaEventInfo } = this.props; 65 const { actions } = this.props;
67 const { hadSubscription } = stores.user.data;
68 // const { defaultTrialPlan } = stores.features.features;
69
70 let label = '';
71 if (!hadSubscription) {
72 // actions.user.activateTrial({ planId: defaultTrialPlan });
73
74 label = 'Start Trial';
75 } else {
76 label = 'Upgrade Account';
77 }
78 66
79 actions.ui.openSettings({ path: 'user' }); 67 actions.ui.openSettings({ path: 'user' });
80
81 if (gaEventInfo) {
82 const { category, event } = gaEventInfo;
83 gaEvent(category, event, label);
84 }
85 } 68 }
86 69
87 render() { 70 render() {
diff --git a/src/components/ui/AppLoader/index.js b/src/components/ui/AppLoader/index.js
index b0c7fed7b..a7f6f4545 100644
--- a/src/components/ui/AppLoader/index.js
+++ b/src/components/ui/AppLoader/index.js
@@ -9,22 +9,26 @@ import { shuffleArray } from '../../../helpers/array-helpers';
9import styles from './styles'; 9import styles from './styles';
10 10
11const textList = shuffleArray([ 11const textList = shuffleArray([
12 'Looking for Sisi', 12 'Adding free features',
13 'Contacting the herald', 13 'Making application usable',
14 'Saddling the unicorn', 14 'Removing unproductive paywalls',
15 'Learning the Waltz', 15 'Creating custom server software',
16 'Visiting Horst & Grete', 16 'Increasing productivity',
17 'Twisting my moustache', 17 'Listening to our userbase',
18 'Playing the trumpet', 18 'Fixing bugs',
19 'Traveling through space & time',
20]); 19]);
21 20
22export default @injectSheet(styles) @withTheme class AppLoader extends Component { 21export default @injectSheet(styles) @withTheme class AppLoader extends Component {
23 static propTypes = { 22 static propTypes = {
24 classes: PropTypes.object.isRequired, 23 classes: PropTypes.object.isRequired,
25 theme: PropTypes.object.isRequired, 24 theme: PropTypes.object.isRequired,
25 texts: PropTypes.array,
26 }; 26 };
27 27
28 static defaultProps = {
29 texts: textList,
30 }
31
28 state = { 32 state = {
29 step: 0, 33 step: 0,
30 }; 34 };
@@ -44,16 +48,16 @@ export default @injectSheet(styles) @withTheme class AppLoader extends Component
44 } 48 }
45 49
46 render() { 50 render() {
47 const { classes, theme } = this.props; 51 const { classes, theme, texts } = this.props;
48 const { step } = this.state; 52 const { step } = this.state;
49 53
50 return ( 54 return (
51 <FullscreenLoader 55 <FullscreenLoader
52 title="Franz" 56 title="Ferdi"
53 className={classes.component} 57 className={classes.component}
54 spinnerColor={theme.colorAppLoaderSpinner} 58 spinnerColor={theme.colorAppLoaderSpinner}
55 > 59 >
56 {textList.map((text, i) => ( 60 {texts.map((text, i) => (
57 <span 61 <span
58 key={text} 62 key={text}
59 className={classnames({ 63 className={classnames({
diff --git a/src/components/ui/Button.js b/src/components/ui/Button.js
index ffc7f7051..5066b9c06 100644
--- a/src/components/ui/Button.js
+++ b/src/components/ui/Button.js
@@ -1,10 +1,10 @@
1import React, { Component } from 'react'; 1import React, { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer, inject } from 'mobx-react';
4import Loader from 'react-loader'; 4import Loader from 'react-loader';
5import classnames from 'classnames'; 5import classnames from 'classnames';
6 6
7export default @observer class Button extends Component { 7export default @inject('stores') @observer class Button extends Component {
8 static propTypes = { 8 static propTypes = {
9 className: PropTypes.string, 9 className: PropTypes.string,
10 label: PropTypes.string.isRequired, 10 label: PropTypes.string.isRequired,
@@ -14,12 +14,19 @@ export default @observer class Button extends Component {
14 buttonType: PropTypes.string, 14 buttonType: PropTypes.string,
15 loaded: PropTypes.bool, 15 loaded: PropTypes.bool,
16 htmlForm: PropTypes.string, 16 htmlForm: PropTypes.string,
17 stores: PropTypes.shape({
18 settings: PropTypes.shape({
19 app: PropTypes.shape({
20 accentColor: PropTypes.string.isRequired,
21 }).isRequired,
22 }).isRequired,
23 }).isRequired,
17 }; 24 };
18 25
19 static defaultProps = { 26 static defaultProps = {
20 className: null, 27 className: null,
21 disabled: false, 28 disabled: false,
22 onClick: () => {}, 29 onClick: () => { },
23 type: 'button', 30 type: 'button',
24 buttonType: '', 31 buttonType: '',
25 loaded: true, 32 loaded: true,
@@ -69,7 +76,7 @@ export default @observer class Button extends Component {
69 loaded={loaded} 76 loaded={loaded}
70 lines={10} 77 lines={10}
71 scale={0.4} 78 scale={0.4}
72 color={buttonType !== 'secondary' ? '#FFF' : '#373a3c'} 79 color={buttonType !== 'secondary' ? '#FFF' : this.props.stores.settings.app.accentColor}
73 component="span" 80 component="span"
74 /> 81 />
75 {label} 82 {label}
diff --git a/src/components/ui/FullscreenLoader/index.js b/src/components/ui/FullscreenLoader/index.js
index 06dab1eb6..d8cdc2e8a 100644
--- a/src/components/ui/FullscreenLoader/index.js
+++ b/src/components/ui/FullscreenLoader/index.js
@@ -1,6 +1,6 @@
1import React, { Component } from 'react'; 1import React, { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer, inject } from 'mobx-react';
4import injectSheet, { withTheme } from 'react-jss'; 4import injectSheet, { withTheme } from 'react-jss';
5import classnames from 'classnames'; 5import classnames from 'classnames';
6 6
@@ -8,7 +8,7 @@ import Loader from '../Loader';
8 8
9import styles from './styles'; 9import styles from './styles';
10 10
11export default @observer @withTheme @injectSheet(styles) class FullscreenLoader extends Component { 11export default @inject('stores') @withTheme @injectSheet(styles) @observer class FullscreenLoader extends Component {
12 static propTypes = { 12 static propTypes = {
13 className: PropTypes.string, 13 className: PropTypes.string,
14 title: PropTypes.string.isRequired, 14 title: PropTypes.string.isRequired,
@@ -16,6 +16,13 @@ export default @observer @withTheme @injectSheet(styles) class FullscreenLoader
16 theme: PropTypes.object.isRequired, 16 theme: PropTypes.object.isRequired,
17 spinnerColor: PropTypes.string, 17 spinnerColor: PropTypes.string,
18 children: PropTypes.node, 18 children: PropTypes.node,
19 stores: PropTypes.shape({
20 settings: PropTypes.shape({
21 app: PropTypes.shape({
22 accentColor: PropTypes.string.isRequired,
23 }).isRequired,
24 }).isRequired,
25 }).isRequired,
19 }; 26 };
20 27
21 static defaultProps = { 28 static defaultProps = {
@@ -32,10 +39,16 @@ export default @observer @withTheme @injectSheet(styles) class FullscreenLoader
32 spinnerColor, 39 spinnerColor,
33 className, 40 className,
34 theme, 41 theme,
42 stores,
35 } = this.props; 43 } = this.props;
36 44
37 return ( 45 return (
38 <div className={classes.wrapper}> 46 <div
47 className={classes.wrapper}
48 style={{
49 background: stores.app.accentColor,
50 }}
51 >
39 <div 52 <div
40 className={classnames({ 53 className={classnames({
41 [`${classes.component}`]: true, 54 [`${classes.component}`]: true,
diff --git a/src/components/ui/Input.js b/src/components/ui/Input.js
index 9b070c4df..4e3eb4ab8 100644
--- a/src/components/ui/Input.js
+++ b/src/components/ui/Input.js
@@ -68,7 +68,7 @@ export default @observer class Input extends Component {
68 68
69 const { passwordScore } = this.state; 69 const { passwordScore } = this.state;
70 70
71 let type = field.type; 71 let { type } = field;
72 if (type === 'password' && this.state.showPassword) { 72 if (type === 'password' && this.state.showPassword) {
73 type = 'text'; 73 type = 'text';
74 } 74 }
diff --git a/src/components/ui/Link.js b/src/components/ui/Link.js
index b88686d5e..5f729844b 100644
--- a/src/components/ui/Link.js
+++ b/src/components/ui/Link.js
@@ -25,6 +25,7 @@ export default @inject('stores') @observer class Link extends Component {
25 className, 25 className,
26 activeClassName, 26 activeClassName,
27 strictFilter, 27 strictFilter,
28 style,
28 } = this.props; 29 } = this.props;
29 const { router } = stores; 30 const { router } = stores;
30 31
@@ -44,6 +45,7 @@ export default @inject('stores') @observer class Link extends Component {
44 <a 45 <a
45 href={router.history.createHref(to)} 46 href={router.history.createHref(to)}
46 className={linkClasses} 47 className={linkClasses}
48 style={style}
47 onClick={e => this.onClick(e)} 49 onClick={e => this.onClick(e)}
48 > 50 >
49 {children} 51 {children}
@@ -65,6 +67,7 @@ Link.wrappedComponent.propTypes = {
65 activeClassName: PropTypes.string, 67 activeClassName: PropTypes.string,
66 strictFilter: PropTypes.bool, 68 strictFilter: PropTypes.bool,
67 target: PropTypes.string, 69 target: PropTypes.string,
70 style: PropTypes.object,
68}; 71};
69 72
70Link.wrappedComponent.defaultProps = { 73Link.wrappedComponent.defaultProps = {
@@ -72,4 +75,5 @@ Link.wrappedComponent.defaultProps = {
72 activeClassName: '', 75 activeClassName: '',
73 strictFilter: false, 76 strictFilter: false,
74 target: '', 77 target: '',
78 style: {},
75}; 79};
diff --git a/src/components/ui/Loader.js b/src/components/ui/Loader.js
index f73296bb6..4d7113aa1 100644
--- a/src/components/ui/Loader.js
+++ b/src/components/ui/Loader.js
@@ -1,22 +1,30 @@
1import React, { Component } from 'react'; 1import React, { Component } from 'react';
2import { observer, inject } from 'mobx-react';
2import PropTypes from 'prop-types'; 3import PropTypes from 'prop-types';
3import Loader from 'react-loader'; 4import Loader from 'react-loader';
4 5
5import { oneOrManyChildElements } from '../../prop-types'; 6import { oneOrManyChildElements } from '../../prop-types';
6 7
7export default class LoaderComponent extends Component { 8export default @inject('stores') @observer class LoaderComponent extends Component {
8 static propTypes = { 9 static propTypes = {
9 children: oneOrManyChildElements, 10 children: oneOrManyChildElements,
10 loaded: PropTypes.bool, 11 loaded: PropTypes.bool,
11 className: PropTypes.string, 12 className: PropTypes.string,
12 color: PropTypes.string, 13 color: PropTypes.string,
14 stores: PropTypes.shape({
15 settings: PropTypes.shape({
16 app: PropTypes.shape({
17 accentColor: PropTypes.string.isRequired,
18 }).isRequired,
19 }).isRequired,
20 }).isRequired,
13 }; 21 };
14 22
15 static defaultProps = { 23 static defaultProps = {
16 children: null, 24 children: null,
17 loaded: false, 25 loaded: false,
18 className: '', 26 className: '',
19 color: '#373a3c', 27 color: 'ACCENT',
20 }; 28 };
21 29
22 render() { 30 render() {
@@ -24,9 +32,10 @@ export default class LoaderComponent extends Component {
24 children, 32 children,
25 loaded, 33 loaded,
26 className, 34 className,
27 color,
28 } = this.props; 35 } = this.props;
29 36
37 const color = this.props.color !== 'ACCENT' ? this.props.color : this.props.stores.settings.app.accentColor;
38
30 return ( 39 return (
31 <Loader 40 <Loader
32 loaded={loaded} 41 loaded={loaded}
diff --git a/src/components/ui/Modal/index.js b/src/components/ui/Modal/index.js
index 63d858c47..0af521452 100644
--- a/src/components/ui/Modal/index.js
+++ b/src/components/ui/Modal/index.js
@@ -41,6 +41,8 @@ export default @injectCSS(styles) class Modal extends Component {
41 showClose, 41 showClose,
42 } = this.props; 42 } = this.props;
43 43
44 const appRoot = document.getElementById('root');
45
44 return ( 46 return (
45 <ReactModal 47 <ReactModal
46 isOpen={isOpen} 48 isOpen={isOpen}
@@ -53,6 +55,7 @@ export default @injectCSS(styles) class Modal extends Component {
53 portal={portal} 55 portal={portal}
54 onRequestClose={close} 56 onRequestClose={close}
55 shouldCloseOnOverlayClick={shouldCloseOnOverlayClick} 57 shouldCloseOnOverlayClick={shouldCloseOnOverlayClick}
58 appElement={appRoot}
56 > 59 >
57 {showClose && close && ( 60 {showClose && close && (
58 <button 61 <button
diff --git a/src/components/ui/PremiumFeatureContainer/index.js b/src/components/ui/PremiumFeatureContainer/index.js
index f1e526560..611c50468 100644
--- a/src/components/ui/PremiumFeatureContainer/index.js
+++ b/src/components/ui/PremiumFeatureContainer/index.js
@@ -9,8 +9,7 @@ import { oneOrManyChildElements } from '../../../prop-types';
9import UserStore from '../../../stores/UserStore'; 9import UserStore from '../../../stores/UserStore';
10 10
11import styles from './styles'; 11import styles from './styles';
12import { gaEvent } from '../../../lib/analytics'; 12import FeatureStore from '../../../stores/FeaturesStore';
13import FeaturesStore from '../../../stores/FeaturesStore';
14 13
15const messages = defineMessages({ 14const messages = defineMessages({
16 action: { 15 action: {
@@ -50,7 +49,6 @@ class PremiumFeatureContainer extends Component {
50 actions, 49 actions,
51 condition, 50 condition,
52 stores, 51 stores,
53 gaEventInfo,
54 } = this.props; 52 } = this.props;
55 53
56 const { intl } = this.context; 54 const { intl } = this.context;
@@ -75,10 +73,6 @@ class PremiumFeatureContainer extends Component {
75 type="button" 73 type="button"
76 onClick={() => { 74 onClick={() => {
77 actions.ui.openSettings({ path: 'user' }); 75 actions.ui.openSettings({ path: 'user' });
78 if (gaEventInfo) {
79 const { category, event, label } = gaEventInfo;
80 gaEvent(category, event, label);
81 }
82 }} 76 }}
83 > 77 >
84 {intl.formatMessage(messages.action)} 78 {intl.formatMessage(messages.action)}
diff --git a/src/components/ui/UpgradeButton/index.js b/src/components/ui/UpgradeButton/index.js
index 73762f0bf..1b764bd90 100644
--- a/src/components/ui/UpgradeButton/index.js
+++ b/src/components/ui/UpgradeButton/index.js
@@ -4,7 +4,6 @@ import { inject, observer } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl'; 4import { defineMessages, intlShape } from 'react-intl';
5 5
6import { Button } from '@meetfranz/forms'; 6import { Button } from '@meetfranz/forms';
7import { gaEvent } from '../../../lib/analytics';
8 7
9import UserStore from '../../../stores/UserStore'; 8import UserStore from '../../../stores/UserStore';
10import ActivateTrialButton from '../ActivateTrialButton'; 9import ActivateTrialButton from '../ActivateTrialButton';
@@ -41,13 +40,9 @@ class UpgradeButton extends Component {
41 }; 40 };
42 41
43 handleCTAClick() { 42 handleCTAClick() {
44 const { actions, gaEventInfo } = this.props; 43 const { actions } = this.props;
45 44
46 actions.ui.openSettings({ path: 'user' }); 45 actions.ui.openSettings({ path: 'user' });
47 if (gaEventInfo) {
48 const { category, event } = gaEventInfo;
49 gaEvent(category, event, 'Upgrade Account');
50 }
51 } 46 }
52 47
53 render() { 48 render() {
diff --git a/src/components/ui/WebviewLoader/index.js b/src/components/ui/WebviewLoader/index.js
index 58b6b6f1b..923f10327 100644
--- a/src/components/ui/WebviewLoader/index.js
+++ b/src/components/ui/WebviewLoader/index.js
@@ -14,7 +14,7 @@ const messages = defineMessages({
14 }, 14 },
15}); 15});
16 16
17export default @observer @injectSheet(styles) class WebviewLoader extends Component { 17export default @injectSheet(styles) @observer class WebviewLoader extends Component {
18 static propTypes = { 18 static propTypes = {
19 name: PropTypes.string.isRequired, 19 name: PropTypes.string.isRequired,
20 classes: PropTypes.object.isRequired, 20 classes: PropTypes.object.isRequired,