aboutsummaryrefslogtreecommitdiffstats
path: root/src/features
diff options
context:
space:
mode:
authorLibravatar Dominik Guzei <dominik.guzei@gmail.com>2019-04-11 16:44:16 +0200
committerLibravatar Dominik Guzei <dominik.guzei@gmail.com>2019-04-11 16:44:16 +0200
commiteaf4aff646eed56e65c8dd8e70143ab5634ad4b4 (patch)
treeae400dca67edfd828a30da1e11d7e5e507785860 /src/features
parentrefactor announcements to newest feature pattern (diff)
downloadferdium-app-eaf4aff646eed56e65c8dd8e70143ab5634ad4b4.tar.gz
ferdium-app-eaf4aff646eed56e65c8dd8e70143ab5634ad4b4.tar.zst
ferdium-app-eaf4aff646eed56e65c8dd8e70143ab5634ad4b4.zip
WIP: announcement feature and workspace fixes
Diffstat (limited to 'src/features')
-rw-r--r--src/features/announcements/actions.js4
-rw-r--r--src/features/announcements/api.js18
-rw-r--r--src/features/announcements/components/AnnouncementScreen.js152
-rw-r--r--src/features/announcements/store.js151
-rw-r--r--src/features/utils/FeatureStore.js29
-rw-r--r--src/features/workspaces/store.js18
6 files changed, 276 insertions, 96 deletions
diff --git a/src/features/announcements/actions.js b/src/features/announcements/actions.js
index 68b262ded..bab496314 100644
--- a/src/features/announcements/actions.js
+++ b/src/features/announcements/actions.js
@@ -2,7 +2,9 @@ import PropTypes from 'prop-types';
2import { createActionsFromDefinitions } from '../../actions/lib/actions'; 2import { createActionsFromDefinitions } from '../../actions/lib/actions';
3 3
4export const announcementActions = createActionsFromDefinitions({ 4export const announcementActions = createActionsFromDefinitions({
5 show: {}, 5 show: {
6 targetVersion: PropTypes.string,
7 },
6}, PropTypes.checkPropTypes); 8}, PropTypes.checkPropTypes);
7 9
8export default announcementActions; 10export default announcementActions;
diff --git a/src/features/announcements/api.js b/src/features/announcements/api.js
index 09fcb8235..a581bd8de 100644
--- a/src/features/announcements/api.js
+++ b/src/features/announcements/api.js
@@ -1,5 +1,6 @@
1import { remote } from 'electron'; 1import { remote } from 'electron';
2import Request from '../../stores/lib/Request'; 2import Request from '../../stores/lib/Request';
3import { API, API_VERSION } from '../../environment';
3 4
4const debug = require('debug')('Franz:feature:announcements:api'); 5const debug = require('debug')('Franz:feature:announcements:api');
5 6
@@ -9,15 +10,24 @@ export const announcementsApi = {
9 return Promise.resolve(remote.app.getVersion()); 10 return Promise.resolve(remote.app.getVersion());
10 }, 11 },
11 12
12 async getAnnouncementForVersion(version) { 13 async getChangelog(version) {
13 debug('fetching release announcement from Github'); 14 debug('fetching release changelog from Github');
14 const url = `https://api.github.com/repos/meetfranz/franz/releases/tags/v${version}`; 15 const url = `https://api.github.com/repos/meetfranz/franz/releases/tags/v${version}`;
15 const request = await window.fetch(url, { method: 'GET' }); 16 const request = await window.fetch(url, { method: 'GET' });
16 if (!request.ok) throw request; 17 if (!request.ok) return null;
17 const data = await request.json(); 18 const data = await request.json();
18 return data.body; 19 return data.body;
19 }, 20 },
21
22 async getAnnouncement(version) {
23 debug('fetching release announcement from api');
24 const url = `${API}/${API_VERSION}/announcements/${version}`;
25 const response = await window.fetch(url, { method: 'GET' });
26 if (!response.ok) return null;
27 return response.json();
28 },
20}; 29};
21 30
22export const getCurrentVersionRequest = new Request(announcementsApi, 'getCurrentVersion'); 31export const getCurrentVersionRequest = new Request(announcementsApi, 'getCurrentVersion');
23export const getAnnouncementRequest = new Request(announcementsApi, 'getAnnouncementForVersion'); 32export const getChangelogRequest = new Request(announcementsApi, 'getChangelog');
33export const getAnnouncementRequest = new Request(announcementsApi, 'getAnnouncement');
diff --git a/src/features/announcements/components/AnnouncementScreen.js b/src/features/announcements/components/AnnouncementScreen.js
index 5b3e7aeaa..2d5efc396 100644
--- a/src/features/announcements/components/AnnouncementScreen.js
+++ b/src/features/announcements/components/AnnouncementScreen.js
@@ -4,27 +4,29 @@ import PropTypes from 'prop-types';
4import { inject, observer } from 'mobx-react'; 4import { inject, observer } from 'mobx-react';
5import { defineMessages, intlShape } from 'react-intl'; 5import { defineMessages, intlShape } from 'react-intl';
6import injectSheet from 'react-jss'; 6import injectSheet from 'react-jss';
7import { themeSidebarWidth } from '../../../../packages/theme/lib/themes/legacy'; 7import { Button } from '@meetfranz/forms';
8
8import { announcementsStore } from '../index'; 9import { announcementsStore } from '../index';
10import UIStore from '../../../stores/UIStore';
9 11
10const messages = defineMessages({ 12const messages = defineMessages({
11 headline: { 13 headline: {
12 id: 'feature.announcements.headline', 14 id: 'feature.announcements.changelog.headline',
13 defaultMessage: '!!!What\'s new in Franz {version}?', 15 defaultMessage: '!!!Changes in Franz {version}',
14 }, 16 },
15}); 17});
16 18
19const smallScreen = '1000px';
20
17const styles = theme => ({ 21const styles = theme => ({
18 container: { 22 container: {
19 background: theme.colorBackground, 23 background: theme.colorBackground,
20 position: 'absolute', 24 position: 'absolute',
21 top: 0, 25 top: 0,
22 zIndex: 140, 26 zIndex: 140,
23 width: `calc(100% - ${themeSidebarWidth})`, 27 width: '100%',
24 display: 'flex', 28 height: '100%',
25 'flex-direction': 'column', 29 overflowY: 'auto',
26 'align-items': 'center',
27 'justify-content': 'center',
28 }, 30 },
29 headline: { 31 headline: {
30 color: theme.colorHeadline, 32 color: theme.colorHeadline,
@@ -33,7 +35,76 @@ const styles = theme => ({
33 'text-align': 'center', 35 'text-align': 'center',
34 'line-height': '1.3em', 36 'line-height': '1.3em',
35 }, 37 },
36 body: { 38 announcement: {
39 height: '100vh',
40 display: 'flex',
41 flexDirection: 'column',
42 justifyContent: 'center',
43 },
44 main: {
45 flexGrow: 1,
46 '& h1': {
47 marginTop: 40,
48 fontSize: 50,
49 color: theme.styleTypes.primary.accent,
50 textAlign: 'center',
51 [`@media(min-width: ${smallScreen})`]: {
52 marginTop: 75,
53 },
54 },
55 '& h2': {
56 fontSize: 24,
57 fontWeight: 300,
58 color: theme.colorText,
59 textAlign: 'center',
60 },
61 },
62 mainBody: {
63 display: 'flex',
64 flexDirection: 'column',
65 alignItems: 'center',
66 width: 'calc(100% - 80px)',
67 height: 'auto',
68 margin: '0 auto',
69 [`@media(min-width: ${smallScreen})`]: {
70 flexDirection: 'row',
71 justifyContent: 'center',
72 },
73 },
74 mainImage: {
75 minWidth: 250,
76 maxWidth: 400,
77 margin: '0 auto',
78 marginBottom: 40,
79 '& img': {
80 width: '100%',
81 },
82 [`@media(min-width: ${smallScreen})`]: {
83 margin: 0,
84 },
85 },
86 mainText: {
87 height: 'auto',
88 maxWidth: 600,
89 textAlign: 'center',
90 '& p': {
91 lineHeight: '1.5em',
92 },
93 [`@media(min-width: ${smallScreen})`]: {
94 textAlign: 'left',
95 },
96 },
97 mainCtaButton: {
98 textAlign: 'center',
99 marginTop: 40,
100 [`@media(min-width: ${smallScreen})`]: {
101 textAlign: 'left',
102 },
103 },
104 spotlight: {
105 height: 'auto',
106 },
107 changelog: {
37 '& h3': { 108 '& h3': {
38 fontSize: '24px', 109 fontSize: '24px',
39 margin: '1.5em 0 1em 0', 110 margin: '1.5em 0 1em 0',
@@ -45,10 +116,13 @@ const styles = theme => ({
45}); 116});
46 117
47 118
48@inject('actions') @injectSheet(styles) @observer 119@inject('stores', 'actions') @injectSheet(styles) @observer
49class AnnouncementScreen extends Component { 120class AnnouncementScreen extends Component {
50 static propTypes = { 121 static propTypes = {
51 classes: PropTypes.object.isRequired, 122 classes: PropTypes.object.isRequired,
123 stores: PropTypes.shape({
124 ui: PropTypes.instanceOf(UIStore).isRequired,
125 }).isRequired,
52 }; 126 };
53 127
54 static contextTypes = { 128 static contextTypes = {
@@ -56,21 +130,55 @@ class AnnouncementScreen extends Component {
56 }; 130 };
57 131
58 render() { 132 render() {
59 const { classes } = this.props; 133 const { classes, stores } = this.props;
60 const { intl } = this.context; 134 const { intl } = this.context;
135 const { changelog, announcement } = announcementsStore;
136 const themeImage = stores.ui.isDarkThemeActive ? 'dark' : 'light';
61 return ( 137 return (
62 <div className={`${classes.container}`}> 138 <div className={`${classes.container}`}>
63 <h1 className={classes.headline}> 139 <div className={classes.announcement}>
64 {intl.formatMessage(messages.headline, { 140 <div className={classes.main}>
65 version: announcementsStore.currentVersion, 141 <h1>{announcement.main.headline}</h1>
66 })} 142 <h2>{announcement.main.subHeadline}</h2>
67 </h1> 143 <div className={classes.mainBody}>
68 <div 144 <div className={classes.mainImage}>
69 className={classes.body} 145 <img
70 dangerouslySetInnerHTML={{ 146 src={announcement.main.image[themeImage]}
71 __html: marked(announcementsStore.announcement, { sanitize: true }), 147 alt=""
72 }} 148 />
73 /> 149 </div>
150 <div className={classes.mainText}>
151 <p
152 dangerouslySetInnerHTML={{
153 __html: marked(announcement.main.text,{ sanitize: true }),
154 }}
155 />
156 <div className={classes.mainCtaButton}>
157 <Button label={announcement.main.cta.label} />
158 </div>
159 </div>
160 </div>
161 </div>
162 {announcement.spotlight && (
163 <div className={classes.spotlight}>
164 <h2>{announcement.spotlight.title}</h2>
165 </div>
166 )}
167 </div>
168 {changelog && (
169 <div className={classes.changelog}>
170 <h1 className={classes.headline}>
171 {intl.formatMessage(messages.headline, {
172 version: announcementsStore.currentVersion,
173 })}
174 </h1>
175 <div
176 dangerouslySetInnerHTML={{
177 __html: marked(changelog, { sanitize: true }),
178 }}
179 />
180 </div>
181 )}
74 </div> 182 </div>
75 ); 183 );
76 } 184 }
diff --git a/src/features/announcements/store.js b/src/features/announcements/store.js
index c59700926..d4fb0a52c 100644
--- a/src/features/announcements/store.js
+++ b/src/features/announcements/store.js
@@ -1,96 +1,93 @@
1import { action, observable, reaction } from 'mobx'; 1import {
2 action,
3 computed,
4 observable,
5 reaction,
6} from 'mobx';
2import semver from 'semver'; 7import semver from 'semver';
8import localStorage from 'mobx-localstorage';
9
3import { FeatureStore } from '../utils/FeatureStore'; 10import { FeatureStore } from '../utils/FeatureStore';
4import { getAnnouncementRequest, getCurrentVersionRequest } from './api'; 11import { getAnnouncementRequest, getChangelogRequest, getCurrentVersionRequest } from './api';
12import { announcementActions } from './actions';
13
14const LOCAL_STORAGE_KEY = 'announcements';
5 15
6const debug = require('debug')('Franz:feature:announcements:store'); 16const debug = require('debug')('Franz:feature:announcements:store');
7 17
8export class AnnouncementsStore extends FeatureStore { 18export class AnnouncementsStore extends FeatureStore {
9 19 @observable targetVersion = null;
10 @observable announcement = null;
11
12 @observable currentVersion = null;
13
14 @observable lastUsedVersion = null;
15 20
16 @observable isAnnouncementVisible = false; 21 @observable isAnnouncementVisible = false;
17 22
18 @observable isFeatureActive = false; 23 @observable isFeatureActive = false;
19 24
20 async start(stores, actions) { 25 @computed get changelog() {
21 debug('AnnouncementsStore::start'); 26 return getChangelogRequest.result;
22 this.stores = stores;
23 this.actions = actions;
24 await this.fetchLastUsedVersion();
25 await this.fetchCurrentVersion();
26 await this.fetchReleaseAnnouncement();
27 this.showAnnouncementIfNotSeenYet();
28
29 this.actions.announcements.show.listen(this._showAnnouncement.bind(this));
30 this.isFeatureActive = true;
31 } 27 }
32 28
33 stop() { 29 @computed get announcement() {
34 debug('AnnouncementsStore::stop'); 30 return getAnnouncementRequest.result;
35 this.isFeatureActive = false;
36 this.isAnnouncementVisible = false;
37 } 31 }
38 32
39 // ====== PUBLIC ====== 33 @computed get settings() {
40 34 return localStorage.getItem(LOCAL_STORAGE_KEY) || {};
41 async fetchLastUsedVersion() {
42 debug('getting last used version from local storage');
43 const lastUsedVersion = window.localStorage.getItem('lastUsedVersion');
44 this._setLastUsedVersion(lastUsedVersion == null ? '0.0.0' : lastUsedVersion);
45 } 35 }
46 36
47 async fetchCurrentVersion() { 37 @computed get lastSeenAnnouncementVersion() {
48 debug('getting current version from api'); 38 return this.settings.lastSeenAnnouncementVersion || null;
49 const version = await getCurrentVersionRequest.execute();
50 this._setCurrentVersion(version);
51 } 39 }
52 40
53 async fetchReleaseAnnouncement() { 41 @computed get currentVersion() {
54 debug('getting release announcement from api'); 42 return getCurrentVersionRequest.result;
55 try {
56 const announcement = await getAnnouncementRequest.execute(this.currentVersion);
57 this._setAnnouncement(announcement);
58 } catch (error) {
59 this._setAnnouncement(null);
60 }
61 } 43 }
62 44
63 showAnnouncementIfNotSeenYet() { 45 @computed get isNewUser() {
64 const { announcement, currentVersion, lastUsedVersion } = this; 46 return this.stores.settings.stats.appStarts <= 1;
65 if (announcement && semver.gt(currentVersion, lastUsedVersion)) {
66 debug(`${currentVersion} < ${lastUsedVersion}: announcement is shown`);
67 this._showAnnouncement();
68 } else {
69 debug(`${currentVersion} >= ${lastUsedVersion}: announcement is hidden`);
70 this._hideAnnouncement();
71 }
72 } 47 }
73 48
74 // ====== PRIVATE ====== 49 async start(stores, actions) {
50 debug('AnnouncementsStore::start');
51 this.stores = stores;
52 this.actions = actions;
53 getCurrentVersionRequest.execute();
75 54
76 @action _setCurrentVersion(version) { 55 this._registerActions([
77 debug(`setting current version to ${version}`); 56 [announcementActions.show, this._showAnnouncement],
78 this.currentVersion = version; 57 ]);
79 }
80 58
81 @action _setLastUsedVersion(version) { 59 this._registerReactions([
82 debug(`setting last used version to ${version}`); 60 this._fetchAnnouncements,
83 this.lastUsedVersion = version; 61 this._showAnnouncementToUsersWhoUpdatedApp,
62 ]);
63 this.isFeatureActive = true;
84 } 64 }
85 65
86 @action _setAnnouncement(announcement) { 66 stop() {
87 debug(`setting announcement to ${announcement}`); 67 super.stop();
88 this.announcement = announcement; 68 debug('AnnouncementsStore::stop');
69 this.isFeatureActive = false;
70 this.isAnnouncementVisible = false;
89 } 71 }
90 72
91 @action _showAnnouncement() { 73 // ======= HELPERS ======= //
74
75 _updateSettings = (changes) => {
76 localStorage.setItem(LOCAL_STORAGE_KEY, {
77 ...this.settings,
78 ...changes,
79 });
80 };
81
82 // ======= ACTIONS ======= //
83
84 @action _showAnnouncement = ({ targetVersion } = {}) => {
85 this.targetVersion = targetVersion || this.currentVersion;
92 this.isAnnouncementVisible = true; 86 this.isAnnouncementVisible = true;
93 this.actions.service.blurActive(); 87 this.actions.service.blurActive();
88 this._updateSettings({
89 lastSeenAnnouncementVersion: this.currentVersion,
90 });
94 const dispose = reaction( 91 const dispose = reaction(
95 () => this.stores.services.active, 92 () => this.stores.services.active,
96 () => { 93 () => {
@@ -98,9 +95,37 @@ export class AnnouncementsStore extends FeatureStore {
98 dispose(); 95 dispose();
99 }, 96 },
100 ); 97 );
101 } 98 };
102 99
103 @action _hideAnnouncement() { 100 @action _hideAnnouncement() {
104 this.isAnnouncementVisible = false; 101 this.isAnnouncementVisible = false;
105 } 102 }
103
104 // ======= REACTIONS ========
105
106 _showAnnouncementToUsersWhoUpdatedApp = () => {
107 const { announcement, isNewUser } = this;
108 console.log(announcement, isNewUser);
109 // Check if there is an announcement and on't show announcements to new users
110 if (!announcement || isNewUser) return;
111
112 this._showAnnouncement();
113
114 // Check if the user has already used current version (= has seen the announcement)
115 // const { currentVersion, lastSeenAnnouncementVersion } = this;
116 // if (semver.gt(currentVersion, lastSeenAnnouncementVersion)) {
117 // debug(`${currentVersion} < ${lastSeenAnnouncementVersion}: announcement is shown`);
118 // this._showAnnouncement();
119 // } else {
120 // debug(`${currentVersion} >= ${lastSeenAnnouncementVersion}: announcement is hidden`);
121 // this._hideAnnouncement();
122 // }
123 };
124
125 _fetchAnnouncements = () => {
126 const targetVersion = this.targetVersion || this.currentVersion;
127 if (!targetVersion) return;
128 getChangelogRequest.execute('5.0.1');
129 getAnnouncementRequest.execute('5.1.0');
130 }
106} 131}
diff --git a/src/features/utils/FeatureStore.js b/src/features/utils/FeatureStore.js
index 66b66a104..48962561d 100644
--- a/src/features/utils/FeatureStore.js
+++ b/src/features/utils/FeatureStore.js
@@ -5,17 +5,38 @@ export class FeatureStore {
5 5
6 _reactions = null; 6 _reactions = null;
7 7
8 _listenToActions(actions) { 8 _registerActions(actions) {
9 if (this._actions) this._actions.forEach(a => a[0].off(a[1]));
10 this._actions = []; 9 this._actions = [];
11 actions.forEach(a => this._actions.push(a)); 10 actions.forEach(a => this._actions.push(a));
11 this._startListeningToActions();
12 }
13
14 _startListeningToActions() {
15 this._stopListeningToActions();
12 this._actions.forEach(a => a[0].listen(a[1])); 16 this._actions.forEach(a => a[0].listen(a[1]));
13 } 17 }
14 18
15 _startReactions(reactions) { 19 _stopListeningToActions() {
16 if (this._reactions) this._reactions.forEach(r => r.stop()); 20 this._actions.forEach(a => a[0].off(a[1]));
21 }
22
23 _registerReactions(reactions) {
17 this._reactions = []; 24 this._reactions = [];
18 reactions.forEach(r => this._reactions.push(new Reaction(r))); 25 reactions.forEach(r => this._reactions.push(new Reaction(r)));
26 this._startReactions();
27 }
28
29 _startReactions() {
30 this._stopReactions();
19 this._reactions.forEach(r => r.start()); 31 this._reactions.forEach(r => r.start());
20 } 32 }
33
34 _stopReactions() {
35 this._reactions.forEach(r => r.stop());
36 }
37
38 stop() {
39 this._stopListeningToActions();
40 this._stopReactions();
41 }
21} 42}
diff --git a/src/features/workspaces/store.js b/src/features/workspaces/store.js
index ea601700e..4841a4e08 100644
--- a/src/features/workspaces/store.js
+++ b/src/features/workspaces/store.js
@@ -51,12 +51,16 @@ export default class WorkspacesStore extends FeatureStore {
51 return getUserWorkspacesRequest.wasExecuted && this.workspaces.length > 0; 51 return getUserWorkspacesRequest.wasExecuted && this.workspaces.length > 0;
52 } 52 }
53 53
54 @computed get isUserAllowedToUseFeature() {
55 return !this.isPremiumUpgradeRequired;
56 }
57
54 start(stores, actions) { 58 start(stores, actions) {
55 debug('WorkspacesStore::start'); 59 debug('WorkspacesStore::start');
56 this.stores = stores; 60 this.stores = stores;
57 this.actions = actions; 61 this.actions = actions;
58 62
59 this._listenToActions([ 63 this._registerActions([
60 [workspaceActions.edit, this._edit], 64 [workspaceActions.edit, this._edit],
61 [workspaceActions.create, this._create], 65 [workspaceActions.create, this._create],
62 [workspaceActions.delete, this._delete], 66 [workspaceActions.delete, this._delete],
@@ -67,7 +71,7 @@ export default class WorkspacesStore extends FeatureStore {
67 [workspaceActions.openWorkspaceSettings, this._openWorkspaceSettings], 71 [workspaceActions.openWorkspaceSettings, this._openWorkspaceSettings],
68 ]); 72 ]);
69 73
70 this._startReactions([ 74 this._registerReactions([
71 this._setWorkspaceBeingEditedReaction, 75 this._setWorkspaceBeingEditedReaction,
72 this._setActiveServiceOnWorkspaceSwitchReaction, 76 this._setActiveServiceOnWorkspaceSwitchReaction,
73 this._setFeatureEnabledReaction, 77 this._setFeatureEnabledReaction,
@@ -75,6 +79,7 @@ export default class WorkspacesStore extends FeatureStore {
75 this._activateLastUsedWorkspaceReaction, 79 this._activateLastUsedWorkspaceReaction,
76 this._openDrawerWithSettingsReaction, 80 this._openDrawerWithSettingsReaction,
77 this._cleanupInvalidServiceReferences, 81 this._cleanupInvalidServiceReferences,
82 this._disableActionsForFreeUser,
78 ]); 83 ]);
79 84
80 getUserWorkspacesRequest.execute(); 85 getUserWorkspacesRequest.execute();
@@ -82,6 +87,7 @@ export default class WorkspacesStore extends FeatureStore {
82 } 87 }
83 88
84 stop() { 89 stop() {
90 super.stop();
85 debug('WorkspacesStore::stop'); 91 debug('WorkspacesStore::stop');
86 this.isFeatureActive = false; 92 this.isFeatureActive = false;
87 this.activeWorkspace = null; 93 this.activeWorkspace = null;
@@ -273,4 +279,12 @@ export default class WorkspacesStore extends FeatureStore {
273 getUserWorkspacesRequest.execute(); 279 getUserWorkspacesRequest.execute();
274 } 280 }
275 }; 281 };
282
283 _disableActionsForFreeUser = () => {
284 if (!this.isUserAllowedToUseFeature) {
285 this._stopListeningToActions();
286 } else {
287 this._startListeningToActions();
288 }
289 }
276} 290}