notifications

This commit is contained in:
Connor Turland 2018-03-07 15:35:27 -05:00
parent cba83c0011
commit 10359a1f53
11 changed files with 232 additions and 228 deletions

View file

@ -26,6 +26,9 @@ Checklist
- [x] Fix images referenced in the JS
- [x] Figure out how authentication of requests from the frontend to the API works
- [x] Figure out how to combine the nodejs realtime server into server.js
- [ ] Notifications: make sure loading states are working for popup and page
- [ ] Notifications: make sure notifications either look nice, or redirect
- [ ] Notifications: pagination
- [ ] Get actioncable working
- [ ] Request unreadNotificationCount
- [ ] Request invite code
@ -34,8 +37,8 @@ Checklist
- [ ] Fix Request An Invite page
- [ ] Make 'new map' action work
- [ ] Modify the remaining rails templates into JSX templates
- [ ] notifications list
- [ ] notification page
- [x] notifications list
- [x] notification page
- [ ] list metacodes
- [ ] new metacode
- [ ] edit metacode

View file

@ -1,13 +1,14 @@
/* global $ */
import { findIndex } from 'lodash'
import GlobalUI from './index'
const Notifications = {
notifications: null,
notifications: [],
unreadNotificationsCount: 0,
init: serverData => {
Notifications.unreadNotificationsCount = serverData.unreadNotificationsCount
},
fetch: render => {
fetchNotifications: render => {
$.ajax({
url: '/notifications.json',
success: function(data) {
@ -16,6 +17,25 @@ const Notifications = {
}
})
},
fetchNotification: (render, id) => {
$.ajax({
url: `/notifications/${id}.json`,
success: function(data) {
const index = findIndex(Notifications.notifications, n => n.id === data.id)
if (index === -1) {
// notification not loaded yet, insert it at the start
Notifications.notifications.unshift(data)
} else {
// notification there, replace it
Notifications.notifications[index] = data
}
render()
},
error: function() {
GlobalUI.notifyUser('There was an error fetching that notification')
}
})
},
incrementUnread: (render) => {
Notifications.unreadNotificationsCount++
render()
@ -54,7 +74,7 @@ const Notifications = {
}
},
error: function() {
GlobalUI.notifyUser('There was an error marking that notification as read')
GlobalUI.notifyUser('There was an error marking that notification as unread')
}
})
}

View file

@ -108,7 +108,8 @@ const ReactApp = {
openInviteLightbox: () => self.openLightbox('invite'),
serverData: self.serverData,
notifications: Notifications.notifications,
fetchNotifications: apply(Notifications.fetch, ReactApp.render),
fetchNotifications: apply(Notifications.fetchNotifications, ReactApp.render),
fetchNotification: apply(Notifications.fetchNotification, ReactApp.render),
markAsRead: apply(Notifications.markAsRead, ReactApp.render),
markAsUnread: apply(Notifications.markAsUnread, ReactApp.render)
},

View file

