Merge branch 'hovercard'

This commit is contained in:
danielgrippi 2011-07-06 13:51:58 -07:00
commit dfd30a71bf
20 changed files with 313 additions and 61 deletions

View file

@ -6,6 +6,8 @@
class AspectMembershipsController < ApplicationController
before_filter :authenticate_user!
respond_to :html, :json, :js
def destroy
#note :id is garbage
@ -17,14 +19,14 @@ class AspectMembershipsController < ApplicationController
if membership && membership.destroy
@aspect = membership.aspect
flash.now[:notice] = I18n.t 'aspect_memberships.destroy.success'
respond_to do |format|
format.js { }
format.html{
redirect_to :back
}
respond_with do |format|
format.all{ }
format.json{ render :json => {
:person_id => @person_id,
:aspect_ids => @contact.aspects.map{|a| a.id}
} }
end
else
@ -44,12 +46,12 @@ class AspectMembershipsController < ApplicationController
@aspect = current_user.aspects.where(:id => params[:aspect_id]).first
if @contact = current_user.share_with(@person, @aspect)
flash.now[:notice] = I18n.t 'aspects.add_to_aspect.success'
respond_with AspectMembership.where(:contact_id => @contact.id, :aspect_id => @aspect.id).first
else
flash[:error] = I18n.t 'contacts.create.failure'
redirect_to :back
#TODO(dan) take this out once the .js template is removed
render :nothing => true
end
end

View file

@ -24,4 +24,5 @@ class ContactsController < ApplicationController
@contacts = current_user.contacts.sharing.includes(:aspect_memberships)
render :layout => false
end
end

View file

@ -106,7 +106,12 @@ class PeopleController < ApplicationController
if params[:only_posts]
render :partial => 'shared/stream', :locals => {:posts => @posts}
else
respond_with @person, :locals => {:post_type => :all}
respond_to do |format|
format.all { respond_with @person, :locals => {:post_type => :all} }
format.json {
render :json => @person.to_json
}
end
end
else
@ -115,6 +120,7 @@ class PeopleController < ApplicationController
end
end
def retrieve_remote
if params[:diaspora_handle]
webfinger(params[:diaspora_handle], :single_aspect_form => true)
@ -131,7 +137,6 @@ class PeopleController < ApplicationController
@aspect = :profile
@contacts_of_contact = @contact.contacts.paginate(:page => params[:page], :per_page => (params[:limit] || 15))
@hashes = hashes_for_people @contacts_of_contact, @aspects
@contact = current_user.contact_for(@person)
@aspects_with_person = @contact.aspects
@aspect_ids = @aspects_with_person.map(&:id)
else
@ -139,8 +144,16 @@ class PeopleController < ApplicationController
redirect_to people_path
end
end
def aspect_membership_dropdown
@person = Person.find(params[:id])
@contact = current_user.contact_for(@person) || Contact.new
render :partial => 'aspect_memberships/aspect_dropdown', :locals => {:contact => @contact, :person => @person, :hang => 'left'}
end
private
def webfinger(account, opts = {})
Resque.enqueue(Job::SocketWebfinger, current_user.id, account, opts)
end
end

View file

@ -59,6 +59,8 @@ module ApplicationHelper
end
def person_link(person, opts={})
opts[:class] ||= ""
opts[:class] << " self" if current_user.person == person
"<a href='/people/#{person.id}' class='#{opts[:class]}'>
#{h(person.name)}
</a>".html_safe

View file

@ -68,16 +68,12 @@ module AspectGlobalHelper
def aspect_dropdown_list_item(aspect, contact, person)
checked = (contact.persisted? && contact.aspect_memberships.detect{ |am| am.aspect_id == aspect.id})
klass = checked ? "selected" : ""
hidden = !checked ? "hidden" : ""
str = <<LISTITEM
<li data-aspect_id=#{aspect.id} class='#{klass}'>
<img src='/images/icons/check_yes_ok.png' width=18 height=18 class='check #{hidden}'/>
<img src='/images/icons/check_yes_ok.png' width=18 height=18 class='check'/>
<img src='/images/icons/check_yes_ok_white.png' width=18 height=18 class='checkWhite'/>
#{aspect.name}
<div class=\"hidden\">
#{aspect_membership_button(aspect, contact, person)}
</div>
</li>
LISTITEM
str.html_safe

View file

@ -16,4 +16,12 @@ class AspectMembership < ActiveRecord::Base
true
end
def as_json(opts={})
{
:id => self.id,
:person_id => self.person.id,
:contact_id => self.contact.id,
:aspect_ids => self.contact.aspects.map{|a| a.id}
}
end
end

View file

