likes/dislikes WIP TODO: better icons, better styling, integration on photo show page

This commit is contained in:
MrZYX 2011-03-20 12:04:45 +01:00
parent 69c0d8e1d9
commit 1b1a23aa8f
26 changed files with 447 additions and 13 deletions

View file

@ -31,7 +31,7 @@ class AspectsController < ApplicationController
@aspect_ids = @aspects.map{|a| a.id}
@posts = StatusMessage.joins(:aspects).where(:pending => false,
:aspects => {:id => @aspect_ids}).includes(:comments, :photos).select('DISTINCT `posts`.*').paginate(
:aspects => {:id => @aspect_ids}).includes(:comments, :photos, :likes, :dislikes).select('DISTINCT `posts`.*').paginate(
:page => params[:page], :per_page => 15, :order => sort_order + ' DESC')
@fakes = PostsFake.new(@posts)

View file

@ -37,10 +37,10 @@ class CommentsController < ApplicationController
format.mobile{ redirect_to status_message_path(@comment.post_id) }
end
else
render :nothing => true, :status => 406
render :nothing => true, :status => 422
end
else
render :nothing => true, :status => 406
render :nothing => true, :status => 422
end
end

View file

@ -0,0 +1,44 @@
# Copyright (c) 2010, Diaspora Inc. This file is
# licensed under the Affero General Public License version 3 or later. See
# the COPYRIGHT file.
class LikesController < ApplicationController
include ApplicationHelper
before_filter :authenticate_user!
respond_to :html, :mobile, :json
def create
target = current_user.find_visible_post_by_id params[:post_id]
positive = (params[:positive] == 'true') ? true : false
if target
@like = current_user.build_like(positive, :on => target)
if @like.save
Rails.logger.info("event=create type=like user=#{current_user.diaspora_handle} status=success like=#{@like.id} positive=#{positive}")
Postzord::Dispatch.new(current_user, @like).post
respond_to do |format|
format.js {
json = { :post_id => @like.post_id,
:html => render_to_string(
:partial => 'likes/likes',
:locals => {
:likes => @like.post.likes,
:dislikes => @like.post.dislikes
}
)
}
render(:json => json, :status => 201)
}
format.html { render :nothing => true, :status => 201 }
format.mobile { redirect_to status_message_path(@like.post_id) }
end
else
render :nothing => true, :status => 422
end
else
render :nothing => true, :status => 422
end
end
end

View file

@ -111,10 +111,10 @@ class PhotosController < ApplicationController
:status => 201}
end
else
render :nothing => true, :status => 406
render :nothing => true, :status => 422
end
else
render :nothing => true, :status => 406
render :nothing => true, :status => 422
end
end

View file

@ -0,0 +1,10 @@
# Copyright (c) 2010, Diaspora Inc. This file is
# licensed under the Affero General Public License version 3 or later. See
# the COPYRIGHT file.
module LikesHelper
def likes_list likes
links = likes.collect { |like| link_to "#{h(like.author.name.titlecase)}", person_path(like.author) }
links.join(", ").html_safe
end
end

View file

@ -50,6 +50,9 @@ module SocketsHelper
elsif object.is_a? Comment
v = render_to_string(:partial => 'comments/comment', :locals => {:comment => object, :person => object.author})
elsif object.is_a? Like
v = render_to_string(:partial => 'likes/likes', :locals => {:likes => object.post.likes, :dislikes => object.post.dislikes})
elsif object.is_a? Notification
v = render_to_string(:partial => 'notifications/popup', :locals => {:note => object, :person => opts[:actor]})
@ -74,6 +77,10 @@ module SocketsHelper
end
if object.is_a? Like
action_hash[:post_guid] = object.post.guid
end
action_hash[:mine?] = object.author && (object.author.owner_id == uid) if object.respond_to?(:author)
I18n.locale = old_locale unless user.nil?

42
app/models/like.rb Normal file
View file

