diff --git a/src/App.tsx b/src/App.tsx index cc7637d5a..c086e7e28 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -36,6 +36,7 @@ import {State as Store} from './reducers/index'; import {StaticView} from './reducers/connections'; import PluginManager from './chrome/PluginManager'; import BaseDevice from './devices/BaseDevice'; +import StatusBar from './chrome/StatusBar'; const version = remote.app.getVersion(); type OwnProps = { @@ -122,6 +123,7 @@ export class App extends React.Component { )} + ); diff --git a/src/chrome/StatusBar.tsx b/src/chrome/StatusBar.tsx new file mode 100644 index 000000000..34c70a6d2 --- /dev/null +++ b/src/chrome/StatusBar.tsx @@ -0,0 +1,50 @@ +/** + * 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 {colors} from '../ui/components/colors'; +import {styled} from '../ui'; +import {connect} from 'react-redux'; +import {State} from '../reducers'; +import React, {ReactElement} from 'react'; +import Text from '../ui/components/Text'; + +const StatusBarContainer = styled(Text)({ + backgroundColor: colors.macOSTitleBarBackgroundBlur, + borderTop: '1px solid #b3b3b3', + lineHeight: '26px', + padding: '0 10px', + textOverflow: 'ellipsis', + overflow: 'hidden', +}); + +type Props = { + statusMessage: string | null; +}; + +export function statusBarView(props: Props): ReactElement | null { + const {statusMessage} = props; + if (statusMessage) { + return ( + + {statusMessage} + + ); + } else { + return null; + } +} + +export default connect( + ({application: {statusMessages}}) => { + if (statusMessages.length > 0) { + return {statusMessage: statusMessages[statusMessages.length - 1]}; + } + return { + statusMessage: null, + }; + }, +)(statusBarView); diff --git a/src/chrome/__tests__/StatusBar.node.tsx b/src/chrome/__tests__/StatusBar.node.tsx new file mode 100644 index 000000000..f97231576 --- /dev/null +++ b/src/chrome/__tests__/StatusBar.node.tsx @@ -0,0 +1,18 @@ +/** + * 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 {statusBarView} from '../StatusBar'; + +test('statusBarView returns null for empty status messages', () => { + const view = statusBarView({statusMessage: null}); + expect(view).toBeNull(); +}); + +test('statusBarView returns non null view when the list of messages is non empty', () => { + const view = statusBarView({statusMessage: 'Last Message'}); + expect(view).toBeDefined(); +}); diff --git a/src/reducers/__tests__/applications.electron.tsx b/src/reducers/__tests__/applications.electron.tsx new file mode 100644 index 000000000..151bccc71 --- /dev/null +++ b/src/reducers/__tests__/applications.electron.tsx @@ -0,0 +1,54 @@ +/** + * 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 reducer from '../application'; +import { + initialState, + addStatusMessage, + removeStatusMessage, +} from '../application'; + +test('ADD_STATUS_MSG, to check if the status messages get pushed to the state', () => { + const state = reducer( + initialState(), + addStatusMessage({msg: 'Status Msg', sender: 'Test'}), + ); + expect(state.statusMessages).toEqual(['Test: Status Msg']); + const updatedstate = reducer( + state, + addStatusMessage({msg: 'Status Msg 2', sender: 'Test'}), + ); + expect(updatedstate.statusMessages).toEqual([ + 'Test: Status Msg', + 'Test: Status Msg 2', + ]); +}); + +test('REMOVE_STATUS_MSG, to check if the status messages gets removed from the state', () => { + const initState = initialState(); + const state = reducer( + initState, + removeStatusMessage({msg: 'Status Msg', sender: 'Test'}), + ); + expect(state).toEqual(initState); + const stateWithMessages = reducer( + reducer( + initialState(), + addStatusMessage({msg: 'Status Msg', sender: 'Test'}), + ), + addStatusMessage({msg: 'Status Msg 2', sender: 'Test'}), + ); + const updatedState = reducer( + stateWithMessages, + removeStatusMessage({msg: 'Status Msg', sender: 'Test'}), + ); + expect(updatedState.statusMessages).toEqual(['Test: Status Msg 2']); + const updatedStateWithNoMessages = reducer( + updatedState, + removeStatusMessage({msg: 'Status Msg 2', sender: 'Test'}), + ); + expect(updatedStateWithNoMessages.statusMessages).toEqual([]); +}); diff --git a/src/reducers/application.tsx b/src/reducers/application.tsx index df7b21483..28975188e 100644 --- a/src/reducers/application.tsx +++ b/src/reducers/application.tsx @@ -42,6 +42,11 @@ export type ServerPorts = { secure: number; }; +type StatusMessageType = { + msg: string; + sender: string; +}; + type SubShareType = | { type: 'file'; @@ -68,6 +73,7 @@ export type State = { downloadingImportData: boolean; launcherMsg: LauncherMsg; flipperRating: number | null; + statusMessages: Array; }; type BooleanActionType = @@ -124,9 +130,17 @@ export type Action = | { type: 'SET_EXPORT_URL'; payload: string; + } + | { + type: 'ADD_STATUS_MSG'; + payload: {msg: string; sender: string}; + } + | { + type: 'REMOVE_STATUS_MSG'; + payload: {msg: string; sender: string}; }; -const initialState: () => State = () => ({ +export const initialState: () => State = () => ({ leftSidebarVisible: true, rightSidebarVisible: true, rightSidebarAvailable: false, @@ -144,8 +158,20 @@ const initialState: () => State = () => ({ message: '', }, flipperRating: null, + statusMessages: [], }); +function statusMessage(sender: string, msg: string): string { + const messageTrimmed = msg.trim(); + const senderTrimmed = sender.trim(); + let statusMessage = senderTrimmed.length > 0 ? senderTrimmed : ''; + statusMessage = + statusMessage.length > 0 && messageTrimmed.length > 0 + ? `${statusMessage}: ${messageTrimmed}` + : ''; + return statusMessage; +} + export default function reducer( state: State | undefined, action: Actions, @@ -221,6 +247,25 @@ export default function reducer( return {...state, share: {...share, url: action.payload}}; } return state; + } else if (action.type === 'ADD_STATUS_MSG') { + const {sender, msg} = action.payload; + const statusMsg = statusMessage(sender, msg); + if (statusMsg.length > 0) { + return { + ...state, + statusMessages: [...state.statusMessages, statusMsg], + }; + } + return state; + } else if (action.type === 'REMOVE_STATUS_MSG') { + const {sender, msg} = action.payload; + const statusMsg = statusMessage(sender, msg); + if (statusMsg.length > 0) { + const statusMessages = [...state.statusMessages]; + statusMessages.splice(statusMessages.indexOf(statusMsg), 1); + return {...state, statusMessages}; + } + return state; } else { return state; } @@ -288,3 +333,13 @@ export const setExportURL = (result: string): Action => ({ type: 'SET_EXPORT_URL', payload: result, }); + +export const addStatusMessage = (payload: StatusMessageType): Action => ({ + type: 'ADD_STATUS_MSG', + payload, +}); + +export const removeStatusMessage = (payload: StatusMessageType): Action => ({ + type: 'REMOVE_STATUS_MSG', + payload, +});