@ -220,12 +220,13 @@ class Person < ActiveRecord::Base
end
def as_json(opts={})
{
:id => self.guid,
json = {
:id => self.id,
:name => self.name,
:avatar => self.profile.image_url(:thumb_small),
:handle => self.diaspora_handle,
:url => "/people/#{self.id}"
:url => "/people/#{self.id}",
:hashtags => self.profile.tags.map{|t| "##{t.name}"}
}
end

View file

@ -19,4 +19,3 @@ if($('#aspects_list').length == 1) {
};
element.fadeTo(200,1);

View file

@ -9,7 +9,7 @@
= person_image_link(comment.author)
.content
%span.from
= person_link(comment.author)
= person_link(comment.author, :class => "hovercardable")
%span{:class => direction_for(comment.text)}
= markdownify(comment.text, :youtube_maps => comment.youtube_titles)

View file

@ -52,6 +52,17 @@
.ajax_loader
= image_tag("ajax-loader.gif")
#hovercard_container
#hovercard
%img.avatar
%h4
%a.person
#hovercard_dropdown_container
.hovercard_footer
.footer_container
.hashtags
%ul#user_menu.dropdown
%li
.right

View file

@ -5,13 +5,9 @@
- if current_user
- contact = current_user.contacts.find_by_person_id(person.id)
- contact ||= Contact.new(:person => person)
- unless person == current_user.person
.right
= render 'aspect_memberships/aspect_dropdown', :contact => contact, :person => person, :hang => 'left'
.content
%span.from
=person_link(person)
.info
= person.profile.format_tags(person.profile.tag_string)
=person_link(person, :class => "hovercardable")
= will_paginate people, :params => {:controller => 'people', :action => 'tag_index'}

View file

@ -18,7 +18,7 @@
.content
%div.post_initial_info
%span.from
= person_link(post.author, :class => 'author')
= person_link(post.author, :class => 'hovercardable')
%time.time.timeago{:datetime => post.created_at, :integer => time_for_sort(post).to_i}
%span.details
\-

View file

@ -33,9 +33,10 @@ javascripts:
- public/javascripts/widgets/infinite-scroll.js
- public/javascripts/widgets/directionDetector.js
- public/javascripts/widgets/notifications.js
- public/javascripts/widgets/notifications-badge.js
- public/javascripts/widgets/flashes.js
- public/javascripts/widgets/post.js
- public/javascripts/widgets/hovercard.js
- public/javascripts/widgets/notifications-badge.js
- public/javascripts/view.js
- public/javascripts/stream.js
- public/javascripts/content-updater.js
@ -45,18 +46,27 @@ javascripts:
- public/javascripts/login.js
mobile:
- public/javascripts/vendor/jquery152.min.js
- public/javascripts/custom-mobile-scripting.js
- public/javascripts/vendor/jquery.mobile-1.0a4.js
- public/javascripts/jquery.infinitescroll-custom.js
- public/javascripts/vendor/underscore.js
- public/javascripts/vendor/backbone-min.js
- public/javascripts/vendor/Mustache.js
- public/javascripts/mobile/app.js
- public/javascripts/mobile/helpers.js
- public/javascripts/mobile/models/*
- public/javascripts/mobile/collections/*
- public/javascripts/mobile/controllers/*
- public/javascripts/mobile/views/*
- public/javascripts/diaspora.js
- public/javascripts/widgets/i18n.js
- public/javascripts/widgets/infinite-scroll.js
- public/javascripts/rails.js
mailchimp:
- public/javascripts/vendor/mailchimp/jquery.form.js
- public/javascripts/vendor/mailchimp/jquery.validate.js
- public/javascripts/vendor/mailchimp/jquery126.min.js
aspects:
- public/javascripts/aspect-edit.js
- public/javascripts/contact-list.js
finder:
- public/javascripts/friend-finder.js
@ -86,12 +96,9 @@ stylesheets:
- public/stylesheets/vendor/fileuploader.css
- public/stylesheets/vendor/tipsy.css
- public/stylesheets/vendor/autoSuggest.css
popup:
- public/stylesheets/application.css
- public/stylesheets/popup.css
- public/stylesheets/ui.css
rtl:
- public/stylesheets/rtl.css
mobile:
- public/stylesheets/mobile.css

View file

@ -104,6 +104,8 @@ Diaspora::Application.routes.draw do
end
end
get "people/:id/aspect_membership_button" => "people#aspect_membership_dropdown", :as => "aspect_membership_button"

View file

@ -9,12 +9,9 @@ var ContactEdit = {
});
},
updateNumber: function(personId){
var dropdown = $(".dropdown_list[data-person_id=" + personId.toString() +"]"),
number = dropdown.find(".selected").length,
button = dropdown.parents(".dropdown").children('.button.toggle');
var replacement;
updateNumber: function(dropdown, personId, number){
var button = dropdown.parents(".dropdown").children('.button.toggle'),
replacement;
if (number == 0) {
button.removeClass("in_aspects");
@ -27,7 +24,7 @@ var ContactEdit = {
}else if (number > 3) {
replacement = Diaspora.widgets.i18n.t('aspect_dropdown.toggle.many', { count: number.toString()})
}else {
//the above one are a totalogy, but I want to have them here once for once we figure out a neat way i18n them
//the above one are a tautology, but I want to have them here once for once we figure out a neat way i18n them
replacement = Diaspora.widgets.i18n.t('aspect_dropdown.toggle.other', { count: number.toString()})
}
@ -36,7 +33,6 @@ var ContactEdit = {
toggleCheckbox:
function(check){
check.toggleClass('hidden');
check.parent('li').toggleClass('selected');
},
@ -44,10 +40,20 @@ var ContactEdit = {
var button = li.find('.button');
if(button.hasClass('disabled') || li.hasClass('newItem')){ return; }
var checkbox = li.find('img.check');
ContactEdit.toggleCheckbox(checkbox);
var checkbox = li.find('img.check'),
selected = li.hasClass("selected"),
routedId = selected ? "/42" : "";
$.fn.callRemote.apply(button);
$.post("/aspect_memberships" + routedId + ".json", {
"aspect_id": li.data("aspect_id"),
"person_id": li.parent().data("person_id"),
"_method": (selected) ? "DELETE" : "POST"
}, function(aspectMembership) {
ContactEdit.toggleCheckbox(checkbox);
ContactEdit.updateNumber(li.closest(".dropdown_list"), li.parent().data("person_id"), aspectMembership.aspect_ids.length);
Diaspora.widgets.publish("aspectDropdown/updated", [li.parent().data("person_id"), li.parents(".dropdown").get(0).outerHTML]);
});
},
};

View file

@ -39,11 +39,14 @@
};
Diaspora.WidgetCollection.prototype.subscribe = function(id, callback, context) {
this.eventsContainer.bind(id, $.proxy(callback, context));
var ids = id.split(" ");
for(var id in ids) {
this.eventsContainer.bind(ids[id], $.proxy(callback, context));
}
};
Diaspora.WidgetCollection.prototype.publish = function(id) {
this.eventsContainer.trigger(id);
Diaspora.WidgetCollection.prototype.publish = function(id, args) {
this.eventsContainer.trigger(id, args);
};
Diaspora.widgets = new Diaspora.WidgetCollection();

View file

@ -0,0 +1,136 @@
(function() {
var HoverCard = function() {
var self = this;
self.jXHRs = [];
this.start = function() {
self.personCache = new this.Cache();
self.dropdownCache = new this.Cache();
var card = $("#hovercard");
self.hoverCard = {
tip: $("#hovercard_container"),
dropdownContainer: $("#hovercard_dropdown_container"),
offset: {
left: -10,
top: 13
},
personLink: card.find("a.person"),
avatar: card.find(".avatar"),
dropdown: card.find(".dropdown_list"),
hashtags: card.find(".hashtags"),
};
$(document.body).delegate("a.hovercardable:not(.self)", "hover", self.handleHoverEvent);
self.hoverCard.tip.hover(self.hoverCardHover, self.clearTimeout);
Diaspora.widgets.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("href") + ".json", 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.dropdown.attr("data-person-id", person.id);
$.each(person.hashtags, function(index, hashtag) {
self.hoverCard.hashtags.append(
$("<a/>", {
href: "/tags/" + hashtag.substring(1)
}).text(hashtag)
);
});
self.dropdownCache.get(self.target.attr("href") + "/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, 200);
}
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.add("hoverCard", HoverCard);
})();

View file

@ -3066,6 +3066,62 @@ ul.left_nav
:margin
:top 30px
.tags_people
.dropdown
:display none
#hovercard
@include border-radius(2px)
@include box-shadow(0,0,5px,#666)
:position relative
.avatar
:position relative
:height 70px
:width 70px
:margin
:right 10px
:background
:color $background
:padding 5px
:bottom 55px
:border 1px solid #999
:width 220px
.hovercard_footer
:position absolute
:bottom 0
:left 0
:background
:color #eee
:width 100%
:height 20px
:min-height 20px
:font
:size smaller
:border
:top 1px solid #ccc
.footer_container
:padding 2px 5px
.hashtags
:overflow hidden
:white-space nowrap
:text-overflow ellipsis
a
:color #999
:margin
:right 4px
#hovercard_container
:padding 10px
:top 5px
:position absolute
:display none
:z-index 10

View file

@ -150,7 +150,10 @@
&:hover
:text-decoration none
&:not(.selected)
.check
:display none
&.hang_right
.wrapper
@ -170,7 +173,7 @@
@include border-radius(3px, 3px, 0, 0)
:border 1px solid #444
:bottom none
.selected
:font-weight bold

View file

@ -40,7 +40,6 @@ describe AspectMembershipsController do
}.should change{
alice.contact_for(bob.person).aspect_memberships.count
}.by(1)
end
it 'creates a contact' do
@ -62,8 +61,19 @@ describe AspectMembershipsController do
:aspect_id => @aspect0.id
flash[:error].should_not be_empty
end
end
context 'json' do
it 'returns a list of aspect ids for the person' do
post :create,
:format => 'json',
:person_id => @person.id,
:aspect_id => @aspect0.id
contact = @controller.current_user.contact_for(@person)
response.body.should == contact.aspect_memberships.first.to_json
end
end
end
describe "#destroy" do
it 'removes contacts from an aspect' do