From 64ffc78f45e334dce9026c701e210cb31075b396 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Tue, 19 Sep 2017 23:48:46 -0400 Subject: [PATCH 01/10] add the container for the notifications dropdown --- app/assets/stylesheets/notifications.scss.erb | 55 ++++++++++++++++--- app/controllers/notifications_controller.rb | 9 ++- .../src/Metamaps/GlobalUI/Notifications.js | 14 +++++ frontend/src/Metamaps/GlobalUI/ReactApp.js | 5 +- .../src/components/App/NotificationBox.js | 41 ++++++++++++++ .../src/components/App/NotificationIcon.js | 16 ++---- frontend/src/components/App/UpperRightUI.js | 38 +++++++++++-- frontend/src/components/App/index.js | 6 +- 8 files changed, 156 insertions(+), 28 deletions(-) create mode 100644 frontend/src/Metamaps/GlobalUI/Notifications.js create mode 100644 frontend/src/components/App/NotificationBox.js diff --git a/app/assets/stylesheets/notifications.scss.erb b/app/assets/stylesheets/notifications.scss.erb index 2f750f90..95524caa 100644 --- a/app/assets/stylesheets/notifications.scss.erb +++ b/app/assets/stylesheets/notifications.scss.erb @@ -13,6 +13,45 @@ $unread_notifications_dot_size: 8px; .notificationsIcon { position: relative; } + + .notificationsBox { + position: absolute; + background: #FFFFFF; + border-radius: 2px; + width: 350px; + right: 0; + top: 50px; + box-shadow: 0 3px 6px rgba(0,0,0,0.16); + + .notificationsBoxTriangle { + min-width: 0 !important; + display: block; + position: absolute; + right: 48px; + width: 20px !important; + height: 20px !important; + margin-left: -10px; + top: -10px; + border-top: 1px solid rgba(49, 72, 67, 0.11) !important; + border-left: 1px solid rgba(49, 72, 67, 0.11) !important; + border-bottom: 0 !important; + border-right: 0 !important; + background-color: #fff; + transform: rotate(45deg); + -webkit-transform: rotate(45deg); + -ms-transform: rotate(45deg); + } + + .notificationsBoxSeeAll { + display: block; + width: 100%; + text-align: center; + padding: 6px 0; + font-family: din-regular, helvetica, sans-serif; + color: #4fb5c0; + border-top: 1px solid rgba(0, 0, 0, 0.1); + } + } } .controller-notifications { @@ -66,12 +105,12 @@ $unread_notifications_dot_size: 8px; & > a { float: left; - width: 85%; + width: 85%; box-sizing: border-box; padding-right: 10px; } - .notification-actor { + .notification-actor { float: left; img { @@ -97,7 +136,7 @@ $unread_notifications_dot_size: 8px; display: inline-block; margin: 5px 0; } - } + } .notification-date { position: absolute; @@ -129,7 +168,7 @@ $unread_notifications_dot_size: 8px; } - + .notificationPage { .thirty-two-avatar { @@ -139,14 +178,14 @@ $unread_notifications_dot_size: 8px; border-radius: 16px; vertical-align: middle; } - + .button { line-height: 32px; - + img { margin-top: 8px; } - + &.decline { background: #DB5D5D; &:hover { @@ -154,7 +193,7 @@ $unread_notifications_dot_size: 8px; } } } - + .notification-body { p, div { margin: 1em auto; diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb index 210c0b43..6d75d24e 100644 --- a/app/controllers/notifications_controller.rb +++ b/app/controllers/notifications_controller.rb @@ -10,10 +10,15 @@ class NotificationsController < ApplicationController respond_to do |format| format.html format.json do - render json: @notifications.map do |notification| + notifications = @notifications.map do |notification| receipt = @receipts.find_by(notification_id: notification.id) - notification.as_json.merge(is_read: receipt.is_read) + notification.as_json.merge( + is_read: receipt.is_read, + notified_object: notification.notified_object, + sender: notification.sender + ) end + render json: notifications end end end diff --git a/frontend/src/Metamaps/GlobalUI/Notifications.js b/frontend/src/Metamaps/GlobalUI/Notifications.js new file mode 100644 index 00000000..1ca9e36a --- /dev/null +++ b/frontend/src/Metamaps/GlobalUI/Notifications.js @@ -0,0 +1,14 @@ +const Notifications = { + notifications: null, + fetch: render => { + $.ajax({ + url: '/notifications.json', + success: function(data) { + Notifications.notifications = data + render() + } + }) + } +} + +export default Notifications diff --git a/frontend/src/Metamaps/GlobalUI/ReactApp.js b/frontend/src/Metamaps/GlobalUI/ReactApp.js index 5cfbf255..8cc85840 100644 --- a/frontend/src/Metamaps/GlobalUI/ReactApp.js +++ b/frontend/src/Metamaps/GlobalUI/ReactApp.js @@ -8,6 +8,7 @@ import apply from 'async/apply' import { notifyUser } from './index.js' import ImportDialog from './ImportDialog' +import Notifications from './Notifications' import Active from '../Active' import DataModel from '../DataModel' import { ExploreMaps, ChatView, TopicCard, ContextMenu } from '../Views' @@ -107,7 +108,9 @@ const ReactApp = { mobileTitleWidth: self.mobileTitleWidth, mobileTitleClick: (e) => Active.Map && InfoBox.toggleBox(e), openInviteLightbox: () => self.openLightbox('invite'), - serverData: self.serverData + serverData: self.serverData, + notifications: Notifications.notifications, + fetchNotifications: apply(Notifications.fetch, ReactApp.render) }, self.getMapProps(), self.getTopicProps(), diff --git a/frontend/src/components/App/NotificationBox.js b/frontend/src/components/App/NotificationBox.js new file mode 100644 index 00000000..675a2a3b --- /dev/null +++ b/frontend/src/components/App/NotificationBox.js @@ -0,0 +1,41 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' + +import onClickOutsideAddon from 'react-onclickoutside' + +class NotificationBox extends Component { + + static propTypes = { + notifications: PropTypes.array, + fetchNotifications: PropTypes.func.isRequired, + toggleNotificationsBox: PropTypes.func.isRequired + } + + componentDidMount = () => { + const { notifications, fetchNotifications } = this.props + if (!notifications) { + fetchNotifications() + } + } + + handleClickOutside = () => { + this.props.toggleNotificationsBox() + } + + render = () => { + const { notifications } = this.props + return
+
+
    + {!notifications &&
  • loading...
  • } +
  • A notification
  • +
  • A notification
  • +
  • A notification
  • +
  • A notification
  • +
+ See all +
+ } +} + +export default onClickOutsideAddon(NotificationBox) diff --git a/frontend/src/components/App/NotificationIcon.js b/frontend/src/components/App/NotificationIcon.js index 36b86b72..b265cec0 100644 --- a/frontend/src/components/App/NotificationIcon.js +++ b/frontend/src/components/App/NotificationIcon.js @@ -4,18 +4,14 @@ import PropTypes from 'prop-types' class NotificationIcon extends Component { static propTypes = { - unreadNotificationsCount: PropTypes.number - } - - constructor(props) { - super(props) - - this.state = { - } + unreadNotificationsCount: PropTypes.number, + toggleNotificationsBox: PropTypes.func } render = () => { + const { toggleNotificationsBox } = this.props let linkClasses = 'notificationsIcon upperRightEl upperRightIcon ' + linkClasses += 'ignore-react-onclickoutside ' if (this.props.unreadNotificationsCount > 0) { linkClasses += 'unread' @@ -24,14 +20,14 @@ class NotificationIcon extends Component { } return ( - +
Notifications
{this.props.unreadNotificationsCount === 0 ? null : (
)} -
+
) } diff --git a/frontend/src/components/App/UpperRightUI.js b/frontend/src/components/App/UpperRightUI.js index d1e53652..a1fa0d32 100644 --- a/frontend/src/components/App/UpperRightUI.js +++ b/frontend/src/components/App/UpperRightUI.js @@ -4,31 +4,51 @@ import PropTypes from 'prop-types' import AccountMenu from './AccountMenu' import LoginForm from './LoginForm' import NotificationIcon from './NotificationIcon' +import NotificationBox from './NotificationBox' class UpperRightUI extends Component { static propTypes = { currentUser: PropTypes.object, signInPage: PropTypes.bool, unreadNotificationsCount: PropTypes.number, + fetchNotifications: PropTypes.func, + notifications: PropTypes.array, openInviteLightbox: PropTypes.func } constructor(props) { super(props) - this.state = {accountBoxOpen: false} + this.state = { + accountBoxOpen: false, + notificationsBoxOpen: false + } } reset = () => { - this.setState({accountBoxOpen: false}) + this.setState({ + accountBoxOpen: false, + notificationsBoxOpen: false + }) } toggleAccountBox = () => { - this.setState({accountBoxOpen: !this.state.accountBoxOpen}) + this.setState({ + accountBoxOpen: !this.state.accountBoxOpen, + notificationsBoxOpen: false + }) + } + + toggleNotificationsBox = () => { + this.setState({ + notificationsBoxOpen: !this.state.notificationsBoxOpen, + accountBoxOpen: false + }) } render () { - const { currentUser, signInPage, unreadNotificationsCount, openInviteLightbox } = this.props - const { accountBoxOpen } = this.state + const { currentUser, signInPage, unreadNotificationsCount, + notifications, fetchNotifications, openInviteLightbox } = this.props + const { accountBoxOpen, notificationsBoxOpen } = this.state return
{currentUser &&
@@ -36,7 +56,13 @@ class UpperRightUI extends Component {
} {currentUser && - + + {notificationsBoxOpen && } } {!signInPage &&
diff --git a/frontend/src/components/App/index.js b/frontend/src/components/App/index.js index ed24e9d1..207da1cd 100644 --- a/frontend/src/components/App/index.js +++ b/frontend/src/components/App/index.js @@ -11,6 +11,8 @@ class App extends Component { children: PropTypes.object, toast: PropTypes.string, unreadNotificationsCount: PropTypes.number, + notifications: PropTypes.array, + fetchNotifications: PropTypes.func, location: PropTypes.object, mobile: PropTypes.bool, mobileTitle: PropTypes.string, @@ -38,7 +40,7 @@ class App extends Component { const { children, toast, unreadNotificationsCount, openInviteLightbox, mobile, mobileTitle, mobileTitleWidth, mobileTitleClick, location, map, userRequested, requestAnswered, requestApproved, serverData, - onRequestAccess } = this.props + onRequestAccess, notifications, fetchNotifications } = 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 @@ -58,6 +60,8 @@ class App extends Component { onRequestClick={onRequestAccess} />} {!mobile && } From 9cc700c64d4d28938a2a9dcbcf316b3c1661c3f1 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Fri, 22 Sep 2017 18:38:38 -0400 Subject: [PATCH 02/10] use decorator pattern for notifs api --- app/assets/stylesheets/notifications.scss.erb | 168 +++++++++--------- app/controllers/notifications_controller.rb | 20 +-- app/decorators/notification_decorator.rb | 47 +++++ app/views/notifications/mark_read.js.erb | 7 - app/views/notifications/mark_unread.js.erb | 7 - config/application.rb | 2 +- .../src/Metamaps/GlobalUI/Notifications.js | 40 +++++ frontend/src/Metamaps/GlobalUI/ReactApp.js | 8 +- frontend/src/Metamaps/GlobalUI/index.js | 3 +- frontend/src/Metamaps/index.js | 3 +- .../src/components/App/NotificationBox.js | 19 +- frontend/src/components/App/UpperRightUI.js | 7 +- frontend/src/components/App/index.js | 7 +- frontend/src/components/Notification.js | 76 ++++++++ 14 files changed, 284 insertions(+), 130 deletions(-) create mode 100644 app/decorators/notification_decorator.rb delete mode 100644 app/views/notifications/mark_read.js.erb delete mode 100644 app/views/notifications/mark_unread.js.erb create mode 100644 frontend/src/components/Notification.js 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
-
    +
      {!notifications &&
    • loading...
    • } -
    • A notification
    • -
    • A notification
    • -
    • A notification
    • -
    • A notification
    • + {notifications && notifications.map(n => { + return + })}
    See all
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 From fc8ac6eef149db11a0182a45032686fe6974edb8 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sat, 23 Sep 2017 11:20:02 -0400 Subject: [PATCH 03/10] nest inconsistent data under data key --- app/decorators/notification_decorator.rb | 60 ++++++++++++------------ 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/app/decorators/notification_decorator.rb b/app/decorators/notification_decorator.rb index bec0685c..5669fb9c 100644 --- a/app/decorators/notification_decorator.rb +++ b/app/decorators/notification_decorator.rb @@ -8,38 +8,40 @@ class NotificationDecorator is_read: receipt.is_read, created_at: notification.created_at, actor: notification.sender, - object: notification.notified_object + data: { + 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 - } + when MAP_ACCESS_APPROVED, MAP_ACCESS_REQUEST, MAP_INVITE_TO_EDIT + map = notification.notified_object&.map + result[:data][:map] = { + id: map&.id, + name: map&.name + } + when TOPIC_ADDED_TO_MAP + topic = notification.notified_object&.eventable + map = notification.notified_object&.map + result[:data][:topic] = { + id: topic&.id, + name: topic&.name + } + result[:data][: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[:data][:topic1] = { + id: topic1&.id, + name: topic1&.name + } + resul[:data][:topic2] = { + id: topic2&.id, + name: topic2&.name + } end result end From 277644f59d223f6b4081598e3ee5753c7b19b02f Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Mon, 25 Sep 2017 15:21:04 -0400 Subject: [PATCH 04/10] improve styling, fix index notifs page --- app/assets/stylesheets/notifications.scss.erb | 38 ++++++++--- app/controllers/notifications_controller.rb | 2 + app/decorators/notification_decorator.rb | 2 +- app/views/notifications/index.html.erb | 2 +- app/views/notifications/mark_read.js.erb | 7 ++ app/views/notifications/mark_unread.js.erb | 7 ++ .../src/Metamaps/GlobalUI/Notifications.js | 14 +++- frontend/src/Metamaps/GlobalUI/index.js | 1 + .../src/components/App/NotificationBox.js | 2 +- frontend/src/components/Notification.js | 68 ++++++++++++++++--- 10 files changed, 121 insertions(+), 22 deletions(-) create mode 100644 app/views/notifications/mark_read.js.erb create mode 100644 app/views/notifications/mark_unread.js.erb diff --git a/app/assets/stylesheets/notifications.scss.erb b/app/assets/stylesheets/notifications.scss.erb index 036652d3..3168f591 100644 --- a/app/assets/stylesheets/notifications.scss.erb +++ b/app/assets/stylesheets/notifications.scss.erb @@ -1,4 +1,6 @@ +$notifications-border-color: #DDDDDD; $unread_notifications_dot_size: 8px; + .unread-notifications-dot { width: $unread_notifications_dot_size; height: $unread_notifications_dot_size; @@ -22,6 +24,7 @@ $unread_notifications_dot_size: 8px; right: 0; top: 50px; box-shadow: 0 3px 6px rgba(0,0,0,0.16); + border: 1px solid $notifications-border-color; .notificationsBoxTriangle { min-width: 0 !important; @@ -31,9 +34,9 @@ $unread_notifications_dot_size: 8px; width: 20px !important; height: 20px !important; margin-left: -10px; - top: -10px; - border-top: 1px solid rgba(49, 72, 67, 0.11) !important; - border-left: 1px solid rgba(49, 72, 67, 0.11) !important; + top: -11px; + border-left: 1px solid $notifications-border-color; + border-top: 1px solid $notifications-border-color; border-bottom: 0 !important; border-right: 0 !important; background-color: #fff; @@ -42,6 +45,11 @@ $unread_notifications_dot_size: 8px; -ms-transform: rotate(45deg); } + ul.notifications { + max-height: 500px; + overflow-y: auto; + } + .notificationsBoxSeeAll { display: block; width: 100%; @@ -51,6 +59,14 @@ $unread_notifications_dot_size: 8px; color: #4fb5c0; border-top: 1px solid rgba(0, 0, 0, 0.1); } + + .notification { + font-size: 13px; + + .notification-body { + border-bottom: 1px solid $notifications-border-color; + } + } } } @@ -121,11 +137,18 @@ $unread_notifications_dot_size: 8px; ul.notifications { list-style: none; + + li:last-child { + .notification-body { + border-bottom: none !important; + } + } } .notification { - padding: 10px; + padding: 10px 10px 0 10px; position: relative; + font-family: 'din-regular', Sans-Serif; &:hover { background: #F6F6F6; @@ -157,9 +180,9 @@ ul.notifications { } .notification-body { - font-family: 'din-regular', Sans-Serif; margin-left: 50px; line-height: 20px; + padding-bottom: 10px; .in-bold { font-family: 'din-medium', Sans-Serif; @@ -180,8 +203,6 @@ ul.notifications { top: 50%; right: 10px; color: #607d8b; - font-size: 13px; - line-height: 13px; margin-top: -6px; } @@ -190,11 +211,12 @@ ul.notifications { float: left; width: 15%; - div { + a, div { position: absolute; top: 50%; margin-top: -10px; text-align: center; + cursor: pointer; } } diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb index 6f557428..15781a32 100644 --- a/app/controllers/notifications_controller.rb +++ b/app/controllers/notifications_controller.rb @@ -43,6 +43,7 @@ class NotificationsController < ApplicationController def mark_read @receipt.update(is_read: true) respond_to do |format| + format.js format.json do render json: NotificationDecorator.decorate(@notification, @receipt) end @@ -52,6 +53,7 @@ class NotificationsController < ApplicationController def mark_unread @receipt.update(is_read: false) respond_to do |format| + format.js format.json do render json: NotificationDecorator.decorate(@notification, @receipt) end diff --git a/app/decorators/notification_decorator.rb b/app/decorators/notification_decorator.rb index 5669fb9c..a2f7584b 100644 --- a/app/decorators/notification_decorator.rb +++ b/app/decorators/notification_decorator.rb @@ -38,7 +38,7 @@ class NotificationDecorator id: topic1&.id, name: topic1&.name } - resul[:data][:topic2] = { + result[:data][:topic2] = { id: topic2&.id, name: topic2&.name } diff --git a/app/views/notifications/index.html.erb b/app/views/notifications/index.html.erb index ec9987f6..3efbcb45 100644 --- a/app/views/notifications/index.html.erb +++ b/app/views/notifications/index.html.erb @@ -8,7 +8,7 @@
      <% blacklist = [MAP_ACCESS_REQUEST, MAP_ACCESS_APPROVED, MAP_INVITE_TO_EDIT] %> - <% notifications = @notifications.to_a.delete_if{|n| blacklist.include?(n.notification_code) && (n.notified_object.nil? || n.notified_object.map.nil?) }%> + <% notifications = @notifications.to_a.delete_if{|n| blacklist.include?(n.notification_code) && (n.notified_object.nil? || n.notified_object.map.nil?) }%> <% notifications.each do |notification| %> <% receipt = @receipts.find_by(notification_id: notification.id) %>
    • diff --git a/app/views/notifications/mark_read.js.erb b/app/views/notifications/mark_read.js.erb new file mode 100644 index 00000000..17e418b8 --- /dev/null +++ b/app/views/notifications/mark_read.js.erb @@ -0,0 +1,7 @@ +$('#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.Notifications.decrementUnread(Metamaps.GlobalUI.ReactApp.render) diff --git a/app/views/notifications/mark_unread.js.erb b/app/views/notifications/mark_unread.js.erb new file mode 100644 index 00000000..873e7af8 --- /dev/null +++ b/app/views/notifications/mark_unread.js.erb @@ -0,0 +1,7 @@ +$('#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.Notifications.incrementUnread(Metamaps.GlobalUI.ReactApp.render) diff --git a/frontend/src/Metamaps/GlobalUI/Notifications.js b/frontend/src/Metamaps/GlobalUI/Notifications.js index 3f0e2f92..0d2e5271 100644 --- a/frontend/src/Metamaps/GlobalUI/Notifications.js +++ b/frontend/src/Metamaps/GlobalUI/Notifications.js @@ -15,14 +15,22 @@ const Notifications = { } }) }, + incrementUnread: (render) => { + Notifications.unreadNotificationsCount++ + render() + }, + decrementUnread: (render) => { + Notifications.unreadNotificationsCount-- + render() + }, markAsRead: (render, id) => { const n = Notifications.notifications.find(n => n.id === id) $.ajax({ - url: `/notifications/${id}/mark_read`, + url: `/notifications/${id}/mark_read.json`, method: 'PUT', success: function(r) { if (n) { - Notifications.unreadNotificationsCount-- + n.is_read = true render() } @@ -35,7 +43,7 @@ const Notifications = { markAsUnread: (render, id) => { const n = Notifications.notifications.find(n => n.id === id) $.ajax({ - url: `/notifications/${id}/mark_unread`, + url: `/notifications/${id}/mark_unread.json`, method: 'PUT', success: function() { if (n) { diff --git a/frontend/src/Metamaps/GlobalUI/index.js b/frontend/src/Metamaps/GlobalUI/index.js index 858ef902..7a7d2ae5 100644 --- a/frontend/src/Metamaps/GlobalUI/index.js +++ b/frontend/src/Metamaps/GlobalUI/index.js @@ -18,6 +18,7 @@ const GlobalUI = { init: function(serverData) { const self = GlobalUI + self.Notifications.init(serverData) self.ReactApp.init(serverData, self.openLightbox) self.CreateMap.init(serverData) self.ImportDialog.init(serverData, self.openLightbox, self.closeLightbox) diff --git a/frontend/src/components/App/NotificationBox.js b/frontend/src/components/App/NotificationBox.js index e5b5daf7..2f9591ff 100644 --- a/frontend/src/components/App/NotificationBox.js +++ b/frontend/src/components/App/NotificationBox.js @@ -31,7 +31,7 @@ class NotificationBox extends Component {
        {!notifications &&
      • loading...
      • } - {notifications && notifications.map(n => { + {notifications && notifications.slice(0, 10).map(n => { return { + 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'] + 'Jul','Aug','Sep','Oct','Nov','Dec'] const created = new Date(created_at) return `${months[created.getMonth()]} ${created.getDate()}` } @@ -47,17 +96,20 @@ class Notification extends Component { render = () => { const { notification } = this.props const classes = `notification ${notification.is_read ? 'read' : 'unread'}` + + if (!notification.data.object) { + return null + } + return
      • -
        -
        {notification.actor.name}
        - Other content -
        +
        -
        +
        {!notification.is_read &&
        mark read
        } @@ -65,10 +117,10 @@ class Notification extends Component { mark unread
        }
        -
        +
        {this.getDate()}
        -
        +
      • } } From 216a19476b16f91d4f8df6a14fb2d835a5fd670b Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Thu, 28 Sep 2017 12:28:33 -0400 Subject: [PATCH 05/10] add hover states and empty case --- app/assets/stylesheets/notifications.scss.erb | 35 ++++--- .../src/components/App/NotificationBox.js | 13 ++- frontend/src/components/Loading.js | 92 +++++++++++++++++++ 3 files changed, 126 insertions(+), 14 deletions(-) create mode 100644 frontend/src/components/Loading.js diff --git a/app/assets/stylesheets/notifications.scss.erb b/app/assets/stylesheets/notifications.scss.erb index 3168f591..f6d01dc8 100644 --- a/app/assets/stylesheets/notifications.scss.erb +++ b/app/assets/stylesheets/notifications.scss.erb @@ -1,4 +1,5 @@ $notifications-border-color: #DDDDDD; +$notifications-hover-color: #F6F6F6; $unread_notifications_dot_size: 8px; .unread-notifications-dot { @@ -48,6 +49,20 @@ $unread_notifications_dot_size: 8px; ul.notifications { max-height: 500px; overflow-y: auto; + + .notification { + font-size: 13px; + + .notification-body { + border-bottom: 1px solid $notifications-border-color; + } + } + + .notificationsEmpty { + font-family: din-regular, helvetica, sans-serif; + margin: 50px 10px; + text-align: center; + } } .notificationsBoxSeeAll { @@ -56,15 +71,11 @@ $unread_notifications_dot_size: 8px; text-align: center; padding: 6px 0; font-family: din-regular, helvetica, sans-serif; - color: #4fb5c0; border-top: 1px solid rgba(0, 0, 0, 0.1); - } - .notification { - font-size: 13px; - - .notification-body { - border-bottom: 1px solid $notifications-border-color; + &:hover { + color: #333; + background: $notifications-hover-color; } } } @@ -150,8 +161,12 @@ ul.notifications { position: relative; font-family: 'din-regular', Sans-Serif; + &.unread { + background: #EEE; + } + &:hover { - background: #F6F6F6; + background: $notifications-hover-color; .notification-read-unread { display:block; @@ -219,8 +234,4 @@ ul.notifications { cursor: pointer; } } - - &.unread { - background: #EEE; - } } diff --git a/frontend/src/components/App/NotificationBox.js b/frontend/src/components/App/NotificationBox.js index 2f9591ff..5d4dcc2b 100644 --- a/frontend/src/components/App/NotificationBox.js +++ b/frontend/src/components/App/NotificationBox.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types' import onClickOutsideAddon from 'react-onclickoutside' import Notification from '../Notification' +import Loading from '../Loading' class NotificationBox extends Component { @@ -27,10 +28,15 @@ class NotificationBox extends Component { render = () => { const { notifications, markAsRead, markAsUnread } = this.props + const empty = notifications && notifications.length === 0 return
          - {!notifications &&
        • loading...
        • } + {!notifications &&
        • } + {empty &&
        • + You have no notifications.
          + More time for dancing. +
        • } {notifications && notifications.slice(0, 10).map(n => { return })}
        - See all + {notifications && !empty && + See all + }
        } } diff --git a/frontend/src/components/Loading.js b/frontend/src/components/Loading.js new file mode 100644 index 00000000..018806af --- /dev/null +++ b/frontend/src/components/Loading.js @@ -0,0 +1,92 @@ +import React from 'react' +import PropTypes from 'prop-types' + +// based on https://www.npmjs.com/package/react-loading-animation + +const loading_style = { + position: 'relative', + margin: '0 auto', + width: '30px', + height: '30px', +} + +const svg_style = { + animation: 'rotate 2s linear infinite', + height: '100%', + transformOrigin: 'center center', + width: '100%', + position: 'absolute', + top: 0, + bottom: 0, + left: 0, + right: 0, + margin: 'auto' +} + +const circle_style = { + strokeDasharray: '1,200', + strokeDashoffset: '0', + animation: 'dash 1.5s ease-in-out infinite, color 6s ease-in-out infinite', + strokeLinecap: 'round' +} + +const animation = `@keyframes rotate { + 100% { + transform: rotate(360deg); + } +} +@keyframes dash { + 0% { + stroke-dasharray: 1,200; + stroke-dashoffset: 0; + } + 50% { + stroke-dasharray: 89,200; + stroke-dashoffset: -35px; + } + 100% { + stroke-dasharray: 89,200; + stroke-dashoffset: -124px; + } +} +@keyframes color { + 100%, 0% { + stroke: #a354cd; + } + 50% { + stroke: #4fb5c0; + } +}` + +class Loading extends React.Component { + static propTypes = { + style: PropTypes.object, + width: PropTypes.string, + height: PropTypes.string, + margin: PropTypes.string + } + + static defaultProps = { + style: {}, + width: '30px', + height: '30px', + margin: '0 auto' + } + + render () { + let { width, height, margin, style } = this.props + + loading_style.width = width + loading_style.height = height + loading_style.margin = margin + + return
        + + + + +
        + } +} + +export default Loading From 8695297a0fa38e676e5d255d6c7cf75cca3c36cb Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Fri, 29 Sep 2017 13:06:19 -0400 Subject: [PATCH 06/10] wasn't using the proper serializer causing frontend error --- app/controllers/notifications_controller.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb index 15781a32..cdbfb2ab 100644 --- a/app/controllers/notifications_controller.rb +++ b/app/controllers/notifications_controller.rb @@ -6,7 +6,6 @@ class NotificationsController < ApplicationController def index @notifications = current_user.mailbox.notifications.page(params[:page]).per(25) - respond_to do |format| format.html format.json do @@ -14,7 +13,11 @@ class NotificationsController < ApplicationController receipt = @receipts.find_by(notification_id: notification.id) NotificationDecorator.decorate(notification, receipt) end - render json: notifications + if notifications.length > 0 + render json: notifications + else + render json: [].to_json + end end end end From 5e6fb6290c19e449e3cbd8a5bdf2a4717afaa0b8 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Fri, 29 Sep 2017 13:06:33 -0400 Subject: [PATCH 07/10] refactor for clarity --- .../src/components/App/NotificationBox.js | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/App/NotificationBox.js b/frontend/src/components/App/NotificationBox.js index 5d4dcc2b..01382f26 100644 --- a/frontend/src/components/App/NotificationBox.js +++ b/frontend/src/components/App/NotificationBox.js @@ -33,16 +33,17 @@ class NotificationBox extends Component {
          {!notifications &&
        • } - {empty &&
        • - You have no notifications.
          - More time for dancing. -
        • } - {notifications && notifications.slice(0, 10).map(n => { - return - })} + {empty ? ( +
        • + You have no notifications.
          + More time for dancing. +
        • + ) : ( + notifications.slice(0, 10).map(n => ) + )}
        {notifications && !empty && From 15f512efef95a9eb36dafaaac66a746ce4bbd0e6 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Fri, 29 Sep 2017 14:05:39 -0400 Subject: [PATCH 08/10] improve notificationbox readability --- app/assets/stylesheets/notifications.scss.erb | 2 +- .../src/Metamaps/GlobalUI/Notifications.js | 2 +- .../src/components/App/NotificationBox.js | 55 +++++++++++++------ 3 files changed, 39 insertions(+), 20 deletions(-) diff --git a/app/assets/stylesheets/notifications.scss.erb b/app/assets/stylesheets/notifications.scss.erb index f6d01dc8..4c97547b 100644 --- a/app/assets/stylesheets/notifications.scss.erb +++ b/app/assets/stylesheets/notifications.scss.erb @@ -149,7 +149,7 @@ $unread_notifications_dot_size: 8px; ul.notifications { list-style: none; - li:last-child { + li:nth-last-child(2) { .notification-body { border-bottom: none !important; } diff --git a/frontend/src/Metamaps/GlobalUI/Notifications.js b/frontend/src/Metamaps/GlobalUI/Notifications.js index 0d2e5271..fef31602 100644 --- a/frontend/src/Metamaps/GlobalUI/Notifications.js +++ b/frontend/src/Metamaps/GlobalUI/Notifications.js @@ -30,7 +30,7 @@ const Notifications = { method: 'PUT', success: function(r) { if (n) { - + Notifications.unreadNotificationsCount-- n.is_read = true render() } diff --git a/frontend/src/components/App/NotificationBox.js b/frontend/src/components/App/NotificationBox.js index 01382f26..3df31e7c 100644 --- a/frontend/src/components/App/NotificationBox.js +++ b/frontend/src/components/App/NotificationBox.js @@ -26,29 +26,48 @@ class NotificationBox extends Component { this.props.toggleNotificationsBox() } - render = () => { + hasSomeNotifications = () => { + const { notifications } = this.props + return notifications && notifications.length > 0 + } + + showLoading = () => { + return
      • + } + + showEmpty = () => { + return
      • + You have no notifications.
        + More time for dancing. +
      • + } + + showNotifications = () => { const { notifications, markAsRead, markAsUnread } = this.props - const empty = notifications && notifications.length === 0 + if (!this.hasSomeNotifications()) { + return this.showEmpty() + } + return notifications.slice(0, 10).map( + n => + ).concat([ +
      • + + See all + +
      • + ]) + } + + render = () => { + const { notifications } = this.props return
          - {!notifications &&
        • } - {empty ? ( -
        • - You have no notifications.
          - More time for dancing. -
        • - ) : ( - notifications.slice(0, 10).map(n => ) - )} + {notifications ? this.showNotifications() : this.showLoading()}
        - {notifications && !empty && - See all - }
        } } From d51a22c5a927e6704d58adb1ef66e2fde30deb94 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Fri, 13 Oct 2017 12:13:08 -0400 Subject: [PATCH 09/10] eslint fixes --- .../src/Metamaps/GlobalUI/Notifications.js | 1 + .../src/components/App/NotificationBox.js | 1 - frontend/src/components/Loading.js | 84 +++++++++---------- frontend/src/components/Notification.js | 7 +- 4 files changed, 46 insertions(+), 47 deletions(-) diff --git a/frontend/src/Metamaps/GlobalUI/Notifications.js b/frontend/src/Metamaps/GlobalUI/Notifications.js index fef31602..29908695 100644 --- a/frontend/src/Metamaps/GlobalUI/Notifications.js +++ b/frontend/src/Metamaps/GlobalUI/Notifications.js @@ -1,3 +1,4 @@ +/* global $ */ import GlobalUI from './index' const Notifications = { diff --git a/frontend/src/components/App/NotificationBox.js b/frontend/src/components/App/NotificationBox.js index 3df31e7c..f5f0b801 100644 --- a/frontend/src/components/App/NotificationBox.js +++ b/frontend/src/components/App/NotificationBox.js @@ -6,7 +6,6 @@ import Notification from '../Notification' import Loading from '../Loading' class NotificationBox extends Component { - static propTypes = { notifications: PropTypes.array, fetchNotifications: PropTypes.func.isRequired, diff --git a/frontend/src/components/Loading.js b/frontend/src/components/Loading.js index 018806af..66382978 100644 --- a/frontend/src/components/Loading.js +++ b/frontend/src/components/Loading.js @@ -3,31 +3,31 @@ import PropTypes from 'prop-types' // based on https://www.npmjs.com/package/react-loading-animation -const loading_style = { - position: 'relative', - margin: '0 auto', - width: '30px', - height: '30px', +const loadingStyle = { + position: 'relative', + margin: '0 auto', + width: '30px', + height: '30px' } -const svg_style = { - animation: 'rotate 2s linear infinite', - height: '100%', - transformOrigin: 'center center', - width: '100%', - position: 'absolute', - top: 0, - bottom: 0, - left: 0, - right: 0, - margin: 'auto' +const svgStyle = { + animation: 'rotate 2s linear infinite', + height: '100%', + transformOrigin: 'center center', + width: '100%', + position: 'absolute', + top: 0, + bottom: 0, + left: 0, + right: 0, + margin: 'auto' } -const circle_style = { - strokeDasharray: '1,200', - strokeDashoffset: '0', - animation: 'dash 1.5s ease-in-out infinite, color 6s ease-in-out infinite', - strokeLinecap: 'round' +const circleStyle = { + strokeDasharray: '1,200', + strokeDashoffset: '0', + animation: 'dash 1.5s ease-in-out infinite, color 6s ease-in-out infinite', + strokeLinecap: 'round' } const animation = `@keyframes rotate { @@ -59,34 +59,34 @@ const animation = `@keyframes rotate { }` class Loading extends React.Component { - static propTypes = { - style: PropTypes.object, - width: PropTypes.string, - height: PropTypes.string, - margin: PropTypes.string - } + static propTypes = { + style: PropTypes.object, + width: PropTypes.string, + height: PropTypes.string, + margin: PropTypes.string + } - static defaultProps = { - style: {}, - width: '30px', - height: '30px', - margin: '0 auto' - } + static defaultProps = { + style: {}, + width: '30px', + height: '30px', + margin: '0 auto' + } - render () { - let { width, height, margin, style } = this.props + render() { + let { width, height, margin, style } = this.props - loading_style.width = width - loading_style.height = height - loading_style.margin = margin + loadingStyle.width = width + loadingStyle.height = height + loadingStyle.margin = margin - return
        + return
        - - + +
        - } + } } export default Loading diff --git a/frontend/src/components/Notification.js b/frontend/src/components/Notification.js index 05da2cc6..22237aaa 100644 --- a/frontend/src/components/Notification.js +++ b/frontend/src/components/Notification.js @@ -3,7 +3,6 @@ import PropTypes from 'prop-types' import outdent from 'outdent' class Notification extends Component { - static propTypes = { markAsRead: PropTypes.func, markAsUnread: PropTypes.func, @@ -32,7 +31,7 @@ class Notification extends Component { let map, topic, topic1, topic2 let result = `
        ${notification.actor.name}
        ` - switch(notification.type) { + switch (notification.type) { case 'ACCESS_APPROVED': map = notification.data.map result += outdent`granted your request to edit map @@ -77,8 +76,8 @@ class Notification extends Component { getDate = () => { const { notification: {created_at} } = this.props - const months = ['Jan','Feb','Mar','Apr','May','Jun', - 'Jul','Aug','Sep','Oct','Nov','Dec'] + 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()}` } From f9c139c19e740889a3e46e3a2b42ea8552c2c074 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Fri, 13 Oct 2017 12:22:05 -0400 Subject: [PATCH 10/10] ruby codeclimate fixes --- app/controllers/notifications_controller.rb | 2 +- app/decorators/notification_decorator.rb | 57 +++++++++++---------- 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb index cdbfb2ab..22a34452 100644 --- a/app/controllers/notifications_controller.rb +++ b/app/controllers/notifications_controller.rb @@ -13,7 +13,7 @@ class NotificationsController < ApplicationController receipt = @receipts.find_by(notification_id: notification.id) NotificationDecorator.decorate(notification, receipt) end - if notifications.length > 0 + if !notifications.empty? render json: notifications else render json: [].to_json diff --git a/app/decorators/notification_decorator.rb b/app/decorators/notification_decorator.rb index a2f7584b..cc503821 100644 --- a/app/decorators/notification_decorator.rb +++ b/app/decorators/notification_decorator.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class NotificationDecorator class << self def decorate(notification, receipt) @@ -14,34 +15,34 @@ class NotificationDecorator } case notification.notification_code - when MAP_ACCESS_APPROVED, MAP_ACCESS_REQUEST, MAP_INVITE_TO_EDIT - map = notification.notified_object&.map - result[:data][:map] = { - id: map&.id, - name: map&.name - } - when TOPIC_ADDED_TO_MAP - topic = notification.notified_object&.eventable - map = notification.notified_object&.map - result[:data][:topic] = { - id: topic&.id, - name: topic&.name - } - result[:data][: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[:data][:topic1] = { - id: topic1&.id, - name: topic1&.name - } - result[:data][:topic2] = { - id: topic2&.id, - name: topic2&.name - } + when MAP_ACCESS_APPROVED, MAP_ACCESS_REQUEST, MAP_INVITE_TO_EDIT + map = notification.notified_object&.map + result[:data][:map] = { + id: map&.id, + name: map&.name + } + when TOPIC_ADDED_TO_MAP + topic = notification.notified_object&.eventable + map = notification.notified_object&.map + result[:data][:topic] = { + id: topic&.id, + name: topic&.name + } + result[:data][: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[:data][:topic1] = { + id: topic1&.id, + name: topic1&.name + } + result[:data][:topic2] = { + id: topic2&.id, + name: topic2&.name + } end result end