aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Dominik Guzei <dominik.guzei@gmail.com>2019-03-12 21:36:10 +0100
committerLibravatar Dominik Guzei <dominik.guzei@gmail.com>2019-03-12 21:37:33 +0100
commit6fb07bcb716af76ec2e96345f37624d12d0d1af0 (patch)
tree276191a782dc1d44f78331e548e43ff71758baca
parentrefactor server api even more (diff)
downloadferdium-app-6fb07bcb716af76ec2e96345f37624d12d0d1af0.tar.gz
ferdium-app-6fb07bcb716af76ec2e96345f37624d12d0d1af0.tar.zst
ferdium-app-6fb07bcb716af76ec2e96345f37624d12d0d1af0.zip
implements basic release announcement feature
-rw-r--r--.eslintrc2
-rw-r--r--package-lock.json5
-rw-r--r--package.json1
-rw-r--r--src/actions/index.js6
-rw-r--r--src/actions/service.js1
-rw-r--r--src/components/layout/AppLayout.js4
-rw-r--r--src/config.js1
-rw-r--r--src/containers/layout/AppLayoutContainer.js2
-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
-rw-r--r--src/i18n/locales/defaultMessages.json95
-rw-r--r--src/i18n/locales/en-US.json2
-rw-r--r--src/i18n/messages/src/components/layout/AppLayout.json24
-rw-r--r--src/i18n/messages/src/features/announcements/Component.json15
-rw-r--r--src/i18n/messages/src/lib/Menu.json53
-rw-r--r--src/lib/Menu.js10
-rw-r--r--src/stores/FeaturesStore.js2
-rw-r--r--src/stores/ServicesStore.js6
22 files changed, 416 insertions, 66 deletions
diff --git a/.eslintrc b/.eslintrc
index 743946d35..a4ffd505c 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -14,7 +14,7 @@
14 "react/jsx-filename-extension": [1, { 14 "react/jsx-filename-extension": [1, {
15 "extensions": [".js", ".jsx"] 15 "extensions": [".js", ".jsx"]
16 }], 16 }],
17 "react/forbid-prop-types": 1, 17 "react/forbid-prop-types": 0,
18 "react/destructuring-assignment": 1, 18 "react/destructuring-assignment": 1,
19 "prefer-destructuring": 1, 19 "prefer-destructuring": 1,
20 "no-underscore-dangle": 0, 20 "no-underscore-dangle": 0,
diff --git a/package-lock.json b/package-lock.json
index 8499abda9..bc333ae50 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12094,6 +12094,11 @@
12094 "object-visit": "^1.0.0" 12094 "object-visit": "^1.0.0"
12095 } 12095 }
12096 }, 12096 },
12097 "marked": {
12098 "version": "0.6.1",
12099 "resolved": "https://registry.npmjs.org/marked/-/marked-0.6.1.tgz",
12100 "integrity": "sha512-+H0L3ibcWhAZE02SKMqmvYsErLo4EAVJxu5h3bHBBDvvjeWXtl92rGUSBYHL2++5Y+RSNgl8dYOAXcYe7lp1fA=="
12101 },
12097 "matchdep": { 12102 "matchdep": {
12098 "version": "2.0.0", 12103 "version": "2.0.0",
12099 "resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz", 12104 "resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz",
diff --git a/package.json b/package.json
index 14e0df7ca..4ddc83777 100644
--- a/package.json
+++ b/package.json
@@ -54,6 +54,7 @@
54 "hex-to-rgba": "1.0.2", 54 "hex-to-rgba": "1.0.2",
55 "jsonwebtoken": "^7.4.1", 55 "jsonwebtoken": "^7.4.1",
56 "lodash": "^4.17.4", 56 "lodash": "^4.17.4",
57 "marked": "0.6.1",
57 "mdi": "^1.9.33", 58 "mdi": "^1.9.33",
58 "mime-types": "2.1.21", 59 "mime-types": "2.1.21",
59 "mobx": "5.7.0", 60 "mobx": "5.7.0",
diff --git a/src/actions/index.js b/src/actions/index.js
index 59acabb0b..dc1d3b6b2 100644
--- a/src/actions/index.js
+++ b/src/actions/index.js
@@ -11,6 +11,7 @@ import payment from './payment';
11import news from './news'; 11import news from './news';
12import settings from './settings'; 12import settings from './settings';
13import requests from './requests'; 13import requests from './requests';
14import announcements from '../features/announcements/actions';
14 15
15const actions = Object.assign({}, { 16const actions = Object.assign({}, {
16 service, 17 service,
@@ -25,4 +26,7 @@ const actions = Object.assign({}, {
25 requests, 26 requests,
26}); 27});
27 28
28export default defineActions(actions, PropTypes.checkPropTypes); 29export default Object.assign(
30 defineActions(actions, PropTypes.checkPropTypes),
31 { announcements },
32);
diff --git a/src/actions/service.js b/src/actions/service.js
index ceaabc31e..ce62560a9 100644
--- a/src/actions/service.js
+++ b/src/actions/service.js
@@ -5,6 +5,7 @@ export default {
5 setActive: { 5 setActive: {
6 serviceId: PropTypes.string.isRequired, 6 serviceId: PropTypes.string.isRequired,
7 }, 7 },
8 blurActive: {},
8 setActiveNext: {}, 9 setActiveNext: {},
9 setActivePrev: {}, 10 setActivePrev: {},
10 showAddServiceInterface: { 11 showAddServiceInterface: {
diff --git a/src/components/layout/AppLayout.js b/src/components/layout/AppLayout.js
index 593149e72..2bda91f73 100644
--- a/src/components/layout/AppLayout.js
+++ b/src/components/layout/AppLayout.js
@@ -13,6 +13,7 @@ import ErrorBoundary from '../util/ErrorBoundary';
13// import globalMessages from '../../i18n/globalMessages'; 13// import globalMessages from '../../i18n/globalMessages';
14 14
15import { isWindows } from '../../environment'; 15import { isWindows } from '../../environment';
16import AnnouncementScreen from '../../features/announcements/Component';
16 17
17function createMarkup(HTMLString) { 18function createMarkup(HTMLString) {
18 return { __html: HTMLString }; 19 return { __html: HTMLString };
@@ -64,6 +65,7 @@ export default @observer class AppLayout extends Component {
64 areRequiredRequestsLoading: PropTypes.bool.isRequired, 65 areRequiredRequestsLoading: PropTypes.bool.isRequired,
65 darkMode: PropTypes.bool.isRequired, 66 darkMode: PropTypes.bool.isRequired,
66 isDelayAppScreenVisible: PropTypes.bool.isRequired, 67 isDelayAppScreenVisible: PropTypes.bool.isRequired,
68 isAnnouncementVisible: PropTypes.bool.isRequired,
67 }; 69 };
68 70
69 static defaultProps = { 71 static defaultProps = {
@@ -93,6 +95,7 @@ export default @observer class AppLayout extends Component {
93 areRequiredRequestsLoading, 95 areRequiredRequestsLoading,
94 darkMode, 96 darkMode,
95 isDelayAppScreenVisible, 97 isDelayAppScreenVisible,
98 isAnnouncementVisible,
96 } = this.props; 99 } = this.props;
97 100
98 const { intl } = this.context; 101 const { intl } = this.context;
@@ -166,6 +169,7 @@ export default @observer class AppLayout extends Component {
166 {isDelayAppScreenVisible && (<DelayApp />)} 169 {isDelayAppScreenVisible && (<DelayApp />)}
167 <BasicAuth /> 170 <BasicAuth />
168 <ShareFranz /> 171 <ShareFranz />
172 {isAnnouncementVisible && (<AnnouncementScreen />)}
169 {services} 173 {services}
170 </div> 174 </div>
171 </div> 175 </div>
diff --git a/src/config.js b/src/config.js
index 479572edb..47d22ca7d 100644
--- a/src/config.js
+++ b/src/config.js
@@ -41,6 +41,7 @@ export const DEFAULT_FEATURES_CONFIG = {
41 }, 41 },
42 isServiceProxyEnabled: false, 42 isServiceProxyEnabled: false,
43 isServiceProxyPremiumFeature: true, 43 isServiceProxyPremiumFeature: true,
44 isAnnouncementsEnabled: true,
44}; 45};
45 46
46export const DEFAULT_WINDOW_OPTIONS = { 47export const DEFAULT_WINDOW_OPTIONS = {
diff --git a/src/containers/layout/AppLayoutContainer.js b/src/containers/layout/AppLayoutContainer.js
index 5a05ce431..f26e51517 100644
--- a/src/containers/layout/AppLayoutContainer.js
+++ b/src/containers/layout/AppLayoutContainer.js
@@ -20,6 +20,7 @@ import Services from '../../components/services/content/Services';
20import AppLoader from '../../components/ui/AppLoader'; 20import AppLoader from '../../components/ui/AppLoader';
21 21
22import { state as delayAppState } from '../../features/delayApp'; 22import { state as delayAppState } from '../../features/delayApp';
23import { announcementsState } from '../../features/announcements/state';
23 24
24export default @inject('stores', 'actions') @observer class AppLayoutContainer extends Component { 25export default @inject('stores', 'actions') @observer class AppLayoutContainer extends Component {
25 static defaultProps = { 26 static defaultProps = {
@@ -134,6 +135,7 @@ export default @inject('stores', 'actions') @observer class AppLayoutContainer e
134 areRequiredRequestsLoading={requests.areRequiredRequestsLoading} 135 areRequiredRequestsLoading={requests.areRequiredRequestsLoading}
135 darkMode={settings.all.app.darkMode} 136 darkMode={settings.all.app.darkMode}
136 isDelayAppScreenVisible={delayAppState.isDelayAppScreenVisible} 137 isDelayAppScreenVisible={delayAppState.isDelayAppScreenVisible}
138 isAnnouncementVisible={announcementsState.isAnnouncementVisible}
137 > 139 >
138 {React.Children.count(children) > 0 ? children : null} 140 {React.Children.count(children) > 0 ? children : null}
139 </AppLayout> 141 </AppLayout>
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}
diff --git a/src/i18n/locales/defaultMessages.json b/src/i18n/locales/defaultMessages.json
index 0641c510c..fcd24c7ef 100644
--- a/src/i18n/locales/defaultMessages.json
+++ b/src/i18n/locales/defaultMessages.json
@@ -625,78 +625,78 @@
625 "defaultMessage": "!!!Your services have been updated.", 625 "defaultMessage": "!!!Your services have been updated.",
626 "end": { 626 "end": {
627 "column": 3, 627 "column": 3,
628 "line": 25 628 "line": 26
629 }, 629 },
630 "file": "src/components/layout/AppLayout.js", 630 "file": "src/components/layout/AppLayout.js",
631 "id": "infobar.servicesUpdated", 631 "id": "infobar.servicesUpdated",
632 "start": { 632 "start": {
633 "column": 19, 633 "column": 19,
634 "line": 22 634 "line": 23
635 } 635 }
636 }, 636 },
637 { 637 {
638 "defaultMessage": "!!!A new update for Franz is available.", 638 "defaultMessage": "!!!A new update for Franz is available.",
639 "end": { 639 "end": {
640 "column": 3, 640 "column": 3,
641 "line": 29 641 "line": 30
642 }, 642 },
643 "file": "src/components/layout/AppLayout.js", 643 "file": "src/components/layout/AppLayout.js",
644 "id": "infobar.updateAvailable", 644 "id": "infobar.updateAvailable",
645 "start": { 645 "start": {
646 "column": 19, 646 "column": 19,
647 "line": 26 647 "line": 27
648 } 648 }
649 }, 649 },
650 { 650 {
651 "defaultMessage": "!!!Reload services", 651 "defaultMessage": "!!!Reload services",
652 "end": { 652 "end": {
653 "column": 3, 653 "column": 3,
654 "line": 33 654 "line": 34
655 }, 655 },
656 "file": "src/components/layout/AppLayout.js", 656 "file": "src/components/layout/AppLayout.js",
657 "id": "infobar.buttonReloadServices", 657 "id": "infobar.buttonReloadServices",
658 "start": { 658 "start": {
659 "column": 24, 659 "column": 24,
660 "line": 30 660 "line": 31
661 } 661 }
662 }, 662 },
663 { 663 {
664 "defaultMessage": "!!!Changelog", 664 "defaultMessage": "!!!Changelog",
665 "end": { 665 "end": {
666 "column": 3, 666 "column": 3,
667 "line": 37 667 "line": 38
668 }, 668 },
669 "file": "src/components/layout/AppLayout.js", 669 "file": "src/components/layout/AppLayout.js",
670 "id": "infobar.buttonChangelog", 670 "id": "infobar.buttonChangelog",
671 "start": { 671 "start": {
672 "column": 13, 672 "column": 13,
673 "line": 34 673 "line": 35
674 } 674 }
675 }, 675 },
676 { 676 {
677 "defaultMessage": "!!!Restart & install update", 677 "defaultMessage": "!!!Restart & install update",
678 "end": { 678 "end": {
679 "column": 3, 679 "column": 3,
680 "line": 41 680 "line": 42
681 }, 681 },
682 "file": "src/components/layout/AppLayout.js", 682 "file": "src/components/layout/AppLayout.js",
683 "id": "infobar.buttonInstallUpdate", 683 "id": "infobar.buttonInstallUpdate",
684 "start": { 684 "start": {
685 "column": 23, 685 "column": 23,
686 "line": 38 686 "line": 39
687 } 687 }
688 }, 688 },
689 { 689 {
690 "defaultMessage": "!!!Could not load services and user information", 690 "defaultMessage": "!!!Could not load services and user information",
691 "end": { 691 "end": {
692 "column": 3, 692 "column": 3,
693 "line": 45 693 "line": 46
694 }, 694 },
695 "file": "src/components/layout/AppLayout.js", 695 "file": "src/components/layout/AppLayout.js",
696 "id": "infobar.requiredRequestsFailed", 696 "id": "infobar.requiredRequestsFailed",
697 "start": { 697 "start": {
698 "column": 26, 698 "column": 26,
699 "line": 42 699 "line": 43
700 } 700 }
701 } 701 }
702 ], 702 ],
@@ -3025,6 +3025,24 @@
3025 { 3025 {
3026 "descriptors": [ 3026 "descriptors": [
3027 { 3027 {
3028 "defaultMessage": "!!!What's new in Franz {version}?",
3029 "end": {
3030 "column": 3,
3031 "line": 14
3032 },
3033 "file": "src/features/announcements/Component.js",
3034 "id": "feature.announcements.headline",
3035 "start": {
3036 "column": 12,
3037 "line": 11
3038 }
3039 }
3040 ],
3041 "path": "src/features/announcements/Component.json"
3042 },
3043 {
3044 "descriptors": [
3045 {
3028 "defaultMessage": "!!!Please purchase license to skip waiting", 3046 "defaultMessage": "!!!Please purchase license to skip waiting",
3029 "end": { 3047 "end": {
3030 "column": 3, 3048 "column": 3,
@@ -3799,133 +3817,146 @@
3799 } 3817 }
3800 }, 3818 },
3801 { 3819 {
3802 "defaultMessage": "!!!Settings", 3820 "defaultMessage": "!!!What's new in Franz?",
3803 "end": { 3821 "end": {
3804 "column": 3, 3822 "column": 3,
3805 "line": 161 3823 "line": 161
3806 }, 3824 },
3807 "file": "src/lib/Menu.js", 3825 "file": "src/lib/Menu.js",
3826 "id": "menu.app.announcement",
3827 "start": {
3828 "column": 16,
3829 "line": 158
3830 }
3831 },
3832 {
3833 "defaultMessage": "!!!Settings",
3834 "end": {
3835 "column": 3,
3836 "line": 165
3837 },
3838 "file": "src/lib/Menu.js",
3808 "id": "menu.app.settings", 3839 "id": "menu.app.settings",
3809 "start": { 3840 "start": {
3810 "column": 12, 3841 "column": 12,
3811 "line": 158 3842 "line": 162
3812 } 3843 }
3813 }, 3844 },
3814 { 3845 {
3815 "defaultMessage": "!!!Hide", 3846 "defaultMessage": "!!!Hide",
3816 "end": { 3847 "end": {
3817 "column": 3, 3848 "column": 3,
3818 "line": 165 3849 "line": 169
3819 }, 3850 },
3820 "file": "src/lib/Menu.js", 3851 "file": "src/lib/Menu.js",
3821 "id": "menu.app.hide", 3852 "id": "menu.app.hide",
3822 "start": { 3853 "start": {
3823 "column": 8, 3854 "column": 8,
3824 "line": 162 3855 "line": 166
3825 } 3856 }
3826 }, 3857 },
3827 { 3858 {
3828 "defaultMessage": "!!!Hide Others", 3859 "defaultMessage": "!!!Hide Others",
3829 "end": { 3860 "end": {
3830 "column": 3, 3861 "column": 3,
3831 "line": 169 3862 "line": 173
3832 }, 3863 },
3833 "file": "src/lib/Menu.js", 3864 "file": "src/lib/Menu.js",
3834 "id": "menu.app.hideOthers", 3865 "id": "menu.app.hideOthers",
3835 "start": { 3866 "start": {
3836 "column": 14, 3867 "column": 14,
3837 "line": 166 3868 "line": 170
3838 } 3869 }
3839 }, 3870 },
3840 { 3871 {
3841 "defaultMessage": "!!!Unhide", 3872 "defaultMessage": "!!!Unhide",
3842 "end": { 3873 "end": {
3843 "column": 3, 3874 "column": 3,
3844 "line": 173 3875 "line": 177
3845 }, 3876 },
3846 "file": "src/lib/Menu.js", 3877 "file": "src/lib/Menu.js",
3847 "id": "menu.app.unhide", 3878 "id": "menu.app.unhide",
3848 "start": { 3879 "start": {
3849 "column": 10, 3880 "column": 10,
3850 "line": 170 3881 "line": 174
3851 } 3882 }
3852 }, 3883 },
3853 { 3884 {
3854 "defaultMessage": "!!!Quit", 3885 "defaultMessage": "!!!Quit",
3855 "end": { 3886 "end": {
3856 "column": 3, 3887 "column": 3,
3857 "line": 177 3888 "line": 181
3858 }, 3889 },
3859 "file": "src/lib/Menu.js", 3890 "file": "src/lib/Menu.js",
3860 "id": "menu.app.quit", 3891 "id": "menu.app.quit",
3861 "start": { 3892 "start": {
3862 "column": 8, 3893 "column": 8,
3863 "line": 174 3894 "line": 178
3864 } 3895 }
3865 }, 3896 },
3866 { 3897 {
3867 "defaultMessage": "!!!Add New Service...", 3898 "defaultMessage": "!!!Add New Service...",
3868 "end": { 3899 "end": {
3869 "column": 3, 3900 "column": 3,
3870 "line": 181 3901 "line": 185
3871 }, 3902 },
3872 "file": "src/lib/Menu.js", 3903 "file": "src/lib/Menu.js",
3873 "id": "menu.services.addNewService", 3904 "id": "menu.services.addNewService",
3874 "start": { 3905 "start": {
3875 "column": 17, 3906 "column": 17,
3876 "line": 178 3907 "line": 182
3877 } 3908 }
3878 }, 3909 },
3879 { 3910 {
3880 "defaultMessage": "!!!Activate next service...", 3911 "defaultMessage": "!!!Activate next service...",
3881 "end": { 3912 "end": {
3882 "column": 3, 3913 "column": 3,
3883 "line": 185 3914 "line": 189
3884 }, 3915 },
3885 "file": "src/lib/Menu.js", 3916 "file": "src/lib/Menu.js",
3886 "id": "menu.services.setNextServiceActive", 3917 "id": "menu.services.setNextServiceActive",
3887 "start": { 3918 "start": {
3888 "column": 23, 3919 "column": 23,
3889 "line": 182 3920 "line": 186
3890 } 3921 }
3891 }, 3922 },
3892 { 3923 {
3893 "defaultMessage": "!!!Activate previous service...", 3924 "defaultMessage": "!!!Activate previous service...",
3894 "end": { 3925 "end": {
3895 "column": 3, 3926 "column": 3,
3896 "line": 189 3927 "line": 193
3897 }, 3928 },
3898 "file": "src/lib/Menu.js", 3929 "file": "src/lib/Menu.js",
3899 "id": "menu.services.activatePreviousService", 3930 "id": "menu.services.activatePreviousService",
3900 "start": { 3931 "start": {
3901 "column": 27, 3932 "column": 27,
3902 "line": 186 3933 "line": 190
3903 } 3934 }
3904 }, 3935 },
3905 { 3936 {
3906 "defaultMessage": "!!!Disable notifications & audio", 3937 "defaultMessage": "!!!Disable notifications & audio",
3907 "end": { 3938 "end": {
3908 "column": 3, 3939 "column": 3,
3909 "line": 193 3940 "line": 197
3910 }, 3941 },
3911 "file": "src/lib/Menu.js", 3942 "file": "src/lib/Menu.js",
3912 "id": "sidebar.muteApp", 3943 "id": "sidebar.muteApp",
3913 "start": { 3944 "start": {
3914 "column": 11, 3945 "column": 11,
3915 "line": 190 3946 "line": 194
3916 } 3947 }
3917 }, 3948 },
3918 { 3949 {
3919 "defaultMessage": "!!!Enable notifications & audio", 3950 "defaultMessage": "!!!Enable notifications & audio",
3920 "end": { 3951 "end": {
3921 "column": 3, 3952 "column": 3,
3922 "line": 197 3953 "line": 201
3923 }, 3954 },
3924 "file": "src/lib/Menu.js", 3955 "file": "src/lib/Menu.js",
3925 "id": "sidebar.unmuteApp", 3956 "id": "sidebar.unmuteApp",
3926 "start": { 3957 "start": {
3927 "column": 13, 3958 "column": 13,
3928 "line": 194 3959 "line": 198
3929 } 3960 }
3930 } 3961 }
3931 ], 3962 ],
diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json
index 7543d38bd..573231c45 100644
--- a/src/i18n/locales/en-US.json
+++ b/src/i18n/locales/en-US.json
@@ -1,6 +1,7 @@
1{ 1{
2 "app.errorHandler.action": "Reload", 2 "app.errorHandler.action": "Reload",
3 "app.errorHandler.headline": "Something went wrong", 3 "app.errorHandler.headline": "Something went wrong",
4 "feature.announcements.headline": "What's new in Franz {version}?",
4 "feature.delayApp.action": "Get a Franz Supporter License", 5 "feature.delayApp.action": "Get a Franz Supporter License",
5 "feature.delayApp.headline": "Please purchase a Franz Supporter License to skip waiting", 6 "feature.delayApp.headline": "Please purchase a Franz Supporter License to skip waiting",
6 "feature.delayApp.text": "Franz will continue in {seconds} seconds.", 7 "feature.delayApp.text": "Franz will continue in {seconds} seconds.",
@@ -43,6 +44,7 @@
43 "login.submit.label": "Sign in", 44 "login.submit.label": "Sign in",
44 "login.tokenExpired": "Your session expired, please login again.", 45 "login.tokenExpired": "Your session expired, please login again.",
45 "menu.app.about": "About Franz", 46 "menu.app.about": "About Franz",
47 "menu.app.announcement": "What's new in Franz?",
46 "menu.app.hide": "Hide", 48 "menu.app.hide": "Hide",
47 "menu.app.hideOthers": "Hide Others", 49 "menu.app.hideOthers": "Hide Others",
48 "menu.app.quit": "Quit", 50 "menu.app.quit": "Quit",
diff --git a/src/i18n/messages/src/components/layout/AppLayout.json b/src/i18n/messages/src/components/layout/AppLayout.json
index 07603d062..384d4b441 100644
--- a/src/i18n/messages/src/components/layout/AppLayout.json
+++ b/src/i18n/messages/src/components/layout/AppLayout.json
@@ -4,11 +4,11 @@
4 "defaultMessage": "!!!Your services have been updated.", 4 "defaultMessage": "!!!Your services have been updated.",
5 "file": "src/components/layout/AppLayout.js", 5 "file": "src/components/layout/AppLayout.js",
6 "start": { 6 "start": {
7 "line": 22, 7 "line": 23,
8 "column": 19 8 "column": 19
9 }, 9 },
10 "end": { 10 "end": {
11 "line": 25, 11 "line": 26,
12 "column": 3 12 "column": 3
13 } 13 }
14 }, 14 },
@@ -17,11 +17,11 @@
17 "defaultMessage": "!!!A new update for Franz is available.", 17 "defaultMessage": "!!!A new update for Franz is available.",
18 "file": "src/components/layout/AppLayout.js", 18 "file": "src/components/layout/AppLayout.js",
19 "start": { 19 "start": {
20 "line": 26, 20 "line": 27,
21 "column": 19 21 "column": 19
22 }, 22 },
23 "end": { 23 "end": {
24 "line": 29, 24 "line": 30,
25 "column": 3 25 "column": 3
26 } 26 }
27 }, 27 },
@@ -30,11 +30,11 @@
30 "defaultMessage": "!!!Reload services", 30 "defaultMessage": "!!!Reload services",
31 "file": "src/components/layout/AppLayout.js", 31 "file": "src/components/layout/AppLayout.js",
32 "start": { 32 "start": {
33 "line": 30, 33 "line": 31,
34 "column": 24 34 "column": 24
35 }, 35 },
36 "end": { 36 "end": {
37 "line": 33, 37 "line": 34,
38 "column": 3 38 "column": 3
39 } 39 }
40 }, 40 },
@@ -43,11 +43,11 @@
43 "defaultMessage": "!!!Changelog", 43 "defaultMessage": "!!!Changelog",
44 "file": "src/components/layout/AppLayout.js", 44 "file": "src/components/layout/AppLayout.js",
45 "start": { 45 "start": {
46 "line": 34, 46 "line": 35,
47 "column": 13 47 "column": 13
48 }, 48 },
49 "end": { 49 "end": {
50 "line": 37, 50 "line": 38,
51 "column": 3 51 "column": 3
52 } 52 }
53 }, 53 },
@@ -56,11 +56,11 @@
56 "defaultMessage": "!!!Restart & install update", 56 "defaultMessage": "!!!Restart & install update",
57 "file": "src/components/layout/AppLayout.js", 57 "file": "src/components/layout/AppLayout.js",
58 "start": { 58 "start": {
59 "line": 38, 59 "line": 39,
60 "column": 23 60 "column": 23
61 }, 61 },
62 "end": { 62 "end": {
63 "line": 41, 63 "line": 42,
64 "column": 3 64 "column": 3
65 } 65 }
66 }, 66 },
@@ -69,11 +69,11 @@
69 "defaultMessage": "!!!Could not load services and user information", 69 "defaultMessage": "!!!Could not load services and user information",
70 "file": "src/components/layout/AppLayout.js", 70 "file": "src/components/layout/AppLayout.js",
71 "start": { 71 "start": {
72 "line": 42, 72 "line": 43,
73 "column": 26 73 "column": 26
74 }, 74 },
75 "end": { 75 "end": {
76 "line": 45, 76 "line": 46,
77 "column": 3 77 "column": 3
78 } 78 }
79 } 79 }
diff --git a/src/i18n/messages/src/features/announcements/Component.json b/src/i18n/messages/src/features/announcements/Component.json
new file mode 100644
index 000000000..18e1b84c5
--- /dev/null
+++ b/src/i18n/messages/src/features/announcements/Component.json
@@ -0,0 +1,15 @@
1[
2 {
3 "id": "feature.announcements.headline",
4 "defaultMessage": "!!!What's new in Franz {version}?",
5 "file": "src/features/announcements/Component.js",
6 "start": {
7 "line": 11,
8 "column": 12
9 },
10 "end": {
11 "line": 14,
12 "column": 3
13 }
14 }
15] \ No newline at end of file
diff --git a/src/i18n/messages/src/lib/Menu.json b/src/i18n/messages/src/lib/Menu.json
index 9314f5cce..0db994871 100644
--- a/src/i18n/messages/src/lib/Menu.json
+++ b/src/i18n/messages/src/lib/Menu.json
@@ -481,15 +481,28 @@
481 } 481 }
482 }, 482 },
483 { 483 {
484 "id": "menu.app.announcement",
485 "defaultMessage": "!!!What's new in Franz?",
486 "file": "src/lib/Menu.js",
487 "start": {
488 "line": 158,
489 "column": 16
490 },
491 "end": {
492 "line": 161,
493 "column": 3
494 }
495 },
496 {
484 "id": "menu.app.settings", 497 "id": "menu.app.settings",
485 "defaultMessage": "!!!Settings", 498 "defaultMessage": "!!!Settings",
486 "file": "src/lib/Menu.js", 499 "file": "src/lib/Menu.js",
487 "start": { 500 "start": {
488 "line": 158, 501 "line": 162,
489 "column": 12 502 "column": 12
490 }, 503 },
491 "end": { 504 "end": {
492 "line": 161, 505 "line": 165,
493 "column": 3 506 "column": 3
494 } 507 }
495 }, 508 },
@@ -498,11 +511,11 @@
498 "defaultMessage": "!!!Hide", 511 "defaultMessage": "!!!Hide",
499 "file": "src/lib/Menu.js", 512 "file": "src/lib/Menu.js",
500 "start": { 513 "start": {
501 "line": 162, 514 "line": 166,
502 "column": 8 515 "column": 8
503 }, 516 },
504 "end": { 517 "end": {
505 "line": 165, 518 "line": 169,
506 "column": 3 519 "column": 3
507 } 520 }
508 }, 521 },
@@ -511,11 +524,11 @@
511 "defaultMessage": "!!!Hide Others", 524 "defaultMessage": "!!!Hide Others",
512 "file": "src/lib/Menu.js", 525 "file": "src/lib/Menu.js",
513 "start": { 526 "start": {
514 "line": 166, 527 "line": 170,
515 "column": 14 528 "column": 14
516 }, 529 },
517 "end": { 530 "end": {
518 "line": 169, 531 "line": 173,
519 "column": 3 532 "column": 3
520 } 533 }
521 }, 534 },
@@ -524,11 +537,11 @@
524 "defaultMessage": "!!!Unhide", 537 "defaultMessage": "!!!Unhide",
525 "file": "src/lib/Menu.js", 538 "file": "src/lib/Menu.js",
526 "start": { 539 "start": {
527 "line": 170, 540 "line": 174,
528 "column": 10 541 "column": 10
529 }, 542 },
530 "end": { 543 "end": {
531 "line": 173, 544 "line": 177,
532 "column": 3 545 "column": 3
533 } 546 }
534 }, 547 },
@@ -537,11 +550,11 @@
537 "defaultMessage": "!!!Quit", 550 "defaultMessage": "!!!Quit",
538 "file": "src/lib/Menu.js", 551 "file": "src/lib/Menu.js",
539 "start": { 552 "start": {
540 "line": 174, 553 "line": 178,
541 "column": 8 554 "column": 8
542 }, 555 },
543 "end": { 556 "end": {
544 "line": 177, 557 "line": 181,
545 "column": 3 558 "column": 3
546 } 559 }
547 }, 560 },
@@ -550,11 +563,11 @@
550 "defaultMessage": "!!!Add New Service...", 563 "defaultMessage": "!!!Add New Service...",
551 "file": "src/lib/Menu.js", 564 "file": "src/lib/Menu.js",
552 "start": { 565 "start": {
553 "line": 178, 566 "line": 182,
554 "column": 17 567 "column": 17
555 }, 568 },
556 "end": { 569 "end": {
557 "line": 181, 570 "line": 185,
558 "column": 3 571 "column": 3
559 } 572 }
560 }, 573 },
@@ -563,11 +576,11 @@
563 "defaultMessage": "!!!Activate next service...", 576 "defaultMessage": "!!!Activate next service...",
564 "file": "src/lib/Menu.js", 577 "file": "src/lib/Menu.js",
565 "start": { 578 "start": {
566 "line": 182, 579 "line": 186,
567 "column": 23 580 "column": 23
568 }, 581 },
569 "end": { 582 "end": {
570 "line": 185, 583 "line": 189,
571 "column": 3 584 "column": 3
572 } 585 }
573 }, 586 },
@@ -576,11 +589,11 @@
576 "defaultMessage": "!!!Activate previous service...", 589 "defaultMessage": "!!!Activate previous service...",
577 "file": "src/lib/Menu.js", 590 "file": "src/lib/Menu.js",
578 "start": { 591 "start": {
579 "line": 186, 592 "line": 190,
580 "column": 27 593 "column": 27
581 }, 594 },
582 "end": { 595 "end": {
583 "line": 189, 596 "line": 193,
584 "column": 3 597 "column": 3
585 } 598 }
586 }, 599 },
@@ -589,11 +602,11 @@
589 "defaultMessage": "!!!Disable notifications & audio", 602 "defaultMessage": "!!!Disable notifications & audio",
590 "file": "src/lib/Menu.js", 603 "file": "src/lib/Menu.js",
591 "start": { 604 "start": {
592 "line": 190, 605 "line": 194,
593 "column": 11 606 "column": 11
594 }, 607 },
595 "end": { 608 "end": {
596 "line": 193, 609 "line": 197,
597 "column": 3 610 "column": 3
598 } 611 }
599 }, 612 },
@@ -602,11 +615,11 @@
602 "defaultMessage": "!!!Enable notifications & audio", 615 "defaultMessage": "!!!Enable notifications & audio",
603 "file": "src/lib/Menu.js", 616 "file": "src/lib/Menu.js",
604 "start": { 617 "start": {
605 "line": 194, 618 "line": 198,
606 "column": 13 619 "column": 13
607 }, 620 },
608 "end": { 621 "end": {
609 "line": 197, 622 "line": 201,
610 "column": 3 623 "column": 3
611 } 624 }
612 } 625 }
diff --git a/src/lib/Menu.js b/src/lib/Menu.js
index 7a60c448f..70f3b2877 100644
--- a/src/lib/Menu.js
+++ b/src/lib/Menu.js
@@ -155,6 +155,10 @@ const menuItems = defineMessages({
155 id: 'menu.app.about', 155 id: 'menu.app.about',
156 defaultMessage: '!!!About Franz', 156 defaultMessage: '!!!About Franz',
157 }, 157 },
158 announcement: {
159 id: 'menu.app.announcement',
160 defaultMessage: '!!!What\'s new in Franz?',
161 },
158 settings: { 162 settings: {
159 id: 'menu.app.settings', 163 id: 'menu.app.settings',
160 defaultMessage: '!!!Settings', 164 defaultMessage: '!!!Settings',
@@ -590,6 +594,12 @@ export default class FranzMenu {
590 role: 'about', 594 role: 'about',
591 }, 595 },
592 { 596 {
597 label: intl.formatMessage(menuItems.announcement),
598 click: () => {
599 this.actions.announcements.show();
600 },
601 },
602 {
593 type: 'separator', 603 type: 'separator',
594 }, 604 },
595 { 605 {
diff --git a/src/stores/FeaturesStore.js b/src/stores/FeaturesStore.js
index d2842083c..1c9044b07 100644
--- a/src/stores/FeaturesStore.js
+++ b/src/stores/FeaturesStore.js
@@ -8,6 +8,7 @@ import spellchecker from '../features/spellchecker';
8import serviceProxy from '../features/serviceProxy'; 8import serviceProxy from '../features/serviceProxy';
9import basicAuth from '../features/basicAuth'; 9import basicAuth from '../features/basicAuth';
10import shareFranz from '../features/shareFranz'; 10import shareFranz from '../features/shareFranz';
11import announcements from '../features/announcements';
11 12
12import { DEFAULT_FEATURES_CONFIG } from '../config'; 13import { DEFAULT_FEATURES_CONFIG } from '../config';
13 14
@@ -58,5 +59,6 @@ export default class FeaturesStore extends Store {
58 serviceProxy(this.stores, this.actions); 59 serviceProxy(this.stores, this.actions);
59 basicAuth(this.stores, this.actions); 60 basicAuth(this.stores, this.actions);
60 shareFranz(this.stores, this.actions); 61 shareFranz(this.stores, this.actions);
62 announcements(this.stores, this.actions);
61 } 63 }
62} 64}
diff --git a/src/stores/ServicesStore.js b/src/stores/ServicesStore.js
index 69e616f0c..88b0331bf 100644
--- a/src/stores/ServicesStore.js
+++ b/src/stores/ServicesStore.js
@@ -35,6 +35,7 @@ export default class ServicesStore extends Store {
35 35
36 // Register action handlers 36 // Register action handlers
37 this.actions.service.setActive.listen(this._setActive.bind(this)); 37 this.actions.service.setActive.listen(this._setActive.bind(this));
38 this.actions.service.blurActive.listen(this._blurActive.bind(this));
38 this.actions.service.setActiveNext.listen(this._setActiveNext.bind(this)); 39 this.actions.service.setActiveNext.listen(this._setActiveNext.bind(this));
39 this.actions.service.setActivePrev.listen(this._setActivePrev.bind(this)); 40 this.actions.service.setActivePrev.listen(this._setActivePrev.bind(this));
40 this.actions.service.showAddServiceInterface.listen(this._showAddServiceInterface.bind(this)); 41 this.actions.service.showAddServiceInterface.listen(this._showAddServiceInterface.bind(this));
@@ -298,6 +299,11 @@ export default class ServicesStore extends Store {
298 this._focusActiveService(); 299 this._focusActiveService();
299 } 300 }
300 301
302 @action _blurActive() {
303 if (!this.active) return;
304 this.active.isActive = false;
305 }
306
301 @action _setActiveNext() { 307 @action _setActiveNext() {
302 const nextIndex = this._wrapIndex(this.allDisplayed.findIndex(service => service.isActive), 1, this.allDisplayed.length); 308 const nextIndex = this._wrapIndex(this.allDisplayed.findIndex(service => service.isActive), 1, this.allDisplayed.length);
303 309