Merge pull request #1142 from metamaps/feature/notifs.box
Notifications Dropdown
This commit is contained in:
commit
55f2425501
17 changed files with 646 additions and 128 deletions
|
@ -1,4 +1,7 @@
|
||||||
|
$notifications-border-color: #DDDDDD;
|
||||||
|
$notifications-hover-color: #F6F6F6;
|
||||||
$unread_notifications_dot_size: 8px;
|
$unread_notifications_dot_size: 8px;
|
||||||
|
|
||||||
.unread-notifications-dot {
|
.unread-notifications-dot {
|
||||||
width: $unread_notifications_dot_size;
|
width: $unread_notifications_dot_size;
|
||||||
height: $unread_notifications_dot_size;
|
height: $unread_notifications_dot_size;
|
||||||
|
@ -13,13 +16,72 @@ $unread_notifications_dot_size: 8px;
|
||||||
.notificationsIcon {
|
.notificationsIcon {
|
||||||
position: relative;
|
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);
|
||||||
|
border: 1px solid $notifications-border-color;
|
||||||
|
|
||||||
|
.notificationsBoxTriangle {
|
||||||
|
min-width: 0 !important;
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
right: 48px;
|
||||||
|
width: 20px !important;
|
||||||
|
height: 20px !important;
|
||||||
|
margin-left: -10px;
|
||||||
|
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;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
-webkit-transform: rotate(45deg);
|
||||||
|
-ms-transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
padding: 6px 0;
|
||||||
|
font-family: din-regular, helvetica, sans-serif;
|
||||||
|
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #333;
|
||||||
|
background: $notifications-hover-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.controller-notifications {
|
.controller-notifications {
|
||||||
ul.notifications {
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notificationPage,
|
.notificationPage,
|
||||||
.notificationsPage {
|
.notificationsPage {
|
||||||
font-family: 'din-regular', Sans-Serif;
|
font-family: 'din-regular', Sans-Serif;
|
||||||
|
@ -47,89 +109,9 @@ $unread_notifications_dot_size: 8px;
|
||||||
.emptyInbox {
|
.emptyInbox {
|
||||||
padding-top: 15px;
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.notificationPage {
|
.notificationPage {
|
||||||
.thirty-two-avatar {
|
.thirty-two-avatar {
|
||||||
|
@ -139,14 +121,14 @@ $unread_notifications_dot_size: 8px;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.decline {
|
&.decline {
|
||||||
background: #DB5D5D;
|
background: #DB5D5D;
|
||||||
&:hover {
|
&:hover {
|
||||||
|
@ -154,7 +136,7 @@ $unread_notifications_dot_size: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-body {
|
.notification-body {
|
||||||
p, div {
|
p, div {
|
||||||
margin: 1em auto;
|
margin: 1em auto;
|
||||||
|
@ -163,3 +145,93 @@ $unread_notifications_dot_size: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ul.notifications {
|
||||||
|
list-style: none;
|
||||||
|
|
||||||
|
li:nth-last-child(2) {
|
||||||
|
.notification-body {
|
||||||
|
border-bottom: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification {
|
||||||
|
padding: 10px 10px 0 10px;
|
||||||
|
position: relative;
|
||||||
|
font-family: 'din-regular', Sans-Serif;
|
||||||
|
|
||||||
|
&.unread {
|
||||||
|
background: #EEE;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $notifications-hover-color;
|
||||||
|
|
||||||
|
.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;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
|
||||||
|
.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;
|
||||||
|
margin-top: -6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-read-unread {
|
||||||
|
display: none;
|
||||||
|
float: left;
|
||||||
|
width: 15%;
|
||||||
|
|
||||||
|
a, div {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
margin-top: -10px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -6,13 +6,17 @@ class NotificationsController < ApplicationController
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@notifications = current_user.mailbox.notifications.page(params[:page]).per(25)
|
@notifications = current_user.mailbox.notifications.page(params[:page]).per(25)
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html
|
format.html
|
||||||
format.json do
|
format.json do
|
||||||
render json: @notifications.map do |notification|
|
notifications = @notifications.map do |notification|
|
||||||
receipt = @receipts.find_by(notification_id: notification.id)
|
receipt = @receipts.find_by(notification_id: notification.id)
|
||||||
notification.as_json.merge(is_read: receipt.is_read)
|
NotificationDecorator.decorate(notification, receipt)
|
||||||
|
end
|
||||||
|
if !notifications.empty?
|
||||||
|
render json: notifications
|
||||||
|
else
|
||||||
|
render json: [].to_json
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -34,9 +38,7 @@ class NotificationsController < ApplicationController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
format.json do
|
format.json do
|
||||||
render json: @notification.as_json.merge(
|
render json: NotificationDecorator.decorate(@notification, @receipt)
|
||||||
is_read: @receipt.is_read
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -46,9 +48,7 @@ class NotificationsController < ApplicationController
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.js
|
format.js
|
||||||
format.json do
|
format.json do
|
||||||
render json: @notification.as_json.merge(
|
render json: NotificationDecorator.decorate(@notification, @receipt)
|
||||||
is_read: @receipt.is_read
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -58,9 +58,7 @@ class NotificationsController < ApplicationController
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.js
|
format.js
|
||||||
format.json do
|
format.json do
|
||||||
render json: @notification.as_json.merge(
|
render json: NotificationDecorator.decorate(@notification, @receipt)
|
||||||
is_read: @receipt.is_read
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
50
app/decorators/notification_decorator.rb
Normal file
50
app/decorators/notification_decorator.rb
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
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,
|
||||||
|
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[: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
|
||||||
|
end
|
||||||
|
end
|
|
@ -8,7 +8,7 @@
|
||||||
</header>
|
</header>
|
||||||
<ul class="notifications">
|
<ul class="notifications">
|
||||||
<% blacklist = [MAP_ACCESS_REQUEST, MAP_ACCESS_APPROVED, MAP_INVITE_TO_EDIT] %>
|
<% 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| %>
|
<% notifications.each do |notification| %>
|
||||||
<% receipt = @receipts.find_by(notification_id: notification.id) %>
|
<% receipt = @receipts.find_by(notification_id: notification.id) %>
|
||||||
<li class="notification <%= receipt.is_read? ? 'read' : 'unread' %>" id="notification-<%= notification.id %>">
|
<li class="notification <%= receipt.is_read? ? 'read' : 'unread' %>" id="notification-<%= notification.id %>">
|
||||||
|
|
|
@ -4,4 +4,4 @@ $('#notification-<%= @notification.id %> .notification-read-unread > a')
|
||||||
$('#notification-<%= @notification.id %>')
|
$('#notification-<%= @notification.id %>')
|
||||||
.removeClass('unread')
|
.removeClass('unread')
|
||||||
.addClass('read')
|
.addClass('read')
|
||||||
Metamaps.GlobalUI.NotificationIcon.render(Metamaps.GlobalUI.NotificationIcon.unreadNotificationsCount - 1)
|
Metamaps.GlobalUI.Notifications.decrementUnread(Metamaps.GlobalUI.ReactApp.render)
|
||||||
|
|
|
@ -4,4 +4,4 @@ $('#notification-<%= @notification.id %> .notification-read-unread > a')
|
||||||
$('#notification-<%= @notification.id %>')
|
$('#notification-<%= @notification.id %>')
|
||||||
.removeClass('read')
|
.removeClass('read')
|
||||||
.addClass('unread')
|
.addClass('unread')
|
||||||
Metamaps.GlobalUI.NotificationIcon.render(Metamaps.GlobalUI.NotificationIcon.unreadNotificationsCount + 1)
|
Metamaps.GlobalUI.Notifications.incrementUnread(Metamaps.GlobalUI.ReactApp.render)
|
||||||
|
|
|
@ -19,7 +19,7 @@ module Metamaps
|
||||||
end
|
end
|
||||||
|
|
||||||
# Custom directories with classes and modules you want to be autoloadable.
|
# 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.
|
# Configure the default encoding used in templates for Ruby 1.9.
|
||||||
config.encoding = 'utf-8'
|
config.encoding = 'utf-8'
|
||||||
|
|
63
frontend/src/Metamaps/GlobalUI/Notifications.js
Normal file
63
frontend/src/Metamaps/GlobalUI/Notifications.js
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
/* global $ */
|
||||||
|
import GlobalUI from './index'
|
||||||
|
|
||||||
|
const Notifications = {
|
||||||
|
notifications: null,
|
||||||
|
unreadNotificationsCount: 0,
|
||||||
|
init: serverData => {
|
||||||
|
Notifications.unreadNotificationsCount = serverData.unreadNotificationsCount
|
||||||
|
},
|
||||||
|
fetch: render => {
|
||||||
|
$.ajax({
|
||||||
|
url: '/notifications.json',
|
||||||
|
success: function(data) {
|
||||||
|
Notifications.notifications = data
|
||||||
|
render()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
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.json`,
|
||||||
|
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.json`,
|
||||||
|
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')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Notifications
|
|
@ -8,6 +8,7 @@ import apply from 'async/apply'
|
||||||
|
|
||||||
import { notifyUser } from './index.js'
|
import { notifyUser } from './index.js'
|
||||||
import ImportDialog from './ImportDialog'
|
import ImportDialog from './ImportDialog'
|
||||||
|
import Notifications from './Notifications'
|
||||||
import Active from '../Active'
|
import Active from '../Active'
|
||||||
import DataModel from '../DataModel'
|
import DataModel from '../DataModel'
|
||||||
import { ExploreMaps, ChatView, TopicCard, ContextMenu } from '../Views'
|
import { ExploreMaps, ChatView, TopicCard, ContextMenu } from '../Views'
|
||||||
|
@ -30,7 +31,6 @@ const ReactApp = {
|
||||||
serverData: {},
|
serverData: {},
|
||||||
mapId: null,
|
mapId: null,
|
||||||
topicId: null,
|
topicId: null,
|
||||||
unreadNotificationsCount: 0,
|
|
||||||
mapsWidth: 0,
|
mapsWidth: 0,
|
||||||
toast: '',
|
toast: '',
|
||||||
mobile: false,
|
mobile: false,
|
||||||
|
@ -40,7 +40,6 @@ const ReactApp = {
|
||||||
init: function(serverData, openLightbox) {
|
init: function(serverData, openLightbox) {
|
||||||
const self = ReactApp
|
const self = ReactApp
|
||||||
self.serverData = serverData
|
self.serverData = serverData
|
||||||
self.unreadNotificationsCount = serverData.unreadNotificationsCount
|
|
||||||
self.mobileTitle = serverData.mobileTitle
|
self.mobileTitle = serverData.mobileTitle
|
||||||
self.openLightbox = openLightbox
|
self.openLightbox = openLightbox
|
||||||
self.metacodeSets = serverData.metacodeSets
|
self.metacodeSets = serverData.metacodeSets
|
||||||
|
@ -99,7 +98,7 @@ const ReactApp = {
|
||||||
getProps: function() {
|
getProps: function() {
|
||||||
const self = ReactApp
|
const self = ReactApp
|
||||||
return merge({
|
return merge({
|
||||||
unreadNotificationsCount: self.unreadNotificationsCount,
|
unreadNotificationsCount: Notifications.unreadNotificationsCount,
|
||||||
currentUser: Active.Mapper,
|
currentUser: Active.Mapper,
|
||||||
toast: self.toast,
|
toast: self.toast,
|
||||||
mobile: self.mobile,
|
mobile: self.mobile,
|
||||||
|
@ -107,7 +106,11 @@ const ReactApp = {
|
||||||
mobileTitleWidth: self.mobileTitleWidth,
|
mobileTitleWidth: self.mobileTitleWidth,
|
||||||
mobileTitleClick: (e) => Active.Map && InfoBox.toggleBox(e),
|
mobileTitleClick: (e) => Active.Map && InfoBox.toggleBox(e),
|
||||||
openInviteLightbox: () => self.openLightbox('invite'),
|
openInviteLightbox: () => self.openLightbox('invite'),
|
||||||
serverData: self.serverData
|
serverData: self.serverData,
|
||||||
|
notifications: Notifications.notifications,
|
||||||
|
fetchNotifications: apply(Notifications.fetch, ReactApp.render),
|
||||||
|
markAsRead: apply(Notifications.markAsRead, ReactApp.render),
|
||||||
|
markAsUnread: apply(Notifications.markAsUnread, ReactApp.render)
|
||||||
},
|
},
|
||||||
self.getMapProps(),
|
self.getMapProps(),
|
||||||
self.getTopicProps(),
|
self.getTopicProps(),
|
||||||
|
|
|
@ -4,6 +4,7 @@ import clipboard from 'clipboard-js'
|
||||||
|
|
||||||
import Create from '../Create'
|
import Create from '../Create'
|
||||||
|
|
||||||
|
import Notifications from './Notifications'
|
||||||
import ReactApp from './ReactApp'
|
import ReactApp from './ReactApp'
|
||||||
import Search from './Search'
|
import Search from './Search'
|
||||||
import CreateMap from './CreateMap'
|
import CreateMap from './CreateMap'
|
||||||
|
@ -17,6 +18,7 @@ const GlobalUI = {
|
||||||
init: function(serverData) {
|
init: function(serverData) {
|
||||||
const self = GlobalUI
|
const self = GlobalUI
|
||||||
|
|
||||||
|
self.Notifications.init(serverData)
|
||||||
self.ReactApp.init(serverData, self.openLightbox)
|
self.ReactApp.init(serverData, self.openLightbox)
|
||||||
self.CreateMap.init(serverData)
|
self.CreateMap.init(serverData)
|
||||||
self.ImportDialog.init(serverData, self.openLightbox, self.closeLightbox)
|
self.ImportDialog.init(serverData, self.openLightbox, self.closeLightbox)
|
||||||
|
@ -151,5 +153,5 @@ const GlobalUI = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { ReactApp, Search, CreateMap, ImportDialog }
|
export { Notifications, ReactApp, Search, CreateMap, ImportDialog }
|
||||||
export default GlobalUI
|
export default GlobalUI
|
||||||
|
|
|
@ -9,7 +9,7 @@ import DataModel from './DataModel'
|
||||||
import Debug from './Debug'
|
import Debug from './Debug'
|
||||||
import Filter from './Filter'
|
import Filter from './Filter'
|
||||||
import GlobalUI, {
|
import GlobalUI, {
|
||||||
ReactApp, Search, CreateMap, ImportDialog
|
Notifications, ReactApp, Search, CreateMap, ImportDialog
|
||||||
} from './GlobalUI'
|
} from './GlobalUI'
|
||||||
import Import from './Import'
|
import Import from './Import'
|
||||||
import JIT from './JIT'
|
import JIT from './JIT'
|
||||||
|
@ -42,6 +42,7 @@ Metamaps.DataModel = DataModel
|
||||||
Metamaps.Debug = Debug
|
Metamaps.Debug = Debug
|
||||||
Metamaps.Filter = Filter
|
Metamaps.Filter = Filter
|
||||||
Metamaps.GlobalUI = GlobalUI
|
Metamaps.GlobalUI = GlobalUI
|
||||||
|
Metamaps.GlobalUI.Notifications = Notifications
|
||||||
Metamaps.GlobalUI.ReactApp = ReactApp
|
Metamaps.GlobalUI.ReactApp = ReactApp
|
||||||
Metamaps.GlobalUI.Search = Search
|
Metamaps.GlobalUI.Search = Search
|
||||||
Metamaps.GlobalUI.CreateMap = CreateMap
|
Metamaps.GlobalUI.CreateMap = CreateMap
|
||||||
|
|
74
frontend/src/components/App/NotificationBox.js
Normal file
74
frontend/src/components/App/NotificationBox.js
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
import React, { Component } from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
import onClickOutsideAddon from 'react-onclickoutside'
|
||||||
|
import Notification from '../Notification'
|
||||||
|
import Loading from '../Loading'
|
||||||
|
|
||||||
|
class NotificationBox extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
notifications: PropTypes.array,
|
||||||
|
fetchNotifications: PropTypes.func.isRequired,
|
||||||
|
toggleNotificationsBox: PropTypes.func.isRequired,
|
||||||
|
markAsRead: PropTypes.func.isRequired,
|
||||||
|
markAsUnread: PropTypes.func.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount = () => {
|
||||||
|
const { notifications, fetchNotifications } = this.props
|
||||||
|
if (!notifications) {
|
||||||
|
fetchNotifications()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClickOutside = () => {
|
||||||
|
this.props.toggleNotificationsBox()
|
||||||
|
}
|
||||||
|
|
||||||
|
hasSomeNotifications = () => {
|
||||||
|
const { notifications } = this.props
|
||||||
|
return notifications && notifications.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading = () => {
|
||||||
|
return <li><Loading margin='30px auto' /></li>
|
||||||
|
}
|
||||||
|
|
||||||
|
showEmpty = () => {
|
||||||
|
return <li className='notificationsEmpty'>
|
||||||
|
You have no notifications. <br />
|
||||||
|
More time for dancing.
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
|
||||||
|
showNotifications = () => {
|
||||||
|
const { notifications, markAsRead, markAsUnread } = this.props
|
||||||
|
if (!this.hasSomeNotifications()) {
|
||||||
|
return this.showEmpty()
|
||||||
|
}
|
||||||
|
return notifications.slice(0, 10).map(
|
||||||
|
n => <Notification notification={n}
|
||||||
|
markAsRead={markAsRead}
|
||||||
|
markAsUnread={markAsUnread}
|
||||||
|
key={`notification-${n.id}`} />
|
||||||
|
).concat([
|
||||||
|
<li key='notification-see-all'>
|
||||||
|
<a href='/notifications' className='notificationsBoxSeeAll'>
|
||||||
|
See all
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
render = () => {
|
||||||
|
const { notifications } = this.props
|
||||||
|
return <div className='notificationsBox'>
|
||||||
|
<div className='notificationsBoxTriangle' />
|
||||||
|
<ul className='notifications'>
|
||||||
|
{notifications ? this.showNotifications() : this.showLoading()}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default onClickOutsideAddon(NotificationBox)
|
|
@ -4,18 +4,14 @@ import PropTypes from 'prop-types'
|
||||||
class NotificationIcon extends Component {
|
class NotificationIcon extends Component {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
unreadNotificationsCount: PropTypes.number
|
unreadNotificationsCount: PropTypes.number,
|
||||||
}
|
toggleNotificationsBox: PropTypes.func
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props)
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render = () => {
|
render = () => {
|
||||||
|
const { toggleNotificationsBox } = this.props
|
||||||
let linkClasses = 'notificationsIcon upperRightEl upperRightIcon '
|
let linkClasses = 'notificationsIcon upperRightEl upperRightIcon '
|
||||||
|
linkClasses += 'ignore-react-onclickoutside '
|
||||||
|
|
||||||
if (this.props.unreadNotificationsCount > 0) {
|
if (this.props.unreadNotificationsCount > 0) {
|
||||||
linkClasses += 'unread'
|
linkClasses += 'unread'
|
||||||
|
@ -24,14 +20,14 @@ class NotificationIcon extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a className={linkClasses} href="/notifications" target="_blank">
|
<div className={linkClasses} onClick={toggleNotificationsBox}>
|
||||||
<div className="tooltipsUnder">
|
<div className="tooltipsUnder">
|
||||||
Notifications
|
Notifications
|
||||||
</div>
|
</div>
|
||||||
{this.props.unreadNotificationsCount === 0 ? null : (
|
{this.props.unreadNotificationsCount === 0 ? null : (
|
||||||
<div className="unread-notifications-dot"></div>
|
<div className="unread-notifications-dot"></div>
|
||||||
)}
|
)}
|
||||||
</a>
|
</div>
|
||||||
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,31 +4,54 @@ import PropTypes from 'prop-types'
|
||||||
import AccountMenu from './AccountMenu'
|
import AccountMenu from './AccountMenu'
|
||||||
import LoginForm from './LoginForm'
|
import LoginForm from './LoginForm'
|
||||||
import NotificationIcon from './NotificationIcon'
|
import NotificationIcon from './NotificationIcon'
|
||||||
|
import NotificationBox from './NotificationBox'
|
||||||
|
|
||||||
class UpperRightUI extends Component {
|
class UpperRightUI extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
currentUser: PropTypes.object,
|
currentUser: PropTypes.object,
|
||||||
signInPage: PropTypes.bool,
|
signInPage: PropTypes.bool,
|
||||||
unreadNotificationsCount: PropTypes.number,
|
unreadNotificationsCount: PropTypes.number,
|
||||||
|
fetchNotifications: PropTypes.func,
|
||||||
|
notifications: PropTypes.array,
|
||||||
|
markAsRead: PropTypes.func.isRequired,
|
||||||
|
markAsUnread: PropTypes.func.isRequired,
|
||||||
openInviteLightbox: PropTypes.func
|
openInviteLightbox: PropTypes.func
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
this.state = {accountBoxOpen: false}
|
this.state = {
|
||||||
|
accountBoxOpen: false,
|
||||||
|
notificationsBoxOpen: false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reset = () => {
|
reset = () => {
|
||||||
this.setState({accountBoxOpen: false})
|
this.setState({
|
||||||
|
accountBoxOpen: false,
|
||||||
|
notificationsBoxOpen: false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleAccountBox = () => {
|
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 () {
|
render () {
|
||||||
const { currentUser, signInPage, unreadNotificationsCount, openInviteLightbox } = this.props
|
const { currentUser, signInPage, unreadNotificationsCount,
|
||||||
const { accountBoxOpen } = this.state
|
notifications, fetchNotifications, openInviteLightbox,
|
||||||
|
markAsRead, markAsUnread } = this.props
|
||||||
|
const { accountBoxOpen, notificationsBoxOpen } = this.state
|
||||||
return <div className="upperRightUI">
|
return <div className="upperRightUI">
|
||||||
{currentUser && <a href="/maps/new" target="_blank" className="addMap upperRightEl upperRightIcon">
|
{currentUser && <a href="/maps/new" target="_blank" className="addMap upperRightEl upperRightIcon">
|
||||||
<div className="tooltipsUnder">
|
<div className="tooltipsUnder">
|
||||||
|
@ -36,7 +59,15 @@ class UpperRightUI extends Component {
|
||||||
</div>
|
</div>
|
||||||
</a>}
|
</a>}
|
||||||
{currentUser && <span id="notification_icon">
|
{currentUser && <span id="notification_icon">
|
||||||
<NotificationIcon unreadNotificationsCount={unreadNotificationsCount} />
|
<NotificationIcon
|
||||||
|
unreadNotificationsCount={unreadNotificationsCount}
|
||||||
|
toggleNotificationsBox={this.toggleNotificationsBox}/>
|
||||||
|
{notificationsBoxOpen && <NotificationBox
|
||||||
|
notifications={notifications}
|
||||||
|
fetchNotifications={fetchNotifications}
|
||||||
|
markAsRead={markAsRead}
|
||||||
|
markAsUnread={markAsUnread}
|
||||||
|
toggleNotificationsBox={this.toggleNotificationsBox}/>}
|
||||||
</span>}
|
</span>}
|
||||||
{!signInPage && <div className="sidebarAccount upperRightEl">
|
{!signInPage && <div className="sidebarAccount upperRightEl">
|
||||||
<div className="sidebarAccountIcon ignore-react-onclickoutside" onClick={this.toggleAccountBox}>
|
<div className="sidebarAccountIcon ignore-react-onclickoutside" onClick={this.toggleAccountBox}>
|
||||||
|
|
|
@ -11,6 +11,10 @@ class App extends Component {
|
||||||
children: PropTypes.object,
|
children: PropTypes.object,
|
||||||
toast: PropTypes.string,
|
toast: PropTypes.string,
|
||||||
unreadNotificationsCount: PropTypes.number,
|
unreadNotificationsCount: PropTypes.number,
|
||||||
|
notifications: PropTypes.array,
|
||||||
|
fetchNotifications: PropTypes.func,
|
||||||
|
markAsRead: PropTypes.func,
|
||||||
|
markAsUnread: PropTypes.func,
|
||||||
location: PropTypes.object,
|
location: PropTypes.object,
|
||||||
mobile: PropTypes.bool,
|
mobile: PropTypes.bool,
|
||||||
mobileTitle: PropTypes.string,
|
mobileTitle: PropTypes.string,
|
||||||
|
@ -38,7 +42,8 @@ class App extends Component {
|
||||||
const { children, toast, unreadNotificationsCount, openInviteLightbox,
|
const { children, toast, unreadNotificationsCount, openInviteLightbox,
|
||||||
mobile, mobileTitle, mobileTitleWidth, mobileTitleClick, location,
|
mobile, mobileTitle, mobileTitleWidth, mobileTitleClick, location,
|
||||||
map, userRequested, requestAnswered, requestApproved, serverData,
|
map, userRequested, requestAnswered, requestApproved, serverData,
|
||||||
onRequestAccess } = this.props
|
onRequestAccess, notifications, fetchNotifications,
|
||||||
|
markAsRead, markAsUnread } = this.props
|
||||||
const { pathname } = location || {}
|
const { pathname } = location || {}
|
||||||
// this fixes a bug that happens otherwise when you logout
|
// this fixes a bug that happens otherwise when you logout
|
||||||
const currentUser = this.props.currentUser && this.props.currentUser.id ? this.props.currentUser : null
|
const currentUser = this.props.currentUser && this.props.currentUser.id ? this.props.currentUser : null
|
||||||
|
@ -58,6 +63,10 @@ class App extends Component {
|
||||||
onRequestClick={onRequestAccess} />}
|
onRequestClick={onRequestAccess} />}
|
||||||
{!mobile && <UpperRightUI currentUser={currentUser}
|
{!mobile && <UpperRightUI currentUser={currentUser}
|
||||||
unreadNotificationsCount={unreadNotificationsCount}
|
unreadNotificationsCount={unreadNotificationsCount}
|
||||||
|
notifications={notifications}
|
||||||
|
fetchNotifications={fetchNotifications}
|
||||||
|
markAsRead={markAsRead}
|
||||||
|
markAsUnread={markAsUnread}
|
||||||
openInviteLightbox={openInviteLightbox}
|
openInviteLightbox={openInviteLightbox}
|
||||||
signInPage={pathname === '/login'} />}
|
signInPage={pathname === '/login'} />}
|
||||||
<Toast message={toast} />
|
<Toast message={toast} />
|
||||||
|
|
92
frontend/src/components/Loading.js
Normal file
92
frontend/src/components/Loading.js
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
// based on https://www.npmjs.com/package/react-loading-animation
|
||||||
|
|
||||||
|
const loadingStyle = {
|
||||||
|
position: 'relative',
|
||||||
|
margin: '0 auto',
|
||||||
|
width: '30px',
|
||||||
|
height: '30px'
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 {
|
||||||
|
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
|
||||||
|
|
||||||
|
loadingStyle.width = width
|
||||||
|
loadingStyle.height = height
|
||||||
|
loadingStyle.margin = margin
|
||||||
|
|
||||||
|
return <div style={Object.assign({}, loadingStyle, style)}>
|
||||||
|
<style>{animation}</style>
|
||||||
|
<svg style={svgStyle} viewBox="25 25 50 50">
|
||||||
|
<circle style={circleStyle} cx="50" cy="50" r="20" fill="none" strokeWidth="4" strokeMiterlimit="10"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Loading
|
127
frontend/src/components/Notification.js
Normal file
127
frontend/src/components/Notification.js
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
import React, { Component } from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import outdent from 'outdent'
|
||||||
|
|
||||||
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationTextHtml = () => {
|
||||||
|
const { notification } = this.props
|
||||||
|
let map, topic, topic1, topic2
|
||||||
|
let result = `<div class='in-bold'>${notification.actor.name}</div>`
|
||||||
|
|
||||||
|
switch (notification.type) {
|
||||||
|
case 'ACCESS_APPROVED':
|
||||||
|
map = notification.data.map
|
||||||
|
result += outdent`granted your request to edit map
|
||||||
|
<span class='in-bold'>${map.name}</span>`
|
||||||
|
break
|
||||||
|
case 'ACCESS_REQUEST':
|
||||||
|
map = notification.data.map
|
||||||
|
result += outdent`wants permission to map with you on
|
||||||
|
<span class='in-bold'>${map.name}</span>`
|
||||||
|
if (!notification.data.object.answered) {
|
||||||
|
result += '<br /><div class="action">Offer a response</div>'
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'INVITE_TO_EDIT':
|
||||||
|
map = notification.data.map
|
||||||
|
result += outdent`gave you edit access to map
|
||||||
|
<span class='in-bold'>${map.name}</span>`
|
||||||
|
break
|
||||||
|
case 'TOPIC_ADDED_TO_MAP':
|
||||||
|
map = notification.data.map
|
||||||
|
topic = notification.data.topic
|
||||||
|
result += outdent`added topic <span class='in-bold'>${topic.name}</span>
|
||||||
|
to map <span class='in-bold'>${map.name}</span>`
|
||||||
|
break
|
||||||
|
case 'TOPIC_CONNECTED_1':
|
||||||
|
topic1 = notification.data.topic1
|
||||||
|
topic2 = notification.data.topic2
|
||||||
|
result += outdent`connected <span class='in-bold'>${topic1.name}</span>
|
||||||
|
to <span class='in-bold'>${topic2.name}</span>`
|
||||||
|
break
|
||||||
|
case 'TOPIC_CONNECTED_2':
|
||||||
|
topic1 = notification.data.topic1
|
||||||
|
topic2 = notification.data.topic2
|
||||||
|
result += outdent`connected <span class='in-bold'>${topic2.name}</span>
|
||||||
|
to <span class='in-bold'>${topic1.name}</span>`
|
||||||
|
break
|
||||||
|
case 'MESSAGE_FROM_DEVS':
|
||||||
|
result += notification.subject
|
||||||
|
}
|
||||||
|
return {__html: result}
|
||||||
|
}
|
||||||
|
|
||||||
|
getDate = () => {
|
||||||
|
const { notification: {created_at} } = this.props
|
||||||
|
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
|
||||||
|
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
|
||||||
|
const created = new Date(created_at)
|
||||||
|
return `${months[created.getMonth()]} ${created.getDate()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
markAsRead = () => {
|
||||||
|
const { notification, markAsRead } = this.props
|
||||||
|
markAsRead(notification.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
markAsUnread = () => {
|
||||||
|
const { notification, markAsUnread } = this.props
|
||||||
|
markAsUnread(notification.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
render = () => {
|
||||||
|
const { notification } = this.props
|
||||||
|
const classes = `notification ${notification.is_read ? 'read' : 'unread'}`
|
||||||
|
|
||||||
|
if (!notification.data.object) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return <li className={classes}>
|
||||||
|
<a href={`/notifications/${notification.id}`}>
|
||||||
|
<div className='notification-actor'>
|
||||||
|
<img src={notification.actor.image} />
|
||||||
|
</div>
|
||||||
|
<div className='notification-body'
|
||||||
|
dangerouslySetInnerHTML={this.notificationTextHtml()} />
|
||||||
|
</a>
|
||||||
|
<div className='notification-read-unread'>
|
||||||
|
{!notification.is_read && <div onClick={this.markAsRead}>
|
||||||
|
mark read
|
||||||
|
</div>}
|
||||||
|
{notification.is_read && <div onClick={this.markAsUnread}>
|
||||||
|
mark unread
|
||||||
|
</div>}
|
||||||
|
</div>
|
||||||
|
<div className='notification-date'>
|
||||||
|
{this.getDate()}
|
||||||
|
</div>
|
||||||
|
<div className='clearfloat'></div>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Notification
|
Loading…
Reference in a new issue