simple version-based notifications system

This commit is contained in:
Guillermo Rauch 2016-10-04 12:41:54 -07:00
parent 71ae9b7e00
commit d01d3868eb
9 changed files with 132 additions and 6 deletions

View file

@ -1,5 +1,9 @@
import {INIT} from '../constants/index';
import rpc from '../rpc'; import rpc from '../rpc';
export function init() { export function init() {
rpc.emit('init'); rpc.emit('init');
return {
type: INIT
}
} }

View file

@ -1,4 +1,11 @@
import {NOTIFICATION_DISMISS} from '../constants/notifications'; import ms from 'ms';
import {version} from '../../package';
import {satisfies} from 'semver';
import {remote} from 'electron';
import {
NOTIFICATION_MESSAGE,
NOTIFICATION_DISMISS
} from '../constants/notifications';
export function dismissNotification(id) { export function dismissNotification(id) {
return { return {
@ -6,3 +13,59 @@ export function dismissNotification(id) {
id id
}; };
} }
export function addNotificationMessage(text, url = null, dismissable = true) {
return {
type: NOTIFICATION_MESSAGE,
text,
url,
dismissable
};
}
export function fetchNotifications() {
return dispatch => {
const retry = err => {
setTimeout(() => dispatch(fetchNotifications()), ms(err ? '10s' : '5m'));
if (err) {
console.error('Notification messages fetch error', err.stack);
}
};
console.log('Checking for notification messages');
fetch('https://hyper-news.now.sh')
.then(res => {
res.json()
.then(data => {
const {messages} = data || {};
if (!messages) {
throw new Error('Bad response');
}
const message = messages.filter(msg => {
return matchVersion(msg.versions);
})[0];
if (message) {
dispatch(addNotificationMessage(
message.text,
message.url,
message.dismissable
));
} else {
console.log('No matching notification messages');
}
retry();
})
.catch(retry);
})
.catch(retry);
};
}
function matchVersion(versions) {
return versions.some(v => {
if (v === '*') {
return true;
}
return satisfies(version, v);
});
}

View file

@ -83,6 +83,7 @@ export default class Notification extends Component {
<a <a
className={css('dismissLink')} className={css('dismissLink')}
onClick={this.handleDismiss} onClick={this.handleDismiss}
style={{ color: this.props.userDismissColor }}
>[x]</a> : >[x]</a> :
null null
} }

View file

