aboutsummaryrefslogtreecommitdiffstats
path: root/src/features/announcements
diff options
context:
space:
mode:
Diffstat (limited to 'src/features/announcements')
-rw-r--r--src/features/announcements/actions.js10
-rw-r--r--src/features/announcements/api.js35
-rw-r--r--src/features/announcements/components/AnnouncementScreen.js300
-rw-r--r--src/features/announcements/constants.js5
-rw-r--r--src/features/announcements/index.js29
-rw-r--r--src/features/announcements/store.js148
6 files changed, 0 insertions, 527 deletions
diff --git a/src/features/announcements/actions.js b/src/features/announcements/actions.js
deleted file mode 100644
index bab496314..000000000
--- a/src/features/announcements/actions.js
+++ /dev/null
@@ -1,10 +0,0 @@
1import PropTypes from 'prop-types';
2import { createActionsFromDefinitions } from '../../actions/lib/actions';
3
4export const announcementActions = createActionsFromDefinitions({
5 show: {
6 targetVersion: PropTypes.string,
7 },
8}, PropTypes.checkPropTypes);
9
10export default announcementActions;
diff --git a/src/features/announcements/api.js b/src/features/announcements/api.js
deleted file mode 100644
index 962f3e694..000000000
--- a/src/features/announcements/api.js
+++ /dev/null
@@ -1,35 +0,0 @@
1import Request from '../../stores/lib/Request';
2import apiBase from '../../api/apiBase';
3import { GITHUB_FERDI_REPO_NAME, GITHUB_NIGHTLIES_REPO_NAME, GITHUB_ORG_NAME } from '../../config';
4import { ferdiVersion } from '../../environment';
5
6const debug = require('debug')('Ferdi:feature:announcements:api');
7
8export const announcementsApi = {
9 async getCurrentVersion() {
10 debug('getting current version of electron app');
11 return Promise.resolve(ferdiVersion);
12 },
13
14 async getChangelog(version) {
15 const ferdiRepoName = version.includes('nightly') ? GITHUB_NIGHTLIES_REPO_NAME : GITHUB_FERDI_REPO_NAME;
16 const url = `https://api.github.com/repos/${GITHUB_ORG_NAME}/${ferdiRepoName}/releases/tags/v${version}`;
17 debug(`fetching release changelog from Github url: ${url}`);
18 const request = await window.fetch(url, { method: 'GET' });
19 if (!request.ok) return null;
20 const data = await request.json();
21 return data.body;
22 },
23
24 async getAnnouncement(version) {
25 const url = `${apiBase()}/announcements/${version}`;
26 debug(`fetching release announcement from api url: ${url}`);
27 const response = await window.fetch(url, { method: 'GET' });
28 if (!response.ok) return null;
29 return response.json();
30 },
31};
32
33export const getCurrentVersionRequest = new Request(announcementsApi, 'getCurrentVersion');
34export const getChangelogRequest = new Request(announcementsApi, 'getChangelog');
35export const getAnnouncementRequest = new Request(announcementsApi, 'getAnnouncement');
diff --git a/src/features/announcements/components/AnnouncementScreen.js b/src/features/announcements/components/AnnouncementScreen.js
deleted file mode 100644
index 315843db3..000000000
--- a/src/features/announcements/components/AnnouncementScreen.js
+++ /dev/null
@@ -1,300 +0,0 @@
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 { Button } from '@meetfranz/forms';
8
9import { announcementsStore } from '../index';
10import UIStore from '../../../stores/UIStore';
11import AppStore from '../../../stores/AppStore';
12
13const renderer = new marked.Renderer();
14
15renderer.link = (href, title, text) => `<a target="_blank" href="${href}" title="${title}">${text}</a>`;
16
17const markedOptions = { sanitize: true, renderer };
18
19const messages = defineMessages({
20 headline: {
21 id: 'feature.announcements.changelog.headline',
22 defaultMessage: '!!!Changes in Ferdi {version}',
23 },
24});
25
26const smallScreen = '1000px';
27
28const styles = (theme) => ({
29 container: {
30 background: theme.colorBackground,
31 position: 'relative',
32 top: 0,
33 zIndex: 140,
34 width: '100%',
35 height: '100%',
36 overflowY: 'auto',
37 },
38 headline: {
39 color: theme.colorHeadline,
40 margin: [25, 0, 40],
41 // 'max-width': 500,
42 'text-align': 'center',
43 'line-height': '1.3em',
44 },
45 announcement: {
46 height: 'auto',
47
48 [`@media(min-width: ${smallScreen})`]: {
49 display: 'flex',
50 flexDirection: 'column',
51 justifyContent: 'center',
52 height: '100vh',
53 },
54 },
55 main: {
56 display: 'flex',
57 flexDirection: 'column',
58 flexGrow: 1,
59 justifyContent: 'center',
60
61 '& h1': {
62 margin: [40, 0, 15],
63 fontSize: 70,
64 color: theme.styleTypes.primary.accent,
65 textAlign: 'center',
66
67 [`@media(min-width: ${smallScreen})`]: {
68 marginTop: 0,
69 },
70 },
71 '& h2': {
72 fontSize: 30,
73 fontWeight: 300,
74 color: theme.colorText,
75 textAlign: 'center',
76 marginBottom: 60,
77 },
78 },
79 mainBody: {
80 display: 'flex',
81 flexDirection: 'column',
82 alignItems: 'center',
83 width: 'calc(100% - 80px)',
84 height: 'auto',
85 margin: '0 auto',
86 [`@media(min-width: ${smallScreen})`]: {
87 flexDirection: 'row',
88 justifyContent: 'center',
89 },
90 },
91 mainImage: {
92 minWidth: 250,
93 maxWidth: 400,
94 margin: '0 auto',
95 marginBottom: 40,
96 '& img': {
97 width: '100%',
98 },
99 [`@media(min-width: ${smallScreen})`]: {
100 margin: 0,
101 },
102 },
103 mainText: {
104 height: 'auto',
105 maxWidth: 600,
106 textAlign: 'center',
107 '& p': {
108 lineHeight: '1.5em',
109 },
110 [`@media(min-width: ${smallScreen})`]: {
111 textAlign: 'left',
112 },
113 },
114 mainCtaButton: {
115 textAlign: 'center',
116 marginTop: 40,
117 [`@media(min-width: ${smallScreen})`]: {
118 textAlign: 'left',
119 },
120 },
121 spotlight: {
122 height: 'auto',
123 background: theme.announcements.spotlight.background,
124 padding: [40, 0],
125 marginTop: 80,
126 [`@media(min-width: ${smallScreen})`]: {
127 marginTop: 0,
128 justifyContent: 'center',
129 alignItems: 'flex-start',
130 display: 'flex',
131 flexDirection: 'row',
132 },
133 },
134 spotlightTopicContainer: {
135 textAlign: 'center',
136 marginBottom: 20,
137
138 [`@media(min-width: ${smallScreen})`]: {
139 marginBottom: 0,
140 minWidth: 250,
141 maxWidth: 330,
142 width: '100%',
143 textAlign: 'right',
144 marginRight: 60,
145 },
146 },
147 spotlightContentContainer: {
148 textAlign: 'center',
149 [`@media(min-width: ${smallScreen})`]: {
150 height: 'auto',
151 maxWidth: 600,
152 paddingRight: 40,
153 textAlign: 'left',
154 },
155 '& p': {
156 lineHeight: '1.5em',
157 },
158 },
159 spotlightTopic: {
160 fontSize: 20,
161 marginBottom: 5,
162 letterSpacing: 0,
163 fontWeight: 100,
164 },
165 spotlightSubject: {
166 fontSize: 20,
167 },
168 changelog: {
169 padding: [0, 60],
170 maxWidth: 700,
171 margin: [100, 'auto'],
172 height: 'auto',
173
174 '& h3': {
175 fontSize: '24px',
176 margin: '1.5em 0 1em 0',
177 },
178 '& li': {
179 marginBottom: '1em',
180 lineHeight: '1.4em',
181 },
182 '& div': {
183 height: 'auto',
184 },
185 },
186});
187
188@inject('stores', 'actions') @injectSheet(styles) @observer
189class AnnouncementScreen extends Component {
190 static propTypes = {
191 classes: PropTypes.object.isRequired,
192 stores: PropTypes.shape({
193 ui: PropTypes.instanceOf(UIStore).isRequired,
194 }).isRequired,
195 actions: PropTypes.shape({
196 app: PropTypes.instanceOf(AppStore).isRequired,
197 }).isRequired,
198 };
199
200 static contextTypes = {
201 intl: intlShape,
202 };
203
204 render() {
205 const { classes, stores, actions } = this.props;
206 const { intl } = this.context;
207 const { changelog, announcement } = announcementsStore;
208 const themeImage = stores.ui.isDarkThemeActive ? 'dark' : 'light';
209 return (
210 <div className={classes.container}>
211 {announcement && (
212 <div className={classes.announcement}>
213 <div className={classes.main}>
214 <h1>{announcement.main.headline}</h1>
215 <h2>{announcement.main.subHeadline}</h2>
216 <div className={classes.mainBody}>
217 <div className={classes.mainImage}>
218 <img
219 src={announcement.main.image[themeImage]}
220 alt=""
221 />
222 </div>
223 <div className={classes.mainText}>
224 <div
225 dangerouslySetInnerHTML={{
226 __html: marked(announcement.main.text, markedOptions),
227 }}
228 />
229 {(announcement.main.cta.label || announcement.main.cta.href) && (
230 <div className={classes.mainCtaButton}>
231 <Button
232 label={announcement.main.cta.label}
233 onClick={() => {
234 const {
235 href,
236 } = announcement.main.cta;
237 if (announcement.main.cta.href.startsWith('http')) {
238 actions.app.openExternalUrl({ url: href });
239 } else {
240 window.location.href = `#${href}`;
241 }
242 }}
243 />
244 </div>
245 )}
246 </div>
247 </div>
248 </div>
249 {announcement.spotlight && (
250 <div className={classes.spotlight}>
251 <div className={classes.spotlightTopicContainer}>
252 <h2 className={classes.spotlightTopic}>{announcement.spotlight.title}</h2>
253 <h3 className={classes.spotlightSubject}>{announcement.spotlight.subject}</h3>
254 </div>
255 <div className={classes.spotlightContentContainer}>
256 <div
257 dangerouslySetInnerHTML={{
258 __html: marked(announcement.spotlight.text, markedOptions),
259 }}
260 />
261 <div className={classes.mainCtaButton}>
262 <Button
263 label={announcement.spotlight.cta.label}
264 onClick={() => {
265 const {
266 href,
267 } = announcement.spotlight.cta;
268 if (announcement.spotlight.cta.href.startsWith('http')) {
269 actions.app.openExternalUrl({ url: href });
270 } else {
271 window.location.href = `#${href}`;
272 }
273 }}
274 />
275 </div>
276 </div>
277 </div>
278 )}
279 </div>
280 )}
281 {changelog && (
282 <div className={classes.changelog}>
283 <h1 className={classes.headline}>
284 {intl.formatMessage(messages.headline, {
285 version: announcementsStore.targetVersion,
286 })}
287 </h1>
288 <div
289 dangerouslySetInnerHTML={{
290 __html: marked(changelog, markedOptions),
291 }}
292 />
293 </div>
294 )}
295 </div>
296 );
297 }
298}
299
300export default AnnouncementScreen;
diff --git a/src/features/announcements/constants.js b/src/features/announcements/constants.js
deleted file mode 100644
index 284226fdf..000000000
--- a/src/features/announcements/constants.js
+++ /dev/null
@@ -1,5 +0,0 @@
1export const ANNOUNCEMENTS_ROUTES = {
2 TARGET: '/announcements/:id',
3};
4
5export const GA_CATEGORY_ANNOUNCEMENTS = 'Announcements';
diff --git a/src/features/announcements/index.js b/src/features/announcements/index.js
deleted file mode 100644
index 19930c5b1..000000000
--- a/src/features/announcements/index.js
+++ /dev/null
@@ -1,29 +0,0 @@
1import { reaction } from 'mobx';
2import { AnnouncementsStore } from './store';
3
4const debug = require('debug')('Ferdi:feature:announcements');
5
6export const GA_CATEGORY_ANNOUNCEMENTS = 'Announcements';
7
8export const announcementsStore = new AnnouncementsStore();
9
10export default function initAnnouncements(stores, actions) {
11 const { features } = stores;
12
13 // Toggle announcement feature
14 reaction(
15 () => (
16 features.features.isAnnouncementsEnabled
17 ),
18 (isEnabled) => {
19 if (isEnabled) {
20 debug('Initializing `announcements` feature');
21 announcementsStore.start(stores, actions);
22 } else if (announcementsStore.isFeatureActive) {
23 debug('Disabling `announcements` feature');
24 announcementsStore.stop();
25 }
26 },
27 { fireImmediately: true },
28 );
29}
diff --git a/src/features/announcements/store.js b/src/features/announcements/store.js
deleted file mode 100644
index 794d20142..000000000
--- a/src/features/announcements/store.js
+++ /dev/null
@@ -1,148 +0,0 @@
1import {
2 action,
3 computed,
4 observable,
5} from 'mobx';
6import semver from 'semver';
7import localStorage from 'mobx-localstorage';
8
9import { FeatureStore } from '../utils/FeatureStore';
10import { ANNOUNCEMENTS_ROUTES } from './constants';
11import { getAnnouncementRequest, getChangelogRequest, getCurrentVersionRequest } from './api';
12import { announcementActions } from './actions';
13import { createActionBindings } from '../utils/ActionBinding';
14import { createReactions } from '../../stores/lib/Reaction';
15import { matchRoute } from '../../helpers/routing-helpers';
16import { DEFAULT_APP_SETTINGS } from '../../environment';
17
18const LOCAL_STORAGE_KEY = 'announcements';
19
20const debug = require('debug')('Ferdi:feature:announcements:store');
21
22export class AnnouncementsStore extends FeatureStore {
23 @observable targetVersion = null;
24
25 @observable isFeatureActive = false;
26
27 @computed get changelog() {
28 return getChangelogRequest.result;
29 }
30
31 @computed get announcement() {
32 if (!this.stores || !getAnnouncementRequest.result) return null;
33 const { locale } = this.stores.app;
34 const announcement = getAnnouncementRequest.result;
35 // User locale
36 if (announcement[locale]) return announcement[locale];
37 // Default locale
38 if (announcement[DEFAULT_APP_SETTINGS.fallbackLocale]) return announcement[DEFAULT_APP_SETTINGS.fallbackLocale];
39 // No locales specified
40 return announcement;
41 }
42
43 @computed get areNewsAvailable() {
44 const isChangelogAvailable = getChangelogRequest.wasExecuted && !!this.changelog;
45 const isAnnouncementAvailable = getAnnouncementRequest.wasExecuted && !!this.announcement;
46 return isChangelogAvailable || isAnnouncementAvailable;
47 }
48
49 @computed get settings() {
50 return localStorage.getItem(LOCAL_STORAGE_KEY) || {};
51 }
52
53 @computed get lastSeenAnnouncementVersion() {
54 return this.settings.lastSeenAnnouncementVersion || null;
55 }
56
57 @computed get currentVersion() {
58 return getCurrentVersionRequest.result;
59 }
60
61 @computed get isNewUser() {
62 return this.stores.settings.stats.appStarts <= 1;
63 }
64
65 @computed get isAnnouncementShown() {
66 const { router } = this.stores;
67 return router.location.pathname.includes('/announcements');
68 }
69
70 async start(stores, actions) {
71 debug('AnnouncementsStore::start');
72 this.stores = stores;
73 this.actions = actions;
74 getCurrentVersionRequest.execute();
75
76 this._registerActions(createActionBindings([
77 [announcementActions.show, this._showAnnouncement],
78 ]));
79
80 this._reactions = createReactions([
81 this._showAnnouncementOnRouteMatch,
82 this._showAnnouncementToUsersWhoUpdatedApp,
83 this._fetchAnnouncements,
84 ]);
85 this._registerReactions(this._reactions);
86 this.isFeatureActive = true;
87 }
88
89 stop() {
90 super.stop();
91 debug('AnnouncementsStore::stop');
92 this.isFeatureActive = false;
93 }
94
95 // ======= HELPERS ======= //
96
97 _updateSettings = (changes) => {
98 localStorage.setItem(LOCAL_STORAGE_KEY, {
99 ...this.settings,
100 ...changes,
101 });
102 };
103
104 // ======= ACTIONS ======= //
105
106 @action _showAnnouncement = ({ targetVersion } = {}) => {
107 const { router } = this.stores;
108 this.targetVersion = targetVersion || this.currentVersion;
109 this._updateSettings({
110 lastSeenAnnouncementVersion: this.currentVersion,
111 });
112 const targetRoute = `/announcements/${this.targetVersion}`;
113 if (router.location.pathname !== targetRoute) {
114 this.stores.router.push(targetRoute);
115 }
116 };
117
118 // ======= REACTIONS ========
119
120 _showAnnouncementToUsersWhoUpdatedApp = () => {
121 const { announcement, isNewUser } = this;
122 // Check if there is an announcement and don't show announcements to new users
123 if (!announcement || isNewUser) return;
124
125 // Check if the user has already used current version (= has seen the announcement)
126 const { currentVersion, lastSeenAnnouncementVersion } = this;
127 if (semver.gt(currentVersion, lastSeenAnnouncementVersion || '0.0.0')) {
128 debug(`${currentVersion} > ${lastSeenAnnouncementVersion}: announcement is shown`);
129 this._showAnnouncement();
130 }
131 };
132
133 _fetchAnnouncements = () => {
134 const targetVersion = this.targetVersion || this.currentVersion;
135 if (!targetVersion) return;
136 getChangelogRequest.reset().execute(targetVersion);
137 getAnnouncementRequest.reset().execute(targetVersion);
138 };
139
140 _showAnnouncementOnRouteMatch = () => {
141 const { router } = this.stores;
142 const match = matchRoute(ANNOUNCEMENTS_ROUTES.TARGET, router.location.pathname);
143 if (match) {
144 const targetVersion = match.id;
145 this._showAnnouncement({ targetVersion });
146 }
147 }
148}