From 4fc9c6416ed19b99a897604e7c405bd188ee46a0 Mon Sep 17 00:00:00 2001 From: Steffen van Bergerem Date: Thu, 13 Mar 2014 21:41:56 +0100 Subject: [PATCH] Port notifications to Bootstrap --- Changelog.md | 1 + .../app/views/notifications_view.js | 86 +++++++++++++++++++ .../javascripts/widgets/notifications.js | 39 ++------- app/assets/stylesheets/application.css.sass | 19 ---- app/assets/stylesheets/new-templates.css.scss | 3 + app/assets/stylesheets/notifications.css.scss | 83 ++++++++++++++++++ .../notifications/_notification.html.haml | 13 +++ app/views/notifications/index.html.haml | 78 ++++++++++------- config/locales/diaspora/en.yml | 30 +++---- features/desktop/notifications.feature | 13 ++- .../step_definitions/notifications_steps.rb | 7 ++ .../app/views/notifications_view_spec.js | 76 ++++++++++++++++ 12 files changed, 351 insertions(+), 97 deletions(-) create mode 100644 app/assets/javascripts/app/views/notifications_view.js create mode 100644 app/assets/stylesheets/notifications.css.scss create mode 100644 app/views/notifications/_notification.html.haml create mode 100644 features/step_definitions/notifications_steps.rb create mode 100644 spec/javascripts/app/views/notifications_view_spec.js diff --git a/Changelog.md b/Changelog.md index c2caa74f3..f3f92c0a9 100644 --- a/Changelog.md +++ b/Changelog.md @@ -14,6 +14,7 @@ * Update to jQuery 10 * Port publisher and bookmarklet to Bootstrap [#4678](https://github.com/diaspora/diaspora/pull/4678) * Improve search page, add better indications [#4794](https://github.com/diaspora/diaspora/pull/4794) +* Port notifications and hovercards to Bootstrap [#4814](https://github.com/diaspora/diaspora/pull/4814) ## Bug fixes * Improve time agos by updating the plugin [#4280](https://github.com/diaspora/diaspora/issues/4280) diff --git a/app/assets/javascripts/app/views/notifications_view.js b/app/assets/javascripts/app/views/notifications_view.js new file mode 100644 index 000000000..4cc920866 --- /dev/null +++ b/app/assets/javascripts/app/views/notifications_view.js @@ -0,0 +1,86 @@ +app.views.Notifications = Backbone.View.extend({ + + events: { + "click .unread-toggle" : "toggleUnread" + }, + + initialize: function() { + Diaspora.page.header.notifications.setUpNotificationPage(this); + $('.aspect_membership_dropdown').each(function(){ + new app.views.AspectMembership({el: this}); + }); + }, + + toggleUnread: function(evt) { + note = $(evt.target).closest(".stream_element"); + unread = note.hasClass("unread"); + + if (unread) { + this.setRead(note.data("guid")); + } + else { + this.setUnread(note.data("guid")); + } + }, + + setRead: function(guid) { + $.ajax({ + url: "/notifications/" + guid, + data: { set_unread: false }, + type: "PUT", + context: this, + success: this.clickSuccess + }); + }, + + setUnread: function(guid) { + $.ajax({ + url: "/notifications/" + guid, + data: { set_unread: true }, + type: "PUT", + context: this, + success: this.clickSuccess + }); + }, + + clickSuccess: function(data) { + type = $('.stream_element[data-guid=' + data["guid"] + ']').data('type'); + this.updateView(data["guid"], type, data["unread"]); + }, + + updateView: function(guid, type, unread) { + change = unread ? 1 : -1; + all_notes = $('ul.nav > li:eq(0) .badge'); + type_notes = $('ul.nav > li[data-type=' + type + '] .badge'); + header_badge = $('#notification_badge .badge_count'); + + note = $('.stream_element[data-guid=' + guid + ']'); + if(unread) { + note.removeClass("read").addClass("unread"); + $(".unread-toggle", note).text(Diaspora.I18n.t('notifications.mark_read')); + } + else { + note.removeClass("unread").addClass("read"); + $(".unread-toggle", note).text(Diaspora.I18n.t('notifications.mark_unread')); + } + + all_notes.text( function(i,text) { return parseInt(text) + change }); + type_notes.text( function(i,text) { return parseInt(text) + change }); + header_badge.text( function(i,text) { return parseInt(text) + change }); + if(all_notes.text()>0){ + all_notes.addClass('badge-important'); + } else { + all_notes.removeClass('badge-important'); + } + if(type_notes.text()>0){ + type_notes.addClass('badge-important'); + } else { + type_notes.removeClass('badge-important'); + } + if(header_badge.text()>0){ + header_badge.removeClass('hidden'); + } else { + header_badge.addClass('hidden'); + } + } +}); diff --git a/app/assets/javascripts/widgets/notifications.js b/app/assets/javascripts/widgets/notifications.js index 0e07bbff0..d8d875a85 100644 --- a/app/assets/javascripts/widgets/notifications.js +++ b/app/assets/javascripts/widgets/notifications.js @@ -12,8 +12,8 @@ $.extend(self, { badge: badge, count: parseInt(badge.html()) || 0, - notificationArea: null, - notificationMenu: notificationMenu + notificationMenu: notificationMenu, + notificationPage: null }); $("a.more").click( function(evt) { @@ -31,11 +31,6 @@ self.notificationMenu.find('.unread').each(function(index) { self.setUpRead( $(this) ); }); - if ( self.notificationArea ) { - self.notificationArea.find('.unread').each(function(index) { - self.setUpRead( $(this) ); - }); - } self.resetCount(); } }); @@ -43,15 +38,8 @@ return false; }); }); - this.setUpNotificationPage = function( contentArea ) { - self.notificationArea = contentArea; - contentArea.find(".unread,.read").each(function(index) { - if ( $(this).hasClass("unread") ) { - self.setUpUnread( $(this) ); - } else { - self.setUpRead( $(this) ); - } - }); + this.setUpNotificationPage = function( page ) { + self.notificationPage = page; } this.unreadClick = function() { $.ajax({ @@ -106,16 +94,9 @@ } } }); - if ( self.notificationArea ) { - self.notificationArea.find('.read,.unread').each(function(index) { - if ( $(this).data("guid") == itemID ) { - if ( isUnread ) { - self.setUpUnread( $(this) ) - } else { - self.setUpRead( $(this) ) - } - } - }); + if ( self.notificationPage != null ) { + var type = $('.notification_element[data-guid=' + data["guid"] + ']').data('type'); + self.notificationPage.updateView(data["guid"], type, isUnread); } }; this.showNotification = function(notification) { @@ -134,18 +115,12 @@ this.changeNotificationCount = function(change) { self.count = Math.max( self.count + change, 0 ) self.badge.text(self.count); - if ( self.notificationArea ) - self.notificationArea.find( ".notification_count" ).text(self.count); if(self.count === 0) { self.badge.addClass("hidden"); - if ( self.notificationArea ) - self.notificationArea.find( ".notification_count" ).removeClass("unread"); } else if(self.count === 1) { self.badge.removeClass("hidden"); - if ( self.notificationArea ) - self.notificationArea.find( ".notification_count" ).addClass("unread"); } }; this.resetCount = function(change) { diff --git a/app/assets/stylesheets/application.css.sass b/app/assets/stylesheets/application.css.sass index 970e26e97..8c2bcc79e 100644 --- a/app/assets/stylesheets/application.css.sass +++ b/app/assets/stylesheets/application.css.sass @@ -776,25 +776,6 @@ ul#press_logos #aspects_list :height auto -.notifications_for_day - .stream_element - :padding 0.2em 0.5em - :width 500px - -.day_group - :min-height 100px - :margin - :bottom 10px - .stream_element - &:last-child - :border none - -.stream.notifications - > li:hover - :background none - :border - :bottom 1px solid #eee - .show_comments :border :top 1px solid $border-grey diff --git a/app/assets/stylesheets/new-templates.css.scss b/app/assets/stylesheets/new-templates.css.scss index 620638da8..3219e81fa 100644 --- a/app/assets/stylesheets/new-templates.css.scss +++ b/app/assets/stylesheets/new-templates.css.scss @@ -38,3 +38,6 @@ /* bookmarklet */ @import 'bookmarklet'; + +/* notifications */ +@import 'notifications'; diff --git a/app/assets/stylesheets/notifications.css.scss b/app/assets/stylesheets/notifications.css.scss new file mode 100644 index 000000000..793414b47 --- /dev/null +++ b/app/assets/stylesheets/notifications.css.scss @@ -0,0 +1,83 @@ +#notifications_container { + padding-top: 50px; + + .nav.nav-tabs{ + li > a { + color: $text-dark-grey; + .entypo { + color: $text-dark-grey; + margin-right: 5px; + } + } + li.active > a { + background-color: $background-grey; + color: $black; + .entypo { color: $black; } + } + } + + .stream { + .header { + border-bottom: 1px solid $border-grey; + .btn-toolbar, h4 { + margin-bottom: 10px; + line-height: 40px; + } + margin-bottom: 10px; + } + + .day_group { + margin-bottom: 20px; + .date { + text-align: center; + color: $light-grey; + margin-bottom: 5px; + .day { + font-size: 40px; + line-height: 40px; + } + .month { + font-size: 16px; + line-height: 16px; + } + } + } + + .media, .media-body { + overflow: visible; + } + + .stream_element.media { + padding: 10px; + margin: 0px; + font-size: 13px; + line-height: 18px; + border-bottom: 1px solid $border-grey; + &:last-child { border: none !important; } + + &.unread { + background-color: $background-grey; + .unread-toggle { opacity: 1 !important; } + } + + &:hover { + .unread-toggle { opacity: 1 !important; } + } + + .avatar { + width: 35px; + height: 35px; + } + + .unread-toggle { + opacity: 0; + margin-top: 4px; + float: right; + } + + .btn-group.aspect_membership_dropdown { margin: 5px 0; } + } + + .pagination { text-align: center; } + } +} diff --git a/app/views/notifications/_notification.html.haml b/app/views/notifications/_notification.html.haml new file mode 100644 index 000000000..0f9dbd020 --- /dev/null +++ b/app/views/notifications/_notification.html.haml @@ -0,0 +1,13 @@ +.media.stream_element{:data=>{:guid => note.id, :type => (Notification.types.key(note.type) || '') }, :class => (note.unread ? 'unread' : 'read')} + %button.btn.btn-link.btn-small.unread-toggle + = note.unread ? t('notifications.index.mark_read') : t('notifications.index.mark_unread') + - if note.type == "Notifications::StartedSharing" && contact = current_user.contact_for(note.effective_target) + .pull-right + = aspect_membership_dropdown(contact, note.effective_target, 'left') + + .media-object.pull-left + = person_image_link note.actors.first, :size => :thumb_small, :class => 'hovercardable' + .media-body + = notification_message_for(note) + %div + = timeago(note.created_at) diff --git a/app/views/notifications/index.html.haml b/app/views/notifications/index.html.haml index e7c33777b..5fa76e331 100644 --- a/app/views/notifications/index.html.haml +++ b/app/views/notifications/index.html.haml @@ -1,38 +1,56 @@ -#notifications_content - .span-13 - %h2 - %span.notification_count{:class => ('unread' if @unread_notification_count >0 )} - = @unread_notification_count - = t('.notifications') - .span-8.last - = link_to t('.mark_all_as_read'), notifications_read_all_path, :class => "button #{'disabled' unless @unread_notification_count > 0}" - .span-24.last - .stream.notifications +.container-fluid#notifications_container + .row-fluid + .span3 + %h3 + = t('.notifications') + %ul.nav.nav-tabs.nav-stacked + %li{ :class => ('active' unless params[:type] && @grouped_unread_notification_counts.has_key?(params[:type])) } + %a{ :href => '/notifications' + (params[:show] == 'unread' ? '?show=unread' : '') } + %span.pull-right.badge{:class => ('badge-important' if @unread_notification_count > 0)} + = @unread_notification_count + = t('.all_notifications') + - @grouped_unread_notification_counts.each do |key, count| + %li{ :class => ('active' if params[:type] == key), :data => { :type => key } } + %a{ :href => '/notifications?type=' + key + (params[:show] == 'unread' ? '&show=unread' : '') } + %span.pull-right.badge{ :class => ('badge-important' if count > 0) } + = count + - case key + - when 'also_commented', 'comment_on_post' + %i.entypo.comment + - when 'liked' + %i.entypo.heart + - when 'mentioned' + %i.entypo.pencil + - when 'reshared' + %i.entypo.retweet + - when 'started_sharing' + %i.entypo.users + = t('.'+key) + + .span9.stream.notifications + .row-fluid.header + .span12 + .btn-toolbar.pull-right + .btn-group + %a.btn.btn-default{ :class => ('active' unless params[:show] == 'unread'), :href => '/notifications' + (params[:type] ? '?type=' + params[:type] : '') } + = t('.show_all') + %a.btn.btn-default{ :class => ('active' if params[:show] == 'unread'), :href => '/notifications?show=unread' + (params[:type] ? '&type=' + params[:type] : '') } + = t('.show_unread') + %a.btn.btn-default{:href => notifications_read_all_path, :class => ('disabled' unless @unread_notification_count > 0)} + = t('.mark_all_as_read') - @group_days.each do |day, notes| - .day_group.span-24.last - .span-3 - .date - .day= the_day(day.split(' ')) - .month= the_month(day.split(' ')) + .day_group.row-fluid + .date.span2 + .day= the_day(day.split(' ')) + .month= the_month(day.split(' ')) - .span-8.notifications_for_day + .notifications_for_day.span10 - notes.each do |note| - .stream_element{:data=>{:guid => note.id}, :class => "#{note.unread ? 'unread' : 'read'}"} - - if note.type == "Notifications::StartedSharing" && contact = current_user.contact_for(note.effective_target) - .float-right - = aspect_membership_dropdown(contact, note.effective_target, 'left') + = render :partial => 'notifications/notification', :locals => { :note => note } - .media - .bd - = person_image_tag note.actors.first, :thumb_medium - = notification_message_for(note) - %div - = timeago(note.created_at) - = link_to t('.mark_unread'), "#", :class => "unread-setter" - - = will_paginate @notifications + = will_paginate @notifications, :renderer => WillPaginate::ActionView::BootstrapLinkRenderer :javascript $(document).ready(function(){ - Diaspora.page.header.notifications.setUpNotificationPage( $("#notifications_content" ) ); + new app.views.Notifications({ el: '#notifications_container' }); }); diff --git a/config/locales/diaspora/en.yml b/config/locales/diaspora/en.yml index 728dfb449..49f6f512d 100644 --- a/config/locales/diaspora/en.yml +++ b/config/locales/diaspora/en.yml @@ -637,25 +637,25 @@ en: one: "%{actors} sent you a message." other: "%{actors} sent you a message." comment_on_post: - zero: "%{actors} commented on your post »%{post_link}«." - one: "%{actors} commented on your post »%{post_link}«." - other: "%{actors} commented on your post »%{post_link}«." + zero: "%{actors} commented on your post %{post_link}." + one: "%{actors} commented on your post %{post_link}." + other: "%{actors} commented on your post %{post_link}." also_commented: - zero: "%{actors} also commented on %{post_author}'s post »%{post_link}«." - one: "%{actors} also commented on %{post_author}'s post »%{post_link}«." - other: "%{actors} also commented on %{post_author}'s post »%{post_link}«." + zero: "%{actors} also commented on %{post_author}'s post %{post_link}." + one: "%{actors} also commented on %{post_author}'s post %{post_link}." + other: "%{actors} also commented on %{post_author}'s post %{post_link}." mentioned: - zero: "%{actors} have mentioned you in the post »%{post_link}«." - one: "%{actors} has mentioned you in the post »%{post_link}«." - other: "%{actors} have mentioned you in the »%{post_link}«." + zero: "%{actors} have mentioned you in the post %{post_link}." + one: "%{actors} has mentioned you in the post %{post_link}." + other: "%{actors} have mentioned you in the %{post_link}." liked: - zero: "%{actors} have liked your post »%{post_link}«." - one: "%{actors} has liked your post »%{post_link}«." - other: "%{actors} have liked your post »%{post_link}«." + zero: "%{actors} have liked your post %{post_link}." + one: "%{actors} has liked your post %{post_link}." + other: "%{actors} have liked your post %{post_link}." reshared: - zero: "%{actors} have reshared your post »%{post_link}«." - one: "%{actors} has reshared your post »%{post_link}«." - other: "%{actors} have reshared your post »%{post_link}«." + zero: "%{actors} have reshared your post %{post_link}." + one: "%{actors} has reshared your post %{post_link}." + other: "%{actors} have reshared your post %{post_link}." post: "post" also_commented_deleted: zero: "%{actors} commented on a deleted post." diff --git a/features/desktop/notifications.feature b/features/desktop/notifications.feature index c935d3a6d..c47205246 100644 --- a/features/desktop/notifications.feature +++ b/features/desktop/notifications.feature @@ -73,5 +73,16 @@ Feature: Notifications When I sign in as "bob@bob.bob" And I follow "Notifications" in the header Then the notification dropdown should be visible - Then I should see "mentioned you in a post" + Then I should see "mentioned you in the post" And I should have 1 email delivery + + Scenario: filter notifications + Given a user with email "bob@bob.bob" is connected with "alice@alice.alice" + And Alice has a post mentioning Bob + When I sign in as "bob@bob.bob" + And I am on the notifications page + Then I should see "mentioned you in the post" + When I filter notifications by likes + Then I should not see "mentioned you in the post" + When I filter notifications by mentions + Then I should see "mentioned you in the post" diff --git a/features/step_definitions/notifications_steps.rb b/features/step_definitions/notifications_steps.rb new file mode 100644 index 000000000..6151b8560 --- /dev/null +++ b/features/step_definitions/notifications_steps.rb @@ -0,0 +1,7 @@ +When /^I filter notifications by likes$/ do + step %(I follow "Liked" within "#notifications_container ul.nav.nav-tabs") +end + +When /^I filter notifications by mentions$/ do + step %(I follow "Mentioned" within "#notifications_container ul.nav.nav-tabs") +end diff --git a/spec/javascripts/app/views/notifications_view_spec.js b/spec/javascripts/app/views/notifications_view_spec.js new file mode 100644 index 000000000..589bc334e --- /dev/null +++ b/spec/javascripts/app/views/notifications_view_spec.js @@ -0,0 +1,76 @@ +describe("app.views.Notifications", function(){ + beforeEach(function() { + spec.loadFixture("notifications"); + this.view = new app.views.Notifications({el: '#notifications_container'}); + }); + + context('mark read', function() { + beforeEach(function() { + this.unreadN = $('.stream_element.unread').first(); + this.guid = this.unreadN.data("guid"); + }); + + it('calls "setRead"', function() { + spyOn(this.view, "setRead"); + this.unreadN.find('.unread-toggle').trigger('click'); + + expect(this.view.setRead).toHaveBeenCalledWith(this.guid); + }); + }); + + context('mark unread', function() { + beforeEach(function() { + this.readN = $('.stream_element.read').first(); + this.guid = this.readN.data("guid"); + }); + + it('calls "setUnread"', function() { + spyOn(this.view, "setUnread"); + this.readN.find('.unread-toggle').trigger('click'); + + expect(this.view.setUnread).toHaveBeenCalledWith(this.guid); + }); + }); + + context('updateView', function() { + beforeEach(function() { + this.readN = $('.stream_element.read').first(); + this.guid = this.readN.data('guid'); + this.type = this.readN.data('type'); + }); + + it('changes the "all notifications" count', function() { + badge = $('ul.nav > li:eq(0) .badge'); + count = parseInt(badge.text()); + + this.view.updateView(this.guid, this.type, true); + expect(parseInt(badge.text())).toBe(count + 1); + + this.view.updateView(this.guid, this.type, false); + expect(parseInt(badge.text())).toBe(count); + }); + + it('changes the notification type count', function() { + badge = $('ul.nav > li[data-type=' + this.type + '] .badge'); + count = parseInt(badge.text()); + + this.view.updateView(this.guid, this.type, true); + expect(parseInt(badge.text())).toBe(count + 1); + + this.view.updateView(this.guid, this.type, false); + expect(parseInt(badge.text())).toBe(count); + }); + + it('toggles the unread class and changes the link text', function() { + this.view.updateView(this.readN.data('guid'), this.readN.data('type'), true); + expect(this.readN.hasClass('unread')).toBeTruethy; + expect(this.readN.hasClass('read')).toBeFalsy; + expect(this.readN.find('.unread-toggle').text()).toContain(Diaspora.I18n.t('notifications.mark_read')); + + this.view.updateView(this.readN.data('guid'), this.readN.data('type'), false); + expect(this.readN.hasClass('read')).toBeTruethy; + expect(this.readN.hasClass('unread')).toBeFalsy; + expect(this.readN.find('.unread-toggle').text()).toContain(Diaspora.I18n.t('notifications.mark_unread')); + }); + }); +});