diff --git a/app/assets/javascripts/app/helpers/handlebars-helpers.js b/app/assets/javascripts/app/helpers/handlebars-helpers.js index 617db4cb0..fd8bd0657 100644 --- a/app/assets/javascripts/app/helpers/handlebars-helpers.js +++ b/app/assets/javascripts/app/helpers/handlebars-helpers.js @@ -7,6 +7,7 @@ Handlebars.registerHelper('imageUrl', function(path){ }); Handlebars.registerHelper('linkToPerson', function(context, block) { + if( !context ) context = this; var 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 = '
'+ + '
'+ + '
'; + 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('', { - 'src': person.avatar[size], + return _.template('<%= title %>', { + 'src': avatar[size], 'img_class': imageClass, - 'title': _.escape(person.name) + 'title': _.escape(name) }); }); diff --git a/app/assets/javascripts/app/models/person.js b/app/assets/javascripts/app/models/person.js new file mode 100644 index 000000000..cae208afa --- /dev/null +++ b/app/assets/javascripts/app/models/person.js @@ -0,0 +1,39 @@ +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); }); + } +}); diff --git a/app/assets/javascripts/app/pages/profile.js b/app/assets/javascripts/app/pages/profile.js index a47a4542c..f856d2d48 100644 --- a/app/assets/javascripts/app/pages/profile.js +++ b/app/assets/javascripts/app/pages/profile.js @@ -1,44 +1,47 @@ -// 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(); } }); diff --git a/app/assets/javascripts/app/router.js b/app/assets/javascripts/app/router.js index 21d90707d..706eecf1a 100644 --- a/app/assets/javascripts/app/router.js +++ b/app/assets/javascripts/app/router.js @@ -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); } }, diff --git a/app/assets/javascripts/app/views.js b/app/assets/javascripts/app/views.js index 80c40b3a5..ea61a1a8d 100644 --- a/app/assets/javascripts/app/views.js +++ b/app/assets/javascripts/app/views.js @@ -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 diff --git a/app/assets/javascripts/app/views/aspect_membership_blueprint_view.js b/app/assets/javascripts/app/views/aspect_membership_blueprint_view.js index 985dd93cc..40b45d312 100644 --- a/app/assets/javascripts/app/views/aspect_membership_blueprint_view.js +++ b/app/assets/javascripts/app/views/aspect_membership_blueprint_view.js @@ -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) { diff --git a/app/assets/javascripts/app/views/aspect_membership_view.js b/app/assets/javascripts/app/views/aspect_membership_view.js index b299fcb00..85b2391e6 100644 --- a/app/assets/javascripts/app/views/aspect_membership_view.js +++ b/app/assets/javascripts/app/views/aspect_membership_view.js @@ -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) { diff --git a/app/assets/javascripts/app/views/profile_sidebar_view.js b/app/assets/javascripts/app/views/profile_sidebar_view.js new file mode 100644 index 000000000..ea318d2f4 --- /dev/null +++ b/app/assets/javascripts/app/views/profile_sidebar_view.js @@ -0,0 +1,18 @@ + +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')); + }, +}); diff --git a/app/assets/stylesheets/profile.css.scss b/app/assets/stylesheets/profile.css.scss index a826ca2ba..de1dfe8cc 100644 --- a/app/assets/stylesheets/profile.css.scss +++ b/app/assets/stylesheets/profile.css.scss @@ -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; diff --git a/app/assets/templates/profile_sidebar_tpl.jst.hbs b/app/assets/templates/profile_sidebar_tpl.jst.hbs new file mode 100644 index 000000000..70240e169 --- /dev/null +++ b/app/assets/templates/profile_sidebar_tpl.jst.hbs @@ -0,0 +1,36 @@ + +
+ {{#linkToPerson this}} + {{{personImage this "l"}}} + {{/linkToPerson}} +
+ +{{#if do_profile_btns}} +
+ {{{sharingBadge this}}} + + {{#if is_receiving}} +
+ +
+
+
+ {{/if}} + + {{#if is_mutual}} +
+ +
+
+
+ {{/if}} + + {{#if is_not_blocked}} +
+ +
+
+
+ {{/if}} +
+{{/if}} diff --git a/app/controllers/people_controller.rb b/app/controllers/people_controller.rb index f160c5992..59f8c5862 100644 --- a/app/controllers/people_controller.rb +++ b/app/controllers/people_controller.rb @@ -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 diff --git a/app/helpers/people_helper.rb b/app/helpers/people_helper.rb index fd426f9f6..75a63cd88 100644 --- a/app/helpers/people_helper.rb +++ b/app/helpers/people_helper.rb @@ -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 diff --git a/app/presenters/avatar_presenter.rb b/app/presenters/avatar_presenter.rb new file mode 100644 index 000000000..10cabb949 --- /dev/null +++ b/app/presenters/avatar_presenter.rb @@ -0,0 +1,12 @@ + +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 diff --git a/app/presenters/base_presenter.rb b/app/presenters/base_presenter.rb index 8f83962e9..86845a8b4 100644 --- a/app/presenters/base_presenter.rb +++ b/app/presenters/base_presenter.rb @@ -1,5 +1,29 @@ 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 diff --git a/app/presenters/person_presenter.rb b/app/presenters/person_presenter.rb index 7b635125a..46b3839f1 100644 --- a/app/presenters/person_presenter.rb +++ b/app/presenters/person_presenter.rb @@ -1,33 +1,64 @@ -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 - end - protected + def own_profile? + current_user.try(:person) == @presentable + end + + 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 diff --git a/app/presenters/profile_presenter.rb b/app/presenters/profile_presenter.rb new file mode 100644 index 000000000..8f916431a --- /dev/null +++ b/app/presenters/profile_presenter.rb @@ -0,0 +1,18 @@ +class ProfilePresenter < BasePresenter + def base_hash + { id: id, + tags: tag_string, + bio: bio, + location: location, + gender: gender, + birthday: formatted_birthday, + searchable: searchable + } + end + + def full_hash + base_hash.merge({ + avatar: AvatarPresenter.new(@presentable).base_hash, + }) + end +end diff --git a/app/views/people/_profile_sidebar.html.haml b/app/views/people/_profile_sidebar.html.haml index 46a43376a..d84c79c1c 100644 --- a/app/views/people/_profile_sidebar.html.haml +++ b/app/views/people/_profile_sidebar.html.haml @@ -3,31 +3,29 @@ -# the COPYRIGHT file. #profile - .profile_photo - = person_image_link(person, :size => :thumb_large, :to => :photos) + .badge + .profile_photo + = person_image_link(person, :size => :thumb_large, :to => :photos) - - if user_signed_in? - - if person != current_user.person - %div#profile_buttons{ :class => profile_buttons_class(@contact, @block) } - = sharing_message(@person, @contact) + - if user_signed_in? + - if person != current_user.person + %div#profile_buttons{ :class => profile_buttons_class(@contact, @block) } + = sharing_message(@person, @contact) - - if @contact.receiving? - .profile_button - = link_to content_tag(:div, nil, :class => 'icons-mention', :title => t('people.show.mention'), :id => 'mention_button'), new_status_message_path(:person_id => @person.id), :rel => 'facebox' - .white_bar + - if @contact.receiving? + .profile_button + = link_to content_tag(:div, nil, :class => 'icons-mention', :title => t('people.show.mention'), :id => 'mention_button'), new_status_message_path(:person_id => @person.id), :rel => 'facebox' + .white_bar - - if @contact.mutual? + - if @contact.mutual? + .profile_button + = link_to content_tag(:div, nil, :class => 'icons-message', :title => t('people.show.message'), :id => 'message_button'), new_conversation_path(:contact_id => @contact.id, :name => @contact.person.name, :facebox => true), :rel => 'facebox' + .white_bar + .profile_button - = link_to content_tag(:div, nil, :class => 'icons-message', :title => t('people.show.message'), :id => 'message_button'), new_conversation_path(:contact_id => @contact.id, :name => @contact.person.name, :facebox => true), :rel => 'facebox' - .white_bar - - .profile_button - = link_to content_tag(:div, nil, :class => 'icons-ignoreuser block_user', :title => t('ignore'), :id => 'block_user_button', :data => { :person_id => @person.id }), '#', :rel => "nofollow" if @block.blank? - - - %br + = link_to content_tag(:div, nil, :class => 'icons-ignoreuser block_user', :title => t('ignore'), :id => 'block_user_button', :data => { :person_id => @person.id }), '#', :rel => "nofollow" if @block.blank? -if contact.sharing? || person == current_user.person %ul#profile_information diff --git a/config/locales/javascript/javascript.en.yml b/config/locales/javascript/javascript.en.yml index 6cd153fd8..9adceca69 100644 --- a/config/locales/javascript/javascript.en.yml +++ b/config/locales/javascript/javascript.en.yml @@ -115,6 +115,11 @@ en: wasnt_that_interesting: "OK, I suppose #<%= tagName %> wasn't all that interesting..." people: not_found: "and no one was found..." + mention: "Mention" + message: "Message" + helper: + is_sharing: "<%= name %> is sharing with you" + is_not_sharing: "<%= name %> is not sharing with you" conversation: participants: "Participants" diff --git a/config/routes.rb b/config/routes.rb index e32770a80..dbb06bde4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -159,6 +159,7 @@ Diaspora::Application.routes.draw do resources :photos get :contacts get "aspect_membership_button" => :aspect_membership_dropdown, :as => "aspect_membership_button" + get :stream get :hovercard member do