re-add hovercards
* added a presenter for hovercard json * added new backbone view for handling hovercard JS * refactoring of PeopleController * finished the backbone js version of hovercards * don't try to make people_controller more restfull, out of scope just add a new route and use that for hovercard json * added spec for people_controller#hovercard * add new exception for "AccountClosed" to be able to raise from anywhere * removed legacy code, since everything got ported to backbone (except the "cache" stuff, but that's not strictly necessary)
This commit is contained in:
parent
42a01f3a38
commit
0092acd492
13 changed files with 243 additions and 159 deletions
|
|
@ -20,6 +20,7 @@
|
|||
* Add progress percentage to upload images. [#3740](https://github.com/diaspora/diaspora/pull/3740)
|
||||
* Mark all unread post-related notifications as read, if one of this gets opened. [#3787](https://github.com/diaspora/diaspora/pull/3787)
|
||||
* Add flash-notice when sending messages to non-contacts. [#3723](https://github.com/diaspora/diaspora/pull/3723)
|
||||
* Re-add hovercards [#3802](https://github.com/diaspora/diaspora/pull/3802)
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ var app = {
|
|||
app.currentUser = app.user(window.current_user_attributes) || new app.models.User()
|
||||
|
||||
if(app.currentUser.authenticated()){
|
||||
app.header = new app.views.Header;
|
||||
app.header = new app.views.Header();
|
||||
$("header").prepend(app.header.el);
|
||||
app.header.render();
|
||||
}
|
||||
|
|
@ -52,7 +52,9 @@ var app = {
|
|||
|
||||
$(".stream_title").text(link.text())
|
||||
app.router.navigate(link.attr("href").substring(1) ,true)
|
||||
})
|
||||
});
|
||||
|
||||
app.hovercard = new app.views.Hovercard();
|
||||
},
|
||||
|
||||
hasPreload : function(prop) {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
Handlebars.registerHelper('t', function(scope, values) {
|
||||
return Diaspora.I18n.t(scope, values.hash)
|
||||
})
|
||||
});
|
||||
|
||||
Handlebars.registerHelper('imageUrl', function(path){
|
||||
return app.baseImageUrl() + path;
|
||||
})
|
||||
});
|
||||
|
||||
Handlebars.registerHelper('linkToPerson', function(context, block) {
|
||||
var html = "<a href=\"/people/" + context.guid + "\" class=\"author-name\">";
|
||||
|
|
@ -12,15 +12,29 @@ Handlebars.registerHelper('linkToPerson', function(context, block) {
|
|||
html+= "</a>";
|
||||
|
||||
return html
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
// allow hovercards for users that are not the current user.
|
||||
// returns the html class name used to trigger hovercards.
|
||||
Handlebars.registerHelper('hovercardable', function(person) {
|
||||
if( app.currentUser.get('guid') != person.guid ) {
|
||||
return 'hovercardable';
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
Handlebars.registerHelper('personImage', function(person, size, imageClass) {
|
||||
/* we return here if person.avatar is blank, because this happens when a
|
||||
* user is unauthenticated. we don't know why this happens... */
|
||||
if(typeof(person.avatar) == "undefined") { return }
|
||||
if( _.isUndefined(person.avatar) ) { return }
|
||||
|
||||
size = (typeof(size) != "string" ? "small" : size);
|
||||
imageClass = (typeof(imageClass) != "string" ? size : imageClass);
|
||||
size = ( !_.isString(size) ) ? "small" : size;
|
||||
imageClass = ( !_.isString(imageClass) ) ? size : imageClass;
|
||||
|
||||
return "<img src=\"" + person.avatar[size] +"\" class=\"avatar " + imageClass + "\" title=\"" + _.escape(person.name) +"\" />";
|
||||
})
|
||||
return _.template('<img src="<%= src %>" class="avatar <%= img_class %>" title="<%= title %>" />', {
|
||||
'src': person.avatar[size],
|
||||
'img_class': imageClass,
|
||||
'title': _.escape(person.name)
|
||||
});
|
||||
});
|
||||
|
|
|
|||
115
app/assets/javascripts/app/views/hovercard_view.js
Normal file
115
app/assets/javascripts/app/views/hovercard_view.js
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
|
||||
app.views.Hovercard = Backbone.View.extend({
|
||||
el: '#hovercard_container',
|
||||
|
||||
initialize: function() {
|
||||
$('.hovercardable')
|
||||
.live('mouseenter', _.bind(this._mouseenterHandler, this))
|
||||
.live('mouseleave', _.bind(this._mouseleaveHandler, this));
|
||||
|
||||
this.show_me = false;
|
||||
|
||||
this.avatar = this.$('.avatar');
|
||||
this.dropdown = this.$('.dropdown_list');
|
||||
this.dropdown_container = this.$('#hovercard_dropdown_container');
|
||||
this.hashtags = this.$('.hashtags');
|
||||
this.person_link = this.$('a.person');
|
||||
this.person_handle = this.$('p.handle');
|
||||
},
|
||||
|
||||
href: function() {
|
||||
return this.$el.parent().attr('href');
|
||||
},
|
||||
|
||||
_mouseenterHandler: function(event) {
|
||||
var el = $(event.target);
|
||||
if( !el.is('a') ) {
|
||||
el = el.parents('a');
|
||||
}
|
||||
|
||||
if( el.attr('href').indexOf('/people') == -1 ) {
|
||||
// can't fetch data from that URL, aborting
|
||||
return false;
|
||||
}
|
||||
|
||||
this.show_me = true;
|
||||
this.showHovercardOn(el);
|
||||
return false;
|
||||
},
|
||||
|
||||
_mouseleaveHandler: function(event) {
|
||||
this.show_me = false;
|
||||
if( this.$el.is(':visible') ) {
|
||||
this.$el.fadeOut('fast');
|
||||
} else {
|
||||
this.$el.hide();
|
||||
}
|
||||
|
||||
this.dropdown_container.empty();
|
||||
return false;
|
||||
},
|
||||
|
||||
showHovercardOn: _.debounce(function(element) {
|
||||
var el = $(element);
|
||||
var hc = this.$el;
|
||||
|
||||
if( !this.show_me ) {
|
||||
// mouse has left element
|
||||
return;
|
||||
}
|
||||
|
||||
hc.hide();
|
||||
hc.prependTo(el);
|
||||
this._positionHovercard();
|
||||
this._populateHovercard();
|
||||
}, 500, true),
|
||||
|
||||
_populateHovercard: function() {
|
||||
var href = this.href();
|
||||
href += "/hovercard.json";
|
||||
|
||||
var self = this;
|
||||
$.get(href, function(person){
|
||||
if( !person || person.length == 0 ) {
|
||||
throw new Error("received data is not a person object");
|
||||
}
|
||||
|
||||
self._populateHovercardWith(person);
|
||||
self.$el.fadeIn('fast');
|
||||
});
|
||||
},
|
||||
|
||||
_populateHovercardWith: function(person) {
|
||||
var self = this;
|
||||
|
||||
this.avatar.attr('src', person.avatar);
|
||||
this.person_link.attr('href', person.url);
|
||||
this.person_link.text(person.name);
|
||||
this.person_handle.text(person.handle);
|
||||
this.dropdown.attr('data-person-id', person.id);
|
||||
|
||||
// set hashtags
|
||||
this.hashtags.empty();
|
||||
this.hashtags.html( $(_.map(person.tags, function(tag){
|
||||
return $('<a/>',{href: "/tags/"+tag.substring(1)}).text(tag)[0] ;
|
||||
})) );
|
||||
|
||||
// set aspect dropdown
|
||||
var href = this.href();
|
||||
href += "/aspect_membership_button"
|
||||
$.get(href, function(response) {
|
||||
self.dropdown_container.html(response);
|
||||
});
|
||||
},
|
||||
|
||||
_positionHovercard: function() {
|
||||
var p = this.$el.parent();
|
||||
var p_pos = p.position();
|
||||
var p_height = p.height();
|
||||
|
||||
this.$el.css({
|
||||
top: p_pos.top + p_height - 25,
|
||||
left: p_pos.left
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -64,7 +64,6 @@
|
|||
events: function() { return Diaspora.page.eventsContainer.data("events"); },
|
||||
flashMessages: this.instantiate("FlashMessages"),
|
||||
header: this.instantiate("Header", body.find("header")),
|
||||
hoverCard: this.instantiate("HoverCard", body.find("#hovercard")),
|
||||
timeAgo: this.instantiate("TimeAgo")
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,138 +0,0 @@
|
|||
(function() {
|
||||
var HoverCard = function() {
|
||||
var self = this;
|
||||
|
||||
self.jXHRs = [];
|
||||
|
||||
self.subscribe("widget/ready", function(evt, hoverCard) {
|
||||
self.personCache = new self.Cache();
|
||||
self.dropdownCache = new self.Cache();
|
||||
|
||||
self.hoverCard = {
|
||||
tip: $("#hovercard_container"),
|
||||
dropdownContainer: $("#hovercard_dropdown_container"),
|
||||
offset: {
|
||||
left: -10,
|
||||
top: 13
|
||||
},
|
||||
personLink: hoverCard.find("a.person"),
|
||||
personHandle: hoverCard.find("p.handle"),
|
||||
avatar: hoverCard.find(".avatar"),
|
||||
dropdown: hoverCard.find(".dropdown_list"),
|
||||
hashtags: hoverCard.find(".hashtags")
|
||||
};
|
||||
|
||||
$(document.body).delegate("a.hovercardable:not(.self)", "hover", self.handleHoverEvent);
|
||||
self.hoverCard.tip.hover(self.hoverCardHover, self.clearTimeout);
|
||||
|
||||
self.subscribe("aspectDropdown/updated aspectDropdown/blurred", function(evt, personId, dropdownHtml) {
|
||||
self.dropdownCache.cache["/people/" + personId + "/aspect_membership_button"] = $(dropdownHtml).removeClass("active").get(0).outerHTML;
|
||||
});
|
||||
});
|
||||
|
||||
this.handleHoverEvent = function(evt) {
|
||||
self.target = $(evt.target);
|
||||
|
||||
if(evt.type === "mouseenter") {
|
||||
self.startHover();
|
||||
}
|
||||
else {
|
||||
self.clearTimeout(evt);
|
||||
}
|
||||
};
|
||||
|
||||
this.startHover = function(evt) {
|
||||
if(!self.hoverCardTimeout) {
|
||||
self.clearTimeout(false);
|
||||
}
|
||||
self.timeout = setTimeout(self.showHoverCard, 600);
|
||||
};
|
||||
|
||||
this.showHoverCard = function() {
|
||||
self.hoverCard.tip.hide();
|
||||
self.hoverCard.tip.prependTo(self.target.parent());
|
||||
|
||||
self.personCache.get(self.target.attr("data-hovercard") + ".json?includes=tags", function(person) {
|
||||
self.populateHovercard(person);
|
||||
});
|
||||
};
|
||||
|
||||
this.populateHovercard = function(person) {
|
||||
var position = self.target.position();
|
||||
self.hoverCard.tip.css({
|
||||
left: position.left + self.hoverCard.offset.left,
|
||||
top: position.top + self.hoverCard.offset.top
|
||||
});
|
||||
|
||||
self.hoverCard.avatar.attr("src", person.avatar);
|
||||
self.hoverCard.personLink.attr("href", person.url);
|
||||
self.hoverCard.personLink.text(person.name);
|
||||
self.hoverCard.personHandle.text(person.handle);
|
||||
self.hoverCard.dropdown.attr("data-person-id", person.id);
|
||||
|
||||
self.hoverCard.hashtags.html("");
|
||||
$.each(person.tags, function(index, hashtag) {
|
||||
self.hoverCard.hashtags.append(
|
||||
$("<a/>", {
|
||||
href: "/tags/" + hashtag.substring(1)
|
||||
}).text(hashtag)
|
||||
);
|
||||
});
|
||||
|
||||
self.dropdownCache.get(self.target.attr("data-hovercard") + "/aspect_membership_button", function(dropdown) {
|
||||
self.hoverCard.dropdownContainer.html(dropdown);
|
||||
self.hoverCard.tip.fadeIn(140);
|
||||
});
|
||||
};
|
||||
|
||||
this.clearTimeout = function(delayed) {
|
||||
self.personCache.clearjXHRs();
|
||||
self.dropdownCache.clearjXHRs();
|
||||
|
||||
function callback() {
|
||||
self.timeout = clearTimeout(self.timeout);
|
||||
self.hoverCard.tip.hide();
|
||||
self.hoverCard.dropdownContainer.html("");
|
||||
}
|
||||
|
||||
if((typeof delayed === "boolean" && delayed) || (typeof delayed === "object" && delayed.type === "mouseleave")) {
|
||||
self.hoverCardTimeout = setTimeout(callback, 20);
|
||||
}
|
||||
else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
this.hoverCardHover = function() {
|
||||
self.hoverCardTimeout = clearTimeout(self.hoverCardTimeout);
|
||||
};
|
||||
|
||||
this.Cache = function() {
|
||||
var self = this;
|
||||
this.cache = {};
|
||||
this.jXHRs = [];
|
||||
|
||||
this.get = function(key, callback) {
|
||||
if(typeof self.cache[key] === "undefined") {
|
||||
self.jXHRs.push($.get(key, function(response) {
|
||||
self.cache[key] = response;
|
||||
callback(response);
|
||||
self.jXHRs.shift();
|
||||
}));
|
||||
}
|
||||
else {
|
||||
callback(self.cache[key]);
|
||||
}
|
||||
};
|
||||
|
||||
this.clearjXHRs = function() {
|
||||
$.each(self.jXHRs, function(index, jXHR) {
|
||||
jXHR.abort();
|
||||
});
|
||||
self.jXHRs = [];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
Diaspora.Widgets.HoverCard = HoverCard;
|
||||
})();
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
display: inline-block;
|
||||
min-width: 250px;
|
||||
max-width: 400px;
|
||||
|
||||
|
||||
background-color: $background;
|
||||
height: 70px;
|
||||
border: 1px solid #999999;
|
||||
|
|
@ -32,13 +32,17 @@
|
|||
& > h4, & > div, & > p {
|
||||
margin-left: $image_width;
|
||||
}
|
||||
|
||||
|
||||
& > h4, & > div, & > p, .hashtags {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
#hovercard_dropdown_container {
|
||||
overflow: visible !important; /* otherwise the aspect dropdown is cropped */
|
||||
}
|
||||
|
||||
padding: 5px {
|
||||
bottom: 25px;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
{{/if}}
|
||||
|
||||
{{#with author}}
|
||||
<a href="/people/{{guid}}" class="img">
|
||||
<a href="/people/{{guid}}" class="img {{{hovercardable this}}}">
|
||||
{{{personImage this}}}
|
||||
</a>
|
||||
{{/with}}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,13 @@ class PeopleController < ApplicationController
|
|||
:format => :html, :layout => false, :status => 404
|
||||
end
|
||||
|
||||
rescue_from Diaspora::AccountClosed do
|
||||
respond_to do |format|
|
||||
format.any { redirect_to :back, :notice => t("people.show.closed_account") }
|
||||
format.json { render :nothing => true, :status => 410 } # 410 GONE
|
||||
end
|
||||
end
|
||||
|
||||
helper_method :search_query
|
||||
|
||||
def index
|
||||
|
|
@ -36,7 +43,7 @@ class PeopleController < ApplicationController
|
|||
if diaspora_id?(search_query)
|
||||
@people = Person.where(:diaspora_handle => search_query.downcase)
|
||||
if @people.empty?
|
||||
Webfinger.in_background(search_query)
|
||||
Webfinger.in_background(search_query)
|
||||
@background_query = search_query.downcase
|
||||
end
|
||||
end
|
||||
|
|
@ -66,11 +73,12 @@ class PeopleController < ApplicationController
|
|||
respond_with @people
|
||||
end
|
||||
|
||||
# renders the persons user profile page
|
||||
def show
|
||||
@person = Person.find_from_guid_or_username(params)
|
||||
|
||||
authenticate_user! if remote_profile_with_no_user_session?
|
||||
return redirect_to :back, :notice => t("people.show.closed_account") if @person.closed_account?
|
||||
raise Diaspora::AccountClosed if @person.closed_account?
|
||||
|
||||
@post_type = :all
|
||||
@aspect = :profile
|
||||
|
|
@ -108,6 +116,23 @@ class PeopleController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
# hovercards fetch some the persons public profile data via json and display
|
||||
# it next to the avatar image in a nice box
|
||||
def hovercard
|
||||
@person = Person.find_from_guid_or_username({:id => params[:person_id]})
|
||||
raise Diaspora::AccountClosed if @person.closed_account?
|
||||
|
||||
respond_to do |format|
|
||||
format.all do
|
||||
redirect_to :action => "show", :id => params[:person_id]
|
||||
end
|
||||
|
||||
format.json do
|
||||
render :json => HovercardPresenter.new(@person)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def last_post
|
||||
@person = Person.find_from_guid_or_username(params)
|
||||
last_post = Post.visible_from_author(@person, current_user).order('posts.created_at DESC').first
|
||||
|
|
@ -148,8 +173,6 @@ class PeopleController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def redirect_if_tag_search
|
||||
if search_query.starts_with?('#')
|
||||
if search_query.length > 1
|
||||
|
|
@ -162,6 +185,8 @@ class PeopleController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def hashes_for_people(people, aspects)
|
||||
ids = people.map{|p| p.id}
|
||||
contacts = {}
|
||||
|
|
|
|||
39
app/presenters/hovercard_presenter.rb
Normal file
39
app/presenters/hovercard_presenter.rb
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
class HovercardPresenter
|
||||
|
||||
attr_accessor :person
|
||||
|
||||
# initialize the presenter with the given Person object
|
||||
def initialize(person)
|
||||
raise ArgumentError, "the given object is not a Person" unless person.class == Person
|
||||
|
||||
self.person = person
|
||||
end
|
||||
|
||||
# returns the json representation of the Person object for use with the
|
||||
# hovercard UI
|
||||
def to_json(options={})
|
||||
{ :id => person.id,
|
||||
:avatar => avatar('small'),
|
||||
:url => profile_url,
|
||||
:name => person.name,
|
||||
:handle => person.diaspora_handle,
|
||||
:tags => person.tags.map { |t| "#"+t.name }
|
||||
}.to_json(options)
|
||||
end
|
||||
|
||||
# get the image url of the profile avatar for the given size
|
||||
# possible sizes: 'small', 'medium', 'large'
|
||||
def avatar(size="small")
|
||||
if !["small", "medium", "large"].include?(size)
|
||||
raise ArgumentError, "the given parameter is not a valid size"
|
||||
end
|
||||
|
||||
person.image_url("thumb_#{size}".to_sym)
|
||||
end
|
||||
|
||||
# return the (relative) url to the user profile page.
|
||||
# uses the 'person_path' url helper from the rails routes
|
||||
def profile_url
|
||||
Rails.application.routes.url_helpers.person_path(person)
|
||||
end
|
||||
end
|
||||
|
|
@ -9,7 +9,7 @@ Diaspora::Application.routes.draw do
|
|||
|
||||
|
||||
get "/atom.xml" => redirect('http://blog.diasporafoundation.org/feed/atom') #too many stupid redirects :()
|
||||
|
||||
|
||||
get 'oembed' => 'posts#oembed', :as => 'oembed'
|
||||
# Posting and Reading
|
||||
resources :reshares
|
||||
|
|
@ -48,7 +48,7 @@ Diaspora::Application.routes.draw do
|
|||
get "liked" => "streams#liked", :as => "liked_stream"
|
||||
get "commented" => "streams#commented", :as => "commented_stream"
|
||||
get "aspects" => "streams#aspects", :as => "aspects_stream"
|
||||
|
||||
|
||||
resources :aspects do
|
||||
put :toggle_contact_visibility
|
||||
end
|
||||
|
|
@ -114,7 +114,7 @@ Diaspora::Application.routes.draw do
|
|||
get 'invitations/email' => 'invitations#email', :as => 'invite_email'
|
||||
get 'users/invitations' => 'invitations#new', :as => 'new_user_invitation'
|
||||
post 'users/invitations' => 'invitations#create', :as => 'new_user_invitation'
|
||||
|
||||
|
||||
get 'login' => redirect('/users/sign_in')
|
||||
|
||||
scope 'admins', :controller => :admins do
|
||||
|
|
@ -145,6 +145,7 @@ Diaspora::Application.routes.draw do
|
|||
resources :photos
|
||||
get :contacts
|
||||
get "aspect_membership_button" => :aspect_membership_dropdown, :as => "aspect_membership_button"
|
||||
get :hovercard
|
||||
|
||||
member do
|
||||
get :last_post
|
||||
|
|
|
|||
|
|
@ -3,6 +3,12 @@
|
|||
# the COPYRIGHT file.
|
||||
|
||||
module Diaspora
|
||||
# the post in question is not public, and that is somehow a problem
|
||||
class NonPublic < StandardError
|
||||
end
|
||||
|
||||
# the account was closed and that should not be the case if we want
|
||||
# to continue
|
||||
class AccountClosed < StandardError
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -367,7 +367,23 @@ describe PeopleController do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#hovercard' do
|
||||
before do
|
||||
@hover_test = FactoryGirl.create(:person)
|
||||
@hover_test.profile.tag_string = '#test #tags'
|
||||
@hover_test.profile.save!
|
||||
end
|
||||
|
||||
it 'redirects html requests' do
|
||||
get :hovercard, :person_id => @hover_test.guid
|
||||
response.should redirect_to person_path(:id => @hover_test.guid)
|
||||
end
|
||||
|
||||
it 'returns json with profile stuff' do
|
||||
get :hovercard, :person_id => @hover_test.guid, :format => 'json'
|
||||
JSON.parse( response.body )['handle'].should == @hover_test.diaspora_handle
|
||||
end
|
||||
end
|
||||
|
||||
describe '#refresh_search ' do
|
||||
before(:each)do
|
||||
|
|
|
|||
Loading…
Reference in a new issue