@ -1,6 +1,9 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import outdent from 'outdent'
import { Link } from 'react-router'
import NotificationDate from './NotificationDate'
import NotificationBody from './NotificationBody'
class Notification extends Component {
static propTypes = {
@ -26,62 +29,6 @@ class Notification extends Component {
})
}
notificationTextHtml = () => {
const { notification } = this.props
let map, topic, topic1, topic2
let result = `<div class='in-bold'>${notification.actor.name}</div>`
switch (notification.type) {
case 'ACCESS_APPROVED':
map = notification.data.map
result += outdent`granted your request to edit map
<span class='in-bold'>${map.name}</span>`
break
case 'ACCESS_REQUEST':
map = notification.data.map
result += outdent`wants permission to map with you on
<span class='in-bold'>${map.name}</span>`
if (!notification.data.object.answered) {
result += '<br /><div class="action">Offer a response</div>'
}
break
case 'INVITE_TO_EDIT':
map = notification.data.map
result += outdent`gave you edit access to map
<span class='in-bold'>${map.name}</span>`
break
case 'TOPIC_ADDED_TO_MAP':
map = notification.data.map
topic = notification.data.topic
result += outdent`added topic <span class='in-bold'>${topic.name}</span>
to map <span class='in-bold'>${map.name}</span>`
break
case 'TOPIC_CONNECTED_1':
topic1 = notification.data.topic1
topic2 = notification.data.topic2
result += outdent`connected <span class='in-bold'>${topic1.name}</span>
to <span class='in-bold'>${topic2.name}</span>`
break
case 'TOPIC_CONNECTED_2':
topic1 = notification.data.topic1
topic2 = notification.data.topic2
result += outdent`connected <span class='in-bold'>${topic2.name}</span>
to <span class='in-bold'>${topic1.name}</span>`
break
case 'MESSAGE_FROM_DEVS':
result += notification.subject
}
return {__html: result}
}
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)
@ -95,19 +42,19 @@ class Notification extends Component {
render = () => {
const { notification } = this.props
const classes = `notification ${notification.is_read ? 'read' : 'unread'}`
const onClick = this.props.onClick || function() {}
if (!notification.data.object) {
return null
}
return <li className={classes}>
<a href={`/notifications/${notification.id}`}>
<Link to={`/notifications/${notification.id}`} onClick={onClick}>
<div className='notification-actor'>
<img src={notification.actor.image} />
</div>
<div className='notification-body'
dangerouslySetInnerHTML={this.notificationTextHtml()} />
</a>
<NotificationBody notification={notification} />
</Link>
<div className='notification-read-unread'>
{!notification.is_read && <div onClick={this.markAsRead}>
mark read
@ -117,7 +64,7 @@ class Notification extends Component {
</div>}
</div>
<div className='notification-date'>
{this.getDate()}
<NotificationDate date={notification.created_at} />
</div>
<div className='clearfloat'></div>
</li>

View file

@ -0,0 +1,70 @@
import React, { Component } from 'react'
import outdent from 'outdent'
import {
MAP_ACCESS_REQUEST,
MAP_ACCESS_APPROVED,
MAP_INVITE_TO_EDIT,
TOPIC_ADDED_TO_MAP,
TOPIC_CONNECTED_1,
TOPIC_CONNECTED_2,
MESSAGE_FROM_DEVS
} from '../constants'
class NotificationBody extends Component {
notificationTextHtml = () => {
const { notification } = this.props
let map, topic, topic1, topic2
let result = `<div class='in-bold'>${notification.actor.name}</div>`
switch (notification.type) {
case MAP_ACCESS_APPROVED:
map = notification.data.map
result += outdent`granted your request to edit map
<span class='in-bold'>${map.name}</span>`
break
case MAP_ACCESS_REQUEST:
map = notification.data.map
result += outdent`wants permission to map with you on
<span class='in-bold'>${map.name}</span>`
if (!notification.data.object.answered) {
result += '<br /><div class="action">Offer a response</div>'
}
break
case MAP_INVITE_TO_EDIT:
map = notification.data.map
result += outdent`gave you edit access to map
<span class='in-bold'>${map.name}</span>`
break
case TOPIC_ADDED_TO_MAP:
map = notification.data.map
topic = notification.data.topic
result += outdent`added topic <span class='in-bold'>${topic.name}</span>
to map <span class='in-bold'>${map.name}</span>`
break
case TOPIC_CONNECTED_1:
topic1 = notification.data.topic1
topic2 = notification.data.topic2
result += outdent`connected <span class='in-bold'>${topic1.name}</span>
to <span class='in-bold'>${topic2.name}</span>`
break
case TOPIC_CONNECTED_2:
topic1 = notification.data.topic1
topic2 = notification.data.topic2
result += outdent`connected <span class='in-bold'>${topic2.name}</span>
to <span class='in-bold'>${topic1.name}</span>`
break
case MESSAGE_FROM_DEVS:
result += notification.subject
}
return {__html: result}
}
render = () => {
return (
<div className='notification-body' dangerouslySetInnerHTML={this.notificationTextHtml()} />
)
}
}
export default NotificationBody

View file

@ -16,10 +16,7 @@ class NotificationBox extends Component {
}
componentDidMount = () => {
const { notifications, fetchNotifications } = this.props
if (!notifications) {
fetchNotifications()
}
this.props.fetchNotifications()
}
handleClickOutside = () => {
@ -28,7 +25,7 @@ class NotificationBox extends Component {
hasSomeNotifications = () => {
const { notifications } = this.props
return notifications && notifications.length > 0
return notifications.length > 0
}
showLoading = () => {
@ -51,10 +48,11 @@ class NotificationBox extends Component {
n => <Notification notification={n}
markAsRead={markAsRead}
markAsUnread={markAsUnread}
key={`notification-${n.id}`} />
key={`notification-${n.id}`}
onClick={() => this.props.toggleNotificationsBox()} />
).concat([
<li key='notification-see-all'>
<Link to='/notifications' className='notificationsBoxSeeAll'>
<Link to='/notifications' className='notificationsBoxSeeAll' onClick={() => this.props.toggleNotificationsBox()}>
See all
</Link>
</li>

View file

@ -0,0 +1,13 @@
import React, { Component } from 'react'
class NotificationDate extends Component {
render = () => {
const { date } = this.props
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
const created = new Date(date)
return <span>{months[created.getMonth()]} {created.getDate()}</span>
}
}
export default NotificationDate

7
src/constants.js Normal file
View file

@ -0,0 +1,7 @@
export const MAP_ACCESS_REQUEST = 'ACCESS_REQUEST'
export const MAP_ACCESS_APPROVED = 'ACCESS_APPROVED'
export const MAP_INVITE_TO_EDIT = 'INVITE_TO_EDIT'
export const TOPIC_ADDED_TO_MAP = 'TOPIC_ADDED_TO_MAP'
export const TOPIC_CONNECTED_1 = 'TOPIC_CONNECTED_1'
export const TOPIC_CONNECTED_2 = 'TOPIC_CONNECTED_2'
export const MESSAGE_FROM_DEVS = 'MESSAGE_FROM_DEVS'

View file

@ -1,13 +1,89 @@
import React, { Component } from 'react'
import { Link } from 'react-router'
import { MAP_ACCESS_REQUEST } from '../../constants'
import NotificationsHeader from './NotificationsHeader'
import Loading from '../../components/Loading'
import NotificationBody from '../../components/NotificationBody'
/* TODO:
allow / decline access loading states
make backend serve HTML for raw body too
*/
class NotificationPage extends Component {
componentDidMount() {
// the notification id
const id = parseInt(this.props.params.id, 10)
if (!this.props.notifications.find(n => n.id === id)) {
this.props.fetchNotification(id)
}
}
render = () => {
return <NotificationsHeader />
const id = parseInt(this.props.params.id, 10)
const notification = this.props.notifications.find(n => n.id === id)
if (!notification) {
return (
<div>
<div id="yield">
<div className="centerContent withPadding back">
<Loading />
</div>
</div>
<NotificationsHeader />
</div>
)
}
const request = notification.data.object
const map = notification.data.map
const subject = notification.type === MAP_ACCESS_REQUEST ?
(<span><span style={{ fontWeight: 'bold' }} className='requesterName'>{request.user.name}</span> wants to collaborate on map <span style={{fontWeight: 'bold'}}>{ map.name }</span></span>)
: notification.subject
return (
<div>
<div id="yield">
<div className="centerContent withPadding back">
<Link to="/notifications">Back to notifications</Link>
</div>
<div className="centerContent notificationPage">
<h2 className="notification-title">
<img width="32" height="32" src={notification.actor.image} className='thirty-two-avatar' />
{subject}
</h2>
{notification.type === MAP_ACCESS_REQUEST && <div className="notification-body">
<p className="main-text">
{request.answered && <span>
{request.approved && <span>You already responded to this access request, and allowed access.</span>}
{!request.approved && <span>You already responded to this access request, and declined access. If you changed your mind, you can still grant
them access by going to the map and adding them as a collaborator.</span>}
</span>}
{!request.answered && <span>
<img src='/images/ellipsis.gif' className='hidden' />
<a className="button allow" data-remote="true" rel="nofollow" data-method="post" href={`/maps/${map.id}/approve_access/${request.id}`}>Allow</a>
<a className="button decline" data-remote="true" rel="nofollow" data-method="post" href={`/maps/${map.id}/deny_access/${request.id}`}>Decline</a>
</span>}
</p>
<Link to={`/maps/${map.id}`}>Go to map</Link>
&nbsp;&nbsp;
<Link to={`/explore/mapper/${request.user.id}`}>View mapper profile</Link>
</div>}
{notification.type !== MAP_ACCESS_REQUEST && <NotificationBody notification={notification} />}
</div>
</div>
<NotificationsHeader />
</div>
)
}
}
export default NotificationPage
export default NotificationPage
/*
<script>
$(document).ready(function() {
$('.notification-body .button').click(function() {
$(this).html('<img src="{ asset_path('ellipsis.gif') }" />')
})
})
</script>
*/

View file

@ -1,78 +1,31 @@
import React, { Component } from 'react'
import { Link } from 'react-router'
import Notification from '../../components/Notification'
import {
MAP_ACCESS_REQUEST,
MAP_ACCESS_APPROVED,
MAP_INVITE_TO_EDIT
} from '../../constants'
import NotificationsHeader from './NotificationsHeader'
// these come from mailboxer.rb in the api repo
const BLACKLIST = ['ACCESS_REQUEST', 'ACCESS_APPROVED', 'INVITE_TO_EDIT']
const BLACKLIST = [MAP_ACCESS_REQUEST, MAP_ACCESS_APPROVED, MAP_INVITE_TO_EDIT]
/* TODO!!
pagination
mark read/unread
receipts
fetchNotifications
*/
function getNotificationText (notification) {
let map, topic, topic1, topic2
switch (notification.type) {
case 'ACCESS_APPROVED':
map = notification.data.map
return (
<span>granted your request to edit map <span className="in-bold">{map.name}</span></span>
)
case 'ACCESS_REQUEST':
map = notification.data.map
return (
<span>
wants permission to map with you on <span className="in-bold">{map.name}</span>
{!notification.data.object.answered && <span>&nbsp;&nbsp;<div className="action">Offer a response</div></span>}
</span>
)
case 'INVITE_TO_EDIT':
map = notification.data.map
return (
<span>
gave you edit access to map <span className="in-bold">{map.name}</span>
</span>
)
case 'TOPIC_ADDED_TO_MAP':
topic = notification.data.topic
map = notification.data.map
return (
<span>
added topic <span className="in-bold">{topic.name}</span> to map <span className="in-bold">{map.name}</span>
</span>
)
case 'TOPIC_CONNECTED_1':
topic1 = notification.data.topic1
topic2 = notification.data.topic2
return (
<span>
connected <span className="in-bold">{topic1.name}</span> to <span className="in-bold">{topic2.name}</span>
</span>
)
case 'TOPIC_CONNECTED_2':
topic1 = notification.data.topic1
topic2 = notification.data.topic2
return (
<span>
connected <span className="in-bold">{topic2.name}</span> to <span className="in-bold">{topic1.name}</span>
</span>
)
case 'MESSAGE_FROM_DEVS':
return (
<span>
{notification.subject}
</span>
)
default:
return null
}
}
class Notifications extends Component {
componentDidMount = () => {
this.props.fetchNotifications()
}
render = () => {
const { markAsRead, markAsUnread } = this.props
const notifications = (this.props.notifications || []).filter(n => !(BLACKLIST.indexOf(n.type) > -1 && (!n.data.object || !n.data.map)))
return (
<div>
@ -83,11 +36,12 @@ class Notifications extends Component {
</header>
<ul className="notifications">
{notifications.map(n => {
// TODO: const receipt = this.props.receipts.find(n => n.notification_id === notification.id)
const receipt = {
is_read: false
}
return <Notification key={n.id} notification={n} receipt={receipt} />
return (
<Notification key={`notification-${n.id}`}
notification={n}
markAsRead={markAsRead}
markAsUnread={markAsUnread} />
)
})}
{notifications.length === 0 && <div className="emptyInbox">
You have no notifications. More time for dancing.
@ -110,30 +64,4 @@ class Paginate extends Component {
}
}
class Notification extends Component {
render = () => {
const { notification, receipt } = this.props
return (
<li className={`notification ${receipt.is_read ? 'read' : 'unread' }`} id={`notification-${ notification.id }`}>
<Link to={`/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>
{getNotificationText(notification)}
</div>
</Link>
<div className="notification-read-unread">
<a data-remote="true" rel="nofollow" data-method="put" href={`/notifications/${notification.id}/mark_${receipt.is_read ? 'un' : ''}read`}>mark as {receipt.is_read ? 'un' : ''}read</a>
</div>
<div className="notification-date">
{notification.created_at}
</div>
<div className="clearfloat"></div>
</li>
)
}
}
export default Notifications

View file

@ -1,59 +0,0 @@
import React, { Component } from react
class MyComponent extends Component {
render = () => {
return (
<div id="yield">
<div className="centerContent withPadding back">
{ link_to 'Back to notifications', notifications_path }
</div>
<div className="centerContent notificationPage">
<h2 className="notification-title">
{ case @notification.notification_code
when MAP_ACCESS_REQUEST
request = @notification.notified_object
map = request.map }
{ image_tag @notification.sender.image(:thirtytwo), className: 'thirty-two-avatar' } <span style='font-weight:bold;' className='requesterName'>{ request.user.name }</span> wants to collaborate on map <span style='font-weight:bold;'>{ map.name }</span>
{ else }
{ @notification.subject }
{ end }
</h2>
{ case @notification.notification_code
when MAP_ACCESS_REQUEST }
<div className="notification-body">
<p className="main-text">
{ if false && request.answered }
{ if request.approved }
You already responded to this access request, and allowed access.
{ elsif !request.approved }
You already responded to this access request, and declined access. If you changed your mind, you can still grant
them access by going to the map and adding them as a collaborator.
{ end }
{ else }
{ image_tag asset_path('ellipsis.gif'), className: 'hidden' }
{ link_to 'Allow', approve_access_post_map_path(id: map.id, request_id: request.id), remote: true, method: :post, className: 'button allow' }
{ link_to 'Decline', deny_access_post_map_path(id: map.id, request_id: request.id), remote: true, method: :post, className: 'button decline' }
<script>
$(document).ready(function() {
$('.notification-body .button').click(function() {
$(this).html('<img src="{ asset_path('ellipsis.gif') }" />')
})
})
</script>
{ end }
</p>
{ link_to 'Go to map', map_url(map) }
&nbsp;&nbsp;{ link_to 'View mapper profile', explore_path(id: request.user.id) }
</div>
{ else }
<div className="notification-body">
{ raw @notification.body }
</div>
{ end }
</div>
</div>
)
}
}
export default MyComponent