Permalink
Browse files

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)
  • Loading branch information...
1 parent 42a01f3 commit 0092acd492b93d2b0a1820b511ee960d1d9926b0 @Raven24 Raven24 committed with Raven24 Dec 18, 2012
View
1 Changelog.md
@@ -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
6 app/assets/javascripts/app/app.js
@@ -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
30 app/assets/javascripts/app/helpers/handlebars-helpers.js
@@ -1,26 +1,40 @@
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\">";
html+= block.fn(context);
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
115 app/assets/javascripts/app/views/hovercard_view.js
@@ -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
1 app/assets/javascripts/diaspora.js
@@ -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
138 app/assets/javascripts/widgets/hovercard.js
@@ -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
8 app/assets/stylesheets/hovercard.css.scss
@@ -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;
};
View
2 app/assets/templates/stream-element_tpl.jst.hbs
@@ -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
33 app/controllers/people_controller.rb
@@ -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 @@ def index
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 @@ def tag_index
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 @@ def show
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 @@ def aspect_membership_dropdown
end
end
- private
-
def redirect_if_tag_search
if search_query.starts_with?('#')
if search_query.length > 1
@@ -162,6 +185,8 @@ def redirect_if_tag_search
end
end
+ private
+
def hashes_for_people(people, aspects)
ids = people.map{|p| p.id}
contacts = {}
View
39 app/presenters/hovercard_presenter.rb
@@ -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
7 config/routes.rb
@@ -9,7 +9,7 @@
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 @@
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 @@
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 @@
resources :photos
get :contacts
get "aspect_membership_button" => :aspect_membership_dropdown, :as => "aspect_membership_button"
+ get :hovercard
member do
get :last_post
View
6 lib/exceptions.rb
@@ -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
16 spec/controllers/people_controller_spec.rb
@@ -367,7 +367,23 @@
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

0 comments on commit 0092acd

Please sign in to comment.