diff --git a/README.md b/README.md index 4906a5f1..c84c65d3 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/Metamaps/GlobalUI/Notifications.js b/src/Metamaps/GlobalUI/Notifications.js index 29908695..b907d00c 100644 --- a/src/Metamaps/GlobalUI/Notifications.js +++ b/src/Metamaps/GlobalUI/Notifications.js @@ -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') } }) } diff --git a/src/Metamaps/GlobalUI/ReactApp.js b/src/Metamaps/GlobalUI/ReactApp.js index 1b3d858f..b3dc3697 100644 --- a/src/Metamaps/GlobalUI/ReactApp.js +++ b/src/Metamaps/GlobalUI/ReactApp.js @@ -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) }, diff --git a/src/components/Notification.js b/src/components/Notification.js index 22237aaa..a41bd307 100644 --- a/src/components/Notification.js +++ b/src/components/Notification.js @@ -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 = `
${notification.actor.name}
` - - switch (notification.type) { - case 'ACCESS_APPROVED': - map = notification.data.map - result += outdent`granted your request to edit map - ${map.name}` - break - case 'ACCESS_REQUEST': - map = notification.data.map - result += outdent`wants permission to map with you on - ${map.name}` - if (!notification.data.object.answered) { - result += '
Offer a response
' - } - break - case 'INVITE_TO_EDIT': - map = notification.data.map - result += outdent`gave you edit access to map - ${map.name}` - break - case 'TOPIC_ADDED_TO_MAP': - map = notification.data.map - topic = notification.data.topic - result += outdent`added topic ${topic.name} - to map ${map.name}` - break - case 'TOPIC_CONNECTED_1': - topic1 = notification.data.topic1 - topic2 = notification.data.topic2 - result += outdent`connected ${topic1.name} - to ${topic2.name}` - break - case 'TOPIC_CONNECTED_2': - topic1 = notification.data.topic1 - topic2 = notification.data.topic2 - result += outdent`connected ${topic2.name} - to ${topic1.name}` - 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
  • - +
    -
    - + +
    {!notification.is_read &&
    mark read @@ -117,7 +64,7 @@ class Notification extends Component {
    }
    - {this.getDate()} +
  • diff --git a/src/components/NotificationBody.js b/src/components/NotificationBody.js new file mode 100644 index 00000000..0fb17735 --- /dev/null +++ b/src/components/NotificationBody.js @@ -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 = `
    ${notification.actor.name}
    ` + + switch (notification.type) { + case MAP_ACCESS_APPROVED: + map = notification.data.map + result += outdent`granted your request to edit map + ${map.name}` + break + case MAP_ACCESS_REQUEST: + map = notification.data.map + result += outdent`wants permission to map with you on + ${map.name}` + if (!notification.data.object.answered) { + result += '
    Offer a response
    ' + } + break + case MAP_INVITE_TO_EDIT: + map = notification.data.map + result += outdent`gave you edit access to map + ${map.name}` + break + case TOPIC_ADDED_TO_MAP: + map = notification.data.map + topic = notification.data.topic + result += outdent`added topic ${topic.name} + to map ${map.name}` + break + case TOPIC_CONNECTED_1: + topic1 = notification.data.topic1 + topic2 = notification.data.topic2 + result += outdent`connected ${topic1.name} + to ${topic2.name}` + break + case TOPIC_CONNECTED_2: + topic1 = notification.data.topic1 + topic2 = notification.data.topic2 + result += outdent`connected ${topic2.name} + to ${topic1.name}` + break + case MESSAGE_FROM_DEVS: + result += notification.subject + } + return {__html: result} + } + + render = () => { + return ( +
    + ) + } +} + +export default NotificationBody \ No newline at end of file diff --git a/src/components/NotificationBox.js b/src/components/NotificationBox.js index 0c144b4e..b2315cee 100644 --- a/src/components/NotificationBox.js +++ b/src/components/NotificationBox.js @@ -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 => + key={`notification-${n.id}`} + onClick={() => this.props.toggleNotificationsBox()} /> ).concat([
  • - + this.props.toggleNotificationsBox()}> See all
  • diff --git a/src/components/NotificationDate.js b/src/components/NotificationDate.js new file mode 100644 index 00000000..b84adf7a --- /dev/null +++ b/src/components/NotificationDate.js @@ -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 {months[created.getMonth()]} {created.getDate()} + } +} + +export default NotificationDate \ No newline at end of file diff --git a/src/constants.js b/src/constants.js new file mode 100644 index 00000000..ead8b055 --- /dev/null +++ b/src/constants.js @@ -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' \ No newline at end of file diff --git a/src/routes/Notifications/NotificationPage.js b/src/routes/Notifications/NotificationPage.js index f55aed3f..d35c6417 100644 --- a/src/routes/Notifications/NotificationPage.js +++ b/src/routes/Notifications/NotificationPage.js @@ -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 + const id = parseInt(this.props.params.id, 10) + const notification = this.props.notifications.find(n => n.id === id) + if (!notification) { + return ( +
    +
    +
    + +
    +
    + +
    + ) + } + const request = notification.data.object + const map = notification.data.map + const subject = notification.type === MAP_ACCESS_REQUEST ? + ({request.user.name} wants to collaborate on map { map.name }) + : notification.subject + return ( +
    +
    +
    + Back to notifications +
    +
    +

    + + {subject} +

    + {notification.type === MAP_ACCESS_REQUEST &&
    +

    + {request.answered && + {request.approved && You already responded to this access request, and allowed access.} + {!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.} + } + {!request.answered && + + Allow + Decline + } +

    + Go to map +    + View mapper profile +
    } + {notification.type !== MAP_ACCESS_REQUEST && } +
    +
    + +
    + ) } } +export default NotificationPage -export default NotificationPage \ No newline at end of file +/* + + */ \ No newline at end of file diff --git a/src/routes/Notifications/Notifications.js b/src/routes/Notifications/Notifications.js index 828de54a..4a8f71e0 100644 --- a/src/routes/Notifications/Notifications.js +++ b/src/routes/Notifications/Notifications.js @@ -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 ( - granted your request to edit map {map.name} - ) - case 'ACCESS_REQUEST': - map = notification.data.map - return ( - - wants permission to map with you on {map.name} - {!notification.data.object.answered &&   
    Offer a response
    } -
    - ) - case 'INVITE_TO_EDIT': - map = notification.data.map - return ( - - gave you edit access to map {map.name} - - ) - case 'TOPIC_ADDED_TO_MAP': - topic = notification.data.topic - map = notification.data.map - return ( - - added topic {topic.name} to map {map.name} - - ) - case 'TOPIC_CONNECTED_1': - topic1 = notification.data.topic1 - topic2 = notification.data.topic2 - return ( - - connected {topic1.name} to {topic2.name} - - ) - case 'TOPIC_CONNECTED_2': - topic1 = notification.data.topic1 - topic2 = notification.data.topic2 - return ( - - connected {topic2.name} to {topic1.name} - - ) - case 'MESSAGE_FROM_DEVS': - return ( - - {notification.subject} - - ) - 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 (
    @@ -83,11 +36,12 @@ class Notifications extends Component {
      {notifications.map(n => { - // TODO: const receipt = this.props.receipts.find(n => n.notification_id === notification.id) - const receipt = { - is_read: false - } - return + return ( + + ) })} {notifications.length === 0 &&
      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 ( -
    • - -
      - -
      -
      -
      {notification.actor.name}
      - {getNotificationText(notification)} -
      - - -
      - {notification.created_at} -
      -
      -
    • - ) - } -} - export default Notifications diff --git a/views/notifications/show.js b/views/notifications/show.js deleted file mode 100644 index d825d9a6..00000000 --- a/views/notifications/show.js +++ /dev/null @@ -1,59 +0,0 @@ -import React, { Component } from react - -class MyComponent extends Component { - render = () => { - return ( -
      -
      - { link_to 'Back to notifications', notifications_path } -
      -
      -

      - { 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' } { request.user.name } wants to collaborate on map { map.name } - { else } - { @notification.subject } - { end } -

      - { case @notification.notification_code - when MAP_ACCESS_REQUEST } -
      -

      - { 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' } - - { end } -

      - { link_to 'Go to map', map_url(map) } -   { link_to 'View mapper profile', explore_path(id: request.user.id) } -
      - { else } -
      - { raw @notification.body } -
      - { end } -
      -
      - ) - } -} - -export default MyComponent