diff --git a/src/PluginContainer.js b/src/PluginContainer.js index a51a41a54..0fa7bdf2a 100644 --- a/src/PluginContainer.js +++ b/src/PluginContainer.js @@ -22,6 +22,8 @@ import { import React from 'react'; import {connect} from 'react-redux'; import {setPluginState} from './reducers/pluginStates.js'; +import {setActiveNotifications} from './reducers/notifications.js'; +import type {NotificationSet} from './plugin.js'; import {devicePlugins} from './device-plugins/index.js'; import plugins from './plugins/index.js'; import {activateMenuItems} from './MenuBar.js'; @@ -52,6 +54,10 @@ type Props = { pluginKey: string, state: Object, }) => void, + setActiveNotifications: ({ + pluginId: string, + notifications: NotificationSet, + }) => void, }; type State = { @@ -124,7 +130,7 @@ class PluginContainer extends Component { }; render() { - const {pluginStates, setPluginState} = this.props; + const {pluginStates, setPluginState, setActiveNotifications} = this.props; const {activePlugin, pluginKey, target} = this.state; if (!activePlugin || !target) { @@ -136,6 +142,11 @@ class PluginContainer extends Component { logger: this.props.logger, persistedState: pluginStates[pluginKey] || {}, setPersistedState: state => setPluginState({pluginKey, state}), + setActiveNotifications: notifications => + setActiveNotifications({ + pluginId: pluginKey, + notifications: notifications, + }), target, ref: this.refChanged, }; @@ -171,5 +182,6 @@ export default connect( }), { setPluginState, + setActiveNotifications, }, )(PluginContainer); diff --git a/src/plugin.js b/src/plugin.js index 8f0e0fc89..e53fbfdc3 100644 --- a/src/plugin.js +++ b/src/plugin.js @@ -24,10 +24,22 @@ export type PluginClient = {| type PluginTarget = BaseDevice | Client; +export type Notification = {| + title: string, + message: string, + severity: 'warning' | 'error', + timestamp?: number, + category?: string, + action?: string, +|}; + +export type NotificationSet = {[id: string]: Notification}; + export type Props = { logger: Logger, persistedState: T, setPersistedState: (state: $Shape) => void, + setActiveNotifications: NotificationSet => void, target: PluginTarget, }; @@ -63,6 +75,9 @@ export class FlipperBasePlugin< // methods to be overriden by plugins init(): void {} teardown(): void {} + computeNotifications(props: Props<*>, state: State): NotificationSet { + return {}; + } // methods to be overridden by subclasses _init(): void {} _teardown(): void {} @@ -82,6 +97,10 @@ export class FlipperBasePlugin< throw new TypeError(`Reducer ${actionData.type} isn't a function`); } } + + componentDidUpdate(props: Props<*>, state: State): void { + props.setActiveNotifications(this.computeNotifications(props, state)); + } } export class FlipperDevicePlugin extends FlipperBasePlugin< diff --git a/src/reducers/index.js b/src/reducers/index.js index 44a1dbb40..197811622 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -9,6 +9,7 @@ import {combineReducers} from 'redux'; import application from './application.js'; import connections from './connections.js'; import pluginStates from './pluginStates.js'; +import notifications from './notifications.js'; import type { State as ApplicationState, @@ -22,6 +23,10 @@ import type { State as PluginsState, Action as PluginsAction, } from './pluginStates.js'; +import type { + State as NotificationsState, + Action as NotificationsAction, +} from './notifications.js'; import type {Store as ReduxStore} from 'redux'; export type Store = ReduxStore< @@ -29,12 +34,18 @@ export type Store = ReduxStore< application: ApplicationState, connections: DevicesState, pluginStates: PluginsState, + notifications: NotificationsState, }, - ApplicationAction | DevicesAction | PluginsAction | {|type: 'INIT'|}, + | ApplicationAction + | DevicesAction + | PluginsAction + | NotificationsAction + | {|type: 'INIT'|}, >; export default combineReducers({ application, connections, pluginStates, + notifications, }); diff --git a/src/reducers/notifications.js b/src/reducers/notifications.js new file mode 100644 index 000000000..bde2d10ab --- /dev/null +++ b/src/reducers/notifications.js @@ -0,0 +1,104 @@ +/** + * Copyright 2018-present Facebook. + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * @format + */ + +import type {Notification, NotificationSet} from '../plugin'; + +type PluginNotification = {| + id: string, + notification: Notification, + pluginId: string, +|}; + +export type State = { + activeNotifications: Array, + invalidatedNotifications: Array, +}; + +type ActiveNotificationsAction = { + type: 'SET_ACTIVE_NOTIFICATIONS', + payload: NotificationSet, + pluginId: string, +}; + +type ClearAllAction = { + type: 'CLEAR_ALL_NOTIFICATIONS', +}; + +export type Action = ActiveNotificationsAction | ClearAllAction; + +const INITIAL_STATE: State = { + activeNotifications: [], + invalidatedNotifications: [], +}; + +export default function reducer( + state: State = INITIAL_STATE, + action: Action, +): State { + switch (action.type) { + case 'SET_ACTIVE_NOTIFICATIONS': { + return activeNotificationsReducer(state, action); + } + case 'CLEAR_ALL_NOTIFICATIONS': + // Q: Should this actually delete them, or just invalidate them? + return INITIAL_STATE; + default: + return state; + } +} + +function activeNotificationsReducer( + state: State, + action: ActiveNotificationsAction, +) { + const {payload, pluginId} = action; + const newActiveNotifications = []; + const newInactivatedNotifications = state.invalidatedNotifications; + for (const activeNotification of state.activeNotifications) { + if (activeNotification.pluginId !== pluginId) { + newActiveNotifications.push(activeNotification); + continue; + } + + if (!payload[activeNotification.id]) { + newInactivatedNotifications.push(activeNotification); + } + } + + for (const id in payload) { + const newNotification = { + id, + notification: payload[id], + pluginId, + }; + newActiveNotifications.push(newNotification); + } + return { + activeNotifications: newActiveNotifications, + invalidatedNotifications: newInactivatedNotifications, + }; +} + +export function setActiveNotifications(payload: { + notifications: { + [id: string]: Notification, + }, + pluginId: string, +}): Action { + const {notifications, pluginId} = payload; + return { + type: 'SET_ACTIVE_NOTIFICATIONS', + payload: notifications, + pluginId, + }; +} + +export function clearAllNotifications(): Action { + return { + type: 'CLEAR_ALL_NOTIFICATIONS', + }; +}