From 4cd8abc905df95975d896da32be9d42aa6cd928f Mon Sep 17 00:00:00 2001
From: Arpit Jalan <arpit@techapj.com>
Date: Fri, 1 Aug 2014 16:36:31 +0530
Subject: [PATCH] FEATURE: dynamically load invites

---
 .../discourse/controllers/user-invited.js.es6 | 34 ++++++++++---------
 .../javascripts/discourse/models/invite.js    |  3 +-
 .../templates/user/invited.js.handlebars      |  7 ++--
 .../discourse/views/user-invited.js.es6       |  4 ++-
 app/controllers/users_controller.rb           |  5 +--
 app/models/invite.rb                          |  9 ++---
 config/locales/server.en.yml                  |  2 +-
 config/site_settings.yml                      |  4 +--
 8 files changed, 37 insertions(+), 31 deletions(-)

diff --git a/app/assets/javascripts/discourse/controllers/user-invited.js.es6 b/app/assets/javascripts/discourse/controllers/user-invited.js.es6
index 611266b48..a37e929e7 100644
--- a/app/assets/javascripts/discourse/controllers/user-invited.js.es6
+++ b/app/assets/javascripts/discourse/controllers/user-invited.js.es6
@@ -10,6 +10,8 @@ export default Ember.ObjectController.extend({
   user: null,
   model: null,
   totalInvites: null,
+  canLoadMore: true,
+  invitesLoading: false,
 
   init: function() {
     this._super();
@@ -30,13 +32,6 @@ export default Ember.ObjectController.extend({
     });
   }, 250).observes('searchTerm'),
 
-  /**
-    The maximum amount of invites that will be displayed in the view
-
-    @property maxInvites
-  **/
-  maxInvites: Discourse.computed.setting('invites_shown'),
-
   /**
     Can the currently logged in user invite users to the site
 
@@ -64,15 +59,6 @@ export default Ember.ObjectController.extend({
     return this.get('totalInvites') > 9;
   }.property('totalInvites'),
 
-  /**
-    Were the results limited by our `maxInvites`
-
-    @property truncated
-  **/
-  truncated: function() {
-    return this.get('invites.length') === Discourse.SiteSettings.invites_shown;
-  }.property('invites.length'),
-
   actions: {
 
     /**
@@ -84,6 +70,22 @@ export default Ember.ObjectController.extend({
     rescind: function(invite) {
       invite.rescind();
       return false;
+    },
+
+    loadMore: function() {
+      var self = this;
+      var model = self.get('model');
+
+      if(self.get('canLoadMore')) {
+        self.set('invitesLoading', true);
+        Discourse.Invite.findInvitedBy(self.get('user'), self.get('searchTerm'), model.invites.length).then(function(invite_model) {
+          self.set('invitesLoading', false);
+          model.invites.pushObjects(invite_model.invites);
+          if(invite_model.invites.length === 0 || invite_model.invites.length < Discourse.SiteSettings.invites_per_page) {
+            self.set('canLoadMore', false);
+          }
+        });
+      }
     }
   }
 
diff --git a/app/assets/javascripts/discourse/models/invite.js b/app/assets/javascripts/discourse/models/invite.js
index a42115256..3ed3ae5ab 100644
--- a/app/assets/javascripts/discourse/models/invite.js
+++ b/app/assets/javascripts/discourse/models/invite.js
@@ -29,11 +29,12 @@ Discourse.Invite.reopenClass({
     return result;
   },
 
-  findInvitedBy: function(user, filter) {
+  findInvitedBy: function(user, filter, offset) {
     if (!user) { return Em.RSVP.resolve(); }
 
     var data = {};
     if (!Em.isNone(filter)) { data.filter = filter; }
+    data.offset = offset || 0;
 
     return Discourse.ajax("/users/" + user.get('username_lower') + "/invited.json", {data: data}).then(function (result) {
       result.invites = result.invites.map(function (i) {
diff --git a/app/assets/javascripts/discourse/templates/user/invited.js.handlebars b/app/assets/javascripts/discourse/templates/user/invited.js.handlebars
index 19d2077ad..1cc1676d9 100644
--- a/app/assets/javascripts/discourse/templates/user/invited.js.handlebars
+++ b/app/assets/javascripts/discourse/templates/user/invited.js.handlebars
@@ -17,7 +17,7 @@
     {{/if}}
 
     {{#if model.invites}}
-      <table class='table'>
+      <table class='table invite-list'>
         <tr>
           <th>{{i18n user.invited.user}}</th>
           <th>{{i18n user.invited.redeemed_at}}</th>
@@ -62,9 +62,8 @@
           </tr>
         {{/each}}
       </table>
-
-      {{#if truncated}}
-        <p>{{i18n user.invited.truncated count=maxInvites}}</p>
+      {{#if invitesLoading}}
+        <div class='spinner'>{{i18n loading}}</div>
       {{/if}}
 
     {{else}}
diff --git a/app/assets/javascripts/discourse/views/user-invited.js.es6 b/app/assets/javascripts/discourse/views/user-invited.js.es6
index dc4f8dc40..55c4d94c8 100644
--- a/app/assets/javascripts/discourse/views/user-invited.js.es6
+++ b/app/assets/javascripts/discourse/views/user-invited.js.es6
@@ -1,3 +1,5 @@
-export default Ember.View.extend({
+export default Ember.View.extend(Discourse.LoadMore, {
+  classNames: ['paginated-topics-list'],
+  eyelineSelector: '.paginated-topics-list .invite-list tr',
   templateName: 'user/invited'
 });
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index c624fd6fb..abb5b212a 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -94,11 +94,12 @@ class UsersController < ApplicationController
 
   def invited
     inviter = fetch_user_from_params
+    offset = params[:offset].to_i || 0
 
     invites = if guardian.can_see_invite_details?(inviter)
-      Invite.find_all_invites_from(inviter)
+      Invite.find_all_invites_from(inviter, offset)
     else
-      Invite.find_redeemed_invites_from(inviter)
+      Invite.find_redeemed_invites_from(inviter, offset)
     end
 
     invites = invites.filter_by(params[:filter])
diff --git a/app/models/invite.rb b/app/models/invite.rb
index dc1db2b1d..7cbcf7be0 100644
--- a/app/models/invite.rb
+++ b/app/models/invite.rb
@@ -126,18 +126,19 @@ class Invite < ActiveRecord::Base
     group_ids
   end
 
-  def self.find_all_invites_from(inviter)
+  def self.find_all_invites_from(inviter, offset=0)
     Invite.where(invited_by_id: inviter.id)
           .includes(:user => :user_stat)
           .order('CASE WHEN invites.user_id IS NOT NULL THEN 0 ELSE 1 END',
                  'user_stats.time_read DESC',
                  'invites.redeemed_at DESC')
-          .limit(SiteSetting.invites_shown)
+          .limit(SiteSetting.invites_per_page)
+          .offset(offset)
           .references('user_stats')
   end
 
-  def self.find_redeemed_invites_from(inviter)
-    find_all_invites_from(inviter).where('invites.user_id IS NOT NULL')
+  def self.find_redeemed_invites_from(inviter, offset=0)
+    find_all_invites_from(inviter, offset).where('invites.user_id IS NOT NULL')
   end
 
   def self.filter_by(email_or_username)
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index f3e1c6941..b0d7a3646 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -951,7 +951,7 @@ en:
 
     enable_names: "Allow showing user full names. Disable to hide full names."
     display_name_on_posts: "Show a user's full name on their posts in addition to their @username."
-    invites_shown: "Maximum invites shown on the user page."
+    invites_per_page: "Default invites shown on the user page."
     short_progress_text_threshold: "After the number of posts in a topic goes above this number, the progress bar will only show the current post number. If you change the progress bar's width, you may need to change this value."
     default_code_lang: "Default programming language syntax highlighting applied to GitHub code blocks (lang-auto, ruby, python etc.)"
     warn_reviving_old_topic_age: "When someone starts replying to a topic where the last reply is older than this many days, a warning will be displayed. Disable by setting to 0."
diff --git a/config/site_settings.yml b/config/site_settings.yml
index 47ae13043..aa66a28de 100644
--- a/config/site_settings.yml
+++ b/config/site_settings.yml
@@ -257,9 +257,9 @@ users:
     default: true
   invite_expiry_days: 30
   invite_passthrough_hours: 0
-  invites_shown:
+  invites_per_page:
     client: true
-    default: 30
+    default: 40
   delete_user_max_post_age:
     client: true
     default: 60