Merge branch 'master' into stream-for-photos

Conflicts:
	app/assets/javascripts/app/views/stream_view.js
This commit is contained in:
danielgrippi 2012-04-18 18:30:54 -07:00
commit e379a6df0f
21 changed files with 277 additions and 159 deletions

View file

@ -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:

View file

@ -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") ? ':' : '://')) +

View file

@ -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);
},

View file

@ -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")
}
}
};

View file

@ -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($("<img>", {
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));

View file

@ -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($("<img>", {
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));

View file

@ -608,6 +608,8 @@ form.new_comment
img
:border none
:min-height 20px
#photo_container
:text
:align center

View file

@ -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

View file

@ -20,6 +20,7 @@ module PostsHelper
def post_iframe_url(post_id, opts={})
opts[:width] ||= 516
opts[:height] ||= 315
"<iframe src='#{post_url(post_id)}' width='#{opts[:width]}px' height='#{opts[:height]}px' frameBorder='0'></iframe>".html_safe
host = AppConfig[:pod_uri].port ==80 ? AppConfig[:pod_uri].host : "#{AppConfig[:pod_uri].host}:#{AppConfig[:pod_uri].port}"
"<iframe src='#{Rails.application.routes.url_helpers.post_url(post_id, :host => host)}' width='#{opts[:width]}px' height='#{opts[:height]}px' frameBorder='0'></iframe>".html_safe
end
end

View file

@ -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

View file

@ -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'

View file

@ -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

View file

@ -34,3 +34,4 @@
= t('.ignoring', :name => @person.first_name)
#paginate
= image_tag "static-loader.png", :height => 14, :width => 14, :class => "loader hidden"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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($('<div id="paginate">OMG</div>'));
expect($("#paginate").text()).toBe("OMG");
this.view.photos.trigger("fetched");
expect($("#paginate")).toBeEmpty();
});
});
});

View file

@ -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($('<div id="paginate">OMG</div>'));
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()
})
});
});

View file

@ -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

View file

@ -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) {