From f217a5bc112b274affe5b4e7a44240aa946a228b Mon Sep 17 00:00:00 2001 From: Steffen van Bergerem Date: Thu, 13 Mar 2014 21:26:05 +0100 Subject: [PATCH 1/3] Add filters to notifications controller --- Changelog.md | 1 + app/controllers/application_controller.rb | 6 +++ app/controllers/conversations_controller.rb | 2 +- app/controllers/notifications_controller.rb | 13 ++++++ app/controllers/posts_controller.rb | 2 +- app/controllers/status_messages_controller.rb | 2 +- app/helpers/notifications_helper.rb | 5 ++- app/models/notification.rb | 11 +++++ .../notifications/_notify_popup_item.haml | 2 +- config/locales/diaspora/en.yml | 42 ++++++++++++------- config/locales/javascript/javascript.en.yml | 4 ++ .../jasmine_fixtures/notifications_spec.rb | 19 +++++++++ .../notifications_controller_spec.rb | 16 ++++++- spec/helpers/notifications_helper_spec.rb | 2 +- 14 files changed, 103 insertions(+), 24 deletions(-) create mode 100644 spec/controllers/jasmine_fixtures/notifications_spec.rb diff --git a/Changelog.md b/Changelog.md index 1f599fb0b..c2caa74f3 100644 --- a/Changelog.md +++ b/Changelog.md @@ -32,6 +32,7 @@ * Add permalinks for comments [#4577](https://github.com/diaspora/diaspora/pull/4577) * New menu for the mobile version [#4673](https://github.com/diaspora/diaspora/pull/4673) * Added comment count to statistic to enable calculations of posts/comments ratios [#4799](https://github.com/diaspora/diaspora/pull/4799) +* Add filters to notifications controller [#4814](https://github.com/diaspora/diaspora/pull/4814) # 0.3.0.3 diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 780852ab4..320ce93f0 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -151,4 +151,10 @@ class ApplicationController < ActionController::Base gon.preloads = {} end + def self.use_bootstrap_for *routes + before_filter -> { + @css_framework = :bootstrap + gon.bootstrap = true + }, only: routes.flatten + end end diff --git a/app/controllers/conversations_controller.rb b/app/controllers/conversations_controller.rb index acc024dca..180c42bf6 100644 --- a/app/controllers/conversations_controller.rb +++ b/app/controllers/conversations_controller.rb @@ -2,7 +2,7 @@ class ConversationsController < ApplicationController before_filter :authenticate_user! layout ->(c) { request.format == :mobile ? "application" : "with_header" } - before_filter -> { @css_framework = :bootstrap } + use_bootstrap_for :index, :show, :new respond_to :html, :mobile, :json, :js diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb index cfdf2be95..85e951436 100644 --- a/app/controllers/notifications_controller.rb +++ b/app/controllers/notifications_controller.rb @@ -5,6 +5,9 @@ class NotificationsController < ApplicationController before_filter :authenticate_user! + layout ->(c) { request.format == :mobile ? "application" : "with_header_with_footer" } + use_bootstrap_for :index + def update note = Notification.where(:recipient_id => current_user.id, :id => params[:id]).first if note @@ -23,6 +26,10 @@ class NotificationsController < ApplicationController def index conditions = {:recipient_id => current_user.id} + if params[:type] && Notification.types.has_key?(params[:type]) + conditions[:type] = Notification.types[params[:type]] + end + if params[:show] == "unread" then conditions[:unread] = true end page = params[:page] || 1 per_page = params[:per_page] || 25 @notifications = WillPaginate::Collection.create(page, per_page, Notification.where(conditions).count ) do |pager| @@ -43,6 +50,12 @@ class NotificationsController < ApplicationController @unread_notification_count = current_user.unread_notifications.count + @grouped_unread_notification_counts = {} + + Notification.types.each_with_object(current_user.unread_notifications.group_by(&:type)) {|(name, type), notifications| + @grouped_unread_notification_counts[name] = notifications.has_key?(type) ? notifications[type].count : 0 + } + respond_to do |format| format.html format.xml { render :xml => @notifications.to_xml } diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index affb08f15..0539da8f7 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -9,7 +9,7 @@ class PostsController < ApplicationController before_filter :set_format_if_malformed_from_status_net, :only => :show before_filter :find_post, :only => [:show, :interactions] - before_filter -> { @css_framework = :bootstrap } + use_bootstrap_for :show respond_to :html, :mobile, diff --git a/app/controllers/status_messages_controller.rb b/app/controllers/status_messages_controller.rb index 772a80c2a..83f92f3ac 100644 --- a/app/controllers/status_messages_controller.rb +++ b/app/controllers/status_messages_controller.rb @@ -7,7 +7,7 @@ class StatusMessagesController < ApplicationController before_filter :remove_getting_started, :only => [:create] - before_filter -> { @css_framework = :bootstrap }, :only => [:bookmarklet] + use_bootstrap_for :bookmarklet respond_to :html, :mobile, diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index 42bf14244..6db4e46e4 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -1,5 +1,6 @@ module NotificationsHelper include PeopleHelper + include PostsHelper def object_link(note, actors) target_type = note.popup_translation_key @@ -7,13 +8,13 @@ module NotificationsHelper if note.instance_of?(Notifications::Mentioned) if post = note.linked_object - translation(target_type, :actors => actors, :count => actors_count, :post_link => link_to(t('notifications.post'), post_path(post)).html_safe) + translation(target_type, :actors => actors, :count => actors_count, :post_link => link_to(post_page_title(post), post_path(post)).html_safe) else t(note.deleted_translation_key, :actors => actors, :count => actors_count).html_safe end elsif note.instance_of?(Notifications::CommentOnPost) || note.instance_of?(Notifications::AlsoCommented) || note.instance_of?(Notifications::Reshared) || note.instance_of?(Notifications::Liked) if post = note.linked_object - translation(target_type, :actors => actors, :count => actors_count, :post_author => h(post.author_name), :post_link => link_to(t('notifications.post'), post_path(post), 'data-ref' => post.id, :class => 'hard_object_link').html_safe) + translation(target_type, :actors => actors, :count => actors_count, :post_author => h(post.author_name), :post_link => link_to(post_page_title(post), post_path(post), 'data-ref' => post.id, :class => 'hard_object_link').html_safe) else t(note.deleted_translation_key, :actors => actors, :count => actors_count).html_safe end diff --git a/app/models/notification.rb b/app/models/notification.rb index a60d8cdb9..2db32f764 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -91,4 +91,15 @@ private def self.suppress_notification?(recipient, post) post.is_a?(Post) && recipient.is_shareable_hidden?(post) end + + def self.types + { + "also_commented" => "Notifications::AlsoCommented", + "comment_on_post" => "Notifications::CommentOnPost", + "liked" => "Notifications::Liked", + "mentioned" => "Notifications::Mentioned", + "reshared" => "Notifications::Reshared", + "started_sharing" => "Notifications::StartedSharing" + } + end end diff --git a/app/views/notifications/_notify_popup_item.haml b/app/views/notifications/_notify_popup_item.haml index 89da6fa53..10b502d6d 100644 --- a/app/views/notifications/_notify_popup_item.haml +++ b/app/views/notifications/_notify_popup_item.haml @@ -1,4 +1,4 @@ -.notification_element{:data=>{:guid => n.id}, :class => (n.unread ? "unread" : "read")} +.notification_element{:data=>{:guid => n.id, :type => (Notification.types.key(n.type) || '')}, :class => (n.unread ? "unread" : "read")} = person_image_tag n.actors.first, :thumb_small = notification_message_for(n) %div diff --git a/config/locales/diaspora/en.yml b/config/locales/diaspora/en.yml index 84d64658d..728dfb449 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_link}." - one: "%{actors} commented on your %{post_link}." - other: "%{actors} commented on your %{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_link}." - one: "%{actors} also commented on %{post_author}'s %{post_link}." - other: "%{actors} also commented on %{post_author}'s %{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 a %{post_link}." - one: "%{actors} has mentioned you in a %{post_link}." - other: "%{actors} have mentioned you in a %{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_link}." - one: "%{actors} has liked your %{post_link}." - other: "%{actors} have liked your %{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_link}." - one: "%{actors} has reshared your %{post_link}." - other: "%{actors} have reshared your %{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." @@ -675,8 +675,18 @@ en: other: "%{actors} mentioned you in a deleted post." index: notifications: "Notifications" - mark_all_as_read: "Mark All as Read" + mark_all_as_read: "Mark all as read" + mark_read: "Mark read" mark_unread: "Mark unread" + show_all: "show all" + show_unread: "show unread" + all_notifications: "All Notifications" + also_commented: "Also commented" + comment_on_post: "Comment on post" + liked: "Liked" + mentioned: "Mentioned" + reshared: "Reshared" + started_sharing: "Started sharing" and_others: zero: "and nobody else" one: "and one more" diff --git a/config/locales/javascript/javascript.en.yml b/config/locales/javascript/javascript.en.yml index 5d65beb6c..0cf179ba4 100644 --- a/config/locales/javascript/javascript.en.yml +++ b/config/locales/javascript/javascript.en.yml @@ -98,6 +98,10 @@ en: conversation: participants: "Participants" + notifications: + mark_read: "Mark read" + mark_unread: "Mark unread" + stream: hide: "Hide" public: "Public" diff --git a/spec/controllers/jasmine_fixtures/notifications_spec.rb b/spec/controllers/jasmine_fixtures/notifications_spec.rb new file mode 100644 index 000000000..8851eb02d --- /dev/null +++ b/spec/controllers/jasmine_fixtures/notifications_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe NotificationsController do + describe '#index' do + before do + sign_in :user, alice + @post = FactoryGirl.create(:status_message) + FactoryGirl.create(:notification, :recipient => alice, :target => @post) + get :read_all + FactoryGirl.create(:notification, :recipient => alice, :target => @post) + eve.share_with(alice.person, eve.aspects.first) + end + + it "generates a jasmine fixture", :fixture => true do + get :index + save_fixture(html_for("body"), "notifications") + end + end +end diff --git a/spec/controllers/notifications_controller_spec.rb b/spec/controllers/notifications_controller_spec.rb index 256b7f465..5b2e37452 100644 --- a/spec/controllers/notifications_controller_spec.rb +++ b/spec/controllers/notifications_controller_spec.rb @@ -120,7 +120,21 @@ describe NotificationsController do Nokogiri(response.body).css('.aspect_membership').should_not be_empty end - + end + + describe "filter notifications" do + it "supports filtering by notification type" do + eve.share_with(alice.person, eve.aspects.first) + get :index, "type" => "started_sharing" + assigns[:notifications].count.should == 1 + end + + it "supports filtering by read/unread" do + get :read_all + 2.times { FactoryGirl.create(:notification, :recipient => alice, :target => @post) } + get :index, "show" => "unread" + assigns[:notifications].count.should == 2 + end end end end diff --git a/spec/helpers/notifications_helper_spec.rb b/spec/helpers/notifications_helper_spec.rb index 1015818eb..94817dd0a 100644 --- a/spec/helpers/notifications_helper_spec.rb +++ b/spec/helpers/notifications_helper_spec.rb @@ -77,7 +77,7 @@ describe NotificationsHelper do output.should include I18n.t("#{@notification.popup_translation_key}", :actors => notification_people_link(@notification), :count => @notification.actors.count, - :post_link => "#{t('notifications.post')}") + :post_link => link_to(post_page_title(@post), post_path(@post), 'data-ref' => @post.id, :class => 'hard_object_link').html_safe) end context 'when post is deleted' do From eabdc7390cab3d5f19e118fcc568925a68ff8d84 Mon Sep 17 00:00:00 2001 From: Steffen van Bergerem Date: Thu, 13 Mar 2014 21:34:22 +0100 Subject: [PATCH 2/3] Port aspect membership dropdown and hovercards --- app/assets/javascripts/app/app.js | 2 +- .../views/aspect_membership_blueprint_view.js | 161 ++++++++++++++++++ .../app/views/aspect_membership_view.js | 59 ++----- .../javascripts/app/views/hovercard_view.js | 6 +- .../views/publisher/aspect_selector_view.js | 2 +- app/assets/stylesheets/aspects.css.scss | 5 +- app/assets/stylesheets/buttons.css.scss | 19 ++- app/assets/stylesheets/hovercard.css.scss | 15 +- app/controllers/people_controller.rb | 3 +- app/helpers/aspect_global_helper.rb | 23 ++- app/helpers/people_helper.rb | 3 +- .../_aspect_membership_dropdown.html.haml | 26 +++ ...t_membership_dropdown_blueprint.html.haml} | 6 +- .../people/_aspect_membership_dropdown.haml | 2 +- .../jasmine_fixtures/people_spec.rb | 16 ++ .../aspect_membership_blueprint_view_spec.js | 118 +++++++++++++ .../app/views/aspect_membership_view_spec.js | 74 ++------ 17 files changed, 415 insertions(+), 125 deletions(-) create mode 100644 app/assets/javascripts/app/views/aspect_membership_blueprint_view.js create mode 100644 app/views/aspect_memberships/_aspect_membership_dropdown.html.haml rename app/views/{shared/_aspect_dropdown.html.haml => aspect_memberships/_aspect_membership_dropdown_blueprint.html.haml} (81%) create mode 100644 spec/javascripts/app/views/aspect_membership_blueprint_view_spec.js diff --git a/app/assets/javascripts/app/app.js b/app/assets/javascripts/app/app.js index 07ad66910..2c9099bf7 100644 --- a/app/assets/javascripts/app/app.js +++ b/app/assets/javascripts/app/app.js @@ -99,7 +99,7 @@ var app = { setupGlobalViews: function() { app.hovercard = new app.views.Hovercard(); - app.aspectMemberships = new app.views.AspectMembership(); + app.aspectMembershipsBlueprint = new app.views.AspectMembershipBlueprint(); app.sidebar = new app.views.Sidebar(); }, diff --git a/app/assets/javascripts/app/views/aspect_membership_blueprint_view.js b/app/assets/javascripts/app/views/aspect_membership_blueprint_view.js new file mode 100644 index 000000000..5608b32c2 --- /dev/null +++ b/app/assets/javascripts/app/views/aspect_membership_blueprint_view.js @@ -0,0 +1,161 @@ +/** + * this view lets the user (de-)select aspect memberships in the context + * of another users profile or the contact page. + * + * updates to the list of aspects are immediately propagated to the server, and + * the results are dislpayed as flash messages. + */ +app.views.AspectMembershipBlueprint = Backbone.View.extend({ + + initialize: function() { + // attach event handler, removing any previous instances + var selector = '.dropdown.aspect_membership .dropdown_list > li'; + $('body') + .off('click', selector) + .on('click', selector, _.bind(this._clickHandler, this)); + + this.list_item = null; + this.dropdown = null; + }, + + // decide what to do when clicked + // -> addMembership + // -> removeMembership + _clickHandler: function(evt) { + this.list_item = $(evt.target); + this.dropdown = this.list_item.parent(); + + this.list_item.addClass('loading'); + + if( this.list_item.is('.selected') ) { + var membership_id = this.list_item.data('membership_id'); + this.removeMembership(membership_id); + } else { + var aspect_id = this.list_item.data('aspect_id'); + var person_id = this.dropdown.data('person_id'); + this.addMembership(person_id, aspect_id); + } + + return false; // stop the event + }, + + // return the (short) name of the person associated with the current dropdown + _name: function() { + return this.dropdown.data('person-short-name'); + }, + + // create a membership for the given person in the given aspect + addMembership: function(person_id, aspect_id) { + var aspect_membership = new app.models.AspectMembership({ + 'person_id': person_id, + 'aspect_id': aspect_id + }); + + aspect_membership.on('sync', this._successSaveCb, this); + aspect_membership.on('error', function() { + this._displayError('aspect_dropdown.error'); + }, this); + + aspect_membership.save(); + }, + + _successSaveCb: function(aspect_membership) { + var aspect_id = aspect_membership.get('aspect_id'); + var membership_id = aspect_membership.get('id'); + var li = this.dropdown.find('li[data-aspect_id="'+aspect_id+'"]'); + + // the user didn't have this person in any aspects before, congratulate them + // on their newly found friendship ;) + if( this.dropdown.find('li.selected').length == 0 ) { + var msg = Diaspora.I18n.t('aspect_dropdown.started_sharing_with', { 'name': this._name() }); + Diaspora.page.flashMessages.render({ 'success':true, 'notice':msg }); + } + + li.attr('data-membership_id', membership_id) // just to be sure... + .data('membership_id', membership_id) + .addClass('selected'); + + this.updateSummary(); + this._done(); + }, + + // show an error flash msg + _displayError: function(msg_id) { + this._done(); + this.dropdown.removeClass('active'); // close the dropdown + + var msg = Diaspora.I18n.t(msg_id, { 'name': this._name() }); + Diaspora.page.flashMessages.render({ 'success':false, 'notice':msg }); + }, + + // remove the membership with the given id + removeMembership: function(membership_id) { + var aspect_membership = new app.models.AspectMembership({ + 'id': membership_id + }); + + aspect_membership.on('sync', this._successDestroyCb, this); + aspect_membership.on('error', function() { + this._displayError('aspect_dropdown.error_remove'); + }, this); + + aspect_membership.destroy(); + }, + + _successDestroyCb: function(aspect_membership) { + var membership_id = aspect_membership.get('id'); + var li = this.dropdown.find('li[data-membership_id="'+membership_id+'"]'); + + li.removeAttr('data-membership_id') + .removeData('membership_id') + .removeClass('selected'); + + // we just removed the last aspect, inform the user with a flash message + // that he is no longer sharing with that person + if( this.dropdown.find('li.selected').length == 0 ) { + var msg = Diaspora.I18n.t('aspect_dropdown.stopped_sharing_with', { 'name': this._name() }); + Diaspora.page.flashMessages.render({ 'success':true, 'notice':msg }); + } + + this.updateSummary(); + this._done(); + }, + + // cleanup tasks after aspect selection + _done: function() { + if( this.list_item ) { + this.list_item.removeClass('loading'); + } + }, + + // refresh the button text to reflect the current aspect selection status + updateSummary: function() { + var btn = this.dropdown.parents('div.aspect_membership').find('.button.toggle'); + var aspects_cnt = this.dropdown.find('li.selected').length; + var txt; + + if( aspects_cnt == 0 ) { + btn.removeClass('in_aspects'); + txt = Diaspora.I18n.t('aspect_dropdown.toggle.zero'); + } else { + btn.addClass('in_aspects'); + txt = this._pluralSummaryTxt(aspects_cnt); + } + + btn.text(txt + ' ▼'); + }, + + _pluralSummaryTxt: function(cnt) { + var all_aspects_cnt = this.dropdown.find('li').length; + + if( cnt == 1 ) { + return this.dropdown.find('li.selected').first().text(); + } + + if( cnt == all_aspects_cnt ) { + return Diaspora.I18n.t('aspect_dropdown.all_aspects'); + } + + return Diaspora.I18n.t('aspect_dropdown.toggle', { 'count':cnt.toString() }); + } +}); diff --git a/app/assets/javascripts/app/views/aspect_membership_view.js b/app/assets/javascripts/app/views/aspect_membership_view.js index aea67f090..f753c3134 100644 --- a/app/assets/javascripts/app/views/aspect_membership_view.js +++ b/app/assets/javascripts/app/views/aspect_membership_view.js @@ -1,3 +1,5 @@ +//= require ./aspects_dropdown_view + /** * this view lets the user (de-)select aspect memberships in the context * of another users profile or the contact page. @@ -5,15 +7,13 @@ * updates to the list of aspects are immediately propagated to the server, and * the results are dislpayed as flash messages. */ -app.views.AspectMembership = Backbone.View.extend({ +app.views.AspectMembership = app.views.AspectsDropdown.extend({ + + events: { + "click ul.aspect_membership.dropdown-menu > li.aspect_selector": "_clickHandler" + }, initialize: function() { - // attach event handler, removing any previous instances - var selector = '.dropdown.aspect_membership .dropdown_list > li'; - $('body') - .off('click', selector) - .on('click', selector, _.bind(this._clickHandler, this)); - this.list_item = null; this.dropdown = null; }, @@ -22,7 +22,7 @@ app.views.AspectMembership = Backbone.View.extend({ // -> addMembership // -> removeMembership _clickHandler: function(evt) { - this.list_item = $(evt.target); + this.list_item = $(evt.target).closest('li.aspect_selector'); this.dropdown = this.list_item.parent(); this.list_item.addClass('loading'); @@ -72,17 +72,16 @@ app.views.AspectMembership = Backbone.View.extend({ } li.attr('data-membership_id', membership_id) // just to be sure... - .data('membership_id', membership_id) - .addClass('selected'); + .data('membership_id', membership_id); - this.updateSummary(); + this.updateSummary(li); this._done(); }, // show an error flash msg _displayError: function(msg_id) { this._done(); - this.dropdown.removeClass('active'); // close the dropdown + this.dropdown.closest('.aspect_membership_dropdown').removeClass('open'); // close the dropdown var msg = Diaspora.I18n.t(msg_id, { 'name': this._name() }); Diaspora.page.flashMessages.render({ 'success':false, 'notice':msg }); @@ -107,8 +106,8 @@ app.views.AspectMembership = Backbone.View.extend({ var li = this.dropdown.find('li[data-membership_id="'+membership_id+'"]'); li.removeAttr('data-membership_id') - .removeData('membership_id') - .removeClass('selected'); + .removeData('membership_id'); + this.updateSummary(li); // we just removed the last aspect, inform the user with a flash message // that he is no longer sharing with that person @@ -117,7 +116,6 @@ app.views.AspectMembership = Backbone.View.extend({ Diaspora.page.flashMessages.render({ 'success':true, 'notice':msg }); } - this.updateSummary(); this._done(); }, @@ -129,33 +127,8 @@ app.views.AspectMembership = Backbone.View.extend({ }, // refresh the button text to reflect the current aspect selection status - updateSummary: function() { - var btn = this.dropdown.parents('div.aspect_membership').find('.button.toggle'); - var aspects_cnt = this.dropdown.find('li.selected').length; - var txt; - - if( aspects_cnt == 0 ) { - btn.removeClass('in_aspects'); - txt = Diaspora.I18n.t('aspect_dropdown.toggle.zero'); - } else { - btn.addClass('in_aspects'); - txt = this._pluralSummaryTxt(aspects_cnt); - } - - btn.text(txt + ' ▼'); + updateSummary: function(target) { + this._toggleCheckbox(target); + this._updateButton('green'); }, - - _pluralSummaryTxt: function(cnt) { - var all_aspects_cnt = this.dropdown.find('li').length; - - if( cnt == 1 ) { - return this.dropdown.find('li.selected').first().text(); - } - - if( cnt == all_aspects_cnt ) { - return Diaspora.I18n.t('aspect_dropdown.all_aspects'); - } - - return Diaspora.I18n.t('aspect_dropdown.toggle', { 'count':cnt.toString() }); - } }); diff --git a/app/assets/javascripts/app/views/hovercard_view.js b/app/assets/javascripts/app/views/hovercard_view.js index c32014a71..dc8e4cf82 100644 --- a/app/assets/javascripts/app/views/hovercard_view.js +++ b/app/assets/javascripts/app/views/hovercard_view.js @@ -103,10 +103,14 @@ app.views.Hovercard = Backbone.View.extend({ // set aspect dropdown var href = this.href(); - href += "/aspect_membership_button" + href += "/aspect_membership_button"; + if(gon.bootstrap == true){ + href += "?bootstrap=true"; + } $.get(href, function(response) { self.dropdown_container.html(response); }); + var aspect_membership = new app.views.AspectMembership({el: self.dropdown_container}); }, _positionHovercard: function() { diff --git a/app/assets/javascripts/app/views/publisher/aspect_selector_view.js b/app/assets/javascripts/app/views/publisher/aspect_selector_view.js index 4f9d53803..f2135f51d 100644 --- a/app/assets/javascripts/app/views/publisher/aspect_selector_view.js +++ b/app/assets/javascripts/app/views/publisher/aspect_selector_view.js @@ -1,4 +1,4 @@ -// require ../aspects_dropdown_view +//= require ../aspects_dropdown_view /* * Aspects view for the publisher. diff --git a/app/assets/stylesheets/aspects.css.scss b/app/assets/stylesheets/aspects.css.scss index 1ffed3318..6389959e5 100644 --- a/app/assets/stylesheets/aspects.css.scss +++ b/app/assets/stylesheets/aspects.css.scss @@ -18,7 +18,10 @@ .icon-refresh { display: inline-block;} .icon-ok { display: none;} } - a { cursor: pointer; } + a { + cursor: pointer; + padding-left: 10px; + } } } diff --git a/app/assets/stylesheets/buttons.css.scss b/app/assets/stylesheets/buttons.css.scss index be77288d9..f8913255b 100644 --- a/app/assets/stylesheets/buttons.css.scss +++ b/app/assets/stylesheets/buttons.css.scss @@ -5,8 +5,25 @@ border: 1px solid darken($button-border-color,20%); &:hover { - @include button-gradient-hover($creation-blue); background: $creation-blue; border: 1px solid darken($button-border-color,35%); } } +.btn-group.open > .btn.creation { + background: $creation-blue; +} + +.btn.green { + $button-border-color: #aaa; + @include button-gradient($green); + color: $grey; + border: 1px solid darken($button-border-color,20%); + + &:hover { + background: $green; + border: 1px solid darken($button-border-color,35%); + } +} +.btn-group.open > .btn.green { + background: $green; +} diff --git a/app/assets/stylesheets/hovercard.css.scss b/app/assets/stylesheets/hovercard.css.scss index 24ec49fd5..eb16b7a11 100644 --- a/app/assets/stylesheets/hovercard.css.scss +++ b/app/assets/stylesheets/hovercard.css.scss @@ -49,22 +49,25 @@ }; h4 { + margin-top: 0px; margin-bottom: 0px; padding-bottom: 0px; - } - - a { - color: $blue; - font-weight: bold !important; + font-size: 16px; + a { + color: $blue; + font-weight: bold !important; + } } p { color: $text-grey; padding-top: 0px; margin-top: 0px; - margin-bottom: 10px; + margin-bottom: 5px; } + .btn-group.aspect_membership_dropdown { margin: 0 !important; } + .hovercard_footer { position: absolute; bottom: 0; diff --git a/app/controllers/people_controller.rb b/app/controllers/people_controller.rb index 3f14f5586..64aa18b62 100644 --- a/app/controllers/people_controller.rb +++ b/app/controllers/people_controller.rb @@ -160,7 +160,8 @@ class PeopleController < ApplicationController return render :text => I18n.t('people.person.thats_you') if @person == current_user.person @contact = current_user.contact_for(@person) || Contact.new - render :partial => 'aspect_membership_dropdown', :locals => {:contact => @contact, :person => @person, :hang => 'left'} + bootstrap = params[:bootstrap] || false + render :partial => 'aspect_membership_dropdown', :locals => {:contact => @contact, :person => @person, :hang => 'left', :bootstrap => bootstrap} end private diff --git a/app/helpers/aspect_global_helper.rb b/app/helpers/aspect_global_helper.rb index 501cfab1a..34641505f 100644 --- a/app/helpers/aspect_global_helper.rb +++ b/app/helpers/aspect_global_helper.rb @@ -3,7 +3,7 @@ # the COPYRIGHT file. module AspectGlobalHelper - def aspect_membership_dropdown(contact, person, hang, aspect=nil) + def aspect_membership_dropdown(contact, person, hang, aspect=nil, force_bootstrap=false) aspect_membership_ids = {} selected_aspects = all_aspects.select{|aspect| contact.in_aspect?(aspect)} @@ -12,12 +12,21 @@ module AspectGlobalHelper aspect_membership_ids[a.id] = record.id end - render "shared/aspect_dropdown", - :selected_aspects => selected_aspects, - :aspect_membership_ids => aspect_membership_ids, - :person => person, - :hang => hang, - :dropdown_class => "aspect_membership" + if bootstrap? || force_bootstrap + render "aspect_memberships/aspect_membership_dropdown", + :selected_aspects => selected_aspects, + :aspect_membership_ids => aspect_membership_ids, + :person => person, + :hang => hang, + :dropdown_class => "aspect_membership" + else + render "aspect_memberships/aspect_membership_dropdown_blueprint", + :selected_aspects => selected_aspects, + :aspect_membership_ids => aspect_membership_ids, + :person => person, + :hang => hang, + :dropdown_class => "aspect_membership" + end end def aspect_dropdown_list_item(aspect, am_id=nil) diff --git a/app/helpers/people_helper.rb b/app/helpers/people_helper.rb index e5ffd25cc..baaeeedac 100644 --- a/app/helpers/people_helper.rb +++ b/app/helpers/people_helper.rb @@ -39,7 +39,8 @@ module PeopleHelper if opts[:to] == :photos link_to person_image_tag(person, opts[:size]), person_photos_path(person) else - " selected_aspects.size>0 ? "green" : "btn-default", "data-toggle" => "dropdown"} + %span.text + - if selected_aspects.size == all_aspects.size + = t('all_aspects') + - elsif selected_aspects.size == 1 + = selected_aspects.first.name + - else + = t('shared.aspect_dropdown.toggle', :count => selected_aspects.size) + %span.caret + + %ul.dropdown-menu{:class => ["pull-#{hang}", defined?(dropdown_class) && dropdown_class], :unSelectable => 'on', 'data-person_id' => (person.id if defined?(person) && person), 'data-service_uid' => (service_uid if defined?(service_uid)), 'data-person-short-name' => (person.first_name if defined?(person) && person)} + - for aspect in all_aspects + %li.aspect_selector{ :class => ('selected' if aspect_membership_ids[aspect.id].present?), 'data-aspect_id' => aspect.id, 'data-membership_id' => aspect_membership_ids[aspect.id] } + %a + %span.status_indicator + %i.icon-ok + %i.icon-refresh + %span.text + = aspect.name + + - if (dropdown_may_create_new_aspect && defined?(person) && person) + %li.divider + %li.newItem + .add_aspect + = link_to t('contacts.index.add_a_new_aspect'), new_aspect_path(:person_id => person.id, :remote => true), :rel => 'facebox' diff --git a/app/views/shared/_aspect_dropdown.html.haml b/app/views/aspect_memberships/_aspect_membership_dropdown_blueprint.html.haml similarity index 81% rename from app/views/shared/_aspect_dropdown.html.haml rename to app/views/aspect_memberships/_aspect_membership_dropdown_blueprint.html.haml index baae64f8c..baed2c0c3 100644 --- a/app/views/shared/_aspect_dropdown.html.haml +++ b/app/views/aspect_memberships/_aspect_membership_dropdown_blueprint.html.haml @@ -1,7 +1,3 @@ --# Copyright (c) 2010-2011, Diaspora Inc. This file is --# licensed under the Affero General Public License version 3 or later. See --# the COPYRIGHT file. - .dropdown{:class => ["hang_#{hang}", defined?(dropdown_class) && dropdown_class]} .button.toggle{:class => ("in_aspects" if selected_aspects.size > 0)} - if selected_aspects.size == all_aspects.size @@ -9,7 +5,7 @@ - elsif selected_aspects.size == 1 = selected_aspects.first.name - else - = t('.toggle', :count => selected_aspects.size) + = t('shared.aspect_dropdown.toggle', :count => selected_aspects.size) ▼ .wrapper diff --git a/app/views/people/_aspect_membership_dropdown.haml b/app/views/people/_aspect_membership_dropdown.haml index 778ca85bf..7efd9f1f3 100644 --- a/app/views/people/_aspect_membership_dropdown.haml +++ b/app/views/people/_aspect_membership_dropdown.haml @@ -1 +1 @@ -= aspect_membership_dropdown(@contact, @person, 'left') += aspect_membership_dropdown(@contact, @person, 'left', nil, bootstrap) diff --git a/spec/controllers/jasmine_fixtures/people_spec.rb b/spec/controllers/jasmine_fixtures/people_spec.rb index 25d227ac9..53ea8ae72 100644 --- a/spec/controllers/jasmine_fixtures/people_spec.rb +++ b/spec/controllers/jasmine_fixtures/people_spec.rb @@ -20,4 +20,20 @@ describe PeopleController do save_fixture(html_for("body"), "pending_external_people_search") end end + + describe '#aspect_membership_dropdown' do + before do + sign_in :user, bob + end + + it "generates a jasmine fixture using Blueprint", :fixture => true do + get :aspect_membership_dropdown, :person_id => alice.person.guid + save_fixture(html_for("body"), "aspect_membership_dropdown_blueprint") + end + + it "generates a jasmine fixture using Bootstrap", :fixture => true do + get :aspect_membership_dropdown, :person_id => alice.person.guid, :bootstrap => true + save_fixture(html_for("body"), "aspect_membership_dropdown_bootstrap") + end + end end diff --git a/spec/javascripts/app/views/aspect_membership_blueprint_view_spec.js b/spec/javascripts/app/views/aspect_membership_blueprint_view_spec.js new file mode 100644 index 000000000..d17405513 --- /dev/null +++ b/spec/javascripts/app/views/aspect_membership_blueprint_view_spec.js @@ -0,0 +1,118 @@ +describe("app.views.AspectMembershipBlueprint", function(){ + beforeEach(function() { + spec.loadFixture("aspect_membership_dropdown_blueprint"); + this.view = new app.views.AspectMembershipBlueprint(); + this.person_id = $('.dropdown_list').data('person_id'); + }); + + it('attaches to the aspect selector', function(){ + spyOn($.fn, 'on'); + view = new app.views.AspectMembership(); + + expect($.fn.on).toHaveBeenCalled(); + }); + + context('adding to aspects', function() { + beforeEach(function() { + this.newAspect = $('li:not(.selected)'); + this.newAspectId = this.newAspect.data('aspect_id'); + }); + + it('calls "addMembership"', function() { + spyOn(this.view, "addMembership"); + this.newAspect.trigger('click'); + + expect(this.view.addMembership).toHaveBeenCalledWith(this.person_id, this.newAspectId); + }); + + it('tries to create a new AspectMembership', function() { + spyOn(app.models.AspectMembership.prototype, "save"); + this.view.addMembership(1, 2); + + expect(app.models.AspectMembership.prototype.save).toHaveBeenCalled(); + }); + + it('displays an error when it fails', function() { + spyOn(this.view, "_displayError"); + spyOn(app.models.AspectMembership.prototype, "save").andCallFake(function() { + this.trigger('error'); + }); + + this.view.addMembership(1, 2); + + expect(this.view._displayError).toHaveBeenCalledWith('aspect_dropdown.error'); + }); + }); + + context('removing from aspects', function(){ + beforeEach(function() { + this.oldAspect = $('li.selected'); + this.oldMembershipId = this.oldAspect.data('membership_id'); + }); + + it('calls "removeMembership"', function(){ + spyOn(this.view, "removeMembership"); + this.oldAspect.trigger('click'); + + expect(this.view.removeMembership).toHaveBeenCalledWith(this.oldMembershipId); + }); + + it('tries to destroy an AspectMembership', function() { + spyOn(app.models.AspectMembership.prototype, "destroy"); + this.view.removeMembership(1); + + expect(app.models.AspectMembership.prototype.destroy).toHaveBeenCalled(); + }); + + it('displays an error when it fails', function() { + spyOn(this.view, "_displayError"); + spyOn(app.models.AspectMembership.prototype, "destroy").andCallFake(function() { + this.trigger('error'); + }); + + this.view.removeMembership(1); + + expect(this.view._displayError).toHaveBeenCalledWith('aspect_dropdown.error_remove'); + }); + }); + + context('summary text in the button', function() { + beforeEach(function() { + this.btn = $('div.button.toggle'); + this.btn.text(""); // reset + this.view.dropdown = $('ul.dropdown_list'); + }); + + it('shows "no aspects" when nothing is selected', function() { + $('li[data-aspect_id]').removeClass('selected'); + this.view.updateSummary(); + + expect(this.btn.text()).toContain(Diaspora.I18n.t('aspect_dropdown.toggle.zero')); + }); + + it('shows "all aspects" when everything is selected', function() { + $('li[data-aspect_id]').addClass('selected'); + this.view.updateSummary(); + + expect(this.btn.text()).toContain(Diaspora.I18n.t('aspect_dropdown.all_aspects')); + }); + + it('shows the name of the selected aspect ( == 1 )', function() { + var list = $('li[data-aspect_id]'); + list.removeClass('selected'); // reset + list.eq(1).addClass('selected'); + this.view.updateSummary(); + + expect(this.btn.text()).toContain(list.eq(1).text()); + }); + + it('shows the number of selected aspects ( > 1)', function() { + var list = $('li[data-aspect_id]'); + list.removeClass('selected'); // reset + $([list.eq(1), list.eq(2)]).addClass('selected'); + this.view.updateSummary(); + + expect(this.btn.text()).toContain(Diaspora.I18n.t('aspect_dropdown.toggle', { 'count':2 })); + }); + }); +}); diff --git a/spec/javascripts/app/views/aspect_membership_view_spec.js b/spec/javascripts/app/views/aspect_membership_view_spec.js index a0ee61128..5f5abe9bb 100644 --- a/spec/javascripts/app/views/aspect_membership_view_spec.js +++ b/spec/javascripts/app/views/aspect_membership_view_spec.js @@ -1,40 +1,22 @@ - describe("app.views.AspectMembership", function(){ beforeEach(function() { // mock a dummy aspect dropdown - this.person = factory.author({name: "My Name"}); - spec.content().html( - '' - ); - - this.view = new app.views.AspectMembership(); - }); - - it('attaches to the aspect selector', function(){ - spyOn($.fn, 'on'); - view = new app.views.AspectMembership(); - - expect($.fn.on).toHaveBeenCalled(); + spec.loadFixture("aspect_membership_dropdown_bootstrap"); + this.view = new app.views.AspectMembership({el: $('.aspect_membership_dropdown')}); + this.person_id = $('.dropdown-menu').data('person_id'); }); context('adding to aspects', function() { beforeEach(function() { - this.newAspect = spec.content().find('li:eq(0)'); - this.newAspectId = 10; + this.newAspect = $('li:not(.selected)'); + this.newAspectId = this.newAspect.data('aspect_id'); }); it('calls "addMembership"', function() { spyOn(this.view, "addMembership"); this.newAspect.trigger('click'); - expect(this.view.addMembership).toHaveBeenCalledWith(this.person.id, this.newAspectId); + expect(this.view.addMembership).toHaveBeenCalledWith(this.person_id, this.newAspectId); }); it('tries to create a new AspectMembership', function() { @@ -58,8 +40,8 @@ describe("app.views.AspectMembership", function(){ context('removing from aspects', function(){ beforeEach(function() { - this.oldAspect = spec.content().find('li:eq(1)'); - this.oldMembershipId = 99; + this.oldAspect = $('li.selected'); + this.oldMembershipId = this.oldAspect.data('membership_id'); }); it('calls "removeMembership"', function(){ @@ -88,43 +70,23 @@ describe("app.views.AspectMembership", function(){ }); }); - context('summary text in the button', function() { + context('updateSummary', function() { beforeEach(function() { - this.btn = spec.content().find('div.button.toggle'); - this.btn.text(""); // reset - this.view.dropdown = spec.content().find('ul.dropdown_list'); + this.Aspect = $('li:eq(0)'); }); - it('shows "no aspects" when nothing is selected', function() { - spec.content().find('li[data-aspect_id]').removeClass('selected'); - this.view.updateSummary(); + it('calls "_toggleCheckbox"', function() { + spyOn(this.view, "_toggleCheckbox"); + this.view.updateSummary(this.Aspect); - expect(this.btn.text()).toContain(Diaspora.I18n.t('aspect_dropdown.toggle.zero')); + expect(this.view._toggleCheckbox).toHaveBeenCalledWith(this.Aspect); }); - it('shows "all aspects" when everything is selected', function() { - spec.content().find('li[data-aspect_id]').addClass('selected'); - this.view.updateSummary(); + it('calls "_updateButton"', function() { + spyOn(this.view, "_updateButton"); + this.view.updateSummary(this.Aspect); - expect(this.btn.text()).toContain(Diaspora.I18n.t('aspect_dropdown.all_aspects')); - }); - - it('shows the name of the selected aspect ( == 1 )', function() { - var list = spec.content().find('li[data-aspect_id]'); - list.removeClass('selected'); // reset - list.eq(1).addClass('selected'); - this.view.updateSummary(); - - expect(this.btn.text()).toContain(list.eq(1).text()); - }); - - it('shows the number of selected aspects ( > 1)', function() { - var list = spec.content().find('li[data-aspect_id]'); - list.removeClass('selected'); // reset - $([list.eq(1), list.eq(2)]).addClass('selected'); - this.view.updateSummary(); - - expect(this.btn.text()).toContain(Diaspora.I18n.t('aspect_dropdown.toggle', { 'count':2 })); + expect(this.view._updateButton).toHaveBeenCalledWith('green'); }); }); }); From 4fc9c6416ed19b99a897604e7c405bd188ee46a0 Mon Sep 17 00:00:00 2001 From: Steffen van Bergerem Date: Thu, 13 Mar 2014 21:41:56 +0100 Subject: [PATCH 3/3] 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')); + }); + }); +});