Skip to content

Commit 61fc9f0

Browse files
SemaiCZEMartin Kruliš
authored andcommitted
System messages showed to users
1 parent 4f7d45e commit 61fc9f0

File tree

10 files changed

+188
-3
lines changed

10 files changed

+188
-3
lines changed

src/components/layout/Sidebar/Admin.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ const Admin = ({ currentUrl, links: { ADMIN_INSTANCES_URI, USERS_URI, FAILURES_U
2929
link={FAILURES_URI}
3030
/>
3131
<MenuItem
32-
icon="flag"
32+
icon="envelope"
3333
title={<FormattedMessage id="app.sidebar.menu.admin.messages" defaultMessage="System Messages" />}
3434
currentPath={currentUrl}
3535
link={MESSAGES_URI}

src/components/widgets/Header/Header.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { IndexLink } from 'react-router';
55
import MediaQuery from 'react-responsive';
66

77
import HeaderNotificationsContainer from '../../../containers/HeaderNotificationsContainer';
8+
import HeaderSystemMessagesContainer from '../../../containers/HeaderSystemMessagesContainer';
89
import HeaderLanguageSwitching from '../HeaderLanguageSwitching';
910
import ClientOnly from '../../helpers/ClientOnly';
1011
import { LoadingIcon } from '../../icons';
@@ -76,6 +77,7 @@ class Header extends Component {
7677
</ClientOnly>
7778
<div className="navbar-custom-menu">
7879
<ul className="nav navbar-nav">
80+
<HeaderSystemMessagesContainer />
7981
<HeaderNotificationsContainer />
8082
{availableLangs.map(lang => (
8183
<HeaderLanguageSwitching lang={lang} active={currentLang === lang} key={lang} currentUrl={currentUrl} />
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import { FormattedMessage, intlShape, injectIntl } from 'react-intl';
4+
import classnames from 'classnames';
5+
import { Label, Alert } from 'react-bootstrap';
6+
7+
import Icon from '../../icons';
8+
import Markdown from '../Markdown';
9+
import { getLocalizedText } from '../../../helpers/localizedData';
10+
11+
import styles from './HeaderSystemMessagesDropdown.less';
12+
13+
const HeaderSystemMessagesDropdown = ({ isOpen, toggleOpen, markClick, systemMessages, intl: { locale } }) => (
14+
<li
15+
className={classnames({
16+
'notifications-menu': true,
17+
dropdown: true,
18+
open: isOpen,
19+
})}>
20+
<a href="#" className="dropdown-toggle" onClick={toggleOpen}>
21+
<Icon icon={['far', 'envelope']} />
22+
{systemMessages.length > 0 && <Label bsStyle="warning">{systemMessages.length}</Label>}
23+
</a>
24+
<ul className={classnames(['dropdown-menu', styles.dropdownMenu])} onClick={markClick}>
25+
<li className="header">
26+
<FormattedMessage
27+
id="app.systemMessages.title"
28+
defaultMessage="You have {count, number} active {count, plural, one {message} two {messages} other {messages}}"
29+
values={{ count: systemMessages.length }}
30+
/>
31+
</li>
32+
<li>
33+
<ul className={classnames(['menu', styles.messageList])}>
34+
{systemMessages.map((message, idx) => (
35+
<Alert key={idx} bsStyle={message.type} className={styles.messageAlert}>
36+
<Markdown source={getLocalizedText(message, locale)} />
37+
</Alert>
38+
))}
39+
</ul>
40+
</li>
41+
</ul>
42+
</li>
43+
);
44+
45+
HeaderSystemMessagesDropdown.propTypes = {
46+
isOpen: PropTypes.bool,
47+
showAll: PropTypes.bool,
48+
toggleOpen: PropTypes.func.isRequired,
49+
markClick: PropTypes.func.isRequired,
50+
systemMessages: PropTypes.array.isRequired,
51+
intl: intlShape.isRequired,
52+
};
53+
54+
export default injectIntl(HeaderSystemMessagesDropdown);
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
.dropdownMenu {
2+
width: 42vw !important;
3+
min-width: 400px !important;
4+
}
5+
6+
.messageList {
7+
max-height: 80vh !important;
8+
}
9+
10+
.messageAlert {
11+
margin-bottom: 0px;
12+
border-radius: 0px !important;
13+
border-style: none;
14+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import HeaderSystemMessagesDropdown from './HeaderSystemMessagesDropdown';
2+
export default HeaderSystemMessagesDropdown;
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import React, { Component } from 'react';
2+
import PropTypes from 'prop-types';
3+
import { canUseDOM } from 'exenv';
4+
import { connect } from 'react-redux';
5+
import HeaderSystemMessagesDropdown from '../../components/widgets/HeaderSystemMessagesDropdown';
6+
import { readyActiveSystemMessagesSelector, fetchManyUserStatus } from '../../redux/selectors/systemMessages';
7+
import FetchManyResourceRenderer from '../../components/helpers/FetchManyResourceRenderer';
8+
import { fetchAllUserMessages } from '../../redux/modules/systemMessages';
9+
10+
class HeaderSystemMessagesContainer extends Component {
11+
state = { isOpen: false };
12+
13+
static loadAsync = (params, dispatch) => Promise.all([dispatch(fetchAllUserMessages)]);
14+
15+
//
16+
// Monitor clicking and hide the notifications panel when the user clicks sideways
17+
18+
componentWillMount = () => {
19+
this.props.loadAsync();
20+
this.lastClick = 0;
21+
if (canUseDOM) {
22+
window.addEventListener('click', () => this.clickAnywhere());
23+
}
24+
};
25+
26+
componentWillUnMount = () => {
27+
if (canUseDOM) {
28+
window.removeEventListener(() => this.clickAnywhere());
29+
}
30+
};
31+
32+
clickAnywhere = () => {
33+
if (this.state.isOpen === true && this.isClickingSomewhereElse()) {
34+
this.close();
35+
}
36+
};
37+
38+
markClick = () => {
39+
this.lastClick = Date.now();
40+
};
41+
42+
/**
43+
* Determines, whether this click is on the container or not - a 50ms tolerance
44+
* between now and the time of last click on the container is defined.
45+
*/
46+
isClickingSomewhereElse = () => Date.now() - this.lastClick > 50;
47+
48+
toggleOpen = e => {
49+
e.preventDefault();
50+
this.state.isOpen ? this.close() : this.open();
51+
this.markClick();
52+
};
53+
54+
close = () => {
55+
this.setState({ isOpen: false });
56+
};
57+
58+
open = () => this.setState({ isOpen: true });
59+
60+
render() {
61+
const { systemMessages, fetchStatus } = this.props;
62+
const { isOpen } = this.state;
63+
64+
return (
65+
<FetchManyResourceRenderer fetchManyStatus={fetchStatus} loading={<span />}>
66+
{() => (
67+
<HeaderSystemMessagesDropdown
68+
isOpen={isOpen}
69+
toggleOpen={this.toggleOpen}
70+
markClick={this.markClick}
71+
systemMessages={systemMessages}
72+
/>
73+
)}
74+
</FetchManyResourceRenderer>
75+
);
76+
}
77+
}
78+
79+
HeaderSystemMessagesContainer.propTypes = {
80+
systemMessages: PropTypes.array.isRequired,
81+
fetchStatus: PropTypes.string,
82+
loadAsync: PropTypes.func.isRequired,
83+
};
84+
85+
export default connect(
86+
state => ({
87+
fetchStatus: fetchManyUserStatus(state),
88+
systemMessages: readyActiveSystemMessagesSelector(state),
89+
}),
90+
dispatch => ({
91+
loadAsync: () => HeaderSystemMessagesContainer.loadAsync({}, dispatch),
92+
})
93+
)(HeaderSystemMessagesContainer);
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import HeaderSystemMessagesContainer from './HeaderSystemMessagesContainer';
2+
export default HeaderSystemMessagesContainer;

src/pages/SystemMessages/SystemMessages.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ class SystemMessages extends Component {
104104
breadcrumbs={[
105105
{
106106
text: <FormattedMessage id="app.systemMessages.title" defaultMessage="System Messages" />,
107-
iconName: 'flag',
107+
iconName: 'envelope',
108108
},
109109
]}>
110110
<React.Fragment>

src/redux/modules/systemMessages.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ export const fetchAllMessages = actions.fetchMany({
1616
endpoint: fetchManyEndpoint,
1717
});
1818

19+
export const fetchManyUserEndpoint = '/notifications';
20+
export const fetchAllUserMessages = actions.fetchMany({
21+
endpoint: fetchManyUserEndpoint,
22+
});
23+
1924
/**
2025
* Reducer takes mainly care about all the state of individual attachments
2126
*/

src/redux/selectors/systemMessages.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createSelector } from 'reselect';
2-
import { fetchManyEndpoint } from '../modules/systemMessages';
2+
import { fetchManyEndpoint, fetchManyUserEndpoint } from '../modules/systemMessages';
33
import { isReady, getJsData } from '../helpers/resourceManager';
44

55
const getSystemMessages = state => state.systemMessages;
@@ -15,6 +15,11 @@ export const fetchManyStatus = createSelector(
1515
state => state.getIn(['fetchManyStatus', fetchManyEndpoint])
1616
);
1717

18+
export const fetchManyUserStatus = createSelector(
19+
getSystemMessages,
20+
state => state.getIn(['fetchManyStatus', fetchManyUserEndpoint])
21+
);
22+
1823
export const getMessage = messageId =>
1924
createSelector(
2025
systemMessagesSelector,
@@ -30,3 +35,11 @@ export const readySystemMessagesSelector = createSelector(
3035
.map(getJsData)
3136
.toArray()
3237
);
38+
39+
export const readyActiveSystemMessagesSelector = createSelector(
40+
readySystemMessagesSelector,
41+
messages =>
42+
messages
43+
.filter(m => m.visibleFrom * 1000 <= Date.now() && m.visibleTo * 1000 >= Date.now())
44+
.sort((a, b) => a.visibleTo - b.visibleTo) // show messages with shortest expiration first
45+
);

0 commit comments

Comments
 (0)