@ -0,0 +1,42 @@
# Copyright (c) 2010, Diaspora Inc. This file is
# licensed under the Affero General Public License version 3 or later. See
# the COPYRIGHT file.
class Like < ActiveRecord::Base
require File.join(Rails.root, 'lib/diaspora/web_socket')
include ROXML
include Diaspora::Webhooks
include Diaspora::Relayable
include Diaspora::Guid
include Diaspora::Socketable
xml_attr :positive
xml_attr :diaspora_handle
belongs_to :post
belongs_to :author, :class_name => 'Person'
validates_uniqueness_of :post_id, :scope => :author_id
def diaspora_handle
self.author.diaspora_handle
end
def diaspora_handle= nh
self.author = Webfinger.new(nh).fetch
end
def parent_class
Post
end
def parent
self.post
end
def parent= parent
self.post = parent
end
end

View file

@ -14,6 +14,8 @@ class Post < ActiveRecord::Base
xml_attr :created_at
has_many :comments, :order => 'created_at ASC'
has_many :likes, :conditions => '`likes`.`positive` = 1'
has_many :dislikes, :conditions => '`likes`.`positive` = 0', :class_name => 'Like'
has_many :post_visibilities
has_many :aspects, :through => :post_visibilities
has_many :mentions, :dependent => :destroy

View file

@ -176,6 +176,32 @@ class User < ActiveRecord::Base
comment
end
######## Liking ########
def build_like(positive, options = {})
like = Like.new(:author_id => self.person.id,
:positive => positive,
:post => options[:on])
like.set_guid
#sign like as liker
like.author_signature = like.sign_with_key(self.encryption_key)
if !like.post_id.blank? && person.owns?(like.parent)
#sign like as post owner
like.parent_author_signature = like.sign_with_key(self.encryption_key)
end
like
end
def liked?(post)
[post.likes, post.dislikes].each do |likes|
likes.each do |like|
return true if like.author_id == self.person.id
end
end
return false
end
######### Mailer #######################
def mail(job, *args)
pref = job.to_s.gsub('Job::Mail', '').underscore

View file

@ -0,0 +1,17 @@
-# Copyright (c) 2010, Diaspora Inc. This file is
-# licensed under the Affero General Public License version 3 or later. See
-# the COPYRIGHT file.
- if likes.length > 0
.likes
= image_tag('icons/happy_smiley.png')
= link_to t('.people_like_this', :count => likes.length), "#", :class => "expand_likes"
%span.hidden.likes_list
= likes_list(likes)
- if dislikes.length > 0
.dislikes
= image_tag('icons/sad_smiley.png')
= link_to t('.people_dislike_this', :count => dislikes.length), "#", :class => "expand_dislikes"
%span.hidden.dislikes_list
= likes_list(dislikes)

View file

@ -66,5 +66,13 @@
= t('_comments')
#photo_stream.stream.show
- if (defined?(current_user) && !current_user.liked?(@parent))
%span.like_links
= link_to t('shared.stream_element.like'), {:controller => "likes", :action => "create", :positive => 'true', :post_id => @parent.id }, :class => "like_it", :remote => true
|
= link_to t('shared.stream_element.dislike'), {:controller => "likes", :action => "create", :positive => 'false', :post_id => @parent.id }, :class => "dislike_it", :remote => true
%div{:data=>{:guid=>@parent.id}}
.likes_container
= render "likes/likes", :post_id => @parent.id, :likes => @parent.likes, :dislikes => @parent.dislikes
= render "comments/comments", :post_id => @parent.id, :comments => @parent.comments, :always_expanded => true

View file

@ -32,6 +32,15 @@
= link_to(how_long_ago(post), status_message_path(post))
- unless (defined?(@commenting_disabled) && @commenting_disabled)
= link_to t('comments.new_comment.comment').downcase, '#', :class => 'focus_comment_textarea'
= link_to t('comments.new_comment.comment'), '#', :class => 'focus_comment_textarea'
- if (defined?(current_user) && !current_user.liked?(post))
%span.like_links
|
= link_to t('.like'), {:controller => "likes", :action => "create", :positive => 'true', :post_id => post.id }, :class => "like_it", :remote => true
|
= link_to t('.dislike'), {:controller => "likes", :action => "create", :positive => 'false', :post_id => post.id }, :class => "dislike_it", :remote => true
.likes_container
= render "likes/likes", :post_id => post.id, :likes => post.likes, :dislikes => post.dislikes, :current_user => current_user
= render "comments/comments", :post_id => post.id, :comments => post.comments, :current_user => current_user, :condensed => true, :commenting_disabled => (defined?(@commenting_disabled) && @commenting_disabled)

