Commit fba3092c authored by Florian Staudacher's avatar Florian Staudacher

* cleanup people_controller#show, add people_controller#stream for json

* introduce new presenters and extend the functionality of the BasePresenter
* add a handlebars template for the profile sidebar, render it everytime we need to update
* introduce a 'aspect_membership:update' global event
parent d00d6981
......@@ -7,6 +7,7 @@ Handlebars.registerHelper('imageUrl', function(path){
});
Handlebars.registerHelper('linkToPerson', function(context, block) {
if( !context ) context = this;
var html = "<a href=\"/people/" + context.guid + "\" class=\"author-name ";
html += Handlebars.helpers.hovercardable(context);
html += "\">";
......@@ -16,6 +17,22 @@ Handlebars.registerHelper('linkToPerson', function(context, block) {
return html
});
// relationship indicator for profile page
Handlebars.registerHelper('sharingBadge', function(person) {
var i18n_scope = 'people.helper.is_not_sharing';
var icon = 'icons-circle';
if( person.is_sharing ) {
i18n_scope = 'people.helper.is_sharing';
icon = 'icons-check_yes_ok';
}
var title = Diaspora.I18n.t(i18n_scope, {name: person.name});
var html = '<div class="sharing_message_container" title="'+title+'" data-placement="bottom">'+
' <div id="sharing_message" class="'+icon+'"></div>'+
'</div>';
return html;
});
// allow hovercards for users that are not the current user.
// returns the html class name used to trigger hovercards.
......@@ -29,15 +46,17 @@ Handlebars.registerHelper('hovercardable', function(person) {
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( _.isUndefined(person.avatar) ) { return }
var avatar = person.avatar || person.profile.avatar;
if( !avatar ) return;
var name = ( person.name ) ? person.name : 'avatar';
size = ( !_.isString(size) ) ? "small" : size;
imageClass = ( !_.isString(imageClass) ) ? size : imageClass;
return _.template('<img src="<%= src %>" class="avatar <%= img_class %>" title="<%= title %>" />', {
'src': person.avatar[size],
return _.template('<img src="<%= src %>" class="avatar <%= img_class %>" title="<%= title %>" alt="<%= title %>" />', {
'src': avatar[size],
'img_class': imageClass,
'title': _.escape(person.name)
'title': _.escape(name)
});
});
......
app.models.Person = Backbone.Model.extend({
urlRoot: '/people',
url: function() {
return this.urlRoot + '/' + this.get('guid');
},
initialize: function() {
if( this.get('profile') )
this.profile = new app.models.Profile(this.get('profile'));
},
isSharing: function() {
var rel = this.get('relationship');
return (rel == 'mutual' || rel == 'sharing');
},
isReceiving: function() {
var rel = this.get('relationship');
return (rel == 'mutual' || rel == 'receiving');
},
isMutual: function() {
return (this.get('relationship') == 'mutual');
},
isBlocked: function() {
return (this.get('relationship') == 'blocked');
},
block: function() {
var self = this;
var block = new app.models.Block({block: {person_id: this.id}});
// return the jqXHR with Promise interface
return block.save()
.done(function() { app.events.trigger('person:block:'+self.id); });
}
});
// TODO: this view should be model-driven an re-render when it was updated,
// instead of changing classes/attributes on elements.
app.pages.Profile = Backbone.View.extend({
// TODO: update the aspect_membership dropdown, too, every time we render the view...
app.pages.Profile = app.views.Base.extend({
events: {
'click #block_user_button': 'blockPerson'
},
subviews: {
'#profile .badge': 'sidebarView'
},
tooltipSelector: '.profile_button div, .sharing_message_container',
initialize: function(opts) {
// cache element references
this.el_profile_btns = this.$('#profile_buttons');
this.el_sharing_msg = this.$('#sharing_message');
if( app.hasPreload('person') )
this.model = new app.models.Person(app.parsePreload('person'));
// init tooltips
this.el_profile_btns.find('.profile_button div, .sharin_message_container')
.tooltip({placement: 'bottom'});
this.model.on('change', this.render, this);
// respond to global events
var person_id = this.$('#profile .avatar:first').data('person_id');
app.events.on('person:block:'+person_id, this._markBlocked, this);
// bind to global events
var id = this.model.get('id');
app.events.on('person:block:'+id, this.reload, this);
app.events.on('aspect_membership:update', this.reload, this);
},
sidebarView: function() {
return new app.views.ProfileSidebar({model: this.model});
},
blockPerson: function(evt) {
if( !confirm(Diaspora.I18n.t('ignore_user')) ) return;
var person_id = $(evt.target).data('person-id');
var block = new app.models.Block({block: {person_id: person_id}});
block.save()
.done(function() { app.events.trigger('person:block:'+person_id); })
.fail(function() { Diaspora.page.flashMessages.render({
var block = this.model.block();
block.fail(function() {
Diaspora.page.flashMessages.render({
success: false,
notice: Diaspora.I18n.t('ignore_failed')
}); });
});
});
return false;
},
_markBlocked: function() {
this.el_profile_btns.attr('class', 'blocked');
this.el_sharing_msg.attr('class', 'icons-circle');
this.el_profile_btns.find('.profile_button, .white_bar').remove();
reload: function() {
this.model.fetch();
}
});
......@@ -54,10 +54,11 @@ app.Router = Backbone.Router.extend({
renderPage : function(pageConstructor){
app.page && app.page.unbind && app.page.unbind(); //old page might mutate global events $(document).keypress, so unbind before creating
app.page = pageConstructor(); //create new page after the world is clean (like that will ever happen)
app.page.render();
if( !$.contains(document, app.page.el) ) {
// view element isn't already attached to the DOM, insert it
$("#container").empty().append(app.page.render().el);
$("#container").empty().append(app.page.el);
}
},
......
......@@ -38,6 +38,7 @@ app.views.Base = Backbone.View.extend({
this.template = HandlebarsTemplates[this.templateName+"_tpl"]
if(!this.template) {
console.log(this.templateName ? ("no template for " + this.templateName) : "no templateName specified")
return;
}
this.$el
......
......@@ -23,6 +23,7 @@ app.views.AspectMembershipBlueprint = Backbone.View.extend({
// -> addMembership
// -> removeMembership
_clickHandler: function(evt) {
var promise = null;
this.list_item = $(evt.target);
this.dropdown = this.list_item.parent();
......@@ -30,13 +31,18 @@ app.views.AspectMembershipBlueprint = Backbone.View.extend({
if( this.list_item.is('.selected') ) {
var membership_id = this.list_item.data('membership_id');
this.removeMembership(membership_id);
promise = this.removeMembership(membership_id);
} else {
var aspect_id = this.list_item.data('aspect_id');
var person_id = this.dropdown.data('person_id');
this.addMembership(person_id, aspect_id);
promise = this.addMembership(person_id, aspect_id);
}
promise && promise.always(function() {
// trigger a global event
app.events.trigger('aspect_membership:update');
});
return false; // stop the event
},
......@@ -57,7 +63,7 @@ app.views.AspectMembershipBlueprint = Backbone.View.extend({
this._displayError('aspect_dropdown.error');
}, this);
aspect_membership.save();
return aspect_membership.save();
},
_successSaveCb: function(aspect_membership) {
......@@ -100,7 +106,7 @@ app.views.AspectMembershipBlueprint = Backbone.View.extend({
this._displayError('aspect_dropdown.error_remove');
}, this);
aspect_membership.destroy();
return aspect_membership.destroy();
},
_successDestroyCb: function(aspect_membership) {
......
......@@ -23,6 +23,7 @@ app.views.AspectMembership = app.views.AspectsDropdown.extend({
// -> addMembership
// -> removeMembership
_clickHandler: function(evt) {
var promise = null;
this.list_item = $(evt.target).closest('li.aspect_selector');
this.dropdown = this.list_item.parent();
......@@ -30,13 +31,18 @@ app.views.AspectMembership = app.views.AspectsDropdown.extend({
if( this.list_item.is('.selected') ) {
var membership_id = this.list_item.data('membership_id');
this.removeMembership(membership_id);
promise = this.removeMembership(membership_id);
} else {
var aspect_id = this.list_item.data('aspect_id');
var person_id = this.dropdown.data('person_id');
this.addMembership(person_id, aspect_id);
promise = this.addMembership(person_id, aspect_id);
}
promise && promise.always(function() {
// trigger a global event
app.events.trigger('aspect_membership:update');
});
return false; // stop the event
},
......@@ -57,7 +63,7 @@ app.views.AspectMembership = app.views.AspectsDropdown.extend({
this._displayError('aspect_dropdown.error');
}, this);
aspect_membership.save();
return aspect_membership.save();
},
_successSaveCb: function(aspect_membership) {
......@@ -99,7 +105,7 @@ app.views.AspectMembership = app.views.AspectsDropdown.extend({
this._displayError('aspect_dropdown.error_remove');
}, this);
aspect_membership.destroy();
return aspect_membership.destroy();
},
_successDestroyCb: function(aspect_membership) {
......
app.views.ProfileSidebar = app.views.Base.extend({
templateName: 'profile_sidebar',
presenter: function() {
return _.extend({}, this.defaultPresenter(), {
do_profile_btns: this._shouldDoProfileBtns(),
is_sharing: this.model.isSharing(),
is_receiving: this.model.isReceiving(),
is_mutual: this.model.isMutual(),
is_not_blocked: !this.model.isBlocked()
});
},
_shouldDoProfileBtns: function() {
return (app.currentUser.authenticated() && !this.model.get('is_own_profile'));
},
});
......@@ -44,7 +44,7 @@
width: 50px;
}
}
.only_sharing {
.sharing {
background-color: rgb(142, 222, 61);
.profile_button {
width: 150px;
......@@ -77,7 +77,7 @@
background-color: white;
@include border-bottom-left-radius(8px);
}
.profile_button {
display: inline-block;
text-align: center;
......@@ -85,13 +85,13 @@
a { @include opacity(0.5); }
a:hover { @include opacity(1); }
.icons-check_yes_ok {
.icons-check_yes_ok {
display: inline-block;
height: 18px;
width: 18px;
}
.icons-circle {
.icons-circle {
display: inline-block;
height: 18px;
width: 18px;
......
<div class="profile_photo">
{{#linkToPerson this}}
{{{personImage this "l"}}}
{{/linkToPerson}}
</div>
{{#if do_profile_btns}}
<div id="profile_buttons" class="{{relationship}}">
{{{sharingBadge this}}}
{{#if is_receiving}}
<div class="profile_button">
<a href="" rel="facebox">
<div id="mention_button" class="icons-mention" title="{{t 'people.mention'}}" data-placement="bottom"></div>
</a>
</div>
{{/if}}
{{#if is_mutual}}
<div class="profile_button">
<a href="" rel="facebox">
<div id="message_button" class="icons-message" title="{{t 'people.message'}}" data-placement="bottom"></div>
</a>
</div>
{{/if}}
{{#if is_not_blocked}}
<div class="profile_button">
<a href="#" rel="nofollow">
<div id="block_user_button" class="icons-ignoreuser block_user" title="{{t 'ignore'}}" data-placement="bottom"></div>
</a>
</div>
{{/if}}
</div>
{{/if}}
......@@ -76,36 +76,51 @@ class PeopleController < ApplicationController
def show
@person = Person.find_from_guid_or_username(params)
# view this profile on the home pod, if you don't want to sign in...
authenticate_user! if remote_profile_with_no_user_session?
raise Diaspora::AccountClosed if @person.closed_account?
mark_corresponding_notifications_read if user_signed_in?
@post_type = :all
@aspect = :profile
@stream = Stream::Person.new(current_user, @person, :max_time => max_time)
@profile = @person.profile
@photos = photos_from(@person)
unless params[:format] == "json" # hovercard
if current_user
@block = current_user.blocks.where(:person_id => @person.id).first
@contact = current_user.contact_for(@person)
if @contact && !params[:only_posts]
@contacts_of_contact_count = @contact.contacts.count(:all)
@contacts_of_contact = @contact.contacts.limit(8)
else
@contact ||= Contact.new
end
end
end
@aspect = :profile # what does this do?
@post_type = :all # for mobile
@person_json = PersonPresenter.new(@person, current_user).full_hash_with_profile
respond_to do |format|
format.all do
@profile = @person.profile
@photos = photos_from(@person)
if current_user
@block = current_user.blocks.where(:person_id => @person.id).first
@contact = current_user.contact_for(@person)
if @contact && !params[:only_posts]
@contacts_of_contact_count = @contact.contacts.count(:all)
@contacts_of_contact = @contact.contacts.limit(8)
else
@contact ||= Contact.new
end
end
gon.preloads[:person] = @person_json
respond_with @person, :locals => {:post_type => :all}
end
format.json { render :json => @stream.stream_posts.map { |p| LastThreeCommentsDecorator.new(PostPresenter.new(p, current_user)) }}
format.json { render :json => @person_json }
end
end
def stream
@person = Person.find_from_guid_or_username(params)
authenticate_user! if remote_profile_with_no_user_session?
raise Diaspora::AccountClosed if @person.closed_account?
respond_to do |format|
format.all { redirect_to person_path(@person) }
format.json do
@stream = Stream::Person.new(current_user, @person, max_time: max_time)
render json: @stream.stream_posts.map { |p| LastThreeCommentsDecorator.new(PostPresenter.new(p, current_user)) }
end
end
end
......
......@@ -14,7 +14,7 @@ module PeopleHelper
end
end
end
def birthday_format(bday)
if bday.year == 1000
I18n.l bday, :format => I18n.t('date.formats.birthday')
......@@ -96,7 +96,7 @@ module PeopleHelper
elsif contact.mutual?
'mutual'
elsif contact.sharing?
'only_sharing'
'sharing'
elsif contact.receiving?
'receiving'
else
......
class AvatarPresenter < BasePresenter
DEFAULT_IMAGE = ActionController::Base.helpers.image_path('user/default.png')
def base_hash
{ s: image_url_small || DEFAULT_IMAGE,
m: image_url_medium || DEFAULT_IMAGE,
l: image_url || DEFAULT_IMAGE
}
end
end
class BasePresenter
def self.as_collection(collection)
collection.map{|object| self.new(object).as_json}
attr_reader :current_user
class << self
def new(*args)
return NilPresenter.new if args[0].nil?
super *args
end
def as_collection(collection, method=:as_json, *args)
collection.map{|object| self.new(object, *args).send(method) }
end
end
def initialize(presentable, curr_user=nil)
@presentable = presentable
@current_user = curr_user
end
def method_missing(method, *args)
@presentable.send(method, *args) if @presentable.respond_to?(method)
end
class NilPresenter
def method_missing(method, *args)
nil
end
end
end
class PersonPresenter
def initialize(person, current_user = nil)
@person = person
@current_user = current_user
class PersonPresenter < BasePresenter
def base_hash
{ id: id,
guid: guid,
name: name,
diaspora_id: diaspora_handle
}
end
def full_hash
base_hash.merge({
relationship: relationship,
is_own_profile: own_profile?
})
end
def full_hash_with_avatar
full_hash.merge({avatar: AvatarPresenter.new(profile).base_hash})
end
def full_hash_with_profile
full_hash.merge({profile: ProfilePresenter.new(profile).full_hash})
end
def as_json(options={})
attrs = @person.as_api_response(:backbone).merge(
{
:is_own_profile => is_own_profile
})
attrs = full_hash_with_avatar
if is_own_profile || person_is_following_current_user
if own_profile? || person_is_following_current_user
attrs.merge!({
:location => @person.location,
:birthday => @person.formatted_birthday,
:bio => @person.bio
:location => @presentable.location,
:birthday => @presentable.formatted_birthday,
:bio => @presentable.bio
})
end
attrs
end
def is_own_profile
@current_user.try(:person) == @person
protected
def own_profile?
current_user.try(:person) == @presentable
end
protected
def relationship
contact = current_user.contact_for(@presentable)
is_blocked = current_user.blocks.where(person_id: id).limit(1).any?
is_mutual = contact ? contact.mutual? : false
is_sharing = contact ? contact.sharing? : false
is_receiving = contact ? contact.receiving? : false
if is_blocked then :blocked
elsif is_mutual then :mutual
elsif is_sharing then :sharing
elsif is_receiving then :receiving
else :not_sharing
end
end
def person_is_following_current_user
@person.shares_with(@current_user)
@presentable.shares_with(@current_user)
end
end
class ProfilePresenter < BasePresenter
def base_hash
{ id: id,
tags: tag_string,
bio: bio,
location: location,
gender: gender,
birthday: formatted_birthday,