diff --git a/Changelog.md b/Changelog.md
index 53735b606..077d4ce5a 100644
--- a/Changelog.md
+++ b/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
diff --git a/app/assets/javascripts/app/app.js b/app/assets/javascripts/app/app.js
index 3b29d5361..a4027d1ae 100644
--- a/app/assets/javascripts/app/app.js
+++ b/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) {
diff --git a/app/assets/javascripts/app/helpers/handlebars-helpers.js b/app/assets/javascripts/app/helpers/handlebars-helpers.js
index e1984aed2..c0caa5dd0 100644
--- a/app/assets/javascripts/app/helpers/handlebars-helpers.js
+++ b/app/assets/javascripts/app/helpers/handlebars-helpers.js
@@ -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 = "";
@@ -12,15 +12,29 @@ Handlebars.registerHelper('linkToPerson', function(context, block) {
html+= "";
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 "
";
-})
+ return _.template('
', {
+ 'src': person.avatar[size],
+ 'img_class': imageClass,
+ 'title': _.escape(person.name)
+ });
+});
diff --git a/app/assets/javascripts/app/views/hovercard_view.js b/app/assets/javascripts/app/views/hovercard_view.js
new file mode 100644
index 000000000..47a808b6a
--- /dev/null
+++ b/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 $('',{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
+ });
+ }
+});
\ No newline at end of file
diff --git a/app/assets/javascripts/diaspora.js b/app/assets/javascripts/diaspora.js
index 749080d42..7d683fdcb 100644
--- a/app/assets/javascripts/diaspora.js
+++ b/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")
});
};
diff --git a/app/assets/javascripts/widgets/hovercard.js b/app/assets/javascripts/widgets/hovercard.js
deleted file mode 100644
index 080b46656..000000000
--- a/app/assets/javascripts/widgets/hovercard.js
+++ /dev/null
@@ -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(
- $("", {
- 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;
-})();
diff --git a/app/assets/stylesheets/hovercard.css.scss b/app/assets/stylesheets/hovercard.css.scss
index 0a03c82b3..a45655daa 100644
--- a/app/assets/stylesheets/hovercard.css.scss
+++ b/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;
};
diff --git a/app/assets/templates/stream-element_tpl.jst.hbs b/app/assets/templates/stream-element_tpl.jst.hbs
index c57305996..2b5c969a0 100644
--- a/app/assets/templates/stream-element_tpl.jst.hbs
+++ b/app/assets/templates/stream-element_tpl.jst.hbs
@@ -18,7 +18,7 @@
{{/if}}
{{#with author}}
-
+
{{{personImage this}}}
{{/with}}
diff --git a/app/controllers/people_controller.rb b/app/controllers/people_controller.rb
index d54a02f1e..486ff56a5 100644
--- a/app/controllers/people_controller.rb
+++ b/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 @@ 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 = {}
diff --git a/app/presenters/hovercard_presenter.rb b/app/presenters/hovercard_presenter.rb
new file mode 100644
index 000000000..368f8d95c
--- /dev/null
+++ b/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
diff --git a/config/routes.rb b/config/routes.rb
index 781166415..57e41286c 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -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
diff --git a/lib/exceptions.rb b/lib/exceptions.rb
index c278f72eb..5b1d92f19 100644
--- a/lib/exceptions.rb
+++ b/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
diff --git a/spec/controllers/people_controller_spec.rb b/spec/controllers/people_controller_spec.rb
index 21bbcc87c..222509082 100644
--- a/spec/controllers/people_controller_spec.rb
+++ b/spec/controllers/people_controller_spec.rb
@@ -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