diff --git a/services/web/app/coffee/Features/Announcements/AnnouncementsHandler.coffee b/services/web/app/coffee/Features/Announcements/AnnouncementsHandler.coffee index a27608343a..ce41e3b96c 100644 --- a/services/web/app/coffee/Features/Announcements/AnnouncementsHandler.coffee +++ b/services/web/app/coffee/Features/Announcements/AnnouncementsHandler.coffee @@ -25,8 +25,15 @@ module.exports = announcementIndex = _.findIndex announcements, (announcement)-> announcement.id == lastSeenBlogId - if announcementIndex != -1 - announcements = announcements.slice(0, announcementIndex) + announcements = _.map announcements, (announcement, index)-> + if announcementIndex == -1 + read = false + else if index >= announcementIndex + read = true + else + read = false + announcement.read = read + return announcement logger.log announcementsLength:announcements?.length, user_id:user_id, "returning announcements" diff --git a/services/web/app/views/project/list.jade b/services/web/app/views/project/list.jade index 6f1d87424a..d9fbf0e6b1 100644 --- a/services/web/app/views/project/list.jade +++ b/services/web/app/views/project/list.jade @@ -19,12 +19,46 @@ block content } }; - .content.content-alt(ng-controller="ProjectPageController") + .content.content-alt.project-list-page(ng-controller="ProjectPageController") .container - - //- div(ng-controller="AnnouncementsController", ng-cloak) - //- .alert.alert-success(ng-show="dataRecived") - //- a(href, ng-click="openLink()") {{title}} and {{totalAnnouncements}} others + .announcements( + ng-controller="AnnouncementsController" + ng-class="{ 'announcements-open': ui.isOpen }" + ng-cloak + ) + .announcements-backdrop( + ng-if="ui.isOpen" + ng-click="toggleAnnouncementsUI();" + ) + a.announcements-btn( + href + ng-if="announcements.length" + ng-click="toggleAnnouncementsUI();" + ng-class="{ 'announcements-btn-open': ui.isOpen, 'announcements-btn-has-new': ui.newItems }" + ) + span.announcements-badge(ng-if="ui.newItems") {{ ui.newItems }} + .announcements-body( + ng-if="ui.isOpen" + ) + .announcements-scroller + .announcement( + ng-repeat="announcement in announcements | filter:(ui.newItems ? { read: false } : '') track by announcement.id" + ) + h2.announcement-header {{ announcement.title }} + p.announcement-description(ng-bind-html="announcement.excerpt") + .announcement-meta + p.announcement-date {{ announcement.date | date:"longDate" }} + a.announcement-link( + ng-href="{{ announcement.url }}" + target="_blank" + ) Read more + div.text-center( + ng-if="ui.newItems > 0 && ui.newItems < announcements.length" + ) + a.btn.btn-default.btn-sm( + href + ng-click="showAll();" + ) Show all .row(ng-cloak) span(ng-if="projects.length > 0") diff --git a/services/web/public/coffee/main/announcements.coffee b/services/web/public/coffee/main/announcements.coffee index b0960bcc09..cccebf56ad 100644 --- a/services/web/public/coffee/main/announcements.coffee +++ b/services/web/public/coffee/main/announcements.coffee @@ -1,20 +1,29 @@ define [ "base" ], (App) -> - App.controller "AnnouncementsController", ($scope, $http, event_tracking, $window) -> + App.controller "AnnouncementsController", ($scope, $http, event_tracking, $window, _) -> + $scope.announcements = [] + $scope.ui = + isOpen: false + newItems: 0 + + refreshAnnouncements = -> + $http.get("/announcements").success (announcements) -> + $scope.announcements = announcements + $scope.ui.newItems = _.filter(announcements, (announcement) -> !announcement.read).length + + markAnnouncementsAsRead = -> + event_tracking.sendMB "announcement-alert-dismissed", { blogPostId: $scope.announcements[0].id } - $scope.dataRecived = false - announcement = null - $http.get("/announcements").success (announcements) -> - if announcements?[0]? - announcement = announcements[0] - $scope.title = announcement.title - $scope.totalAnnouncements = announcements.length - $scope.dataRecived = true + refreshAnnouncements() - dismissannouncement = -> - event_tracking.sendMB "announcement-alert-dismissed", {blogPostId:announcement.id} + $scope.toggleAnnouncementsUI = -> + $scope.ui.isOpen = !$scope.ui.isOpen + + if !$scope.ui.isOpen and $scope.ui.newItems + $scope.ui.newItems = 0 + markAnnouncementsAsRead() + + $scope.showAll = -> + $scope.ui.newItems = 0 - $scope.openLink = -> - dismissannouncement() - $window.location.href = announcement.url diff --git a/services/web/public/stylesheets/app/project-list.less b/services/web/public/stylesheets/app/project-list.less index de3307a017..5e03c1facc 100644 --- a/services/web/public/stylesheets/app/project-list.less +++ b/services/web/public/stylesheets/app/project-list.less @@ -1,3 +1,26 @@ +@announcements-shadow: 0 2px 20px rgba(0, 0, 0, 0.5); + +@keyframes pulse { + 0% { + opacity: .7; + } + 100% { + opacity: .9; + } +} +@keyframes fade-in { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +.project-list-page { + position: relative; +} + .project-header { .btn-group > .btn { padding-left: @line-height-base / 2; @@ -293,3 +316,146 @@ ul.project-list { margin-left:-100px; } } + +.announcements { + position: absolute; + bottom: 0; + right: 0; + height: 150px; + width: 100%; + pointer-events: none; + overflow: hidden; + + &-open { + top: -100%; + height: auto; + pointer-events: all; + } +} + +.announcements-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: rgba(0, 0, 0, 0.35); + opacity: 0; + animation: fade-in 0.35s forwards; + z-index: 1; +} + +.announcements-btn { + position: absolute; + bottom: -50px; + right: 3%; + width: 80px; + height: 80px; + background: url(/img/lion-128.png) no-repeat center/80% transparent; + border-radius: 50%; + box-shadow: none; + z-index: 1; + pointer-events: all; + transition: bottom 0.25s cubic-bezier(0.68, -0.55, 0.265, 1.55), + background 0.25s ease, + box-shadow 0.25s ease; + + &:hover { + bottom: -45px; + } + + &-open, &-open:hover, + &-has-new, &-has-new:hover { + background-color: #FFF; + box-shadow: @announcements-shadow; + bottom: 30px; + } +} + .announcements-badge { + display: inline-block; + position: absolute; + font-size: 11px; + height: 1.8em; + min-width: 1.8em; + border-radius: 0.9em; + line-height: 1.8; + padding: 0 2px; + top: 1px; + right: 1px; + font-weight: bold; + color: #FFF; + background-color: @red; + vertical-align: baseline; + white-space: nowrap; + text-align: center; + z-index: 1; + animation: pulse 1s alternate infinite; + } + +.announcements-body { + display: flex; + flex-direction: column; + align-items: stretch; + position: absolute; + right: 3%; + margin-right: 95px; + bottom: 30px; + width: 700px; + max-height: 52%; + min-height: 100px; + background: #FFF; + z-index: 1; + box-shadow: @announcements-shadow; + border-radius: @border-radius-base; + animation: fade-in 0.35s forwards; + + &::after { + content: "\25b8"; + position: absolute; + left: 100%; + bottom: 17px; + width: 30px; + color: #FFF; + text-shadow: @announcements-shadow; + font-size: 2em; + overflow: hidden; + text-indent: -7px; + } +} + + .announcements-scroller { + padding: @line-height-computed; + flex-grow: 0; + overflow-x: hidden; + overflow-y: auto; + } + .announcement { + margin-bottom: @line-height-computed * 1.5; + &:last-child { + margin-bottom: 0; + } + } + .announcement-header { + .page-header; + margin: 0; + } + + .announcement-description { + margin: (@line-height-computed / 4) 0 (@line-height-computed / 2); + } + + .announcement-meta { + .clearfix; + font-size: 0.9em; + } + + .announcement-date { + float: left; + color: @gray; + margin: 0; + } + + .announcement-link { + float: right; + margin: 0; + } diff --git a/services/web/test/UnitTests/coffee/Announcement/AnnouncementsHandlerTests.coffee b/services/web/test/UnitTests/coffee/Announcement/AnnouncementsHandlerTests.coffee index 7578ea3a64..49e8292f97 100644 --- a/services/web/test/UnitTests/coffee/Announcement/AnnouncementsHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Announcement/AnnouncementsHandlerTests.coffee @@ -42,10 +42,13 @@ describe 'AnnouncementsHandler', -> @BlogHandler.getLatestAnnouncements.callsArgWith(0, null, @stubbedAnnouncements) - it "should return all announcements if there are no getLastOccurance", (done)-> + it "should mark all announcements as read is false", (done)-> @AnalyticsManager.getLastOccurance.callsArgWith(2, null, []) @handler.getUnreadAnnouncements @user_id, (err, announcements)=> - announcements.length.should.equal 4 + announcements[0].read.should.equal false + announcements[1].read.should.equal false + announcements[2].read.should.equal false + announcements[3].read.should.equal false done() it "should should be sorted again to ensure correct order", (done)-> @@ -57,16 +60,30 @@ describe 'AnnouncementsHandler', -> announcements[0].should.equal @stubbedAnnouncements[0] done() - it "should return ones older than the last blog id", (done)-> + it "should return older ones marked as read as well", (done)-> @AnalyticsManager.getLastOccurance.callsArgWith(2, null, {segmentation:{blogPostId:"/2014/04/12/title-date-irrelivant"}}) @handler.getUnreadAnnouncements @user_id, (err, announcements)=> - announcements.length.should.equal 2 announcements[0].id.should.equal @stubbedAnnouncements[0].id + announcements[0].read.should.equal false + announcements[1].id.should.equal @stubbedAnnouncements[1].id + announcements[1].read.should.equal false + + announcements[2].id.should.equal @stubbedAnnouncements[3].id + announcements[2].read.should.equal true + + announcements[3].id.should.equal @stubbedAnnouncements[2].id + announcements[3].read.should.equal true + done() - it "should return none when the latest id is the first element", (done)-> + it "should return all of them marked as read", (done)-> @AnalyticsManager.getLastOccurance.callsArgWith(2, null, {segmentation:{blogPostId:"/2016/11/01/introducting-latex-code-checker"}}) @handler.getUnreadAnnouncements @user_id, (err, announcements)=> - announcements.length.should.equal 0 + announcements[0].read.should.equal true + announcements[1].read.should.equal true + announcements[2].read.should.equal true + announcements[3].read.should.equal true done() + +