use decorator pattern for notifs api
This commit is contained in:
parent
64ffc78f45
commit
9cc700c64d
14 changed files with 284 additions and 130 deletions
|
@ -55,10 +55,6 @@ $unread_notifications_dot_size: 8px;
|
|||
}
|
||||
|
||||
.controller-notifications {
|
||||
ul.notifications {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.notificationPage,
|
||||
.notificationsPage {
|
||||
font-family: 'din-regular', Sans-Serif;
|
||||
|
@ -86,86 +82,6 @@ $unread_notifications_dot_size: 8px;
|
|||
.emptyInbox {
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.notification {
|
||||
padding: 10px;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background: #F6F6F6;
|
||||
|
||||
.notification-read-unread {
|
||||
display:block;
|
||||
}
|
||||
|
||||
.notification-date {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
& > a {
|
||||
float: left;
|
||||
width: 85%;
|
||||
box-sizing: border-box;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.notification-actor {
|
||||
float: left;
|
||||
|
||||
img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.notification-body {
|
||||
margin-left: 50px;
|
||||
line-height: 20px;
|
||||
|
||||
.in-bold {
|
||||
font-family: 'din-medium', Sans-Serif;
|
||||
}
|
||||
|
||||
.action {
|
||||
background: #4fb5c0;
|
||||
color: #FFF;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
display: inline-block;
|
||||
margin: 5px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.notification-date {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 10px;
|
||||
color: #607d8b;
|
||||
font-size: 13px;
|
||||
line-height: 13px;
|
||||
margin-top: -6px;
|
||||
}
|
||||
|
||||
.notification-read-unread {
|
||||
display: none;
|
||||
float: left;
|
||||
width: 15%;
|
||||
|
||||
a {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
margin-top: -10px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
&.unread {
|
||||
background: #EEE;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -202,3 +118,87 @@ $unread_notifications_dot_size: 8px;
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul.notifications {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.notification {
|
||||
padding: 10px;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background: #F6F6F6;
|
||||
|
||||
.notification-read-unread {
|
||||
display:block;
|
||||
}
|
||||
|
||||
.notification-date {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
& > a {
|
||||
float: left;
|
||||
width: 85%;
|
||||
box-sizing: border-box;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.notification-actor {
|
||||
float: left;
|
||||
|
||||
img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.notification-body {
|
||||
font-family: 'din-regular', Sans-Serif;
|
||||
margin-left: 50px;
|
||||
line-height: 20px;
|
||||
|
||||
.in-bold {
|
||||
font-family: 'din-medium', Sans-Serif;
|
||||
}
|
||||
|
||||
.action {
|
||||
background: #4fb5c0;
|
||||
color: #FFF;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
display: inline-block;
|
||||
margin: 5px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.notification-date {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 10px;
|
||||
color: #607d8b;
|
||||
font-size: 13px;
|
||||
line-height: 13px;
|
||||
margin-top: -6px;
|
||||
}
|
||||
|
||||
.notification-read-unread {
|
||||
display: none;
|
||||
float: left;
|
||||
width: 15%;
|
||||
|
||||
div {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
margin-top: -10px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
&.unread {
|
||||
background: #EEE;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,11 +12,7 @@ class NotificationsController < ApplicationController
|
|||
format.json do
|
||||
notifications = @notifications.map do |notification|
|
||||
receipt = @receipts.find_by(notification_id: notification.id)
|
||||
notification.as_json.merge(
|
||||
is_read: receipt.is_read,
|
||||
notified_object: notification.notified_object,
|
||||
sender: notification.sender
|
||||
)
|
||||
NotificationDecorator.decorate(notification, receipt)
|
||||
end
|
||||
render json: notifications
|
||||
end
|
||||
|
@ -39,9 +35,7 @@ class NotificationsController < ApplicationController
|
|||
end
|
||||
end
|
||||
format.json do
|
||||
render json: @notification.as_json.merge(
|
||||
is_read: @receipt.is_read
|
||||
)
|
||||
render json: NotificationDecorator.decorate(@notification, @receipt)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -49,11 +43,8 @@ class NotificationsController < ApplicationController
|
|||
def mark_read
|
||||
@receipt.update(is_read: true)
|
||||
respond_to do |format|
|
||||
format.js
|
||||
format.json do
|
||||
render json: @notification.as_json.merge(
|
||||
is_read: @receipt.is_read
|
||||
)
|
||||
render json: NotificationDecorator.decorate(@notification, @receipt)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -61,11 +52,8 @@ class NotificationsController < ApplicationController
|
|||
def mark_unread
|
||||
@receipt.update(is_read: false)
|
||||
respond_to do |format|
|
||||
format.js
|
||||
format.json do
|
||||
render json: @notification.as_json.merge(
|
||||
is_read: @receipt.is_read
|
||||
)
|
||||
render json: NotificationDecorator.decorate(@notification, @receipt)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
47
app/decorators/notification_decorator.rb
Normal file
47
app/decorators/notification_decorator.rb
Normal file
|
@ -0,0 +1,47 @@
|
|||
class NotificationDecorator
|
||||
class << self
|
||||
def decorate(notification, receipt)
|
||||
result = {
|
||||
id: notification.id,
|
||||
type: notification.notification_code,
|
||||
subject: notification.subject,
|
||||
is_read: receipt.is_read,
|
||||
created_at: notification.created_at,
|
||||
actor: notification.sender,
|
||||
object: notification.notified_object
|
||||
}
|
||||
|
||||
case notification.notification_code
|
||||
when MAP_ACCESS_APPROVED, MAP_ACCESS_REQUEST, MAP_INVITE_TO_EDIT
|
||||
map = notification.notified_object&.map
|
||||
result[:map] = {
|
||||
id: map&.id,
|
||||
name: map&.name
|
||||
}
|
||||
when TOPIC_ADDED_TO_MAP
|
||||
topic = notification.notified_object&.eventable
|
||||
map = notification.notified_object&.map
|
||||
result[:topic] = {
|
||||
id: topic&.id,
|
||||
name: topic&.name
|
||||
}
|
||||
result[:map] = {
|
||||
id: map&.id,
|
||||
name: map&.name
|
||||
}
|
||||
when TOPIC_CONNECTED_1, TOPIC_CONNECTED_2
|
||||
topic1 = notification.notified_object&.topic1
|
||||
topic2 = notification.notified_object&.topic2
|
||||
result[:topic1] = {
|
||||
id: topic1&.id,
|
||||
name: topic1&.name
|
||||
}
|
||||
resul[:topic2] = {
|
||||
id: topic2&.id,
|
||||
name: topic2&.name
|
||||
}
|
||||
end
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,7 +0,0 @@
|
|||
$('#notification-<%= @notification.id %> .notification-read-unread > a')
|
||||
.text('mark as unread')
|
||||
.attr('href', '<%= mark_unread_notification_path(@notification.id) %>')
|
||||
$('#notification-<%= @notification.id %>')
|
||||
.removeClass('unread')
|
||||
.addClass('read')
|
||||
Metamaps.GlobalUI.NotificationIcon.render(Metamaps.GlobalUI.NotificationIcon.unreadNotificationsCount - 1)
|
|
@ -1,7 +0,0 @@
|
|||
$('#notification-<%= @notification.id %> .notification-read-unread > a')
|
||||
.text('mark as read')
|
||||
.attr('href', '<%= mark_read_notification_path(@notification.id) %>')
|
||||
$('#notification-<%= @notification.id %>')
|
||||
.removeClass('read')
|
||||
.addClass('unread')
|
||||
Metamaps.GlobalUI.NotificationIcon.render(Metamaps.GlobalUI.NotificationIcon.unreadNotificationsCount + 1)
|
|
@ -19,7 +19,7 @@ module Metamaps
|
|||
end
|
||||
|
||||
# Custom directories with classes and modules you want to be autoloadable.
|
||||
config.autoload_paths << Rails.root.join('app', 'services')
|
||||
config.autoload_paths << Rails.root.join('app', 'decorators', 'services')
|
||||
|
||||
# Configure the default encoding used in templates for Ruby 1.9.
|
||||
config.encoding = 'utf-8'
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
import GlobalUI from './index'
|
||||
|
||||
const Notifications = {
|
||||
notifications: null,
|
||||
unreadNotificationsCount: 0,
|
||||
init: serverData => {
|
||||
Notifications.unreadNotificationsCount = serverData.unreadNotificationsCount
|
||||
},
|
||||
fetch: render => {
|
||||
$.ajax({
|
||||
url: '/notifications.json',
|
||||
|
@ -8,6 +14,40 @@ const Notifications = {
|
|||
render()
|
||||
}
|
||||
})
|
||||
},
|
||||
markAsRead: (render, id) => {
|
||||
const n = Notifications.notifications.find(n => n.id === id)
|
||||
$.ajax({
|
||||
url: `/notifications/${id}/mark_read`,
|
||||
method: 'PUT',
|
||||
success: function(r) {
|
||||
if (n) {
|
||||
Notifications.unreadNotificationsCount--
|
||||
n.is_read = true
|
||||
render()
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
GlobalUI.notifyUser('There was an error marking that notification as read')
|
||||
}
|
||||
})
|
||||
},
|
||||
markAsUnread: (render, id) => {
|
||||
const n = Notifications.notifications.find(n => n.id === id)
|
||||
$.ajax({
|
||||
url: `/notifications/${id}/mark_unread`,
|
||||
method: 'PUT',
|
||||
success: function() {
|
||||
if (n) {
|
||||
Notifications.unreadNotificationsCount++
|
||||
n.is_read = false
|
||||
render()
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
GlobalUI.notifyUser('There was an error marking that notification as read')
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -31,7 +31,6 @@ const ReactApp = {
|
|||
serverData: {},
|
||||
mapId: null,
|
||||
topicId: null,
|
||||
unreadNotificationsCount: 0,
|
||||
mapsWidth: 0,
|
||||
toast: '',
|
||||
mobile: false,
|
||||
|
@ -41,7 +40,6 @@ const ReactApp = {
|
|||
init: function(serverData, openLightbox) {
|
||||
const self = ReactApp
|
||||
self.serverData = serverData
|
||||
self.unreadNotificationsCount = serverData.unreadNotificationsCount
|
||||
self.mobileTitle = serverData.mobileTitle
|
||||
self.openLightbox = openLightbox
|
||||
self.metacodeSets = serverData.metacodeSets
|
||||
|
@ -100,7 +98,7 @@ const ReactApp = {
|
|||
getProps: function() {
|
||||
const self = ReactApp
|
||||
return merge({
|
||||
unreadNotificationsCount: self.unreadNotificationsCount,
|
||||
unreadNotificationsCount: Notifications.unreadNotificationsCount,
|
||||
currentUser: Active.Mapper,
|
||||
toast: self.toast,
|
||||
mobile: self.mobile,
|
||||
|
@ -110,7 +108,9 @@ const ReactApp = {
|
|||
openInviteLightbox: () => self.openLightbox('invite'),
|
||||
serverData: self.serverData,
|
||||
notifications: Notifications.notifications,
|
||||
fetchNotifications: apply(Notifications.fetch, ReactApp.render)
|
||||
fetchNotifications: apply(Notifications.fetch, ReactApp.render),
|
||||
markAsRead: apply(Notifications.markAsRead, ReactApp.render),
|
||||
markAsUnread: apply(Notifications.markAsUnread, ReactApp.render)
|
||||
},
|
||||
self.getMapProps(),
|
||||
self.getTopicProps(),
|
||||
|
|
|
@ -4,6 +4,7 @@ import clipboard from 'clipboard-js'
|
|||
|
||||
import Create from '../Create'
|
||||
|
||||
import Notifications from './Notifications'
|
||||
import ReactApp from './ReactApp'
|
||||
import Search from './Search'
|
||||
import CreateMap from './CreateMap'
|
||||
|
@ -151,5 +152,5 @@ const GlobalUI = {
|
|||
}
|
||||
}
|
||||
|
||||
export { ReactApp, Search, CreateMap, ImportDialog }
|
||||
export { Notifications, ReactApp, Search, CreateMap, ImportDialog }
|
||||
export default GlobalUI
|
||||
|
|
|
@ -9,7 +9,7 @@ import DataModel from './DataModel'
|
|||
import Debug from './Debug'
|
||||
import Filter from './Filter'
|
||||
import GlobalUI, {
|
||||
ReactApp, Search, CreateMap, ImportDialog
|
||||
Notifications, ReactApp, Search, CreateMap, ImportDialog
|
||||
} from './GlobalUI'
|
||||
import Import from './Import'
|
||||
import JIT from './JIT'
|
||||
|
@ -42,6 +42,7 @@ Metamaps.DataModel = DataModel
|
|||
Metamaps.Debug = Debug
|
||||
Metamaps.Filter = Filter
|
||||
Metamaps.GlobalUI = GlobalUI
|
||||
Metamaps.GlobalUI.Notifications = Notifications
|
||||
Metamaps.GlobalUI.ReactApp = ReactApp
|
||||
Metamaps.GlobalUI.Search = Search
|
||||
Metamaps.GlobalUI.CreateMap = CreateMap
|
||||
|
|
|
@ -2,13 +2,16 @@ import React, { Component } from 'react'
|
|||
import PropTypes from 'prop-types'
|
||||
|
||||
import onClickOutsideAddon from 'react-onclickoutside'
|
||||
import Notification from '../Notification'
|
||||
|
||||
class NotificationBox extends Component {
|
||||
|
||||
static propTypes = {
|
||||
notifications: PropTypes.array,
|
||||
fetchNotifications: PropTypes.func.isRequired,
|
||||
toggleNotificationsBox: PropTypes.func.isRequired
|
||||
toggleNotificationsBox: PropTypes.func.isRequired,
|
||||
markAsRead: PropTypes.func.isRequired,
|
||||
markAsUnread: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
componentDidMount = () => {
|
||||
|
@ -23,15 +26,17 @@ class NotificationBox extends Component {
|
|||
}
|
||||
|
||||
render = () => {
|
||||
const { notifications } = this.props
|
||||
const { notifications, markAsRead, markAsUnread } = this.props
|
||||
return <div className='notificationsBox'>
|
||||
<div className='notificationsBoxTriangle' />
|
||||
<ul>
|
||||
<ul className='notifications'>
|
||||
{!notifications && <li>loading...</li>}
|
||||
<li>A notification</li>
|
||||
<li>A notification</li>
|
||||
<li>A notification</li>
|
||||
<li>A notification</li>
|
||||
{notifications && notifications.map(n => {
|
||||
return <Notification notification={n}
|
||||
markAsRead={markAsRead}
|
||||
markAsUnread={markAsUnread}
|
||||
key={`notification-${n.id}`} />
|
||||
})}
|
||||
</ul>
|
||||
<a href='/notifications' className='notificationsBoxSeeAll'>See all</a>
|
||||
</div>
|
||||
|
|
|
@ -13,6 +13,8 @@ class UpperRightUI extends Component {
|
|||
unreadNotificationsCount: PropTypes.number,
|
||||
fetchNotifications: PropTypes.func,
|
||||
notifications: PropTypes.array,
|
||||
markAsRead: PropTypes.func.isRequired,
|
||||
markAsUnread: PropTypes.func.isRequired,
|
||||
openInviteLightbox: PropTypes.func
|
||||
}
|
||||
|
||||
|
@ -47,7 +49,8 @@ class UpperRightUI extends Component {
|
|||
|
||||
render () {
|
||||
const { currentUser, signInPage, unreadNotificationsCount,
|
||||
notifications, fetchNotifications, openInviteLightbox } = this.props
|
||||
notifications, fetchNotifications, openInviteLightbox,
|
||||
markAsRead, markAsUnread } = this.props
|
||||
const { accountBoxOpen, notificationsBoxOpen } = this.state
|
||||
return <div className="upperRightUI">
|
||||
{currentUser && <a href="/maps/new" target="_blank" className="addMap upperRightEl upperRightIcon">
|
||||
|
@ -62,6 +65,8 @@ class UpperRightUI extends Component {
|
|||
{notificationsBoxOpen && <NotificationBox
|
||||
notifications={notifications}
|
||||
fetchNotifications={fetchNotifications}
|
||||
markAsRead={markAsRead}
|
||||
markAsUnread={markAsUnread}
|
||||
toggleNotificationsBox={this.toggleNotificationsBox}/>}
|
||||
</span>}
|
||||
{!signInPage && <div className="sidebarAccount upperRightEl">
|
||||
|
|
|
@ -13,6 +13,8 @@ class App extends Component {
|
|||
unreadNotificationsCount: PropTypes.number,
|
||||
notifications: PropTypes.array,
|
||||
fetchNotifications: PropTypes.func,
|
||||
markAsRead: PropTypes.func,
|
||||
markAsUnread: PropTypes.func,
|
||||
location: PropTypes.object,
|
||||
mobile: PropTypes.bool,
|
||||
mobileTitle: PropTypes.string,
|
||||
|
@ -40,7 +42,8 @@ class App extends Component {
|
|||
const { children, toast, unreadNotificationsCount, openInviteLightbox,
|
||||
mobile, mobileTitle, mobileTitleWidth, mobileTitleClick, location,
|
||||
map, userRequested, requestAnswered, requestApproved, serverData,
|
||||
onRequestAccess, notifications, fetchNotifications } = this.props
|
||||
onRequestAccess, notifications, fetchNotifications,
|
||||
markAsRead, markAsUnread } = this.props
|
||||
const { pathname } = location || {}
|
||||
// this fixes a bug that happens otherwise when you logout
|
||||
const currentUser = this.props.currentUser && this.props.currentUser.id ? this.props.currentUser : null
|
||||
|
@ -62,6 +65,8 @@ class App extends Component {
|
|||
unreadNotificationsCount={unreadNotificationsCount}
|
||||
notifications={notifications}
|
||||
fetchNotifications={fetchNotifications}
|
||||
markAsRead={markAsRead}
|
||||
markAsUnread={markAsUnread}
|
||||
openInviteLightbox={openInviteLightbox}
|
||||
signInPage={pathname === '/login'} />}
|
||||
<Toast message={toast} />
|
||||
|
|
76
frontend/src/components/Notification.js
Normal file
76
frontend/src/components/Notification.js
Normal file
|
@ -0,0 +1,76 @@
|
|||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
class Notification extends Component {
|
||||
|
||||
static propTypes = {
|
||||
markAsRead: PropTypes.func,
|
||||
markAsUnread: PropTypes.func,
|
||||
notification: PropTypes.shape({
|
||||
id: PropTypes.number.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
subject: PropTypes.string.isRequired,
|
||||
is_read: PropTypes.bool.isRequired,
|
||||
created_at: PropTypes.string.isRequired,
|
||||
actor: PropTypes.shape({
|
||||
id: PropTypes.number,
|
||||
name: PropTypes.string,
|
||||
image: PropTypes.string,
|
||||
admin: PropTypes.boolean
|
||||
}),
|
||||
object: PropTypes.object,
|
||||
map: PropTypes.object,
|
||||
topic: PropTypes.object,
|
||||
topic1: PropTypes.object,
|
||||
topic2: PropTypes.object
|
||||
})
|
||||
}
|
||||
|
||||
getDate = () => {
|
||||
const { notification: {created_at} } = this.props
|
||||
const months = ['Jan','Feb','Mar','Apr','May','Jun',
|
||||
'Jul','Aug','Sep','Oct','Nov','Dec']
|
||||
const created = new Date(created_at)
|
||||
return `${months[created.getMonth()]} ${created.getDate()}`
|
||||
}
|
||||
|
||||
markAsRead = () => {
|
||||
const { notification, markAsRead } = this.props
|
||||
markAsRead(notification.id)
|
||||
}
|
||||
|
||||
markAsUnread = () => {
|
||||
const { notification, markAsUnread } = this.props
|
||||
markAsUnread(notification.id)
|
||||
}
|
||||
|
||||
render = () => {
|
||||
const { notification } = this.props
|
||||
const classes = `notification ${notification.is_read ? 'read' : 'unread'}`
|
||||
return <li className={classes}>
|
||||
<a href={`/notifications/${notification.id}`}>
|
||||
<div className='notification-actor'>
|
||||
<img src={notification.actor.image} />
|
||||
</div>
|
||||
<div className="notification-body">
|
||||
<div className="in-bold">{notification.actor.name}</div>
|
||||
Other content
|
||||
</div>
|
||||
</a>
|
||||
<div className="notification-read-unread">
|
||||
{!notification.is_read && <div onClick={this.markAsRead}>
|
||||
mark read
|
||||
</div>}
|
||||
{notification.is_read && <div onClick={this.markAsUnread}>
|
||||
mark unread
|
||||
</div>}
|
||||
</div>
|
||||
<div className="notification-date">
|
||||
{this.getDate()}
|
||||
</div>
|
||||
<div className="clearfloat"></div>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
||||
export default Notification
|
Loading…
Reference in a new issue