diff options
Diffstat (limited to 'src/components')
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'; | |||
8 | const messages = defineMessages({ | 8 | const 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'; | |||
12 | const messages = defineMessages({ | 12 | const 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 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { observer } from 'mobx-react'; | ||
4 | import { defineMessages, intlShape } from 'react-intl'; | ||
5 | |||
6 | import Form from '../../lib/Form'; | ||
7 | import { required } from '../../helpers/validation-helpers'; | ||
8 | import Input from '../ui/Input'; | ||
9 | import Button from '../ui/Button'; | ||
10 | import Infobox from '../ui/Infobox'; | ||
11 | |||
12 | import { globalError as globalErrorPropType } from '../../prop-types'; | ||
13 | |||
14 | const 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 | |||
37 | export 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 */ | ||
1 | import React, { Component } from 'react'; | 2 | import React, { Component } from 'react'; |
2 | import PropTypes from 'prop-types'; | 3 | import PropTypes from 'prop-types'; |
3 | import { observer } from 'mobx-react'; | 4 | import { observer, inject } from 'mobx-react'; |
4 | import { defineMessages, intlShape } from 'react-intl'; | 5 | import { defineMessages, intlShape } from 'react-intl'; |
5 | 6 | ||
6 | import { isDevMode, useLiveAPI } from '../../environment'; | 7 | import { isDevMode, useLiveAPI } from '../../environment'; |
7 | import Form from '../../lib/Form'; | 8 | import Form from '../../lib/Form'; |
8 | import { required, email } from '../../helpers/validation-helpers'; | 9 | import { required, email } from '../../helpers/validation-helpers'; |
10 | import serverlessLogin from '../../helpers/serverless-helpers'; | ||
9 | import Input from '../ui/Input'; | 11 | import Input from '../ui/Input'; |
10 | import Button from '../ui/Button'; | 12 | import Button from '../ui/Button'; |
11 | import Link from '../ui/Link'; | 13 | import 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 | ||
55 | export default @observer class Login extends Component { | 73 | export 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 | ||
143 | export default @observer @injectSheet(styles) class Signup extends Component { | 143 | export 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 */ | ||
1 | import React, { Component } from 'react'; | 2 | import React, { Component } from 'react'; |
2 | import PropTypes from 'prop-types'; | 3 | import PropTypes from 'prop-types'; |
3 | import { observer } from 'mobx-react'; | 4 | import { observer, inject } from 'mobx-react'; |
4 | import { defineMessages, intlShape } from 'react-intl'; | 5 | import { defineMessages, intlShape } from 'react-intl'; |
5 | 6 | ||
6 | import { isDevMode, useLiveAPI } from '../../environment'; | 7 | import { isDevMode, useLiveAPI } from '../../environment'; |
7 | import Form from '../../lib/Form'; | 8 | import Form from '../../lib/Form'; |
8 | import { required, email, minLength } from '../../helpers/validation-helpers'; | 9 | import { required, email, minLength } from '../../helpers/validation-helpers'; |
10 | import serverlessLogin from '../../helpers/serverless-helpers'; | ||
9 | import Input from '../ui/Input'; | 11 | import Input from '../ui/Input'; |
10 | import Button from '../ui/Button'; | 12 | import Button from '../ui/Button'; |
11 | import Link from '../ui/Link'; | 13 | import 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 | ||
67 | export default @observer class Signup extends Component { | 77 | export 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 | & | 189 | & |
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 */ | ||
1 | import React, { Component } from 'react'; | 2 | import React, { Component } from 'react'; |
2 | import PropTypes from 'prop-types'; | 3 | import PropTypes from 'prop-types'; |
3 | import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; | 4 | import { observer, PropTypes as MobxPropTypes, inject } from 'mobx-react'; |
4 | import { defineMessages, intlShape } from 'react-intl'; | 5 | import { defineMessages, intlShape } from 'react-intl'; |
6 | import serverlessLogin from '../../helpers/serverless-helpers'; | ||
5 | 7 | ||
6 | import Link from '../ui/Link'; | 8 | import 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 | ||
19 | export default @observer class Login extends Component { | 25 | export 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'; | |||
6 | import injectSheet from 'react-jss'; | 6 | import injectSheet from 'react-jss'; |
7 | 7 | ||
8 | import InfoBar from '../ui/InfoBar'; | 8 | import InfoBar from '../ui/InfoBar'; |
9 | import { Component as DelayApp } from '../../features/delayApp'; | ||
10 | import { Component as BasicAuth } from '../../features/basicAuth'; | 9 | import { Component as BasicAuth } from '../../features/basicAuth'; |
11 | import { Component as ShareFranz } from '../../features/shareFranz'; | 10 | import { Component as ShareFranz } from '../../features/shareFranz'; |
11 | import { Component as QuickSwitch } from '../../features/quickSwitch'; | ||
12 | import ErrorBoundary from '../util/ErrorBoundary'; | 12 | import 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 | ||
44 | const styles = theme => ({ | 48 | const 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'; | |||
2 | import PropTypes from 'prop-types'; | 2 | import PropTypes from 'prop-types'; |
3 | import ReactTooltip from 'react-tooltip'; | 3 | import ReactTooltip from 'react-tooltip'; |
4 | import { defineMessages, intlShape } from 'react-intl'; | 4 | import { defineMessages, intlShape } from 'react-intl'; |
5 | import { observer } from 'mobx-react'; | 5 | import { inject, observer } from 'mobx-react'; |
6 | import { Link } from 'react-router'; | ||
6 | 7 | ||
7 | import Tabbar from '../services/tabs/Tabbar'; | 8 | import Tabbar from '../services/tabs/Tabbar'; |
8 | import { ctrlKey } from '../../environment'; | 9 | import { ctrlKey } from '../../environment'; |
9 | import { GA_CATEGORY_WORKSPACES, workspaceStore } from '../../features/workspaces'; | 10 | import { workspaceStore } from '../../features/workspaces'; |
10 | import { gaEvent } from '../../lib/analytics'; | 11 | import { todosStore } from '../../features/todos'; |
11 | import { todosStore, GA_CATEGORY_TODOS } from '../../features/todos'; | ||
12 | import { todoActions } from '../../features/todos/actions'; | 12 | import { todoActions } from '../../features/todos/actions'; |
13 | 13 | ||
14 | const messages = defineMessages({ | 14 | const 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 | ||
49 | export default @observer class Sidebar extends Component { | 53 | export 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 @@ | |||
1 | import React, { Component, Fragment } from 'react'; | 1 | import React, { Component, Fragment } from 'react'; |
2 | import PropTypes from 'prop-types'; | 2 | import PropTypes from 'prop-types'; |
3 | import { autorun } from 'mobx'; | 3 | import { autorun, reaction } from 'mobx'; |
4 | import { observer } from 'mobx-react'; | 4 | import { observer, inject } from 'mobx-react'; |
5 | import classnames from 'classnames'; | 5 | import classnames from 'classnames'; |
6 | 6 | ||
7 | import ServiceModel from '../../../models/Service'; | 7 | import ServiceModel from '../../../models/Service'; |
@@ -10,12 +10,12 @@ import WebviewLoader from '../../ui/WebviewLoader'; | |||
10 | import WebviewCrashHandler from './WebviewCrashHandler'; | 10 | import WebviewCrashHandler from './WebviewCrashHandler'; |
11 | import WebviewErrorHandler from './ErrorHandlers/WebviewErrorHandler'; | 11 | import WebviewErrorHandler from './ErrorHandlers/WebviewErrorHandler'; |
12 | import ServiceDisabled from './ServiceDisabled'; | 12 | import ServiceDisabled from './ServiceDisabled'; |
13 | import ServiceRestricted from './ServiceRestricted'; | ||
14 | import ServiceWebview from './ServiceWebview'; | 13 | import ServiceWebview from './ServiceWebview'; |
14 | import SettingsStore from '../../../stores/SettingsStore'; | ||
15 | import WebControlsScreen from '../../../features/webControls/containers/WebControlsScreen'; | 15 | import WebControlsScreen from '../../../features/webControls/containers/WebControlsScreen'; |
16 | import { CUSTOM_WEBSITE_ID } from '../../../features/webControls/constants'; | 16 | import { CUSTOM_WEBSITE_ID } from '../../../features/webControls/constants'; |
17 | 17 | ||
18 | export default @observer class ServiceView extends Component { | 18 | export 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'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 @@ | |||
1 | import React, { Component } from 'react'; | 1 | import React, { Component } from 'react'; |
2 | import PropTypes from 'prop-types'; | 2 | import PropTypes from 'prop-types'; |
3 | import { observer } from 'mobx-react'; | 3 | import { observer } from 'mobx-react'; |
4 | import { observable, reaction } from 'mobx'; | ||
4 | import ElectronWebView from 'react-electron-web-view'; | 5 | import ElectronWebView from 'react-electron-web-view'; |
5 | 6 | ||
6 | import ServiceModel from '../../../models/Service'; | 7 | import ServiceModel from '../../../models/Service'; |
7 | 8 | ||
9 | const debug = require('debug')('Ferdi:Services'); | ||
10 | |||
8 | @observer | 11 | @observer |
9 | class ServiceWebview extends Component { | 12 | class 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 @@ | |||
1 | import React, { Component } from 'react'; | 1 | import React, { Component } from 'react'; |
2 | import PropTypes from 'prop-types'; | 2 | import PropTypes from 'prop-types'; |
3 | import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; | 3 | import { observer, PropTypes as MobxPropTypes, inject } from 'mobx-react'; |
4 | import { Link } from 'react-router'; | 4 | import { Link } from 'react-router'; |
5 | import { defineMessages, intlShape } from 'react-intl'; | 5 | import { defineMessages, intlShape } from 'react-intl'; |
6 | import Confetti from 'react-confetti'; | 6 | import Confetti from 'react-confetti'; |
@@ -9,16 +9,29 @@ import injectSheet from 'react-jss'; | |||
9 | 9 | ||
10 | import ServiceView from './ServiceView'; | 10 | import ServiceView from './ServiceView'; |
11 | import Appear from '../../ui/effects/Appear'; | 11 | import Appear from '../../ui/effects/Appear'; |
12 | import serverlessLogin from '../../../helpers/serverless-helpers'; | ||
12 | 13 | ||
13 | const messages = defineMessages({ | 14 | const 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 | ||
34 | export default @observer @injectSheet(styles) class Services extends Component { | 47 | export 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'; | |||
3 | import { defineMessages, intlShape } from 'react-intl'; | 3 | import { defineMessages, intlShape } from 'react-intl'; |
4 | import { inject, observer } from 'mobx-react'; | 4 | import { inject, observer } from 'mobx-react'; |
5 | import { ProBadge } from '@meetfranz/ui'; | 5 | import { ProBadge } from '@meetfranz/ui'; |
6 | import { RouterStore } from 'mobx-react-router'; | ||
6 | 7 | ||
8 | import { LOCAL_SERVER, LIVE_API } from '../../../config'; | ||
7 | import Link from '../../ui/Link'; | 9 | import Link from '../../ui/Link'; |
8 | import { workspaceStore } from '../../../features/workspaces'; | 10 | import { workspaceStore } from '../../../features/workspaces'; |
9 | import UIStore from '../../../stores/UIStore'; | 11 | import UIStore from '../../../stores/UIStore'; |
12 | import SettingsStore from '../../../stores/SettingsStore'; | ||
10 | import UserStore from '../../../stores/UserStore'; | 13 | import UserStore from '../../../stores/UserStore'; |
11 | import { serviceLimitStore } from '../../../features/serviceLimit'; | 14 | import { 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 | ||
48 | export default @inject('stores') @observer class SettingsNavigation extends Component { | 51 | export 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'; | |||
9 | import Toggle from '../../ui/Toggle'; | 9 | import Toggle from '../../ui/Toggle'; |
10 | import Select from '../../ui/Select'; | 10 | import Select from '../../ui/Select'; |
11 | import PremiumFeatureContainer from '../../ui/PremiumFeatureContainer'; | 11 | import PremiumFeatureContainer from '../../ui/PremiumFeatureContainer'; |
12 | import Input from '../../ui/Input'; | ||
12 | 13 | ||
13 | import { FRANZ_TRANSLATION } from '../../../config'; | 14 | import { FRANZ_TRANSLATION } from '../../../config'; |
14 | 15 | ||
16 | function escapeHtml(unsafe) { | ||
17 | return unsafe | ||
18 | .replace(/&/g, '&') | ||
19 | .replace(/</g, '<') | ||
20 | .replace(/>/g, '>') | ||
21 | .replace(/"/g, '"') | ||
22 | .replace(/'/g, '''); | ||
23 | } | ||
24 | |||
15 | const messages = defineMessages({ | 25 | const 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 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { defineMessages, intlShape } from 'react-intl'; | ||
4 | |||
5 | import Button from '../../ui/Button'; | ||
6 | |||
7 | const 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 | |||
30 | class 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 | |||
73 | export 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 | ||
43 | const styles = { | 51 | const 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 | ||
38 | export default @observer @injectSheet(styles) class SubscriptionForm extends Component { | 38 | export 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 | ||
60 | export default @observer @injectSheet(styles) class TrialForm extends Component { | 60 | export 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'; | |||
5 | import classnames from 'classnames'; | 5 | import classnames from 'classnames'; |
6 | 6 | ||
7 | import { Button } from '@meetfranz/forms'; | 7 | import { Button } from '@meetfranz/forms'; |
8 | import { gaEvent } from '../../../lib/analytics'; | ||
9 | 8 | ||
10 | import UserStore from '../../../stores/UserStore'; | 9 | import 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'; | |||
9 | import styles from './styles'; | 9 | import styles from './styles'; |
10 | 10 | ||
11 | const textList = shuffleArray([ | 11 | const 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 | ||
22 | export default @injectSheet(styles) @withTheme class AppLoader extends Component { | 21 | export 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 @@ | |||
1 | import React, { Component } from 'react'; | 1 | import React, { Component } from 'react'; |
2 | import PropTypes from 'prop-types'; | 2 | import PropTypes from 'prop-types'; |
3 | import { observer } from 'mobx-react'; | 3 | import { observer, inject } from 'mobx-react'; |
4 | import Loader from 'react-loader'; | 4 | import Loader from 'react-loader'; |
5 | import classnames from 'classnames'; | 5 | import classnames from 'classnames'; |
6 | 6 | ||
7 | export default @observer class Button extends Component { | 7 | export 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 @@ | |||
1 | import React, { Component } from 'react'; | 1 | import React, { Component } from 'react'; |
2 | import PropTypes from 'prop-types'; | 2 | import PropTypes from 'prop-types'; |
3 | import { observer } from 'mobx-react'; | 3 | import { observer, inject } from 'mobx-react'; |
4 | import injectSheet, { withTheme } from 'react-jss'; | 4 | import injectSheet, { withTheme } from 'react-jss'; |
5 | import classnames from 'classnames'; | 5 | import classnames from 'classnames'; |
6 | 6 | ||
@@ -8,7 +8,7 @@ import Loader from '../Loader'; | |||
8 | 8 | ||
9 | import styles from './styles'; | 9 | import styles from './styles'; |
10 | 10 | ||
11 | export default @observer @withTheme @injectSheet(styles) class FullscreenLoader extends Component { | 11 | export 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 | ||
70 | Link.wrappedComponent.defaultProps = { | 73 | Link.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 @@ | |||
1 | import React, { Component } from 'react'; | 1 | import React, { Component } from 'react'; |
2 | import { observer, inject } from 'mobx-react'; | ||
2 | import PropTypes from 'prop-types'; | 3 | import PropTypes from 'prop-types'; |
3 | import Loader from 'react-loader'; | 4 | import Loader from 'react-loader'; |
4 | 5 | ||
5 | import { oneOrManyChildElements } from '../../prop-types'; | 6 | import { oneOrManyChildElements } from '../../prop-types'; |
6 | 7 | ||
7 | export default class LoaderComponent extends Component { | 8 | export 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'; | |||
9 | import UserStore from '../../../stores/UserStore'; | 9 | import UserStore from '../../../stores/UserStore'; |
10 | 10 | ||
11 | import styles from './styles'; | 11 | import styles from './styles'; |
12 | import { gaEvent } from '../../../lib/analytics'; | 12 | import FeatureStore from '../../../stores/FeaturesStore'; |
13 | import FeaturesStore from '../../../stores/FeaturesStore'; | ||
14 | 13 | ||
15 | const messages = defineMessages({ | 14 | const 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'; | |||
4 | import { defineMessages, intlShape } from 'react-intl'; | 4 | import { defineMessages, intlShape } from 'react-intl'; |
5 | 5 | ||
6 | import { Button } from '@meetfranz/forms'; | 6 | import { Button } from '@meetfranz/forms'; |
7 | import { gaEvent } from '../../../lib/analytics'; | ||
8 | 7 | ||
9 | import UserStore from '../../../stores/UserStore'; | 8 | import UserStore from '../../../stores/UserStore'; |
10 | import ActivateTrialButton from '../ActivateTrialButton'; | 9 | import 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 | ||
17 | export default @observer @injectSheet(styles) class WebviewLoader extends Component { | 17 | export 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, |