diff options
author | Dominik Guzei <dominik.guzei@gmail.com> | 2019-03-12 21:36:10 +0100 |
---|---|---|
committer | Dominik Guzei <dominik.guzei@gmail.com> | 2019-03-12 21:37:33 +0100 |
commit | 6fb07bcb716af76ec2e96345f37624d12d0d1af0 (patch) | |
tree | 276191a782dc1d44f78331e548e43ff71758baca /src/features | |
parent | refactor server api even more (diff) | |
download | ferdium-app-6fb07bcb716af76ec2e96345f37624d12d0d1af0.tar.gz ferdium-app-6fb07bcb716af76ec2e96345f37624d12d0d1af0.tar.zst ferdium-app-6fb07bcb716af76ec2e96345f37624d12d0d1af0.zip |
implements basic release announcement feature
Diffstat (limited to 'src/features')
-rw-r--r-- | src/features/announcements/Component.js | 77 | ||||
-rw-r--r-- | src/features/announcements/actions.js | 8 | ||||
-rw-r--r-- | src/features/announcements/api.js | 19 | ||||
-rw-r--r-- | src/features/announcements/index.js | 37 | ||||
-rw-r--r-- | src/features/announcements/state.js | 17 | ||||
-rw-r--r-- | src/features/announcements/store.js | 95 |
6 files changed, 253 insertions, 0 deletions
diff --git a/src/features/announcements/Component.js b/src/features/announcements/Component.js new file mode 100644 index 000000000..5d95f5d84 --- /dev/null +++ b/src/features/announcements/Component.js | |||
@@ -0,0 +1,77 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import marked from 'marked'; | ||
3 | import PropTypes from 'prop-types'; | ||
4 | import { inject, observer } from 'mobx-react'; | ||
5 | import { defineMessages, intlShape } from 'react-intl'; | ||
6 | import injectSheet from 'react-jss'; | ||
7 | import { themeSidebarWidth } from '@meetfranz/theme/lib/themes/legacy'; | ||
8 | import state from './state'; | ||
9 | |||
10 | const messages = defineMessages({ | ||
11 | headline: { | ||
12 | id: 'feature.announcements.headline', | ||
13 | defaultMessage: '!!!What\'s new in Franz {version}?', | ||
14 | }, | ||
15 | }); | ||
16 | |||
17 | const styles = theme => ({ | ||
18 | container: { | ||
19 | background: theme.colorBackground, | ||
20 | position: 'absolute', | ||
21 | top: 0, | ||
22 | zIndex: 140, | ||
23 | width: `calc(100% - ${themeSidebarWidth})`, | ||
24 | display: 'flex', | ||
25 | 'flex-direction': 'column', | ||
26 | 'align-items': 'center', | ||
27 | 'justify-content': 'center', | ||
28 | }, | ||
29 | headline: { | ||
30 | color: theme.colorHeadline, | ||
31 | margin: [25, 0, 40], | ||
32 | 'max-width': 500, | ||
33 | 'text-align': 'center', | ||
34 | 'line-height': '1.3em', | ||
35 | }, | ||
36 | body: { | ||
37 | '& h3': { | ||
38 | fontSize: '24px', | ||
39 | margin: '1.5em 0 1em 0', | ||
40 | }, | ||
41 | '& li': { | ||
42 | marginBottom: '1em', | ||
43 | }, | ||
44 | }, | ||
45 | }); | ||
46 | |||
47 | |||
48 | @inject('actions') @injectSheet(styles) @observer | ||
49 | class AnnouncementScreen extends Component { | ||
50 | static propTypes = { | ||
51 | classes: PropTypes.object.isRequired, | ||
52 | }; | ||
53 | |||
54 | static contextTypes = { | ||
55 | intl: intlShape, | ||
56 | }; | ||
57 | |||
58 | render() { | ||
59 | const { classes } = this.props; | ||
60 | const { intl } = this.context; | ||
61 | return ( | ||
62 | <div className={`${classes.container}`}> | ||
63 | <h1 className={classes.headline}> | ||
64 | {intl.formatMessage(messages.headline, { version: state.currentVersion })} | ||
65 | </h1> | ||
66 | <div | ||
67 | className={classes.body} | ||
68 | dangerouslySetInnerHTML={{ | ||
69 | __html: marked(state.announcement, { sanitize: true }), | ||
70 | }} | ||
71 | /> | ||
72 | </div> | ||
73 | ); | ||
74 | } | ||
75 | } | ||
76 | |||
77 | export default AnnouncementScreen; | ||
diff --git a/src/features/announcements/actions.js b/src/features/announcements/actions.js new file mode 100644 index 000000000..68b262ded --- /dev/null +++ b/src/features/announcements/actions.js | |||
@@ -0,0 +1,8 @@ | |||
1 | import PropTypes from 'prop-types'; | ||
2 | import { createActionsFromDefinitions } from '../../actions/lib/actions'; | ||
3 | |||
4 | export const announcementActions = createActionsFromDefinitions({ | ||
5 | show: {}, | ||
6 | }, PropTypes.checkPropTypes); | ||
7 | |||
8 | export default announcementActions; | ||
diff --git a/src/features/announcements/api.js b/src/features/announcements/api.js new file mode 100644 index 000000000..ec16066a6 --- /dev/null +++ b/src/features/announcements/api.js | |||
@@ -0,0 +1,19 @@ | |||
1 | import { remote } from 'electron'; | ||
2 | |||
3 | const debug = require('debug')('Franz:feature:announcements:api'); | ||
4 | |||
5 | export default { | ||
6 | async getCurrentVersion() { | ||
7 | debug('getting current version of electron app'); | ||
8 | return Promise.resolve(remote.app.getVersion()); | ||
9 | }, | ||
10 | |||
11 | async getAnnouncementForVersion(version) { | ||
12 | debug('fetching release announcement from Github'); | ||
13 | const url = `https://api.github.com/repos/meetfranz/franz/releases/tags/v${version}`; | ||
14 | const request = await window.fetch(url, { method: 'GET' }); | ||
15 | if (!request.ok) throw request; | ||
16 | const data = await request.json(); | ||
17 | return data.body; | ||
18 | }, | ||
19 | }; | ||
diff --git a/src/features/announcements/index.js b/src/features/announcements/index.js new file mode 100644 index 000000000..5ea74e0af --- /dev/null +++ b/src/features/announcements/index.js | |||
@@ -0,0 +1,37 @@ | |||
1 | import { reaction, runInAction } from 'mobx'; | ||
2 | import { AnnouncementsStore } from './store'; | ||
3 | import api from './api'; | ||
4 | import state, { resetState } from './state'; | ||
5 | |||
6 | const debug = require('debug')('Franz:feature:announcements'); | ||
7 | |||
8 | let store = null; | ||
9 | |||
10 | export default function initAnnouncements(stores, actions) { | ||
11 | // const { features } = stores; | ||
12 | |||
13 | // Toggle workspace feature | ||
14 | reaction( | ||
15 | () => ( | ||
16 | true | ||
17 | // features.features.isAnnouncementsEnabled | ||
18 | ), | ||
19 | (isEnabled) => { | ||
20 | if (isEnabled) { | ||
21 | debug('Initializing `announcements` feature'); | ||
22 | store = new AnnouncementsStore(stores, api, actions, state); | ||
23 | store.initialize(); | ||
24 | runInAction(() => { state.isFeatureActive = true; }); | ||
25 | } else if (store) { | ||
26 | debug('Disabling `announcements` feature'); | ||
27 | runInAction(() => { state.isFeatureActive = false; }); | ||
28 | store.teardown(); | ||
29 | store = null; | ||
30 | resetState(); // Reset state to default | ||
31 | } | ||
32 | }, | ||
33 | { | ||
34 | fireImmediately: true, | ||
35 | }, | ||
36 | ); | ||
37 | } | ||
diff --git a/src/features/announcements/state.js b/src/features/announcements/state.js new file mode 100644 index 000000000..81b632253 --- /dev/null +++ b/src/features/announcements/state.js | |||
@@ -0,0 +1,17 @@ | |||
1 | import { observable } from 'mobx'; | ||
2 | |||
3 | const defaultState = { | ||
4 | announcement: null, | ||
5 | currentVersion: null, | ||
6 | lastUsedVersion: null, | ||
7 | isAnnouncementVisible: false, | ||
8 | isFeatureActive: false, | ||
9 | }; | ||
10 | |||
11 | export const announcementsState = observable(defaultState); | ||
12 | |||
13 | export function resetState() { | ||
14 | Object.assign(announcementsState, defaultState); | ||
15 | } | ||
16 | |||
17 | export default announcementsState; | ||
diff --git a/src/features/announcements/store.js b/src/features/announcements/store.js new file mode 100644 index 000000000..004a44062 --- /dev/null +++ b/src/features/announcements/store.js | |||
@@ -0,0 +1,95 @@ | |||
1 | import { action, observable, reaction } from 'mobx'; | ||
2 | import semver from 'semver'; | ||
3 | |||
4 | import Request from '../../stores/lib/Request'; | ||
5 | import Store from '../../stores/lib/Store'; | ||
6 | |||
7 | const debug = require('debug')('Franz:feature:announcements:store'); | ||
8 | |||
9 | export class AnnouncementsStore extends Store { | ||
10 | @observable getCurrentVersion = new Request(this.api, 'getCurrentVersion'); | ||
11 | |||
12 | @observable getAnnouncement = new Request(this.api, 'getAnnouncementForVersion'); | ||
13 | |||
14 | constructor(stores, api, actions, state) { | ||
15 | super(stores, api, actions); | ||
16 | this.state = state; | ||
17 | } | ||
18 | |||
19 | async setup() { | ||
20 | await this.fetchLastUsedVersion(); | ||
21 | await this.fetchCurrentVersion(); | ||
22 | await this.fetchReleaseAnnouncement(); | ||
23 | this.showAnnouncementIfNotSeenYet(); | ||
24 | |||
25 | this.actions.announcements.show.listen(this._showAnnouncement.bind(this)); | ||
26 | } | ||
27 | |||
28 | // ====== PUBLIC ====== | ||
29 | |||
30 | async fetchLastUsedVersion() { | ||
31 | debug('getting last used version from local storage'); | ||
32 | const lastUsedVersion = window.localStorage.getItem('lastUsedVersion'); | ||
33 | this._setLastUsedVersion(lastUsedVersion == null ? '0.0.0' : lastUsedVersion); | ||
34 | } | ||
35 | |||
36 | async fetchCurrentVersion() { | ||
37 | debug('getting current version from api'); | ||
38 | const version = await this.getCurrentVersion.execute(); | ||
39 | this._setCurrentVersion(version); | ||
40 | } | ||
41 | |||
42 | async fetchReleaseAnnouncement() { | ||
43 | debug('getting release announcement from api'); | ||
44 | try { | ||
45 | const announcement = await this.getAnnouncement.execute(this.state.currentVersion); | ||
46 | this._setAnnouncement(announcement); | ||
47 | } catch (error) { | ||
48 | this._setAnnouncement(null); | ||
49 | } | ||
50 | } | ||
51 | |||
52 | showAnnouncementIfNotSeenYet() { | ||
53 | const { announcement, currentVersion, lastUsedVersion } = this.state; | ||
54 | if (announcement && semver.gt(currentVersion, lastUsedVersion)) { | ||
55 | debug(`${currentVersion} < ${lastUsedVersion}: announcement is shown`); | ||
56 | this._showAnnouncement(); | ||
57 | } else { | ||
58 | debug(`${currentVersion} >= ${lastUsedVersion}: announcement is hidden`); | ||
59 | this._hideAnnouncement(); | ||
60 | } | ||
61 | } | ||
62 | |||
63 | // ====== PRIVATE ====== | ||
64 | |||
65 | @action _setCurrentVersion(version) { | ||
66 | debug(`setting current version to ${version}`); | ||
67 | this.state.currentVersion = version; | ||
68 | } | ||
69 | |||
70 | @action _setLastUsedVersion(version) { | ||
71 | debug(`setting last used version to ${version}`); | ||
72 | this.state.lastUsedVersion = version; | ||
73 | } | ||
74 | |||
75 | @action _setAnnouncement(announcement) { | ||
76 | debug(`setting announcement to ${announcement}`); | ||
77 | this.state.announcement = announcement; | ||
78 | } | ||
79 | |||
80 | @action _showAnnouncement() { | ||
81 | this.state.isAnnouncementVisible = true; | ||
82 | this.actions.service.blurActive(); | ||
83 | const dispose = reaction( | ||
84 | () => this.stores.services.active, | ||
85 | () => { | ||
86 | this._hideAnnouncement(); | ||
87 | dispose(); | ||
88 | }, | ||
89 | ); | ||
90 | } | ||
91 | |||
92 | @action _hideAnnouncement() { | ||
93 | this.state.isAnnouncementVisible = false; | ||
94 | } | ||
95 | } | ||