use decorator pattern for notifs api

This commit is contained in:
Connor Turland 2017-09-22 18:38:38 -04:00
parent 64ffc78f45
commit 9cc700c64d
14 changed files with 284 additions and 130 deletions

View file

@ -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;
}
}

View file

@ -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

View 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

View file

@ -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)

View file

@ -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)

View file

@ -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'

View file

@ -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')
}
})
}
}

View file

@ -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(),

View file

@ -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

View file

@ -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

View file

@ -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>

View file

@ -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">

View file

@ -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} />

View 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