Skip to content

Commit

Permalink
Fix bug 1486511: Add user notifications (#1253)
Browse files Browse the repository at this point in the history
  • Loading branch information
mathjazz committed Apr 10, 2019
1 parent 46910e3 commit 94a33cc
Show file tree
Hide file tree
Showing 15 changed files with 708 additions and 19 deletions.
10 changes: 9 additions & 1 deletion frontend/public/static/locale/en-US/translate.ftl
Expand Up @@ -159,7 +159,15 @@ otherlocales-translation-copy =
.title = Copy Into Translation (Tab)
## User
## User Notifications
## Shows user notifications menu.

user-UserNotificationsMenu--no-notifications-title = No new notifications.
user-UserNotificationsMenu--no-notifications-description = Here you’ll see updates for localizations you contribute to.
user-UserNotificationsMenu--see-all-notifications = See all Notifications
## User Menu
## Shows user menu entries and options to sign in or out.

user-AppSwitcher--leave-translate-next = Leave Translate.Next
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/core/api/user.js
Expand Up @@ -20,6 +20,16 @@ export default class UserAPI extends APIBase {
return await this.fetch('/user-data/', 'GET', null, headers);
}

/**
* Mark all notifications of the current user as read.
*/
async markAllNotificationsAsRead(): Promise<Object> {
const headers = new Headers();
headers.append('X-Requested-With', 'XMLHttpRequest');

return await this.fetch('/notifications/mark-all-as-read/', 'GET', null, headers);
}

/**
* Sign out the current user.
*/
Expand Down
40 changes: 25 additions & 15 deletions frontend/src/core/user/actions.js
Expand Up @@ -12,6 +12,21 @@ export type Settings = {
};


/**
* Update the user settings.
*/
export type UpdateSettingsAction = {|
+type: typeof UPDATE_SETTINGS,
+settings: Settings,
|};
export function updateSettings(settings: Settings): UpdateSettingsAction {
return {
type: UPDATE_SETTINGS,
settings,
};
}


/**
* Update the user data.
*/
Expand All @@ -28,17 +43,14 @@ export function update(data: Object): UpdateAction {


/**
* Update the user settings.
* Sign out the current user.
*/
export type UpdateSettingsAction = {|
+type: typeof UPDATE_SETTINGS,
+settings: Settings,
|};
export function updateSettings(settings: Settings): UpdateSettingsAction {
return {
type: UPDATE_SETTINGS,
settings,
};
export function signOut(url: string): Function {
return async dispatch => {
await api.user.signOut(url);

dispatch(get());
}
}


Expand All @@ -51,12 +63,9 @@ export function saveSetting(setting: string, value: boolean, username: string):
}


/**
* Sign out the current user.
*/
export function signOut(url: string): Function {
export function markAllNotificationsAsRead(): Function {
return async dispatch => {
await api.user.signOut(url);
await api.user.markAllNotificationsAsRead();

dispatch(get());
}
Expand All @@ -79,6 +88,7 @@ export function get(): Function {

export default {
get,
markAllNotificationsAsRead,
saveSetting,
signOut,
update,
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/core/user/components/UserControls.js
Expand Up @@ -10,6 +10,7 @@ import * as user from 'core/user';
import AppSwitcher from './AppSwitcher';
import SignIn from './SignIn';
import UserAutoUpdater from './UserAutoUpdater';
import UserNotificationsMenu from './UserNotificationsMenu';
import UserMenu from './UserMenu';
import { actions, NAME } from '..';

Expand Down Expand Up @@ -37,6 +38,10 @@ export class UserControlsBase extends React.Component<InternalProps> {
this.props.dispatch(actions.get());
}

markAllNotificationsAsRead = () => {
this.props.dispatch(actions.markAllNotificationsAsRead());
}

signUserOut = () => {
const { user } = this.props;
this.props.dispatch(actions.signOut(user.signOutURL));
Expand All @@ -58,6 +63,11 @@ export class UserControlsBase extends React.Component<InternalProps> {
user={ user }
/>

<UserNotificationsMenu
markAllNotificationsAsRead={ this.markAllNotificationsAsRead }
user={ user }
/>

{ user.isAuthenticated ? null :
<SignIn url={ user.signInURL } />
}
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/core/user/components/UserMenu.css
@@ -1,10 +1,11 @@
.user-menu {
float: right;
height: 60px;
}

.user-menu .selector {
cursor: pointer;
margin: 6px 5px 6px 5px;
height: 100%;
}

.user-menu .selector img,
Expand All @@ -17,12 +18,14 @@
.user-menu .selector img {
height: 44px;
width: 44px;
margin: 6px 5px 6px 5px;
}

.user-menu .selector .menu-icon {
font-size: 20px;
height: 20px;
width: 20px;
margin: 6px 5px 6px 5px;
padding: 12px;
text-align: center;
}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/core/user/components/UserMenu.js
Expand Up @@ -74,7 +74,7 @@ export class UserMenuBase extends React.Component<Props, State> {

return <div className="user-menu">
<div
className="button selector"
className="selector"
onClick={ this.toggleVisibility }
>
{ user.isAuthenticated ?
Expand Down
109 changes: 109 additions & 0 deletions frontend/src/core/user/components/UserNotification.css
@@ -0,0 +1,109 @@
.user-notification {
border-top: 1px solid #333941;
cursor: default;
padding: 0;
}

.user-notification:first-child {
border-color: #272a2f;
}

.user-notification:first-child:hover {
border-color: #333941;
}

.user-notification.unread {
background: #3F4752;
}

.user-notification.read {
animation-duration: 2s;
animation-name: fadeout-background;
}

@keyframes fadeout-background {
0% {
background: #3F4752;
}

50% {
background: #3F4752;
}

100% {
background: #272a2f;
}
}

.user-notification .item-content {
display: block;
padding: 10px;
}

.user-notification span {
float: none;
}

.user-notification a {
color: #F36;
display: inline;
}

.user-notification .verb {
color: #EBEBEB;
padding: 0 3px;
}

.user-notification .message {
padding: 10px;
}

.user-notification .message {
padding: 10px;
}

/* Users can include HTML tags in their messages */
.user-notification .message h1,
.user-notification .message h2,
.user-notification .message h3,
.user-notification .message h4,
.user-notification .message h5,
.user-notification .message h6 {
color: #EBEBEB;
font-size: 14px;
font-style: normal;
font-weight: bold;
letter-spacing: 0;
text-transform: none;
}

.user-notification .message h1 {
font-size: 18px;
}

.user-notification .message h2 {
font-size: 16px;
}

.user-notification .message p {
padding: 5px 0;
}

.user-notification .message ol {
margin-left: 1.2em;
}

.user-notification .message ul {
list-style: inside;
}
/* End message styling */

.user-notification .timeago {
color: #888888;
display: block;
font-size: 11px;
font-weight: normal;
margin-top: 8px;
text-align: right;
text-transform: uppercase;
}
90 changes: 90 additions & 0 deletions frontend/src/core/user/components/UserNotification.js
@@ -0,0 +1,90 @@
/* @flow */

import * as React from 'react';
import TimeAgo from 'react-timeago';

import './UserNotification.css';


type Props = {
notification: Object,
};

type State = {|
markAsRead: boolean,
|};


/**
* Renders a single notification in the notifications menu.
*/
export default class UserNotification extends React.Component<Props, State> {
constructor(props: Props) {
super(props);

this.state = {
markAsRead: false,
};
}

componentDidUpdate(prevProps: Props) {
if (prevProps.notification.unread && !this.props.notification.unread) {
this.setState({
markAsRead: true,
});
}
}

render() {
const { notification } = this.props;

let className = 'user-notification';
if (notification.unread) {
className += ' unread';
}
else if (this.state.markAsRead) {
className += ' read';
}

return <li
className={ className }
data-id={ notification.id }
data-level={ notification.level }
>
<div className="item-content">
<span className="actor">
<a href={ notification.actor.url }>
{ notification.actor.anchor }
</a>
</span>

<span className="verb">{ notification.verb }</span>

{ !notification.target ? null :
<span className="target">
<a href={ notification.target.url }>
{ notification.target.anchor }
</a>
</span>
}

<TimeAgo
className="timeago"
date={ notification.date_iso }
title={ `${notification.date} UTC` }
/>

{ !notification.description ? null :
<div
className="message"
// We can safely use notification.description as it is either generated
// by the code or sanitized when coming from the DB. See:
// - pontoon.projects.forms.NotificationsForm()
// - pontoon.base.forms.HtmlField()
dangerouslySetInnerHTML={{ __html: notification.description }}
/>
}
</div>
</li>;
}
}

0 comments on commit 94a33cc

Please sign in to comment.