aboutsummaryrefslogtreecommitdiffstats
path: root/src/features/announcements
diff options
context:
space:
mode:
Diffstat (limited to 'src/features/announcements')
-rw-r--r--src/features/announcements/Component.js77
-rw-r--r--src/features/announcements/actions.js8
-rw-r--r--src/features/announcements/api.js19
-rw-r--r--src/features/announcements/index.js37
-rw-r--r--src/features/announcements/state.js17
-rw-r--r--src/features/announcements/store.js95
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 @@
1import React, { Component } from 'react';
2import marked from 'marked';
3import PropTypes from 'prop-types';
4import { inject, observer } from 'mobx-react';
5import { defineMessages, intlShape } from 'react-intl';
6import injectSheet from 'react-jss';
7import { themeSidebarWidth } from '@meetfranz/theme/lib/themes/legacy';
8import state from './state';
9
10const messages = defineMessages({
11 headline: {
12 id: 'feature.announcements.headline',
13 defaultMessage: '!!!What\'s new in Franz {version}?',
14 },
15});
16
17const 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
49class 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
77export 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 @@
1import PropTypes from 'prop-types';
2import { createActionsFromDefinitions } from '../../actions/lib/actions';
3
4export const announcementActions = createActionsFromDefinitions({
5 show: {},
6}, PropTypes.checkPropTypes);
7
8export 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 @@
1import { remote } from 'electron';
2
3const debug = require('debug')('Franz:feature:announcements:api');
4
5export 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 @@
1import { reaction, runInAction } from 'mobx';
2import { AnnouncementsStore } from './store';
3import api from './api';
4import state, { resetState } from './state';
5
6const debug = require('debug')('Franz:feature:announcements');
7
8let store = null;
9
10export 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 @@
1import { observable } from 'mobx';
2
3const defaultState = {
4 announcement: null,
5 currentVersion: null,
6 lastUsedVersion: null,
7 isAnnouncementVisible: false,
8 isFeatureActive: false,
9};
10
11export const announcementsState = observable(defaultState);
12
13export function resetState() {
14 Object.assign(announcementsState, defaultState);
15}
16
17export 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 @@
1import { action, observable, reaction } from 'mobx';
2import semver from 'semver';
3
4import Request from '../../stores/lib/Request';
5import Store from '../../stores/lib/Store';
6
7const debug = require('debug')('Franz:feature:announcements:store');
8
9export 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}