From 4a537e890d95e8666985ce77df4c6327582332be Mon Sep 17 00:00:00 2001 From: Dominik Guzei Date: Mon, 10 Dec 2018 17:15:37 +0100 Subject: basic setup for workspaces feature --- src/config.js | 1 + 1 file changed, 1 insertion(+) (limited to 'src/config.js') diff --git a/src/config.js b/src/config.js index 789ddd1a0..d7a485b8a 100644 --- a/src/config.js +++ b/src/config.js @@ -37,6 +37,7 @@ export const DEFAULT_FEATURES_CONFIG = { }, isServiceProxyEnabled: false, isServiceProxyPremiumFeature: true, + isWorkspaceEnabled: true, }; export const DEFAULT_WINDOW_OPTIONS = { -- cgit v1.2.3-70-g09d2 From fd04044be1fe7207e75ed7cb1ddb622cc9cc93bf Mon Sep 17 00:00:00 2001 From: Dominik Guzei Date: Thu, 10 Jan 2019 13:43:23 +0100 Subject: define workspaces as premium feature --- src/config.js | 1 + src/features/workspaces/index.js | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) (limited to 'src/config.js') diff --git a/src/config.js b/src/config.js index d7a485b8a..d327185f0 100644 --- a/src/config.js +++ b/src/config.js @@ -37,6 +37,7 @@ export const DEFAULT_FEATURES_CONFIG = { }, isServiceProxyEnabled: false, isServiceProxyPremiumFeature: true, + isWorkspacePremiumFeature: true, isWorkspaceEnabled: true, }; diff --git a/src/features/workspaces/index.js b/src/features/workspaces/index.js index b7e1090e8..b4cfd3c2d 100644 --- a/src/features/workspaces/index.js +++ b/src/features/workspaces/index.js @@ -13,7 +13,11 @@ export const state = observable(defaultState); export default function initWorkspaces(stores, actions) { const { features, user } = stores; reaction( - () => features.features.isWorkspaceEnabled && user.isLoggedIn, + () => ( + features.features.isWorkspaceEnabled && ( + !features.features.isWorkspacePremiumFeature || user.data.isPremium + ) + ), (isEnabled) => { if (isEnabled) { debug('Initializing `workspaces` feature'); -- cgit v1.2.3-70-g09d2 From 6fb07bcb716af76ec2e96345f37624d12d0d1af0 Mon Sep 17 00:00:00 2001 From: Dominik Guzei Date: Tue, 12 Mar 2019 21:36:10 +0100 Subject: implements basic release announcement feature --- .eslintrc | 2 +- package-lock.json | 5 ++ package.json | 1 + src/actions/index.js | 6 +- src/actions/service.js | 1 + src/components/layout/AppLayout.js | 4 + src/config.js | 1 + src/containers/layout/AppLayoutContainer.js | 2 + src/features/announcements/Component.js | 77 ++++++++++++++++++ src/features/announcements/actions.js | 8 ++ src/features/announcements/api.js | 19 +++++ src/features/announcements/index.js | 37 +++++++++ src/features/announcements/state.js | 17 ++++ src/features/announcements/store.js | 95 ++++++++++++++++++++++ src/i18n/locales/defaultMessages.json | 95 ++++++++++++++-------- src/i18n/locales/en-US.json | 2 + .../messages/src/components/layout/AppLayout.json | 24 +++--- .../src/features/announcements/Component.json | 15 ++++ src/i18n/messages/src/lib/Menu.json | 53 +++++++----- src/lib/Menu.js | 10 +++ src/stores/FeaturesStore.js | 2 + src/stores/ServicesStore.js | 6 ++ 22 files changed, 416 insertions(+), 66 deletions(-) create mode 100644 src/features/announcements/Component.js create mode 100644 src/features/announcements/actions.js create mode 100644 src/features/announcements/api.js create mode 100644 src/features/announcements/index.js create mode 100644 src/features/announcements/state.js create mode 100644 src/features/announcements/store.js create mode 100644 src/i18n/messages/src/features/announcements/Component.json (limited to 'src/config.js') diff --git a/.eslintrc b/.eslintrc index 743946d35..a4ffd505c 100644 --- a/.eslintrc +++ b/.eslintrc @@ -14,7 +14,7 @@ "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], - "react/forbid-prop-types": 1, + "react/forbid-prop-types": 0, "react/destructuring-assignment": 1, "prefer-destructuring": 1, "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 @@ "object-visit": "^1.0.0" } }, + "marked": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/marked/-/marked-0.6.1.tgz", + "integrity": "sha512-+H0L3ibcWhAZE02SKMqmvYsErLo4EAVJxu5h3bHBBDvvjeWXtl92rGUSBYHL2++5Y+RSNgl8dYOAXcYe7lp1fA==" + }, "matchdep": { "version": "2.0.0", "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 @@ "hex-to-rgba": "1.0.2", "jsonwebtoken": "^7.4.1", "lodash": "^4.17.4", + "marked": "0.6.1", "mdi": "^1.9.33", "mime-types": "2.1.21", "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'; import news from './news'; import settings from './settings'; import requests from './requests'; +import announcements from '../features/announcements/actions'; const actions = Object.assign({}, { service, @@ -25,4 +26,7 @@ const actions = Object.assign({}, { requests, }); -export default defineActions(actions, PropTypes.checkPropTypes); +export default Object.assign( + defineActions(actions, PropTypes.checkPropTypes), + { announcements }, +); 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 { setActive: { serviceId: PropTypes.string.isRequired, }, + blurActive: {}, setActiveNext: {}, setActivePrev: {}, 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'; // import globalMessages from '../../i18n/globalMessages'; import { isWindows } from '../../environment'; +import AnnouncementScreen from '../../features/announcements/Component'; function createMarkup(HTMLString) { return { __html: HTMLString }; @@ -64,6 +65,7 @@ export default @observer class AppLayout extends Component { areRequiredRequestsLoading: PropTypes.bool.isRequired, darkMode: PropTypes.bool.isRequired, isDelayAppScreenVisible: PropTypes.bool.isRequired, + isAnnouncementVisible: PropTypes.bool.isRequired, }; static defaultProps = { @@ -93,6 +95,7 @@ export default @observer class AppLayout extends Component { areRequiredRequestsLoading, darkMode, isDelayAppScreenVisible, + isAnnouncementVisible, } = this.props; const { intl } = this.context; @@ -166,6 +169,7 @@ export default @observer class AppLayout extends Component { {isDelayAppScreenVisible && ()} + {isAnnouncementVisible && ()} {services} 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 = { }, isServiceProxyEnabled: false, isServiceProxyPremiumFeature: true, + isAnnouncementsEnabled: true, }; export 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'; import AppLoader from '../../components/ui/AppLoader'; import { state as delayAppState } from '../../features/delayApp'; +import { announcementsState } from '../../features/announcements/state'; export default @inject('stores', 'actions') @observer class AppLayoutContainer extends Component { static defaultProps = { @@ -134,6 +135,7 @@ export default @inject('stores', 'actions') @observer class AppLayoutContainer e areRequiredRequestsLoading={requests.areRequiredRequestsLoading} darkMode={settings.all.app.darkMode} isDelayAppScreenVisible={delayAppState.isDelayAppScreenVisible} + isAnnouncementVisible={announcementsState.isAnnouncementVisible} > {React.Children.count(children) > 0 ? children : null} 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 @@ +import React, { Component } from 'react'; +import marked from 'marked'; +import PropTypes from 'prop-types'; +import { inject, observer } from 'mobx-react'; +import { defineMessages, intlShape } from 'react-intl'; +import injectSheet from 'react-jss'; +import { themeSidebarWidth } from '@meetfranz/theme/lib/themes/legacy'; +import state from './state'; + +const messages = defineMessages({ + headline: { + id: 'feature.announcements.headline', + defaultMessage: '!!!What\'s new in Franz {version}?', + }, +}); + +const styles = theme => ({ + container: { + background: theme.colorBackground, + position: 'absolute', + top: 0, + zIndex: 140, + width: `calc(100% - ${themeSidebarWidth})`, + display: 'flex', + 'flex-direction': 'column', + 'align-items': 'center', + 'justify-content': 'center', + }, + headline: { + color: theme.colorHeadline, + margin: [25, 0, 40], + 'max-width': 500, + 'text-align': 'center', + 'line-height': '1.3em', + }, + body: { + '& h3': { + fontSize: '24px', + margin: '1.5em 0 1em 0', + }, + '& li': { + marginBottom: '1em', + }, + }, +}); + + +@inject('actions') @injectSheet(styles) @observer +class AnnouncementScreen extends Component { + static propTypes = { + classes: PropTypes.object.isRequired, + }; + + static contextTypes = { + intl: intlShape, + }; + + render() { + const { classes } = this.props; + const { intl } = this.context; + return ( +
+

+ {intl.formatMessage(messages.headline, { version: state.currentVersion })} +

+
+
+ ); + } +} + +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 @@ +import PropTypes from 'prop-types'; +import { createActionsFromDefinitions } from '../../actions/lib/actions'; + +export const announcementActions = createActionsFromDefinitions({ + show: {}, +}, PropTypes.checkPropTypes); + +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 @@ +import { remote } from 'electron'; + +const debug = require('debug')('Franz:feature:announcements:api'); + +export default { + async getCurrentVersion() { + debug('getting current version of electron app'); + return Promise.resolve(remote.app.getVersion()); + }, + + async getAnnouncementForVersion(version) { + debug('fetching release announcement from Github'); + const url = `https://api.github.com/repos/meetfranz/franz/releases/tags/v${version}`; + const request = await window.fetch(url, { method: 'GET' }); + if (!request.ok) throw request; + const data = await request.json(); + return data.body; + }, +}; 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 @@ +import { reaction, runInAction } from 'mobx'; +import { AnnouncementsStore } from './store'; +import api from './api'; +import state, { resetState } from './state'; + +const debug = require('debug')('Franz:feature:announcements'); + +let store = null; + +export default function initAnnouncements(stores, actions) { + // const { features } = stores; + + // Toggle workspace feature + reaction( + () => ( + true + // features.features.isAnnouncementsEnabled + ), + (isEnabled) => { + if (isEnabled) { + debug('Initializing `announcements` feature'); + store = new AnnouncementsStore(stores, api, actions, state); + store.initialize(); + runInAction(() => { state.isFeatureActive = true; }); + } else if (store) { + debug('Disabling `announcements` feature'); + runInAction(() => { state.isFeatureActive = false; }); + store.teardown(); + store = null; + resetState(); // Reset state to default + } + }, + { + fireImmediately: true, + }, + ); +} 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 @@ +import { observable } from 'mobx'; + +const defaultState = { + announcement: null, + currentVersion: null, + lastUsedVersion: null, + isAnnouncementVisible: false, + isFeatureActive: false, +}; + +export const announcementsState = observable(defaultState); + +export function resetState() { + Object.assign(announcementsState, defaultState); +} + +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 @@ +import { action, observable, reaction } from 'mobx'; +import semver from 'semver'; + +import Request from '../../stores/lib/Request'; +import Store from '../../stores/lib/Store'; + +const debug = require('debug')('Franz:feature:announcements:store'); + +export class AnnouncementsStore extends Store { + @observable getCurrentVersion = new Request(this.api, 'getCurrentVersion'); + + @observable getAnnouncement = new Request(this.api, 'getAnnouncementForVersion'); + + constructor(stores, api, actions, state) { + super(stores, api, actions); + this.state = state; + } + + async setup() { + await this.fetchLastUsedVersion(); + await this.fetchCurrentVersion(); + await this.fetchReleaseAnnouncement(); + this.showAnnouncementIfNotSeenYet(); + + this.actions.announcements.show.listen(this._showAnnouncement.bind(this)); + } + + // ====== PUBLIC ====== + + async fetchLastUsedVersion() { + debug('getting last used version from local storage'); + const lastUsedVersion = window.localStorage.getItem('lastUsedVersion'); + this._setLastUsedVersion(lastUsedVersion == null ? '0.0.0' : lastUsedVersion); + } + + async fetchCurrentVersion() { + debug('getting current version from api'); + const version = await this.getCurrentVersion.execute(); + this._setCurrentVersion(version); + } + + async fetchReleaseAnnouncement() { + debug('getting release announcement from api'); + try { + const announcement = await this.getAnnouncement.execute(this.state.currentVersion); + this._setAnnouncement(announcement); + } catch (error) { + this._setAnnouncement(null); + } + } + + showAnnouncementIfNotSeenYet() { + const { announcement, currentVersion, lastUsedVersion } = this.state; + if (announcement && semver.gt(currentVersion, lastUsedVersion)) { + debug(`${currentVersion} < ${lastUsedVersion}: announcement is shown`); + this._showAnnouncement(); + } else { + debug(`${currentVersion} >= ${lastUsedVersion}: announcement is hidden`); + this._hideAnnouncement(); + } + } + + // ====== PRIVATE ====== + + @action _setCurrentVersion(version) { + debug(`setting current version to ${version}`); + this.state.currentVersion = version; + } + + @action _setLastUsedVersion(version) { + debug(`setting last used version to ${version}`); + this.state.lastUsedVersion = version; + } + + @action _setAnnouncement(announcement) { + debug(`setting announcement to ${announcement}`); + this.state.announcement = announcement; + } + + @action _showAnnouncement() { + this.state.isAnnouncementVisible = true; + this.actions.service.blurActive(); + const dispose = reaction( + () => this.stores.services.active, + () => { + this._hideAnnouncement(); + dispose(); + }, + ); + } + + @action _hideAnnouncement() { + this.state.isAnnouncementVisible = false; + } +} 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 @@ "defaultMessage": "!!!Your services have been updated.", "end": { "column": 3, - "line": 25 + "line": 26 }, "file": "src/components/layout/AppLayout.js", "id": "infobar.servicesUpdated", "start": { "column": 19, - "line": 22 + "line": 23 } }, { "defaultMessage": "!!!A new update for Franz is available.", "end": { "column": 3, - "line": 29 + "line": 30 }, "file": "src/components/layout/AppLayout.js", "id": "infobar.updateAvailable", "start": { "column": 19, - "line": 26 + "line": 27 } }, { "defaultMessage": "!!!Reload services", "end": { "column": 3, - "line": 33 + "line": 34 }, "file": "src/components/layout/AppLayout.js", "id": "infobar.buttonReloadServices", "start": { "column": 24, - "line": 30 + "line": 31 } }, { "defaultMessage": "!!!Changelog", "end": { "column": 3, - "line": 37 + "line": 38 }, "file": "src/components/layout/AppLayout.js", "id": "infobar.buttonChangelog", "start": { "column": 13, - "line": 34 + "line": 35 } }, { "defaultMessage": "!!!Restart & install update", "end": { "column": 3, - "line": 41 + "line": 42 }, "file": "src/components/layout/AppLayout.js", "id": "infobar.buttonInstallUpdate", "start": { "column": 23, - "line": 38 + "line": 39 } }, { "defaultMessage": "!!!Could not load services and user information", "end": { "column": 3, - "line": 45 + "line": 46 }, "file": "src/components/layout/AppLayout.js", "id": "infobar.requiredRequestsFailed", "start": { "column": 26, - "line": 42 + "line": 43 } } ], @@ -3022,6 +3022,24 @@ ], "path": "src/containers/settings/EditUserScreen.json" }, + { + "descriptors": [ + { + "defaultMessage": "!!!What's new in Franz {version}?", + "end": { + "column": 3, + "line": 14 + }, + "file": "src/features/announcements/Component.js", + "id": "feature.announcements.headline", + "start": { + "column": 12, + "line": 11 + } + } + ], + "path": "src/features/announcements/Component.json" + }, { "descriptors": [ { @@ -3799,133 +3817,146 @@ } }, { - "defaultMessage": "!!!Settings", + "defaultMessage": "!!!What's new in Franz?", "end": { "column": 3, "line": 161 }, "file": "src/lib/Menu.js", + "id": "menu.app.announcement", + "start": { + "column": 16, + "line": 158 + } + }, + { + "defaultMessage": "!!!Settings", + "end": { + "column": 3, + "line": 165 + }, + "file": "src/lib/Menu.js", "id": "menu.app.settings", "start": { "column": 12, - "line": 158 + "line": 162 } }, { "defaultMessage": "!!!Hide", "end": { "column": 3, - "line": 165 + "line": 169 }, "file": "src/lib/Menu.js", "id": "menu.app.hide", "start": { "column": 8, - "line": 162 + "line": 166 } }, { "defaultMessage": "!!!Hide Others", "end": { "column": 3, - "line": 169 + "line": 173 }, "file": "src/lib/Menu.js", "id": "menu.app.hideOthers", "start": { "column": 14, - "line": 166 + "line": 170 } }, { "defaultMessage": "!!!Unhide", "end": { "column": 3, - "line": 173 + "line": 177 }, "file": "src/lib/Menu.js", "id": "menu.app.unhide", "start": { "column": 10, - "line": 170 + "line": 174 } }, { "defaultMessage": "!!!Quit", "end": { "column": 3, - "line": 177 + "line": 181 }, "file": "src/lib/Menu.js", "id": "menu.app.quit", "start": { "column": 8, - "line": 174 + "line": 178 } }, { "defaultMessage": "!!!Add New Service...", "end": { "column": 3, - "line": 181 + "line": 185 }, "file": "src/lib/Menu.js", "id": "menu.services.addNewService", "start": { "column": 17, - "line": 178 + "line": 182 } }, { "defaultMessage": "!!!Activate next service...", "end": { "column": 3, - "line": 185 + "line": 189 }, "file": "src/lib/Menu.js", "id": "menu.services.setNextServiceActive", "start": { "column": 23, - "line": 182 + "line": 186 } }, { "defaultMessage": "!!!Activate previous service...", "end": { "column": 3, - "line": 189 + "line": 193 }, "file": "src/lib/Menu.js", "id": "menu.services.activatePreviousService", "start": { "column": 27, - "line": 186 + "line": 190 } }, { "defaultMessage": "!!!Disable notifications & audio", "end": { "column": 3, - "line": 193 + "line": 197 }, "file": "src/lib/Menu.js", "id": "sidebar.muteApp", "start": { "column": 11, - "line": 190 + "line": 194 } }, { "defaultMessage": "!!!Enable notifications & audio", "end": { "column": 3, - "line": 197 + "line": 201 }, "file": "src/lib/Menu.js", "id": "sidebar.unmuteApp", "start": { "column": 13, - "line": 194 + "line": 198 } } ], 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 @@ { "app.errorHandler.action": "Reload", "app.errorHandler.headline": "Something went wrong", + "feature.announcements.headline": "What's new in Franz {version}?", "feature.delayApp.action": "Get a Franz Supporter License", "feature.delayApp.headline": "Please purchase a Franz Supporter License to skip waiting", "feature.delayApp.text": "Franz will continue in {seconds} seconds.", @@ -43,6 +44,7 @@ "login.submit.label": "Sign in", "login.tokenExpired": "Your session expired, please login again.", "menu.app.about": "About Franz", + "menu.app.announcement": "What's new in Franz?", "menu.app.hide": "Hide", "menu.app.hideOthers": "Hide Others", "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 @@ "defaultMessage": "!!!Your services have been updated.", "file": "src/components/layout/AppLayout.js", "start": { - "line": 22, + "line": 23, "column": 19 }, "end": { - "line": 25, + "line": 26, "column": 3 } }, @@ -17,11 +17,11 @@ "defaultMessage": "!!!A new update for Franz is available.", "file": "src/components/layout/AppLayout.js", "start": { - "line": 26, + "line": 27, "column": 19 }, "end": { - "line": 29, + "line": 30, "column": 3 } }, @@ -30,11 +30,11 @@ "defaultMessage": "!!!Reload services", "file": "src/components/layout/AppLayout.js", "start": { - "line": 30, + "line": 31, "column": 24 }, "end": { - "line": 33, + "line": 34, "column": 3 } }, @@ -43,11 +43,11 @@ "defaultMessage": "!!!Changelog", "file": "src/components/layout/AppLayout.js", "start": { - "line": 34, + "line": 35, "column": 13 }, "end": { - "line": 37, + "line": 38, "column": 3 } }, @@ -56,11 +56,11 @@ "defaultMessage": "!!!Restart & install update", "file": "src/components/layout/AppLayout.js", "start": { - "line": 38, + "line": 39, "column": 23 }, "end": { - "line": 41, + "line": 42, "column": 3 } }, @@ -69,11 +69,11 @@ "defaultMessage": "!!!Could not load services and user information", "file": "src/components/layout/AppLayout.js", "start": { - "line": 42, + "line": 43, "column": 26 }, "end": { - "line": 45, + "line": 46, "column": 3 } } 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 @@ +[ + { + "id": "feature.announcements.headline", + "defaultMessage": "!!!What's new in Franz {version}?", + "file": "src/features/announcements/Component.js", + "start": { + "line": 11, + "column": 12 + }, + "end": { + "line": 14, + "column": 3 + } + } +] \ 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 @@ -480,16 +480,29 @@ "column": 3 } }, + { + "id": "menu.app.announcement", + "defaultMessage": "!!!What's new in Franz?", + "file": "src/lib/Menu.js", + "start": { + "line": 158, + "column": 16 + }, + "end": { + "line": 161, + "column": 3 + } + }, { "id": "menu.app.settings", "defaultMessage": "!!!Settings", "file": "src/lib/Menu.js", "start": { - "line": 158, + "line": 162, "column": 12 }, "end": { - "line": 161, + "line": 165, "column": 3 } }, @@ -498,11 +511,11 @@ "defaultMessage": "!!!Hide", "file": "src/lib/Menu.js", "start": { - "line": 162, + "line": 166, "column": 8 }, "end": { - "line": 165, + "line": 169, "column": 3 } }, @@ -511,11 +524,11 @@ "defaultMessage": "!!!Hide Others", "file": "src/lib/Menu.js", "start": { - "line": 166, + "line": 170, "column": 14 }, "end": { - "line": 169, + "line": 173, "column": 3 } }, @@ -524,11 +537,11 @@ "defaultMessage": "!!!Unhide", "file": "src/lib/Menu.js", "start": { - "line": 170, + "line": 174, "column": 10 }, "end": { - "line": 173, + "line": 177, "column": 3 } }, @@ -537,11 +550,11 @@ "defaultMessage": "!!!Quit", "file": "src/lib/Menu.js", "start": { - "line": 174, + "line": 178, "column": 8 }, "end": { - "line": 177, + "line": 181, "column": 3 } }, @@ -550,11 +563,11 @@ "defaultMessage": "!!!Add New Service...", "file": "src/lib/Menu.js", "start": { - "line": 178, + "line": 182, "column": 17 }, "end": { - "line": 181, + "line": 185, "column": 3 } }, @@ -563,11 +576,11 @@ "defaultMessage": "!!!Activate next service...", "file": "src/lib/Menu.js", "start": { - "line": 182, + "line": 186, "column": 23 }, "end": { - "line": 185, + "line": 189, "column": 3 } }, @@ -576,11 +589,11 @@ "defaultMessage": "!!!Activate previous service...", "file": "src/lib/Menu.js", "start": { - "line": 186, + "line": 190, "column": 27 }, "end": { - "line": 189, + "line": 193, "column": 3 } }, @@ -589,11 +602,11 @@ "defaultMessage": "!!!Disable notifications & audio", "file": "src/lib/Menu.js", "start": { - "line": 190, + "line": 194, "column": 11 }, "end": { - "line": 193, + "line": 197, "column": 3 } }, @@ -602,11 +615,11 @@ "defaultMessage": "!!!Enable notifications & audio", "file": "src/lib/Menu.js", "start": { - "line": 194, + "line": 198, "column": 13 }, "end": { - "line": 197, + "line": 201, "column": 3 } } 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({ id: 'menu.app.about', defaultMessage: '!!!About Franz', }, + announcement: { + id: 'menu.app.announcement', + defaultMessage: '!!!What\'s new in Franz?', + }, settings: { id: 'menu.app.settings', defaultMessage: '!!!Settings', @@ -589,6 +593,12 @@ export default class FranzMenu { label: intl.formatMessage(menuItems.about), role: 'about', }, + { + label: intl.formatMessage(menuItems.announcement), + click: () => { + this.actions.announcements.show(); + }, + }, { type: 'separator', }, 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'; import serviceProxy from '../features/serviceProxy'; import basicAuth from '../features/basicAuth'; import shareFranz from '../features/shareFranz'; +import announcements from '../features/announcements'; import { DEFAULT_FEATURES_CONFIG } from '../config'; @@ -58,5 +59,6 @@ export default class FeaturesStore extends Store { serviceProxy(this.stores, this.actions); basicAuth(this.stores, this.actions); shareFranz(this.stores, this.actions); + announcements(this.stores, this.actions); } } 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 { // Register action handlers this.actions.service.setActive.listen(this._setActive.bind(this)); + this.actions.service.blurActive.listen(this._blurActive.bind(this)); this.actions.service.setActiveNext.listen(this._setActiveNext.bind(this)); this.actions.service.setActivePrev.listen(this._setActivePrev.bind(this)); this.actions.service.showAddServiceInterface.listen(this._showAddServiceInterface.bind(this)); @@ -298,6 +299,11 @@ export default class ServicesStore extends Store { this._focusActiveService(); } + @action _blurActive() { + if (!this.active) return; + this.active.isActive = false; + } + @action _setActiveNext() { const nextIndex = this._wrapIndex(this.allDisplayed.findIndex(service => service.isActive), 1, this.allDisplayed.length); -- cgit v1.2.3-70-g09d2 From d05a8efffadd926165d516d6efd8c8b893648ebe Mon Sep 17 00:00:00 2001 From: Dominik Guzei Date: Tue, 26 Mar 2019 13:47:54 +0100 Subject: hide workspace feature if it is disabled --- src/components/layout/Sidebar.js | 25 ++++++----- .../settings/navigation/SettingsNavigation.js | 21 +++++---- src/config.js | 2 +- src/features/workspaces/store.js | 5 +++ src/i18n/locales/defaultMessages.json | 52 +++++++++++----------- .../messages/src/components/layout/Sidebar.json | 24 +++++----- .../settings/navigation/SettingsNavigation.json | 28 ++++++------ src/lib/Menu.js | 6 ++- src/stores/FeaturesStore.js | 4 +- 9 files changed, 90 insertions(+), 77 deletions(-) (limited to 'src/config.js') diff --git a/src/components/layout/Sidebar.js b/src/components/layout/Sidebar.js index de379875e..f7bacfe0f 100644 --- a/src/components/layout/Sidebar.js +++ b/src/components/layout/Sidebar.js @@ -6,6 +6,7 @@ import { observer } from 'mobx-react'; import Tabbar from '../services/tabs/Tabbar'; import { ctrlKey } from '../../environment'; +import { workspaceStore } from '../../features/workspaces'; const messages = defineMessages({ settings: { @@ -88,17 +89,19 @@ export default @observer class Sidebar extends Component { enableToolTip={() => this.enableToolTip()} disableToolTip={() => this.disableToolTip()} /> - + {workspaceStore.isFeatureActive ? ( + + ) : null}
- - )} - - {user.isSubscriptionOwner && ( - isLoadingOrdersInfo ? ( - - ) : ( -
- {orders.length > 0 && ( - -
-

{intl.formatMessage(messages.headlineSubscription)}

-
- {orders[0].name} - {orders[0].price} +
+
-
-
-

{intl.formatMessage(messages.headlineInvoices)}

- - - {orders.map(order => ( - - - - - ))} - -
- {moment(order.date).format('DD.MM.YYYY')} - - -
-
- - )} +
+
- ) + )} {!user.isPremium && ( diff --git a/src/config.js b/src/config.js index 479572edb..4423e61e9 100644 --- a/src/config.js +++ b/src/config.js @@ -12,6 +12,11 @@ export const CHECK_INTERVAL = ms('1h'); // How often should we perform checks export const LOCAL_API = 'http://localhost:3000'; export const DEV_API = 'https://dev.franzinfra.com'; export const LIVE_API = 'https://api.franzinfra.com'; + +export const LOCAL_API_WEBSITE = 'http://localhost:3333'; +export const DEV_API_WEBSITE = 'https://meetfranz.com'; +export const LIVE_API_WEBSITE = 'https://meetfranz.com'; + export const GA_ID = !isDevMode ? 'UA-74126766-10' : 'UA-74126766-12'; export const DEFAULT_APP_SETTINGS = { diff --git a/src/containers/settings/AccountScreen.js b/src/containers/settings/AccountScreen.js index ce1b9c333..0c837fab9 100644 --- a/src/containers/settings/AccountScreen.js +++ b/src/containers/settings/AccountScreen.js @@ -9,6 +9,7 @@ import AppStore from '../../stores/AppStore'; import AccountDashboard from '../../components/settings/account/AccountDashboard'; import ErrorBoundary from '../../components/util/ErrorBoundary'; +import { WEBSITE } from '../../environment'; const { BrowserWindow } = remote; @@ -24,69 +25,77 @@ export default @inject('stores', 'actions') @observer class AccountScreen extend onCloseWindow() { const { user, payment } = this.props.stores; user.getUserInfoRequest.invalidate({ immediately: true }); - payment.ordersDataRequest.invalidate({ immediately: true }); } reloadData() { const { user, payment } = this.props.stores; user.getUserInfoRequest.reload(); - payment.ordersDataRequest.reload(); payment.plansRequest.reload(); } async handlePaymentDashboard() { const { actions, stores } = this.props; - actions.payment.createDashboardUrl(); - - const dashboard = await stores.payment.createDashboardUrlRequest; - - if (dashboard.url) { - const paymentWindow = new BrowserWindow({ - title: '🔒 Franz Subscription Dashboard', - parent: remote.getCurrentWindow(), - modal: false, - width: 900, - minWidth: 600, - webPreferences: { - nodeIntegration: false, - }, - }); - paymentWindow.loadURL(dashboard.url); - - paymentWindow.on('closed', () => { - this.onCloseWindow(); - }); - } + // actions.payment.createDashboardUrl(); + + // const dashboard = await stores.payment.createDashboardUrlRequest; + + // if (dashboard.url) { + // const paymentWindow = new BrowserWindow({ + // title: '🔒 Franz Subscription Dashboard', + // parent: remote.getCurrentWindow(), + // modal: false, + // width: 900, + // minWidth: 600, + // webPreferences: { + // nodeIntegration: false, + // }, + // }); + // paymentWindow.loadURL(dashboard.url); + + // paymentWindow.on('closed', () => { + // this.onCloseWindow(); + // }); + // } + + const url = `${WEBSITE}/user/billing?token=${stores.user.authToken}&utm_source=app&utm_medium=edit_profile`; + + actions.app.openExternalUrl({ url }); + } + + handleWebsiteLink(route) { + const { actions, stores } = this.props; + + const url = `${WEBSITE}${route}?authToken=${stores.user.authToken}&utm_source=app&utm_medium=account_dashboard`; + console.log(url); + + actions.app.openExternalUrl({ url }); } render() { const { user, payment } = this.props.stores; - const { openExternalUrl } = this.props.actions.app; const { user: userActions } = this.props.actions; const isLoadingUserInfo = user.getUserInfoRequest.isExecuting; - const isLoadingOrdersInfo = payment.ordersDataRequest.isExecuting; const isLoadingPlans = payment.plansRequest.isExecuting; return ( this.reloadData()} - isCreatingPaymentDashboardUrl={payment.createDashboardUrlRequest.isExecuting} - openDashboard={price => this.handlePaymentDashboard(price)} - openExternalUrl={url => openExternalUrl({ url })} onCloseSubscriptionWindow={() => this.onCloseWindow()} deleteAccount={userActions.delete} isLoadingDeleteAccount={user.deleteAccountRequest.isExecuting} isDeleteAccountSuccessful={user.deleteAccountRequest.wasExecuted && !user.deleteAccountRequest.isError} + openEditAccount={() => this.handleWebsiteLink('/user/profile')} + openBilling={() => this.handleWebsiteLink('/user/billing')} + openInvoices={() => this.handleWebsiteLink('/user/invoices')} /> ); diff --git a/src/environment.js b/src/environment.js index 73b1c7ab2..68fa45173 100644 --- a/src/environment.js +++ b/src/environment.js @@ -1,6 +1,13 @@ import isDev from 'electron-is-dev'; -import { LIVE_API, DEV_API, LOCAL_API } from './config'; +import { + LIVE_API, + DEV_API, + LOCAL_API, + LOCAL_API_WEBSITE, + DEV_API_WEBSITE, + LIVE_API_WEBSITE, +} from './config'; export const isDevMode = isDev; export const useLiveAPI = process.env.LIVE_API; @@ -19,12 +26,17 @@ export const ctrlKey = isMac ? '⌘' : 'Ctrl'; export const cmdKey = isMac ? 'Cmd' : 'Ctrl'; let api; +let web; if (!isDevMode || (isDevMode && useLiveAPI)) { api = LIVE_API; + web = LIVE_API_WEBSITE; } else if (isDevMode && useLocalAPI) { api = LOCAL_API; + web = LOCAL_API_WEBSITE; } else { api = DEV_API; + web = DEV_API_WEBSITE; } export const API = api; +export const WEBSITE = web; diff --git a/src/stores/PaymentStore.js b/src/stores/PaymentStore.js index 4cabee194..d4de476c8 100644 --- a/src/stores/PaymentStore.js +++ b/src/stores/PaymentStore.js @@ -10,15 +10,10 @@ export default class PaymentStore extends Store { @observable createHostedPageRequest = new Request(this.api.payment, 'getHostedPage'); - @observable createDashboardUrlRequest = new Request(this.api.payment, 'getDashboardUrl'); - - @observable ordersDataRequest = new CachedRequest(this.api.payment, 'getOrders'); - constructor(...args) { super(...args); this.actions.payment.createHostedPage.listen(this._createHostedPage.bind(this)); - this.actions.payment.createDashboardUrl.listen(this._createDashboardUrl.bind(this)); } @computed get plan() { @@ -28,10 +23,6 @@ export default class PaymentStore extends Store { return this.plansRequest.execute().result || {}; } - @computed get orders() { - return this.ordersDataRequest.execute().result || []; - } - @action _createHostedPage({ planId }) { const request = this.createHostedPageRequest.execute(planId); @@ -39,12 +30,4 @@ export default class PaymentStore extends Store { return request; } - - @action _createDashboardUrl() { - const request = this.createDashboardUrlRequest.execute(); - - gaEvent('Payment', 'createDashboardUrl'); - - return request; - } } diff --git a/src/styles/badge.scss b/src/styles/badge.scss index f9fac039a..be3a1055d 100644 --- a/src/styles/badge.scss +++ b/src/styles/badge.scss @@ -19,6 +19,7 @@ display: inline-block; font-size: 14px; padding: 5px 10px; + letter-spacing: 0; &.badge--primary, &.badge--premium { diff --git a/src/styles/settings.scss b/src/styles/settings.scss index 750b6bedd..b286c6f1b 100644 --- a/src/styles/settings.scss +++ b/src/styles/settings.scss @@ -62,13 +62,18 @@ .account { .account__box { background: $dark-theme-gray-darker; } - .invoices { - td { border-bottom: 1px solid $dark-theme-gray-darker; } - .invoices__action button { color: $theme-brand-primary; } + .badge--premium { + margin-left: 10px; + } + + .manage-user-links { + margin-top: 20px; + display: flex; + justify-content: space-between; } } - .premium-info { + .premium-info { background: $dark-theme-gray-darker; border: 2px solid $theme-brand-primary; } -- cgit v1.2.3-70-g09d2 From 25fc276d5e3f754f915500e91229b8607febc478 Mon Sep 17 00:00:00 2001 From: Stefan Malzner Date: Tue, 9 Apr 2019 01:20:35 +0200 Subject: Add settings websocket --- package-lock.json | 43 +++++++++++---- package.json | 3 +- src/config.js | 4 ++ src/environment.js | 8 +++ src/features/settingsWS/actions.js | 10 ++++ src/features/settingsWS/index.js | 35 ++++++++++++ src/features/settingsWS/state.js | 13 +++++ src/features/settingsWS/store.js | 107 +++++++++++++++++++++++++++++++++++++ src/stores/FeaturesStore.js | 2 + 9 files changed, 214 insertions(+), 11 deletions(-) create mode 100755 src/features/settingsWS/actions.js create mode 100755 src/features/settingsWS/index.js create mode 100755 src/features/settingsWS/state.js create mode 100755 src/features/settingsWS/store.js (limited to 'src/config.js') diff --git a/package-lock.json b/package-lock.json index e9b2cdcf2..34fd0875c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1945,13 +1945,13 @@ "requires": { "@mdi/js": "^3.3.92", "@mdi/react": "^1.1.0", - "@meetfranz/theme": "^1.0.7", + "@meetfranz/theme": "^1.0.13", "react-html-attributes": "^1.4.3", "react-loader": "^2.4.5" }, "dependencies": { "@meetfranz/theme": { - "version": "1.0.9", + "version": "1.0.13", "bundled": true, "requires": { "color": "^3.1.0" @@ -1970,12 +1970,12 @@ "requires": { "@mdi/js": "^3.3.92", "@mdi/react": "^1.1.0", - "@meetfranz/theme": "^1.0.7", + "@meetfranz/theme": "^1.0.13", "react-loader": "^2.4.5" }, "dependencies": { "@meetfranz/theme": { - "version": "1.0.9", + "version": "1.0.13", "bundled": true, "requires": { "color": "^3.1.0" @@ -2994,6 +2994,11 @@ "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=", "dev": true }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" + }, "async-settle": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-1.0.0.tgz", @@ -6543,6 +6548,16 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=", "dev": true + }, + "ws": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ws/-/ws-1.1.5.tgz", + "integrity": "sha512-o3KqipXNUdS7wpQzBHSe180lBGO60SoK0yVo3CYJgb2MkobuWuBX6dhkYP5ORCLd55y+SaflMOV5fqAB53ux4w==", + "dev": true, + "requires": { + "options": ">=0.0.5", + "ultron": "1.0.x" + } } } }, @@ -6580,6 +6595,16 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", "dev": true + }, + "ws": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ws/-/ws-1.1.5.tgz", + "integrity": "sha512-o3KqipXNUdS7wpQzBHSe180lBGO60SoK0yVo3CYJgb2MkobuWuBX6dhkYP5ORCLd55y+SaflMOV5fqAB53ux4w==", + "dev": true, + "requires": { + "options": ">=0.0.5", + "ultron": "1.0.x" + } } } }, @@ -19292,13 +19317,11 @@ } }, "ws": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/ws/-/ws-1.1.5.tgz", - "integrity": "sha512-o3KqipXNUdS7wpQzBHSe180lBGO60SoK0yVo3CYJgb2MkobuWuBX6dhkYP5ORCLd55y+SaflMOV5fqAB53ux4w==", - "dev": true, + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", + "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", "requires": { - "options": ">=0.0.5", - "ultron": "1.0.x" + "async-limiter": "~1.0.0" } }, "wtf-8": { diff --git a/package.json b/package.json index 8f5c2e8c8..481e44af1 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,8 @@ "semver": "^5.4.1", "smoothscroll-polyfill": "^0.3.4", "tar": "^4.0.2", - "uuid": "^3.0.1" + "uuid": "^3.0.1", + "ws": "6.2.1" }, "devDependencies": { "@adlk/misty": "^0.1.1", diff --git a/src/config.js b/src/config.js index 4423e61e9..05ee07ee5 100644 --- a/src/config.js +++ b/src/config.js @@ -13,6 +13,10 @@ export const LOCAL_API = 'http://localhost:3000'; export const DEV_API = 'https://dev.franzinfra.com'; export const LIVE_API = 'https://api.franzinfra.com'; +export const LOCAL_WS_API = 'ws://localhost:3000'; +export const DEV_WS_API = 'wss://dev.franzinfra.com'; +export const LIVE_WS_API = 'wss://api.franzinfra.com'; + export const LOCAL_API_WEBSITE = 'http://localhost:3333'; export const DEV_API_WEBSITE = 'https://meetfranz.com'; export const LIVE_API_WEBSITE = 'https://meetfranz.com'; diff --git a/src/environment.js b/src/environment.js index 68fa45173..e47b373b0 100644 --- a/src/environment.js +++ b/src/environment.js @@ -7,6 +7,9 @@ import { LOCAL_API_WEBSITE, DEV_API_WEBSITE, LIVE_API_WEBSITE, + LIVE_WS_API, + LOCAL_WS_API, + DEV_WS_API, } from './config'; export const isDevMode = isDev; @@ -26,17 +29,22 @@ export const ctrlKey = isMac ? '⌘' : 'Ctrl'; export const cmdKey = isMac ? 'Cmd' : 'Ctrl'; let api; +let wsApi; let web; if (!isDevMode || (isDevMode && useLiveAPI)) { api = LIVE_API; + wsApi = LIVE_WS_API; web = LIVE_API_WEBSITE; } else if (isDevMode && useLocalAPI) { api = LOCAL_API; + wsApi = LOCAL_WS_API; web = LOCAL_API_WEBSITE; } else { api = DEV_API; + wsApi = DEV_WS_API; web = DEV_API_WEBSITE; } export const API = api; +export const WS_API = wsApi; export const WEBSITE = web; diff --git a/src/features/settingsWS/actions.js b/src/features/settingsWS/actions.js new file mode 100755 index 000000000..631670c8a --- /dev/null +++ b/src/features/settingsWS/actions.js @@ -0,0 +1,10 @@ +import PropTypes from 'prop-types'; +import { createActionsFromDefinitions } from '../../actions/lib/actions'; + +export const settingsWSActions = createActionsFromDefinitions({ + greet: { + name: PropTypes.string.isRequired, + }, +}, PropTypes.checkPropTypes); + +export default settingsWSActions; diff --git a/src/features/settingsWS/index.js b/src/features/settingsWS/index.js new file mode 100755 index 000000000..1e268f184 --- /dev/null +++ b/src/features/settingsWS/index.js @@ -0,0 +1,35 @@ +import { reaction, runInAction } from 'mobx'; +import { SettingsWSStore } from './store'; +import state, { resetState } from './state'; + +const debug = require('debug')('Franz:feature:settingsWS'); + +let store = null; + +export default function initAnnouncements(stores, actions) { + const { features } = stores; + + // Toggle workspace feature + reaction( + () => ( + features.features.isSettingsWSEnabled + ), + (isEnabled) => { + if (isEnabled) { + debug('Initializing `settingsWS` feature'); + store = new SettingsWSStore(stores, null, actions, state); + store.initialize(); + runInAction(() => { state.isFeatureActive = true; }); + } else if (store) { + debug('Disabling `settingsWS` feature'); + runInAction(() => { state.isFeatureActive = false; }); + store.teardown(); + store = null; + resetState(); // Reset state to default + } + }, + { + fireImmediately: true, + }, + ); +} diff --git a/src/features/settingsWS/state.js b/src/features/settingsWS/state.js new file mode 100755 index 000000000..7b16b2b6e --- /dev/null +++ b/src/features/settingsWS/state.js @@ -0,0 +1,13 @@ +import { observable } from 'mobx'; + +const defaultState = { + isFeatureActive: false, +}; + +export const settingsWSState = observable(defaultState); + +export function resetState() { + Object.assign(settingsWSState, defaultState); +} + +export default settingsWSState; diff --git a/src/features/settingsWS/store.js b/src/features/settingsWS/store.js new file mode 100755 index 000000000..21fd3440e --- /dev/null +++ b/src/features/settingsWS/store.js @@ -0,0 +1,107 @@ +import { observable, reaction } from 'mobx'; +import WebSocket from 'ws'; +import ms from 'ms'; + +import Store from '../../stores/lib/Store'; +import { WS_API } from '../../environment'; + +const debug = require('debug')('Franz:feature:settingsWS:store'); + +export class SettingsWSStore extends Store { + ws = null; + + @observable connected = false; + + pingTimeout = null; + + reconnectTimeout = null; + + constructor(stores, api, actions, state) { + super(stores, api, actions); + this.state = state; + } + + setup() { + this.connect(); + + reaction(() => !this.connected, this.reconnect.bind(this)); + } + + connect() { + try { + const wsURL = `${WS_API}/ws/${this.stores.user.data.id}`; + debug('Setting up WebSocket to', wsURL); + + this.ws = new WebSocket(wsURL); + + this.ws.on('open', () => { + debug('Opened WebSocket'); + this.send({ + action: 'authorize', + token: this.stores.user.authToken, + }); + + this.connected = true; + + this.heartbeat(); + }); + + this.ws.on('message', (data) => { + const resp = JSON.parse(data); + debug('Received message', resp); + + if (resp.id) { + this.stores.user.getUserInfoRequest.patch((result) => { + if (!result) return; + + debug('Patching user object with new values'); + Object.assign(result, resp); + }); + } + }); + + this.ws.on('ping', this.heartbeat.bind(this)); + } catch (err) { + console.err(err); + } + } + + heartbeat() { + debug('Heartbeat'); + clearTimeout(this.pingTimeout); + + // Use `WebSocket#terminate()` and not `WebSocket#close()`. Delay should be + // equal to the interval at which your server sends out pings plus a + // conservative assumption of the latency. + this.pingTimeout = setTimeout(() => { + debug('Terminating connection reconnecting in 35'); + this.ws.terminate(); + + this.connected = false; + }, ms('35s')); + } + + send(data) { + if (this.ws) { + this.ws.send(JSON.stringify(data)); + debug('Sending data', data); + } else { + debug('WebSocket is not initialized'); + } + } + + reconnect() { + if (!this.connected) { + debug('Trying to reconnect in 30s'); + this.reconnectTimeout = setInterval(() => { + debug('Trying to reconnect'); + this.connect(); + }, ms('30s')); + } else { + debug('Clearing reconnect interval'); + clearInterval(this.reconnectTimeout); + } + } +} + +export default SettingsWSStore; diff --git a/src/stores/FeaturesStore.js b/src/stores/FeaturesStore.js index d2842083c..e60d79075 100644 --- a/src/stores/FeaturesStore.js +++ b/src/stores/FeaturesStore.js @@ -8,6 +8,7 @@ import spellchecker from '../features/spellchecker'; import serviceProxy from '../features/serviceProxy'; import basicAuth from '../features/basicAuth'; import shareFranz from '../features/shareFranz'; +import settingsWS from '../features/settingsWS'; import { DEFAULT_FEATURES_CONFIG } from '../config'; @@ -58,5 +59,6 @@ export default class FeaturesStore extends Store { serviceProxy(this.stores, this.actions); basicAuth(this.stores, this.actions); shareFranz(this.stores, this.actions); + settingsWS(this.stores, this.actions); } } -- cgit v1.2.3-70-g09d2 From 365e22d671613725699ecad0183efff07419e005 Mon Sep 17 00:00:00 2001 From: Stefan Malzner Date: Mon, 15 Apr 2019 17:41:44 +0200 Subject: Add stats api --- src/config.js | 1 + src/lib/analytics.js | 20 +++++++++++++++++++- src/stores/ServicesStore.js | 4 +++- 3 files changed, 23 insertions(+), 2 deletions(-) (limited to 'src/config.js') diff --git a/src/config.js b/src/config.js index e7745b61d..3b5ce7dda 100644 --- a/src/config.js +++ b/src/config.js @@ -12,6 +12,7 @@ export const CHECK_INTERVAL = ms('1h'); // How often should we perform checks export const LOCAL_API = 'http://localhost:3000'; export const DEV_API = 'https://dev.franzinfra.com'; export const LIVE_API = 'https://api.franzinfra.com'; +export const STATS_API = 'https://stats.franzinfra.com'; export const GA_ID = !isDevMode ? 'UA-74126766-10' : 'UA-74126766-12'; export const DEFAULT_APP_SETTINGS = { diff --git a/src/lib/analytics.js b/src/lib/analytics.js index e7daa9d06..663aafe22 100644 --- a/src/lib/analytics.js +++ b/src/lib/analytics.js @@ -1,5 +1,8 @@ import { remote } from 'electron'; -import { GA_ID } from '../config'; +import querystring from 'querystring'; + +import { GA_ID, STATS_API } from '../config'; +import { isDevMode } from '../environment'; const debug = require('debug')('Franz:Analytics'); @@ -35,3 +38,18 @@ export function gaEvent(category, action, label) { ga('send', 'event', category, action, label); debug('GA track event', category, action, label); } + +export function statsEvent(key, value) { + const params = { + key, + value, + platform: process.platform, + version: remote.app.getVersion(), + }; + + debug('Send Franz stats event', params); + + if (!isDevMode) { + window.fetch(`${STATS_API}/event/?${querystring.stringify(params)}`); + } +} diff --git a/src/stores/ServicesStore.js b/src/stores/ServicesStore.js index d04fdd0c5..13f929c2f 100644 --- a/src/stores/ServicesStore.js +++ b/src/stores/ServicesStore.js @@ -11,7 +11,7 @@ import Store from './lib/Store'; import Request from './lib/Request'; import CachedRequest from './lib/CachedRequest'; import { matchRoute } from '../helpers/routing-helpers'; -import { gaEvent } from '../lib/analytics'; +import { gaEvent, statsEvent } from '../lib/analytics'; import { workspaceStore } from '../features/workspaces'; const debug = require('debug')('Franz:ServiceStore'); @@ -299,6 +299,8 @@ export default class ServicesStore extends Store { }); service.isActive = true; + statsEvent('activate-service', service.recipe.id); + this._focusActiveService(); } -- cgit v1.2.3-70-g09d2