diff --git a/lib/stream.rb b/lib/stream.rb new file mode 100644 index 000000000..e778980cf --- /dev/null +++ b/lib/stream.rb @@ -0,0 +1,3 @@ +module Stream + +end diff --git a/lib/stream/aspect.rb b/lib/stream/aspect.rb new file mode 100644 index 000000000..86d9f817f --- /dev/null +++ b/lib/stream/aspect.rb @@ -0,0 +1,120 @@ +# Copyright (c) 2010-2011, Diaspora Inc. This file is +# licensed under the Affero General Public License version 3 or later. See +# the COPYRIGHT file. + +class Stream::Aspect< Stream::Base + + # @param user [User] + # @param inputted_aspect_ids [Array] Ids of aspects for given stream + # @param aspect_ids [Array] Aspects this stream is responsible for + # @opt max_time [Integer] Unix timestamp of stream's post ceiling + # @opt order [String] Order of posts (i.e. 'created_at', 'updated_at') + # @return [void] + def initialize(user, inputted_aspect_ids, opts={}) + super(user, opts) + @inputted_aspect_ids = inputted_aspect_ids + end + + # Filters aspects given the stream's aspect ids on initialization and the user. + # Will disclude aspects from inputted aspect ids if user is not associated with their + # target aspects. + # + # @return [ActiveRecord::Association] Filtered aspects given the stream's user + def aspects + @aspects ||= lambda do + a = user.aspects + a = a.where(:id => @inputted_aspect_ids) if @inputted_aspect_ids.any? + a + end.call + end + + # Maps ids into an array from #aspects + # + # @return [Array] Aspect ids + def aspect_ids + @aspect_ids ||= aspects.map { |a| a.id } + end + + # @return [ActiveRecord::Association] AR association of posts + def posts + # NOTE(this should be something like Post.all_for_stream(@user, aspect_ids, {}) that calls visible_posts + @posts ||= user.visible_posts(:all_aspects? => for_all_aspects?, + :by_members_of => aspect_ids, + :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 + def people + @people ||= Person.all_from_aspects(aspect_ids, user).includes(:profile) + end + + # @return [String] URL + def link(opts={}) + Rails.application.routes.url_helpers.aspects_path(opts) + end + + # The first aspect in #aspects, given the stream is not for all aspects, or #aspects size is 1 + # @note aspects.first is used for mobile. NOTE(this is a hack and should be fixed) + # @return [Aspect,Symbol] + def aspect + if !for_all_aspects? || aspects.size == 1 + aspects.first + end + end + + # Only ajax in the stream if all aspects are present. + # In this case, we know we're on the first page of the stream, + # as the default view for aspects/index is showing posts from + # all a user's aspects. + # + # @return [Boolean] see #for_all_aspects? + def ajax_stream? + for_all_aspects? + end + + # The title that will display at the top of the stream's + # publisher box. + # + # @return [String] + def title + if self.for_all_aspects? + I18n.t('aspects.aspect_stream.stream') + else + self.aspects.to_sentence + end + end + + # Determine whether or not the stream is displaying across + # all of the user's aspects. + # + # @return [Boolean] + def for_all_aspects? + @all_aspects ||= aspect_ids.length == user.aspects.size + end + + # Provides a translated title for contacts box on the right pane. + # + # @return [String] + def contacts_title + if self.for_all_aspects? || self.aspect_ids.size > 1 + I18n.t('_contacts') + else + "#{self.aspect.name} (#{self.people.size})" + end + end + + # Provides a link to the user to the contacts page that corresponds with + # the stream's active aspects. + # + # @return [String] Link to contacts + def contacts_link + if for_all_aspects? || aspect_ids.size > 1 + Rails.application.routes.url_helpers.contacts_path + else + Rails.application.routes.url_helpers.contacts_path(:a_id => aspect.id) + end + end +end diff --git a/lib/stream/base.rb b/lib/stream/base.rb new file mode 100644 index 000000000..b19d05fc3 --- /dev/null +++ b/lib/stream/base.rb @@ -0,0 +1,92 @@ +class Stream::Base + TYPES_OF_POST_IN_STREAM = ['StatusMessage', 'Reshare', 'ActivityStreams::Photo'] + attr_accessor :max_time, :order, :user + + def initialize(user, opts={}) + self.user = user + self.max_time = opts[:max_time] + self.order = opts[:order] + end + + def random_featured_user + @random_featured_user ||= Person.find_by_diaspora_handle(featured_diaspora_id) + end + + def has_featured_users? + random_featured_user.present? + end + + #requied to implement said stream + def link(opts={}) + 'change me in lib/base_stream.rb!' + end + + def can_comment?(post) + true + end + + def title + 'a title' + end + + def posts + [] + 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) + end + + def contacts_link_title + I18n.translate('aspects.selected_contacts.view_all_contacts') + end + + def contacts_title + 'change me in lib/base_stream.rb!' + end + + def contacts_link + '#' + end + + #helpers + def ajax_stream? + false + end + + def for_all_aspects? + true + end + + + #NOTE: MBS bad bad methods the fact we need these means our views are foobared. please kill them and make them + #private methods on the streams that need them + def aspects + @user.aspects + end + + def aspect + aspects.first + end + + def aspect_ids + aspects.map{|x| x.id} + end + + def max_time=(time_string) + @max_time = Time.at(time_string.to_i) unless time_string.blank? + @max_time ||= (Time.now + 1) + end + + def order=(order_string) + @order = order_string + @order ||= 'created_at' + end + + private + def featured_diaspora_id + @featured_diaspora_id ||= AppConfig[:featured_users].try(:sample, 1) + end +end diff --git a/lib/stream/featured_users.rb b/lib/stream/featured_users.rb new file mode 100644 index 000000000..d4e1a7447 --- /dev/null +++ b/lib/stream/featured_users.rb @@ -0,0 +1,29 @@ +class Stream::FeaturedUsers < Stream::Base + def title + "Featured users doing cool stuff!" + end + + def link(opts={}) + Rails.application.routes.url_helpers.featured_path(opts) + end + + def contacts_title + "This week's featured users" + end + + def contacts_link + Rails.application.routes.url_helpers.featured_users_path + end + + def contacts_link_title + I18n.translate('aspects.selected_contacts.view_all_featured_users') + end + + def posts + Post.all_public.where(:author_id => people.map{|x| x.id}).for_a_stream(max_time, order) + end + + def people + Person.featured_users + end +end diff --git a/lib/stream/followed_tag.rb b/lib/stream/followed_tag.rb new file mode 100644 index 000000000..5b7a5f4ab --- /dev/null +++ b/lib/stream/followed_tag.rb @@ -0,0 +1,49 @@ +# Copyright (c) 2010-2011, Diaspora Inc. This file is +# licensed under the Affero General Public License version 3 or later. See +# the COPYRIGHT file. + +class Stream::FollowedTag < Stream::Base + + def link(opts={}) + Rails.application.routes.url_helpers.tag_followings_path(opts) + end + + def title + I18n.t('aspects.index.tags_following') + end + + # @return [ActiveRecord::Association] AR association of posts + def posts + return [] if tag_string.empty? + @posts ||= StatusMessage.tag_stream(user, tag_array, max_time, order) + end + + def contacts_title + I18n.translate('streams.tags.contacts_title') + end + + def can_comment?(post) + @can_comment_cache ||= {} + @can_comment_cache[post.id] ||= contacts_in_stream.find{|contact| contact.person_id == post.author.id}.present? + @can_comment_cache[post.id] ||= user.person.id == post.author.id + @can_comment_cache[post.id] + end + + def contacts_in_stream + @contacts_in_stream ||= Contact.where(:user_id => user.id, :person_id => people.map{|x| x.id}).all + end + + private + + def tag_string + @tag_string ||= tags.join(', '){|tag| tag.name}.to_s + end + + def tag_array + tags.map{|x| x.name} + end + + def tags + @tags = user.followed_tags + end +end diff --git a/lib/stream/mention.rb b/lib/stream/mention.rb new file mode 100644 index 000000000..5bdb81079 --- /dev/null +++ b/lib/stream/mention.rb @@ -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. + +class Stream::Mention < Stream::Base + def link(opts={}) + Rails.application.routes.url_helpers.mentions_path(opts) + end + + def title + I18n.translate("streams.mentions.title") + end + + + # @return [ActiveRecord::Association] AR association of posts + def posts + @posts ||= StatusMessage.where_person_is_mentioned(self.user.person).for_a_stream(max_time, order) + end + + def contacts_title + I18n.translate('streams.mentions.contacts_title') + end +end diff --git a/lib/stream/public.rb b/lib/stream/public.rb new file mode 100644 index 000000000..a9056795e --- /dev/null +++ b/lib/stream/public.rb @@ -0,0 +1,27 @@ +# Copyright (c) 2010-2011, Diaspora Inc. This file is +# licensed under the Affero General Public License version 3 or later. See +# the COPYRIGHT file. + +class Stream::Public < Stream::Base + def link(opts={}) + Rails.application.routes.url_helpers.public_stream_path(opts) + end + + def title + I18n.translate("streams.public.title") + end + + # @return [ActiveRecord::Association] AR association of posts + def posts + @posts ||= Post.all_public.for_a_stream(max_time, order) + end + + + def contacts_title + I18n.translate("streams.public.contacts_title") + end + + def can_comment?(post) + post.author.local? + end +end diff --git a/lib/stream/soup.rb b/lib/stream/soup.rb new file mode 100644 index 000000000..b227d4727 --- /dev/null +++ b/lib/stream/soup.rb @@ -0,0 +1,50 @@ +class Stream::Soup < Stream::Base + def link(opts) + Rails.application.routes.url_helpers.soup_path + end + + def title + I18n.t('streams.soup.title') + end + + def contacts_title + I18n.t('streams.soup.contacts_title') + end + + def posts + post_ids = aspect_posts_ids + followed_tag_ids + mentioned_post_ids + post_ids += featured_user_post_ids + Post.where(:id => post_ids).for_a_stream(max_time, order) + end + + private + + def aspect_posts_ids + user.visible_post_ids(:limit => 15, :order => order, :max_time => max_time) + end + + def followed_tag_ids + StatusMessage.tag_stream(user, tag_array, max_time, order).map{|x| x.id} + end + + def mentioned_post_ids + ids(StatusMessage.where_person_is_mentioned(user.person).for_a_stream(max_time, order)) + end + + def featured_user_post_ids + ids(Post.all_public.where(:author_id => featured_user_ids).for_a_stream(max_time, order)) + end + + #worthless helpers + def featured_user_ids + ids(Person.featured_users) + end + + def tag_array + user.followed_tags.map{|x| x.name} + end + + def ids(enumerable) + enumerable.map{|x| x.id} + end +end diff --git a/spec/lib/stream/aspect_spec.rb b/spec/lib/stream/aspect_spec.rb new file mode 100644 index 000000000..c5f920430 --- /dev/null +++ b/spec/lib/stream/aspect_spec.rb @@ -0,0 +1,155 @@ +# Copyright (c) 2010-2011, Diaspora Inc. This file is +# licensed under the Affero General Public License version 3 or later. See +# the COPYRIGHT file. + +require 'spec_helper' + +describe Stream::Aspect do + describe '#aspects' do + it 'queries the user given initialized aspect ids' do + alice = stub.as_null_object + stream = Stream::Aspect.new(alice, [1,2,3]) + + alice.aspects.should_receive(:where) + stream.aspects + end + + it "returns all the user's aspects if no aspect ids are specified" do + alice = stub.as_null_object + stream = Stream::Aspect.new(alice, []) + + alice.aspects.should_not_receive(:where) + stream.aspects + end + + it 'filters aspects given a user' do + alice = stub(:aspects => [stub(:id => 1)]) + alice.aspects.stub(:where).and_return(alice.aspects) + stream = Stream::Aspect.new(alice, [1,2,3]) + + stream.aspects.should == alice.aspects + end + end + + describe '#aspect_ids' do + it 'maps ids from aspects' do + alice = stub.as_null_object + aspects = stub.as_null_object + + stream = Stream::Aspect.new(alice, [1,2]) + + stream.should_receive(:aspects).and_return(aspects) + aspects.should_receive(:map) + stream.aspect_ids + end + end + + describe '#posts' do + before do + @alice = stub.as_null_object + end + + it 'calls visible posts for the given user' do + stream = Stream::Aspect.new(@alice, [1,2]) + + @alice.should_receive(:visible_posts).and_return(stub.as_null_object) + stream.posts + end + + it 'is called with 3 types' do + stream = Stream::Aspect.new(@alice, [1,2], :order => 'created_at') + @alice.should_receive(:visible_posts).with(hash_including(:type=> ['StatusMessage', 'Reshare', 'ActivityStreams::Photo'])).and_return(stub.as_null_object) + stream.posts + end + + it 'respects ordering' do + stream = Stream::Aspect.new(@alice, [1,2], :order => 'created_at') + @alice.should_receive(:visible_posts).with(hash_including(:order => 'created_at DESC')).and_return(stub.as_null_object) + stream.posts + end + + it 'respects max_time' do + stream = Stream::Aspect.new(@alice, [1,2], :max_time => 123) + @alice.should_receive(:visible_posts).with(hash_including(:max_time => instance_of(Time))).and_return(stub.as_null_object) + stream.posts + end + + it 'passes for_all_aspects to visible posts' do + stream = Stream::Aspect.new(@alice, [1,2], :max_time => 123) + all_aspects = mock + stream.stub(:for_all_aspects?).and_return(all_aspects) + @alice.should_receive(:visible_posts).with(hash_including(:all_aspects? => all_aspects)).and_return(stub.as_null_object) + stream.posts + end + end + + describe '#people' do + it 'should call Person.all_from_aspects' do + class Person ; end + + alice = stub.as_null_object + aspect_ids = [1,2,3] + stream = Stream::Aspect.new(alice, []) + + stream.stub(:aspect_ids).and_return(aspect_ids) + Person.should_receive(:all_from_aspects).with(stream.aspect_ids, alice).and_return(stub(:includes => :profile)) + stream.people + end + end + + describe '#aspect' do + before do + alice = stub.as_null_object + @stream = Stream::Aspect.new(alice, [1,2]) + end + + it "returns an aspect if the stream is not for all the user's aspects" do + @stream.stub(:for_all_aspects?).and_return(false) + @stream.aspect.should_not be_nil + end + + it "returns nothing if the stream is not for all the user's aspects" do + @stream.stub(:for_all_aspects?).and_return(true) + @stream.aspect.should be_nil + end + end + + describe 'for_all_aspects?' do + before do + alice = stub.as_null_object + alice.aspects.stub(:size).and_return(2) + @stream = Stream::Aspect.new(alice, [1,2]) + end + + it "is true if the count of aspect_ids is equal to the size of the user's aspect count" do + @stream.aspect_ids.stub(:length).and_return(2) + @stream.should be_for_all_aspects + end + + it "is false if the count of aspect_ids is not equal to the size of the user's aspect count" do + @stream.aspect_ids.stub(:length).and_return(1) + @stream.should_not be_for_all_aspects + end + end + + describe '.ajax_stream?' do + before do + @stream = Stream::Aspect.new(stub, stub) + end + it 'is true stream is for all aspects?' do + @stream.stub(:for_all_aspects?).and_return(true) + @stream.ajax_stream?.should be_true + end + + it 'is false if it is not for all aspects' do + @stream.stub(:for_all_aspects?).and_return(false) + @stream.ajax_stream?.should be_false + end + end + describe 'shared behaviors' do + before do + @stream = Stream::Aspect.new(alice, alice.aspects.map(&:id)) + end + it_should_behave_like 'it is a stream' + end +end diff --git a/spec/lib/stream/base_spec.rb b/spec/lib/stream/base_spec.rb new file mode 100644 index 000000000..f1f6f60ad --- /dev/null +++ b/spec/lib/stream/base_spec.rb @@ -0,0 +1,11 @@ +require 'spec_helper' +require File.join(Rails.root, 'spec', 'shared_behaviors', 'stream') +describe Stream::Base do + before do + @stream = Stream::Base.new(stub) + end + + describe 'shared behaviors' do + it_should_behave_like 'it is a stream' + end +end diff --git a/spec/lib/stream/featured_spec.rb b/spec/lib/stream/featured_spec.rb new file mode 100644 index 000000000..53eb2353c --- /dev/null +++ b/spec/lib/stream/featured_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' +require File.join(Rails.root, 'spec', 'shared_behaviors', 'stream') + +describe Stream::FeaturedUsers do + before do + @stream = Stream::FeaturedUsers.new(Factory(:user), :max_time => Time.now, :order => 'updated_at') + end + + describe 'shared behaviors' do + it_should_behave_like 'it is a stream' + end +end diff --git a/spec/lib/stream/followed_tag_spec.rb b/spec/lib/stream/followed_tag_spec.rb new file mode 100644 index 000000000..2bc19a548 --- /dev/null +++ b/spec/lib/stream/followed_tag_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' +require File.join(Rails.root, 'spec', 'shared_behaviors', 'stream') + +describe Stream::FollowedTag do + before do + @stream = Stream::FollowedTag.new(Factory(:user), :max_time => Time.now, :order => 'updated_at') + @stream.stub(:tag_string).and_return("foo") + end + + describe 'shared behaviors' do + it_should_behave_like 'it is a stream' + end + + describe '.can_comment?' do + before do + @stream = Stream::FollowedTag.new(alice) + @stream.stub(:people).and_return([bob.person]) + end + + it 'returns true if user is a contact of the post author' do + post = Factory(:status_message, :author => bob.person) + @stream.can_comment?(post).should be_true + end + + it 'returns true if a user is the author of the post' do + post = Factory(:status_message, :author => alice.person) + @stream.can_comment?(post).should be_true + end + + it 'returns false otherwise' do + post = Factory(:status_message, :author => eve.person) + @stream.can_comment?(post).should be_false + end + end +end diff --git a/spec/lib/stream/mention_spec.rb b/spec/lib/stream/mention_spec.rb new file mode 100644 index 000000000..f27ff8e4d --- /dev/null +++ b/spec/lib/stream/mention_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' +require File.join(Rails.root, 'spec', 'shared_behaviors', 'stream') + +describe Stream::Mention do + before do + @stream = Stream::Mention.new(Factory(:user), :max_time => Time.now, :order => 'updated_at') + end + + describe 'shared behaviors' do + it_should_behave_like 'it is a stream' + end +end diff --git a/spec/lib/stream/public_spec.rb b/spec/lib/stream/public_spec.rb new file mode 100644 index 000000000..0a9db92eb --- /dev/null +++ b/spec/lib/stream/public_spec.rb @@ -0,0 +1,11 @@ +require 'spec_helper' +require File.join(Rails.root, 'spec', 'shared_behaviors', 'stream') +describe Stream::Public do + before do + @stream = Stream::Public.new(stub) + end + + describe 'shared behaviors' do + it_should_behave_like 'it is a stream' + end +end diff --git a/spec/lib/stream/soups_spec.rb b/spec/lib/stream/soups_spec.rb new file mode 100644 index 000000000..ad92f9110 --- /dev/null +++ b/spec/lib/stream/soups_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' +require File.join(Rails.root, 'spec', 'shared_behaviors', 'stream') + +describe Stream::Soup do + before do + @stream = Stream::Soup.new(Factory(:user), :max_time => Time.now, :order => 'updated_at') + end + + describe 'shared behaviors' do + it_should_behave_like 'it is a stream' + end +end