diff --git a/Changelog.md b/Changelog.md
index aabca0644c610da7ac9d6b5609fbed38967e5969..4eab24864603708cf422b7d375974d4cd3f0b9d4 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -84,6 +84,7 @@ diaspora.yml file**. The existing settings from 0.4.x and before will not work a
 * Make sure conversations without any visibilities left are deleted [#5478](https://github.com/diaspora/diaspora/pull/5478)
 * Change tooltip for delete button in conversations view [#5477](https://github.com/diaspora/diaspora/pull/5477)
 * Replace a modifier-rescue with a specific rescue [#5491](https://github.com/diaspora/diaspora/pull/5491)
+* Port contacts page to backbone [#5473](https://github.com/diaspora/diaspora/pull/5473)
 
 ## Bug fixes
 * orca cannot see 'Add Contact' button [#5158](https://github.com/diaspora/diaspora/pull/5158)
diff --git a/app/assets/javascripts/app/collections/aspect_memberships.js b/app/assets/javascripts/app/collections/aspect_memberships.js
new file mode 100644
index 0000000000000000000000000000000000000000..dd8ee1b5d2297e961f662cf7f7d44cab0c43355d
--- /dev/null
+++ b/app/assets/javascripts/app/collections/aspect_memberships.js
@@ -0,0 +1,6 @@
+// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
+
+app.collections.AspectMemberships = Backbone.Collection.extend({
+  model: app.models.AspectMembership
+})
+// @license-end
diff --git a/app/assets/javascripts/app/collections/contacts.js b/app/assets/javascripts/app/collections/contacts.js
new file mode 100644
index 0000000000000000000000000000000000000000..d0592155f29c9373ba5ff0a86874e488bc223dbc
--- /dev/null
+++ b/app/assets/javascripts/app/collections/contacts.js
@@ -0,0 +1,21 @@
+// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
+
+app.collections.Contacts = Backbone.Collection.extend({
+  model: app.models.Contact,
+
+  comparator : function(con1, con2) {
+    if( !con1.person || !con2.person ) return 1;
+
+    if(app.aspect) {
+      var inAspect1 = con1.inAspect(app.aspect.get('id'));
+      var inAspect2 = con2.inAspect(app.aspect.get('id'));
+      if(  inAspect1 && !inAspect2 ) return -1;
+      if( !inAspect1 &&  inAspect2 ) return 1;
+    }
+
+    var n1 = con1.person.get('name');
+    var n2 = con2.person.get('name');
+    return n1.localeCompare(n2);
+  }
+});
+// @license-end
diff --git a/app/assets/javascripts/app/helpers/handlebars-helpers.js b/app/assets/javascripts/app/helpers/handlebars-helpers.js
index 00c78ffaff8438aa1a673c9edccaffadfd8dce0a..17f42e3c2a639b752fc7571a32340cf0aa076023 100644
--- a/app/assets/javascripts/app/helpers/handlebars-helpers.js
+++ b/app/assets/javascripts/app/helpers/handlebars-helpers.js
@@ -21,7 +21,7 @@ Handlebars.registerHelper('urlTo', function(path_helper, id, data){
   return Routes[path_helper+'_path'](id, data.hash);
 });
 
-Handlebars.registerHelper('linkToPerson', function(context, block) {
+Handlebars.registerHelper('linkToAuthor', function(context, block) {
   if( !context ) context = this;
   var html = "<a href=\"/people/" + context.guid + "\" class=\"author-name ";
       html += Handlebars.helpers.hovercardable(context);
@@ -32,6 +32,15 @@ Handlebars.registerHelper('linkToPerson', function(context, block) {
   return html
 });
 
+Handlebars.registerHelper('linkToPerson', function(context, block) {
+  if( !context ) context = this;
+  var html = "<a href=\"/people/" + context.guid + "\" class=\"name\">";
+      html += block.fn(context);
+      html += "</a>";
+
+  return html
+});
+
 // relationship indicator for profile page
 Handlebars.registerHelper('sharingMessage', function(person) {
   var i18n_scope = 'people.helper.is_not_sharing';
@@ -107,5 +116,20 @@ Handlebars.registerHelper('isCurrentProfilePage', function(id, diaspora_handle,
   return Handlebars.helpers.isCurrentPage('person', id, options) ||
          Handlebars.helpers.isCurrentPage('user_profile', username, options);
 });
+
+Handlebars.registerHelper('aspectMembershipIndicator', function(contact,in_aspect) {
+  if(!app.aspect || !app.aspect.get('id')) return '<div class="aspect_membership_dropdown placeholder"></div>';
+
+  var html = '<i class="entypo ';
+  if( in_aspect == 'in_aspect' ) {
+    html += 'circled-cross contact_remove-from-aspect" ';
+    html += 'title="' + Diaspora.I18n.t('contacts.remove_contact') + '" ';
+  } else {
+    html += 'circled-plus contact_add-to-aspect" ';
+    html += 'title="' + Diaspora.I18n.t('contacts.add_contact') + '" ';
+  }
+  html += '></i>';
+  return html;
+});
 // @license-end
 
diff --git a/app/assets/javascripts/app/models/contact.js b/app/assets/javascripts/app/models/contact.js
new file mode 100644
index 0000000000000000000000000000000000000000..a1e014c4c1340ebc20d38cdd7e51bc7dd838f76f
--- /dev/null
+++ b/app/assets/javascripts/app/models/contact.js
@@ -0,0 +1,13 @@
+// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
+
+app.models.Contact = Backbone.Model.extend({
+  initialize : function() {
+    this.aspect_memberships = new app.collections.AspectMemberships(this.get('aspect_memberships'));
+    if( this.get('person') ) this.person = new app.models.Person(this.get('person'));
+  },
+
+  inAspect : function(id) {
+    return this.aspect_memberships.any(function(membership){ return membership.get('aspect').id == id; });
+  }
+});
+// @license-end
diff --git a/app/assets/javascripts/app/views/contacts_view.js b/app/assets/javascripts/app/pages/contacts.js
similarity index 50%
rename from app/assets/javascripts/app/views/contacts_view.js
rename to app/assets/javascripts/app/pages/contacts.js
index 970719247e14825fb916792e349183204ccd640f..ac44783944f0fa7e59b0bf083f0751a403d623fa 100644
--- a/app/assets/javascripts/app/views/contacts_view.js
+++ b/app/assets/javascripts/app/pages/contacts.js
@@ -1,6 +1,6 @@
 // @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
 
-app.views.Contacts = Backbone.View.extend({
+app.pages.Contacts = Backbone.View.extend({
 
   el: "#contacts_container",
 
@@ -8,17 +8,15 @@ app.views.Contacts = Backbone.View.extend({
     "click #contacts_visibility_toggle" : "toggleContactVisibility",
     "click #chat_privilege_toggle" : "toggleChatPrivilege",
     "click #change_aspect_name" : "showAspectNameForm",
-    "click .contact_remove-from-aspect" : "removeContactFromAspect",
-    "click .contact_add-to-aspect" : "addContactToAspect",
     "keyup #contact_list_search" : "searchContactList"
   },
 
-  initialize: function() {
+  initialize: function(opts) {
     this.visibility_toggle = $("#contacts_visibility_toggle .entypo");
     this.chat_toggle = $("#chat_privilege_toggle .entypo");
+    this.stream = opts.stream;
+    this.stream.render();
     $("#people_stream.contacts .header .entypo").tooltip({ 'placement': 'bottom'});
-    $(".contact_remove-from-aspect").tooltip();
-    $(".contact_add-to-aspect").tooltip();
     $(document).on('ajax:success', 'form.edit_aspect', this.updateAspectName);
   },
 
@@ -69,65 +67,8 @@ app.views.Contacts = Backbone.View.extend({
     $(".header > h3").show();
   },
 
-  addContactToAspect: function(e){
-    var contact = $(e.currentTarget);
-    var aspect_membership = new app.models.AspectMembership({
-      'person_id': contact.attr('data-person_id'),
-      'aspect_id': contact.attr('data-aspect_id')
-    });
-
-    aspect_membership.save({},{
-      success: function(model,response){
-        contact.attr('data-membership_id',model.id)
-               .tooltip('destroy')
-               .removeAttr('data-original-title')
-               .removeClass("contact_add-to-aspect").removeClass("circled-plus")
-               .addClass("contact_remove-from-aspect").addClass("circled-cross")
-               .attr('title', Diaspora.I18n.t('contacts.remove_contact'))
-               .tooltip()
-               .closest('.stream_element').addClass('in_aspect');
-      },
-      error: function(model,response){
-        var msg = Diaspora.I18n.t('contacts.error_add', { 'name':contact.closest('.stream_element').find('.name').text() });
-        Diaspora.page.flashMessages.render({ 'success':false, 'notice':msg });
-      }
-    });
-  },
-
-  removeContactFromAspect: function(e){
-    var contact = $(e.currentTarget);
-    var aspect_membership = new app.models.AspectMembership({
-      'id': contact.attr('data-membership_id')
-    });
-
-    aspect_membership.destroy({
-      success: function(model,response){
-        contact.removeAttr('data-membership_id')
-               .tooltip('destroy')
-               .removeAttr('data-original-title')
-               .removeClass("contact_remove-from-aspect").removeClass("circled-cross")
-               .addClass("contact_add-to-aspect").addClass("circled-plus")
-               .attr('title', Diaspora.I18n.t('contacts.add_contact'))
-               .tooltip()
-               .closest('.stream_element').removeClass('in_aspect');
-      },
-      error: function(model,response){
-        var msg = Diaspora.I18n.t('contacts.error_remove', { 'name':contact.closest('.stream_element').find('.name').text() });
-        Diaspora.page.flashMessages.render({ 'success':false, 'notice':msg });
-      }
-    });
-  },
-
   searchContactList: function(e) {
-    var query = new RegExp($(e.target).val(),'i');
-
-    $("#people_stream.stream.contacts .stream_element").each(function(){
-      if($(this).find(".name").text().match(query)){
-        $(this).show();
-      } else {
-        $(this).hide();
-      }
-    });
+    this.stream.search($(e.target).val());
   }
 });
 // @license-end
diff --git a/app/assets/javascripts/app/router.js b/app/assets/javascripts/app/router.js
index c71222ff8bdddcc513052c46b9492f03894dc7c3..76398707cd5105fa2e1a61bab7c65fa1cf1499fa 100644
--- a/app/assets/javascripts/app/router.js
+++ b/app/assets/javascripts/app/router.js
@@ -43,7 +43,15 @@ app.Router = Backbone.Router.extend({
   },
 
   contacts: function() {
-    app.contacts = new app.views.Contacts();
+    app.aspect = new app.models.Aspect(gon.preloads.aspect);
+    app.contacts = new app.collections.Contacts(app.parsePreload('contacts'));
+
+    var stream = new app.views.ContactStream({
+      collection: app.contacts,
+      el: $('.stream.contacts #contact_stream'),
+    });
+
+    app.page = new app.pages.Contacts({stream: stream});
   },
 
   conversations: function() {
diff --git a/app/assets/javascripts/app/views/contact_stream_view.js b/app/assets/javascripts/app/views/contact_stream_view.js
new file mode 100644
index 0000000000000000000000000000000000000000..ad93301bd809be1ff5c92f97dee0d5e0ebd022cf
--- /dev/null
+++ b/app/assets/javascripts/app/views/contact_stream_view.js
@@ -0,0 +1,77 @@
+// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
+
+app.views.ContactStream = Backbone.View.extend({
+  initialize: function() {
+    this.itemCount = 0;
+    this.perPage = 25;
+    this.query = '';
+    this.resultList = this.collection.toArray();
+    var throttledScroll = _.throttle(_.bind(this.infScroll, this), 200);
+    $(window).scroll(throttledScroll);
+    this.on('renderContacts', this.renderContacts, this);
+  },
+
+  render: function() {
+    if( _.isEmpty(this.resultList) ) {
+      var content = document.createDocumentFragment();
+      content = '<div id="no_contacts" class="well">' +
+                '  <h4>' +
+                     Diaspora.I18n.t('contacts.search_no_results') +
+                '  </h4>' +
+                '</div>';
+      this.$el.html(content);
+    } else {
+      this.$el.html('');
+      this.renderContacts();
+    }
+  },
+
+  renderContacts: function() {
+    this.$el.addClass("loading");
+    var content = document.createDocumentFragment();
+    _.rest(_.first(this.resultList , this.itemCount + this.perPage), this.itemCount).forEach( function(item) {
+      var view = new app.views.Contact({model: item});
+      content.appendChild(view.render().el);
+    });
+
+    var size = _.size(this.resultList);
+    if( this.itemCount + this.perPage >= size ){
+      this.itemCount = size;
+      this.off('renderContacts');
+    } else {
+      this.itemCount += this.perPage;
+    }
+    this.$el.append(content);
+    this.$el.removeClass("loading");
+  },
+
+  search: function(query) {
+    query = query.trim();
+    if( query || this.query ) {
+      this.off('renderContacts');
+      this.on('renderContacts', this.renderContacts, this);
+      this.itemCount = 0;
+      if( query ) {
+        this.query = query;
+        var regex = new RegExp(query,'i');
+        this.resultList = this.collection.filter(function(contact) {
+          return regex.test(contact.get('person').name) ||
+                 regex.test(contact.get('person').diaspora_id);
+        });
+      } else {
+        this.resultList = this.collection.toArray();
+        this.query = '';
+      }
+      this.render();
+    }
+  },
+
+  infScroll: function() {
+    if( this.$el.hasClass('loading') ) return;
+
+    var distanceTop = $(window).height() + $(window).scrollTop(),
+        distanceBottom = $(document).height() - distanceTop;
+    if(distanceBottom < 300) this.trigger('renderContacts');
+  }
+});
+// @license-end
diff --git a/app/assets/javascripts/app/views/contact_view.js b/app/assets/javascripts/app/views/contact_view.js
new file mode 100644
index 0000000000000000000000000000000000000000..4fd2d7efb5b024d704db8fccdbf7e81fd004430f
--- /dev/null
+++ b/app/assets/javascripts/app/views/contact_view.js
@@ -0,0 +1,72 @@
+// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
+
+app.views.Contact = app.views.Base.extend({
+  templateName: 'contact',
+
+  events: {
+    "click .contact_add-to-aspect" : "addContactToAspect",
+    "click .contact_remove-from-aspect" : "removeContactFromAspect"
+  },
+
+  tooltipSelector: '.contact_add-to-aspect, .contact_remove-from-aspect',
+
+  presenter: function() {
+    return _.extend(this.defaultPresenter(), {
+      person_id : this.model.get('person_id'),
+      person : this.model.get('person'),
+      in_aspect: (app.aspect && this.model.inAspect(app.aspect.get('id'))) ? 'in_aspect' : '',
+    });
+  },
+
+  postRenderTemplate: function() {
+    var self = this;
+    var dropdownEl = this.$('.aspect_membership_dropdown.placeholder');
+    if( dropdownEl.length == 0 ) {
+      return;
+    }
+
+    // TODO render me client side!!!
+    var href = this.model.person.url() + '/aspect_membership_button?size=small';
+    if( gon.bootstrap ) href += '&bootstrap=true';
+
+    $.get(href, function(resp) {
+      dropdownEl.html(resp);
+      new app.views.AspectMembership({el: $('.aspect_dropdown',dropdownEl)});
+
+      // UGLY (re-)attach the facebox
+      self.$('a[rel*=facebox]').facebox();
+    });
+  },
+
+  addContactToAspect: function(){
+    var self = this;
+    this.model.aspect_memberships.create({
+      'person_id': this.model.get('person_id'),
+      'aspect_id': app.aspect.get('id')
+    },{
+      success: function(model,response){
+        self.render();
+      },
+      error: function(model,response){
+        var msg = Diaspora.I18n.t('contacts.error_add', { 'name': self.model.get('person').name });
+        Diaspora.page.flashMessages.render({ 'success':false, 'notice':msg });
+      }
+    });
+  },
+
+  removeContactFromAspect: function(){
+    var self = this;
+    this.model.aspect_memberships
+      .find(function(membership){ return membership.get('aspect').id == app.aspect.id; })
+      .destroy({
+        success: function(model,response){
+          self.render();
+        },
+        error: function(model,response){
+          var msg = Diaspora.I18n.t('contacts.error_remove', { 'name': self.model.get('person').name });
+          Diaspora.page.flashMessages.render({ 'success':false, 'notice':msg });
+        }
+      });
+  }
+});
+// @license-end
diff --git a/app/assets/stylesheets/contacts.css.scss b/app/assets/stylesheets/contacts.css.scss
index 5bf2597a631c8ae92234ff6b06c390557de3eab1..b2eae53054c4941847d1a5d2d43f08a2f726881c 100644
--- a/app/assets/stylesheets/contacts.css.scss
+++ b/app/assets/stylesheets/contacts.css.scss
@@ -66,8 +66,6 @@
 }
 
 #no_contacts {
+  margin-top: 20px;
   text-align: center;
-  padding: 10px;
-  background-color: #eee;
-  color: $text-dark-grey;
 }
diff --git a/app/assets/templates/comment_tpl.jst.hbs b/app/assets/templates/comment_tpl.jst.hbs
index 23eb368d9fe3eea5fbf05bf0f06740db319727f4..3196cad33235756b0608e8f3465dd44fb7cb7083 100644
--- a/app/assets/templates/comment_tpl.jst.hbs
+++ b/app/assets/templates/comment_tpl.jst.hbs
@@ -1,8 +1,8 @@
 <div id="{{guid}}">
   <div class="img">
-    {{#linkToPerson author}}
+    {{#linkToAuthor author}}
       {{{personImage this "small" "small"}}}
-    {{/linkToPerson}}
+    {{/linkToAuthor}}
   </div>
 
   <div class="bd">
diff --git a/app/assets/templates/contact_tpl.jst.hbs b/app/assets/templates/contact_tpl.jst.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..a89542f4ef1e04857cdb674edaf51b8bfa72359c
--- /dev/null
+++ b/app/assets/templates/contact_tpl.jst.hbs
@@ -0,0 +1,23 @@
+<div class="stream_element media contact {{in_aspect}}" id={{person_id}}>
+  <div class="pull-right">
+    {{{aspectMembershipIndicator this in_aspect}}}
+  </div>
+
+  <div class="media-object pull-left">
+    {{{personImage person 'small'}}}
+  </div>
+
+  <div class="media-body">
+    {{#linkToPerson person}}
+      {{name}}
+    {{/linkToPerson}}
+
+    <div class="info diaspora_handle">
+      {{person.diaspora_id}}
+    </div>
+
+    <div class="info tags">
+      {{{fmtTags person.profile.tags}}}
+    </div>
+  </div>
+</div>
diff --git a/app/assets/templates/single-post-viewer/single-post-content_tpl.jst.hbs b/app/assets/templates/single-post-viewer/single-post-content_tpl.jst.hbs
index a144e03902378fad2b71b27262eefa364783cc5a..e804b3c956cc6e0717078d3a3ca6b34ed087a50e 100644
--- a/app/assets/templates/single-post-viewer/single-post-content_tpl.jst.hbs
+++ b/app/assets/templates/single-post-viewer/single-post-content_tpl.jst.hbs
@@ -3,26 +3,26 @@
     <div id='post-info' class='span8'>
       <div class="img pull-left">
         {{#if root}}
-          {{#linkToPerson root.author}}
+          {{#linkToAuthor root.author}}
             {{{personImage this 'medium'}}}
-          {{/linkToPerson}}
+          {{/linkToAuthor}}
         {{else}}
-          {{#linkToPerson author}}
+          {{#linkToAuthor author}}
             {{{personImage this 'medium'}}}
-          {{/linkToPerson}}
+          {{/linkToAuthor}}
         {{/if}}
       </div>
 
       <div class="bd">
         <span class='author'>
           {{#if root}}
-            {{#linkToPerson root.author}}
+            {{#linkToAuthor root.author}}
               {{name}}
-            {{/linkToPerson}}
+            {{/linkToAuthor}}
           {{else}}
-            {{#linkToPerson author}}
+            {{#linkToAuthor author}}
               {{name}}
-            {{/linkToPerson}}
+            {{/linkToAuthor}}
           {{/if}}
         </span>
             
@@ -69,14 +69,14 @@
       <div class='span8' id='reshare-info'>
         <i class='entypo retweet small pull-left'></i>
         <div class="img pull-left">
-          {{#linkToPerson author}}
+          {{#linkToAuthor author}}
             {{{personImage this 'small'}}}
-          {{/linkToPerson}}
+          {{/linkToAuthor}}
         </div>
         <span class="author">
-          {{#linkToPerson author}}
+          {{#linkToAuthor author}}
             {{name}}
-          {{/linkToPerson}}
+          {{/linkToAuthor}}
         </span>
         <span class="post-time">
           <a href="/posts/{{id}}">
diff --git a/app/assets/templates/single-post-viewer/single-post-interactions_tpl.jst.hbs b/app/assets/templates/single-post-viewer/single-post-interactions_tpl.jst.hbs
index aebe7a6801dfe1300c08e5e72ec05f21575abdd8..2488ea0de6cc9f371316f2b20689d1a44fc0da24 100644
--- a/app/assets/templates/single-post-viewer/single-post-interactions_tpl.jst.hbs
+++ b/app/assets/templates/single-post-viewer/single-post-interactions_tpl.jst.hbs
@@ -6,9 +6,9 @@
     </span>
     <span>
       {{#each reshares}}
-        {{#linkToPerson author}}
+        {{#linkToAuthor author}}
           {{{personImage this 'small' 'micro'}}}
-        {{/linkToPerson}}
+        {{/linkToAuthor}}
       {{/each}}
     </span>
   </div>
@@ -21,9 +21,9 @@
     </span>
     <span>
       {{#each likes}}
-        {{#linkToPerson author}}
+        {{#linkToAuthor author}}
           {{{personImage this 'small' 'micro'}}}
-        {{/linkToPerson}}
+        {{/linkToAuthor}}
       {{/each}}
     </span>
   </div>
diff --git a/app/assets/templates/stream-faces_tpl.jst.hbs b/app/assets/templates/stream-faces_tpl.jst.hbs
index 6db001f192370cbdd0f368a48922a1a48cc68206..2e9898905ca1c1776d789132d91c7086439cda88 100644
--- a/app/assets/templates/stream-faces_tpl.jst.hbs
+++ b/app/assets/templates/stream-faces_tpl.jst.hbs
@@ -1,5 +1,5 @@
 {{#people}}
-  {{#linkToPerson this}}
+  {{#linkToAuthor this}}
     {{{personImage this "small"}}}
-  {{/linkToPerson}}
+  {{/linkToAuthor}}
 {{/people}}
diff --git a/app/assets/templates/stream-frame_tpl.jst.hbs b/app/assets/templates/stream-frame_tpl.jst.hbs
index 5574a479de1b55ce97740f3fd23995d9e1d6e4e3..a9f55163c1f53d8c7c87d42e1dd5fca2092abd7b 100644
--- a/app/assets/templates/stream-frame_tpl.jst.hbs
+++ b/app/assets/templates/stream-frame_tpl.jst.hbs
@@ -14,16 +14,16 @@
     <div class='stream-frame-feedback'></div>
 
     <div class="media author">
-        {{#linkToPerson author}}
+        {{#linkToAuthor author}}
             <div class="img">
                 <div class="profile-image-container smaller" style="background-image : url('{{avatar.large}}')"></div>
             </div>
-        {{/linkToPerson}}
+        {{/linkToAuthor}}
 
         <div class="bd">
-            {{#linkToPerson author}}
+            {{#linkToAuthor author}}
                 {{name}}
-            {{/linkToPerson}}
+            {{/linkToAuthor}}
         </div>
     </div>
 
diff --git a/app/controllers/aspect_memberships_controller.rb b/app/controllers/aspect_memberships_controller.rb
index 5009902b92641293c2baf4d6a934c823153f72f1..eddfa39359ab1a5408675a58303444ca1d122fcb 100644
--- a/app/controllers/aspect_memberships_controller.rb
+++ b/app/controllers/aspect_memberships_controller.rb
@@ -34,10 +34,7 @@ class AspectMembershipsController < ApplicationController
     respond_to do |format|
       format.json do
         if success
-          render :json => {
-            :person_id  => contact.person_id,
-            :aspect_ids => contact.aspects.map{|a| a.id}
-          }
+          render :json => AspectMembershipPresenter.new(membership).base_hash
         else
           render :text => membership.errors.full_messages, :status => 403
         end
@@ -57,7 +54,9 @@ class AspectMembershipsController < ApplicationController
       flash.now[:notice] =  I18n.t('aspects.add_to_aspect.success')
       respond_with do |format|
         format.json do
-          render :json => AspectMembership.where(:contact_id => @contact.id, :aspect_id => @aspect.id).first.to_json
+          render :json => AspectMembershipPresenter.new(
+            AspectMembership.where(:contact_id => @contact.id, :aspect_id => @aspect.id).first)
+          .base_hash
         end
 
         format.all { redirect_to :back }
diff --git a/app/controllers/contacts_controller.rb b/app/controllers/contacts_controller.rb
index 9a57ba3e43d613c379e4e4987cb4cbfb50dadfc8..0e6168c9c607b8eb86a721222e06640ae78ce54f 100644
--- a/app/controllers/contacts_controller.rb
+++ b/app/controllers/contacts_controller.rb
@@ -40,32 +40,24 @@ class ContactsController < ApplicationController
 
     @contacts = contacts_by_type(type)
     @contacts_size = @contacts.length
+    gon.preloads[:contacts] = @contacts.map{ |c| ContactPresenter.new(c, current_user).full_hash_with_person }
   end
 
   def contacts_by_type(type)
-    contacts = case type
+    case type
       when "all"
-        [current_user.contacts]
+        current_user.contacts
       when "only_sharing"
-        [current_user.contacts.only_sharing]
+        current_user.contacts.only_sharing
       when "receiving"
-        [current_user.contacts.receiving]
+        current_user.contacts.receiving
       when "by_aspect"
         @aspect = current_user.aspects.find(params[:a_id])
-        @contacts_in_aspect = @aspect.contacts
-        @contacts_not_in_aspect = current_user.contacts.where.not(contacts: {id: @contacts_in_aspect.pluck(:id) })
-        [@contacts_in_aspect, @contacts_not_in_aspect].map {|relation|
-          relation.includes(:aspect_memberships)
-        }
+        gon.preloads[:aspect] = AspectPresenter.new(@aspect).as_json
+        current_user.contacts
       else
         raise ArgumentError, "unknown type #{type}"
       end
-
-    contacts.map {|relation|
-      relation.includes(:person => :profile).to_a.tap {|contacts|
-        contacts.sort_by! {|contact| contact.person.name }
-      }
-    }.inject(:+).paginate(:page => params[:page], :per_page => 25)
   end
 
   def set_up_contacts_mobile
diff --git a/app/helpers/contacts_helper.rb b/app/helpers/contacts_helper.rb
index 5ea96b5a268812308a1c60e19bb5ee00da46a0bd..3fdee52f35acaad92a6600a4655693130e43d74a 100644
--- a/app/helpers/contacts_helper.rb
+++ b/app/helpers/contacts_helper.rb
@@ -1,25 +1,9 @@
 module ContactsHelper
   def contact_aspect_dropdown(contact)
-    membership = contact.aspect_memberships.where(:aspect_id => @aspect.id).first unless @aspect.nil?
-
-    if membership
-      content_tag(:i, nil, :class => 'entypo circled-cross contact_remove-from-aspect',
-                  :title => t('contacts.index.remove_contact'),
-                  'data-aspect_id' => @aspect.id,
-                  'data-person_id' => contact.person_id,
-                  'data-membership_id' => membership.id )
-
-    elsif @aspect.nil?
-      render :partial => 'people/relationship_action',
-              :locals => { :person => contact.person,
-                           :contact => contact,
-                           :current_user => current_user }
-    else
-      content_tag(:i, nil, :class => 'entypo circled-plus contact_add-to-aspect',
-                  :title => t('contacts.index.add_contact'),
-                  'data-aspect_id' => @aspect.id,
-                  'data-person_id' => contact.person_id )
-    end
+    render :partial => 'people/relationship_action',
+            :locals => { :person => contact.person,
+                         :contact => contact,
+                         :current_user => current_user }
   end
 
   def start_a_conversation_link(aspect, contacts_size)
diff --git a/app/presenters/aspect_membership_presenter.rb b/app/presenters/aspect_membership_presenter.rb
new file mode 100644
index 0000000000000000000000000000000000000000..28a96e38f203130c4aef55f03b0b62f6e26acd2b
--- /dev/null
+++ b/app/presenters/aspect_membership_presenter.rb
@@ -0,0 +1,11 @@
+class AspectMembershipPresenter < BasePresenter
+  def initialize(membership)
+    @membership = membership
+  end
+
+  def base_hash
+    { id: @membership.id,
+      aspect: AspectPresenter.new(@membership.aspect).as_json,
+    }
+  end
+end
diff --git a/app/presenters/contact_presenter.rb b/app/presenters/contact_presenter.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9ab8b2d2675b7fe2fbcbac6a8baeaf74b8f29ce3
--- /dev/null
+++ b/app/presenters/contact_presenter.rb
@@ -0,0 +1,17 @@
+class ContactPresenter < BasePresenter
+  def base_hash
+    { id: id,
+      person_id: person_id
+    }
+  end
+
+  def full_hash
+    base_hash.merge({
+      aspect_memberships: aspect_memberships.map{ |membership| AspectMembershipPresenter.new(membership).base_hash }
+    })
+  end
+
+  def full_hash_with_person
+    full_hash.merge({person: PersonPresenter.new(person).full_hash_with_profile})
+  end
+end
diff --git a/app/views/contacts/_contact.html.haml b/app/views/contacts/_contact.html.haml
deleted file mode 100644
index 3e6aeb8a409964359a2278b7de5adaf3a47b71d5..0000000000000000000000000000000000000000
--- a/app/views/contacts/_contact.html.haml
+++ /dev/null
@@ -1,12 +0,0 @@
-- membership = contact.aspect_memberships.where(:aspect_id => @aspect.id).first unless @aspect.nil?
-.media.stream_element{:id => contact.person_id, :class => ("in_aspect" if membership)}
-  .pull-right
-    = contact_aspect_dropdown(contact)
-  .media-object.pull-left
-    = person_image_link(contact.person, :size => :thumb_small)
-  .media-body
-    = person_link(contact.person, :class => 'name')
-    .info.diaspora_handle
-      = contact.person_diaspora_handle
-    .info.tags
-      = Diaspora::Taggable.format_tags(contact.person.profile.tag_string)
diff --git a/app/views/contacts/index.html.haml b/app/views/contacts/index.html.haml
index 4d8f223b4f246903255956b540efea0546b012bf..62161cbfad7ddb08369909a6a4361e1e0bb435b8 100644
--- a/app/views/contacts/index.html.haml
+++ b/app/views/contacts/index.html.haml
@@ -11,8 +11,9 @@
         = render 'contacts/header'
 
         - if @contacts_size > 0
-          = render @contacts
-          = will_paginate @contacts
+          #contact_stream
+            -# JS
+
         - else
           .no_contacts
             %h3
diff --git a/config/locales/javascript/javascript.en.yml b/config/locales/javascript/javascript.en.yml
index 78fdc4533b3d9e243d92aacc13b3a9e07ea1940a..77fbeb3d14e3d38d735f30ab950e7d245ecfbec0 100644
--- a/config/locales/javascript/javascript.en.yml
+++ b/config/locales/javascript/javascript.en.yml
@@ -48,6 +48,7 @@ en:
       remove_contact: "Remove contact"
       error_add: "Couldn't add <%= name %> to the aspect :("
       error_remove: "Couldn't remove <%= name %> from the aspect :("
+      search_no_results: "No contacts found"
 
     my_activity: "My Activity"
     my_stream: "Stream"
diff --git a/spec/controllers/aspect_memberships_controller_spec.rb b/spec/controllers/aspect_memberships_controller_spec.rb
index 0a5e125e909f93b4e0f7a124dda0638f26f2a6b0..abdae8de8ad58b33b258ebbeab27d1013fa2de84 100644
--- a/spec/controllers/aspect_memberships_controller_spec.rb
+++ b/spec/controllers/aspect_memberships_controller_spec.rb
@@ -72,14 +72,14 @@ describe AspectMembershipsController, :type => :controller do
     end
 
     context 'json' do
-      it 'returns a list of aspect ids for the person' do
+      it 'returns the aspect membership' do
         post :create,
         :format => :json,
         :person_id => @person.id,
         :aspect_id => @aspect0.id
 
         contact = @controller.current_user.contact_for(@person)
-        expect(response.body).to eq(contact.aspect_memberships.first.to_json)
+        expect(response.body).to eq(AspectMembershipPresenter.new(contact.aspect_memberships.first).base_hash.to_json)
       end
     end
   end
diff --git a/spec/controllers/jasmine_fixtures/contacts_spec.rb b/spec/controllers/jasmine_fixtures/contacts_spec.rb
index f21500def401143154683d54f4dde14991f4a4e2..6311c3b34385006c5076909071a6feeeb16f32da 100644
--- a/spec/controllers/jasmine_fixtures/contacts_spec.rb
+++ b/spec/controllers/jasmine_fixtures/contacts_spec.rb
@@ -10,12 +10,20 @@ describe ContactsController, :type => :controller do
       AppConfig.chat.enabled = true
       @aspect = bob.aspects.create(:name => "another aspect")
       bob.share_with alice.person, @aspect
+      bob.share_with eve.person, @aspect
       sign_in :user, bob
     end
 
-    it "generates a jasmine fixture", :fixture => true do
+    it "generates the aspects_manage fixture", :fixture => true do
       get :index, :a_id => @aspect.id
       save_fixture(html_for("body"), "aspects_manage")
     end
+
+    it "generates the contacts_json fixture", :fixture => true do
+      json = bob.contacts.map { |c|
+               ContactPresenter.new(c, bob).full_hash_with_person
+             }.to_json
+      save_fixture(json, "contacts_json")
+    end
   end
 end
diff --git a/spec/javascripts/app/collections/contacts_collection_spec.js b/spec/javascripts/app/collections/contacts_collection_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..3b64d4bd9724caa1faeb7ec2a649a46e1bebb75e
--- /dev/null
+++ b/spec/javascripts/app/collections/contacts_collection_spec.js
@@ -0,0 +1,37 @@
+describe("app.collections.Contacts", function(){
+  beforeEach(function(){
+    this.collection = new app.collections.Contacts();
+  });
+
+  describe("comparator", function() {
+    beforeEach(function(){
+      this.aspect = new app.models.Aspect({id: 42, name: "cats"});
+      this.con1 = new app.models.Contact({
+                    person: { name: "aaa" },
+                    aspect_memberships: []
+                  });
+      this.con2 = new app.models.Contact({
+                    person: { name: "aaa" },
+                    aspect_memberships: [{id: 23, aspect: this.aspect}]
+                  });
+      this.con3 = new app.models.Contact({
+                    person: { name: "zzz" },
+                    aspect_memberships: [{id: 23, aspect: this.aspect}]
+                  });
+    });
+
+    it("should compare the username if app.aspect is not present", function() {
+      expect(this.collection.comparator(this.con1, this.con3)).toBeLessThan(0);
+    });
+
+    it("should compare the aspect memberships if app.aspect is present", function() {
+      app.aspect = this.aspect;
+      expect(this.collection.comparator(this.con1, this.con3)).toBeGreaterThan(0);
+    });
+
+    it("should compare the username if the contacts have equal aspect memberships", function() {
+      app.aspect = this.aspect;
+      expect(this.collection.comparator(this.con2, this.con3)).toBeLessThan(0);
+    });
+  });
+});
diff --git a/spec/javascripts/app/models/contact_spec.js b/spec/javascripts/app/models/contact_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..b5fda20c1ae06ce0dfa20a0f97d34e9dc1749bf4
--- /dev/null
+++ b/spec/javascripts/app/models/contact_spec.js
@@ -0,0 +1,20 @@
+describe("app.models.Contact", function() {
+
+  beforeEach(function(){
+    this.aspect = factory.aspect();
+    this.contact = new app.models.Contact({
+                     person: { name: "aaa" },
+                     aspect_memberships: [{id: 42, aspect: this.aspect}]
+                   });
+  });
+
+  describe("inAspect", function(){
+    it("returns true if the contact has been added to the aspect", function(){
+      expect(this.contact.inAspect(this.aspect.id)).toBeTruthy();
+    });
+
+    it("returns false if the contact hasn't been added to the aspect", function(){
+      expect(this.contact.inAspect(this.aspect.id+1)).toBeFalsy();
+    });
+  });
+});
diff --git a/spec/javascripts/app/pages/contacts_spec.js b/spec/javascripts/app/pages/contacts_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..e62bce96a030f1c9a1d5e0d5220caa2bb551b1e5
--- /dev/null
+++ b/spec/javascripts/app/pages/contacts_spec.js
@@ -0,0 +1,101 @@
+describe("app.pages.Contacts", function(){
+  beforeEach(function() {
+    spec.loadFixture("aspects_manage");
+    this.view = new app.pages.Contacts({
+      stream: {
+        render: function(){}
+      }
+    });
+    Diaspora.I18n.load({
+      contacts: {
+        aspect_list_is_visible: "Contacts in this aspect are able to see each other.",
+        aspect_list_is_not_visible: "Contacts in this aspect are not able to see each other.",
+        aspect_chat_is_enabled: "Contacts in this aspect are able to chat with you.",
+        aspect_chat_is_not_enabled: "Contacts in this aspect are not able to chat with you.",
+      }
+    });
+  });
+
+  context('toggle chat privilege', function() {
+    beforeEach(function() {
+      this.chat_toggle = $("#chat_privilege_toggle");
+      this.chat_icon = $("#chat_privilege_toggle .entypo");
+    });
+
+    it('updates the title for the tooltip', function() {
+      expect(this.chat_icon.attr('data-original-title')).toBe(
+        Diaspora.I18n.t("contacts.aspect_chat_is_not_enabled")
+      );
+      this.chat_toggle.trigger('click');
+      expect(this.chat_icon.attr('data-original-title')).toBe(
+        Diaspora.I18n.t("contacts.aspect_chat_is_enabled")
+      );
+    });
+
+    it('toggles the chat icon', function() {
+      expect(this.chat_icon.hasClass('enabled')).toBeFalsy();
+      this.chat_toggle.trigger('click');
+      expect(this.chat_icon.hasClass('enabled')).toBeTruthy();
+    });
+  });
+
+  context('toggle contacts visibility', function() {
+    beforeEach(function() {
+      this.visibility_toggle = $("#contacts_visibility_toggle");
+      this.lock_icon = $("#contacts_visibility_toggle .entypo");
+    });
+
+    it('updates the title for the tooltip', function() {
+      expect(this.lock_icon.attr('data-original-title')).toBe(
+        Diaspora.I18n.t("contacts.aspect_list_is_visible")
+      );
+
+      this.visibility_toggle.trigger('click');
+
+      expect(this.lock_icon.attr('data-original-title')).toBe(
+        Diaspora.I18n.t("contacts.aspect_list_is_not_visible")
+      );
+    });
+
+    it('toggles the lock icon', function() {
+      expect(this.lock_icon.hasClass('lock-open')).toBeTruthy();
+      expect(this.lock_icon.hasClass('lock')).toBeFalsy();
+
+      this.visibility_toggle.trigger('click');
+
+      expect(this.lock_icon.hasClass('lock')).toBeTruthy();
+      expect(this.lock_icon.hasClass('lock-open')).toBeFalsy();
+    });
+  });
+
+  context('show aspect name form', function() {
+    beforeEach(function() {
+      this.button = $('#change_aspect_name');
+    });
+
+    it('shows the form', function() {
+      expect($('#aspect_name_form').css('display')).toBe('none');
+      this.button.trigger('click');
+      expect($('#aspect_name_form').css('display')).not.toBe('none');
+    });
+
+    it('hides the aspect name', function() {
+      expect($('.header > h3').css('display')).not.toBe('none');
+      this.button.trigger('click');
+      expect($('.header > h3').css('display')).toBe('none');
+    });
+  });
+
+  context('search contact list', function() {
+    beforeEach(function() {
+      this.searchinput = $('#contact_list_search');
+    });
+
+    it('calls stream.search', function() {
+      this.view.stream.search = jasmine.createSpy();
+      this.searchinput.val("Username");
+      this.searchinput.trigger('keyup');
+      expect(this.view.stream.search).toHaveBeenCalledWith("Username");
+    });
+  });
+});
diff --git a/spec/javascripts/app/views/contact_stream_view_spec.js b/spec/javascripts/app/views/contact_stream_view_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..955dd2e7b16f7feac26721b8fd83e65b22fdb800
--- /dev/null
+++ b/spec/javascripts/app/views/contact_stream_view_spec.js
@@ -0,0 +1,77 @@
+describe("app.views.ContactStream", function() {
+  beforeEach(function() {
+    loginAs({name: "alice", avatar : {small : "http://avatar.com/photo.jpg"}});
+    spec.loadFixture("aspects_manage");
+    this.contacts = new app.collections.Contacts($.parseJSON(spec.readFixture("contacts_json")));
+    app.aspect = new app.models.Aspect(this.contacts.first().get('aspect_memberships')[0].aspect);
+    this.view = new app.views.ContactStream({
+      collection : this.contacts,
+      el: $('.stream.contacts #contact_stream')
+    });
+
+    this.view.perPage=1;
+
+    //clean the page
+    this.view.$el.html('');
+  });
+
+  describe("initialize", function() {
+    it("binds an infinite scroll listener", function() {
+      spyOn($.fn, "scroll");
+      new app.views.ContactStream({collection : this.contacts});
+      expect($.fn.scroll).toHaveBeenCalled();
+    });
+  });
+
+  describe("search", function() {
+    it("filters the contacts", function() {
+      this.view.render();
+      expect(this.view.$el.html()).toContain("alice");
+      this.view.search("eve");
+      expect(this.view.$el.html()).not.toContain("alice");
+      expect(this.view.$el.html()).toContain("eve");
+    });
+  });
+
+  describe("infScroll", function() {
+    beforeEach(function() {
+      this.view.off("renderContacts");
+      this.fn = jasmine.createSpy();
+      this.view.on("renderContacts", this.fn);
+      spyOn($.fn, "height").and.returnValue(0);
+      spyOn($.fn, "scrollTop").and.returnValue(100);
+    });
+
+    it("triggers renderContacts when the user is at the bottom of the page", function() {
+      this.view.infScroll();
+      expect(this.fn).toHaveBeenCalled();
+    });
+  });
+
+  describe("render", function() {
+    beforeEach(function() {
+      spyOn(this.view, "renderContacts");
+    });
+
+    it("calls renderContacts", function() {
+      this.view.render();
+      expect(this.view.renderContacts).toHaveBeenCalled();
+    });
+  });
+
+  describe("renderContacts", function() {
+    beforeEach(function() {
+      this.view.off("renderContacts");
+      this.view.renderContacts();
+    });
+
+    it("renders perPage contacts", function() {
+      expect(this.view.$el.find('.stream_element.contact').length).toBe(1);
+    });
+
+    it("renders more contacts when called a second time", function() {
+      this.view.renderContacts();
+      expect(this.view.$el.find('.stream_element.contact').length).toBe(2);
+    });
+  });
+});
diff --git a/spec/javascripts/app/views/contact_view_spec.js b/spec/javascripts/app/views/contact_view_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..0dee502e938d5b252997215d785095e0b3d80302
--- /dev/null
+++ b/spec/javascripts/app/views/contact_view_spec.js
@@ -0,0 +1,136 @@
+describe("app.views.Contact", function(){
+  beforeEach(function() {
+    this.aspect1 = factory.aspect({id: 1});
+    this.aspect2 = factory.aspect({id: 2});
+
+    this.model = new app.models.Contact({
+      person_id: 42,
+      person: { id: 42, name: 'alice' },
+      aspect_memberships: [{id: 23, aspect: this.aspect1}]
+    });
+    this.view = new app.views.Contact({ model: this.model });
+    Diaspora.I18n.load({
+      contacts: {
+        add_contact: "Add contact",
+        remove_contact: "Remove contact",
+        error_add: "Couldn't add <%= name %> to the aspect :(",
+        error_remove: "Couldn't remove <%= name %> from the aspect :("
+      }
+    });
+  });
+
+  context("#presenter", function() {
+    it("contains necessary elements", function() {
+      app.aspect = this.aspect1;
+      expect(this.view.presenter()).toEqual(jasmine.objectContaining({
+        person_id: 42,
+        person: jasmine.objectContaining({id: 42, name: 'alice'}),
+        in_aspect: 'in_aspect'
+      }));
+    });
+  });
+
+  context('add contact to aspect', function() {
+    beforeEach(function() {
+      app.aspect = this.aspect2;
+      this.view.render();
+      this.button = this.view.$el.find('.contact_add-to-aspect');
+      this.contact = this.view.$el.find('.stream_element.contact');
+      this.aspect_membership = {id: 42, aspect: app.aspect.toJSON()};
+      this.response = JSON.stringify(this.aspect_membership);
+    });
+
+    it('sends a correct ajax request', function() {
+      this.button.trigger('click');
+      var obj = $.parseJSON(jasmine.Ajax.requests.mostRecent().params);
+      expect(obj.person_id).toBe(this.model.get('person_id'));
+      expect(obj.aspect_id).toBe(app.aspect.get('id'));
+    });
+
+    it('adds a aspect_membership to the contact', function() {
+      expect(this.model.aspect_memberships.length).toBe(1);
+      $('.contact_add-to-aspect',this.contact).trigger('click');
+      jasmine.Ajax.requests.mostRecent().response({
+        status: 200, // success
+        responseText: this.response
+      });
+      expect(this.model.aspect_memberships.length).toBe(2);
+    });
+
+    it('calls render', function() {
+      spyOn(this.view, 'render');
+      $('.contact_add-to-aspect',this.contact).trigger('click');
+      jasmine.Ajax.requests.mostRecent().response({
+        status: 200, // success
+        responseText: this.response
+      });
+      expect(this.view.render).toHaveBeenCalled();
+    });
+
+
+    it('displays a flash message on errors', function(){
+      $('.contact_add-to-aspect',this.contact).trigger('click');
+      jasmine.Ajax.requests.mostRecent().response({
+        status: 400, // fail
+      });
+      expect($('[id^="flash"]')).toBeErrorFlashMessage(
+        Diaspora.I18n.t(
+          'contacts.error_add',
+          {name: this.model.get('person').name}
+        )
+      );
+    });
+  });
+
+  context('remove contact from aspect', function() {
+    beforeEach(function() {
+      app.aspect = this.aspect1;
+      this.view.render();
+      this.button = this.view.$el.find('.contact_remove-from-aspect');
+      this.contact = this.view.$el.find('.stream_element.contact');
+      this.aspect_membership = this.model.aspect_memberships.first().toJSON();
+      this.response = JSON.stringify(this.aspect_membership);
+    });
+
+    it('sends a correct ajax request', function() {
+      $('.contact_remove-from-aspect',this.contact).trigger('click');
+      expect(jasmine.Ajax.requests.mostRecent().url).toBe(
+        "/aspect_memberships/"+this.aspect_membership.id
+      );
+    });
+
+    it('removes the aspect_membership from the contact', function() {
+      expect(this.model.aspect_memberships.length).toBe(1);
+      $('.contact_remove-from-aspect',this.contact).trigger('click');
+      jasmine.Ajax.requests.mostRecent().response({
+        status: 200, // success
+        responseText: this.response
+      });
+      expect(this.model.aspect_memberships.length).toBe(0);
+    });
+
+    it('calls render', function() {
+      spyOn(this.view, 'render');
+      $('.contact_remove-from-aspect',this.contact).trigger('click');
+      jasmine.Ajax.requests.mostRecent().response({
+        status: 200, // success
+        responseText: this.response,
+      });
+      expect(this.view.render).toHaveBeenCalled();
+    });
+
+    it('displays a flash message on errors', function(){
+      $('.contact_remove-from-aspect',this.contact).trigger('click');
+      jasmine.Ajax.requests.mostRecent().response({
+        status: 400, // fail
+      });
+      expect($('[id^="flash"]')).toBeErrorFlashMessage(
+        Diaspora.I18n.t(
+          'contacts.error_remove',
+          {name: this.model.get('person').name}
+        )
+      );
+    });
+  });
+
+});
diff --git a/spec/javascripts/app/views/contacts_view_spec.js b/spec/javascripts/app/views/contacts_view_spec.js
deleted file mode 100644
index b3a9f736cb862348941e88fd2840bd59bca42dc4..0000000000000000000000000000000000000000
--- a/spec/javascripts/app/views/contacts_view_spec.js
+++ /dev/null
@@ -1,240 +0,0 @@
-describe("app.views.Contacts", function(){
-  beforeEach(function() {
-    spec.loadFixture("aspects_manage");
-    this.view = new app.views.Contacts();
-    Diaspora.I18n.load({
-      contacts: {
-        add_contact: "Add contact",
-        aspect_list_is_visible: "Contacts in this aspect are able to see each other.",
-        aspect_list_is_not_visible: "Contacts in this aspect are not able to see each other.",
-        aspect_chat_is_enabled: "Contacts in this aspect are able to chat with you.",
-        aspect_chat_is_not_enabled: "Contacts in this aspect are not able to chat with you.",
-        remove_contact: "Remove contact",
-        error_add: "Couldn't add <%= name %> to the aspect :(",
-        error_remove: "Couldn't remove <%= name %> from the aspect :("
-      }
-    });
-  });
-
-  context('toggle chat privilege', function() {
-    beforeEach(function() {
-      this.chat_toggle = $("#chat_privilege_toggle");
-      this.chat_icon = $("#chat_privilege_toggle .entypo");
-    });
-
-    it('updates the title for the tooltip', function() {
-      expect(this.chat_icon.attr('data-original-title')).toBe(
-        Diaspora.I18n.t("contacts.aspect_chat_is_not_enabled")
-      );
-      this.chat_toggle.trigger('click');
-      expect(this.chat_icon.attr('data-original-title')).toBe(
-        Diaspora.I18n.t("contacts.aspect_chat_is_enabled")
-      );
-    });
-
-    it('toggles the chat icon', function() {
-      expect(this.chat_icon.hasClass('enabled')).toBeFalsy;
-      this.chat_toggle.trigger('click');
-      expect(this.chat_icon.hasClass('enabled')).toBeTruethy;
-    });
-  });
-
-  context('toggle contacts visibility', function() {
-    beforeEach(function() {
-      this.visibility_toggle = $("#contacts_visibility_toggle");
-      this.lock_icon = $("#contacts_visibility_toggle .entypo");
-    });
-
-    it('updates the title for the tooltip', function() {
-      expect(this.lock_icon.attr('data-original-title')).toBe(
-        Diaspora.I18n.t("contacts.aspect_list_is_visible")
-      );
-
-      this.visibility_toggle.trigger('click');
-
-      expect(this.lock_icon.attr('data-original-title')).toBe(
-        Diaspora.I18n.t("contacts.aspect_list_is_not_visible")
-      );
-    });
-
-    it('toggles the lock icon', function() {
-      expect(this.lock_icon.hasClass('lock-open')).toBeTruethy;
-      expect(this.lock_icon.hasClass('lock')).toBeFalsy;
-
-      this.visibility_toggle.trigger('click');
-
-      expect(this.lock_icon.hasClass('lock')).toBeTruethy;
-      expect(this.lock_icon.hasClass('lock-open')).toBeFalsy;
-    });
-  });
-
-  context('show aspect name form', function() {
-    beforeEach(function() {
-      this.button = $('#change_aspect_name');
-    });
-
-    it('shows the form', function() {
-      expect($('#aspect_name_form').css('display')).toBe('none');
-      this.button.trigger('click');
-      expect($('#aspect_name_form').css('display')).not.toBe('none');
-    });
-
-    it('hides the aspect name', function() {
-      expect($('.header > h3').css('display')).not.toBe('none');
-      this.button.trigger('click');
-      expect($('.header > h3').css('display')).toBe('none');
-    });
-  });
-
-  context('add contact to aspect', function() {
-    beforeEach(function() {
-      this.contact = $('#people_stream .stream_element').last();
-      this.button = this.contact.find('.contact_add-to-aspect');
-      this.person_id = this.button.attr('data-person_id');
-      this.aspect_id = this.button.attr('data-aspect_id');
-    });
-
-    it('sends a correct ajax request', function() {
-      jasmine.Ajax.install();
-      $('.contact_add-to-aspect',this.contact).trigger('click');
-      var obj = $.parseJSON(jasmine.Ajax.requests.mostRecent().params);
-      expect(obj.person_id).toBe(this.person_id);
-      expect(obj.aspect_id).toBe(this.aspect_id);
-    });
-
-    it('adds a membership id to the contact', function() {
-      jasmine.Ajax.install();
-      $('.contact_add-to-aspect',this.contact).trigger('click');
-      jasmine.Ajax.requests.mostRecent().response({
-        status: 200, // success
-        responseText: '{ "id": 42 }'
-      });
-      expect(this.button.attr('data-membership_id')).toBe('42');
-    });
-
-    it('displays a flash message on errors', function(){
-      jasmine.Ajax.install();
-      $('.contact_add-to-aspect',this.contact).trigger('click');
-      jasmine.Ajax.requests.mostRecent().response({
-        status: 400, // fail
-      });
-      expect($('[id^="flash"]')).toBeErrorFlashMessage(
-        Diaspora.I18n.t(
-          'contacts.error_add',
-          {name: this.contact.find('.name').text()}
-        )
-      );
-    });
-
-    it('changes the appearance of the contact', function() {
-      expect(this.button.hasClass('contact_add-to-aspect')).toBeTruethy;
-      expect(this.button.hasClass('circled-cross')).toBeTruethy;
-      expect(this.contact.hasClass('in_aspect')).toBeTruethy;
-      expect(this.button.hasClass('contact_remove-from-aspect')).toBeFalsy;
-      expect(this.button.hasClass('circled-plus')).toBeFalsy;
-      expect(this.button.attr('data-original-title')).toBe(
-        Diaspora.I18n.t('contacts.add_contact')
-      );
-      jasmine.Ajax.install();
-      $('.contact_add-to-aspect',this.contact).trigger('click');
-      jasmine.Ajax.requests.mostRecent().response({
-        status: 200, // success
-        responseText: '{ "id": 42 }'
-      });
-      expect(this.button.hasClass('contact_add-to-aspect')).toBeFalsy;
-      expect(this.button.hasClass('circled-cross')).toBeFalsy;
-      expect(this.contact.hasClass('in_aspect')).toBeFalsy;
-      expect(this.button.hasClass('contact_remove-from-aspect')).toBeTruethy;
-      expect(this.button.hasClass('circled-plus')).toBeTruethy;
-      expect(this.button.attr('data-original-title')).toBe(
-        Diaspora.I18n.t('contacts.remove_contact')
-      );
-    });
-  });
-
-  context('remove contact from aspect', function() {
-    beforeEach(function() {
-      this.contact = $('#people_stream .stream_element').first();
-      this.button = this.contact.find('.contact_remove-from-aspect');
-      this.person_id = this.button.attr('data-person_id');
-      this.aspect_id = this.button.attr('data-aspect_id');
-      this.membership_id = this.button.attr('data-membership_id');
-
-    });
-
-    it('sends a correct ajax request', function() {
-      jasmine.Ajax.install();
-      $('.contact_remove-from-aspect',this.contact).trigger('click');
-      expect(jasmine.Ajax.requests.mostRecent().url).toBe(
-        "/aspect_memberships/"+this.membership_id
-      );
-    });
-
-    it('removes the membership id from the contact', function() {
-      jasmine.Ajax.install();
-      $('.contact_remove-from-aspect',this.contact).trigger('click');
-      jasmine.Ajax.requests.mostRecent().response({
-        status: 200, // success
-        responseText: '{}'
-      });
-      expect(this.button.attr('data-membership_id')).toBe(undefined);
-    });
-
-    it('displays a flash message on errors', function(){
-      jasmine.Ajax.install();
-      $('.contact_remove-from-aspect',this.contact).trigger('click');
-      jasmine.Ajax.requests.mostRecent().response({
-        status: 400, // fail
-      });
-      expect($('[id^="flash"]')).toBeErrorFlashMessage(
-        Diaspora.I18n.t(
-          'contacts.error_remove',
-          {name: this.contact.find('.name').text()}
-        )
-      );
-    });
-
-    it('changes the appearance of the contact', function() {
-      expect(this.button.hasClass('contact_add-to-aspect')).toBeFalsy;
-      expect(this.button.hasClass('circled-cross')).toBeFalsy;
-      expect(this.contact.hasClass('in_aspect')).toBeFalsy;
-      expect(this.button.hasClass('contact_remove-from-aspect')).toBeTruethy;
-      expect(this.button.hasClass('circled-plus')).toBeTruethy;
-      expect(this.button.attr('data-original-title')).toBe(
-        Diaspora.I18n.t('contacts.remove_contact')
-      );
-
-      jasmine.Ajax.install();
-      $('.contact_remove-from-aspect',this.contact).trigger('click');
-      jasmine.Ajax.requests.mostRecent().response({
-        status: 200, // success
-        responseText: '{}'
-      });
-
-      expect(this.button.hasClass('contact_add-to-aspect')).toBeTruethy;
-      expect(this.button.hasClass('circled-cross')).toBeTruethy;
-      expect(this.contact.hasClass('in_aspect')).toBeTruethy;
-      expect(this.button.hasClass('contact_remove-from-aspect')).toBeFalsy;
-      expect(this.button.hasClass('circled-plus')).toBeFalsy;
-      expect(this.button.attr('data-original-title')).toBe(
-        Diaspora.I18n.t('contacts.add_contact')
-      );
-    });
-  });
-
-  context('search contact list', function() {
-    beforeEach(function() {
-      this.searchinput = $('#contact_list_search');
-      this.username = $('.stream_element .name').first().text();
-    });
-
-    it('filters the contact list by name', function() {
-      expect($('.stream_element').length).toBeGreaterThan(1);
-      this.searchinput.val(this.username);
-      this.searchinput.trigger('keyup');
-      expect($('.stream_element:visible').length).toBe(1);
-      expect($('.stream_element:visible .name').first().text()).toBe(this.username);
-    });
-  });
-
-});
diff --git a/spec/javascripts/app/views/notifications_view_spec.js b/spec/javascripts/app/views/notifications_view_spec.js
index 53c5640fcceb8f6f1d9c09eb669efcf6331749f9..12034e4a6d1cfba93599f0371b55df8d6178e62a 100644
--- a/spec/javascripts/app/views/notifications_view_spec.js
+++ b/spec/javascripts/app/views/notifications_view_spec.js
@@ -63,13 +63,13 @@ describe("app.views.Notifications", function(){
 
     it('toggles the unread class and changes the title', function() {
       this.view.updateView(this.readN.data('guid'), this.readN.data('type'), true);
-      expect(this.readN.hasClass('unread')).toBeTruethy;
-      expect(this.readN.hasClass('read')).toBeFalsy;
+      expect(this.readN.hasClass('unread')).toBeTruthy();
+      expect(this.readN.hasClass('read')).toBeFalsy();
       expect(this.readN.find('.unread-toggle .entypo').data('original-title')).toBe(Diaspora.I18n.t('notifications.mark_read'));
 
       this.view.updateView(this.readN.data('guid'), this.readN.data('type'), false);
-      expect(this.readN.hasClass('read')).toBeTruethy;
-      expect(this.readN.hasClass('unread')).toBeFalsy;
+      expect(this.readN.hasClass('read')).toBeTruthy();
+      expect(this.readN.hasClass('unread')).toBeFalsy();
       expect(this.readN.find('.unread-toggle .entypo').data('original-title')).toBe(Diaspora.I18n.t('notifications.mark_unread'));
     });
   });
diff --git a/spec/presenters/aspect_membership_presenter_spec.rb b/spec/presenters/aspect_membership_presenter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..86d25de66e8a6999db32e201105e942e54d14da7
--- /dev/null
+++ b/spec/presenters/aspect_membership_presenter_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe AspectMembershipPresenter do
+  before do
+    @am = alice.aspects.where(:name => "generic").first.aspect_memberships.first
+    @presenter = AspectMembershipPresenter.new(@am)
+  end
+
+  describe '#base_hash' do
+    it 'works' do
+      expect(@presenter.base_hash).to be_present
+    end
+  end
+
+end
diff --git a/spec/presenters/contact_presenter_spec.rb b/spec/presenters/contact_presenter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2a1e0cefae6e95783f2a189c1959452d442fca59
--- /dev/null
+++ b/spec/presenters/contact_presenter_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+describe ContactPresenter do
+  before do
+    @presenter = ContactPresenter.new(alice.contact_for(bob.person))
+  end
+
+  describe '#base_hash' do
+    it 'works' do
+      expect(@presenter.base_hash).to be_present
+    end
+  end
+
+  describe '#full_hash' do
+    it 'works' do
+      expect(@presenter.full_hash).to be_present
+    end
+  end
+
+  describe '#full_hash_with_person' do
+    it 'works' do
+      expect(@presenter.full_hash_with_person).to be_present
+    end
+  end
+
+end