Merge branch 'hotfix/21.56.9' into master
[gitter.git] / public / js / views / app / addPeopleView.js
blob1da0cccbc357189eaba9f3de7dac53d3eb05f78f
1 'use strict';
2 var Marionette = require('backbone.marionette');
3 var ModalView = require('../modals/modal');
4 var Backbone = require('backbone');
5 var clientEnv = require('gitter-client-env');
6 var avatars = require('gitter-web-avatars');
7 var context = require('gitter-web-client-context');
8 var apiClient = require('../../components/api-client');
9 var Typeahead = require('../controls/typeahead');
10 var userSearchModels = require('../../collections/user-search');
11 var template = require('./tmpl/addPeople.hbs');
12 var userSearchItemTemplate = require('./tmpl/userSearchItem.hbs');
13 var itemTemplate = require('./tmpl/addPeopleItemView.hbs');
14 require('../behaviors/widgets');
16 var DEFAULT_AVATAR_UNTIL_AVATARS_SERVICE_ARRIVES = avatars.getDefault();
17 /**
18  *  Ridiculously sloppy regexp based email validator, let the server
19  *  do the real validation
20  */
21 function isEmailAddress(string) {
22   return /^[^@]+@[^@]+\.[^@]+$/.test(string);
25 var RowView = Marionette.ItemView.extend({
26   events: {
27     'submit form': 'invite'
28   },
29   modelEvents: {
30     change: 'render'
31   },
32   behaviors: {
33     Widgets: {}
34   },
35   ui: {
36     email: 'input[type=email]'
37   },
38   tagName: 'div',
39   className: 'gtrPeopleRosterItem',
40   template: itemTemplate,
41   invite: function(e) {
42     e.preventDefault();
43     var model = this.model;
44     var email = this.ui.email.val().trim();
46     var self = this;
48     apiClient.room
49       .post('/invites', { githubUsername: this.model.get('username'), email: email })
50       .then(function() {
51         model.set({
52           email: email,
53           unreachable: false,
54           invited: true,
55           added: false
56         });
57       })
58       .catch(function(e) {
59         var message = e.friendlyMessage || 'Unable to invite user to Gitter';
60         self.trigger('invite:error', message);
61       });
62   }
63 });
65 var View = Marionette.CompositeView.extend({
66   childViewContainer: '.gtrPeopleAddRoster',
67   childView: RowView,
68   template: template,
69   ui: {
70     input: '.gtrInput',
71     share: '.js-add-people-share',
72     loading: '.js-add-roster-loading',
73     validation: '#modal-failure',
74     success: '#modal-success'
75   },
77   initialize: function() {
78     if (!this.collection) {
79       var ResultsCollection = Backbone.Collection.extend({
80         comparator: function(a, b) {
81           return b.get('timeAdded') - a.get('timeAdded');
82         }
83       });
85       this.collection = new ResultsCollection();
86     }
88     this.listenTo(this, 'menuItemClicked', this.menuItemClicked);
89   },
91   onChildviewInviteError: function(childView, message) {
92     // jshint unused:true
93     this.ui.loading.toggleClass('hide', true);
94     this.showValidationMessage(message);
95   },
97   selected: function(m) {
98     this.addUserToRoom(m);
99     this.typeahead.dropdown.hide();
100   },
102   menuItemClicked: function(button) {
103     switch (button) {
104       case 'share':
105         this.dialog.hide();
106         window.location.hash = '#share';
107         break;
109       case 'done':
110         this.dialog.hide();
111         break;
112     }
113   },
115   /**
116    * showMessage() slides the given element down then up
117    *
118    * el   DOM Element - element to be animated
119    */
120   showMessage: function(el) {
121     el.slideDown('fast');
122     setTimeout(function() {
123       el.slideUp('fast');
124       return;
125     }, 10000);
126   },
128   showValidationMessage: function(message) {
129     this.ui.validation.text(message);
130     this.showMessage(this.ui.validation);
131   },
133   showSuccessMessage: function(message) {
134     this.ui.success.text(message);
135     this.showMessage(this.ui.success);
136   },
138   handleError: function(/*res, status, message */) {
139     // TODO: what should go here?
140   },
142   /**
143    * addUserToRoom() sends request and handles response of adding an user to a room
144    *
145    * m    BackboneModel - the user to be added to the room
146    */
147   addUserToRoom: function(model) {
148     var self = this;
150     self.ui.loading.toggleClass('hide');
151     var username = model.get('username');
152     var email = model.get('email');
153     var body;
154     if (username) {
155       body = { githubUsername: username };
156     } else if (email) {
157       body = { email: email.trim() };
158     }
160     return apiClient.room
161       .post('/invites', body)
162       .then(function(invite) {
163         self.ui.loading.toggleClass('hide');
164         model.set({
165           added: invite.status === 'added',
166           invited: invite.status === 'invited',
167           unreachable: false,
168           timeAdded: Date.now(),
169           email: invite.email,
170           user: invite.user,
171           username: (invite.user && invite.user.username) || username
172         });
174         self.collection.add(model);
175         self.typeahead.clear();
176       })
177       .catch(function(e) {
178         self.ui.loading.toggleClass('hide');
179         var message = e.friendlyMessage || 'Error';
181         // XXX: why not use the payment required status code for this?
182         if (message.match(/has reached its limit/)) {
183           self.dialog.showPremium();
184         }
186         self.typeahead.clear();
187         switch (e.status) {
188           case 501:
189             message = `Inviting a user by email is limited to ${
190               clientEnv['inviteEmailAbuseThresholdPerDay']
191             } per day, see #2153`;
192             break;
193           case 409:
194             message = model.get('username') + ' has already been invited';
195             break;
196           case 428:
197             model.set({
198               added: false,
199               invited: false,
200               unreachable: true,
201               timeAdded: Date.now(),
202               email: null,
203               user: null
204             });
205             self.collection.add(model);
206             return;
207         }
209         self.showValidationMessage(message);
210       });
211   },
213   onRender: function() {
214     var self = this;
216     setTimeout(function() {
217       self.ui.input.focus();
218     }, 10);
220     this.typeahead = new Typeahead({
221       collection: new userSearchModels.Collection(),
222       itemTemplate: userSearchItemTemplate,
223       el: this.ui.input[0],
224       autoSelector: function(input) {
225         return function(m) {
226           var displayName = m.get('displayName');
227           var username = m.get('username');
229           return (
230             (displayName && displayName.indexOf(input) >= 0) ||
231             (username && username.indexOf(input) >= 0)
232           );
233         };
234       },
235       fetch: function(input, collection, fetchSuccess) {
236         if (input.indexOf('@') >= 0) {
237           if (isEmailAddress(input)) {
238             this.collection.reset([
239               {
240                 displayName: input,
241                 email: input,
242                 avatarUrlSmall: DEFAULT_AVATAR_UNTIL_AVATARS_SERVICE_ARRIVES,
243                 avatarUrlMedium: DEFAULT_AVATAR_UNTIL_AVATARS_SERVICE_ARRIVES
244               }
245             ]);
246           } else {
247             this.collection.reset([]);
248           }
250           return fetchSuccess();
251         }
253         this.collection.fetch(
254           { data: { q: input } },
255           { add: true, remove: true, merge: true, success: fetchSuccess }
256         );
257       }
258     });
260     this.listenTo(this.typeahead, 'selected', this.selected);
261   },
263   onDestroy: function() {
264     if (this.typeahead) {
265       this.typeahead.destroy();
266     }
267   }
270 var modalButtons = [];
272 if (context.troupe().get('security') !== 'PRIVATE') {
273   modalButtons.push({
274     action: 'share',
275     pull: 'right',
276     text: 'Share this room',
277     className: 'modal--default__footer__link'
278   });
281 module.exports = ModalView.extend({
282   disableAutoFocus: true,
283   initialize: function(options) {
284     options = options || {};
285     options.title = options.title || 'Add people to this room';
287     ModalView.prototype.initialize.call(this, options);
288     this.view = new View(options);
289   },
290   menuItems: modalButtons