View file

@ -157,7 +157,7 @@ en:
many: "%{count} comments"
other: "%{count} comments"
new_comment:
comment: "Comment"
comment: "comment"
commenting: "Commenting..."
contacts:
@ -262,6 +262,22 @@ en:
toggle: "toggle mobile site"
public_feed: "Public Diaspora Feed for %{name}"
likes:
likes:
people_like_this:
zero: "no people liked this"
one: "1 person liked this"
few: "%{count} people liked this"
many: "%{count} people liked this"
other: "%{count} people liked this"
people_dislike_this:
zero: "no people disliked this"
one: "1 person disliked this"
few: "%{count} people disliked this"
many: "%{count} people disliked this"
other: "%{count} people disliked this"
notifications:
request_accepted: "accepted your share request."
new_request: "offered to share with you."
@ -537,6 +553,9 @@ en:
contact_list:
all_contacts: "All contacts"
cannot_remove: "Cannot remove person from last aspect. (If you want to disconnect from this person you must remove contact.)"
stream_element:
like: "I like this"
dislike: "I dislike this"
status_messages:
new:

View file

@ -6,6 +6,8 @@ Diaspora::Application.routes.draw do
resources :status_messages, :only => [:new, :create, :destroy, :show]
resources :comments, :only => [:create]
resources :requests, :only => [:destroy, :create]
match '/likes' => 'likes#create'
resources :likes, :only => [:create]
match 'tags/:name' => 'tags#show'
resources :tags, :only => [:show]

View file

@ -0,0 +1,21 @@
class AddLikes < ActiveRecord::Migration
def self.up
create_table :likes do |t|
t.boolean :positive, :default => true
t.integer :post_id
t.integer :author_id
t.string :guid
t.text :author_signature
t.text :parent_author_signature
t.timestamps
end
add_index :likes, :guid, :unique => true
add_index :likes, :post_id
add_foreign_key(:likes, :posts, :dependant => :delete)
add_foreign_key(:likes, :people, :column => :author_id, :dependant => :delete)
end
def self.down
drop_table :likes
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended to check this file into your version control system.
ActiveRecord::Schema.define(:version => 20110319005509) do
ActiveRecord::Schema.define(:version => 201110319172136) do
create_table "aspect_memberships", :force => true do |t|
t.integer "aspect_id", :null => false
@ -105,6 +105,21 @@ ActiveRecord::Schema.define(:version => 20110319005509) do
add_index "invitations", ["recipient_id"], :name => "index_invitations_on_recipient_id"
add_index "invitations", ["sender_id"], :name => "index_invitations_on_sender_id"
create_table "likes", :force => true do |t|
t.boolean "positive", :default => true
t.integer "post_id"
t.integer "author_id"
t.string "guid"
t.text "author_signature"
t.text "parent_author_signature"
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "likes", ["author_id"], :name => "likes_author_id_fk"
add_index "likes", ["guid"], :name => "index_likes_on_guid", :unique => true
add_index "likes", ["post_id"], :name => "index_likes_on_post_id"
create_table "mentions", :force => true do |t|
t.integer "post_id", :null => false
t.integer "person_id", :null => false
@ -342,6 +357,9 @@ ActiveRecord::Schema.define(:version => 20110319005509) do
add_foreign_key "invitations", "users", :name => "invitations_recipient_id_fk", :column => "recipient_id", :dependent => :delete
add_foreign_key "invitations", "users", :name => "invitations_sender_id_fk", :column => "sender_id", :dependent => :delete
add_foreign_key "likes", "people", :name => "likes_author_id_fk", :column => "author_id"
add_foreign_key "likes", "posts", :name => "likes_post_id_fk"
add_foreign_key "notification_actors", "notifications", :name => "notification_actors_notification_id_fk", :dependent => :delete
add_foreign_key "posts", "people", :name => "posts_author_id_fk", :column => "author_id", :dependent => :delete

