diff options
author | 2019-05-12 20:00:41 +0700 | |
---|---|---|
committer | 2019-05-12 20:00:41 +0700 | |
commit | d8a1d5f9151cc31f4c2b5c0096a35e49b2c74d61 (patch) | |
tree | 3974d449d8ef389fc61bf880ae758b5debc22a80 /src/components | |
parent | Use dark background in SVG logo (diff) | |
parent | Update CHANGELOG.md (diff) | |
download | ferdium-app-d8a1d5f9151cc31f4c2b5c0096a35e49b2c74d61.tar.gz ferdium-app-d8a1d5f9151cc31f4c2b5c0096a35e49b2c74d61.tar.zst ferdium-app-d8a1d5f9151cc31f4c2b5c0096a35e49b2c74d61.zip |
Merge tag 'v5.1.0'
# Conflicts:
# README.md
# src/components/layout/AppLayout.js
Diffstat (limited to 'src/components')
25 files changed, 747 insertions, 364 deletions
diff --git a/src/components/layout/AppLayout.js b/src/components/layout/AppLayout.js index a1641bc4a..d0476ef04 100644 --- a/src/components/layout/AppLayout.js +++ b/src/components/layout/AppLayout.js | |||
@@ -3,14 +3,20 @@ import PropTypes from 'prop-types'; | |||
3 | import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; | 3 | import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; |
4 | import { defineMessages, intlShape } from 'react-intl'; | 4 | import { defineMessages, intlShape } from 'react-intl'; |
5 | import { TitleBar } from 'electron-react-titlebar'; | 5 | import { TitleBar } from 'electron-react-titlebar'; |
6 | import injectSheet from 'react-jss'; | ||
6 | 7 | ||
7 | import InfoBar from '../ui/InfoBar'; | 8 | import InfoBar from '../ui/InfoBar'; |
8 | import { Component as BasicAuth } from '../../features/basicAuth'; | 9 | import { Component as BasicAuth } from '../../features/basicAuth'; |
10 | import { Component as ShareFranz } from '../../features/shareFranz'; | ||
9 | import ErrorBoundary from '../util/ErrorBoundary'; | 11 | import ErrorBoundary from '../util/ErrorBoundary'; |
10 | 12 | ||
11 | // import globalMessages from '../../i18n/globalMessages'; | 13 | // import globalMessages from '../../i18n/globalMessages'; |
12 | 14 | ||
13 | import { isWindows } from '../../environment'; | 15 | import { isWindows } from '../../environment'; |
16 | import AnnouncementScreen from '../../features/announcements/components/AnnouncementScreen'; | ||
17 | import WorkspaceSwitchingIndicator from '../../features/workspaces/components/WorkspaceSwitchingIndicator'; | ||
18 | import { workspaceStore } from '../../features/workspaces'; | ||
19 | import { announcementActions } from '../../features/announcements/actions'; | ||
14 | 20 | ||
15 | function createMarkup(HTMLString) { | 21 | function createMarkup(HTMLString) { |
16 | return { __html: HTMLString }; | 22 | return { __html: HTMLString }; |
@@ -43,18 +49,30 @@ const messages = defineMessages({ | |||
43 | }, | 49 | }, |
44 | }); | 50 | }); |
45 | 51 | ||
46 | export default | 52 | const styles = theme => ({ |
47 | @observer | 53 | appContent: { |
54 | width: `calc(100% + ${theme.workspaces.drawer.width}px)`, | ||
55 | transition: 'transform 0.5s ease', | ||
56 | transform() { | ||
57 | return workspaceStore.isWorkspaceDrawerOpen ? 'translateX(0)' : `translateX(-${theme.workspaces.drawer.width}px)`; | ||
58 | }, | ||
59 | }, | ||
60 | }); | ||
61 | |||
62 | @injectSheet(styles) @observer | ||
48 | class AppLayout extends Component { | 63 | class AppLayout extends Component { |
49 | static propTypes = { | 64 | static propTypes = { |
65 | classes: PropTypes.object.isRequired, | ||
50 | isFullScreen: PropTypes.bool.isRequired, | 66 | isFullScreen: PropTypes.bool.isRequired, |
51 | sidebar: PropTypes.element.isRequired, | 67 | sidebar: PropTypes.element.isRequired, |
68 | workspacesDrawer: PropTypes.element.isRequired, | ||
52 | services: PropTypes.element.isRequired, | 69 | services: PropTypes.element.isRequired, |
53 | children: PropTypes.element, | 70 | children: PropTypes.element, |
54 | news: MobxPropTypes.arrayOrObservableArray.isRequired, | 71 | news: MobxPropTypes.arrayOrObservableArray.isRequired, |
55 | // isOnline: PropTypes.bool.isRequired, | 72 | // isOnline: PropTypes.bool.isRequired, |
56 | showServicesUpdatedInfoBar: PropTypes.bool.isRequired, | 73 | showServicesUpdatedInfoBar: PropTypes.bool.isRequired, |
57 | appUpdateIsDownloaded: PropTypes.bool.isRequired, | 74 | appUpdateIsDownloaded: PropTypes.bool.isRequired, |
75 | nextAppReleaseVersion: PropTypes.string, | ||
58 | removeNewsItem: PropTypes.func.isRequired, | 76 | removeNewsItem: PropTypes.func.isRequired, |
59 | reloadServicesAfterUpdate: PropTypes.func.isRequired, | 77 | reloadServicesAfterUpdate: PropTypes.func.isRequired, |
60 | installAppUpdate: PropTypes.func.isRequired, | 78 | installAppUpdate: PropTypes.func.isRequired, |
@@ -63,10 +81,13 @@ class AppLayout extends Component { | |||
63 | retryRequiredRequests: PropTypes.func.isRequired, | 81 | retryRequiredRequests: PropTypes.func.isRequired, |
64 | areRequiredRequestsLoading: PropTypes.bool.isRequired, | 82 | areRequiredRequestsLoading: PropTypes.bool.isRequired, |
65 | darkMode: PropTypes.bool.isRequired, | 83 | darkMode: PropTypes.bool.isRequired, |
84 | isDelayAppScreenVisible: PropTypes.bool.isRequired, | ||
85 | isAnnouncementVisible: PropTypes.bool.isRequired, | ||
66 | }; | 86 | }; |
67 | 87 | ||
68 | static defaultProps = { | 88 | static defaultProps = { |
69 | children: [], | 89 | children: [], |
90 | nextAppReleaseVersion: null, | ||
70 | }; | 91 | }; |
71 | 92 | ||
72 | static contextTypes = { | 93 | static contextTypes = { |
@@ -75,7 +96,9 @@ class AppLayout extends Component { | |||
75 | 96 | ||
76 | render() { | 97 | render() { |
77 | const { | 98 | const { |
99 | classes, | ||
78 | isFullScreen, | 100 | isFullScreen, |
101 | workspacesDrawer, | ||
79 | sidebar, | 102 | sidebar, |
80 | services, | 103 | services, |
81 | children, | 104 | children, |
@@ -83,6 +106,7 @@ class AppLayout extends Component { | |||
83 | news, | 106 | news, |
84 | showServicesUpdatedInfoBar, | 107 | showServicesUpdatedInfoBar, |
85 | appUpdateIsDownloaded, | 108 | appUpdateIsDownloaded, |
109 | nextAppReleaseVersion, | ||
86 | removeNewsItem, | 110 | removeNewsItem, |
87 | reloadServicesAfterUpdate, | 111 | reloadServicesAfterUpdate, |
88 | installAppUpdate, | 112 | installAppUpdate, |
@@ -91,6 +115,8 @@ class AppLayout extends Component { | |||
91 | retryRequiredRequests, | 115 | retryRequiredRequests, |
92 | areRequiredRequestsLoading, | 116 | areRequiredRequestsLoading, |
93 | darkMode, | 117 | darkMode, |
118 | isDelayAppScreenVisible, | ||
119 | isAnnouncementVisible, | ||
94 | } = this.props; | 120 | } = this.props; |
95 | 121 | ||
96 | const { intl } = this.context; | 122 | const { intl } = this.context; |
@@ -99,29 +125,23 @@ class AppLayout extends Component { | |||
99 | <ErrorBoundary> | 125 | <ErrorBoundary> |
100 | <div className={darkMode ? 'theme__dark' : ''}> | 126 | <div className={darkMode ? 'theme__dark' : ''}> |
101 | <div className="app"> | 127 | <div className="app"> |
102 | {isWindows && !isFullScreen && ( | 128 | {isWindows && !isFullScreen && <TitleBar menu={window.franz.menu.template} icon="assets/images/logo.svg" />} |
103 | <TitleBar | 129 | <div className={`app__content ${classes.appContent}`}> |
104 | menu={window.franz.menu.template} | 130 | {workspacesDrawer} |
105 | icon="assets/images/logo.svg" | ||
106 | /> | ||
107 | )} | ||
108 | <div className="app__content"> | ||
109 | {sidebar} | 131 | {sidebar} |
110 | <div className="app__service"> | 132 | <div className="app__service"> |
111 | {news.length > 0 | 133 | <WorkspaceSwitchingIndicator /> |
112 | && news.map(item => ( | 134 | {news.length > 0 && news.map(item => ( |
113 | <InfoBar | 135 | <InfoBar |
114 | key={item.id} | 136 | key={item.id} |
115 | position="top" | 137 | position="top" |
116 | type={item.type} | 138 | type={item.type} |
117 | sticky={item.sticky} | 139 | sticky={item.sticky} |
118 | onHide={() => removeNewsItem({ newsId: item.id })} | 140 | onHide={() => removeNewsItem({ newsId: item.id })} |
119 | > | 141 | > |
120 | <span | 142 | <span dangerouslySetInnerHTML={createMarkup(item.message)} /> |
121 | dangerouslySetInnerHTML={createMarkup(item.message)} | 143 | </InfoBar> |
122 | /> | 144 | ))} |
123 | </InfoBar> | ||
124 | ))} | ||
125 | {/* {!isOnline && ( | 145 | {/* {!isOnline && ( |
126 | <InfoBar | 146 | <InfoBar |
127 | type="danger" | 147 | type="danger" |
@@ -164,12 +184,18 @@ class AppLayout extends Component { | |||
164 | <span className="mdi mdi-information" /> | 184 | <span className="mdi mdi-information" /> |
165 | {intl.formatMessage(messages.updateAvailable)} | 185 | {intl.formatMessage(messages.updateAvailable)} |
166 | {' '} | 186 | {' '} |
167 | <a href="https://meetfranz.com/changelog" target="_blank"> | 187 | <button |
188 | className="info-bar__inline-button" | ||
189 | type="button" | ||
190 | onClick={() => announcementActions.show({ targetVersion: nextAppReleaseVersion })} | ||
191 | > | ||
168 | <u>{intl.formatMessage(messages.changelog)}</u> | 192 | <u>{intl.formatMessage(messages.changelog)}</u> |
169 | </a> | 193 | </button> |
170 | </InfoBar> | 194 | </InfoBar> |
171 | )} | 195 | )} |
172 | <BasicAuth /> | 196 | <BasicAuth /> |
197 | <ShareFranz /> | ||
198 | {isAnnouncementVisible && (<AnnouncementScreen />)} | ||
173 | {services} | 199 | {services} |
174 | </div> | 200 | </div> |
175 | </div> | 201 | </div> |
@@ -180,3 +206,5 @@ class AppLayout extends Component { | |||
180 | ); | 206 | ); |
181 | } | 207 | } |
182 | } | 208 | } |
209 | |||
210 | export default AppLayout; | ||
diff --git a/src/components/layout/Sidebar.js b/src/components/layout/Sidebar.js index 609a3b604..36c1f2e39 100644 --- a/src/components/layout/Sidebar.js +++ b/src/components/layout/Sidebar.js | |||
@@ -6,6 +6,8 @@ import { observer } from 'mobx-react'; | |||
6 | 6 | ||
7 | import Tabbar from '../services/tabs/Tabbar'; | 7 | import Tabbar from '../services/tabs/Tabbar'; |
8 | import { ctrlKey } from '../../environment'; | 8 | import { ctrlKey } from '../../environment'; |
9 | import { GA_CATEGORY_WORKSPACES, workspaceStore } from '../../features/workspaces'; | ||
10 | import { gaEvent } from '../../lib/analytics'; | ||
9 | 11 | ||
10 | const messages = defineMessages({ | 12 | const messages = defineMessages({ |
11 | settings: { | 13 | settings: { |
@@ -24,6 +26,14 @@ const messages = defineMessages({ | |||
24 | id: 'sidebar.unmuteApp', | 26 | id: 'sidebar.unmuteApp', |
25 | defaultMessage: '!!!Enable notifications & audio', | 27 | defaultMessage: '!!!Enable notifications & audio', |
26 | }, | 28 | }, |
29 | openWorkspaceDrawer: { | ||
30 | id: 'sidebar.openWorkspaceDrawer', | ||
31 | defaultMessage: '!!!Open workspace drawer', | ||
32 | }, | ||
33 | closeWorkspaceDrawer: { | ||
34 | id: 'sidebar.closeWorkspaceDrawer', | ||
35 | defaultMessage: '!!!Close workspace drawer', | ||
36 | }, | ||
27 | }); | 37 | }); |
28 | 38 | ||
29 | export default @observer class Sidebar extends Component { | 39 | export default @observer class Sidebar extends Component { |
@@ -31,7 +41,9 @@ export default @observer class Sidebar extends Component { | |||
31 | openSettings: PropTypes.func.isRequired, | 41 | openSettings: PropTypes.func.isRequired, |
32 | toggleMuteApp: PropTypes.func.isRequired, | 42 | toggleMuteApp: PropTypes.func.isRequired, |
33 | isAppMuted: PropTypes.bool.isRequired, | 43 | isAppMuted: PropTypes.bool.isRequired, |
34 | } | 44 | isWorkspaceDrawerOpen: PropTypes.bool.isRequired, |
45 | toggleWorkspaceDrawer: PropTypes.func.isRequired, | ||
46 | }; | ||
35 | 47 | ||
36 | static contextTypes = { | 48 | static contextTypes = { |
37 | intl: intlShape, | 49 | intl: intlShape, |
@@ -53,9 +65,23 @@ export default @observer class Sidebar extends Component { | |||
53 | this.setState({ tooltipEnabled: false }); | 65 | this.setState({ tooltipEnabled: false }); |
54 | } | 66 | } |
55 | 67 | ||
68 | updateToolTip() { | ||
69 | this.disableToolTip(); | ||
70 | setTimeout(this.enableToolTip.bind(this)); | ||
71 | } | ||
72 | |||
56 | render() { | 73 | render() { |
57 | const { openSettings, toggleMuteApp, isAppMuted } = this.props; | 74 | const { |
75 | openSettings, | ||
76 | toggleMuteApp, | ||
77 | isAppMuted, | ||
78 | isWorkspaceDrawerOpen, | ||
79 | toggleWorkspaceDrawer, | ||
80 | } = this.props; | ||
58 | const { intl } = this.context; | 81 | const { intl } = this.context; |
82 | const workspaceToggleMessage = ( | ||
83 | isWorkspaceDrawerOpen ? messages.closeWorkspaceDrawer : messages.openWorkspaceDrawer | ||
84 | ); | ||
59 | 85 | ||
60 | return ( | 86 | return ( |
61 | <div className="sidebar"> | 87 | <div className="sidebar"> |
@@ -64,9 +90,26 @@ export default @observer class Sidebar extends Component { | |||
64 | enableToolTip={() => this.enableToolTip()} | 90 | enableToolTip={() => this.enableToolTip()} |
65 | disableToolTip={() => this.disableToolTip()} | 91 | disableToolTip={() => this.disableToolTip()} |
66 | /> | 92 | /> |
93 | {workspaceStore.isFeatureEnabled ? ( | ||
94 | <button | ||
95 | type="button" | ||
96 | onClick={() => { | ||
97 | toggleWorkspaceDrawer(); | ||
98 | this.updateToolTip(); | ||
99 | gaEvent(GA_CATEGORY_WORKSPACES, 'toggleDrawer', 'sidebar'); | ||
100 | }} | ||
101 | className={`sidebar__button sidebar__button--workspaces ${isWorkspaceDrawerOpen ? 'is-active' : ''}`} | ||
102 | data-tip={`${intl.formatMessage(workspaceToggleMessage)} (${ctrlKey}+D)`} | ||
103 | > | ||
104 | <i className="mdi mdi-view-grid" /> | ||
105 | </button> | ||
106 | ) : null} | ||
67 | <button | 107 | <button |
68 | type="button" | 108 | type="button" |
69 | onClick={toggleMuteApp} | 109 | onClick={() => { |
110 | toggleMuteApp(); | ||
111 | this.updateToolTip(); | ||
112 | }} | ||
70 | className={`sidebar__button sidebar__button--audio ${isAppMuted ? 'is-muted' : ''}`} | 113 | className={`sidebar__button sidebar__button--audio ${isAppMuted ? 'is-muted' : ''}`} |
71 | data-tip={`${intl.formatMessage(isAppMuted ? messages.unmute : messages.mute)} (${ctrlKey}+Shift+M)`} | 114 | data-tip={`${intl.formatMessage(isAppMuted ? messages.unmute : messages.mute)} (${ctrlKey}+Shift+M)`} |
72 | > | 115 | > |
diff --git a/src/components/services/content/ServiceView.js b/src/components/services/content/ServiceView.js new file mode 100644 index 000000000..13148b9b3 --- /dev/null +++ b/src/components/services/content/ServiceView.js | |||
@@ -0,0 +1,139 @@ | |||
1 | import React, { Component, Fragment } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { autorun } from 'mobx'; | ||
4 | import { observer } from 'mobx-react'; | ||
5 | import classnames from 'classnames'; | ||
6 | |||
7 | import ServiceModel from '../../../models/Service'; | ||
8 | import StatusBarTargetUrl from '../../ui/StatusBarTargetUrl'; | ||
9 | import WebviewLoader from '../../ui/WebviewLoader'; | ||
10 | import WebviewCrashHandler from './WebviewCrashHandler'; | ||
11 | import WebviewErrorHandler from './ErrorHandlers/WebviewErrorHandler'; | ||
12 | import ServiceDisabled from './ServiceDisabled'; | ||
13 | import ServiceWebview from './ServiceWebview'; | ||
14 | |||
15 | export default @observer class ServiceView extends Component { | ||
16 | static propTypes = { | ||
17 | service: PropTypes.instanceOf(ServiceModel).isRequired, | ||
18 | setWebviewReference: PropTypes.func.isRequired, | ||
19 | detachService: PropTypes.func.isRequired, | ||
20 | reload: PropTypes.func.isRequired, | ||
21 | edit: PropTypes.func.isRequired, | ||
22 | enable: PropTypes.func.isRequired, | ||
23 | isActive: PropTypes.bool, | ||
24 | }; | ||
25 | |||
26 | static defaultProps = { | ||
27 | isActive: false, | ||
28 | }; | ||
29 | |||
30 | state = { | ||
31 | forceRepaint: false, | ||
32 | targetUrl: '', | ||
33 | statusBarVisible: false, | ||
34 | }; | ||
35 | |||
36 | autorunDisposer = null; | ||
37 | |||
38 | forceRepaintTimeout = null; | ||
39 | |||
40 | componentDidMount() { | ||
41 | this.autorunDisposer = autorun(() => { | ||
42 | if (this.props.service.isActive) { | ||
43 | this.setState({ forceRepaint: true }); | ||
44 | this.forceRepaintTimeout = setTimeout(() => { | ||
45 | this.setState({ forceRepaint: false }); | ||
46 | }, 100); | ||
47 | } | ||
48 | }); | ||
49 | } | ||
50 | |||
51 | componentWillUnmount() { | ||
52 | this.autorunDisposer(); | ||
53 | clearTimeout(this.forceRepaintTimeout); | ||
54 | } | ||
55 | |||
56 | updateTargetUrl = (event) => { | ||
57 | let visible = true; | ||
58 | if (event.url === '' || event.url === '#') { | ||
59 | visible = false; | ||
60 | } | ||
61 | this.setState({ | ||
62 | targetUrl: event.url, | ||
63 | statusBarVisible: visible, | ||
64 | }); | ||
65 | }; | ||
66 | |||
67 | render() { | ||
68 | const { | ||
69 | detachService, | ||
70 | service, | ||
71 | setWebviewReference, | ||
72 | reload, | ||
73 | edit, | ||
74 | enable, | ||
75 | } = this.props; | ||
76 | |||
77 | const webviewClasses = classnames({ | ||
78 | services__webview: true, | ||
79 | 'services__webview-wrapper': true, | ||
80 | 'is-active': service.isActive, | ||
81 | 'services__webview--force-repaint': this.state.forceRepaint, | ||
82 | }); | ||
83 | |||
84 | let statusBar = null; | ||
85 | if (this.state.statusBarVisible) { | ||
86 | statusBar = ( | ||
87 | <StatusBarTargetUrl text={this.state.targetUrl} /> | ||
88 | ); | ||
89 | } | ||
90 | |||
91 | return ( | ||
92 | <div className={webviewClasses}> | ||
93 | {service.isActive && service.isEnabled && ( | ||
94 | <Fragment> | ||
95 | {service.hasCrashed && ( | ||
96 | <WebviewCrashHandler | ||
97 | name={service.recipe.name} | ||
98 | webview={service.webview} | ||
99 | reload={reload} | ||
100 | /> | ||
101 | )} | ||
102 | {service.isEnabled && service.isLoading && service.isFirstLoad && ( | ||
103 | <WebviewLoader | ||
104 | loaded={false} | ||
105 | name={service.name} | ||
106 | /> | ||
107 | )} | ||
108 | {service.isError && ( | ||
109 | <WebviewErrorHandler | ||
110 | name={service.recipe.name} | ||
111 | errorMessage={service.errorMessage} | ||
112 | reload={reload} | ||
113 | edit={edit} | ||
114 | /> | ||
115 | )} | ||
116 | </Fragment> | ||
117 | )} | ||
118 | {!service.isEnabled ? ( | ||
119 | <Fragment> | ||
120 | {service.isActive && ( | ||
121 | <ServiceDisabled | ||
122 | name={service.recipe.name} | ||
123 | webview={service.webview} | ||
124 | enable={enable} | ||
125 | /> | ||
126 | )} | ||
127 | </Fragment> | ||
128 | ) : ( | ||
129 | <ServiceWebview | ||
130 | service={service} | ||
131 | setWebviewReference={setWebviewReference} | ||
132 | detachService={detachService} | ||
133 | /> | ||
134 | )} | ||
135 | {statusBar} | ||
136 | </div> | ||
137 | ); | ||
138 | } | ||
139 | } | ||
diff --git a/src/components/services/content/ServiceWebview.js b/src/components/services/content/ServiceWebview.js index bb577e4cc..7252c695f 100644 --- a/src/components/services/content/ServiceWebview.js +++ b/src/components/services/content/ServiceWebview.js | |||
@@ -1,145 +1,50 @@ | |||
1 | import React, { Component, Fragment } from 'react'; | 1 | import React, { Component } from 'react'; |
2 | import PropTypes from 'prop-types'; | 2 | import PropTypes from 'prop-types'; |
3 | import { autorun } from 'mobx'; | ||
4 | import { observer } from 'mobx-react'; | 3 | import { observer } from 'mobx-react'; |
5 | import Webview from 'react-electron-web-view'; | 4 | import ElectronWebView from 'react-electron-web-view'; |
6 | import classnames from 'classnames'; | ||
7 | 5 | ||
8 | import ServiceModel from '../../../models/Service'; | 6 | import ServiceModel from '../../../models/Service'; |
9 | import StatusBarTargetUrl from '../../ui/StatusBarTargetUrl'; | ||
10 | import WebviewLoader from '../../ui/WebviewLoader'; | ||
11 | import WebviewCrashHandler from './WebviewCrashHandler'; | ||
12 | import WebviewErrorHandler from './ErrorHandlers/WebviewErrorHandler'; | ||
13 | import ServiceDisabled from './ServiceDisabled'; | ||
14 | 7 | ||
15 | export default @observer class ServiceWebview extends Component { | 8 | @observer |
9 | class ServiceWebview extends Component { | ||
16 | static propTypes = { | 10 | static propTypes = { |
17 | service: PropTypes.instanceOf(ServiceModel).isRequired, | 11 | service: PropTypes.instanceOf(ServiceModel).isRequired, |
18 | setWebviewReference: PropTypes.func.isRequired, | 12 | setWebviewReference: PropTypes.func.isRequired, |
19 | reload: PropTypes.func.isRequired, | 13 | detachService: PropTypes.func.isRequired, |
20 | edit: PropTypes.func.isRequired, | ||
21 | enable: PropTypes.func.isRequired, | ||
22 | isActive: PropTypes.bool, | ||
23 | }; | 14 | }; |
24 | 15 | ||
25 | static defaultProps = { | ||
26 | isActive: false, | ||
27 | }; | ||
28 | |||
29 | state = { | ||
30 | forceRepaint: false, | ||
31 | targetUrl: '', | ||
32 | statusBarVisible: false, | ||
33 | }; | ||
34 | |||
35 | autorunDisposer = null; | ||
36 | |||
37 | webview = null; | 16 | webview = null; |
38 | 17 | ||
39 | componentDidMount() { | ||
40 | this.autorunDisposer = autorun(() => { | ||
41 | if (this.props.service.isActive) { | ||
42 | this.setState({ forceRepaint: true }); | ||
43 | setTimeout(() => { | ||
44 | this.setState({ forceRepaint: false }); | ||
45 | }, 100); | ||
46 | } | ||
47 | }); | ||
48 | } | ||
49 | |||
50 | componentWillUnmount() { | 18 | componentWillUnmount() { |
51 | this.autorunDisposer(); | 19 | const { service, detachService } = this.props; |
52 | } | 20 | detachService({ service }); |
53 | |||
54 | updateTargetUrl = (event) => { | ||
55 | let visible = true; | ||
56 | if (event.url === '' || event.url === '#') { | ||
57 | visible = false; | ||
58 | } | ||
59 | this.setState({ | ||
60 | targetUrl: event.url, | ||
61 | statusBarVisible: visible, | ||
62 | }); | ||
63 | } | 21 | } |
64 | 22 | ||
65 | render() { | 23 | render() { |
66 | const { | 24 | const { |
67 | service, | 25 | service, |
68 | setWebviewReference, | 26 | setWebviewReference, |
69 | reload, | ||
70 | edit, | ||
71 | enable, | ||
72 | } = this.props; | 27 | } = this.props; |
73 | 28 | ||
74 | const webviewClasses = classnames({ | ||
75 | services__webview: true, | ||
76 | 'services__webview-wrapper': true, | ||
77 | 'is-active': service.isActive, | ||
78 | 'services__webview--force-repaint': this.state.forceRepaint, | ||
79 | }); | ||
80 | |||
81 | let statusBar = null; | ||
82 | if (this.state.statusBarVisible) { | ||
83 | statusBar = ( | ||
84 | <StatusBarTargetUrl text={this.state.targetUrl} /> | ||
85 | ); | ||
86 | } | ||
87 | |||
88 | return ( | 29 | return ( |
89 | <div className={webviewClasses}> | 30 | <ElectronWebView |
90 | {service.isActive && service.isEnabled && ( | 31 | ref={(webview) => { this.webview = webview; }} |
91 | <Fragment> | 32 | autosize |
92 | {service.hasCrashed && ( | 33 | src={service.url} |
93 | <WebviewCrashHandler | 34 | preload="./webview/recipe.js" |
94 | name={service.recipe.name} | 35 | partition={`persist:service-${service.id}`} |
95 | webview={service.webview} | 36 | onDidAttach={() => { |
96 | reload={reload} | 37 | setWebviewReference({ |
97 | /> | 38 | serviceId: service.id, |
98 | )} | 39 | webview: this.webview.view, |
99 | {service.isEnabled && service.isLoading && service.isFirstLoad && ( | 40 | }); |
100 | <WebviewLoader | 41 | }} |
101 | loaded={false} | 42 | onUpdateTargetUrl={this.updateTargetUrl} |
102 | name={service.name} | 43 | useragent={service.userAgent} |
103 | /> | 44 | allowpopups |
104 | )} | 45 | /> |
105 | {service.isError && ( | ||
106 | <WebviewErrorHandler | ||
107 | name={service.recipe.name} | ||
108 | errorMessage={service.errorMessage} | ||
109 | reload={reload} | ||
110 | edit={edit} | ||
111 | /> | ||
112 | )} | ||
113 | </Fragment> | ||
114 | )} | ||
115 | {!service.isEnabled ? ( | ||
116 | <Fragment> | ||
117 | {service.isActive && ( | ||
118 | <ServiceDisabled | ||
119 | name={service.recipe.name} | ||
120 | webview={service.webview} | ||
121 | enable={enable} | ||
122 | /> | ||
123 | )} | ||
124 | </Fragment> | ||
125 | ) : ( | ||
126 | <Webview | ||
127 | ref={(element) => { this.webview = element; }} | ||
128 | autosize | ||
129 | src={service.url} | ||
130 | preload="./webview/recipe.js" | ||
131 | partition={`persist:service-${service.id}`} | ||
132 | onDidAttach={() => setWebviewReference({ | ||
133 | serviceId: service.id, | ||
134 | webview: this.webview.view, | ||
135 | })} | ||
136 | onUpdateTargetUrl={this.updateTargetUrl} | ||
137 | useragent={service.userAgent} | ||
138 | allowpopups | ||
139 | /> | ||
140 | )} | ||
141 | {statusBar} | ||
142 | </div> | ||
143 | ); | 46 | ); |
144 | } | 47 | } |
145 | } | 48 | } |
49 | |||
50 | export default ServiceWebview; | ||
diff --git a/src/components/services/content/Services.js b/src/components/services/content/Services.js index 54f16ba12..8f8c38a11 100644 --- a/src/components/services/content/Services.js +++ b/src/components/services/content/Services.js | |||
@@ -4,7 +4,7 @@ import { observer, PropTypes as MobxPropTypes } 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 | 6 | ||
7 | import Webview from './ServiceWebview'; | 7 | import ServiceView from './ServiceView'; |
8 | import Appear from '../../ui/effects/Appear'; | 8 | import Appear from '../../ui/effects/Appear'; |
9 | 9 | ||
10 | const messages = defineMessages({ | 10 | const messages = defineMessages({ |
@@ -22,6 +22,7 @@ export default @observer class Services extends Component { | |||
22 | static propTypes = { | 22 | static propTypes = { |
23 | services: MobxPropTypes.arrayOrObservableArray, | 23 | services: MobxPropTypes.arrayOrObservableArray, |
24 | setWebviewReference: PropTypes.func.isRequired, | 24 | setWebviewReference: PropTypes.func.isRequired, |
25 | detachService: PropTypes.func.isRequired, | ||
25 | handleIPCMessage: PropTypes.func.isRequired, | 26 | handleIPCMessage: PropTypes.func.isRequired, |
26 | openWindow: PropTypes.func.isRequired, | 27 | openWindow: PropTypes.func.isRequired, |
27 | reload: PropTypes.func.isRequired, | 28 | reload: PropTypes.func.isRequired, |
@@ -42,6 +43,7 @@ export default @observer class Services extends Component { | |||
42 | services, | 43 | services, |
43 | handleIPCMessage, | 44 | handleIPCMessage, |
44 | setWebviewReference, | 45 | setWebviewReference, |
46 | detachService, | ||
45 | openWindow, | 47 | openWindow, |
46 | reload, | 48 | reload, |
47 | openSettings, | 49 | openSettings, |
@@ -71,11 +73,12 @@ export default @observer class Services extends Component { | |||
71 | </Appear> | 73 | </Appear> |
72 | )} | 74 | )} |
73 | {services.map(service => ( | 75 | {services.map(service => ( |
74 | <Webview | 76 | <ServiceView |
75 | key={service.id} | 77 | key={service.id} |
76 | service={service} | 78 | service={service} |
77 | handleIPCMessage={handleIPCMessage} | 79 | handleIPCMessage={handleIPCMessage} |
78 | setWebviewReference={setWebviewReference} | 80 | setWebviewReference={setWebviewReference} |
81 | detachService={detachService} | ||
79 | openWindow={openWindow} | 82 | openWindow={openWindow} |
80 | reload={() => reload({ serviceId: service.id })} | 83 | reload={() => reload({ serviceId: service.id })} |
81 | edit={() => openSettings({ path: `services/edit/${service.id}` })} | 84 | edit={() => openSettings({ path: `services/edit/${service.id}` })} |
diff --git a/src/components/services/content/WebviewCrashHandler.js b/src/components/services/content/WebviewCrashHandler.js index 42bc3c877..7a69dba87 100644 --- a/src/components/services/content/WebviewCrashHandler.js +++ b/src/components/services/content/WebviewCrashHandler.js | |||
@@ -2,6 +2,7 @@ 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 { defineMessages, intlShape } from 'react-intl'; | 4 | import { defineMessages, intlShape } from 'react-intl'; |
5 | import ms from 'ms'; | ||
5 | 6 | ||
6 | import Button from '../../ui/Button'; | 7 | import Button from '../../ui/Button'; |
7 | 8 | ||
@@ -35,12 +36,12 @@ export default @observer class WebviewCrashHandler extends Component { | |||
35 | }; | 36 | }; |
36 | 37 | ||
37 | state = { | 38 | state = { |
38 | countdown: 10000, | 39 | countdown: ms('10s'), |
39 | } | 40 | } |
40 | 41 | ||
41 | countdownInterval = null; | 42 | countdownInterval = null; |
42 | 43 | ||
43 | countdownIntervalTimeout = 1000; | 44 | countdownIntervalTimeout = ms('1s'); |
44 | 45 | ||
45 | 46 | ||
46 | componentDidMount() { | 47 | componentDidMount() { |
@@ -75,7 +76,7 @@ export default @observer class WebviewCrashHandler extends Component { | |||
75 | <p className="footnote"> | 76 | <p className="footnote"> |
76 | {intl.formatMessage(messages.autoReload, { | 77 | {intl.formatMessage(messages.autoReload, { |
77 | name, | 78 | name, |
78 | seconds: this.state.countdown / 1000, | 79 | seconds: this.state.countdown / ms('1s'), |
79 | })} | 80 | })} |
80 | </p> | 81 | </p> |
81 | </div> | 82 | </div> |
diff --git a/src/components/services/tabs/Tabbar.js b/src/components/services/tabs/Tabbar.js index dd5c2140f..5e8260ad0 100644 --- a/src/components/services/tabs/Tabbar.js +++ b/src/components/services/tabs/Tabbar.js | |||
@@ -19,7 +19,7 @@ export default @observer class TabBar extends Component { | |||
19 | updateService: PropTypes.func.isRequired, | 19 | updateService: PropTypes.func.isRequired, |
20 | showMessageBadgeWhenMutedSetting: PropTypes.bool.isRequired, | 20 | showMessageBadgeWhenMutedSetting: PropTypes.bool.isRequired, |
21 | showMessageBadgesEvenWhenMuted: PropTypes.bool.isRequired, | 21 | showMessageBadgesEvenWhenMuted: PropTypes.bool.isRequired, |
22 | } | 22 | }; |
23 | 23 | ||
24 | onSortEnd = ({ oldIndex, newIndex }) => { | 24 | onSortEnd = ({ oldIndex, newIndex }) => { |
25 | const { | 25 | const { |
@@ -45,7 +45,7 @@ export default @observer class TabBar extends Component { | |||
45 | redirect: false, | 45 | redirect: false, |
46 | }); | 46 | }); |
47 | } | 47 | } |
48 | } | 48 | }; |
49 | 49 | ||
50 | disableService({ serviceId }) { | 50 | disableService({ serviceId }) { |
51 | this.toggleService({ serviceId, isEnabled: false }); | 51 | this.toggleService({ serviceId, isEnabled: false }); |
diff --git a/src/components/settings/account/AccountDashboard.js b/src/components/settings/account/AccountDashboard.js index 9c9543749..3f6964b6b 100644 --- a/src/components/settings/account/AccountDashboard.js +++ b/src/components/settings/account/AccountDashboard.js | |||
@@ -3,12 +3,11 @@ import PropTypes from 'prop-types'; | |||
3 | import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; | 3 | import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; |
4 | import { defineMessages, intlShape } from 'react-intl'; | 4 | import { defineMessages, intlShape } from 'react-intl'; |
5 | import ReactTooltip from 'react-tooltip'; | 5 | import ReactTooltip from 'react-tooltip'; |
6 | import moment from 'moment'; | 6 | import { ProBadge } from '@meetfranz/ui'; |
7 | 7 | ||
8 | import Loader from '../../ui/Loader'; | 8 | import Loader from '../../ui/Loader'; |
9 | import Button from '../../ui/Button'; | 9 | import Button from '../../ui/Button'; |
10 | import Infobox from '../../ui/Infobox'; | 10 | import Infobox from '../../ui/Infobox'; |
11 | import Link from '../../ui/Link'; | ||
12 | import SubscriptionForm from '../../../containers/subscription/SubscriptionFormScreen'; | 11 | import SubscriptionForm from '../../../containers/subscription/SubscriptionFormScreen'; |
13 | 12 | ||
14 | const messages = defineMessages({ | 13 | const messages = defineMessages({ |
@@ -24,10 +23,6 @@ const messages = defineMessages({ | |||
24 | id: 'settings.account.headlineUpgrade', | 23 | id: 'settings.account.headlineUpgrade', |
25 | defaultMessage: '!!!Upgrade your Account', | 24 | defaultMessage: '!!!Upgrade your Account', |
26 | }, | 25 | }, |
27 | headlineInvoices: { | ||
28 | id: 'settings.account.headlineInvoices', | ||
29 | defaultMessage: '!!Invoices', | ||
30 | }, | ||
31 | headlineDangerZone: { | 26 | headlineDangerZone: { |
32 | id: 'settings.account.headlineDangerZone', | 27 | id: 'settings.account.headlineDangerZone', |
33 | defaultMessage: '!!Danger Zone', | 28 | defaultMessage: '!!Danger Zone', |
@@ -44,14 +39,14 @@ const messages = defineMessages({ | |||
44 | id: 'settings.account.accountType.premium', | 39 | id: 'settings.account.accountType.premium', |
45 | defaultMessage: '!!!Premium Supporter Account', | 40 | defaultMessage: '!!!Premium Supporter Account', |
46 | }, | 41 | }, |
47 | accountTypeEnterprise: { | ||
48 | id: 'settings.account.accountType.enterprise', | ||
49 | defaultMessage: '!!!Enterprise Account', | ||
50 | }, | ||
51 | accountEditButton: { | 42 | accountEditButton: { |
52 | id: 'settings.account.account.editButton', | 43 | id: 'settings.account.account.editButton', |
53 | defaultMessage: '!!!Edit Account', | 44 | defaultMessage: '!!!Edit Account', |
54 | }, | 45 | }, |
46 | invoicesButton: { | ||
47 | id: 'settings.account.headlineInvoices', | ||
48 | defaultMessage: '!!Invoices', | ||
49 | }, | ||
55 | invoiceDownload: { | 50 | invoiceDownload: { |
56 | id: 'settings.account.invoiceDownload', | 51 | id: 'settings.account.invoiceDownload', |
57 | defaultMessage: '!!!Download', | 52 | defaultMessage: '!!!Download', |
@@ -81,19 +76,17 @@ const messages = defineMessages({ | |||
81 | export default @observer class AccountDashboard extends Component { | 76 | export default @observer class AccountDashboard extends Component { |
82 | static propTypes = { | 77 | static propTypes = { |
83 | user: MobxPropTypes.observableObject.isRequired, | 78 | user: MobxPropTypes.observableObject.isRequired, |
84 | orders: MobxPropTypes.arrayOrObservableArray.isRequired, | ||
85 | isLoading: PropTypes.bool.isRequired, | 79 | isLoading: PropTypes.bool.isRequired, |
86 | isLoadingOrdersInfo: PropTypes.bool.isRequired, | ||
87 | isLoadingPlans: PropTypes.bool.isRequired, | 80 | isLoadingPlans: PropTypes.bool.isRequired, |
88 | isCreatingPaymentDashboardUrl: PropTypes.bool.isRequired, | ||
89 | userInfoRequestFailed: PropTypes.bool.isRequired, | 81 | userInfoRequestFailed: PropTypes.bool.isRequired, |
90 | retryUserInfoRequest: PropTypes.func.isRequired, | 82 | retryUserInfoRequest: PropTypes.func.isRequired, |
91 | openDashboard: PropTypes.func.isRequired, | ||
92 | openExternalUrl: PropTypes.func.isRequired, | ||
93 | onCloseSubscriptionWindow: PropTypes.func.isRequired, | 83 | onCloseSubscriptionWindow: PropTypes.func.isRequired, |
94 | deleteAccount: PropTypes.func.isRequired, | 84 | deleteAccount: PropTypes.func.isRequired, |
95 | isLoadingDeleteAccount: PropTypes.bool.isRequired, | 85 | isLoadingDeleteAccount: PropTypes.bool.isRequired, |
96 | isDeleteAccountSuccessful: PropTypes.bool.isRequired, | 86 | isDeleteAccountSuccessful: PropTypes.bool.isRequired, |
87 | openEditAccount: PropTypes.func.isRequired, | ||
88 | openBilling: PropTypes.func.isRequired, | ||
89 | openInvoices: PropTypes.func.isRequired, | ||
97 | }; | 90 | }; |
98 | 91 | ||
99 | static contextTypes = { | 92 | static contextTypes = { |
@@ -103,12 +96,7 @@ export default @observer class AccountDashboard extends Component { | |||
103 | render() { | 96 | render() { |
104 | const { | 97 | const { |
105 | user, | 98 | user, |
106 | orders, | ||
107 | isLoading, | 99 | isLoading, |
108 | isCreatingPaymentDashboardUrl, | ||
109 | openDashboard, | ||
110 | openExternalUrl, | ||
111 | isLoadingOrdersInfo, | ||
112 | isLoadingPlans, | 100 | isLoadingPlans, |
113 | userInfoRequestFailed, | 101 | userInfoRequestFailed, |
114 | retryUserInfoRequest, | 102 | retryUserInfoRequest, |
@@ -116,6 +104,9 @@ export default @observer class AccountDashboard extends Component { | |||
116 | deleteAccount, | 104 | deleteAccount, |
117 | isLoadingDeleteAccount, | 105 | isLoadingDeleteAccount, |
118 | isDeleteAccountSuccessful, | 106 | isDeleteAccountSuccessful, |
107 | openEditAccount, | ||
108 | openBilling, | ||
109 | openInvoices, | ||
119 | } = this.props; | 110 | } = this.props; |
120 | const { intl } = this.context; | 111 | const { intl } = this.context; |
121 | 112 | ||
@@ -153,116 +144,56 @@ export default @observer class AccountDashboard extends Component { | |||
153 | src="./assets/images/logo.svg" | 144 | src="./assets/images/logo.svg" |
154 | alt="" | 145 | alt="" |
155 | /> | 146 | /> |
156 | {user.isPremium && ( | ||
157 | <span | ||
158 | className="account__avatar-premium emoji" | ||
159 | data-tip="Premium Supporter Account" | ||
160 | > | ||
161 | <img src="./assets/images/emoji/star.png" alt="" /> | ||
162 | </span> | ||
163 | )} | ||
164 | </div> | 147 | </div> |
165 | <div className="account__info"> | 148 | <div className="account__info"> |
166 | <h2> | 149 | <h2> |
167 | {`${user.firstname} ${user.lastname}`} | 150 | <span className="username">{`${user.firstname} ${user.lastname}`}</span> |
151 | {user.isPremium && ( | ||
152 | <> | ||
153 | {' '} | ||
154 | <ProBadge /> | ||
155 | <span className="badge badge--premium">{intl.formatMessage(messages.accountTypePremium)}</span> | ||
156 | </> | ||
157 | )} | ||
168 | </h2> | 158 | </h2> |
169 | {user.organization && `${user.organization}, `} | 159 | {user.organization && `${user.organization}, `} |
170 | {user.email} | 160 | {user.email} |
171 | <br /> | ||
172 | {!user.isEnterprise && !user.isPremium && ( | ||
173 | <span className="badge badge">{intl.formatMessage(messages.accountTypeBasic)}</span> | ||
174 | )} | ||
175 | {user.isPremium && ( | 161 | {user.isPremium && ( |
176 | <span className="badge badge--premium">{intl.formatMessage(messages.accountTypePremium)}</span> | 162 | <div className="manage-user-links"> |
177 | )} | 163 | <Button |
178 | {user.isEnterprise && ( | 164 | label={intl.formatMessage(messages.accountEditButton)} |
179 | <span className="badge badge--success">{intl.formatMessage(messages.accountTypeEnterprise)}</span> | 165 | className="franz-form__button--inverted" |
166 | onClick={openEditAccount} | ||
167 | /> | ||
168 | {user.isSubscriptionOwner && ( | ||
169 | <> | ||
170 | <Button | ||
171 | label={intl.formatMessage(messages.manageSubscriptionButtonLabel)} | ||
172 | className="franz-form__button--inverted" | ||
173 | onClick={openBilling} | ||
174 | /> | ||
175 | <Button | ||
176 | label={intl.formatMessage(messages.invoicesButton)} | ||
177 | className="franz-form__button--inverted" | ||
178 | onClick={openInvoices} | ||
179 | /> | ||
180 | </> | ||
181 | )} | ||
182 | </div> | ||
180 | )} | 183 | )} |
181 | </div> | 184 | </div> |
182 | <Link to="/settings/user/edit" className="button"> | 185 | {!user.isPremium && ( |
183 | {intl.formatMessage(messages.accountEditButton)} | 186 | <Button |
184 | </Link> | 187 | label={intl.formatMessage(messages.accountEditButton)} |
185 | {user.emailValidated} | 188 | className="franz-form__button--inverted" |
186 | </div> | 189 | onClick={openEditAccount} |
187 | </div> | 190 | /> |
188 | )} | ||
189 | |||
190 | {user.isSubscriptionOwner && ( | ||
191 | isLoadingOrdersInfo ? ( | ||
192 | <Loader /> | ||
193 | ) : ( | ||
194 | <div className="account franz-form"> | ||
195 | {orders.length > 0 && ( | ||
196 | <Fragment> | ||
197 | <div className="account__box"> | ||
198 | <h2>{intl.formatMessage(messages.headlineSubscription)}</h2> | ||
199 | <div className="account__subscription"> | ||
200 | {orders[0].name} | ||
201 | <span className="badge">{orders[0].price}</span> | ||
202 | <Button | ||
203 | label={intl.formatMessage(messages.manageSubscriptionButtonLabel)} | ||
204 | className="account__subscription-button franz-form__button--inverted" | ||
205 | loaded={!isCreatingPaymentDashboardUrl} | ||
206 | onClick={() => openDashboard()} | ||
207 | /> | ||
208 | </div> | ||
209 | </div> | ||
210 | <div className="account__box"> | ||
211 | <h2>{intl.formatMessage(messages.headlineInvoices)}</h2> | ||
212 | <table className="invoices"> | ||
213 | <tbody> | ||
214 | {orders.map(order => ( | ||
215 | <tr key={order.id}> | ||
216 | <td className="invoices__date"> | ||
217 | {moment(order.date).format('DD.MM.YYYY')} | ||
218 | </td> | ||
219 | <td className="invoices__action"> | ||
220 | <button | ||
221 | type="button" | ||
222 | onClick={() => openExternalUrl(order.invoiceUrl)} | ||
223 | > | ||
224 | {intl.formatMessage(messages.invoiceDownload)} | ||
225 | </button> | ||
226 | </td> | ||
227 | </tr> | ||
228 | ))} | ||
229 | </tbody> | ||
230 | </table> | ||
231 | </div> | ||
232 | </Fragment> | ||
233 | )} | 191 | )} |
234 | </div> | 192 | </div> |
235 | ) | ||
236 | )} | ||
237 | |||
238 | {user.isEnterprise && ( | ||
239 | <div className="account"> | ||
240 | <div className="account__box"> | ||
241 | <h2>{user.company.name}</h2> | ||
242 | <p> | ||
243 | Technical contact: | ||
244 | <Link | ||
245 | className="link" | ||
246 | target="_blank" | ||
247 | to={`mailto:${user.company.contact.technical}?subject=Franz`} | ||
248 | > | ||
249 | {user.company.contact.technical} | ||
250 | </Link> | ||
251 | <br /> | ||
252 | General contact: | ||
253 | <Link | ||
254 | className="link" | ||
255 | target="_blank" | ||
256 | to={`mailto:${user.company.contact.default}?subject=Franz`} | ||
257 | > | ||
258 | {user.company.contact.default} | ||
259 | </Link> | ||
260 | </p> | ||
261 | </div> | ||
262 | </div> | 193 | </div> |
263 | )} | 194 | )} |
264 | 195 | ||
265 | {!user.isEnterprise && !user.isPremium && ( | 196 | {!user.isPremium && ( |
266 | isLoadingPlans ? ( | 197 | isLoadingPlans ? ( |
267 | <Loader /> | 198 | <Loader /> |
268 | ) : ( | 199 | ) : ( |
@@ -277,27 +208,25 @@ export default @observer class AccountDashboard extends Component { | |||
277 | ) | 208 | ) |
278 | )} | 209 | )} |
279 | 210 | ||
280 | {!user.isEnterprise && ( | 211 | <div className="account franz-form"> |
281 | <div className="account franz-form"> | 212 | <div className="account__box"> |
282 | <div className="account__box"> | 213 | <h2>{intl.formatMessage(messages.headlineDangerZone)}</h2> |
283 | <h2>{intl.formatMessage(messages.headlineDangerZone)}</h2> | 214 | {!isDeleteAccountSuccessful && ( |
284 | {!isDeleteAccountSuccessful && ( | 215 | <div className="account__subscription"> |
285 | <div className="account__subscription"> | 216 | <p>{intl.formatMessage(messages.deleteInfo)}</p> |
286 | <p>{intl.formatMessage(messages.deleteInfo)}</p> | 217 | <Button |
287 | <Button | 218 | label={intl.formatMessage(messages.deleteAccount)} |
288 | label={intl.formatMessage(messages.deleteAccount)} | 219 | buttonType="danger" |
289 | buttonType="danger" | 220 | onClick={() => deleteAccount()} |
290 | onClick={() => deleteAccount()} | 221 | loaded={!isLoadingDeleteAccount} |
291 | loaded={!isLoadingDeleteAccount} | 222 | /> |
292 | /> | ||
293 | </div> | ||
294 | )} | ||
295 | {isDeleteAccountSuccessful && ( | ||
296 | <p>{intl.formatMessage(messages.deleteEmailSent)}</p> | ||
297 | )} | ||
298 | </div> | 223 | </div> |
224 | )} | ||
225 | {isDeleteAccountSuccessful && ( | ||
226 | <p>{intl.formatMessage(messages.deleteEmailSent)}</p> | ||
227 | )} | ||
299 | </div> | 228 | </div> |
300 | )} | 229 | </div> |
301 | </Fragment> | 230 | </Fragment> |
302 | )} | 231 | )} |
303 | </div> | 232 | </div> |
diff --git a/src/components/settings/navigation/SettingsNavigation.js b/src/components/settings/navigation/SettingsNavigation.js index 953f702f8..df4b3b3b2 100644 --- a/src/components/settings/navigation/SettingsNavigation.js +++ b/src/components/settings/navigation/SettingsNavigation.js | |||
@@ -2,8 +2,12 @@ import React, { Component } from 'react'; | |||
2 | import PropTypes from 'prop-types'; | 2 | 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 | 6 | ||
6 | import Link from '../../ui/Link'; | 7 | import Link from '../../ui/Link'; |
8 | import { workspaceStore } from '../../../features/workspaces'; | ||
9 | import UIStore from '../../../stores/UIStore'; | ||
10 | import UserStore from '../../../stores/UserStore'; | ||
7 | 11 | ||
8 | const messages = defineMessages({ | 12 | const messages = defineMessages({ |
9 | availableServices: { | 13 | availableServices: { |
@@ -14,10 +18,18 @@ const messages = defineMessages({ | |||
14 | id: 'settings.navigation.yourServices', | 18 | id: 'settings.navigation.yourServices', |
15 | defaultMessage: '!!!Your services', | 19 | defaultMessage: '!!!Your services', |
16 | }, | 20 | }, |
21 | yourWorkspaces: { | ||
22 | id: 'settings.navigation.yourWorkspaces', | ||
23 | defaultMessage: '!!!Your workspaces', | ||
24 | }, | ||
17 | account: { | 25 | account: { |
18 | id: 'settings.navigation.account', | 26 | id: 'settings.navigation.account', |
19 | defaultMessage: '!!!Account', | 27 | defaultMessage: '!!!Account', |
20 | }, | 28 | }, |
29 | team: { | ||
30 | id: 'settings.navigation.team', | ||
31 | defaultMessage: '!!!Manage Team', | ||
32 | }, | ||
21 | settings: { | 33 | settings: { |
22 | id: 'settings.navigation.settings', | 34 | id: 'settings.navigation.settings', |
23 | defaultMessage: '!!!Settings', | 35 | defaultMessage: '!!!Settings', |
@@ -34,7 +46,12 @@ const messages = defineMessages({ | |||
34 | 46 | ||
35 | export default @inject('stores') @observer class SettingsNavigation extends Component { | 47 | export default @inject('stores') @observer class SettingsNavigation extends Component { |
36 | static propTypes = { | 48 | static propTypes = { |
49 | stores: PropTypes.shape({ | ||
50 | ui: PropTypes.instanceOf(UIStore).isRequired, | ||
51 | user: PropTypes.instanceOf(UserStore).isRequired, | ||
52 | }).isRequired, | ||
37 | serviceCount: PropTypes.number.isRequired, | 53 | serviceCount: PropTypes.number.isRequired, |
54 | workspaceCount: PropTypes.number.isRequired, | ||
38 | }; | 55 | }; |
39 | 56 | ||
40 | static contextTypes = { | 57 | static contextTypes = { |
@@ -42,7 +59,9 @@ export default @inject('stores') @observer class SettingsNavigation extends Comp | |||
42 | }; | 59 | }; |
43 | 60 | ||
44 | render() { | 61 | render() { |
45 | const { serviceCount } = this.props; | 62 | const { serviceCount, workspaceCount, stores } = this.props; |
63 | const { isDarkThemeActive } = stores.ui; | ||
64 | const { router, user } = stores; | ||
46 | const { intl } = this.context; | 65 | const { intl } = this.context; |
47 | 66 | ||
48 | return ( | 67 | return ( |
@@ -63,6 +82,21 @@ export default @inject('stores') @observer class SettingsNavigation extends Comp | |||
63 | {' '} | 82 | {' '} |
64 | <span className="badge">{serviceCount}</span> | 83 | <span className="badge">{serviceCount}</span> |
65 | </Link> | 84 | </Link> |
85 | {workspaceStore.isFeatureEnabled ? ( | ||
86 | <Link | ||
87 | to="/settings/workspaces" | ||
88 | className="settings-navigation__link" | ||
89 | activeClassName="is-active" | ||
90 | > | ||
91 | {intl.formatMessage(messages.yourWorkspaces)} | ||
92 | {' '} | ||
93 | {workspaceStore.isPremiumUpgradeRequired ? ( | ||
94 | <ProBadge inverted={!isDarkThemeActive && workspaceStore.isSettingsRouteActive} /> | ||
95 | ) : ( | ||
96 | <span className="badge">{workspaceCount}</span> | ||
97 | )} | ||
98 | </Link> | ||
99 | ) : null} | ||
66 | <Link | 100 | <Link |
67 | to="/settings/user" | 101 | to="/settings/user" |
68 | className="settings-navigation__link" | 102 | className="settings-navigation__link" |
@@ -71,6 +105,16 @@ export default @inject('stores') @observer class SettingsNavigation extends Comp | |||
71 | {intl.formatMessage(messages.account)} | 105 | {intl.formatMessage(messages.account)} |
72 | </Link> | 106 | </Link> |
73 | <Link | 107 | <Link |
108 | to="/settings/team" | ||
109 | className="settings-navigation__link" | ||
110 | activeClassName="is-active" | ||
111 | > | ||
112 | {intl.formatMessage(messages.team)} | ||
113 | {!user.data.isPremium && ( | ||
114 | <ProBadge inverted={!isDarkThemeActive && router.location.pathname === '/settings/team'} /> | ||
115 | )} | ||
116 | </Link> | ||
117 | <Link | ||
74 | to="/settings/app" | 118 | to="/settings/app" |
75 | className="settings-navigation__link" | 119 | className="settings-navigation__link" |
76 | activeClassName="is-active" | 120 | activeClassName="is-active" |
diff --git a/src/components/settings/services/EditServiceForm.js b/src/components/settings/services/EditServiceForm.js index 468d85c45..4ba2eb844 100644 --- a/src/components/settings/services/EditServiceForm.js +++ b/src/components/settings/services/EditServiceForm.js | |||
@@ -128,7 +128,8 @@ export default @observer class EditServiceForm extends Component { | |||
128 | isSaving: PropTypes.bool.isRequired, | 128 | isSaving: PropTypes.bool.isRequired, |
129 | isDeleting: PropTypes.bool.isRequired, | 129 | isDeleting: PropTypes.bool.isRequired, |
130 | isProxyFeatureEnabled: PropTypes.bool.isRequired, | 130 | isProxyFeatureEnabled: PropTypes.bool.isRequired, |
131 | isProxyFeaturePremiumFeature: PropTypes.bool.isRequired, | 131 | isProxyPremiumFeature: PropTypes.bool.isRequired, |
132 | isSpellcheckerPremiumFeature: PropTypes.bool.isRequired, | ||
132 | }; | 133 | }; |
133 | 134 | ||
134 | static defaultProps = { | 135 | static defaultProps = { |
@@ -191,7 +192,8 @@ export default @observer class EditServiceForm extends Component { | |||
191 | isDeleting, | 192 | isDeleting, |
192 | onDelete, | 193 | onDelete, |
193 | isProxyFeatureEnabled, | 194 | isProxyFeatureEnabled, |
194 | isProxyFeaturePremiumFeature, | 195 | isProxyPremiumFeature, |
196 | isSpellcheckerPremiumFeature, | ||
195 | } = this.props; | 197 | } = this.props; |
196 | const { intl } = this.context; | 198 | const { intl } = this.context; |
197 | 199 | ||
@@ -339,14 +341,20 @@ export default @observer class EditServiceForm extends Component { | |||
339 | </div> | 341 | </div> |
340 | </div> | 342 | </div> |
341 | 343 | ||
342 | <PremiumFeatureContainer> | 344 | <PremiumFeatureContainer |
345 | condition={isSpellcheckerPremiumFeature} | ||
346 | gaEventInfo={{ category: 'User', event: 'upgrade', label: 'spellchecker' }} | ||
347 | > | ||
343 | <div className="settings__settings-group"> | 348 | <div className="settings__settings-group"> |
344 | <Select field={form.$('spellcheckerLanguage')} /> | 349 | <Select field={form.$('spellcheckerLanguage')} /> |
345 | </div> | 350 | </div> |
346 | </PremiumFeatureContainer> | 351 | </PremiumFeatureContainer> |
347 | 352 | ||
348 | {isProxyFeatureEnabled && ( | 353 | {isProxyFeatureEnabled && ( |
349 | <PremiumFeatureContainer condition={isProxyFeaturePremiumFeature}> | 354 | <PremiumFeatureContainer |
355 | condition={isProxyPremiumFeature} | ||
356 | gaEventInfo={{ category: 'User', event: 'upgrade', label: 'proxy' }} | ||
357 | > | ||
350 | <div className="settings__settings-group"> | 358 | <div className="settings__settings-group"> |
351 | <h3> | 359 | <h3> |
352 | {intl.formatMessage(messages.headlineProxy)} | 360 | {intl.formatMessage(messages.headlineProxy)} |
diff --git a/src/components/settings/services/ServicesDashboard.js b/src/components/settings/services/ServicesDashboard.js index a12df7372..53bae12df 100644 --- a/src/components/settings/services/ServicesDashboard.js +++ b/src/components/settings/services/ServicesDashboard.js | |||
@@ -65,7 +65,7 @@ export default @observer class ServicesDashboard extends Component { | |||
65 | 65 | ||
66 | static defaultProps = { | 66 | static defaultProps = { |
67 | searchNeedle: '', | 67 | searchNeedle: '', |
68 | } | 68 | }; |
69 | 69 | ||
70 | static contextTypes = { | 70 | static contextTypes = { |
71 | intl: intlShape, | 71 | intl: intlShape, |
diff --git a/src/components/settings/settings/EditSettingsForm.js b/src/components/settings/settings/EditSettingsForm.js index a92e559f3..efd453356 100644 --- a/src/components/settings/settings/EditSettingsForm.js +++ b/src/components/settings/settings/EditSettingsForm.js | |||
@@ -81,6 +81,10 @@ const messages = defineMessages({ | |||
81 | id: 'settings.app.restartRequired', | 81 | id: 'settings.app.restartRequired', |
82 | defaultMessage: '!!!Changes require restart', | 82 | defaultMessage: '!!!Changes require restart', |
83 | }, | 83 | }, |
84 | languageDisclaimer: { | ||
85 | id: 'settings.app.languageDisclaimer', | ||
86 | defaultMessage: '!!!Official translations are English & German. All other languages are community based translations.', | ||
87 | }, | ||
84 | }); | 88 | }); |
85 | 89 | ||
86 | export default @observer class EditSettingsForm extends Component { | 90 | export default @observer class EditSettingsForm extends Component { |
@@ -170,6 +174,7 @@ export default @observer class EditSettingsForm extends Component { | |||
170 | <Select field={form.$('locale')} showLabel={false} /> | 174 | <Select field={form.$('locale')} showLabel={false} /> |
171 | <PremiumFeatureContainer | 175 | <PremiumFeatureContainer |
172 | condition={isSpellcheckerPremiumFeature} | 176 | condition={isSpellcheckerPremiumFeature} |
177 | gaEventInfo={{ category: 'User', event: 'upgrade', label: 'spellchecker' }} | ||
173 | > | 178 | > |
174 | <Fragment> | 179 | <Fragment> |
175 | <Toggle | 180 | <Toggle |
@@ -238,6 +243,10 @@ export default @observer class EditSettingsForm extends Component { | |||
238 | {intl.formatMessage(messages.currentVersion)} | 243 | {intl.formatMessage(messages.currentVersion)} |
239 | {' '} | 244 | {' '} |
240 | {remote.app.getVersion()} | 245 | {remote.app.getVersion()} |
246 | <p className="settings__message"> | ||
247 | <span className="mdi mdi-information" /> | ||
248 | {intl.formatMessage(messages.languageDisclaimer)} | ||
249 | </p> | ||
241 | </form> | 250 | </form> |
242 | </div> | 251 | </div> |
243 | </div> | 252 | </div> |
diff --git a/src/components/settings/team/TeamDashboard.js b/src/components/settings/team/TeamDashboard.js new file mode 100644 index 000000000..82c517fcb --- /dev/null +++ b/src/components/settings/team/TeamDashboard.js | |||
@@ -0,0 +1,152 @@ | |||
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 | import ReactTooltip from 'react-tooltip'; | ||
6 | import injectSheet from 'react-jss'; | ||
7 | |||
8 | import Loader from '../../ui/Loader'; | ||
9 | import Button from '../../ui/Button'; | ||
10 | import Infobox from '../../ui/Infobox'; | ||
11 | import PremiumFeatureContainer from '../../ui/PremiumFeatureContainer'; | ||
12 | |||
13 | const messages = defineMessages({ | ||
14 | headline: { | ||
15 | id: 'settings.team.headline', | ||
16 | defaultMessage: '!!!Team', | ||
17 | }, | ||
18 | contentHeadline: { | ||
19 | id: 'settings.team.contentHeadline', | ||
20 | defaultMessage: '!!!Franz for Teams', | ||
21 | }, | ||
22 | intro: { | ||
23 | id: 'settings.team.intro', | ||
24 | 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.', | ||
25 | }, | ||
26 | copy: { | ||
27 | id: 'settings.team.copy', | ||
28 | 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!', | ||
29 | }, | ||
30 | manageButton: { | ||
31 | id: 'settings.team.manageAction', | ||
32 | defaultMessage: '!!!Manage your Team on meetfranz.com', | ||
33 | }, | ||
34 | upgradeButton: { | ||
35 | id: 'settings.team.upgradeAction', | ||
36 | defaultMessage: '!!!Upgrade your Account', | ||
37 | }, | ||
38 | }); | ||
39 | |||
40 | const styles = { | ||
41 | cta: { | ||
42 | margin: [40, 'auto'], | ||
43 | }, | ||
44 | container: { | ||
45 | display: 'flex', | ||
46 | flexDirection: 'column', | ||
47 | height: 'auto', | ||
48 | |||
49 | '@media(min-width: 800px)': { | ||
50 | flexDirection: 'row', | ||
51 | }, | ||
52 | }, | ||
53 | content: { | ||
54 | height: 'auto', | ||
55 | order: 1, | ||
56 | |||
57 | '@media(min-width: 800px)': { | ||
58 | order: 0, | ||
59 | }, | ||
60 | }, | ||
61 | image: { | ||
62 | display: 'block', | ||
63 | height: 150, | ||
64 | order: 0, | ||
65 | margin: [0, 'auto', 40, 'auto'], | ||
66 | |||
67 | '@media(min-width: 800px)': { | ||
68 | marginLeft: 40, | ||
69 | order: 1, | ||
70 | }, | ||
71 | }, | ||
72 | }; | ||
73 | |||
74 | |||
75 | export default @injectSheet(styles) @observer class TeamDashboard extends Component { | ||
76 | static propTypes = { | ||
77 | isLoading: PropTypes.bool.isRequired, | ||
78 | userInfoRequestFailed: PropTypes.bool.isRequired, | ||
79 | retryUserInfoRequest: PropTypes.func.isRequired, | ||
80 | openTeamManagement: PropTypes.func.isRequired, | ||
81 | classes: PropTypes.object.isRequired, | ||
82 | }; | ||
83 | |||
84 | static contextTypes = { | ||
85 | intl: intlShape, | ||
86 | }; | ||
87 | |||
88 | render() { | ||
89 | const { | ||
90 | isLoading, | ||
91 | userInfoRequestFailed, | ||
92 | retryUserInfoRequest, | ||
93 | openTeamManagement, | ||
94 | classes, | ||
95 | } = this.props; | ||
96 | const { intl } = this.context; | ||
97 | |||
98 | return ( | ||
99 | <div className="settings__main"> | ||
100 | <div className="settings__header"> | ||
101 | <span className="settings__header-item"> | ||
102 | {intl.formatMessage(messages.headline)} | ||
103 | </span> | ||
104 | </div> | ||
105 | <div className="settings__body"> | ||
106 | {isLoading && ( | ||
107 | <Loader /> | ||
108 | )} | ||
109 | |||
110 | {!isLoading && userInfoRequestFailed && ( | ||
111 | <Infobox | ||
112 | icon="alert" | ||
113 | type="danger" | ||
114 | ctaLabel={intl.formatMessage(messages.tryReloadUserInfoRequest)} | ||
115 | ctaLoading={isLoading} | ||
116 | ctaOnClick={retryUserInfoRequest} | ||
117 | > | ||
118 | {intl.formatMessage(messages.userInfoRequestFailed)} | ||
119 | </Infobox> | ||
120 | )} | ||
121 | |||
122 | {!userInfoRequestFailed && ( | ||
123 | <> | ||
124 | {!isLoading && ( | ||
125 | <> | ||
126 | <PremiumFeatureContainer> | ||
127 | <> | ||
128 | <h1>{intl.formatMessage(messages.contentHeadline)}</h1> | ||
129 | <div className={classes.container}> | ||
130 | <div className={classes.content}> | ||
131 | <p>{intl.formatMessage(messages.intro)}</p> | ||
132 | <p>{intl.formatMessage(messages.copy)}</p> | ||
133 | </div> | ||
134 | <img className={classes.image} src="https://cdn.franzinfra.com/announcements/assets/teams.png" alt="Franz for Teams" /> | ||
135 | </div> | ||
136 | <Button | ||
137 | label={intl.formatMessage(messages.manageButton)} | ||
138 | onClick={openTeamManagement} | ||
139 | className={classes.cta} | ||
140 | /> | ||
141 | </> | ||
142 | </PremiumFeatureContainer> | ||
143 | </> | ||
144 | )} | ||
145 | </> | ||
146 | )} | ||
147 | </div> | ||
148 | <ReactTooltip place="right" type="dark" effect="solid" /> | ||
149 | </div> | ||
150 | ); | ||
151 | } | ||
152 | } | ||
diff --git a/src/components/settings/user/EditUserForm.js b/src/components/settings/user/EditUserForm.js index 0e3ac6b10..a1a353e57 100644 --- a/src/components/settings/user/EditUserForm.js +++ b/src/components/settings/user/EditUserForm.js | |||
@@ -3,11 +3,10 @@ import PropTypes from 'prop-types'; | |||
3 | import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; | 3 | import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; |
4 | import { defineMessages, intlShape } from 'react-intl'; | 4 | import { defineMessages, intlShape } from 'react-intl'; |
5 | import { Link } from 'react-router'; | 5 | import { Link } from 'react-router'; |
6 | 6 | import { Input } from '@meetfranz/forms'; | |
7 | // import { Link } from 'react-router'; | ||
8 | 7 | ||
9 | import Form from '../../../lib/Form'; | 8 | import Form from '../../../lib/Form'; |
10 | import Input from '../../ui/Input'; | 9 | // import Input from '../../ui/Input'; |
11 | import Button from '../../ui/Button'; | 10 | import Button from '../../ui/Button'; |
12 | import Radio from '../../ui/Radio'; | 11 | import Radio from '../../ui/Radio'; |
13 | import Infobox from '../../ui/Infobox'; | 12 | import Infobox from '../../ui/Infobox'; |
@@ -39,13 +38,12 @@ const messages = defineMessages({ | |||
39 | }, | 38 | }, |
40 | }); | 39 | }); |
41 | 40 | ||
42 | export default @observer class EditServiceForm extends Component { | 41 | export default @observer class EditUserForm extends Component { |
43 | static propTypes = { | 42 | static propTypes = { |
44 | status: MobxPropTypes.observableArray.isRequired, | 43 | status: MobxPropTypes.observableArray.isRequired, |
45 | form: PropTypes.instanceOf(Form).isRequired, | 44 | form: PropTypes.instanceOf(Form).isRequired, |
46 | onSubmit: PropTypes.func.isRequired, | 45 | onSubmit: PropTypes.func.isRequired, |
47 | isSaving: PropTypes.bool.isRequired, | 46 | isSaving: PropTypes.bool.isRequired, |
48 | isEnterprise: PropTypes.bool.isRequired, | ||
49 | }; | 47 | }; |
50 | 48 | ||
51 | static contextTypes = { | 49 | static contextTypes = { |
@@ -68,7 +66,6 @@ export default @observer class EditServiceForm extends Component { | |||
68 | // user, | 66 | // user, |
69 | status, | 67 | status, |
70 | form, | 68 | form, |
71 | isEnterprise, | ||
72 | isSaving, | 69 | isSaving, |
73 | } = this.props; | 70 | } = this.props; |
74 | const { intl } = this.context; | 71 | const { intl } = this.context; |
@@ -98,23 +95,21 @@ export default @observer class EditServiceForm extends Component { | |||
98 | )} | 95 | )} |
99 | <h2>{intl.formatMessage(messages.headlineAccount)}</h2> | 96 | <h2>{intl.formatMessage(messages.headlineAccount)}</h2> |
100 | <div className="grid__row"> | 97 | <div className="grid__row"> |
101 | <Input field={form.$('firstname')} focus /> | 98 | <Input {...form.$('firstname').bind()} focus /> |
102 | <Input field={form.$('lastname')} /> | 99 | <Input {...form.$('lastname').bind()} /> |
103 | </div> | 100 | </div> |
104 | <Input field={form.$('email')} /> | 101 | <Input {...form.$('email').bind()} /> |
105 | {!isEnterprise && ( | 102 | <Radio field={form.$('accountType')} /> |
106 | <Radio field={form.$('accountType')} /> | 103 | {form.$('accountType').value === 'company' && ( |
107 | )} | ||
108 | {!isEnterprise && form.$('accountType').value === 'company' && ( | ||
109 | <Input field={form.$('organization')} /> | 104 | <Input field={form.$('organization')} /> |
110 | )} | 105 | )} |
111 | <h2>{intl.formatMessage(messages.headlinePassword)}</h2> | 106 | <h2>{intl.formatMessage(messages.headlinePassword)}</h2> |
112 | <Input | 107 | <Input |
113 | field={form.$('oldPassword')} | 108 | {...form.$('oldPassword').bind()} |
114 | showPasswordToggle | 109 | showPasswordToggle |
115 | /> | 110 | /> |
116 | <Input | 111 | <Input |
117 | field={form.$('newPassword')} | 112 | {...form.$('newPassword').bind()} |
118 | showPasswordToggle | 113 | showPasswordToggle |
119 | scorePassword | 114 | scorePassword |
120 | /> | 115 | /> |
diff --git a/src/components/subscription/SubscriptionForm.js b/src/components/subscription/SubscriptionForm.js index 90da8ddc3..50f1e0522 100644 --- a/src/components/subscription/SubscriptionForm.js +++ b/src/components/subscription/SubscriptionForm.js | |||
@@ -35,31 +35,33 @@ const messages = defineMessages({ | |||
35 | id: 'subscription.includedFeatures', | 35 | id: 'subscription.includedFeatures', |
36 | defaultMessage: '!!!The Franz Premium Supporter Account includes', | 36 | defaultMessage: '!!!The Franz Premium Supporter Account includes', |
37 | }, | 37 | }, |
38 | features: { | 38 | onpremise: { |
39 | onpremise: { | 39 | id: 'subscription.features.onpremise.mattermost', |
40 | id: 'subscription.features.onpremise.mattermost', | 40 | defaultMessage: '!!!Add on-premise/hosted services like Mattermost', |
41 | defaultMessage: '!!!Add on-premise/hosted services like Mattermost', | 41 | }, |
42 | }, | 42 | noInterruptions: { |
43 | noInterruptions: { | 43 | id: 'subscription.features.noInterruptions', |
44 | id: 'subscription.features.noInterruptions', | 44 | defaultMessage: '!!!No app delays & nagging to upgrade license', |
45 | defaultMessage: '!!!No app delays & nagging to upgrade license', | 45 | }, |
46 | }, | 46 | proxy: { |
47 | proxy: { | 47 | id: 'subscription.features.proxy', |
48 | id: 'subscription.features.proxy', | 48 | defaultMessage: '!!!Proxy support for services', |
49 | defaultMessage: '!!!Proxy support for services', | 49 | }, |
50 | }, | 50 | spellchecker: { |
51 | spellchecker: { | 51 | id: 'subscription.features.spellchecker', |
52 | id: 'subscription.features.spellchecker', | 52 | defaultMessage: '!!!Support for Spellchecker', |
53 | defaultMessage: '!!!Support for Spellchecker', | 53 | }, |
54 | }, | 54 | workspaces: { |
55 | ads: { | 55 | id: 'subscription.features.workspaces', |
56 | id: 'subscription.features.ads', | 56 | defaultMessage: '!!!Organize your services in workspaces', |
57 | defaultMessage: '!!!No ads, ever!', | 57 | }, |
58 | }, | 58 | ads: { |
59 | comingSoon: { | 59 | id: 'subscription.features.ads', |
60 | id: 'subscription.features.comingSoon', | 60 | defaultMessage: '!!!No ads, ever!', |
61 | defaultMessage: '!!!coming soon', | 61 | }, |
62 | }, | 62 | comingSoon: { |
63 | id: 'subscription.features.comingSoon', | ||
64 | defaultMessage: '!!!coming soon', | ||
63 | }, | 65 | }, |
64 | euTaxInfo: { | 66 | euTaxInfo: { |
65 | id: 'subscription.euTaxInfo', | 67 | id: 'subscription.euTaxInfo', |
@@ -85,7 +87,7 @@ export default @observer class SubscriptionForm extends Component { | |||
85 | showSkipOption: false, | 87 | showSkipOption: false, |
86 | skipAction: () => null, | 88 | skipAction: () => null, |
87 | skipButtonLabel: '', | 89 | skipButtonLabel: '', |
88 | } | 90 | }; |
89 | 91 | ||
90 | static contextTypes = { | 92 | static contextTypes = { |
91 | intl: intlShape, | 93 | intl: intlShape, |
@@ -162,18 +164,21 @@ export default @observer class SubscriptionForm extends Component { | |||
162 | </p> | 164 | </p> |
163 | <div className="subscription"> | 165 | <div className="subscription"> |
164 | <ul className="subscription__premium-features"> | 166 | <ul className="subscription__premium-features"> |
165 | <li>{intl.formatMessage(messages.features.onpremise)}</li> | 167 | <li>{intl.formatMessage(messages.onpremise)}</li> |
168 | <li> | ||
169 | {intl.formatMessage(messages.noInterruptions)} | ||
170 | </li> | ||
166 | <li> | 171 | <li> |
167 | {intl.formatMessage(messages.features.noInterruptions)} | 172 | {intl.formatMessage(messages.spellchecker)} |
168 | </li> | 173 | </li> |
169 | <li> | 174 | <li> |
170 | {intl.formatMessage(messages.features.spellchecker)} | 175 | {intl.formatMessage(messages.proxy)} |
171 | </li> | 176 | </li> |
172 | <li> | 177 | <li> |
173 | {intl.formatMessage(messages.features.proxy)} | 178 | {intl.formatMessage(messages.workspaces)} |
174 | </li> | 179 | </li> |
175 | <li> | 180 | <li> |
176 | {intl.formatMessage(messages.features.ads)} | 181 | {intl.formatMessage(messages.ads)} |
177 | </li> | 182 | </li> |
178 | </ul> | 183 | </ul> |
179 | </div> | 184 | </div> |
diff --git a/src/components/subscription/SubscriptionPopup.js b/src/components/subscription/SubscriptionPopup.js index b5d7c4b2d..0f6f0260f 100644 --- a/src/components/subscription/SubscriptionPopup.js +++ b/src/components/subscription/SubscriptionPopup.js | |||
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; | |||
3 | import { observer } from 'mobx-react'; | 3 | import { observer } from 'mobx-react'; |
4 | import { defineMessages, intlShape } from 'react-intl'; | 4 | import { defineMessages, intlShape } from 'react-intl'; |
5 | import Webview from 'react-electron-web-view'; | 5 | import Webview from 'react-electron-web-view'; |
6 | import ms from 'ms'; | ||
6 | 7 | ||
7 | import Button from '../ui/Button'; | 8 | import Button from '../ui/Button'; |
8 | 9 | ||
@@ -42,7 +43,7 @@ export default @observer class SubscriptionPopup extends Component { | |||
42 | 43 | ||
43 | setTimeout(() => { | 44 | setTimeout(() => { |
44 | this.props.closeWindow(); | 45 | this.props.closeWindow(); |
45 | }, 4000); | 46 | }, ms('4s')); |
46 | } | 47 | } |
47 | 48 | ||
48 | render() { | 49 | render() { |
diff --git a/src/components/ui/AppLoader/index.js b/src/components/ui/AppLoader/index.js index 61053f6d1..b0c7fed7b 100644 --- a/src/components/ui/AppLoader/index.js +++ b/src/components/ui/AppLoader/index.js | |||
@@ -23,11 +23,11 @@ export default @injectSheet(styles) @withTheme class AppLoader extends Component | |||
23 | static propTypes = { | 23 | static propTypes = { |
24 | classes: PropTypes.object.isRequired, | 24 | classes: PropTypes.object.isRequired, |
25 | theme: PropTypes.object.isRequired, | 25 | theme: PropTypes.object.isRequired, |
26 | } | 26 | }; |
27 | 27 | ||
28 | state = { | 28 | state = { |
29 | step: 0, | 29 | step: 0, |
30 | } | 30 | }; |
31 | 31 | ||
32 | interval = null; | 32 | interval = null; |
33 | 33 | ||
diff --git a/src/components/ui/FullscreenLoader/index.js b/src/components/ui/FullscreenLoader/index.js index 6ecf4d395..06dab1eb6 100644 --- a/src/components/ui/FullscreenLoader/index.js +++ b/src/components/ui/FullscreenLoader/index.js | |||
@@ -16,13 +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 | } | 19 | }; |
20 | 20 | ||
21 | static defaultProps = { | 21 | static defaultProps = { |
22 | className: null, | 22 | className: null, |
23 | spinnerColor: null, | 23 | spinnerColor: null, |
24 | children: null, | 24 | children: null, |
25 | } | 25 | }; |
26 | 26 | ||
27 | render() { | 27 | render() { |
28 | const { | 28 | const { |
diff --git a/src/components/ui/Infobox.js b/src/components/ui/Infobox.js index a33c6474a..0917ee9f0 100644 --- a/src/components/ui/Infobox.js +++ b/src/components/ui/Infobox.js | |||
@@ -13,6 +13,8 @@ export default @observer class Infobox extends Component { | |||
13 | ctaLabel: PropTypes.string, | 13 | ctaLabel: PropTypes.string, |
14 | ctaLoading: PropTypes.bool, | 14 | ctaLoading: PropTypes.bool, |
15 | dismissable: PropTypes.bool, | 15 | dismissable: PropTypes.bool, |
16 | onDismiss: PropTypes.func, | ||
17 | onSeen: PropTypes.func, | ||
16 | }; | 18 | }; |
17 | 19 | ||
18 | static defaultProps = { | 20 | static defaultProps = { |
@@ -22,12 +24,19 @@ export default @observer class Infobox extends Component { | |||
22 | ctaOnClick: () => null, | 24 | ctaOnClick: () => null, |
23 | ctaLabel: '', | 25 | ctaLabel: '', |
24 | ctaLoading: false, | 26 | ctaLoading: false, |
27 | onDismiss: () => null, | ||
28 | onSeen: () => null, | ||
25 | }; | 29 | }; |
26 | 30 | ||
27 | state = { | 31 | state = { |
28 | dismissed: false, | 32 | dismissed: false, |
29 | }; | 33 | }; |
30 | 34 | ||
35 | componentDidMount() { | ||
36 | const { onSeen } = this.props; | ||
37 | if (onSeen) onSeen(); | ||
38 | } | ||
39 | |||
31 | render() { | 40 | render() { |
32 | const { | 41 | const { |
33 | children, | 42 | children, |
@@ -37,6 +46,7 @@ export default @observer class Infobox extends Component { | |||
37 | ctaLoading, | 46 | ctaLoading, |
38 | ctaOnClick, | 47 | ctaOnClick, |
39 | dismissable, | 48 | dismissable, |
49 | onDismiss, | ||
40 | } = this.props; | 50 | } = this.props; |
41 | 51 | ||
42 | if (this.state.dismissed) { | 52 | if (this.state.dismissed) { |
@@ -76,9 +86,10 @@ export default @observer class Infobox extends Component { | |||
76 | {dismissable && ( | 86 | {dismissable && ( |
77 | <button | 87 | <button |
78 | type="button" | 88 | type="button" |
79 | onClick={() => this.setState({ | 89 | onClick={() => { |
80 | dismissed: true, | 90 | this.setState({ dismissed: true }); |
81 | })} | 91 | if (onDismiss) onDismiss(); |
92 | }} | ||
82 | className="infobox__delete mdi mdi-close" | 93 | className="infobox__delete mdi mdi-close" |
83 | /> | 94 | /> |
84 | )} | 95 | )} |
diff --git a/src/components/ui/Modal/index.js b/src/components/ui/Modal/index.js index d84e4c713..0b7154760 100644 --- a/src/components/ui/Modal/index.js +++ b/src/components/ui/Modal/index.js | |||
@@ -3,9 +3,12 @@ import ReactModal from 'react-modal'; | |||
3 | import PropTypes from 'prop-types'; | 3 | import PropTypes from 'prop-types'; |
4 | import classnames from 'classnames'; | 4 | import classnames from 'classnames'; |
5 | import injectCSS from 'react-jss'; | 5 | import injectCSS from 'react-jss'; |
6 | import { Icon } from '@meetfranz/ui'; | ||
6 | 7 | ||
7 | import styles from './styles'; | 8 | import styles from './styles'; |
8 | 9 | ||
10 | // ReactModal.setAppElement('#root'); | ||
11 | |||
9 | export default @injectCSS(styles) class Modal extends Component { | 12 | export default @injectCSS(styles) class Modal extends Component { |
10 | static propTypes = { | 13 | static propTypes = { |
11 | children: PropTypes.node.isRequired, | 14 | children: PropTypes.node.isRequired, |
@@ -14,11 +17,15 @@ export default @injectCSS(styles) class Modal extends Component { | |||
14 | isOpen: PropTypes.bool.isRequired, | 17 | isOpen: PropTypes.bool.isRequired, |
15 | portal: PropTypes.string, | 18 | portal: PropTypes.string, |
16 | close: PropTypes.func.isRequired, | 19 | close: PropTypes.func.isRequired, |
20 | shouldCloseOnOverlayClick: PropTypes.bool, | ||
21 | showClose: PropTypes.bool, | ||
17 | } | 22 | } |
18 | 23 | ||
19 | static defaultProps = { | 24 | static defaultProps = { |
20 | className: null, | 25 | className: null, |
21 | portal: 'modal-portal', | 26 | portal: 'modal-portal', |
27 | shouldCloseOnOverlayClick: false, | ||
28 | showClose: true, | ||
22 | } | 29 | } |
23 | 30 | ||
24 | render() { | 31 | render() { |
@@ -29,6 +36,8 @@ export default @injectCSS(styles) class Modal extends Component { | |||
29 | isOpen, | 36 | isOpen, |
30 | portal, | 37 | portal, |
31 | close, | 38 | close, |
39 | shouldCloseOnOverlayClick, | ||
40 | showClose, | ||
32 | } = this.props; | 41 | } = this.props; |
33 | 42 | ||
34 | return ( | 43 | return ( |
@@ -42,14 +51,17 @@ export default @injectCSS(styles) class Modal extends Component { | |||
42 | overlayClassName={classes.overlay} | 51 | overlayClassName={classes.overlay} |
43 | portal={portal} | 52 | portal={portal} |
44 | onRequestClose={close} | 53 | onRequestClose={close} |
54 | shouldCloseOnOverlayClick={shouldCloseOnOverlayClick} | ||
45 | > | 55 | > |
46 | {/* <button | 56 | {showClose && close && ( |
47 | type="button" | 57 | <button |
48 | className={classnames({ | 58 | type="button" |
49 | [`${classes.close}`]: true, | 59 | className={classes.close} |
50 | 'mdi mdi-close': true, | 60 | onClick={close} |
51 | })} | 61 | > |
52 | /> */} | 62 | <Icon icon="mdiClose" size={1.5} /> |
63 | </button> | ||
64 | )} | ||
53 | <div className={classes.content}> | 65 | <div className={classes.content}> |
54 | {children} | 66 | {children} |
55 | </div> | 67 | </div> |
diff --git a/src/components/ui/Modal/styles.js b/src/components/ui/Modal/styles.js index 56fecbf55..49b970c97 100644 --- a/src/components/ui/Modal/styles.js +++ b/src/components/ui/Modal/styles.js | |||
@@ -28,5 +28,6 @@ export default theme => ({ | |||
28 | position: 'absolute', | 28 | position: 'absolute', |
29 | top: 0, | 29 | top: 0, |
30 | right: 0, | 30 | right: 0, |
31 | padding: 20, | ||
31 | }, | 32 | }, |
32 | }); | 33 | }); |
diff --git a/src/components/ui/PremiumFeatureContainer/index.js b/src/components/ui/PremiumFeatureContainer/index.js index 67cd6af0b..3c1e0fac3 100644 --- a/src/components/ui/PremiumFeatureContainer/index.js +++ b/src/components/ui/PremiumFeatureContainer/index.js | |||
@@ -9,6 +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 | 13 | ||
13 | const messages = defineMessages({ | 14 | const messages = defineMessages({ |
14 | action: { | 15 | action: { |
@@ -17,14 +18,21 @@ const messages = defineMessages({ | |||
17 | }, | 18 | }, |
18 | }); | 19 | }); |
19 | 20 | ||
20 | export default @inject('stores', 'actions') @injectSheet(styles) @observer class PremiumFeatureContainer extends Component { | 21 | @inject('stores', 'actions') @injectSheet(styles) @observer |
22 | class PremiumFeatureContainer extends Component { | ||
21 | static propTypes = { | 23 | static propTypes = { |
22 | classes: PropTypes.object.isRequired, | 24 | classes: PropTypes.object.isRequired, |
23 | condition: PropTypes.bool, | 25 | condition: PropTypes.bool, |
26 | gaEventInfo: PropTypes.shape({ | ||
27 | category: PropTypes.string.isRequired, | ||
28 | event: PropTypes.string.isRequired, | ||
29 | label: PropTypes.string, | ||
30 | }), | ||
24 | }; | 31 | }; |
25 | 32 | ||
26 | static defaultProps = { | 33 | static defaultProps = { |
27 | condition: true, | 34 | condition: true, |
35 | gaEventInfo: null, | ||
28 | }; | 36 | }; |
29 | 37 | ||
30 | static contextTypes = { | 38 | static contextTypes = { |
@@ -38,6 +46,7 @@ export default @inject('stores', 'actions') @injectSheet(styles) @observer class | |||
38 | actions, | 46 | actions, |
39 | condition, | 47 | condition, |
40 | stores, | 48 | stores, |
49 | gaEventInfo, | ||
41 | } = this.props; | 50 | } = this.props; |
42 | 51 | ||
43 | const { intl } = this.context; | 52 | const { intl } = this.context; |
@@ -49,7 +58,13 @@ export default @inject('stores', 'actions') @injectSheet(styles) @observer class | |||
49 | <button | 58 | <button |
50 | className={classes.actionButton} | 59 | className={classes.actionButton} |
51 | type="button" | 60 | type="button" |
52 | onClick={() => actions.ui.openSettings({ path: 'user' })} | 61 | onClick={() => { |
62 | actions.ui.openSettings({ path: 'user' }); | ||
63 | if (gaEventInfo) { | ||
64 | const { category, event, label } = gaEventInfo; | ||
65 | gaEvent(category, event, label); | ||
66 | } | ||
67 | }} | ||
53 | > | 68 | > |
54 | {intl.formatMessage(messages.action)} | 69 | {intl.formatMessage(messages.action)} |
55 | </button> | 70 | </button> |
@@ -73,3 +88,5 @@ PremiumFeatureContainer.wrappedComponent.propTypes = { | |||
73 | }).isRequired, | 88 | }).isRequired, |
74 | }).isRequired, | 89 | }).isRequired, |
75 | }; | 90 | }; |
91 | |||
92 | export default PremiumFeatureContainer; | ||
diff --git a/src/components/ui/PremiumFeatureContainer/styles.js b/src/components/ui/PremiumFeatureContainer/styles.js index 81d6666c6..41881e044 100644 --- a/src/components/ui/PremiumFeatureContainer/styles.js +++ b/src/components/ui/PremiumFeatureContainer/styles.js | |||
@@ -6,6 +6,7 @@ export default theme => ({ | |||
6 | padding: 20, | 6 | padding: 20, |
7 | 'border-radius': theme.borderRadius, | 7 | 'border-radius': theme.borderRadius, |
8 | pointerEvents: 'none', | 8 | pointerEvents: 'none', |
9 | height: 'auto', | ||
9 | }, | 10 | }, |
10 | titleContainer: { | 11 | titleContainer: { |
11 | display: 'flex', | 12 | display: 'flex', |
@@ -19,14 +20,14 @@ export default theme => ({ | |||
19 | color: theme.colorSubscriptionContainerActionButtonColor, | 20 | color: theme.colorSubscriptionContainerActionButtonColor, |
20 | 'margin-left': 'auto', | 21 | 'margin-left': 'auto', |
21 | 'border-radius': theme.borderRadiusSmall, | 22 | 'border-radius': theme.borderRadiusSmall, |
22 | padding: [2, 4], | 23 | padding: [4, 8], |
23 | 'font-size': 12, | 24 | 'font-size': 12, |
24 | pointerEvents: 'initial', | 25 | pointerEvents: 'initial', |
25 | }, | 26 | }, |
26 | content: { | 27 | content: { |
27 | opacity: 0.5, | 28 | opacity: 0.5, |
28 | 'margin-top': 20, | 29 | 'margin-top': 20, |
29 | '& :last-child': { | 30 | '& > :last-child': { |
30 | 'margin-bottom': 0, | 31 | 'margin-bottom': 0, |
31 | }, | 32 | }, |
32 | }, | 33 | }, |
diff --git a/src/components/ui/ServiceIcon.js b/src/components/ui/ServiceIcon.js new file mode 100644 index 000000000..0b9155a4e --- /dev/null +++ b/src/components/ui/ServiceIcon.js | |||
@@ -0,0 +1,67 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { observer } from 'mobx-react'; | ||
4 | import injectSheet from 'react-jss'; | ||
5 | import classnames from 'classnames'; | ||
6 | |||
7 | import ServiceModel from '../../models/Service'; | ||
8 | |||
9 | const styles = theme => ({ | ||
10 | root: { | ||
11 | height: 'auto', | ||
12 | }, | ||
13 | icon: { | ||
14 | width: theme.serviceIcon.width, | ||
15 | }, | ||
16 | isCustomIcon: { | ||
17 | width: theme.serviceIcon.isCustom.width, | ||
18 | border: theme.serviceIcon.isCustom.border, | ||
19 | borderRadius: theme.serviceIcon.isCustom.borderRadius, | ||
20 | }, | ||
21 | isDisabled: { | ||
22 | filter: 'grayscale(100%)', | ||
23 | opacity: '.5', | ||
24 | }, | ||
25 | }); | ||
26 | |||
27 | @injectSheet(styles) @observer | ||
28 | class ServiceIcon extends Component { | ||
29 | static propTypes = { | ||
30 | classes: PropTypes.object.isRequired, | ||
31 | service: PropTypes.instanceOf(ServiceModel).isRequired, | ||
32 | className: PropTypes.string, | ||
33 | }; | ||
34 | |||
35 | static defaultProps = { | ||
36 | className: '', | ||
37 | }; | ||
38 | |||
39 | render() { | ||
40 | const { | ||
41 | classes, | ||
42 | className, | ||
43 | service, | ||
44 | } = this.props; | ||
45 | |||
46 | return ( | ||
47 | <div | ||
48 | className={classnames([ | ||
49 | classes.root, | ||
50 | className, | ||
51 | ])} | ||
52 | > | ||
53 | <img | ||
54 | src={service.icon} | ||
55 | className={classnames([ | ||
56 | classes.icon, | ||
57 | service.isEnabled ? null : classes.isDisabled, | ||
58 | service.hasCustomIcon ? classes.isCustomIcon : null, | ||
59 | ])} | ||
60 | alt="" | ||
61 | /> | ||
62 | </div> | ||
63 | ); | ||
64 | } | ||
65 | } | ||
66 | |||
67 | export default ServiceIcon; | ||
diff --git a/src/components/ui/WebviewLoader/index.js b/src/components/ui/WebviewLoader/index.js index 3a3dbbe49..58b6b6f1b 100644 --- a/src/components/ui/WebviewLoader/index.js +++ b/src/components/ui/WebviewLoader/index.js | |||
@@ -2,23 +2,35 @@ 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 injectSheet from 'react-jss'; | 4 | import injectSheet from 'react-jss'; |
5 | import { defineMessages, intlShape } from 'react-intl'; | ||
5 | 6 | ||
6 | import FullscreenLoader from '../FullscreenLoader'; | 7 | import FullscreenLoader from '../FullscreenLoader'; |
7 | |||
8 | import styles from './styles'; | 8 | import styles from './styles'; |
9 | 9 | ||
10 | const messages = defineMessages({ | ||
11 | loading: { | ||
12 | id: 'service.webviewLoader.loading', | ||
13 | defaultMessage: '!!!Loading', | ||
14 | }, | ||
15 | }); | ||
16 | |||
10 | export default @observer @injectSheet(styles) class WebviewLoader extends Component { | 17 | export default @observer @injectSheet(styles) class WebviewLoader extends Component { |
11 | static propTypes = { | 18 | static propTypes = { |
12 | name: PropTypes.string.isRequired, | 19 | name: PropTypes.string.isRequired, |
13 | classes: PropTypes.object.isRequired, | 20 | classes: PropTypes.object.isRequired, |
14 | } | 21 | }; |
22 | |||
23 | static contextTypes = { | ||
24 | intl: intlShape, | ||
25 | }; | ||
15 | 26 | ||
16 | render() { | 27 | render() { |
17 | const { classes, name } = this.props; | 28 | const { classes, name } = this.props; |
29 | const { intl } = this.context; | ||
18 | return ( | 30 | return ( |
19 | <FullscreenLoader | 31 | <FullscreenLoader |
20 | className={classes.component} | 32 | className={classes.component} |
21 | title={`Loading ${name}`} | 33 | title={`${intl.formatMessage(messages.loading)} ${name}`} |
22 | /> | 34 | /> |
23 | ); | 35 | ); |
24 | } | 36 | } |