aboutsummaryrefslogtreecommitdiffstats
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/AppUpdateInfoBar.js4
-rw-r--r--src/components/auth/AuthLayout.js9
-rw-r--r--src/components/auth/ChangeServer.js80
-rw-r--r--src/components/auth/Login.js4
-rw-r--r--src/components/auth/Signup.js7
-rw-r--r--src/components/auth/Welcome.js4
-rw-r--r--src/components/layout/AppLayout.js9
-rw-r--r--src/components/services/content/ConnectionLostBanner.js119
-rw-r--r--src/components/services/content/ServiceView.js8
-rw-r--r--src/components/services/tabs/TabItem.js2
-rw-r--r--src/components/settings/account/AccountDashboard.js43
-rw-r--r--src/components/settings/navigation/SettingsNavigation.js25
-rw-r--r--src/components/settings/services/EditServiceForm.js31
-rw-r--r--src/components/settings/settings/EditSettingsForm.js168
-rw-r--r--src/components/settings/team/TeamDashboard.js6
-rw-r--r--src/components/ui/FeatureList.js5
-rw-r--r--src/components/ui/Slider.js65
17 files changed, 440 insertions, 149 deletions
diff --git a/src/components/AppUpdateInfoBar.js b/src/components/AppUpdateInfoBar.js
index 4108fdf12..f51fe029b 100644
--- a/src/components/AppUpdateInfoBar.js
+++ b/src/components/AppUpdateInfoBar.js
@@ -24,6 +24,7 @@ class AppUpdateInfoBar extends Component {
24 static propTypes = { 24 static propTypes = {
25 onInstallUpdate: PropTypes.func.isRequired, 25 onInstallUpdate: PropTypes.func.isRequired,
26 nextAppReleaseVersion: PropTypes.string, 26 nextAppReleaseVersion: PropTypes.string,
27 onHide: PropTypes.func.isRequired,
27 }; 28 };
28 29
29 static defaultProps = { 30 static defaultProps = {
@@ -39,6 +40,7 @@ class AppUpdateInfoBar extends Component {
39 const { 40 const {
40 onInstallUpdate, 41 onInstallUpdate,
41 nextAppReleaseVersion, 42 nextAppReleaseVersion,
43 onHide,
42 } = this.props; 44 } = this.props;
43 45
44 return ( 46 return (
@@ -46,7 +48,7 @@ class AppUpdateInfoBar extends Component {
46 type="primary" 48 type="primary"
47 ctaLabel={intl.formatMessage(messages.buttonInstallUpdate)} 49 ctaLabel={intl.formatMessage(messages.buttonInstallUpdate)}
48 onClick={onInstallUpdate} 50 onClick={onInstallUpdate}
49 sticky 51 onHide={onHide}
50 > 52 >
51 <span className="mdi mdi-information" /> 53 <span className="mdi mdi-information" />
52 {intl.formatMessage(messages.updateAvailable)} 54 {intl.formatMessage(messages.updateAvailable)}
diff --git a/src/components/auth/AuthLayout.js b/src/components/auth/AuthLayout.js
index 0c5198583..4783fc6a0 100644
--- a/src/components/auth/AuthLayout.js
+++ b/src/components/auth/AuthLayout.js
@@ -27,6 +27,10 @@ export default @observer class AuthLayout extends Component {
27 appUpdateIsDownloaded: PropTypes.bool.isRequired, 27 appUpdateIsDownloaded: PropTypes.bool.isRequired,
28 }; 28 };
29 29
30 state = {
31 shouldShowAppUpdateInfoBar: true,
32 }
33
30 static defaultProps = { 34 static defaultProps = {
31 nextAppReleaseVersion: null, 35 nextAppReleaseVersion: null,
32 }; 36 };
@@ -62,10 +66,13 @@ export default @observer class AuthLayout extends Component {
62 {intl.formatMessage(globalMessages.notConnectedToTheInternet)} 66 {intl.formatMessage(globalMessages.notConnectedToTheInternet)}
63 </InfoBar> 67 </InfoBar>
64 )} 68 )}
65 {appUpdateIsDownloaded && ( 69 {appUpdateIsDownloaded && this.state.shouldShowAppUpdateInfoBar && (
66 <AppUpdateInfoBar 70 <AppUpdateInfoBar
67 nextAppReleaseVersion={nextAppReleaseVersion} 71 nextAppReleaseVersion={nextAppReleaseVersion}
68 onInstallUpdate={installAppUpdate} 72 onInstallUpdate={installAppUpdate}
73 onHide={() => {
74 this.setState({ shouldShowAppUpdateInfoBar: false });
75 }}
69 /> 76 />
70 )} 77 )}
71 {isOnline && !isAPIHealthy && ( 78 {isOnline && !isAPIHealthy && (
diff --git a/src/components/auth/ChangeServer.js b/src/components/auth/ChangeServer.js
new file mode 100644
index 000000000..433334b6c
--- /dev/null
+++ b/src/components/auth/ChangeServer.js
@@ -0,0 +1,80 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl';
5
6import Form from '../../lib/Form';
7import Input from '../ui/Input';
8import Button from '../ui/Button';
9
10const messages = defineMessages({
11 headline: {
12 id: 'changeserver.headline',
13 defaultMessage: '!!!Change server',
14 },
15 label: {
16 id: 'changeserver.label',
17 defaultMessage: '!!!Server',
18 },
19 submit: {
20 id: 'changeserver.submit',
21 defaultMessage: '!!!Submit',
22 },
23});
24
25export default @observer class ChangeServer extends Component {
26 static propTypes = {
27 onSubmit: PropTypes.func.isRequired,
28 server: PropTypes.string.isRequired,
29 };
30
31 static contextTypes = {
32 intl: intlShape,
33 };
34
35 form = new Form({
36 fields: {
37 server: {
38 label: this.context.intl.formatMessage(messages.label),
39 value: '',
40 },
41 },
42 }, this.context.intl);
43
44 componentDidMount() {
45 this.form.$('server').value = this.props.server;
46 }
47
48 submit(e) {
49 e.preventDefault();
50 this.form.submit({
51 onSuccess: (form) => {
52 this.props.onSubmit(form.values());
53 },
54 onError: () => { },
55 });
56 }
57
58 render() {
59 const { form } = this;
60 const { intl } = this.context;
61
62 return (
63 <div className="auth__container">
64 <form className="franz-form auth__form" onSubmit={e => this.submit(e)}>
65 <h1>{intl.formatMessage(messages.headline)}</h1>
66
67 <Input
68 field={form.$('server')}
69 focus
70 />
71 <Button
72 type="submit"
73 className="auth__button"
74 label={intl.formatMessage(messages.submit)}
75 />
76 </form>
77 </div>
78 );
79 }
80}
diff --git a/src/components/auth/Login.js b/src/components/auth/Login.js
index e25121de0..f33d134c8 100644
--- a/src/components/auth/Login.js
+++ b/src/components/auth/Login.js
@@ -78,6 +78,7 @@ export default @inject('actions') @observer class Login extends Component {
78 isServerLogout: PropTypes.bool.isRequired, 78 isServerLogout: PropTypes.bool.isRequired,
79 signupRoute: PropTypes.string.isRequired, 79 signupRoute: PropTypes.string.isRequired,
80 passwordRoute: PropTypes.string.isRequired, 80 passwordRoute: PropTypes.string.isRequired,
81 changeServerRoute: PropTypes.string.isRequired,
81 error: globalErrorPropType.isRequired, 82 error: globalErrorPropType.isRequired,
82 actions: PropTypes.object.isRequired, 83 actions: PropTypes.object.isRequired,
83 }; 84 };
@@ -127,6 +128,7 @@ export default @inject('actions') @observer class Login extends Component {
127 isServerLogout, 128 isServerLogout,
128 signupRoute, 129 signupRoute,
129 passwordRoute, 130 passwordRoute,
131 changeServerRoute,
130 error, 132 error,
131 } = this.props; 133 } = this.props;
132 134
@@ -194,7 +196,7 @@ export default @inject('actions') @observer class Login extends Component {
194 )} 196 )}
195 </form> 197 </form>
196 <div className="auth__links"> 198 <div className="auth__links">
197 <Link to="/settings/app">{intl.formatMessage(messages.changeServer)}</Link> 199 <Link to={changeServerRoute}>{intl.formatMessage(messages.changeServer)}</Link>
198 <a onClick={this.useLocalServer.bind(this)}>{intl.formatMessage(messages.serverless)}</a> 200 <a onClick={this.useLocalServer.bind(this)}>{intl.formatMessage(messages.serverless)}</a>
199 <Link to={signupRoute}>{intl.formatMessage(messages.signupLink)}</Link> 201 <Link to={signupRoute}>{intl.formatMessage(messages.signupLink)}</Link>
200 <Link to={passwordRoute}>{intl.formatMessage(messages.passwordLink)}</Link> 202 <Link to={passwordRoute}>{intl.formatMessage(messages.passwordLink)}</Link>
diff --git a/src/components/auth/Signup.js b/src/components/auth/Signup.js
index a166155a7..6a7db5cde 100644
--- a/src/components/auth/Signup.js
+++ b/src/components/auth/Signup.js
@@ -79,6 +79,7 @@ export default @inject('actions') @observer class Signup extends Component {
79 onSubmit: PropTypes.func.isRequired, 79 onSubmit: PropTypes.func.isRequired,
80 isSubmitting: PropTypes.bool.isRequired, 80 isSubmitting: PropTypes.bool.isRequired,
81 loginRoute: PropTypes.string.isRequired, 81 loginRoute: PropTypes.string.isRequired,
82 changeServerRoute: PropTypes.string.isRequired,
82 error: globalErrorPropType.isRequired, 83 error: globalErrorPropType.isRequired,
83 actions: PropTypes.object.isRequired, 84 actions: PropTypes.object.isRequired,
84 }; 85 };
@@ -130,7 +131,9 @@ export default @inject('actions') @observer class Signup extends Component {
130 render() { 131 render() {
131 const { form } = this; 132 const { form } = this;
132 const { intl } = this.context; 133 const { intl } = this.context;
133 const { isSubmitting, loginRoute, error } = this.props; 134 const {
135 isSubmitting, loginRoute, error, changeServerRoute,
136 } = this.props;
134 137
135 const termsBase = window.ferdi.stores.settings.all.app.server !== 'https://api.franzinfra.com' ? window.ferdi.stores.settings.all.app.server : 'https://meetfranz.com'; 138 const termsBase = window.ferdi.stores.settings.all.app.server !== 'https://api.franzinfra.com' ? window.ferdi.stores.settings.all.app.server : 'https://meetfranz.com';
136 139
@@ -198,7 +201,7 @@ export default @inject('actions') @observer class Signup extends Component {
198 </p> 201 </p>
199 </form> 202 </form>
200 <div className="auth__links"> 203 <div className="auth__links">
201 <Link to="/settings/app">{intl.formatMessage(messages.changeServer)}</Link> 204 <Link to={changeServerRoute}>{intl.formatMessage(messages.changeServer)}</Link>
202 <a onClick={this.useLocalServer.bind(this)}>{intl.formatMessage(messages.serverless)}</a> 205 <a onClick={this.useLocalServer.bind(this)}>{intl.formatMessage(messages.serverless)}</a>
203 <Link to={loginRoute}>{intl.formatMessage(messages.loginLink)}</Link> 206 <Link to={loginRoute}>{intl.formatMessage(messages.loginLink)}</Link>
204 </div> 207 </div>
diff --git a/src/components/auth/Welcome.js b/src/components/auth/Welcome.js
index 1453c1d7c..6e742e0c1 100644
--- a/src/components/auth/Welcome.js
+++ b/src/components/auth/Welcome.js
@@ -26,6 +26,7 @@ export default @inject('actions') @observer class Login extends Component {
26 static propTypes = { 26 static propTypes = {
27 loginRoute: PropTypes.string.isRequired, 27 loginRoute: PropTypes.string.isRequired,
28 signupRoute: PropTypes.string.isRequired, 28 signupRoute: PropTypes.string.isRequired,
29 changeServerRoute: PropTypes.string.isRequired,
29 recipes: MobxPropTypes.arrayOrObservableArray.isRequired, 30 recipes: MobxPropTypes.arrayOrObservableArray.isRequired,
30 actions: PropTypes.object.isRequired, 31 actions: PropTypes.object.isRequired,
31 }; 32 };
@@ -43,6 +44,7 @@ export default @inject('actions') @observer class Login extends Component {
43 const { 44 const {
44 loginRoute, 45 loginRoute,
45 signupRoute, 46 signupRoute,
47 changeServerRoute,
46 recipes, 48 recipes,
47 } = this.props; 49 } = this.props;
48 50
@@ -71,7 +73,7 @@ export default @inject('actions') @observer class Login extends Component {
71 <br /> 73 <br />
72 74
73 75
74 <Link to="settings/app"> 76 <Link to={changeServerRoute}>
75 <span style={{ 77 <span style={{
76 textAlign: 'center', 78 textAlign: 'center',
77 width: '100%', 79 width: '100%',
diff --git a/src/components/layout/AppLayout.js b/src/components/layout/AppLayout.js
index fe43c42d2..3b732e602 100644
--- a/src/components/layout/AppLayout.js
+++ b/src/components/layout/AppLayout.js
@@ -81,6 +81,10 @@ class AppLayout extends Component {
81 hasActivatedTrial: PropTypes.bool.isRequired, 81 hasActivatedTrial: PropTypes.bool.isRequired,
82 }; 82 };
83 83
84 state = {
85 shouldShowAppUpdateInfoBar: true,
86 }
87
84 static defaultProps = { 88 static defaultProps = {
85 children: [], 89 children: [],
86 nextAppReleaseVersion: null, 90 nextAppReleaseVersion: null,
@@ -181,10 +185,13 @@ class AppLayout extends Component {
181 {intl.formatMessage(messages.servicesUpdated)} 185 {intl.formatMessage(messages.servicesUpdated)}
182 </InfoBar> 186 </InfoBar>
183 )} 187 )}
184 {appUpdateIsDownloaded && ( 188 { appUpdateIsDownloaded && this.state.shouldShowAppUpdateInfoBar && (
185 <AppUpdateInfoBar 189 <AppUpdateInfoBar
186 nextAppReleaseVersion={nextAppReleaseVersion} 190 nextAppReleaseVersion={nextAppReleaseVersion}
187 onInstallUpdate={installAppUpdate} 191 onInstallUpdate={installAppUpdate}
192 onHide={() => {
193 this.setState({ shouldShowAppUpdateInfoBar: false });
194 }}
188 /> 195 />
189 )} 196 )}
190 <BasicAuth /> 197 <BasicAuth />
diff --git a/src/components/services/content/ConnectionLostBanner.js b/src/components/services/content/ConnectionLostBanner.js
new file mode 100644
index 000000000..9609a65b1
--- /dev/null
+++ b/src/components/services/content/ConnectionLostBanner.js
@@ -0,0 +1,119 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import injectSheet from 'react-jss';
5import { Icon } from '@meetfranz/ui';
6import { intlShape, defineMessages } from 'react-intl';
7
8import {
9 mdiAlert,
10} from '@mdi/js';
11import { LIVE_API_WEBSITE } from '../../../config';
12// import { Button } from '@meetfranz/forms';
13
14const messages = defineMessages({
15 text: {
16 id: 'connectionLostBanner.message',
17 defaultMessage: '!!!Oh no! Franz lost the connection to {name}.',
18 },
19 moreInformation: {
20 id: 'connectionLostBanner.informationLink',
21 defaultMessage: '!!!What happened?',
22 },
23 cta: {
24 id: 'connectionLostBanner.cta',
25 defaultMessage: '!!!Reload Service',
26 },
27});
28
29const styles = theme => ({
30 root: {
31 background: theme.colorBackground,
32 borderRadius: theme.borderRadius,
33 position: 'absolute',
34 zIndex: 300,
35 height: 50,
36 display: 'flex',
37 flexDirection: 'row',
38 alignItems: 'center',
39 bottom: 10,
40 right: 10,
41 justifyContent: 'center',
42 padding: 10,
43 fontSize: 12,
44 },
45 link: {
46 display: 'inline-flex',
47 opacity: 0.7,
48 },
49 button: {
50 transition: 'opacity 0.25s',
51 color: theme.colorText,
52 border: [1, 'solid', theme.colorText],
53 borderRadius: theme.borderRadiusSmall,
54 padding: 4,
55 fontSize: 12,
56 marginLeft: 15,
57
58 '&:hover': {
59 opacity: 0.8,
60 },
61 },
62 icon: {
63 marginRight: 10,
64 fill: theme.styleTypes.danger.accent,
65 },
66});
67
68@injectSheet(styles) @observer
69class ConnectionLostBanner extends Component {
70 static propTypes = {
71 classes: PropTypes.object.isRequired,
72 name: PropTypes.string.isRequired,
73 reload: PropTypes.func.isRequired,
74 }
75
76 static contextTypes = {
77 intl: intlShape,
78 };
79
80 inputRef = React.createRef();
81
82 render() {
83 const {
84 classes,
85 name,
86 reload,
87 } = this.props;
88
89 const { intl } = this.context;
90
91 return (
92 <div className={classes.root}>
93 <Icon
94 icon={mdiAlert}
95 className={classes.icon}
96 />
97 <p>
98 {intl.formatMessage(messages.text, { name })}
99 <br />
100 <a
101 href={`${LIVE_API_WEBSITE}/support#what-does-franz-lost-the-connection-to-service-mean`}
102 className={classes.link}
103 >
104 {intl.formatMessage(messages.moreInformation)}
105 </a>
106 </p>
107 <button
108 type="button"
109 className={classes.button}
110 onClick={reload}
111 >
112 {intl.formatMessage(messages.cta)}
113 </button>
114 </div>
115 );
116 }
117}
118
119export default ConnectionLostBanner;
diff --git a/src/components/services/content/ServiceView.js b/src/components/services/content/ServiceView.js
index f6832038a..d91016c71 100644
--- a/src/components/services/content/ServiceView.js
+++ b/src/components/services/content/ServiceView.js
@@ -193,7 +193,7 @@ export default @inject('stores', 'actions') @observer class ServiceView extends
193 </Fragment> 193 </Fragment>
194 ) : ( 194 ) : (
195 <> 195 <>
196 {!service.isHibernating ? ( 196 {(!service.isHibernating || service.disableHibernation) ? (
197 <> 197 <>
198 {showNavBar && ( 198 {showNavBar && (
199 <WebControlsScreen service={service} /> 199 <WebControlsScreen service={service} />
@@ -203,6 +203,12 @@ export default @inject('stores', 'actions') @observer class ServiceView extends
203 setWebviewReference={setWebviewReference} 203 setWebviewReference={setWebviewReference}
204 detachService={detachService} 204 detachService={detachService}
205 /> 205 />
206 {/* {service.lostRecipeConnection && (
207 <ConnectionLostBanner
208 name={service.name}
209 reload={reload}
210 />
211 )} */}
206 </> 212 </>
207 ) : ( 213 ) : (
208 <div> 214 <div>
diff --git a/src/components/services/tabs/TabItem.js b/src/components/services/tabs/TabItem.js
index 36338a910..ea7a66a62 100644
--- a/src/components/services/tabs/TabItem.js
+++ b/src/components/services/tabs/TabItem.js
@@ -145,7 +145,7 @@ class TabItem extends Component {
145 145
146 </span> 146 </span>
147 )} 147 )}
148 {service.isHibernating && ( 148 {service.isHibernating && !service.disableHibernation && (
149 <span className="tab-item__message-count hibernating"> 149 <span className="tab-item__message-count hibernating">
150 150
151 </span> 151 </span>
diff --git a/src/components/settings/account/AccountDashboard.js b/src/components/settings/account/AccountDashboard.js
index 7d6bad883..5c3dc21d0 100644
--- a/src/components/settings/account/AccountDashboard.js
+++ b/src/components/settings/account/AccountDashboard.js
@@ -154,6 +154,7 @@ class AccountDashboard extends Component {
154 } 154 }
155 155
156 const isUsingWithoutAccount = server === LOCAL_SERVER; 156 const isUsingWithoutAccount = server === LOCAL_SERVER;
157 const isUsingFranzServer = server === 'https://api.franzinfra.com';
157 158
158 return ( 159 return (
159 <div className="settings__main"> 160 <div className="settings__main">
@@ -208,7 +209,7 @@ class AccountDashboard extends Component {
208 </div> 209 </div>
209 <div className="account__info"> 210 <div className="account__info">
210 <H1> 211 <H1>
211 <span className="username">{`${user.firstname} ${user.lastname}`}</span> 212 <span className="username">{`${user.firstname} ${isUsingFranzServer ? user.lastname : ''}`}</span>
212 {user.isPremium && ( 213 {user.isPremium && (
213 <> 214 <>
214 {' '} 215 {' '}
@@ -243,7 +244,7 @@ class AccountDashboard extends Component {
243 )} 244 )}
244 </div> 245 </div>
245 </div> 246 </div>
246 {user.isPremium && user.isSubscriptionOwner && ( 247 {user.isPremium && user.isSubscriptionOwner && isUsingFranzServer && (
247 <div className="account"> 248 <div className="account">
248 <div className="account__box"> 249 <div className="account__box">
249 <H2>{intl.formatMessage(messages.yourLicense)}</H2> 250 <H2>{intl.formatMessage(messages.yourLicense)}</H2>
@@ -322,25 +323,27 @@ class AccountDashboard extends Component {
322 </> 323 </>
323 )} 324 )}
324 325
325 <div className="account franz-form"> 326 {isUsingFranzServer && (
326 <div className="account__box"> 327 <div className="account franz-form">
327 <H2>{intl.formatMessage(messages.headlineDangerZone)}</H2> 328 <div className="account__box">
328 {!isDeleteAccountSuccessful && ( 329 <H2>{intl.formatMessage(messages.headlineDangerZone)}</H2>
329 <div className="account__subscription"> 330 {!isDeleteAccountSuccessful && (
330 <p>{intl.formatMessage(messages.deleteInfo)}</p> 331 <div className="account__subscription">
331 <Button 332 <p>{intl.formatMessage(messages.deleteInfo)}</p>
332 label={intl.formatMessage(messages.deleteAccount)} 333 <Button
333 buttonType="danger" 334 label={intl.formatMessage(messages.deleteAccount)}
334 onClick={() => deleteAccount()} 335 buttonType="danger"
335 loaded={!isLoadingDeleteAccount} 336 onClick={() => deleteAccount()}
336 /> 337 loaded={!isLoadingDeleteAccount}
337 </div> 338 />
338 )} 339 </div>
339 {isDeleteAccountSuccessful && ( 340 )}
340 <p>{intl.formatMessage(messages.deleteEmailSent)}</p> 341 {isDeleteAccountSuccessful && (
341 )} 342 <p>{intl.formatMessage(messages.deleteEmailSent)}</p>
343 )}
344 </div>
342 </div> 345 </div>
343 </div> 346 )}
344 </> 347 </>
345 )} 348 )}
346 </> 349 </>
diff --git a/src/components/settings/navigation/SettingsNavigation.js b/src/components/settings/navigation/SettingsNavigation.js
index eb3249fa0..6b03f05be 100644
--- a/src/components/settings/navigation/SettingsNavigation.js
+++ b/src/components/settings/navigation/SettingsNavigation.js
@@ -105,6 +105,7 @@ export default @inject('stores', 'actions') @observer class SettingsNavigation e
105 const { intl } = this.context; 105 const { intl } = this.context;
106 const isLoggedIn = Boolean(localStorage.getItem('authToken')); 106 const isLoggedIn = Boolean(localStorage.getItem('authToken'));
107 const isUsingWithoutAccount = stores.settings.app.server === LOCAL_SERVER; 107 const isUsingWithoutAccount = stores.settings.app.server === LOCAL_SERVER;
108 const isUsingFranzServer = stores.settings.app.server === 'https://api.franzinfra.com';
108 109
109 return ( 110 return (
110 <div className="settings-navigation"> 111 <div className="settings-navigation">
@@ -154,17 +155,19 @@ export default @inject('stores', 'actions') @observer class SettingsNavigation e
154 > 155 >
155 {intl.formatMessage(messages.account)} 156 {intl.formatMessage(messages.account)}
156 </Link> 157 </Link>
157 <Link 158 {isUsingFranzServer && (
158 to="/settings/team" 159 <Link
159 className="settings-navigation__link" 160 to="/settings/team"
160 activeClassName="is-active" 161 className="settings-navigation__link"
161 disabled={!isLoggedIn} 162 activeClassName="is-active"
162 > 163 disabled={!isLoggedIn}
163 {intl.formatMessage(messages.team)} 164 >
164 {!user.data.isPremium && ( 165 {intl.formatMessage(messages.team)}
165 <ProBadge inverted={!isDarkThemeActive && router.location.pathname === '/settings/team'} /> 166 {!user.data.isPremium && (
166 )} 167 <ProBadge inverted={!isDarkThemeActive && router.location.pathname === '/settings/team'} />
167 </Link> 168 )}
169 </Link>
170 )}
168 <Link 171 <Link
169 to="/settings/app" 172 to="/settings/app"
170 className="settings-navigation__link" 173 className="settings-navigation__link"
diff --git a/src/components/settings/services/EditServiceForm.js b/src/components/settings/services/EditServiceForm.js
index 98051d78f..4fd1f99ef 100644
--- a/src/components/settings/services/EditServiceForm.js
+++ b/src/components/settings/services/EditServiceForm.js
@@ -12,6 +12,7 @@ import Service from '../../../models/Service';
12import Tabs, { TabItem } from '../../ui/Tabs'; 12import Tabs, { TabItem } from '../../ui/Tabs';
13import Input from '../../ui/Input'; 13import Input from '../../ui/Input';
14import Toggle from '../../ui/Toggle'; 14import Toggle from '../../ui/Toggle';
15import Slider from '../../ui/Slider';
15import Button from '../../ui/Button'; 16import Button from '../../ui/Button';
16import ImageUpload from '../../ui/ImageUpload'; 17import ImageUpload from '../../ui/ImageUpload';
17import Select from '../../ui/Select'; 18import Select from '../../ui/Select';
@@ -93,6 +94,10 @@ const messages = defineMessages({
93 id: 'settings.service.form.isMutedInfo', 94 id: 'settings.service.form.isMutedInfo',
94 defaultMessage: '!!!When disabled, all notification sounds and audio playback are muted', 95 defaultMessage: '!!!When disabled, all notification sounds and audio playback are muted',
95 }, 96 },
97 disableHibernationInfo: {
98 id: 'settings.service.form.disableHibernationInfo',
99 defaultMessage: '!!!You currently have hibernation enabled but you can disable hibernation for individual services using this option.',
100 },
96 headlineNotifications: { 101 headlineNotifications: {
97 id: 'settings.service.form.headlineNotifications', 102 id: 'settings.service.form.headlineNotifications',
98 defaultMessage: '!!!Notifications', 103 defaultMessage: '!!!Notifications',
@@ -105,6 +110,10 @@ const messages = defineMessages({
105 id: 'settings.service.form.headlineGeneral', 110 id: 'settings.service.form.headlineGeneral',
106 defaultMessage: '!!!General', 111 defaultMessage: '!!!General',
107 }, 112 },
113 headlineDarkReaderSettings: {
114 id: 'settings.service.form.headlineDarkReaderSettings',
115 defaultMessage: '!!!DarkReader Settings',
116 },
108 iconDelete: { 117 iconDelete: {
109 id: 'settings.service.form.iconDelete', 118 id: 'settings.service.form.iconDelete',
110 defaultMessage: '!!!Delete', 119 defaultMessage: '!!!Delete',
@@ -149,6 +158,7 @@ export default @observer class EditServiceForm extends Component {
149 isProxyFeatureEnabled: PropTypes.bool.isRequired, 158 isProxyFeatureEnabled: PropTypes.bool.isRequired,
150 isServiceProxyIncludedInCurrentPlan: PropTypes.bool.isRequired, 159 isServiceProxyIncludedInCurrentPlan: PropTypes.bool.isRequired,
151 isSpellcheckerIncludedInCurrentPlan: PropTypes.bool.isRequired, 160 isSpellcheckerIncludedInCurrentPlan: PropTypes.bool.isRequired,
161 isHibernationFeatureActive: PropTypes.bool.isRequired,
152 }; 162 };
153 163
154 static defaultProps = { 164 static defaultProps = {
@@ -214,6 +224,7 @@ export default @observer class EditServiceForm extends Component {
214 isProxyFeatureEnabled, 224 isProxyFeatureEnabled,
215 isServiceProxyIncludedInCurrentPlan, 225 isServiceProxyIncludedInCurrentPlan,
216 isSpellcheckerIncludedInCurrentPlan, 226 isSpellcheckerIncludedInCurrentPlan,
227 isHibernationFeatureActive,
217 } = this.props; 228 } = this.props;
218 const { intl } = this.context; 229 const { intl } = this.context;
219 230
@@ -359,8 +370,26 @@ export default @observer class EditServiceForm extends Component {
359 370
360 <div className="settings__settings-group"> 371 <div className="settings__settings-group">
361 <h3>{intl.formatMessage(messages.headlineGeneral)}</h3> 372 <h3>{intl.formatMessage(messages.headlineGeneral)}</h3>
362 <Toggle field={form.$('isDarkModeEnabled')} />
363 <Toggle field={form.$('isEnabled')} /> 373 <Toggle field={form.$('isEnabled')} />
374 {isHibernationFeatureActive && (
375 <>
376 <Toggle field={form.$('disableHibernation')} />
377 <p className="settings__help">
378 {intl.formatMessage(messages.disableHibernationInfo)}
379 </p>
380 </>
381 )}
382 <Toggle field={form.$('isDarkModeEnabled')} />
383 {form.$('isDarkModeEnabled').value
384 && (
385 <>
386 <h3>{intl.formatMessage(messages.headlineDarkReaderSettings)}</h3>
387 <Slider field={form.$('darkReaderBrightness')} />
388 <Slider field={form.$('darkReaderContrast')} />
389 <Slider field={form.$('darkReaderSepia')} />
390 </>
391 )
392 }
364 </div> 393 </div>
365 </div> 394 </div>
366 <div className="service-icon"> 395 <div className="service-icon">
diff --git a/src/components/settings/settings/EditSettingsForm.js b/src/components/settings/settings/EditSettingsForm.js
index 9564d837b..e1c2a2d4f 100644
--- a/src/components/settings/settings/EditSettingsForm.js
+++ b/src/components/settings/settings/EditSettingsForm.js
@@ -12,21 +12,12 @@ import PremiumFeatureContainer from '../../ui/PremiumFeatureContainer';
12import Input from '../../ui/Input'; 12import Input from '../../ui/Input';
13 13
14import { FRANZ_TRANSLATION } from '../../../config'; 14import { FRANZ_TRANSLATION } from '../../../config';
15import { isMac } from '../../../environment'; 15import { isMac, isWindows } from '../../../environment';
16 16
17const { 17const {
18 systemPreferences, 18 systemPreferences,
19} = remote; 19} = remote;
20 20
21function escapeHtml(unsafe) {
22 return unsafe
23 .replace(/&/g, '&amp;')
24 .replace(/</g, '&lt;')
25 .replace(/>/g, '&gt;')
26 .replace(/"/g, '&quot;')
27 .replace(/'/g, '&#039;');
28}
29
30const messages = defineMessages({ 21const messages = defineMessages({
31 headline: { 22 headline: {
32 id: 'settings.app.headline', 23 id: 'settings.app.headline',
@@ -48,14 +39,6 @@ const messages = defineMessages({
48 id: 'settings.app.inactivityLockInfo', 39 id: 'settings.app.inactivityLockInfo',
49 defaultMessage: '!!!Minutes of inactivity, after which Ferdi should automatically lock. Use 0 to disable', 40 defaultMessage: '!!!Minutes of inactivity, after which Ferdi should automatically lock. Use 0 to disable',
50 }, 41 },
51 serverInfo: {
52 id: 'settings.app.serverInfo',
53 defaultMessage: '!!!We advice you to logout after changing your server as your settings might not be saved otherwise.',
54 },
55 serverMoneyInfo: {
56 id: 'settings.app.serverMoneyInfo',
57 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.',
58 },
59 todoServerInfo: { 42 todoServerInfo: {
60 id: 'settings.app.todoServerInfo', 43 id: 'settings.app.todoServerInfo',
61 defaultMessage: '!!!This server will be used for the "Franz Todo" feature. (default: https://app.franztodos.com)', 44 defaultMessage: '!!!This server will be used for the "Franz Todo" feature. (default: https://app.franztodos.com)',
@@ -173,12 +156,11 @@ export default @observer class EditSettingsForm extends Component {
173 cacheSize: PropTypes.string.isRequired, 156 cacheSize: PropTypes.string.isRequired,
174 isSpellcheckerIncludedInCurrentPlan: PropTypes.bool.isRequired, 157 isSpellcheckerIncludedInCurrentPlan: PropTypes.bool.isRequired,
175 isTodosEnabled: PropTypes.bool.isRequired, 158 isTodosEnabled: PropTypes.bool.isRequired,
159 isTodosActivated: PropTypes.bool.isRequired,
176 isWorkspaceEnabled: PropTypes.bool.isRequired, 160 isWorkspaceEnabled: PropTypes.bool.isRequired,
177 server: PropTypes.string.isRequired, 161 automaticUpdates: PropTypes.bool.isRequired,
178 noUpdates: PropTypes.bool.isRequired,
179 hibernationEnabled: PropTypes.bool.isRequired, 162 hibernationEnabled: PropTypes.bool.isRequired,
180 isDarkmodeEnabled: PropTypes.bool.isRequired, 163 isDarkmodeEnabled: PropTypes.bool.isRequired,
181 isTrayEnabled: PropTypes.bool.isRequired,
182 isAdaptableDarkModeEnabled: PropTypes.bool.isRequired, 164 isAdaptableDarkModeEnabled: PropTypes.bool.isRequired,
183 openProcessManager: PropTypes.func.isRequired, 165 openProcessManager: PropTypes.func.isRequired,
184 }; 166 };
@@ -214,12 +196,11 @@ export default @observer class EditSettingsForm extends Component {
214 isSpellcheckerIncludedInCurrentPlan, 196 isSpellcheckerIncludedInCurrentPlan,
215 isTodosEnabled, 197 isTodosEnabled,
216 isWorkspaceEnabled, 198 isWorkspaceEnabled,
217 server, 199 automaticUpdates,
218 noUpdates,
219 hibernationEnabled, 200 hibernationEnabled,
220 isDarkmodeEnabled, 201 isDarkmodeEnabled,
221 isTrayEnabled,
222 openProcessManager, 202 openProcessManager,
203 isTodosActivated,
223 } = this.props; 204 } = this.props;
224 const { intl } = this.context; 205 const { intl } = this.context;
225 206
@@ -232,8 +213,6 @@ export default @observer class EditSettingsForm extends Component {
232 updateButtonLabelMessage = messages.buttonSearchForUpdate; 213 updateButtonLabelMessage = messages.buttonSearchForUpdate;
233 } 214 }
234 215
235 const isLoggedIn = Boolean(localStorage.getItem('authToken'));
236
237 const { 216 const {
238 lockingFeatureEnabled, 217 lockingFeatureEnabled,
239 scheduledDNDEnabled, 218 scheduledDNDEnabled,
@@ -256,7 +235,7 @@ export default @observer class EditSettingsForm extends Component {
256 <Toggle field={form.$('runInBackground')} /> 235 <Toggle field={form.$('runInBackground')} />
257 <Toggle field={form.$('enableSystemTray')} /> 236 <Toggle field={form.$('enableSystemTray')} />
258 <Toggle field={form.$('reloadAfterResume')} /> 237 <Toggle field={form.$('reloadAfterResume')} />
259 {isTrayEnabled && <Toggle field={form.$('startMinimized')} />} 238 <Toggle field={form.$('startMinimized')} />
260 {process.platform === 'win32' && ( 239 {process.platform === 'win32' && (
261 <Toggle field={form.$('minimizeToSystemTray')} /> 240 <Toggle field={form.$('minimizeToSystemTray')} />
262 )} 241 )}
@@ -287,74 +266,41 @@ export default @observer class EditSettingsForm extends Component {
287 266
288 <Hr /> 267 <Hr />
289 268
290 <Input
291 placeholder="Server"
292 onChange={e => this.submit(e)}
293 field={form.$('server')}
294 autoFocus
295 />
296 {isLoggedIn && (
297 <p
298 className="settings__message"
299 style={{
300 borderTop: 0, marginTop: 0, paddingTop: 0, marginBottom: '2rem',
301 }}
302 >
303 { intl.formatMessage(messages.serverInfo) }
304 </p>
305 )}
306 {server === 'https://api.franzinfra.com' && (
307 <p
308 className="settings__message"
309 style={{
310 borderTop: 0, marginTop: 0, paddingTop: 0, marginBottom: '2rem',
311 }}
312 >
313 <span
314 dangerouslySetInnerHTML={{
315 __html:
316 // Needed to make links work
317 escapeHtml(
318 intl.formatMessage(messages.serverMoneyInfo),
319 ).replace('[Link 1]', '<a href="https://www.meetfranz.com/pricing" target="_blank">')
320 .replace('[Link 2]', '<a href="https://github.com/getferdi/server" target="_blank">')
321 .replace(/\[\/Link]/g, '</a>'),
322 }}
323 style={{
324 whiteSpace: 'pre-wrap',
325 }}
326 />
327 </p>
328 )}
329
330 <Hr />
331
332 {isWorkspaceEnabled && ( 269 {isWorkspaceEnabled && (
333 <Toggle field={form.$('keepAllWorkspacesLoaded')} /> 270 <Toggle field={form.$('keepAllWorkspacesLoaded')} />
334 )} 271 )}
335 272
336
337 <Hr /> 273 <Hr />
338 274
339 {isTodosEnabled && ( 275 {isTodosEnabled && (
340 <> 276 <>
341 <Toggle field={form.$('enableTodos')} /> 277 <Toggle field={form.$('enableTodos')} />
342 <Input 278 {isTodosActivated && (
343 placeholder="Todo Server" 279 <div>
344 onChange={e => this.submit(e)} 280 <Select field={form.$('predefinedTodoServer')} />
345 field={form.$('todoServer')} 281 {form.$('predefinedTodoServer').value === 'isUsingCustomTodoService' && (
346 /> 282 <div>
347 <p 283 <Input
348 className="settings__message" 284 placeholder="Todo Server"
349 style={{ 285 onChange={e => this.submit(e)}
350 borderTop: 0, marginTop: 0, paddingTop: 0, marginBottom: '2rem', 286 field={form.$('customTodoServer')}
351 }} 287 />
352 > 288 <p
353 { intl.formatMessage(messages.todoServerInfo) } 289 className="settings__message"
354 </p> 290 style={{
291 borderTop: 0, marginTop: 0, paddingTop: 0, marginBottom: '2rem',
292 }}
293 >
294 { intl.formatMessage(messages.todoServerInfo) }
295 </p>
296 </div>
297 )}
298 </div>
299 )}
355 </> 300 </>
356 )} 301 )}
357 302
303
358 <Hr /> 304 <Hr />
359 305
360 <Toggle field={form.$('lockingFeatureEnabled')} /> 306 <Toggle field={form.$('lockingFeatureEnabled')} />
@@ -455,10 +401,12 @@ export default @observer class EditSettingsForm extends Component {
455 <Toggle field={form.$('showDisabledServices')} /> 401 <Toggle field={form.$('showDisabledServices')} />
456 <Toggle field={form.$('showMessageBadgeWhenMuted')} /> 402 <Toggle field={form.$('showMessageBadgeWhenMuted')} />
457 403
404 {isMac && <Toggle field={form.$('showDragArea')} />}
405
458 <Hr /> 406 <Hr />
459 407
460 {isMac && <Toggle field={form.$('adaptableDarkMode')} />} 408 {(isMac || isWindows) && <Toggle field={form.$('adaptableDarkMode')} />}
461 {!(isMac && isAdaptableDarkModeEnabled) && <Toggle field={form.$('darkMode')} />} 409 {!((isMac || isWindows) && isAdaptableDarkModeEnabled) && <Toggle field={form.$('darkMode')} />}
462 {(isDarkmodeEnabled || isAdaptableDarkModeEnabled) && ( 410 {(isDarkmodeEnabled || isAdaptableDarkModeEnabled) && (
463 <> 411 <>
464 <Toggle field={form.$('universalDarkMode')} /> 412 <Toggle field={form.$('universalDarkMode')} />
@@ -555,36 +503,46 @@ export default @observer class EditSettingsForm extends Component {
555 503
556 {/* Updates */} 504 {/* Updates */}
557 <h2 id="updates">{intl.formatMessage(messages.headlineUpdates)}</h2> 505 <h2 id="updates">{intl.formatMessage(messages.headlineUpdates)}</h2>
558 {updateIsReadyToInstall ? ( 506 <Toggle field={form.$('automaticUpdates')} />
559 <Button 507 {automaticUpdates && (
560 label={intl.formatMessage(messages.buttonInstallUpdate)} 508 <div>
561 onClick={installUpdate} 509 <Toggle field={form.$('beta')} />
562 /> 510 {updateIsReadyToInstall ? (
563 ) : ( 511 <Button
564 <Button 512 label={intl.formatMessage(messages.buttonInstallUpdate)}
565 buttonType="secondary" 513 onClick={installUpdate}
566 label={intl.formatMessage(updateButtonLabelMessage)} 514 />
567 onClick={checkForUpdates} 515 ) : (
568 disabled={noUpdates || isCheckingForUpdates || isUpdateAvailable} 516 <Button
569 loaded={!isCheckingForUpdates || !isUpdateAvailable} 517 buttonType="secondary"
570 /> 518 label={intl.formatMessage(updateButtonLabelMessage)}
519 onClick={checkForUpdates}
520 disabled={!automaticUpdates || isCheckingForUpdates || isUpdateAvailable}
521 loaded={!isCheckingForUpdates || !isUpdateAvailable}
522 />
523 )}
524 <br />
525 </div>
571 )} 526 )}
572 <br />
573 <Toggle field={form.$('beta')} />
574 <Toggle field={form.$('noUpdates')} />
575 {intl.formatMessage(messages.currentVersion)} 527 {intl.formatMessage(messages.currentVersion)}
576 {' '} 528 {' '}
577 {remote.app.getVersion()} 529 {remote.app.getVersion()}
578 <br /> 530 {noUpdateAvailable && (
579 <br /> 531 <>
580 {noUpdateAvailable && intl.formatMessage(messages.updateStatusUpToDate)} 532 <br />
533 <br />
534 {intl.formatMessage(messages.updateStatusUpToDate)}
535 </>
536 )
537 }
581 <p className="settings__message"> 538 <p className="settings__message">
582
583 <span className="mdi mdi-github-face" /> 539 <span className="mdi mdi-github-face" />
584 <span> 540 <span>
541
585 Ferdi is based on 542 Ferdi is based on
586 {' '} 543 {' '}
587 <a href="https://github.com/meetfranz/franz" target="_blank">Franz</a> 544 <a href="https://github.com/meetfranz/franz" target="_blank">Franz</a>
545
588 , a project published 546 , a project published
589 under the 547 under the
590 {' '} 548 {' '}
diff --git a/src/components/settings/team/TeamDashboard.js b/src/components/settings/team/TeamDashboard.js
index 3d5358d89..72358d485 100644
--- a/src/components/settings/team/TeamDashboard.js
+++ b/src/components/settings/team/TeamDashboard.js
@@ -20,15 +20,15 @@ const messages = defineMessages({
20 }, 20 },
21 contentHeadline: { 21 contentHeadline: {
22 id: 'settings.team.contentHeadline', 22 id: 'settings.team.contentHeadline',
23 defaultMessage: '!!!Ferdi for Teams', 23 defaultMessage: '!!!Franz Team Management',
24 }, 24 },
25 intro: { 25 intro: {
26 id: 'settings.team.intro', 26 id: 'settings.team.intro',
27 defaultMessage: '!!!You and your team use Franz? You can now manage Premium subscriptions for as many colleagues, friends or family members as you want, all from within one account.', 27 defaultMessage: '!!!Your are currently using Franz Servers, which is why you have access to Team Management.',
28 }, 28 },
29 copy: { 29 copy: {
30 id: 'settings.team.copy', 30 id: 'settings.team.copy',
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!', 31 defaultMessage: '!!!Franz\'s Team Management allows you to manage Franz Subscriptions for multiple users. Please keep in mind that having a Franz Premium subscription will give you no advantages in using Ferdi: The only reason you still have access to Team Management is so you can manage your legacy Franz Teams and so that you don\'t loose any functionality in managing your account.',
32 }, 32 },
33 manageButton: { 33 manageButton: {
34 id: 'settings.team.manageAction', 34 id: 'settings.team.manageAction',
diff --git a/src/components/ui/FeatureList.js b/src/components/ui/FeatureList.js
index f1039709c..dbc2a9078 100644
--- a/src/components/ui/FeatureList.js
+++ b/src/components/ui/FeatureList.js
@@ -66,6 +66,10 @@ const messages = defineMessages({
66 id: 'pricing.features.adFree', 66 id: 'pricing.features.adFree',
67 defaultMessage: '!!!Forever ad-free', 67 defaultMessage: '!!!Forever ad-free',
68 }, 68 },
69 appDelayEnabled: {
70 id: 'pricing.features.appDelaysEnabled',
71 defaultMessage: '!!!Occasional Waiting Screens',
72 },
69}); 73});
70 74
71export class FeatureList extends Component { 75export class FeatureList extends Component {
@@ -96,6 +100,7 @@ export class FeatureList extends Component {
96 const features = []; 100 const features = [];
97 if (plan === PLANS.FREE) { 101 if (plan === PLANS.FREE) {
98 features.push( 102 features.push(
103 messages.appDelayEnabled,
99 messages.upToThreeServices, 104 messages.upToThreeServices,
100 messages.availableRecipes, 105 messages.availableRecipes,
101 messages.accountSync, 106 messages.accountSync,
diff --git a/src/components/ui/Slider.js b/src/components/ui/Slider.js
new file mode 100644
index 000000000..b00a6a3f8
--- /dev/null
+++ b/src/components/ui/Slider.js
@@ -0,0 +1,65 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import classnames from 'classnames';
5import { Field } from 'mobx-react-form';
6
7export default @observer class Slider extends Component {
8 static propTypes = {
9 field: PropTypes.instanceOf(Field).isRequired,
10 className: PropTypes.string,
11 showLabel: PropTypes.bool,
12 disabled: PropTypes.bool,
13 };
14
15 static defaultProps = {
16 className: '',
17 showLabel: true,
18 disabled: false,
19 };
20
21 onChange(e) {
22 const { field } = this.props;
23
24 field.onChange(e);
25 }
26
27 render() {
28 const {
29 field,
30 className,
31 showLabel,
32 disabled,
33 } = this.props;
34
35 if (field.value === '' && field.default !== '') {
36 field.value = field.default;
37 }
38
39 return (
40 <div
41 className={classnames([
42 'franz-form__field',
43 'franz-form__slider-wrapper',
44 className,
45 ])}
46 >
47 <div className="slider-container">
48 <input
49 className="slider"
50 type="range"
51 id={field.id}
52 name={field.name}
53 value={field.value}
54 min="1"
55 max="100"
56 onChange={e => (!disabled ? this.onChange(e) : null)}
57 />
58 </div>
59
60 {field.error && <div className={field.error}>{field.error}</div>}
61 {field.label && showLabel && <label className="franz-form__label" htmlFor={field.id}>{field.label}</label>}
62 </div>
63 );
64 }
65}