diff --git a/README.md b/README.md index 8a629c41f..65d2c9bb3 100644 --- a/README.md +++ b/README.md @@ -18,27 +18,32 @@ With Diaspora you can: Documentation is available on our [wiki](https://github.com/diaspora/diaspora/wiki) +[Pull Request Guidelines](https://github.com/diaspora/diaspora/wiki/Pull-Request-Guidelines) + +Before submitting code, feel free to sign our [Contributor License Agreement](https://github.com/diaspora/diaspora/wiki/New-CLA--12-13-10) [Sign Here](https://spreadsheets.google.com/a/joindiaspora.com/spreadsheet/viewform?formkey=dFdRTnY0TGtfaklKQXZNUndsMlJ2eGc6MQ) + ## Quick Start: Here's how you can get a development environment up and running. You can check out system-specific guides [here](https://github.com/diaspora/diaspora/wiki/Installation-Guides). ### Step 1: Download the script -```wget https://raw.github.com/diaspora/diaspora/ec5289bd3b9b5608d339b28e1e30272f380a9211/script/install.sh +``` + curl https://raw.github.com/diaspora/diaspora/master/script/install.sh | /bin/sh ``` -### Step 2: Set permissions and run -```chmod +x install.sh && install.sh +### Step 2: Follow the instructions + + +### Step 3: Run the the development server +``` + rails s ``` -### Step 3: Follow the instructions +then visit 'http://localhost:3000' in your browser. - -### Step 4: Run the test server -```rails s +### Step 4: Run tests ``` - -### Step 5: Run tests -```rake tests + rake ``` ## Resources: diff --git a/app/assets/javascripts/app/helpers/text_formatter.js b/app/assets/javascripts/app/helpers/text_formatter.js index fe4069911..0759a6665 100644 --- a/app/assets/javascripts/app/helpers/text_formatter.js +++ b/app/assets/javascripts/app/helpers/text_formatter.js @@ -29,6 +29,8 @@ text = text.replace(linkRegex, function() { var unicodeUrl = arguments[3]; var addr = parse_url(unicodeUrl); + if( !addr.host ) addr.host = ""; // must not be 'undefined' + var asciiUrl = // rebuild the url (!addr.scheme ? '' : addr.scheme + ( (addr.scheme.toLowerCase()=="mailto") ? ':' : '://')) + diff --git a/app/assets/javascripts/app/router.js b/app/assets/javascripts/app/router.js index 064cdd523..23a6dfbf9 100644 --- a/app/assets/javascripts/app/router.js +++ b/app/assets/javascripts/app/router.js @@ -39,6 +39,8 @@ app.Router = Backbone.Router.extend({ photos : function() { app.photos = new app.models.Stream([], {collection: app.collections.Photos}); app.page = new app.views.Photos({model : app.photos}); + + $("#main_stream").html(app.page.render().el); }, diff --git a/app/assets/javascripts/app/views.js b/app/assets/javascripts/app/views.js index 0e6f2aaf0..fc7329390 100644 --- a/app/assets/javascripts/app/views.js +++ b/app/assets/javascripts/app/views.js @@ -21,7 +21,8 @@ app.views.Base = Backbone.View.extend({ }, defaultPresenter : function(){ - var modelJson = this.model ? _.clone(this.model.attributes) : {} + var modelJson = this.model && this.model.attributes ? _.clone(this.model.attributes) : {} + return _.extend(modelJson, { current_user : app.currentUser.attributes, loggedIn : app.currentUser.authenticated() @@ -71,3 +72,66 @@ app.views.Base = Backbone.View.extend({ $(".tooltip").remove(); } }); + +// Mixin to render a collection that fetches more via infinite scroll, for a view that has no template. +// Requires: +// a stream model, bound as this.stream +// a stream's posts, bound as this.collection +// a postClass to be declared +// a #paginate div in the layout +// a call to setupInfiniteScroll + +app.views.infiniteScrollMixin = { + setupInfiniteScroll : function() { + this.postViews = this.postViews || [] + + this.bind("loadMore", this.fetchAndshowLoader, this) + this.stream.bind("fetched", this.hideLoader, this) + this.stream.bind("allPostsLoaded", this.unbindInfScroll, this) + this.collection.bind("add", this.addPost, this); + + var throttledScroll = _.throttle(_.bind(this.infScroll, this), 200); + $(window).scroll(throttledScroll); + }, + + renderTemplate : function() { + if(this.stream.isFetching()) { this.showLoader() } + }, + + addPost : function(post) { + var postView = new this.postClass({ model: post }) + , placeInStream = (this.collection.at(0).id == post.id) ? "prepend" : "append"; + + this.$el[placeInStream](postView.render().el); + this.postViews.push(postView) + }, + + unbindInfScroll : function() { + $(window).unbind("scroll"); + }, + + fetchAndshowLoader : function(){ + if(this.stream.isFetching()) { return false } + this.stream.fetch() + this.showLoader() + }, + + showLoader: function(){ + $("#paginate .loader").removeClass("hidden") + }, + + hideLoader: function() { + $("#paginate .loader").addClass("hidden") + }, + + infScroll : function() { + var $window = $(window) + , distFromTop = $window.height() + $window.scrollTop() + , distFromBottom = $(document).height() - distFromTop + , bufferPx = 500; + + if(distFromBottom < bufferPx) { + this.trigger("loadMore") + } + } +}; diff --git a/app/assets/javascripts/app/views/photos_view.js b/app/assets/javascripts/app/views/photos_view.js index d20b1e224..dff7052d3 100644 --- a/app/assets/javascripts/app/views/photos_view.js +++ b/app/assets/javascripts/app/views/photos_view.js @@ -1,52 +1,16 @@ -app.views.Photos = Backbone.View.extend({ - - events : {}, - +app.views.Photos = Backbone.View.extend(_.extend({ initialize : function(options) { - this.photos = this.model; - this.collection = this.model.items; + this.stream = this.model; + this.collection = this.stream.items; - this.setupEvents(); - this.setupLightbox(); + // viable for extraction + this.stream.fetch(); + + this.setupLightbox() + this.setupInfiniteScroll() }, - setupEvents : function(){ - this.photos.bind("fetched", this.removeLoader, this) - this.collection.bind("add", this.addPhoto, this); - }, - - addPhoto : function(photo) { - var photoView = new app.views.Photo({ model: photo }); - - $(this.el)[ - (this.collection.at(0).id == photo.id) - ? "prepend" - : "append" - ](photoView.render().el); - - return this; - }, - - render : function(evt) { - if(evt) {evt.preventDefault(); } - - if(this.model.fetch()) { - this.appendLoader(); - }; - - return this; - }, - - appendLoader: function(){ - $("#paginate").html($("", { - src : "/assets/static-loader.png", - "class" : "loader" - })); - }, - - removeLoader: function() { - $("#paginate").empty(); - }, + postClass : app.views.Photo, setupLightbox : function(){ this.lightbox = Diaspora.BaseWidget.instantiate("Lightbox"); @@ -55,6 +19,5 @@ app.views.Photos = Backbone.View.extend({ imageSelector: 'img.photo' }); $(this.el).delegate("a.photo-link", "click", this.lightbox.lightboxImageClicked); - }, - -}); + } +}, app.views.infiniteScrollMixin)); diff --git a/app/assets/javascripts/app/views/stream_view.js b/app/assets/javascripts/app/views/stream_view.js index bf4de92f3..5df7574ca 100644 --- a/app/assets/javascripts/app/views/stream_view.js +++ b/app/assets/javascripts/app/views/stream_view.js @@ -1,77 +1,27 @@ -app.views.Stream = Backbone.View.extend({ +app.views.Stream = Backbone.View.extend(_.extend({ initialize: function(options) { this.stream = this.model - this.collection = this.model.items + this.collection = this.stream.items - this.setupEvents() - this.setupInfiniteScroll() - this.setupLightbox() this.postViews = [] + + this.setupNSFW() + this.setupLightbox() + this.setupInfiniteScroll() }, - setupEvents : function(){ - this.stream.bind("fetched", this.removeLoader, this) - this.stream.bind("allItemsLoaded", this.unbindInfScroll, this) - this.collection.bind("add", this.addPost, this); - - app.currentUser.bind("nsfwChanged", reRenderPostViews, this) - function reRenderPostViews() { - _.map(this.postViews, function(view){ view.render() }) - } - }, - - addPost : function(post) { - var postView = new app.views.StreamPost({ model: post }) - , placeInStream = (this.collection.at(0).id == post.id) ? "prepend" : "append"; - - this.$el[placeInStream](postView.render().el); - this.postViews.push(postView) - }, - - unbindInfScroll : function() { - $(window).unbind("scroll"); - }, - - render : function() { - if(this.stream.isFetching()) { this.appendLoader() } - return this; - }, - - fetchAndAppendLoader : function(){ - if(this.stream.isFetching()) { return false } - this.stream.fetch() - this.appendLoader() - }, - - appendLoader: function(){ - $("#paginate").html($("", { - src : "/assets/static-loader.png", - "class" : "loader" - })); - }, - - removeLoader: function() { - $("#paginate").empty(); - }, + postClass : app.views.StreamPost, setupLightbox : function(){ this.lightbox = Diaspora.BaseWidget.instantiate("Lightbox"); this.$el.delegate("a.stream-photo-link", "click", this.lightbox.lightboxImageClicked); }, - setupInfiniteScroll : function() { - var throttledScroll = _.throttle(_.bind(this.infScroll, this), 200); - $(window).scroll(throttledScroll); - }, + setupNSFW : function(){ + app.currentUser.bind("nsfwChanged", reRenderPostViews, this) - infScroll : function() { - var $window = $(window) - , distFromTop = $window.height() + $window.scrollTop() - , distFromBottom = $(document).height() - distFromTop - , bufferPx = 500; - - if(distFromBottom < bufferPx) { - this.fetchAndAppendLoader() + function reRenderPostViews() { + _.map(this.postViews, function(view){ view.render() }) } } -}); +}, app.views.infiniteScrollMixin)); diff --git a/app/assets/stylesheets/application.css.sass b/app/assets/stylesheets/application.css.sass index f224ebb7a..47158db90 100644 --- a/app/assets/stylesheets/application.css.sass +++ b/app/assets/stylesheets/application.css.sass @@ -608,6 +608,8 @@ form.new_comment img :border none + :min-height 20px + #photo_container :text :align center diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 5dee5bb46..60d0ec9f3 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -7,7 +7,7 @@ require Rails.root.join("app", "presenters", "post_presenter") class PostsController < ApplicationController include PostsHelper - before_filter :authenticate_user!, :except => [:show, :iframe] + before_filter :authenticate_user!, :except => [:show, :iframe, :oembed] before_filter :set_format_if_malformed_from_status_net, :only => :show layout 'post' @@ -24,13 +24,7 @@ class PostsController < ApplicationController end def show - key = params[:id].to_s.length <= 8 ? :id : :guid - - if user_signed_in? - @post = current_user.find_visible_shareable_by_id(Post, params[:id], :key => key) - else - @post = Post.where(key => params[:id], :public => true).includes(:author, :comments => :author).first - end + @post = find_by_guid_or_id_with_current_user(params[:id]) if @post # @commenting_disabled = can_not_comment_on_post? @@ -59,6 +53,17 @@ class PostsController < ApplicationController render :text => post_iframe_url(params[:id]), :layout => false end + def oembed + post_id = OEmbedPresenter.id_from_url(params.delete(:url)) + post = find_by_guid_or_id_with_current_user(post_id) + if post.present? + oembed = OEmbedPresenter.new(post, params.slice(:format, :maxheight, :minheight)) + render :json => oembed + else + render :nothing => true, :status => 404 + end + end + def destroy @post = current_user.posts.where(:id => params[:id]).first if @post @@ -76,6 +81,16 @@ class PostsController < ApplicationController private + def find_by_guid_or_id_with_current_user(id) + key = id.to_s.length <= 8 ? :id : :guid + if user_signed_in? + current_user.find_visible_shareable_by_id(Post, id, :key => key) + else + Post.where(key => id, :public => true).includes(:author, :comments => :author).first + end + + end + def set_format_if_malformed_from_status_net request.format = :html if request.format == 'application/html+xml' end diff --git a/app/helpers/posts_helper.rb b/app/helpers/posts_helper.rb index 35e981a1c..e12ed5901 100644 --- a/app/helpers/posts_helper.rb +++ b/app/helpers/posts_helper.rb @@ -20,6 +20,7 @@ module PostsHelper def post_iframe_url(post_id, opts={}) opts[:width] ||= 516 opts[:height] ||= 315 - "".html_safe + host = AppConfig[:pod_uri].port ==80 ? AppConfig[:pod_uri].host : "#{AppConfig[:pod_uri].host}:#{AppConfig[:pod_uri].port}" + "".html_safe end end diff --git a/app/presenters/o_embed_presenter.rb b/app/presenters/o_embed_presenter.rb new file mode 100644 index 000000000..0723b7df9 --- /dev/null +++ b/app/presenters/o_embed_presenter.rb @@ -0,0 +1,49 @@ +require 'uri' +class OEmbedPresenter + include PostsHelper + include ActionView::Helpers::TextHelper + + def initialize(post, opts = {}) + @post = post + @opts = opts + end + + def to_json(opts={}) + as_json(opts).to_json + end + + def as_json(opts={}) + { + :provider_name => "Diaspora", + :provider_url => AppConfig[:pod_url], + :type => 'rich', + :version => '1.0', + :title => post_title, + :author_name => post_author, + :author_url => post_author_url, + :width => @opts.fetch(:maxwidth, 516), + :height => @opts.fetch(:maxheight, 320), + :html => iframe_html + } + end + + def self.id_from_url(url) + URI.parse(url).path.gsub(%r{\/posts\/|\/p\/}, '') + end + + def post_title + post_page_title(@post) + end + + def post_author + @post.author.name + end + + def post_author_url + Rails.application.routes.url_helpers.person_url(@post.author, :host => AppConfig[:pod_uri].host) + end + + def iframe_html + post_iframe_url(@post.id, :height => @opts[:maxheight], :width => @opts[:maxwidth]) + end +end \ No newline at end of file diff --git a/app/views/aspects/_aspect_stream.haml b/app/views/aspects/_aspect_stream.haml index 79771a15b..91cbbbe81 100644 --- a/app/views/aspects/_aspect_stream.haml +++ b/app/views/aspects/_aspect_stream.haml @@ -13,6 +13,7 @@ #main_stream.stream #paginate + = image_tag "static-loader.png", :height => 14, :width => 14, :class => "loader hidden" - if current_user.contacts.size < 2 = render 'aspects/no_contacts_message' \ No newline at end of file diff --git a/app/views/layouts/post.html.haml b/app/views/layouts/post.html.haml index 75ae0cb19..98f941219 100644 --- a/app/views/layouts/post.html.haml +++ b/app/views/layouts/post.html.haml @@ -44,6 +44,8 @@ = set_asset_host = translation_missing_warnings = current_user_atom_tag + - if @post.present? + %link{:rel => 'alternate', :type => "application/json+oembed", :href => "#{oembed_url(:url => post_url(@post))}"} = yield(:head) = csrf_meta_tag diff --git a/app/views/people/show.html.haml b/app/views/people/show.html.haml index 54ae49a59..4448b1d22 100644 --- a/app/views/people/show.html.haml +++ b/app/views/people/show.html.haml @@ -34,3 +34,4 @@ = t('.ignoring', :name => @person.first_name) #paginate + = image_tag "static-loader.png", :height => 14, :width => 14, :class => "loader hidden" diff --git a/config/environments/test.rb b/config/environments/test.rb index 0a01b0369..1aaa3b27a 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -47,4 +47,6 @@ Diaspora::Application.configure do # This is necessary if your schema can't be completely dumped by the schema dumper, # like if you have constraints or database-specific column types # config.active_record.schema_format = :sql + + config.default_url_options = { :host => AppConfig[:pod_uri].host} end diff --git a/config/initializers/silence_assets.rb b/config/initializers/silence_assets.rb new file mode 100644 index 000000000..2bae826fd --- /dev/null +++ b/config/initializers/silence_assets.rb @@ -0,0 +1,16 @@ +if Rails.env.development? + + Rails.application.assets.logger = Logger.new('/dev/null') + + Rails::Rack::Logger.class_eval do + def call_with_quiet_assets(env) + previous_level = Rails.logger.level + Rails.logger.level = Logger::ERROR if env['PATH_INFO'] =~ %r{^/assets/} + call_without_quiet_assets(env) + ensure + Rails.logger.level = previous_level + end + alias_method_chain :call, :quiet_assets + end + +end \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index d556997cb..daee08957 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -5,6 +5,7 @@ Diaspora::Application.routes.draw do mount RailsAdmin::Engine => '/admin_panel', :as => 'rails_admin' + get 'oembed' => 'posts#oembed', :as => 'oembed' # Posting and Reading resources :reshares diff --git a/spec/controllers/posts_controller_spec.rb b/spec/controllers/posts_controller_spec.rb index 2a53a661b..221beaca8 100644 --- a/spec/controllers/posts_controller_spec.rb +++ b/spec/controllers/posts_controller_spec.rb @@ -120,6 +120,19 @@ describe PostsController do end end + describe 'oembed' do + it 'works when you can see it' do + sign_in alice + get :oembed, :url => "/posts/#{@message.id}" + response.body.should match /iframe/ + end + + it 'returns a 404 response when the post is not found' do + get :oembed, :url => "/posts/#{@message.id}" + response.should_not be_success + end + end + describe '#destroy' do before do sign_in alice diff --git a/spec/javascripts/app/views/photos_view_spec.js b/spec/javascripts/app/views/photos_view_spec.js index 51cedb9c7..5f4a8c9d2 100644 --- a/spec/javascripts/app/views/photos_view_spec.js +++ b/spec/javascripts/app/views/photos_view_spec.js @@ -2,27 +2,31 @@ describe("app.views.Photos", function() { beforeEach(function() { loginAs({name: "alice", avatar : {small : "http://avatar.com/photo.jpg"}}); - this._photos = $.parseJSON(spec.readFixture("photos_json"))["photos"]; + this.photos = $.parseJSON(spec.readFixture("photos_json"))["photos"]; - this.photos = new app.models.Stream([], {collection: app.collections.Photos}); - this.photos.add(this._photos); + this.stream = new app.models.Stream([], {collection: app.collections.Photos}); + this.stream.add(this.photos); - this.view = new app.views.Photos({model : this.photos}); + this.view = new app.views.Photos({model : this.stream}); // do this manually because we've moved loadMore into render?? this.view.render(); _.each(this.view.collection.models, function(photo) { - this.view.addPhoto(photo); + this.view.addPost(photo); }, this); }); describe("initialize", function() { - // nothing there yet + it("binds an infinite scroll listener", function() { + spyOn($.fn, "scroll"); + new app.views.Stream({model : this.stream}); + expect($.fn.scroll).toHaveBeenCalled(); + }); }); describe("#render", function() { beforeEach(function() { - this.photo = this.photos.items.models[0]; + this.photo = this.stream.items.models[0]; this.photoElement = $(this.view.$("#" + this.photo.get("guid"))); }); @@ -32,14 +36,4 @@ describe("app.views.Photos", function() { }); }); }); - - describe("removeLoader", function() { - it("emptys the pagination div when the stream is fetched", function() { - $("#jasmine_content").append($('
OMG
')); - expect($("#paginate").text()).toBe("OMG"); - this.view.photos.trigger("fetched"); - expect($("#paginate")).toBeEmpty(); - }); - }); - }); diff --git a/spec/javascripts/app/views/stream_view_spec.js b/spec/javascripts/app/views/stream_view_spec.js index 79f397b66..616c1d4a8 100644 --- a/spec/javascripts/app/views/stream_view_spec.js +++ b/spec/javascripts/app/views/stream_view_spec.js @@ -43,19 +43,17 @@ describe("app.views.Stream", function() { it("fetches moar when the user is at the bottom of the page", function() { spyOn($.fn, "height").andReturn(0); spyOn($.fn, "scrollTop").andReturn(100); - spyOn(this.view, "fetchAndAppendLoader"); + spyOn(this.view.model, "fetch"); this.view.infScroll(); - expect(this.view.fetchAndAppendLoader).toHaveBeenCalled(); - }); - }); - describe("removeLoader", function() { - it("emptys the pagination div when the stream is fetched", function() { - $("#jasmine_content").append($('
OMG
')); - expect($("#paginate").text()).toBe("OMG"); - this.view.stream.trigger("fetched"); - expect($("#paginate")).toBeEmpty(); + waitsFor(function(){ + return this.view.model.fetch.wasCalled + }, "the infinite scroll function didn't fetch the stream") + + runs(function(){ + expect(this.view.model.fetch).toHaveBeenCalled() + }) }); }); diff --git a/spec/presenters/o_embed_presenter_spec.rb b/spec/presenters/o_embed_presenter_spec.rb new file mode 100644 index 000000000..7182b6a1a --- /dev/null +++ b/spec/presenters/o_embed_presenter_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' +describe OEmbedPresenter do + before do + @oembed = OEmbedPresenter.new(Factory(:status_message)) + end + + it 'is a hash' do + @oembed.as_json.should be_a Hash + end + + context 'required options from oembed spec' do + it 'supports maxheight + maxwidth(required)' do + oembed = OEmbedPresenter.new(Factory(:status_message), :maxwidth => 200, :maxheight => 300).as_json + oembed[:width].should == 200 + oembed[:height].should == 300 + end + end + + describe '#iframe_html' do + it 'passes the height options to post_iframe_url' do + @oembed.should_receive(:post_iframe_url).with(instance_of(Fixnum), instance_of(Hash)) + @oembed.iframe_html + end + end + + describe '.id_from_url' do + it 'takes a long post url and gives you the id' do + OEmbedPresenter.id_from_url('http://localhost:400/posts/1').should == "1" + end + + it 'takes a short post url and gives you the id' do + OEmbedPresenter.id_from_url('http://localhost:400/p/1').should == "1" + end + end +end \ No newline at end of file diff --git a/vendor/assets/javascripts/jquery.mentionsInput.js b/vendor/assets/javascripts/jquery.mentionsInput.js index a61746ac6..274f24404 100644 --- a/vendor/assets/javascripts/jquery.mentionsInput.js +++ b/vendor/assets/javascripts/jquery.mentionsInput.js @@ -200,8 +200,10 @@ } function onInputBoxKeyPress(e) { - var typedValue = String.fromCharCode(e.which || e.keyCode); - inputBuffer.push(typedValue); + if(e.keyCode != KEY.BACKSPACE) { + var typedValue = String.fromCharCode(e.which || e.keyCode); + inputBuffer.push(typedValue); + } } function onInputBoxKeyDown(e) {