@ -36,6 +36,34 @@ export default class Notifications extends Component {
/> />
} }
{
this.props.messageShowing &&
<Notification
key="message"
backgroundColor="#FE354E"
text={this.props.messageText}
onDismiss={this.props.onDismissMessage}
userDismissable={this.props.messageDismissable}
userDismissColor="#AA2D3C"
>{
this.props.messageURL ? [
this.props.messageText,
' (',
<a
key="link"
style={{color: '#fff'}}
onClick={ev => {
window.require('electron').shell.openExternal(ev.target.href);
ev.preventDefault();
}}
href={this.props.messageURL}
>more</a>,
')'
] : null
}
</Notification>
}
{ {
this.props.updateShowing && this.props.updateShowing &&
<Notification <Notification

View file

@ -1 +1,2 @@
export const NOTIFICATION_MESSAGE = 'NOTIFICATION_MESSAGE';
export const NOTIFICATION_DISMISS = 'NOTIFICATION_DISMISS'; export const NOTIFICATION_DISMISS = 'NOTIFICATION_DISMISS';

View file

@ -36,6 +36,13 @@ const NotificationsContainer = connect(
updateVersion: ui.updateVersion, updateVersion: ui.updateVersion,
updateNote: ui.updateNotes.split('\n')[0] updateNote: ui.updateNotes.split('\n')[0]
}); });
} else if (notifications.message) {
Object.assign(state_, {
messageShowing: true,
messageText: ui.messageText,
messageURL: ui.messageURL,
messageDismissable: ui.messageDismissable
});
} }
return state_; return state_;
@ -51,6 +58,9 @@ const NotificationsContainer = connect(
onDismissUpdate: () => { onDismissUpdate: () => {
dispatch(dismissNotification('updates')); dispatch(dismissNotification('updates'));
}, },
onDismissMessage: () => {
dispatch(dismissNotification('message'));
},
onUpdateInstall: () => { onUpdateInstall: () => {
dispatch(installUpdate()); dispatch(installUpdate());
} }

View file

@ -12,6 +12,7 @@ import * as uiActions from './actions/ui';
import * as updaterActions from './actions/updater'; import * as updaterActions from './actions/updater';
import * as sessionActions from './actions/sessions'; import * as sessionActions from './actions/sessions';
import * as termGroupActions from './actions/term-groups'; import * as termGroupActions from './actions/term-groups';
import {fetchNotifications} from './actions/notifications';
import HyperTermContainer from './containers/hyperterm'; import HyperTermContainer from './containers/hyperterm';
import {loadConfig, reloadConfig} from './actions/config'; import {loadConfig, reloadConfig} from './actions/config';
import configureStore from './store/configure-store'; import configureStore from './store/configure-store';
@ -36,6 +37,7 @@ config.subscribe(() => {
// and subscribe to all user intents for example from menus // and subscribe to all user intents for example from menus
rpc.on('ready', () => { rpc.on('ready', () => {
store_.dispatch(init()); store_.dispatch(init());
store_.dispatch(fetchNotifications());
store_.dispatch(uiActions.setFontSmoothing()); store_.dispatch(uiActions.setFontSmoothing());
}); });

View file

@ -8,7 +8,7 @@ import {
UI_WINDOW_MAXIMIZE, UI_WINDOW_MAXIMIZE,
UI_WINDOW_UNMAXIMIZE UI_WINDOW_UNMAXIMIZE
} from '../constants/ui'; } from '../constants/ui';
import {NOTIFICATION_DISMISS} from '../constants/notifications'; import {NOTIFICATION_MESSAGE, NOTIFICATION_DISMISS} from '../constants/notifications';
import { import {
SESSION_ADD, SESSION_ADD,
SESSION_RESIZE, SESSION_RESIZE,
@ -62,13 +62,17 @@ const initial = Immutable({
notifications: { notifications: {
font: false, font: false,
resize: false, resize: false,
updates: false updates: false,
message: false
}, },
foregroundColor: '#fff', foregroundColor: '#fff',
backgroundColor: '#000', backgroundColor: '#000',
maximized: false, maximized: false,
updateVersion: null, updateVersion: null,
updateNotes: null, updateNotes: null,
messageText: null,
messageURL: null,
messageDismissable: null,
bell: 'SOUND', bell: 'SOUND',
bellSoundURL: 'lib-resource:hterm/audio/bell', bellSoundURL: 'lib-resource:hterm/audio/bell',
copyOnSelect: false, copyOnSelect: false,
@ -273,6 +277,14 @@ const reducer = (state = initial, action) => {
}, {deep: true}); }, {deep: true});
break; break;
case NOTIFICATION_MESSAGE:
state_ = state.merge({
messageText: action.text,
messageURL: action.url || null,
messageDismissable: action.dismissable === true
});
break;
case UPDATE_AVAILABLE: case UPDATE_AVAILABLE:
state_ = state.merge({ state_ = state.merge({
updateVersion: action.version, updateVersion: action.version,
@ -296,6 +308,10 @@ const reducer = (state = initial, action) => {
state_ = state_.merge({notifications: {resize: true}}, {deep: true}); state_ = state_.merge({notifications: {resize: true}}, {deep: true});
} }
if (state.messageText !== state_.messageText || state.messageURL !== state_.messageURL) {
state_ = state_.merge({notifications: {message: true}}, {deep: true});
}
if (state.updateVersion !== state_.updateVersion) { if (state.updateVersion !== state_.updateVersion) {
state_ = state_.merge({notifications: {updates: true}}, {deep: true}); state_ = state_.merge({notifications: {updates: true}}, {deep: true});
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "hyperterm", "name": "hyperterm",
"version": "1.0.0", "version": "0.8.0",
"description": "Web app that runs in the renderer process", "description": "Web app that runs in the renderer process",
"repository": "zeit/hyperterm", "repository": "zeit/hyperterm",
"license": "MIT", "license": "MIT",
@ -28,8 +28,9 @@
"redux": "3.6.0", "redux": "3.6.0",
"redux-thunk": "2.1.0", "redux-thunk": "2.1.0",
"reselect": "2.5.4", "reselect": "2.5.4",
"uuid": "2.0.2", "seamless-immutable": "6.1.3",
"seamless-immutable": "6.1.3" "semver": "5.3.0",
"uuid": "2.0.2"
}, },
"devDependencies": { "devDependencies": {
"ava": "^0.16.0", "ava": "^0.16.0",