diff --git a/app/assets/stylesheets/notifications.scss.erb b/app/assets/stylesheets/notifications.scss.erb index 95524caa..036652d3 100644 --- a/app/assets/stylesheets/notifications.scss.erb +++ b/app/assets/stylesheets/notifications.scss.erb @@ -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; + } +} diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb index 6d75d24e..6f557428 100644 --- a/app/controllers/notifications_controller.rb +++ b/app/controllers/notifications_controller.rb @@ -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 diff --git a/app/decorators/notification_decorator.rb b/app/decorators/notification_decorator.rb new file mode 100644 index 00000000..bec0685c --- /dev/null +++ b/app/decorators/notification_decorator.rb @@ -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 diff --git a/app/views/notifications/mark_read.js.erb b/app/views/notifications/mark_read.js.erb deleted file mode 100644 index cbf2cf13..00000000 --- a/app/views/notifications/mark_read.js.erb +++ /dev/null @@ -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) \ No newline at end of file diff --git a/app/views/notifications/mark_unread.js.erb b/app/views/notifications/mark_unread.js.erb deleted file mode 100644 index 3fffab24..00000000 --- a/app/views/notifications/mark_unread.js.erb +++ /dev/null @@ -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) \ No newline at end of file diff --git a/config/application.rb b/config/application.rb index ff5a621c..8731e76a 100644 --- a/config/application.rb +++ b/config/application.rb @@ -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' diff --git a/frontend/src/Metamaps/GlobalUI/Notifications.js b/frontend/src/Metamaps/GlobalUI/Notifications.js index 1ca9e36a..3f0e2f92 100644 --- a/frontend/src/Metamaps/GlobalUI/Notifications.js +++ b/frontend/src/Metamaps/GlobalUI/Notifications.js @@ -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') + } + }) } } diff --git a/frontend/src/Metamaps/GlobalUI/ReactApp.js b/frontend/src/Metamaps/GlobalUI/ReactApp.js index 8cc85840..caa483d2 100644 --- a/frontend/src/Metamaps/GlobalUI/ReactApp.js +++ b/frontend/src/Metamaps/GlobalUI/ReactApp.js @@ -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(), diff --git a/frontend/src/Metamaps/GlobalUI/index.js b/frontend/src/Metamaps/GlobalUI/index.js index b2dd6654..858ef902 100644 --- a/frontend/src/Metamaps/GlobalUI/index.js +++ b/frontend/src/Metamaps/GlobalUI/index.js @@ -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 diff --git a/frontend/src/Metamaps/index.js b/frontend/src/Metamaps/index.js index 12fcbe60..747cb643 100644 --- a/frontend/src/Metamaps/index.js +++ b/frontend/src/Metamaps/index.js @@ -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 diff --git a/frontend/src/components/App/NotificationBox.js b/frontend/src/components/App/NotificationBox.js index 675a2a3b..e5b5daf7 100644 --- a/frontend/src/components/App/NotificationBox.js +++ b/frontend/src/components/App/NotificationBox.js @@ -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
-
diff --git a/frontend/src/components/App/UpperRightUI.js b/frontend/src/components/App/UpperRightUI.js index a1fa0d32..08276c82 100644 --- a/frontend/src/components/App/UpperRightUI.js +++ b/frontend/src/components/App/UpperRightUI.js @@ -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
{currentUser && @@ -62,6 +65,8 @@ class UpperRightUI extends Component { {notificationsBoxOpen && } } {!signInPage &&
diff --git a/frontend/src/components/App/index.js b/frontend/src/components/App/index.js index 207da1cd..9d4d3ee0 100644 --- a/frontend/src/components/App/index.js +++ b/frontend/src/components/App/index.js @@ -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'} />} diff --git a/frontend/src/components/Notification.js b/frontend/src/components/Notification.js new file mode 100644 index 00000000..40212f9a --- /dev/null +++ b/frontend/src/components/Notification.js @@ -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
  • + +
    + +
    +
    +
    {notification.actor.name}
    + Other content +
    +
    +
    + {!notification.is_read &&
    + mark read +
    } + {notification.is_read &&
    + mark unread +
    } +
    +
    + {this.getDate()} +
    +
    +
  • + } +} + +export default Notification