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