Notifications module
parent
4115b3bb93
commit
529c91d9e9
|
@ -465,6 +465,8 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven
|
||||||
{templateUrl: "user/mail-notifications.html"})
|
{templateUrl: "user/mail-notifications.html"})
|
||||||
$routeProvider.when("/user-settings/live-notifications",
|
$routeProvider.when("/user-settings/live-notifications",
|
||||||
{templateUrl: "user/live-notifications.html"})
|
{templateUrl: "user/live-notifications.html"})
|
||||||
|
$routeProvider.when("/user-settings/web-notifications",
|
||||||
|
{templateUrl: "user/web-notifications.html"})
|
||||||
$routeProvider.when("/change-email/:email_token",
|
$routeProvider.when("/change-email/:email_token",
|
||||||
{templateUrl: "user/change-email.html"})
|
{templateUrl: "user/change-email.html"})
|
||||||
$routeProvider.when("/cancel-account/:cancel_token",
|
$routeProvider.when("/cancel-account/:cancel_token",
|
||||||
|
@ -487,6 +489,19 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Notifications
|
||||||
|
$routeProvider.when("/notifications",
|
||||||
|
{
|
||||||
|
templateUrl: "notifications/notifications.html",
|
||||||
|
loader: true,
|
||||||
|
access: {
|
||||||
|
requiresLogin: true
|
||||||
|
},
|
||||||
|
controller: "Notifications",
|
||||||
|
controllerAs: "vm"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
$routeProvider.when("/profile/:slug",
|
$routeProvider.when("/profile/:slug",
|
||||||
{
|
{
|
||||||
templateUrl: "profile/profile.html",
|
templateUrl: "profile/profile.html",
|
||||||
|
@ -937,6 +952,7 @@ modules = [
|
||||||
"taigaExternalApps",
|
"taigaExternalApps",
|
||||||
"taigaDiscover",
|
"taigaDiscover",
|
||||||
"taigaHistory",
|
"taigaHistory",
|
||||||
|
"taigaNotifications",
|
||||||
"taigaWikiHistory",
|
"taigaWikiHistory",
|
||||||
"taigaEpics",
|
"taigaEpics",
|
||||||
"taigaUtils"
|
"taigaUtils"
|
||||||
|
|
|
@ -123,8 +123,10 @@ urls = {
|
||||||
"user-settings-user-project-settings": "/user-settings/user-project-settings"
|
"user-settings-user-project-settings": "/user-settings/user-project-settings"
|
||||||
"user-settings-mail-notifications": "/user-settings/mail-notifications"
|
"user-settings-mail-notifications": "/user-settings/mail-notifications"
|
||||||
"user-settings-live-notifications": "/user-settings/live-notifications"
|
"user-settings-live-notifications": "/user-settings/live-notifications"
|
||||||
|
"user-settings-web-notifications": "/user-settings/web-notifications"
|
||||||
"user-settings-contrib": "/user-settings/contrib/:plugin"
|
"user-settings-contrib": "/user-settings/contrib/:plugin"
|
||||||
|
|
||||||
|
"notifications": "/notifications"
|
||||||
}
|
}
|
||||||
|
|
||||||
init = ($log, $navurls) ->
|
init = ($log, $navurls) ->
|
||||||
|
|
|
@ -46,6 +46,7 @@ urls = {
|
||||||
# User - Notification
|
# User - Notification
|
||||||
"permissions": "/permissions"
|
"permissions": "/permissions"
|
||||||
"notify-policies": "/notify-policies"
|
"notify-policies": "/notify-policies"
|
||||||
|
"notifications": "/web-notifications"
|
||||||
|
|
||||||
# User Project Settings
|
# User Project Settings
|
||||||
"user-project-settings": "/user-project-settings"
|
"user-project-settings": "/user-project-settings"
|
||||||
|
|
|
@ -44,7 +44,7 @@ class UserLiveNotificationsController extends mixOf(taiga.Controller, taiga.Page
|
||||||
]
|
]
|
||||||
|
|
||||||
constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, @navUrls, @auth, @errorHandlingService) ->
|
constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, @navUrls, @auth, @errorHandlingService) ->
|
||||||
@scope.sectionName = "USER_SETTINGS.NOTIFICATIONS.LIVE_SECTION_NAME"
|
@scope.sectionName = "USER_SETTINGS.EVENTS.LIVE_SECTION_NAME"
|
||||||
@scope.user = @auth.getUser()
|
@scope.user = @auth.getUser()
|
||||||
promise = @.loadInitialData()
|
promise = @.loadInitialData()
|
||||||
promise.then null, @.onInitialDataError.bind(@)
|
promise.then null, @.onInitialDataError.bind(@)
|
||||||
|
|
|
@ -0,0 +1,121 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2018 Taiga Agile LLC
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# File: modules/user-settings/live-notifications.coffee
|
||||||
|
###
|
||||||
|
|
||||||
|
taiga = @.taiga
|
||||||
|
mixOf = @.taiga.mixOf
|
||||||
|
bindOnce = @.taiga.bindOnce
|
||||||
|
|
||||||
|
module = angular.module("taigaUserSettings")
|
||||||
|
|
||||||
|
|
||||||
|
#############################################################################
|
||||||
|
## User Web Notifications Controller
|
||||||
|
#############################################################################
|
||||||
|
|
||||||
|
class UserWebNotificationsController extends mixOf(taiga.Controller, taiga.PageMixin)
|
||||||
|
@.$inject = [
|
||||||
|
"$scope",
|
||||||
|
"$tgResources",
|
||||||
|
"$tgAuth"
|
||||||
|
]
|
||||||
|
|
||||||
|
constructor: (@scope, @rs, @auth) ->
|
||||||
|
@scope.sectionName = "USER_SETTINGS.EVENTS.SECTION_NAME"
|
||||||
|
@scope.user = @auth.getUser()
|
||||||
|
promise = @.loadInitialData()
|
||||||
|
promise.then null, @.onInitialDataError.bind(@)
|
||||||
|
|
||||||
|
loadInitialData: ->
|
||||||
|
return @rs.notifyPolicies.list().then (notifyPolicies) =>
|
||||||
|
@scope.notifyPolicies = notifyPolicies
|
||||||
|
return notifyPolicies
|
||||||
|
|
||||||
|
module.controller("UserWebNotificationsController", UserWebNotificationsController)
|
||||||
|
|
||||||
|
|
||||||
|
#############################################################################
|
||||||
|
## User Web Notifications Directive
|
||||||
|
#############################################################################
|
||||||
|
|
||||||
|
UserWebNotificationsDirective = () ->
|
||||||
|
link = ($scope, $el, $attrs) ->
|
||||||
|
$scope.$on "$destroy", ->
|
||||||
|
$el.off()
|
||||||
|
|
||||||
|
return {link:link}
|
||||||
|
|
||||||
|
module.directive("tgUserWebNotifications", UserWebNotificationsDirective)
|
||||||
|
|
||||||
|
|
||||||
|
#############################################################################
|
||||||
|
## User Web Notifications List Directive
|
||||||
|
#############################################################################
|
||||||
|
|
||||||
|
UserWebNotificationsListDirective = ($repo, $confirm, $compile) ->
|
||||||
|
template = _.template("""
|
||||||
|
<% _.each(notifyPolicies, function (notifyPolicy, index) { %>
|
||||||
|
<div class="policy-table-row">
|
||||||
|
<div class="policy-table-project"><span><%- notifyPolicy.project_name %></span></div>
|
||||||
|
<div class="policy-table-all">
|
||||||
|
<div class="check" data-index="<%- index %>">
|
||||||
|
<input type="checkbox"
|
||||||
|
<% if(notifyPolicy.web_notify_level) { %> checked="checked" <% } %>
|
||||||
|
name="policy-<%- notifyPolicy.id %>" id="policy-<%- notifyPolicy.id %>"/>
|
||||||
|
<div></div>
|
||||||
|
<span class="check-text check-yes" translate="COMMON.YES"></span>
|
||||||
|
<span class="check-text check-no"" translate="COMMON.NO"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% }) %>
|
||||||
|
""")
|
||||||
|
|
||||||
|
link = ($scope, $el, $attrs) ->
|
||||||
|
render = ->
|
||||||
|
$el.off()
|
||||||
|
|
||||||
|
ctx = {notifyPolicies: $scope.notifyPolicies}
|
||||||
|
html = template(ctx)
|
||||||
|
|
||||||
|
$el.html($compile(html)($scope))
|
||||||
|
|
||||||
|
$el.on "click", ".check", (event) ->
|
||||||
|
target = angular.element(event.currentTarget)
|
||||||
|
policyIndex = target.data('index')
|
||||||
|
policy = $scope.notifyPolicies[policyIndex]
|
||||||
|
policy.web_notify_level = !policy.web_notify_level
|
||||||
|
|
||||||
|
onSuccess = ->
|
||||||
|
$confirm.notify("success")
|
||||||
|
target.find("input").prop("checked", policy.web_notify_level)
|
||||||
|
|
||||||
|
onError = ->
|
||||||
|
$confirm.notify("error")
|
||||||
|
|
||||||
|
$repo.save(policy).then(onSuccess, onError)
|
||||||
|
|
||||||
|
$scope.$on "$destroy", ->
|
||||||
|
$el.off()
|
||||||
|
|
||||||
|
bindOnce($scope, $attrs.ngModel, render)
|
||||||
|
|
||||||
|
return {link:link}
|
||||||
|
|
||||||
|
module.directive("tgUserWebNotificationsList",
|
||||||
|
["$tgRepo", "$tgConfirm", "$compile", UserWebNotificationsListDirective])
|
|
@ -1550,7 +1550,8 @@
|
||||||
"USER_PROFILE": "User profile",
|
"USER_PROFILE": "User profile",
|
||||||
"CHANGE_PASSWORD": "Change password",
|
"CHANGE_PASSWORD": "Change password",
|
||||||
"EMAIL_NOTIFICATIONS": "Email notifications",
|
"EMAIL_NOTIFICATIONS": "Email notifications",
|
||||||
"DESKTOP_NOTIFICATIONS": "Desktop notifications"
|
"DESKTOP_NOTIFICATIONS": "Desktop notifications",
|
||||||
|
"EVENTS": "Events"
|
||||||
},
|
},
|
||||||
"NOTIFICATIONS": {
|
"NOTIFICATIONS": {
|
||||||
"LIVE_SECTION_NAME": "Desktop Notifications",
|
"LIVE_SECTION_NAME": "Desktop Notifications",
|
||||||
|
@ -1569,6 +1570,13 @@
|
||||||
"COLUMN_PROJECT": "Project",
|
"COLUMN_PROJECT": "Project",
|
||||||
"COLUMN_STARTPAGE": "Start page",
|
"COLUMN_STARTPAGE": "Start page",
|
||||||
"DEFAULT_VALUE": "Default"
|
"DEFAULT_VALUE": "Default"
|
||||||
|
},
|
||||||
|
"EVENTS": {
|
||||||
|
"SECTION_NAME": "Events",
|
||||||
|
"SECTION_DESCRIPTION": "Important events in Taiga header",
|
||||||
|
"SECTION_DESCRIPTION_EXPANDED": "(direct mentions, updates in items that you are watching...)",
|
||||||
|
"COLUMN_ENABLED": "Enabled",
|
||||||
|
"COLUMN_PROJECT": "Project"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"USER_PROFILE": {
|
"USER_PROFILE": {
|
||||||
|
@ -1666,7 +1674,25 @@
|
||||||
"US_REMOVED_FROM_MILESTONE": "{{username}} has added the US {{obj_name}} to the backlog",
|
"US_REMOVED_FROM_MILESTONE": "{{username}} has added the US {{obj_name}} to the backlog",
|
||||||
"BLOCKED": "{{username}} has blocked {{obj_name}}",
|
"BLOCKED": "{{username}} has blocked {{obj_name}}",
|
||||||
"UNBLOCKED": "{{username}} has unblocked {{obj_name}}",
|
"UNBLOCKED": "{{username}} has unblocked {{obj_name}}",
|
||||||
"NEW_USER": "{{username}} has joined Taiga"
|
"NEW_USER": "{{username}} has joined Taiga",
|
||||||
|
"ITEM_TYPES": {
|
||||||
|
"USERSTORY": "User Story",
|
||||||
|
"ISSUE": "Issue",
|
||||||
|
"TASK": "Task"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"EVENTS": {
|
||||||
|
"TITLE": "Events",
|
||||||
|
"MY_EVENTS": "My events",
|
||||||
|
"DISMISS_ALL": "Dismiss all",
|
||||||
|
"VIEW_ALL": "View all",
|
||||||
|
"NO_NEW_EVENTS": "No new events",
|
||||||
|
"ASSIGNED_YOU": "{{username}} assigned you to {{obj_name}}",
|
||||||
|
"ADDED_YOU_AS_WATCHER": "{{username}} added you as watcher on {{obj_name}}",
|
||||||
|
"ADDED_YOU_AS_MEMBER": "{{username}} added you as member",
|
||||||
|
"MENTIONED_YOU": "{{username}} mentioned you on {{obj_name}}",
|
||||||
|
"MENTIONED_YOU_IN_COMMENT": "{{username}} mentioned you in a comment on {{obj_name}}",
|
||||||
|
"COMMENTED": "{{username}} has commented on {{obj_name}}"
|
||||||
},
|
},
|
||||||
"LEGAL": {
|
"LEGAL": {
|
||||||
"TERMS_OF_SERVICE_AND_PRIVACY_POLICY_AD": "<span>When creating a new account, you agree to our </span><a href=\"{{ termsOfServiceUrl }}\" title=\"See terms of service\" target=\"_blank\">terms of service</a><span> and </span><a href=\"{{ privacyPolicyUrl }}\" title=\"See privacy policy\" target=\"_blank\">privacy policy</a>.",
|
"TERMS_OF_SERVICE_AND_PRIVACY_POLICY_AD": "<span>When creating a new account, you agree to our </span><a href=\"{{ termsOfServiceUrl }}\" title=\"See terms of service\" target=\"_blank\">terms of service</a><span> and </span><a href=\"{{ privacyPolicyUrl }}\" title=\"See privacy policy\" target=\"_blank\">privacy policy</a>.",
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2018 Taiga Agile LLC
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# File: navigation-bar/dropdown-notifications/dropdown-notifications.directive.coffee
|
||||||
|
###
|
||||||
|
|
||||||
|
timeout = @.taiga.timeout
|
||||||
|
|
||||||
|
DropdownNotificationsDirective = ($rootScope, notificationsService, currentUserService) ->
|
||||||
|
link = ($scope, $el, $attrs, $ctrl) ->
|
||||||
|
$scope.notificationsList = []
|
||||||
|
$scope.loading = true
|
||||||
|
|
||||||
|
$scope.$on "notifications:loaded", (event, total) ->
|
||||||
|
$scope.loading = false
|
||||||
|
if $scope.total != undefined && total > $scope.total
|
||||||
|
$scope.newEvent = true
|
||||||
|
timeout 100, ->
|
||||||
|
$scope.total = total
|
||||||
|
$scope.$apply()
|
||||||
|
timeout 2000, ->
|
||||||
|
$scope.newEvent = false
|
||||||
|
else
|
||||||
|
$scope.total = total
|
||||||
|
|
||||||
|
$scope.$on "notifications:loading", () ->
|
||||||
|
$scope.loading = true
|
||||||
|
|
||||||
|
$scope.setAllAsRead = () ->
|
||||||
|
notificationsService.setNotificationsAsRead().then ->
|
||||||
|
$rootScope.$emit("notifications:updated")
|
||||||
|
|
||||||
|
directive = {
|
||||||
|
templateUrl: "navigation-bar/dropdown-notifications/dropdown-notifications.html"
|
||||||
|
scope: true
|
||||||
|
link: link
|
||||||
|
}
|
||||||
|
|
||||||
|
return directive
|
||||||
|
|
||||||
|
angular.module("taigaNavigationBar")
|
||||||
|
.directive("tgDropdownNotifications", ["$rootScope", "tgNotificationsService",
|
||||||
|
"tgCurrentUserService", DropdownNotificationsDirective])
|
|
@ -0,0 +1,26 @@
|
||||||
|
a(
|
||||||
|
href=""
|
||||||
|
title="{{ 'EVENTS.TITLE' | translate }}"
|
||||||
|
tg-nav="notifications"
|
||||||
|
ng-mouseover="visible=true"
|
||||||
|
)
|
||||||
|
tg-svg(svg-icon="icon-bell")
|
||||||
|
div.counter(ng-if="total", ng-class="{'active': newEvent}") {{ total }}
|
||||||
|
div.navbar-dropdown-notifications(
|
||||||
|
ng-show="visible",
|
||||||
|
ng-mouseleave="visible=false"
|
||||||
|
)
|
||||||
|
.header
|
||||||
|
span.notifications-title {{ 'EVENTS.MY_EVENTS' | translate }}
|
||||||
|
a(href="", ng-click="setAllAsRead()") {{ 'EVENTS.DISMISS_ALL' | translate }}
|
||||||
|
a(tg-nav="notifications") {{ 'EVENTS.VIEW_ALL' | translate }}
|
||||||
|
|
||||||
|
.notifications-wrapper
|
||||||
|
.empty(ng-if="!total && !loading")
|
||||||
|
span {{ 'EVENTS.NO_NEW_EVENTS' | translate }}
|
||||||
|
|
||||||
|
tg-notifications-list#my-notifications.dropdown-notifications-list(
|
||||||
|
only-unread="true",
|
||||||
|
infinite-scroll-container="#my-notifications"
|
||||||
|
infinite-scroll-distance="1"
|
||||||
|
)
|
|
@ -54,4 +54,9 @@ nav.navbar(ng-if="vm.isEnabledHeader")
|
||||||
tg-svg(svg-icon="icon-discover")
|
tg-svg(svg-icon="icon-discover")
|
||||||
|
|
||||||
div.topnav-dropdown-wrapper(ng-show="vm.projects.size", tg-dropdown-project-list)
|
div.topnav-dropdown-wrapper(ng-show="vm.projects.size", tg-dropdown-project-list)
|
||||||
|
|
||||||
|
div.topnav-dropdown-wrapper(
|
||||||
|
tg-dropdown-notifications
|
||||||
|
)
|
||||||
|
|
||||||
div.topnav-dropdown-wrapper(tg-dropdown-user)
|
div.topnav-dropdown-wrapper(tg-dropdown-user)
|
||||||
|
|
|
@ -37,7 +37,8 @@ $dropdown-width: 350px;
|
||||||
}
|
}
|
||||||
.nav-right {
|
.nav-right {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
a {
|
> a,
|
||||||
|
.topnav-dropdown-wrapper > a {
|
||||||
color: $white;
|
color: $white;
|
||||||
padding: .5rem 2rem;
|
padding: .5rem 2rem;
|
||||||
}
|
}
|
||||||
|
@ -47,7 +48,8 @@ $dropdown-width: 350px;
|
||||||
transition: all .2s linear;
|
transition: all .2s linear;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
a {
|
> a,
|
||||||
|
.topnav-dropdown-wrapper > a {
|
||||||
color: $white;
|
color: $white;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
transition: all .2s linear;
|
transition: all .2s linear;
|
||||||
|
@ -70,7 +72,7 @@ $dropdown-width: 350px;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
img {
|
.user-avatar img {
|
||||||
height: 2.5rem;
|
height: 2.5rem;
|
||||||
margin-left: .5rem;
|
margin-left: .5rem;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
@ -169,3 +171,106 @@ $dropdown-width: 350px;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@keyframes highlightFadeOut {
|
||||||
|
0% {
|
||||||
|
background: $primary-light;
|
||||||
|
font-size: .75rem;
|
||||||
|
height: 22px;
|
||||||
|
left: 49px;
|
||||||
|
line-height: 22px;
|
||||||
|
top: -1px;
|
||||||
|
width: 22px;
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
font-size: .7rem;
|
||||||
|
height: 20px;
|
||||||
|
left: 50px;
|
||||||
|
line-height: 20px;
|
||||||
|
top: 0;
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background: $primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.topnav-dropdown-wrapper {
|
||||||
|
position: relative;
|
||||||
|
&:hover {
|
||||||
|
.navbar-dropdown-notifications {
|
||||||
|
animation: dropdownFade .2s cubic-bezier(.09, 0, .99, .01) both;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.counter {
|
||||||
|
background: $primary;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: $white;
|
||||||
|
font-size: .7rem;
|
||||||
|
height: 20px;
|
||||||
|
left: 50px;
|
||||||
|
line-height: 20px;
|
||||||
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
top: 0;
|
||||||
|
width: 20px;
|
||||||
|
&.active {
|
||||||
|
animation-duration: 2s;
|
||||||
|
animation-name: highlightFadeOut;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navbar-dropdown-notifications {
|
||||||
|
$width: 450px;
|
||||||
|
background: $white;
|
||||||
|
border: 1px solid $whitish;
|
||||||
|
border-radius: 2px;
|
||||||
|
box-shadow: 0 0 3px 3px rgba($gray-lighter, .2);
|
||||||
|
color: $blackish;
|
||||||
|
display: none;
|
||||||
|
left: calc(50% - 450px/2);
|
||||||
|
margin-top: 1px;
|
||||||
|
min-width: 450px;
|
||||||
|
padding: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: 2.4rem;
|
||||||
|
z-index: 999;
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
color: $gray-light;
|
||||||
|
padding: 2.5em 0;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
@include arrow('bottom', $mass-white, $mass-white, 1, 8);
|
||||||
|
background: $mass-white;
|
||||||
|
color: $gray;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
padding: .6rem 0 .5rem .7rem;
|
||||||
|
.notifications-title {
|
||||||
|
flex-grow: 1;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: $gray-light;
|
||||||
|
font-size: .9rem;
|
||||||
|
padding: 0 .7rem 0 0;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: $primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.notifications-wrapper {
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
.notifications-list {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2018 Taiga Agile LLC
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# File: notifications/notifications-list/notifications-list.directive.coffee
|
||||||
|
###
|
||||||
|
|
||||||
|
NotificationsListDirective = ->
|
||||||
|
return {
|
||||||
|
templateUrl: "notifications/notifications-list/notifications-list.html",
|
||||||
|
controller: "Notifications",
|
||||||
|
controllerAs: "vm",
|
||||||
|
bindToController: true,
|
||||||
|
scope: {
|
||||||
|
infiniteScrollContainer: "@",
|
||||||
|
infiniteScrollDistance: "=",
|
||||||
|
onlyUnread: "=onlyUnread"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
angular.module("taigaNotifications").directive("tgNotificationsList", NotificationsListDirective)
|
|
@ -0,0 +1,36 @@
|
||||||
|
section.notifications-list
|
||||||
|
div(ng-if="vm.loading")
|
||||||
|
div.spin
|
||||||
|
img(
|
||||||
|
src="/#{v}/svg/spinner-circle.svg"
|
||||||
|
alt="Loading..."
|
||||||
|
)
|
||||||
|
div(
|
||||||
|
ng-if="!vm.loading"
|
||||||
|
infinite-scroll="vm.loadNotifications()"
|
||||||
|
infinite-scroll-disabled="vm.scrollDisabled"
|
||||||
|
ng-attr-infinite-scroll-container="vm.infiniteScrollContainer"
|
||||||
|
ng-attr-infinite-scroll-distance="vm.infiniteScrollDistance"
|
||||||
|
)
|
||||||
|
.entry(
|
||||||
|
tg-repeat="notification in vm.notificationsList"
|
||||||
|
ng-class="{'new': !notification.get('read')}"
|
||||||
|
)
|
||||||
|
.entry-avatar
|
||||||
|
// profile image with url
|
||||||
|
.profile-picture(ng-if="notification.getIn(['data', 'user', 'is_profile_visible'])")
|
||||||
|
a(tg-nav="user-profile:username=notification.getIn(['data', 'user', 'username'])", title="{{::notification.getIn(['data', 'user', 'name']) }}")
|
||||||
|
img(
|
||||||
|
tg-avatar="notification.getIn(['data', 'user'])"
|
||||||
|
alt="{{::notification.getIn(['data', 'user', 'name'])}}"
|
||||||
|
)
|
||||||
|
// profile image without url
|
||||||
|
.profile-picture(ng-if="!notification.getIn(['data', 'user', 'is_profile_visible'])")
|
||||||
|
img(
|
||||||
|
tg-avatar="notification.getIn(['data', 'user'])"
|
||||||
|
alt="{{::notification.getIn(['data', 'user', 'name'])}}"
|
||||||
|
)
|
||||||
|
.entry-content
|
||||||
|
p(tg-compile-html="notification.get('title_html')")
|
||||||
|
span.entry-project {{::notification.getIn(['data', 'project', 'name'])}}
|
||||||
|
.entry-date {{::notification.get('created') | momentFromNow}}
|
|
@ -0,0 +1,101 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2018 Taiga Agile LLC
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# File: notifications/notifications.controller.coffee
|
||||||
|
###
|
||||||
|
|
||||||
|
taiga = @.taiga
|
||||||
|
|
||||||
|
mixOf = @.taiga.mixOf
|
||||||
|
debounceLeading = @.taiga.debounceLeading
|
||||||
|
|
||||||
|
class NotificationsController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.FiltersMixin)
|
||||||
|
@.$inject = [
|
||||||
|
"$rootScope",
|
||||||
|
"$scope",
|
||||||
|
"tgNotificationsService"
|
||||||
|
"tgCurrentUserService",
|
||||||
|
"$tgEvents",
|
||||||
|
"$location"
|
||||||
|
"$window"
|
||||||
|
]
|
||||||
|
|
||||||
|
constructor: (@rootScope, @scope, @notificationsService, @currentUserService, @events,
|
||||||
|
@location, @window) ->
|
||||||
|
@.total = 0
|
||||||
|
@.user = @currentUserService.getUser()
|
||||||
|
@.scrollDisabled = false
|
||||||
|
@.initList()
|
||||||
|
@.initializeSubscription()
|
||||||
|
@.loadNotifications()
|
||||||
|
|
||||||
|
@rootScope.$on "notifications:updated", (event) =>
|
||||||
|
@.reloadList()
|
||||||
|
|
||||||
|
initList: ()->
|
||||||
|
@.notificationsList = Immutable.List()
|
||||||
|
@.list = @notificationsService.getNotificationsList(@.user.get("id"), @.onlyUnread?)
|
||||||
|
@.loading = !@.list?
|
||||||
|
|
||||||
|
reloadList: ()->
|
||||||
|
@.initList()
|
||||||
|
@.loadNotifications()
|
||||||
|
|
||||||
|
loadNotifications: () ->
|
||||||
|
@.scrollDisabled = true
|
||||||
|
@.loading = true
|
||||||
|
@scope.$emit("notifications:loading")
|
||||||
|
return @.list
|
||||||
|
.next()
|
||||||
|
.then (response) =>
|
||||||
|
@.notificationsList = @.notificationsList.concat(response.get("items"))
|
||||||
|
|
||||||
|
if response.get("next")
|
||||||
|
@.scrollDisabled = false
|
||||||
|
|
||||||
|
@.total = response.get("total")
|
||||||
|
|
||||||
|
@scope.$emit("notifications:loaded", @.total)
|
||||||
|
|
||||||
|
@.loading = false
|
||||||
|
return @.notificationsList
|
||||||
|
|
||||||
|
setAsRead: (notification, url) ->
|
||||||
|
@.loading = true
|
||||||
|
@scope.$emit("notifications:loading")
|
||||||
|
@notificationsService.setNotificationAsRead(notification.get("id")).then =>
|
||||||
|
if @location.$$url == url
|
||||||
|
@window.location.reload()
|
||||||
|
else
|
||||||
|
@rootScope.$broadcast "notifications:updated"
|
||||||
|
@location.path(url)
|
||||||
|
|
||||||
|
setAllAsRead: () ->
|
||||||
|
@.loading = true
|
||||||
|
@scope.$emit("notifications:loading")
|
||||||
|
@notificationsService.setNotificationsAsRead().then =>
|
||||||
|
@rootScope.$emit("notifications:updated")
|
||||||
|
|
||||||
|
initializeSubscription: ->
|
||||||
|
routingKey = "web_notifications.#{@.user.get("id")}"
|
||||||
|
randomTimeout = taiga.randomInt(700, 1000)
|
||||||
|
@events.subscribe(
|
||||||
|
@scope,
|
||||||
|
routingKey,
|
||||||
|
debounceLeading(randomTimeout, (message) => @rootScope.$broadcast "notifications:updated")
|
||||||
|
)
|
||||||
|
|
||||||
|
angular.module("taigaNotifications").controller("Notifications", NotificationsController)
|
|
@ -0,0 +1,10 @@
|
||||||
|
div.wrapper
|
||||||
|
div.notifications-page.centered
|
||||||
|
header.header
|
||||||
|
h1.title {{ 'EVENTS.MY_EVENTS' | translate }}
|
||||||
|
a.action(
|
||||||
|
href="",
|
||||||
|
ng-if="!vm.notificationsList.get(0).get('read')",
|
||||||
|
ng-click="vm.setAllAsRead()") {{ 'EVENTS.DISMISS_ALL' | translate }}
|
||||||
|
span.action.disabled(ng-if="vm.notificationsList.get(0).get('read')") {{ 'EVENTS.DISMISS_ALL' | translate }}
|
||||||
|
tg-notifications-list()
|
|
@ -0,0 +1,20 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2018 Taiga Agile LLC
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# File: projects/projects.module.coffee
|
||||||
|
###
|
||||||
|
|
||||||
|
angular.module("taigaNotifications", [])
|
|
@ -0,0 +1,128 @@
|
||||||
|
.notifications-page {
|
||||||
|
margin-top: 1.5em;
|
||||||
|
min-width: initial;
|
||||||
|
padding-bottom: 5em;
|
||||||
|
.header {
|
||||||
|
align-items: center;
|
||||||
|
background: $mass-white;
|
||||||
|
display: flex;
|
||||||
|
margin: 0;
|
||||||
|
padding: .5em 1em;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
color: $black;
|
||||||
|
flex-grow: 1;
|
||||||
|
font-size: 1.8em;
|
||||||
|
line-height: 1.8em;
|
||||||
|
margin: 0;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action {
|
||||||
|
color: $primary;
|
||||||
|
font-size: 1em;
|
||||||
|
line-height: .75em;
|
||||||
|
margin-right: 1em;
|
||||||
|
&.disabled {
|
||||||
|
color: $gray-lighter;
|
||||||
|
}
|
||||||
|
&:not(.disabled):hover {
|
||||||
|
color: $primary-light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.notifications-list .entry {
|
||||||
|
align-items: center;
|
||||||
|
font-size: .95rem;
|
||||||
|
margin: .5rem 0;
|
||||||
|
padding: 1rem .6rem;
|
||||||
|
&.new {
|
||||||
|
background-color: $primary-lighter;
|
||||||
|
}
|
||||||
|
.entry-content {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
}
|
||||||
|
.entry-project {
|
||||||
|
max-width: initial;
|
||||||
|
text-overflow: initial;
|
||||||
|
white-space: initial;
|
||||||
|
}
|
||||||
|
.entry-avatar {
|
||||||
|
flex-basis: 3rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
width: 3rem;
|
||||||
|
}
|
||||||
|
.entry-date {
|
||||||
|
font-size: .85rem;
|
||||||
|
margin-right: .5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notifications-list {
|
||||||
|
.entry {
|
||||||
|
border-bottom: 1px solid $whitish;
|
||||||
|
color: $blackish;
|
||||||
|
display: flex;
|
||||||
|
font-size: .9rem;
|
||||||
|
margin: 0 .8rem;
|
||||||
|
padding: .8rem 0;
|
||||||
|
position: relative;
|
||||||
|
p {
|
||||||
|
line-height: 1.25em;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
@include font-type(bold);
|
||||||
|
&.user-link,
|
||||||
|
&.project-link {
|
||||||
|
color: $blackish;
|
||||||
|
}
|
||||||
|
&.object-link {
|
||||||
|
color: $primary;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
color: $primary-light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.entry-avatar {
|
||||||
|
border-radius: .1rem;
|
||||||
|
flex-basis: 2.5rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: .7rem;
|
||||||
|
vertical-align: center;
|
||||||
|
width: 2.5rem;
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.entry-content {
|
||||||
|
flex-grow: 1;
|
||||||
|
line-height: 1.1em;
|
||||||
|
margin-right: .7rem;
|
||||||
|
}
|
||||||
|
.entry-project {
|
||||||
|
@include font-type(bold);
|
||||||
|
color: $gray-lighter;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: .9rem;
|
||||||
|
margin-top: .5em;
|
||||||
|
max-width: 200px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.entry-date {
|
||||||
|
font-size: .7rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.spin {
|
||||||
|
margin: 5% auto;
|
||||||
|
width: 3rem;
|
||||||
|
img {
|
||||||
|
@include loading-spinner;
|
||||||
|
max-height: 3rem;
|
||||||
|
max-width: 3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,191 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2018 Taiga Agile LLC
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# File: user-notification/user-notification/user-notification.service.coffee
|
||||||
|
###
|
||||||
|
|
||||||
|
taiga = @.taiga
|
||||||
|
|
||||||
|
class NotificationsService extends taiga.Service
|
||||||
|
@.$inject = [
|
||||||
|
"tgResources",
|
||||||
|
"tgUserTimelinePaginationSequenceService",
|
||||||
|
"$translate",
|
||||||
|
"$tgNavUrls"
|
||||||
|
]
|
||||||
|
|
||||||
|
_notificationTypes = [
|
||||||
|
{ # Assigned to you
|
||||||
|
check: (notification) -> return notification.get('event_type') == 1
|
||||||
|
key: 'EVENTS.ASSIGNED_YOU',
|
||||||
|
translate_params: ['username', 'obj_name']
|
||||||
|
},
|
||||||
|
{ # Mentioned you in a object description
|
||||||
|
check: (notification) -> return notification.get('event_type') == 2
|
||||||
|
key: 'EVENTS.MENTIONED_YOU',
|
||||||
|
translate_params: ['username', 'obj_name'],
|
||||||
|
},
|
||||||
|
{ # Added you as watcher
|
||||||
|
check: (notification) -> return notification.get('event_type') == 3
|
||||||
|
key: 'EVENTS.ADDED_YOU_AS_WATCHER',
|
||||||
|
translate_params: ['username', 'obj_name'],
|
||||||
|
},
|
||||||
|
{ # Added you as member
|
||||||
|
check: (notification) -> return notification.get('event_type') == 4
|
||||||
|
key: 'EVENTS.ADDED_YOU_AS_MEMBER',
|
||||||
|
translate_params: ['username']
|
||||||
|
},
|
||||||
|
{ # Commented
|
||||||
|
check: (notification) -> return notification.get('event_type') == 5
|
||||||
|
key: 'EVENTS.COMMENTED',
|
||||||
|
translate_params: ['username', 'obj_name'],
|
||||||
|
},
|
||||||
|
{ # Mentioned you in a comment
|
||||||
|
check: (notification) -> return notification.get('event_type') == 6
|
||||||
|
key: 'EVENTS.MENTIONED_YOU_IN_COMMENT',
|
||||||
|
translate_params: ['username', 'obj_name'],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
_params = {
|
||||||
|
username: (notification) ->
|
||||||
|
user = notification.getIn(['data', 'user'])
|
||||||
|
if user.get('is_profile_visible')
|
||||||
|
title_attr = @translate.instant('COMMON.SEE_USER_PROFILE', {username: user.get('username')})
|
||||||
|
url = @navUrls.resolve('user-profile', {
|
||||||
|
username: notification.getIn(['data', 'user', 'username'])
|
||||||
|
})
|
||||||
|
return @._getLink(notification, url, user.get('name'), 'user-link', title_attr)
|
||||||
|
else
|
||||||
|
return @._getUsernameSpan(user.get('name'))
|
||||||
|
|
||||||
|
project_name: (notification) ->
|
||||||
|
url = @navUrls.resolve('project', {
|
||||||
|
project: notification.getIn(['data', 'project', 'slug'])
|
||||||
|
})
|
||||||
|
return @._getLink(notification, url, notification.getIn(["data", "project", "name"]), 'project-link')
|
||||||
|
|
||||||
|
obj_name: (notification) ->
|
||||||
|
obj = @._getNotificationObject(notification)
|
||||||
|
url = @._getDetailObjUrl(notification, obj.get('content_type'))
|
||||||
|
text = '#' + obj.get('ref') + ' ' + obj.get('subject')
|
||||||
|
return @._getLink(notification, url, text, 'object-link' )
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor: (
|
||||||
|
@rs,
|
||||||
|
@userTimelinePaginationSequenceService,
|
||||||
|
@translate,
|
||||||
|
@navUrls
|
||||||
|
) ->
|
||||||
|
|
||||||
|
getNotificationsList: (userId, onlyUnread) ->
|
||||||
|
total = 0
|
||||||
|
config = {}
|
||||||
|
config.fetch = (page) =>
|
||||||
|
return @rs.users.getNotifications(userId, page, onlyUnread)
|
||||||
|
.then (response) ->
|
||||||
|
return response
|
||||||
|
|
||||||
|
config.map = (obj) => @._addNotificationAttributes(obj)
|
||||||
|
return @userTimelinePaginationSequenceService.generate(config)
|
||||||
|
|
||||||
|
setNotificationAsRead: (notificationId) ->
|
||||||
|
return @rs.users.setNotificationAsRead(notificationId)
|
||||||
|
|
||||||
|
setNotificationsAsRead: () ->
|
||||||
|
return @rs.users.setNotificationsAsRead()
|
||||||
|
|
||||||
|
_getNotificationObject: (notification) ->
|
||||||
|
if notification.get('data').get('obj')
|
||||||
|
return notification.get('data').get('obj')
|
||||||
|
|
||||||
|
_getType: (notification) ->
|
||||||
|
return _.find _notificationTypes, (obj) ->
|
||||||
|
return obj.check(notification)
|
||||||
|
|
||||||
|
_addNotificationAttributes: (notification) ->
|
||||||
|
event_type = notification.get('event_type')
|
||||||
|
|
||||||
|
type = @._getType(notification)
|
||||||
|
|
||||||
|
title = @._getTitle(notification, event_type, type)
|
||||||
|
|
||||||
|
notification = notification.set('title_html', title)
|
||||||
|
notification = notification.set('obj', @._getNotificationObject(notification))
|
||||||
|
|
||||||
|
return notification
|
||||||
|
|
||||||
|
_translateTitleParams: (param, notification, event) ->
|
||||||
|
return _params[param].call(this, notification, event)
|
||||||
|
|
||||||
|
_getDetailObjUrl: (notification, contentType) ->
|
||||||
|
urlMapping = {
|
||||||
|
"issue": "project-issues-detail",
|
||||||
|
"task": "project-tasks-detail",
|
||||||
|
"userstory": "project-userstories-detail",
|
||||||
|
}
|
||||||
|
url = @navUrls.resolve(urlMapping[contentType], {
|
||||||
|
project: notification.getIn(['data', 'project', 'slug']),
|
||||||
|
ref: notification.getIn(['data', 'obj', 'ref'])
|
||||||
|
})
|
||||||
|
|
||||||
|
return url
|
||||||
|
|
||||||
|
_getLink: (notification, url, text, css, title) ->
|
||||||
|
title = title || text
|
||||||
|
|
||||||
|
span = $('<span>')
|
||||||
|
.attr('ng-non-bindable', true)
|
||||||
|
.text(text)
|
||||||
|
|
||||||
|
return $('<a href="">')
|
||||||
|
.attr('title', title)
|
||||||
|
.attr('class', css)
|
||||||
|
.attr('ng-click', "vm.setAsRead(notification, \"#{url}\")")
|
||||||
|
.append(span)
|
||||||
|
.prop('outerHTML')
|
||||||
|
|
||||||
|
_getUsernameSpan: (text) ->
|
||||||
|
title = title || text
|
||||||
|
|
||||||
|
return $('<span>')
|
||||||
|
.addClass('username')
|
||||||
|
.text(text)
|
||||||
|
.prop('outerHTML')
|
||||||
|
|
||||||
|
_getParams: (notification, event_type, notification_type) ->
|
||||||
|
params = {}
|
||||||
|
|
||||||
|
notification_type.translate_params.forEach (param) =>
|
||||||
|
params[param] = @._translateTitleParams(param, notification, event_type)
|
||||||
|
return params
|
||||||
|
|
||||||
|
_getTitle: (notification, event_type, notification_type) ->
|
||||||
|
params = @._getParams(notification, event_type, notification_type)
|
||||||
|
|
||||||
|
paramsKeys = {}
|
||||||
|
Object.keys(params).forEach (key) -> paramsKeys[key] = '{{' +key + '}}'
|
||||||
|
|
||||||
|
translation = @translate.instant(notification_type.key, paramsKeys)
|
||||||
|
|
||||||
|
Object.keys(params).forEach (key) ->
|
||||||
|
find = '{{' +key + '}}'
|
||||||
|
translation = translation.replace(new RegExp(find, 'g'), params[key])
|
||||||
|
|
||||||
|
return translation
|
||||||
|
|
||||||
|
angular.module("taigaNotifications").service("tgNotificationsService", NotificationsService)
|
|
@ -150,6 +150,41 @@ Resource = (urlsService, http, paginateResponseService) ->
|
||||||
result = Immutable.fromJS(result)
|
result = Immutable.fromJS(result)
|
||||||
return paginateResponseService(result)
|
return paginateResponseService(result)
|
||||||
|
|
||||||
|
service.getNotifications = (userId, page, onlyUnread) ->
|
||||||
|
params = {
|
||||||
|
page: page
|
||||||
|
}
|
||||||
|
if onlyUnread
|
||||||
|
params['only_unread'] = true
|
||||||
|
|
||||||
|
url = urlsService.resolve("notifications")
|
||||||
|
|
||||||
|
return http.get(url, params, {
|
||||||
|
headers: {
|
||||||
|
'x-lazy-pagination': true
|
||||||
|
}
|
||||||
|
}).then (result) ->
|
||||||
|
result = Immutable.fromJS(result)
|
||||||
|
paginateResponse = Immutable.Map({
|
||||||
|
"data": result.get("data").get("objects"),
|
||||||
|
"next": !!result.get("headers")("x-pagination-next"),
|
||||||
|
"prev": !!result.get("headers")("x-pagination-prev"),
|
||||||
|
"current": result.get("headers")("x-pagination-current"),
|
||||||
|
"count": result.get("headers")("x-pagination-count"),
|
||||||
|
"total": result.get("data").get("total")
|
||||||
|
})
|
||||||
|
return paginateResponse
|
||||||
|
|
||||||
|
service.setNotificationAsRead = (notificationId) ->
|
||||||
|
url = "#{urlsService.resolve("notifications")}/#{notificationId}/set-as-read"
|
||||||
|
return http.patch(url).then (result) ->
|
||||||
|
return result
|
||||||
|
|
||||||
|
service.setNotificationsAsRead = () ->
|
||||||
|
url = "#{urlsService.resolve("notifications")}/set-as-read"
|
||||||
|
return http.post(url).then (result) ->
|
||||||
|
return result
|
||||||
|
|
||||||
return () ->
|
return () ->
|
||||||
return {"users": service}
|
return {"users": service}
|
||||||
|
|
||||||
|
|
|
@ -47,11 +47,14 @@ UserTimelinePaginationSequence = () ->
|
||||||
if items.size < config.minItems && response.get("next")
|
if items.size < config.minItems && response.get("next")
|
||||||
return getContent()
|
return getContent()
|
||||||
|
|
||||||
return Immutable.Map({
|
pagination = Immutable.Map({
|
||||||
items: items,
|
items: items,
|
||||||
|
total: response.get("total"),
|
||||||
next: response.get("next")
|
next: response.get("next")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return pagination
|
||||||
|
|
||||||
return {
|
return {
|
||||||
next: () -> next()
|
next: () -> next()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
section.profile-timeline
|
section.profile-timeline
|
||||||
div(ng-if="!vm.timelineList.size")
|
div(ng-if="vm.loading")
|
||||||
div.spin
|
div.spin
|
||||||
img(
|
img(
|
||||||
src="/#{v}/svg/spinner-circle.svg"
|
src="/#{v}/svg/spinner-circle.svg"
|
||||||
|
|
|
@ -16,6 +16,9 @@ section.admin-menu
|
||||||
li#usersettingsmenu-live-notifications
|
li#usersettingsmenu-live-notifications
|
||||||
a(href="", tg-nav="user-settings-live-notifications", title="{{ 'USER_SETTINGS.MENU.DESKTOP_NOTIFICATIONS' | translate }}")
|
a(href="", tg-nav="user-settings-live-notifications", title="{{ 'USER_SETTINGS.MENU.DESKTOP_NOTIFICATIONS' | translate }}")
|
||||||
span.title(translate="USER_SETTINGS.MENU.DESKTOP_NOTIFICATIONS")
|
span.title(translate="USER_SETTINGS.MENU.DESKTOP_NOTIFICATIONS")
|
||||||
|
li#usersettingsmenu-web-notifications
|
||||||
|
a(href="", tg-nav="user-settings-web-notifications", title="{{ 'USER_SETTINGS.MENU.EVENTS' | translate }}")
|
||||||
|
span.title(translate="USER_SETTINGS.MENU.EVENTS")
|
||||||
li#usersettings-contrib(ng-repeat="plugin in userSettingsPlugins")
|
li#usersettings-contrib(ng-repeat="plugin in userSettingsPlugins")
|
||||||
a(
|
a(
|
||||||
href=""
|
href=""
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
section.policy-table
|
||||||
|
div.policy-table-header
|
||||||
|
div.policy-table-row
|
||||||
|
div.policy-table-project
|
||||||
|
span(translate="USER_SETTINGS.EVENTS.COLUMN_PROJECT")
|
||||||
|
div.policy-table-enabled
|
||||||
|
span(translate="USER_SETTINGS.EVENTS.COLUMN_ENABLED")
|
||||||
|
div.policy-table-body(tg-user-web-notifications-list, ng-model="notifyPolicies")
|
|
@ -0,0 +1,24 @@
|
||||||
|
doctype html
|
||||||
|
|
||||||
|
div.wrapper(
|
||||||
|
tg-user-web-notifications
|
||||||
|
ng-controller="UserWebNotificationsController as ctrl",
|
||||||
|
ng-init="section='web-notifications'"
|
||||||
|
)
|
||||||
|
|
||||||
|
sidebar.menu-secondary.sidebar.settings-nav(tg-user-settings-navigation="web-notifications")
|
||||||
|
include ../includes/modules/user-settings-menu
|
||||||
|
|
||||||
|
section.main.admin-common
|
||||||
|
header
|
||||||
|
h1
|
||||||
|
span.green
|
||||||
|
| {{sectionName | translate}}
|
||||||
|
tg-svg.green(svg-icon="icon-bell")
|
||||||
|
|
||||||
|
p.total
|
||||||
|
span(translate="USER_SETTINGS.EVENTS.SECTION_DESCRIPTION")
|
||||||
|
|
|
||||||
|
small(translate="USER_SETTINGS.EVENTS.SECTION_DESCRIPTION_EXPANDED")
|
||||||
|
|
||||||
|
include ../includes/modules/user-settings/web-notifications-table
|
|
@ -68,6 +68,7 @@ h1 {
|
||||||
|
|
||||||
.green {
|
.green {
|
||||||
color: $primary;
|
color: $primary;
|
||||||
|
fill: $primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
.date {
|
.date {
|
||||||
|
|
|
@ -14,7 +14,8 @@
|
||||||
.policy-table-project,
|
.policy-table-project,
|
||||||
.policy-table-all,
|
.policy-table-all,
|
||||||
.policy-table-involved,
|
.policy-table-involved,
|
||||||
.policy-table-none {
|
.policy-table-none,
|
||||||
|
.policy-table-enabled {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,7 +26,8 @@
|
||||||
|
|
||||||
.policy-table-all,
|
.policy-table-all,
|
||||||
.policy-table-involved,
|
.policy-table-involved,
|
||||||
.policy-table-none {
|
.policy-table-none,
|
||||||
|
.policy-table-enabled {
|
||||||
flex-basis: 0;
|
flex-basis: 0;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,13 @@
|
||||||
<svg height="0" style="position: absolute; width: 0; height: 0;" version="1.1" width="0" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg">
|
<svg height="0" style="position: absolute; width: 0; height: 0;" version="1.1" width="0" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg">
|
||||||
<defs>
|
<defs>
|
||||||
|
<symbol id="icon-bell" viewBox="0 0 100 100">
|
||||||
|
<title>bell</title>
|
||||||
|
<g transform="translate(0,-191.16665)">
|
||||||
|
<path style="fill-opacity:1;stroke-width:7.27812004;"
|
||||||
|
d="m 52.916943,191.16665 c -7.112236,0 -13.04271,5.15386 -14.29523,11.91257 -18.460239,8.56605 -31.7004628,32.57982 -31.7890308,54.8856 l -0.01114,13.1094 H 99.011791 l -0.0232,-13.1094 c -0.08417,-22.30704 -13.315506,-46.3206 -31.778082,-54.8856 -1.252625,-6.75871 -7.182994,-11.91257 -14.295293,-11.91257 z M 38.360599,279.8647 v 1.15368 c 0.03368,8.75953 6.489039,15.97886 14.556344,15.9816 8.067957,-10e-4 14.503375,-7.22165 14.54548,-15.9816 l 0.01116,-1.15368 z" id="path10199" inkscape:connector-curvature="0">
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
</symbol>
|
||||||
<symbol id="icon-iocaine" viewBox="0 0 1024 1024">
|
<symbol id="icon-iocaine" viewBox="0 0 1024 1024">
|
||||||
<title>iocaine</title>
|
<title>iocaine</title>
|
||||||
<path
|
<path
|
||||||
|
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 74 KiB |
|
@ -14,6 +14,7 @@ $white: #fff;
|
||||||
|
|
||||||
// Primary colors
|
// Primary colors
|
||||||
$primary-light: #212121;
|
$primary-light: #212121;
|
||||||
|
$primary-lighter: #E0E0E0;
|
||||||
$primary: #000;
|
$primary: #000;
|
||||||
$primary-dark: #000;
|
$primary-dark: #000;
|
||||||
$primary-background: #dfdfdf;
|
$primary-background: #dfdfdf;
|
||||||
|
|
|
@ -17,6 +17,7 @@ $mass-white: #f5f5f5;
|
||||||
|
|
||||||
// Primary colors
|
// Primary colors
|
||||||
$primary-light: #8c9eff;
|
$primary-light: #8c9eff;
|
||||||
|
$primary-lighter: #ECEDFB;
|
||||||
$primary: #3f51b5;
|
$primary: #3f51b5;
|
||||||
$primary-dark: #1a237e;
|
$primary-dark: #1a237e;
|
||||||
$primary-background: #929dd8;
|
$primary-background: #929dd8;
|
||||||
|
|
|
@ -17,6 +17,7 @@ $mass-white: #f5f5f5;
|
||||||
|
|
||||||
// Primary colors
|
// Primary colors
|
||||||
$primary-light: #9dce0a;
|
$primary-light: #9dce0a;
|
||||||
|
$primary-lighter: #EFF3E3;
|
||||||
$primary: #5b8200;
|
$primary: #5b8200;
|
||||||
$primary-dark: #879b89;
|
$primary-dark: #879b89;
|
||||||
$primary-background: #E8F5E3;
|
$primary-background: #E8F5E3;
|
||||||
|
|
Loading…
Reference in New Issue