Binary file not shown.

After

Width:  |  Height:  |  Size: 493 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 B

View file

@ -39,6 +39,32 @@ var Stream = {
}
});
// like/dislike
$stream.delegate("a.expand_likes", "click", function(evt) {
evt.preventDefault();
$(this).siblings('.likes_list').fadeToggle('fast');
});
$stream.delegate("a.expand_dislikes", "click", function(evt) {
evt.preventDefault();
$(this).siblings('.dislikes_list').fadeToggle('fast');
});
$(".like_it, .dislike_it").live('ajax:loading', function(data, json, xhr) {
$(this).parent().fadeOut('fast');
});
$(".like_it, .dislike_it").live('ajax:success', function(data, json, xhr) {
$(this).parent().detach();
json = $.parseJSON(json);
WebSocketReceiver.processLike(json.post_id, json.html);
});
$('.like_it, .dislike_it').live('ajax:failure', function(data, html, xhr) {
Diaspora.widgets.alert.alert('Failed to like/dislike!');
$(this).parent().fadeIn('fast');
});
// reshare button action
$stream.delegate(".reshare_button", "click", function(evt) {
evt.preventDefault();

View file

@ -36,6 +36,9 @@ var WebSocketReceiver = {
'my_post?': obj['my_post?']
});
} else if (obj['class']=="likes") {
WebSocketReceiver.processLike(obj.post_id, obj.html)
} else {
WebSocketReceiver.processPost(obj['class'], obj.post_id, obj.html, obj.aspect_ids);
}
@ -116,6 +119,11 @@ var WebSocketReceiver = {
Diaspora.widgets.timeago.updateTimeAgo();
},
processLike: function(postId, html) {
var post = $("*[data-guid='"+postId+"']");
$(".likes_container", post).fadeOut('fast').html(html).fadeIn('fast');
},
processPost: function(className, postId, html, aspectIds) {
if(WebSocketReceiver.onPageForAspects(aspectIds)) {
WebSocketReceiver.addPostToStream(postId, html);

View file

@ -563,7 +563,9 @@ header
:align right
ul.comments,
ul.show_comments
ul.show_comments,
div.likes,
div.dislikes
:margin 0
:top 0.5em
:padding 0
@ -2320,7 +2322,9 @@ h3,h4
:position relative
:z-index 10
ul.show_comments
ul.show_comments,
div.likes,
div.dislikes
:margin
:bottom -0.5em
> li
@ -2852,3 +2856,16 @@ h1.tag
.share_with
:background
:color rgb(245,245,245)
.likes_container
.likes,
.dislikes
:border-bottom 1px solid white
a
:padding 1px
:vertical-align middle
:font-size smaller
img
:width 12px
:height 12px
:margin-left 0.5em

View file

@ -61,7 +61,7 @@ describe CommentsController do
it 'posts no comment' do
@user1.should_not_receive(:comment)
post :create, comment_hash
response.code.should == '406'
response.code.should == '422'
end
end
end

View file

@ -0,0 +1,68 @@
# Copyright (c) 2010, Diaspora Inc. This file is
# licensed under the Affero General Public License version 3 or later. See
# the COPYRIGHT file.
require 'spec_helper'
describe LikesController do
render_views
before do
@user1 = alice
@user2 = bob
@aspect1 = @user1.aspects.first
@aspect2 = @user2.aspects.first
sign_in :user, @user1
end
describe '#create' do
let(:like_hash) {
{:positive => 1,
:post_id => "#{@post.id}"}
}
let(:dislike_hash) {
{:positive => 0,
:post_id => "#{@post.id}"}
}
context "on my own post" do
before do
@post = @user1.post :status_message, :text => "AWESOME", :to => @aspect1.id
end
it 'responds to format js' do
post :create, like_hash.merge(:format => 'js')
response.code.should == '201'
end
end
context "on a post from a contact" do
before do
@post = @user2.post :status_message, :text => "AWESOME", :to => @aspect2.id
end
it 'likes' do
post :create, like_hash
response.code.should == '201'
end
it 'dislikes' do
post :create, dislike_hash
response.code.should == '201'
end
it "doesn't post multiple times" do
@user1.like(1, :on => @post)
post :create, dislike_hash
response.code.should == '422'
end
end
context "on a post from a stranger" do
before do
@post = eve.post :status_message, :text => "AWESOME", :to => eve.aspects.first.id
end
it "doesn't post" do
@user1.should_not_receive(:like)
post :create, like_hash
response.code.should == '422'
end
end
end
end

View file

@ -162,9 +162,9 @@ describe PhotosController do
response.code.should == "201"
end
it 'should return a 406 on failure' do
it 'should return a 422 on failure' do
get :make_profile_photo, :photo_id => @bobs_photo.id
response.code.should == "406"
response.code.should == "422"
end
end

80
spec/models/like_spec.rb Normal file
View file

@ -0,0 +1,80 @@
# Copyright (c) 2010, Diaspora Inc. This file is
# licensed under the Affero General Public License version 3 or later. See
# the COPYRIGHT file.
require 'spec_helper'
require File.join(Rails.root, "spec", "shared_behaviors", "relayable")
describe Like do
before do
@alices_aspect = alice.aspects.first
@bobs_aspect = bob.aspects.first
@bob = bob
@eve = eve
@status = alice.post(:status_message, :text => "hello", :to => @alices_aspect.id)
end
describe 'User#like' do
it "should be able to like on one's own status" do
alice.like(1, :on => @status)
@status.reload.likes.first.positive.should == true
end
it "should be able to like on a contact's status" do
bob.like(0, :on => @status)
@status.reload.dislikes.first.positive.should == false
end
it "does not allow multiple likes" do
lambda {
alice.like(1, :on => @status)
alice.like(0, :on => @status)
}.should raise_error
end
end
describe 'xml' do
before do
@liker = Factory.create(:user)
@liker_aspect = @liker.aspects.create(:name => "dummies")
connect_users(alice, @alices_aspect, @liker, @liker_aspect)
@post = alice.post :status_message, :text => "huhu", :to => @alices_aspect.id
@like = @liker.like 0, :on => @post
@xml = @like.to_xml.to_s
end
it 'serializes the sender handle' do
@xml.include?(@liker.diaspora_handle).should be_true
end
it' serializes the post_guid' do
@xml.should include(@post.guid)
end
describe 'marshalling' do
before do
@marshalled_like = Like.from_xml(@xml)
end
it 'marshals the author' do
@marshalled_like.author.should == @liker.person
end
it 'marshals the post' do
@marshalled_like.post.should == @post
end
end
end
describe 'it is relayable' do
before do
@local_luke, @local_leia, @remote_raphael = set_up_friends
@remote_parent = Factory.create(:status_message, :author => @remote_raphael)
@local_parent = @local_luke.post :status_message, :text => "foobar", :to => @local_luke.aspects.first
@object_by_parent_author = @local_luke.like(1, :on => @local_parent)
@object_by_recipient = @local_leia.build_like(1, :on => @local_parent)
@dup_object_by_parent_author = @object_by_parent_author.dup
@object_on_remote_parent = @local_luke.like(0, :on => @remote_parent)
end
it_should_behave_like 'it is relayable'
end
end

View file

@ -37,6 +37,16 @@ class User
end
end
def like(positive, options ={})
fantasy_resque do
l = build_like(positive, options)
if l.save!
Postzord::Dispatch.new(self, l).post
end
l
end
end
def post_at_time(time)
p = self.post(:status_message, :text => 'hi', :to => self.aspects.first)
p.created_at = time