Merge pull request #3802 from Raven24/hovercards

Bring back the hovercards... backbone style
This commit is contained in:
Dennis Schubert 2012-12-29 06:41:17 -08:00
commit 679d119d4d
13 changed files with 243 additions and 159 deletions

View file

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

View file

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

View file

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

View 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
});
}
});

View file

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

View file

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

View file

@ -39,6 +39,10 @@
text-overflow: ellipsis;
}
#hovercard_dropdown_container {
overflow: visible !important; /* otherwise the aspect dropdown is cropped */
}
padding: 5px {
bottom: 25px;
};

View file

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

View file

@ -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
@ -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 = {}

View 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

View file

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

View file

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

View file

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