diff --git a/frontend/src/App/State/AppState.ts b/frontend/src/App/State/AppState.ts index 0f1aa5dee..60524fe10 100644 --- a/frontend/src/App/State/AppState.ts +++ b/frontend/src/App/State/AppState.ts @@ -11,6 +11,7 @@ import EpisodeFilesAppState from './EpisodeFilesAppState'; import EpisodesAppState from './EpisodesAppState'; import HistoryAppState from './HistoryAppState'; import InteractiveImportAppState from './InteractiveImportAppState'; +import MessagesAppState from './MessagesAppState'; import OAuthAppState from './OAuthAppState'; import OrganizePreviewAppState from './OrganizePreviewAppState'; import ParseAppState from './ParseAppState'; @@ -76,6 +77,7 @@ export interface AppSectionState { error?: Error; isPopulated: boolean; }; + messages: MessagesAppState; } interface AppState { diff --git a/frontend/src/App/State/MessagesAppState.ts b/frontend/src/App/State/MessagesAppState.ts new file mode 100644 index 000000000..9f258ba4b --- /dev/null +++ b/frontend/src/App/State/MessagesAppState.ts @@ -0,0 +1,15 @@ +import ModelBase from 'App/ModelBase'; +import AppSectionState from 'App/State/AppSectionState'; + +export type MessageType = 'error' | 'info' | 'success' | 'warning'; + +export interface Message extends ModelBase { + hideAfter: number; + message: string; + name: string; + type: MessageType; +} + +type MessagesAppState = AppSectionState; + +export default MessagesAppState; diff --git a/frontend/src/Components/Page/Sidebar/Messages/Message.js b/frontend/src/Components/Page/Sidebar/Messages/Message.js deleted file mode 100644 index ff43a5e20..000000000 --- a/frontend/src/Components/Page/Sidebar/Messages/Message.js +++ /dev/null @@ -1,70 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import Icon from 'Components/Icon'; -import { icons } from 'Helpers/Props'; -import styles from './Message.css'; - -function getIconName(name) { - switch (name) { - case 'ApplicationUpdate': - return icons.RESTART; - case 'Backup': - return icons.BACKUP; - case 'CheckHealth': - return icons.HEALTH; - case 'EpisodeSearch': - return icons.SEARCH; - case 'Housekeeping': - return icons.HOUSEKEEPING; - case 'RefreshSeries': - return icons.REFRESH; - case 'RssSync': - return icons.RSS; - case 'SeasonSearch': - return icons.SEARCH; - case 'SeriesSearch': - return icons.SEARCH; - case 'UpdateSceneMapping': - return icons.REFRESH; - default: - return icons.SPINNER; - } -} - -function Message(props) { - const { - name, - message, - type - } = props; - - return ( -
-
- -
- -
- {message} -
-
- ); -} - -Message.propTypes = { - name: PropTypes.string.isRequired, - message: PropTypes.string.isRequired, - type: PropTypes.string.isRequired -}; - -export default Message; diff --git a/frontend/src/Components/Page/Sidebar/Messages/Message.tsx b/frontend/src/Components/Page/Sidebar/Messages/Message.tsx new file mode 100644 index 000000000..2de1613dd --- /dev/null +++ b/frontend/src/Components/Page/Sidebar/Messages/Message.tsx @@ -0,0 +1,76 @@ +import classNames from 'classnames'; +import React, { useEffect, useMemo, useRef } from 'react'; +import { useDispatch } from 'react-redux'; +import { MessageType } from 'App/State/MessagesAppState'; +import Icon, { IconName } from 'Components/Icon'; +import { icons } from 'Helpers/Props'; +import { hideMessage } from 'Store/Actions/appActions'; +import styles from './Message.css'; + +interface MessageProps { + id: number; + hideAfter: number; + name: string; + message: string; + type: Extract; +} + +function Message({ id, hideAfter, name, message, type }: MessageProps) { + const dispatch = useDispatch(); + const dismissTimeout = useRef>(); + + const icon: IconName = useMemo(() => { + switch (name) { + case 'ApplicationUpdate': + return icons.RESTART; + case 'Backup': + return icons.BACKUP; + case 'CheckHealth': + return icons.HEALTH; + case 'EpisodeSearch': + return icons.SEARCH; + case 'Housekeeping': + return icons.HOUSEKEEPING; + case 'RefreshSeries': + return icons.REFRESH; + case 'RssSync': + return icons.RSS; + case 'SeasonSearch': + return icons.SEARCH; + case 'SeriesSearch': + return icons.SEARCH; + case 'UpdateSceneMapping': + return icons.REFRESH; + default: + return icons.SPINNER; + } + }, [name]); + + useEffect(() => { + if (hideAfter) { + dismissTimeout.current = setTimeout(() => { + dispatch(hideMessage({ id })); + + dismissTimeout.current = undefined; + }, hideAfter * 1000); + } + + return () => { + if (dismissTimeout.current) { + clearTimeout(dismissTimeout.current); + } + }; + }, [id, hideAfter, message, type, dispatch]); + + return ( +
+
+ +
+ +
{message}
+
+ ); +} + +export default Message; diff --git a/frontend/src/Components/Page/Sidebar/Messages/MessageConnector.js b/frontend/src/Components/Page/Sidebar/Messages/MessageConnector.js deleted file mode 100644 index e92722343..000000000 --- a/frontend/src/Components/Page/Sidebar/Messages/MessageConnector.js +++ /dev/null @@ -1,67 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { hideMessage } from 'Store/Actions/appActions'; -import Message from './Message'; - -const mapDispatchToProps = { - hideMessage -}; - -class MessageConnector extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this._hideTimeoutId = null; - this.scheduleHideMessage(props.hideAfter); - } - - componentDidUpdate() { - this.scheduleHideMessage(this.props.hideAfter); - } - - // - // Control - - scheduleHideMessage = (hideAfter) => { - if (this._hideTimeoutId) { - clearTimeout(this._hideTimeoutId); - } - - if (hideAfter) { - this._hideTimeoutId = setTimeout(this.hideMessage, hideAfter * 1000); - } - }; - - hideMessage = () => { - this.props.hideMessage({ id: this.props.id }); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -MessageConnector.propTypes = { - id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, - hideAfter: PropTypes.number.isRequired, - hideMessage: PropTypes.func.isRequired -}; - -MessageConnector.defaultProps = { - // Hide messages after 60 seconds if there is no activity - // hideAfter: 60 -}; - -export default connect(undefined, mapDispatchToProps)(MessageConnector); diff --git a/frontend/src/Components/Page/Sidebar/Messages/Messages.js b/frontend/src/Components/Page/Sidebar/Messages/Messages.js deleted file mode 100644 index ec8876f6e..000000000 --- a/frontend/src/Components/Page/Sidebar/Messages/Messages.js +++ /dev/null @@ -1,27 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import MessageConnector from './MessageConnector'; -import styles from './Messages.css'; - -function Messages({ messages }) { - return ( -
- { - messages.map((message) => { - return ( - - ); - }) - } -
- ); -} - -Messages.propTypes = { - messages: PropTypes.arrayOf(PropTypes.object).isRequired -}; - -export default Messages; diff --git a/frontend/src/Components/Page/Sidebar/Messages/Messages.tsx b/frontend/src/Components/Page/Sidebar/Messages/Messages.tsx new file mode 100644 index 000000000..d6ea1057a --- /dev/null +++ b/frontend/src/Components/Page/Sidebar/Messages/Messages.tsx @@ -0,0 +1,28 @@ +import React, { useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import { Message as MessageModel } from 'App/State/MessagesAppState'; +import Message from './Message'; +import styles from './Messages.css'; + +function Messages() { + const items = useSelector((state: AppState) => state.app.messages.items); + + const messages = useMemo(() => { + return items.reduce((acc, item) => { + acc.unshift(item); + + return acc; + }, []); + }, [items]); + + return ( +
+ {messages.map((message) => { + return ; + })} +
+ ); +} + +export default Messages; diff --git a/frontend/src/Components/Page/Sidebar/Messages/MessagesConnector.js b/frontend/src/Components/Page/Sidebar/Messages/MessagesConnector.js deleted file mode 100644 index 5d20d9194..000000000 --- a/frontend/src/Components/Page/Sidebar/Messages/MessagesConnector.js +++ /dev/null @@ -1,16 +0,0 @@ -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import Messages from './Messages'; - -function createMapStateToProps() { - return createSelector( - (state) => state.app.messages.items, - (messages) => { - return { - messages: messages.slice().reverse() - }; - } - ); -} - -export default connect(createMapStateToProps)(Messages); diff --git a/frontend/src/Components/Page/Sidebar/PageSidebar.tsx b/frontend/src/Components/Page/Sidebar/PageSidebar.tsx index 7b3ef8292..f6b4b02fb 100644 --- a/frontend/src/Components/Page/Sidebar/PageSidebar.tsx +++ b/frontend/src/Components/Page/Sidebar/PageSidebar.tsx @@ -19,7 +19,7 @@ import { setIsSidebarVisible } from 'Store/Actions/appActions'; import dimensions from 'Styles/Variables/dimensions'; import HealthStatus from 'System/Status/Health/HealthStatus'; import translate from 'Utilities/String/translate'; -import MessagesConnector from './Messages/MessagesConnector'; +import Messages from './Messages/Messages'; import PageSidebarItem from './PageSidebarItem'; import styles from './PageSidebar.css'; @@ -511,7 +511,7 @@ function PageSidebar({ isSidebarVisible, isSmallScreen }: PageSidebarProps) { })} - + );