diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 35b2c4f2d..1314686d8 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -154,7 +154,7 @@ class ApplicationController < ActionController::Base @stream = stream_klass.new(current_user, :max_time => max_time, :order => sort_order) if params[:only_posts] - render :partial => 'shared/stream', :locals => {:posts => @stream.posts} + render :partial => 'shared/stream', :locals => {:posts => @stream.stream_posts} else render 'aspects/index' end diff --git a/app/controllers/aspect_memberships_controller.rb b/app/controllers/aspect_memberships_controller.rb index d9a55d761..189c1fb5b 100644 --- a/app/controllers/aspect_memberships_controller.rb +++ b/app/controllers/aspect_memberships_controller.rb @@ -46,12 +46,11 @@ class AspectMembershipsController < ApplicationController @aspect = current_user.aspects.where(:id => params[:aspect_id]).first if @contact = current_user.share_with(@person, @aspect) - flash.now[:notice] = I18n.t 'aspects.add_to_aspect.success' + flash.now[:notice] = I18n.t('aspects.add_to_aspect.success') respond_with AspectMembership.where(:contact_id => @contact.id, :aspect_id => @aspect.id).first else - flash[:error] = I18n.t 'contacts.create.failure' - #TODO(dan) take this out once the .js template is removed - render :nothing => true + flash.now[:error] = I18n.t('contacts.create.failure') + render :nothing => true, :status => 409 end end diff --git a/app/controllers/aspects_controller.rb b/app/controllers/aspects_controller.rb index 5ca11fa67..942e95a7b 100644 --- a/app/controllers/aspects_controller.rb +++ b/app/controllers/aspects_controller.rb @@ -20,7 +20,7 @@ class AspectsController < ApplicationController :max_time => params[:max_time].to_i) if params[:only_posts] - render :partial => 'shared/stream', :locals => {:posts => @stream.posts} + render :partial => 'shared/stream', :locals => {:posts => @stream.stream_posts} end end diff --git a/app/controllers/blocks_controller.rb b/app/controllers/blocks_controller.rb new file mode 100644 index 000000000..1065bd103 --- /dev/null +++ b/app/controllers/blocks_controller.rb @@ -0,0 +1,32 @@ +class BlocksController < ApplicationController + before_filter :authenticate_user! + + def create + block = current_user.blocks.new(params[:block]) + + if block.save + disconnect_if_contact(block.person) + notice = {:notice => t('blocks.create.success')} + else + notice = {:error => t('blocks.create.failure')} + end + redirect_to :back, notice + end + + def destroy + if current_user.blocks.find(params[:id]).delete + notice = {:notice => t('blocks.destroy.success')} + else + notice = {:error => t('blocks.destroy.failure')} + end + redirect_to :back, notice + end + + protected + + def disconnect_if_contact(person) + if contact = current_user.contact_for(person) + current_user.disconnect(contact, :force => true) + end + end +end diff --git a/app/controllers/people_controller.rb b/app/controllers/people_controller.rb index 36990b730..623618ebb 100644 --- a/app/controllers/people_controller.rb +++ b/app/controllers/people_controller.rb @@ -95,6 +95,7 @@ class PeopleController < ApplicationController unless params[:format] == "json" # hovercard if current_user + @block = current_user.blocks.where(:person_id => @person.id).first @contact = current_user.contact_for(@person) @aspects_with_person = [] if @contact && !params[:only_posts] @@ -112,7 +113,7 @@ class PeopleController < ApplicationController end if params[:only_posts] - render :partial => 'shared/stream', :locals => {:posts => @stream.posts} + render :partial => 'shared/stream', :locals => {:posts => @stream.stream_posts} else respond_to do |format| format.all { respond_with @person, :locals => {:post_type => :all} } diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index d5757bedf..a4e094fa2 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -38,7 +38,7 @@ class TagsController < ApplicationController @stream = Stream::Tag.new(current_user, params[:name], :max_time => max_time, :page => params[:page]) if params[:only_posts] - render :partial => 'shared/stream', :locals => {:posts => @stream.posts} + render :partial => 'shared/stream', :locals => {:posts => @stream.stream_posts} return end end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 0628ce12f..505f3c8ed 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -20,6 +20,10 @@ class UsersController < ApplicationController end end + def privacy_settings + @blocks = current_user.blocks.includes(:person) + end + def update password_changed = false @user = current_user diff --git a/app/helpers/stream_element_helper.rb b/app/helpers/stream_element_helper.rb new file mode 100644 index 000000000..5c4ee610e --- /dev/null +++ b/app/helpers/stream_element_helper.rb @@ -0,0 +1,19 @@ +module StreamElementHelper + def block_user_control(author) + if user_signed_in? && current_user.person.id != author.id + link_to image_tag('deletelabel.png'), blocks_path(:block => {:person_id => author.id}), + :class => 'block_user delete', + :confirm => t('.ignore_user_description'), + :title => t('.ignore_user', :name => author.first_name), + :method => :post + end + end + + def delete_or_hide_button(post) + if user_signed_in? && current_user.owns?(post) + link_to image_tag('deletelabel.png'), post_path(post), :confirm => t('are_you_sure'), :method => :delete, :remote => true, :class => "delete remove_post", :title => t('delete') + else + link_to image_tag('deletelabel.png'), share_visibility_path(:id => "42", :post_id => post.id), :method => :put, :remote => true, :class => "delete remove_post vis_hide", :title => t('.hide_and_mute') + end + end +end diff --git a/app/helpers/stream_helper.rb b/app/helpers/stream_helper.rb index 6144989bb..bfd573616 100644 --- a/app/helpers/stream_helper.rb +++ b/app/helpers/stream_helper.rb @@ -10,15 +10,15 @@ module StreamHelper "/apps/1?#{{:max_time => @posts.last.created_at.to_i}.to_param}" elsif controller.instance_of?(PeopleController) person_path(@person, :max_time => time_for_scroll(opts[:ajax_stream], @stream)) - elsif controller.instance_of?(TagFollowingsController) + elsif controller.instance_of?(TagFollowingsController) tag_followings_path(:max_time => time_for_scroll(opts[:ajax_stream], @stream), :sort_order => session[:sort_order]) - elsif controller.instance_of?(CommunitySpotlightController) + elsif controller.instance_of?(CommunitySpotlightController) spotlight_path(:max_time => time_for_scroll(opts[:ajax_stream], @stream), :sort_order => session[:sort_order]) - elsif controller.instance_of?(MentionsController) + elsif controller.instance_of?(MentionsController) mentions_path(:max_time => time_for_scroll(opts[:ajax_stream], @stream), :sort_order => session[:sort_order]) - elsif controller.instance_of?(MultisController) + elsif controller.instance_of?(MultisController) multi_path(:max_time => time_for_scroll(opts[:ajax_stream], @stream), :sort_order => session[:sort_order]) - elsif controller.instance_of?(PostsController) + elsif controller.instance_of?(PostsController) public_stream_path(:max_time => time_for_scroll(opts[:ajax_stream], @stream), :sort_order => session[:sort_order]) elsif controller.instance_of?(AspectsController) aspects_path(:max_time => time_for_scroll(opts[:ajax_stream], @stream), :a_ids => @stream.aspect_ids, :sort_order => session[:sort_order]) @@ -28,15 +28,14 @@ module StreamHelper end def time_for_scroll(ajax_stream, stream) - if ajax_stream || stream.posts.empty? + if ajax_stream || stream.stream_posts.empty? (Time.now() + 1).to_i else - stream.posts.last.send(stream.order.to_sym).to_i + stream.stream_posts.last.send(stream.order.to_sym).to_i end - end - def time_for_sort post + def time_for_sort(post) if controller.instance_of?(AspectsController) post.send(session[:sort_order].to_sym) else diff --git a/app/models/block.rb b/app/models/block.rb new file mode 100644 index 000000000..2409a9d3b --- /dev/null +++ b/app/models/block.rb @@ -0,0 +1,15 @@ +class Block < ActiveRecord::Base + belongs_to :person + belongs_to :user + + validates :user_id, :presence => true + validates :person_id, :presence => true, :uniqueness => { :scope => :user_id } + + validate :not_blocking_yourself + + def not_blocking_yourself + if self.user.person.id == self.person_id + errors[:person_id] << "stop blocking yourself!" + end + end +end \ No newline at end of file diff --git a/app/models/contact.rb b/app/models/contact.rb index ba7358911..1d19283b1 100644 --- a/app/models/contact.rb +++ b/app/models/contact.rb @@ -15,11 +15,13 @@ class Contact < ActiveRecord::Base has_many :share_visibilities, :source => :shareable, :source_type => 'Post' has_many :posts, :through => :share_visibilities, :source => :shareable, :source_type => 'Post' - validate :not_contact_for_self + validate :not_contact_for_self, + :not_blocked_user validates_uniqueness_of :person_id, :scope => :user_id - before_destroy :destroy_notifications + before_destroy :destroy_notifications, + :repopulate_cache! # contact.sharing is true when contact.person is sharing with contact.user scope :sharing, lambda { @@ -39,7 +41,14 @@ class Contact < ActiveRecord::Base Notification.where(:target_type => "Person", :target_id => person_id, :recipient_id => user_id, - :type => "Notifications::StartedSharing").delete_all + :type => "Notifications::StartedSharing").delete_all + end + + def repopulate_cache! + if RedisCache.configured? + cache = RedisCache.new(self.user) + cache.repopulate! + end end def dispatch_request @@ -90,5 +99,14 @@ class Contact < ActiveRecord::Base errors[:base] << 'Cannot create self-contact' end end + + def not_blocked_user + if user.blocks.where(:person_id => person_id).exists? + errors[:base] << 'Cannot connect to an ignored user' + false + else + true + end + end end diff --git a/app/models/post.rb b/app/models/post.rb index 559564e69..e161f8fca 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -21,11 +21,25 @@ class Post < ActiveRecord::Base after_create :cache_for_author #scopes - scope :includes_for_a_stream, includes(:o_embed_cache, {:author => :profile}, :mentions => {:person => :profile}) #note should include root and photos, but i think those are both on status_message + scope :includes_for_a_stream, includes(:o_embed_cache, {:author => :profile}, :mentions => {:person => :profile}) #note should include root and photos, but i think those are both on status_message - def self.for_a_stream(max_time, order) - self.for_visible_shareable_sql(max_time, order). - includes_for_a_stream + def self.excluding_blocks(user) + people = user.blocks.includes(:person).map{|b| b.person} + + if people.present? + where("posts.author_id NOT IN (?)", people.map { |person| person.id }) + else + scoped + end + end + + def self.for_a_stream(max_time, order, user=nil) + scope = self.for_visible_shareable_sql(max_time, order). + includes_for_a_stream + + scope = scope.excluding_blocks(user) if user.present? + + scope end ############# diff --git a/app/models/status_message.rb b/app/models/status_message.rb index 2860afab9..69c10c477 100644 --- a/app/models/status_message.rb +++ b/app/models/status_message.rb @@ -37,8 +37,7 @@ class StatusMessage < Post def self.tag_stream(user, tag_array, max_time, order) owned_or_visible_by_user(user). - joins(:tags).where(:tags => {:name => tag_array}). - for_a_stream(max_time, order) + joins(:tags).where(:tags => {:name => tag_array}) end def text(opts = {}) diff --git a/app/models/user.rb b/app/models/user.rb index 96cd712fb..8871b2371 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -18,7 +18,7 @@ class User < ActiveRecord::Base before_validation :strip_and_downcase_username before_validation :set_current_language, :on => :create - + validates :username, :presence => true, :uniqueness => true validates_format_of :username, :with => /\A[A-Za-z0-9_]+\z/ validates_length_of :username, :maximum => 32 @@ -43,6 +43,7 @@ class User < ActiveRecord::Base has_many :user_preferences, :dependent => :destroy has_many :tag_followings, :dependent => :destroy has_many :followed_tags, :through => :tag_followings, :source => :tag, :order => 'tags.name' + has_many :blocks has_many :authorizations, :class_name => 'OAuth2::Provider::Models::ActiveRecord::Authorization', :foreign_key => :resource_owner_id has_many :applications, :through => :authorizations, :source => :client diff --git a/app/views/aspects/_aspect_stream.haml b/app/views/aspects/_aspect_stream.haml index 5ef3067a7..4aa415ddd 100644 --- a/app/views/aspects/_aspect_stream.haml +++ b/app/views/aspects/_aspect_stream.haml @@ -19,8 +19,8 @@ #gs-shim{:title => "3. #{t('.stay_updated')}", 'data-content' => t('.stay_updated_explanation')} #main_stream.stream{:data => {:guids => stream.aspect_ids.join(','), :time_for_scroll => time_for_scroll(stream.ajax_stream?, stream)}} - - if !stream.ajax_stream? && stream.posts.length > 0 - = render 'shared/stream', :posts => stream.posts + - if !stream.ajax_stream? && stream.stream_posts.length > 0 + = render 'shared/stream', :posts => stream.stream_posts #pagination =link_to(t('more'), next_page_path(:ajax_stream => stream.ajax_stream?), :class => 'paginate') diff --git a/app/views/aspects/index.mobile.haml b/app/views/aspects/index.mobile.haml index 806b88d18..4c5ab0cb9 100644 --- a/app/views/aspects/index.mobile.haml +++ b/app/views/aspects/index.mobile.haml @@ -9,8 +9,8 @@ = @stream.aspect #main_stream.stream - = render 'shared/stream', :posts => @stream.posts - -if @stream.posts.length > 0 + = render 'shared/stream', :posts => @stream.stream_posts + -if @stream.stream_posts.length > 0 #pagination %a.more-link.paginate{:href => next_page_path} %h1 diff --git a/app/views/people/_sub_header.html.haml b/app/views/people/_sub_header.html.haml index 334c60fe4..e11abc0b2 100644 --- a/app/views/people/_sub_header.html.haml +++ b/app/views/people/_sub_header.html.haml @@ -1,7 +1,13 @@ #author_info .right - if user_signed_in? && current_user.person != person - = aspect_membership_dropdown(contact, person, 'right') + - if @block.present? + = link_to t('users.privacy_settings.stop_ignoring'), block_path(@block), + :method => :delete, + :class => "button" + + - else + = aspect_membership_dropdown(contact, person, 'right') - elsif user_signed_in? && current_user.person == person = link_to t('people.profile_sidebar.edit_my_profile'), edit_profile_path, :class => 'button creation' diff --git a/app/views/people/show.html.haml b/app/views/people/show.html.haml index 05d4e76a6..314b6282f 100644 --- a/app/views/people/show.html.haml +++ b/app/views/people/show.html.haml @@ -27,17 +27,20 @@ = render 'people/sub_header', :person => @person, :contact => @contact / hackity hack until we get a photo stream - - if (@posts && @posts.length > 0) || (defined?(@stream) && @stream.posts.length > 0) + - if (@posts && @posts.length > 0) || @stream.stream_posts.length > 0 -if @post_type == :photos = render 'photos/index', :photos => @posts - else #main_stream.stream - = render 'shared/stream', :posts => @stream.posts + = render 'shared/stream', :posts => @stream.stream_posts #pagination =link_to(t('more'), next_page_path, :class => 'paginate') - else #main_stream %div{:style=>"text-align:center;", :class => "dull"} - = t('.has_not_shared_with_you_yet', :name => @person.first_name) + - if @block.present? + = t('.ignoring', :name => @person.first_name) + - else + = t('.has_not_shared_with_you_yet', :name => @person.first_name) diff --git a/app/views/people/show.mobile.haml b/app/views/people/show.mobile.haml index 414b9a875..c07b90bf9 100644 --- a/app/views/people/show.mobile.haml +++ b/app/views/people/show.mobile.haml @@ -20,12 +20,12 @@ = link_to t('.return_to_aspects'), aspects_manage_path = t('.to_accept_or_ignore') -- if @stream.posts.length > 0 +- if @stream.stream_posts.length > 0 -if @post_type == :photos - = render 'photos/index', :photos => @stream.posts + = render 'photos/index', :photos => @stream.stream_posts - else #main_stream.stream - = render 'shared/stream', :posts => @stream.posts + = render 'shared/stream', :posts => @stream.stream_posts #pagination =link_to(t('more'), next_page_path, :class => 'paginate') - else diff --git a/app/views/shared/_settings_nav.haml b/app/views/shared/_settings_nav.haml index fec8cf1a2..fcde8f046 100644 --- a/app/views/shared/_settings_nav.haml +++ b/app/views/shared/_settings_nav.haml @@ -1,5 +1,6 @@ %ul#settings_nav %li= link_to_unless_current t('profile'), edit_profile_path %li= link_to_unless_current t('account'), edit_user_path + %li= link_to_unless_current t('privacy'), privacy_settings_path %li= link_to_unless_current t('_services'), services_path %li= link_to_unless_current t('_applications'), authorizations_path diff --git a/app/views/shared/_stream_element.html.haml b/app/views/shared/_stream_element.html.haml index 9babe8298..db6e7468a 100644 --- a/app/views/shared/_stream_element.html.haml +++ b/app/views/shared/_stream_element.html.haml @@ -5,10 +5,9 @@ .stream_element{:id => post.guid, :class => from_group(post)} .right.controls - - if current_user && post.author.owner_id == current_user.id - = link_to image_tag('deletelabel.png'), post_path(post), :confirm => t('are_you_sure'), :method => :delete, :remote => true, :class => "delete stream_element_delete", :title => t('delete') - - else - = link_to image_tag('deletelabel.png'), share_visibility_path(:id => "42", :post_id => post.id), :method => :put, :remote => true, :class => "delete stream_element_delete vis_hide", :title => t('.hide_and_mute') + = block_user_control(post.author) + = delete_or_hide_button(post) + = image_tag 'ajax-loader.gif', :class => "hide_loader hidden" .undo_text.hidden diff --git a/app/views/tags/show.haml b/app/views/tags/show.haml index 99f2dae38..8d8ca74d8 100644 --- a/app/views/tags/show.haml +++ b/app/views/tags/show.haml @@ -67,8 +67,8 @@ %hr #main_stream.stream - - if @stream.posts.length > 0 - = render 'shared/stream', :posts => @stream.posts + - if @stream.stream_posts.length > 0 + = render 'shared/stream', :posts => @stream.stream_posts #pagination =link_to(t('more'), next_page_path, :class => 'paginate') - else diff --git a/app/views/tags/show.mobile.haml b/app/views/tags/show.mobile.haml index d87643c3d..a99d0387c 100644 --- a/app/views/tags/show.mobile.haml +++ b/app/views/tags/show.mobile.haml @@ -6,8 +6,8 @@ = @stream.display_tag_name #main_stream.stream - = render 'shared/stream', :posts => @stream.posts - -if @stream.posts.length > 0 + = render 'shared/stream', :posts => @stream.stream_posts + -if @stream.stream_posts.length > 0 #pagination %a.more-link.paginate{:href => next_page_path} %h1 diff --git a/app/views/users/privacy_settings.html.haml b/app/views/users/privacy_settings.html.haml new file mode 100644 index 000000000..7fd59f8a0 --- /dev/null +++ b/app/views/users/privacy_settings.html.haml @@ -0,0 +1,23 @@ +-# Copyright (c) 2010-2011, Diaspora Inc. This file is +-# licensed under the Affero General Public License version 3 or later. See +-# the COPYRIGHT file. + +- content_for :page_title do + = t('.title') + +#section_header + %h2 + = t('privacy') + = render 'shared/settings_nav' + +.span-12.prepend-5.last + %h3 + = t('.ignored_users') + + - @blocks.each do |block| + = block.person.name + \- + = link_to t('.stop_ignoring'), block_path(block), + :method => :delete + %br + diff --git a/config/locales/diaspora/en.yml b/config/locales/diaspora/en.yml index 25c08ff57..d1f4d555c 100644 --- a/config/locales/diaspora/en.yml +++ b/config/locales/diaspora/en.yml @@ -9,6 +9,7 @@ en: settings: "Settings" profile: "Profile" account: "Account" + privacy: "Privacy" _services: "Services" _applications: "Applications" _photos: "photos" @@ -543,6 +544,7 @@ en: start_sharing: "start sharing" message: "Message" mention: "Mention" + ignoring: "You are ignoring all posts from %{name}." sub_header: you_have_no_tags: "you have no tags!" add_some: "add some" @@ -605,7 +607,7 @@ en: permalink: "permalink" not_found: "Sorry, we couldn't find that post." - post_visibilites: + share_visibilites: update: post_hidden_and_muted: "%{name}'s post has been hidden, and notifications have been muted." see_it_on_their_profile: "If you want to see updates on this post, visit %{name}'s profile page." @@ -722,6 +724,14 @@ en: invite: "invite" not_on_diaspora: "Not yet on Diaspora" + blocks: + create: + success: "Alright, you won't see that user in your stream again. #silencio!" + failure: "I couldn't ignore that user. #evasion" + destroy: + success: "Let's see what they have to say! #sayhello" + failure: "I couldn't stop ignoring that user. #evasion" + shared: aspect_dropdown: add_to_aspect: "Add contact" @@ -767,7 +777,6 @@ en: reshare: reshare: "Reshare" public_explain: - control_your_audience: "Control your Audience" new_user_welcome_message: "Use #hashtags to classify your posts and find people who share your interests. Call out awesome people with @Mentions" visibility_dropdown: "Use this dropdown to change visibility of your post. (We suggest you make this first one public.)" @@ -785,7 +794,9 @@ en: connect_to_comment: "Connect to this user to comment on their post" currently_unavailable: 'commenting currently unavailable' via: "via %{link}" - hide_and_mute: "Hide and Mute" + ignore_user: "Ignore %{name}" + ignore_user_description: "Ignore and remove user from all aspects?" + hide_and_mute: "Hide and mute post" like: "Like" unlike: "Unlike" dislike: "Dislike" @@ -891,6 +902,7 @@ en: simply_visit: "Simply visit" on_your_mobile_device: "on your mobile device to access Diaspora* mobile." works_on_modern: "Works on all modern smartphones" + edit: export_data: "Export Data" photo_export_unavailable: "Photo exporting currently unavailable" @@ -919,6 +931,12 @@ en: show_community_spotlight: "Show Community Spotlight in Stream?" show_getting_started: 'Re-enable Getting Started' getting_started: 'New User Prefrences' + + privacy_settings: + title: "Privacy Settings" + ignored_users: "Ignored Users" + stop_ignoring: "Stop ignoring" + destroy: "Your account has been locked. It may take 20 minutes for us to finish closing your account. Thank you for trying Diaspora." getting_started: well_hello_there: "Well, hello there!" diff --git a/config/locales/javascript/javascript.en.yml b/config/locales/javascript/javascript.en.yml index 3e2e91877..3497eba47 100644 --- a/config/locales/javascript/javascript.en.yml +++ b/config/locales/javascript/javascript.en.yml @@ -40,6 +40,7 @@ en: all_aspects: "All aspects" stopped_sharing_with: "You have stopped sharing with {{name}}." started_sharing_with: "You have started sharing with {{name}}!" + error: "Couldn't start sharing with {{name}}. Are you ignoring them?" toggle: zero: "Select aspects" one: "In {{count}} aspect" diff --git a/config/routes.rb b/config/routes.rb index 2b37f97bf..1be9714a8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -76,10 +76,11 @@ Diaspora::Application.routes.draw do end controller :users do - get 'public/:username' => :public, :as => 'users_public' - match 'getting_started' => :getting_started, :as => 'getting_started' + get 'public/:username' => :public, :as => 'users_public' + match 'getting_started' => :getting_started, :as => 'getting_started' + match 'privacy' => :privacy_settings, :as => 'privacy_settings' get 'getting_started_completed' => :getting_started_completed - get 'confirm_email/:token' => :confirm_email, :as => 'confirm_email' + get 'confirm_email/:token' => :confirm_email, :as => 'confirm_email' end # This is a hack to overide a route created by devise. @@ -109,8 +110,9 @@ Diaspora::Application.routes.draw do resources :contacts, :except => [:update, :create] do get :sharing, :on => :collection end - resources :aspect_memberships, :only => [:destroy, :create, :update] - resources :share_visibilities, :only => [:update] + resources :aspect_memberships, :only => [:destroy, :create, :update] + resources :share_visibilities, :only => [:update] + resources :blocks, :only => [:create, :destroy] get 'spotlight' => 'community_spotlight#index', :as => 'spotlight' diff --git a/db/migrate/20111101202137_create_blocks.rb b/db/migrate/20111101202137_create_blocks.rb new file mode 100644 index 000000000..31e451740 --- /dev/null +++ b/db/migrate/20111101202137_create_blocks.rb @@ -0,0 +1,12 @@ +class CreateBlocks < ActiveRecord::Migration + def self.up + create_table :blocks do |t| + t.integer :user_id + t.integer :person_id + end + end + + def self.down + drop_table :blocks + end +end diff --git a/db/schema.rb b/db/schema.rb index a2b1512c2..4a3992302 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20111026173547) do +ActiveRecord::Schema.define(:version => 20111101202137) do create_table "aspect_memberships", :force => true do |t| t.integer "aspect_id", :null => false @@ -47,6 +47,11 @@ ActiveRecord::Schema.define(:version => 20111026173547) do add_index "aspects", ["user_id", "contacts_visible"], :name => "index_aspects_on_user_id_and_contacts_visible" add_index "aspects", ["user_id"], :name => "index_aspects_on_user_id" + create_table "blocks", :force => true do |t| + t.integer "user_id" + t.integer "person_id" + end + create_table "comments", :force => true do |t| t.text "text", :null => false t.integer "commentable_id", :null => false diff --git a/features/blocks_user.feature b/features/blocks_user.feature new file mode 100644 index 000000000..d74620b1a --- /dev/null +++ b/features/blocks_user.feature @@ -0,0 +1,18 @@ +@javascript +Feature: Blocking a user from the stream + Background: + Given a user named "Bob Jones" with email "bob@bob.bob" + And a user named "Alice Smith" with email "alice@alice.alice" + And a user with email "bob@bob.bob" is connected with "alice@alice.alice" + And Alice has a post mentioning Bob + + + Scenario: Blocking a user + When I sign in as "bob@bob.bob" + And I am on the home page + And I preemptively confirm the alert + And I wait for the ajax to finish + When I click on the first block button + And I am on the home page + And I wait for the ajax to finish + Then I should not see any posts in my stream diff --git a/features/comments.feature b/features/comments.feature index 1441fddf1..df0509574 100644 --- a/features/comments.feature +++ b/features/comments.feature @@ -52,7 +52,6 @@ Feature: commenting And I preemptively confirm the alert And I click to delete the first comment And I wait for the ajax to finish - And I wait for 2 seconds Then I should not see "is that a poodle?" Scenario: expand the comment form in the main stream and an individual aspect stream diff --git a/features/signs_up.feature b/features/signs_up.feature index f911ea229..4a2e13376 100644 --- a/features/signs_up.feature +++ b/features/signs_up.feature @@ -7,7 +7,7 @@ Feature: new user registration And I fill in "user_email" with "ohai@example.com" And I fill in "user_password" with "secret" And I fill in "user_password_confirmation" with "secret" - And I press "Create my account" + And I press "Create my account!" Then I should be on the getting started page And I should see "Well, hello there!" And I should see "Who are you?" diff --git a/features/step_definitions/custom_web_steps.rb b/features/step_definitions/custom_web_steps.rb index 89e22582a..9682bbea5 100644 --- a/features/step_definitions/custom_web_steps.rb +++ b/features/step_definitions/custom_web_steps.rb @@ -58,7 +58,7 @@ And /^I hover over the "([^"]+)"$/ do |element| end When /^I click to delete the first post$/ do - page.execute_script('$(".stream_element").first().find(".stream_element_delete").first().click()') + page.execute_script('$(".stream_element").first().find(".remove_post").first().click()') end When /^I click to delete the first comment$/ do diff --git a/features/step_definitions/posts_steps.rb b/features/step_definitions/posts_steps.rb index 5bd2ff50a..c6c9c80d5 100644 --- a/features/step_definitions/posts_steps.rb +++ b/features/step_definitions/posts_steps.rb @@ -13,6 +13,10 @@ Then /^I should not see an uploaded image within the photo drop zone$/ do find("#photodropzone img").should be_nil end +Then /^I should not see any posts in my stream$/ do + find(".stream_element").should be_nil +end + Given /^"([^"]*)" has a public post with text "([^"]*)"$/ do |email, text| user = User.find_by_email(email) user.post(:status_message, :text => text, :public => true, :to => user.aspects) @@ -26,3 +30,7 @@ end When /^The user deletes their first post$/ do @me.posts.first.destroy end + +When /^I click on the first block button/ do + find(".block_user").click +end diff --git a/lib/diaspora/redis_cache.rb b/lib/diaspora/redis_cache.rb index 0d980d71f..60b8a4c33 100644 --- a/lib/diaspora/redis_cache.rb +++ b/lib/diaspora/redis_cache.rb @@ -4,10 +4,10 @@ class RedisCache - SUPPORTED_CACHES = [:created_at] #['updated_at', + SUPPORTED_CACHES = [:created_at] CACHE_LIMIT = 100 - def initialize(user, order_field) + def initialize(user, order_field=:created_at) @user = user @order_field = order_field.to_s end diff --git a/lib/diaspora/user/connecting.rb b/lib/diaspora/user/connecting.rb index bcaf168a7..47ef5f07a 100644 --- a/lib/diaspora/user/connecting.rb +++ b/lib/diaspora/user/connecting.rb @@ -11,6 +11,8 @@ module Diaspora # @return [Contact] The newly made contact for the passed in person. def share_with(person, aspect) contact = self.contacts.find_or_initialize_by_person_id(person.id) + return false unless contact.valid? + unless contact.receiving? contact.dispatch_request contact.receiving = true @@ -56,7 +58,7 @@ module Diaspora end end - def disconnect(bad_contact) + def disconnect(bad_contact, opts={}) person = bad_contact.person Rails.logger.info("event=disconnect user=#{diaspora_handle} target=#{person.diaspora_handle}") retraction = Retraction.for(self) @@ -64,7 +66,7 @@ module Diaspora Postzord::Dispatcher.build(self, retraction).post AspectMembership.where(:contact_id => bad_contact.id).delete_all - remove_contact(bad_contact) + remove_contact(bad_contact, opts) end def disconnected_by(person) diff --git a/lib/diaspora/user/querying.rb b/lib/diaspora/user/querying.rb index c9b76bf6e..d6a83a637 100644 --- a/lib/diaspora/user/querying.rb +++ b/lib/diaspora/user/querying.rb @@ -141,7 +141,7 @@ module Diaspora end def shareables_from(klass, person) - return self.person.send(klass.table_name).where(:pending => false).order("created_at DESC") if person == self.person + return self.person.send(klass.table_name).where(:pending => false).order("#{klass.table_name}.created_at DESC") if person == self.person con = Contact.arel_table p = klass.arel_table shareable_ids = [] diff --git a/lib/stream/aspect.rb b/lib/stream/aspect.rb index 0cf4ce668..3ddacf0ae 100644 --- a/lib/stream/aspect.rb +++ b/lib/stream/aspect.rb @@ -43,7 +43,7 @@ class Stream::Aspect < Stream::Base :type => TYPES_OF_POST_IN_STREAM, :order => "#{order} DESC", :max_time => max_time - ).for_a_stream(max_time, order) + ) end # @return [ActiveRecord::Association] AR association of people within stream's given aspects diff --git a/lib/stream/base.rb b/lib/stream/base.rb index a117ee996..89b24eaba 100644 --- a/lib/stream/base.rb +++ b/lib/stream/base.rb @@ -43,13 +43,19 @@ class Stream::Base # @return [ActiveRecord::Relation] def posts - [] + Post.scoped + end + + # @return [ActiveRecord::Relation] + def stream_posts + self.posts.for_a_stream(max_time, order, self.user) end # @return [ActiveRecord::Association] AR association of people within stream's given aspects def people - people_ids = posts.map{|x| x.author_id} - Person.where(:id => people_ids).includes(:profile) + people_ids = self.stream_posts.map{|x| x.author_id} + Person.where(:id => people_ids). + includes(:profile) end # @return [String] diff --git a/lib/stream/community_spotlight.rb b/lib/stream/community_spotlight.rb index dee1ed33a..340bda6c0 100644 --- a/lib/stream/community_spotlight.rb +++ b/lib/stream/community_spotlight.rb @@ -20,7 +20,7 @@ class Stream::CommunitySpotlight < Stream::Base end def posts - Post.all_public.where(:author_id => people.map{|x| x.id}).for_a_stream(max_time, order) + Post.all_public.where(:author_id => people.map{|x| x.id}) end def people diff --git a/lib/stream/mention.rb b/lib/stream/mention.rb index b385f5af9..83ad8b773 100644 --- a/lib/stream/mention.rb +++ b/lib/stream/mention.rb @@ -13,7 +13,7 @@ class Stream::Mention < Stream::Base # @return [ActiveRecord::Association] AR association of posts def posts - @posts ||= StatusMessage.where_person_is_mentioned(self.user.person).for_a_stream(max_time, order) + @posts ||= StatusMessage.where_person_is_mentioned(self.user.person) end def contacts_title diff --git a/lib/stream/multi.rb b/lib/stream/multi.rb index f02277617..cbdbaf00d 100644 --- a/lib/stream/multi.rb +++ b/lib/stream/multi.rb @@ -19,13 +19,13 @@ class Stream::Multi < Stream::Base @posts ||= lambda do post_ids = aspects_post_ids + followed_tags_post_ids + mentioned_post_ids post_ids += community_spotlight_post_ids if include_community_spotlight? - Post.where(:id => post_ids).for_a_stream(max_time, order) + Post.where(:id => post_ids) end.call end # @return [Boolean] def ajax_stream? - false + true end #emits an enum of the groups which the post appeared diff --git a/lib/stream/person.rb b/lib/stream/person.rb index 83cd47d4b..87c2442f3 100644 --- a/lib/stream/person.rb +++ b/lib/stream/person.rb @@ -15,9 +15,9 @@ class Stream::Person < Stream::Base def posts @posts ||= lambda do if user - posts = self.user.posts_from(@person).for_a_stream(max_time, order) + posts = self.user.posts_from(@person) else - posts = @person.posts.where(:public => true).for_a_stream(max_time, order) + posts = @person.posts.where(:public => true) end posts end.call diff --git a/lib/stream/public.rb b/lib/stream/public.rb index a9056795e..f5b6f421a 100644 --- a/lib/stream/public.rb +++ b/lib/stream/public.rb @@ -13,7 +13,7 @@ class Stream::Public < Stream::Base # @return [ActiveRecord::Association] AR association of posts def posts - @posts ||= Post.all_public.for_a_stream(max_time, order) + @posts ||= Post.all_public end diff --git a/lib/stream/tag.rb b/lib/stream/tag.rb index 48528964d..375a643a9 100644 --- a/lib/stream/tag.rb +++ b/lib/stream/tag.rb @@ -52,6 +52,6 @@ class Stream::Tag < Stream::Base else posts = posts.all_public end - posts.tagged_with(tag_name).for_a_stream(max_time, 'created_at') + posts.tagged_with(tag_name) end end diff --git a/public/javascripts/contact-edit.js b/public/javascripts/contact-edit.js index 4e55200df..0e00c1c08 100644 --- a/public/javascripts/contact-edit.js +++ b/public/javascripts/contact-edit.js @@ -6,7 +6,6 @@ var ContactEdit = { init: function(){ $.extend(ContactEdit, AspectsDropdown); $('.dropdown.aspect_membership .dropdown_list > li, .dropdown.inviter .dropdown_list > li').live('click', function(evt){ - ContactEdit.processClick($(this), evt); }); }, @@ -64,7 +63,10 @@ var ContactEdit = { }, toggleAspectMembership: function(li, evt) { - var button = li.find('.button'); + var button = li.find('.button'), + dropdown = li.closest('.dropdown'), + dropdownList = li.parent('.dropdown_list'); + if(button.hasClass('disabled') || li.hasClass('newItem')){ return; } var selected = li.hasClass("selected"), @@ -75,12 +77,19 @@ var ContactEdit = { "person_id": li.parent().data("person_id"), "_method": (selected) ? "DELETE" : "POST" }, function(aspectMembership) { - li.removeClass("loading"); ContactEdit.toggleCheckbox(li); ContactEdit.updateNumber(li.closest(".dropdown_list"), li.parent().data("person_id"), aspectMembership.aspect_ids.length, 'in_aspects'); Diaspora.page.publish("aspectDropdown/updated", [li.parent().data("person_id"), li.parents(".dropdown").parent(".right").html()]); - }); + }) + .error(function() { + var message = Diaspora.I18n.t("aspect_dropdown.error", {name: dropdownList.data('person-short-name')}); + Diaspora.page.flashMessages.render({success: false, notice: message}); + dropdown.removeClass('active'); + }) + .complete(function() { + li.removeClass("loading"); + }); } }; diff --git a/public/javascripts/widgets/stream-element.js b/public/javascripts/widgets/stream-element.js index ac431f891..a042da3a7 100644 --- a/public/javascripts/widgets/stream-element.js +++ b/public/javascripts/widgets/stream-element.js @@ -13,7 +13,8 @@ timeAgo: self.instantiate("TimeAgo", element.find(".timeago a abbr.timeago")), content: element.find(".content .collapsible"), - deletePostLink: element.find("a.stream_element_delete"), + blockUserLink: element.find(".block_user"), + deletePostLink: element.find(".remove_post"), focusCommentLink: element.find("a.focus_comment_textarea"), hidePostLoader: element.find("img.hide_loader"), hidePostUndo: element.find("a.stream_element_hide_undo"), @@ -22,8 +23,11 @@ }); // twipsy tooltips - self.deletePostLink.twipsy(); - self.postScope.twipsy(); + $([ + self.blockUserLink, + self.deletePostLink, + self.postScope + ]).map(function() { this.twipsy(); }); // collapse long posts self.content.expander({ @@ -40,8 +44,10 @@ self.deletePostLink.click(function(evt) { evt.preventDefault(); - self.deletePostLink.toggleClass("hidden"); - self.hidePostLoader.toggleClass("hidden"); + $([ + self.deletePostLink, + self.hidePostLoader + ]).toggleClass("hidden"); }); self.focusCommentLink.click(function(evt) { diff --git a/public/stylesheets/sass/application.sass b/public/stylesheets/sass/application.sass index 525032183..3da4106b7 100644 --- a/public/stylesheets/sass/application.sass +++ b/public/stylesheets/sass/application.sass @@ -96,6 +96,8 @@ form :padding :top 45px :bottom 8px + :left 12px + :right 12px #flash_notice .message @@ -791,9 +793,6 @@ a.paginate, #infscr-loading :text-align center :width 100% - &:hover - :border 1px solid #1C6D99 - #main_stream :position relative :z-index 0 diff --git a/spec/controllers/blocks_controller_spec.rb b/spec/controllers/blocks_controller_spec.rb new file mode 100644 index 000000000..0fe07a91c --- /dev/null +++ b/spec/controllers/blocks_controller_spec.rb @@ -0,0 +1,67 @@ +require 'spec_helper' + +describe BlocksController do + before do + sign_in alice + end + + describe "#create" do + it "creates a block" do + expect { + post :create, :block => {:person_id => eve.person.id} + }.should change { alice.blocks.count }.by(1) + end + + it "redirects back" do + post :create, :block => { :person_id => 2 } + + response.should be_redirect + end + + it "notifies the user" do + post :create, :block => { :person_id => 2 } + + flash.should_not be_empty + end + + it "calls #disconnect_if_contact" do + @controller.should_receive(:disconnect_if_contact).with(bob.person) + post :create, :block => {:person_id => bob.person.id} + end + end + + describe "#destroy" do + before do + @block = alice.blocks.create(:person => eve.person) + end + + it "redirects back" do + delete :destroy, :id => @block.id + response.should be_redirect + end + + it "removes a block" do + expect { + delete :destroy, :id => @block.id + }.should change { alice.blocks.count }.by(-1) + end + end + + describe "#disconnect_if_contact" do + before do + @controller.stub(:current_user).and_return(alice) + end + + it "calls disconnect with the force option if there is a contact for a given user" do + contact = alice.contact_for(bob.person) + alice.stub(:contact_for).and_return(contact) + alice.should_receive(:disconnect).with(contact, hash_including(:force => true)) + @controller.send(:disconnect_if_contact, bob.person) + end + + it "doesn't call disconnect if there is a contact for a given user" do + alice.should_not_receive(:disconnect) + @controller.send(:disconnect_if_contact, eve.person) + end + end +end diff --git a/spec/controllers/people_controller_spec.rb b/spec/controllers/people_controller_spec.rb index 650b5dee3..62305276a 100644 --- a/spec/controllers/people_controller_spec.rb +++ b/spec/controllers/people_controller_spec.rb @@ -242,7 +242,7 @@ describe PeopleController do it 'is sorted by created_at desc' do get :show, :id => @person.id - assigns[:stream].posts.should == @public_posts.sort_by{|p| p.created_at}.reverse + assigns[:stream].stream_posts.should == @public_posts.sort_by{|p| p.created_at}.reverse end end diff --git a/spec/controllers/tag_followings_controller_spec.rb b/spec/controllers/tag_followings_controller_spec.rb index dfb17a2f2..613ef8485 100644 --- a/spec/controllers/tag_followings_controller_spec.rb +++ b/spec/controllers/tag_followings_controller_spec.rb @@ -13,6 +13,7 @@ describe TagFollowingsController do before do @tag = ActsAsTaggableOn::Tag.create!(:name => "partytimeexcellent") sign_in :user, bob + bob.followed_tags.create(:name => "testing") end describe 'index' do diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 4f3b48483..f20ef2057 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -159,6 +159,13 @@ describe UsersController do end end + describe '#privacy_settings' do + it "returns a 200" do + get 'privacy_settings' + response.status.should == 200 + end + end + describe '#edit' do it "returns a 200" do get 'edit', :id => @user.id @@ -220,15 +227,13 @@ describe UsersController do describe 'getting_started' do it 'does not fail miserably' do - get :getting_started - response.should be_success - + get :getting_started + response.should be_success end it 'does not fail miserably on mobile' do - get :getting_started, :format => :mobile - response.should be_success - + get :getting_started, :format => :mobile + response.should be_success end end end diff --git a/spec/lib/stream/base_spec.rb b/spec/lib/stream/base_spec.rb index d9f538292..6fcd5004b 100644 --- a/spec/lib/stream/base_spec.rb +++ b/spec/lib/stream/base_spec.rb @@ -43,6 +43,13 @@ describe Stream::Base do end end + describe '#people' do + it 'excludes blocked people' do + @stream.should_receive(:stream_posts).and_return(stub.as_null_object) + @stream.people + end + end + describe 'shared behaviors' do it_should_behave_like 'it is a stream' end diff --git a/spec/lib/stream/public_spec.rb b/spec/lib/stream/public_spec.rb index 9f0fdacf1..cb09c3962 100644 --- a/spec/lib/stream/public_spec.rb +++ b/spec/lib/stream/public_spec.rb @@ -3,7 +3,7 @@ require File.join(Rails.root, 'spec', 'shared_behaviors', 'stream') describe Stream::Public do before do - @stream = Stream::Public.new(stub) + @stream = Stream::Public.new(alice) end describe 'shared behaviors' do diff --git a/spec/models/block_spec.rb b/spec/models/block_spec.rb new file mode 100644 index 000000000..a6b598c06 --- /dev/null +++ b/spec/models/block_spec.rb @@ -0,0 +1,11 @@ +require 'spec_helper' + +describe Block do + describe 'validations' do + it 'doesnt allow you to block yourself' do + block = alice.blocks.create(:person => alice.person) + + block.should have(1).error_on(:person_id) + end + end +end \ No newline at end of file diff --git a/spec/models/contact_spec.rb b/spec/models/contact_spec.rb index 2bf82f661..1a3c507aa 100644 --- a/spec/models/contact_spec.rb +++ b/spec/models/contact_spec.rb @@ -174,4 +174,52 @@ describe Contact do end end end + + describe "#repopulate_cache" do + before do + @contact = bob.contact_for(alice.person) + end + + it "repopulates the cache if the cache exists" do + cache = stub(:repopulate!) + RedisCache.stub(:configured? => true, :new => cache) + + cache.should_receive(:repopulate!) + @contact.repopulate_cache! + end + + it "does not touch the cache if it is not configured" do + RedisCache.stub(:configured?).and_return(false) + RedisCache.should_not_receive(:new) + @contact.repopulate_cache! + end + + it "gets called on destroy" do + @contact.should_receive(:repopulate_cache!) + @contact.destroy + end + end + + describe "#not_blocked_user" do + before do + @contact = alice.contact_for(bob.person) + end + + it "is called on validate" do + @contact.should_receive(:not_blocked_user) + @contact.valid? + end + + it "adds to errors if potential contact is blocked by user" do + person = eve.person + block = alice.blocks.create(:person => person) + bad_contact = alice.contacts.create(:person => person) + + bad_contact.send(:not_blocked_user).should be_false + end + + it "does not add to errors" do + @contact.send(:not_blocked_user).should be_true + end + end end diff --git a/spec/models/post_spec.rb b/spec/models/post_spec.rb index 648a0c24b..55f9b3430 100644 --- a/spec/models/post_spec.rb +++ b/spec/models/post_spec.rb @@ -58,6 +58,33 @@ describe Post do Post.should_receive(:includes_for_a_stream) Post.for_a_stream(stub, stub) end + + it 'calls excluding_blocks if a user is present' do + user = stub + Post.should_receive(:excluding_blocks).with(user) + Post.for_a_stream(stub, stub, user) + end + end + + describe '.excluding_blocks' do + before do + @post = Factory(:status_message, :author => alice.person) + @other_post = Factory(:status_message, :author => eve.person) + + bob.blocks.create(:person => alice.person) + end + + it 'does not included blocked users posts' do + Post.excluding_blocks(bob).should_not include(@post) + end + + it 'includes not blocked users posts' do + Post.excluding_blocks(bob).should include(@other_post) + end + + it 'returns posts if you dont have any blocks' do + Post.excluding_blocks(alice).count.should == 2 + end end context 'having some posts' do diff --git a/spec/models/user/connecting_spec.rb b/spec/models/user/connecting_spec.rb index c5cf3da1a..78d86934b 100644 --- a/spec/models/user/connecting_spec.rb +++ b/spec/models/user/connecting_spec.rb @@ -52,8 +52,8 @@ describe Diaspora::UserModules::Connecting do it 'calls remove contact' do contact = bob.contact_for(alice.person) - bob.should_receive(:remove_contact).with(contact) - bob.disconnect contact + bob.should_receive(:remove_contact).with(contact, {}) + bob.disconnect(contact) end it 'dispatches a retraction' do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 6902db86b..8ac6489b8 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -158,7 +158,7 @@ describe User do alice.username = "hexagooooooooooooooooooooooooooon" alice.should_not be_valid end - + it "cannot be one of the blacklist names" do ['hostmaster', 'postmaster', 'root', 'webmaster'].each do |username| alice.username = username @@ -382,9 +382,7 @@ describe User do end end - describe '.find_or_create_by_invitation' do - - end + describe '.find_or_create_by_invitation' describe '.create_from_invitation!' do before do @@ -400,7 +398,6 @@ describe User do it 'sets the email if the service is email' do @user.email.should == @inv.identifier end - end describe 'update_user_preferences' do @@ -551,7 +548,6 @@ describe User do alice.remove_all_traces end - it 'should remove mentions' do alice.should_receive(:remove_mentions) alice.remove_all_traces