~mrooney/etherpad/ubuntu

« back to all changes in this revision

Viewing changes to trunk/trunk/etherpad/src/static/js/pad_userlist.js

  • Committer: Aaron Iba
  • Date: 2009-12-18 07:40:23 UTC
  • Revision ID: hg-v1:a9f8774a2e00cc15b35857471fecea17f649e3c9
initial code push

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
/**
 
2
 * Copyright 2009 Google Inc.
 
3
 * 
 
4
 * Licensed under the Apache License, Version 2.0 (the "License");
 
5
 * you may not use this file except in compliance with the License.
 
6
 * You may obtain a copy of the License at
 
7
 * 
 
8
 *      http://www.apache.org/licenses/LICENSE-2.0
 
9
 * 
 
10
 * Unless required by applicable law or agreed to in writing, software
 
11
 * distributed under the License is distributed on an "AS-IS" BASIS,
 
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 
13
 * See the License for the specific language governing permissions and
 
14
 * limitations under the License.
 
15
 */
 
16
 
 
17
 
 
18
var paduserlist = (function() {
 
19
 
 
20
  var rowManager = (function() {
 
21
    // The row manager handles rendering rows of the user list and animating
 
22
    // their insertion, removal, and reordering.  It manipulates TD height
 
23
    // and TD opacity.
 
24
 
 
25
    function nextRowId() {
 
26
      return "usertr"+(nextRowId.counter++);
 
27
    }
 
28
    nextRowId.counter = 1;
 
29
    // objects are shared; fields are "domId","data","animationStep"
 
30
    var rowsFadingOut = []; // unordered set
 
31
    var rowsFadingIn = []; // unordered set
 
32
    var rowsPresent = []; // in order
 
33
 
 
34
    var ANIMATION_START = -12; // just starting to fade in
 
35
    var ANIMATION_END = 12; // just finishing fading out
 
36
    function getAnimationHeight(step, power) {
 
37
      var a = Math.abs(step/12);
 
38
      if (power == 2) a = a*a;
 
39
      else if (power == 3) a = a*a*a;
 
40
      else if (power == 4) a = a*a*a*a;
 
41
      else if (power >= 5) a = a*a*a*a*a;
 
42
      return Math.round(26*(1-a));
 
43
    }
 
44
    var OPACITY_STEPS = 6;
 
45
 
 
46
    var ANIMATION_STEP_TIME = 20;
 
47
    var LOWER_FRAMERATE_FACTOR = 2;
 
48
    var scheduleAnimation = padutils.makeAnimationScheduler(animateStep, ANIMATION_STEP_TIME,
 
49
                                                            LOWER_FRAMERATE_FACTOR).scheduleAnimation;
 
50
 
 
51
    var NUMCOLS = 4;
 
52
 
 
53
    // we do lots of manipulation of table rows and stuff that JQuery makes ok, despite
 
54
    // IE's poor handling when manipulating the DOM directly.
 
55
 
 
56
    function getEmptyRowHtml(height) {
 
57
      return '<td colspan="'+NUMCOLS+'" style="border:0;height:'+height+'px"><!-- --></td>';
 
58
    }
 
59
    function isNameEditable(data) {
 
60
      return (! data.name) && (data.status != 'Disconnected');
 
61
    }
 
62
    function replaceUserRowContents(tr, height, data) {
 
63
      var tds = getUserRowHtml(height, data).match(/<td.*?<\/td>/gi);
 
64
      if (isNameEditable(data) && tr.find("td.usertdname input:enabled").length > 0) {
 
65
        // preserve input field node
 
66
        for(var i=0; i<tds.length; i++) {
 
67
          var oldTd = $(tr.find("td").get(i));
 
68
          if (! oldTd.hasClass('usertdname')) {
 
69
            oldTd.replaceWith(tds[i]);
 
70
          }
 
71
        }
 
72
      }
 
73
      else {
 
74
        tr.html(tds.join(''));
 
75
      }
 
76
      return tr;
 
77
    }
 
78
    function getUserRowHtml(height, data) {
 
79
      var nameHtml;
 
80
      var isGuest = (data.id.charAt(0) != 'p');
 
81
      if (data.name) {
 
82
        nameHtml = padutils.escapeHtml(data.name);
 
83
        if (isGuest && pad.getIsProPad()) {
 
84
          nameHtml += ' (Guest)';
 
85
        }
 
86
      }
 
87
      else {
 
88
        nameHtml = '<input type="text" class="editempty newinput" value="unnamed" '+
 
89
          (isNameEditable(data) ? '' : 'disabled="disabled" ')+
 
90
          '/>';
 
91
      }
 
92
 
 
93
      return ['<td style="height:',height,'px" class="usertdswatch"><div class="swatch" style="background:'+data.color+'">&nbsp;</div></td>',
 
94
              '<td style="height:',height,'px" class="usertdname">',nameHtml,'</td>',
 
95
              '<td style="height:',height,'px" class="usertdstatus">',padutils.escapeHtml(data.status),'</td>',
 
96
              '<td style="height:',height,'px" class="activity">',padutils.escapeHtml(data.activity),'</td>'].join('');
 
97
    }
 
98
    function getRowHtml(id, innerHtml) {
 
99
      return '<tr id="'+id+'">'+innerHtml+'</tr>';
 
100
    }
 
101
    function rowNode(row) {
 
102
      return $("#"+row.domId);
 
103
    }
 
104
    function handleRowData(row) {
 
105
      if (row.data && row.data.status == 'Disconnected') {
 
106
        row.opacity = 0.5;
 
107
      }
 
108
      else {
 
109
        delete row.opacity;
 
110
      }
 
111
    }
 
112
    function handleRowNode(tr, data) {
 
113
      if (data.titleText) {
 
114
        tr.attr('title', data.titleText);
 
115
      }
 
116
      else {
 
117
        tr.removeAttr('title');
 
118
      }
 
119
    }
 
120
    function handleOtherUserInputs() {
 
121
      // handle 'INPUT' elements for naming other unnamed users
 
122
      $("#otheruserstable input.newinput").each(function() {
 
123
        var input = $(this);
 
124
        var tr = input.closest("tr");
 
125
        if (tr.length > 0) {
 
126
          var index = tr.parent().children().index(tr);
 
127
          if (index >= 0) {
 
128
            var userId = rowsPresent[index].data.id;
 
129
            rowManagerMakeNameEditor($(this), userId);
 
130
          }
 
131
        }
 
132
      }).removeClass('newinput');
 
133
    }
 
134
 
 
135
    // animationPower is 0 to skip animation, 1 for linear, 2 for quadratic, etc.
 
136
    function insertRow(position, data, animationPower) {
 
137
      position = Math.max(0, Math.min(rowsPresent.length, position));
 
138
      animationPower = (animationPower === undefined ? 4 : animationPower);
 
139
 
 
140
      var domId = nextRowId();
 
141
      var row = {data: data, animationStep: ANIMATION_START, domId: domId,
 
142
                 animationPower: animationPower};
 
143
      handleRowData(row);
 
144
      rowsPresent.splice(position, 0, row);
 
145
      var tr;
 
146
      if (animationPower == 0) {
 
147
        tr = $(getRowHtml(domId, getUserRowHtml(getAnimationHeight(0), data)));
 
148
        row.animationStep = 0;
 
149
      }
 
150
      else {
 
151
        rowsFadingIn.push(row);
 
152
        tr = $(getRowHtml(domId, getEmptyRowHtml(getAnimationHeight(ANIMATION_START))));
 
153
      }
 
154
      handleRowNode(tr, data);
 
155
      if (position == 0) {
 
156
        $("table#otheruserstable").prepend(tr);
 
157
      }
 
158
      else {
 
159
        rowNode(rowsPresent[position-1]).after(tr);
 
160
      }
 
161
 
 
162
      if (animationPower != 0) {
 
163
        scheduleAnimation();
 
164
      }
 
165
 
 
166
      handleOtherUserInputs();
 
167
 
 
168
      return row;
 
169
    }
 
170
 
 
171
    function updateRow(position, data) {
 
172
      var row = rowsPresent[position];
 
173
      if (row) {
 
174
        row.data = data;
 
175
        handleRowData(row);
 
176
        if (row.animationStep == 0) {
 
177
          // not currently animating
 
178
          var tr = rowNode(row);
 
179
          replaceUserRowContents(tr, getAnimationHeight(0), row.data).find(
 
180
            "td").css('opacity', (row.opacity === undefined ? 1 : row.opacity));
 
181
          handleRowNode(tr, data);
 
182
          handleOtherUserInputs();
 
183
        }
 
184
      }
 
185
    }
 
186
 
 
187
    function removeRow(position, animationPower) {
 
188
      animationPower = (animationPower === undefined ? 4 : animationPower);
 
189
      var row = rowsPresent[position];
 
190
      if (row) {
 
191
        rowsPresent.splice(position, 1); // remove
 
192
        if (animationPower == 0) {
 
193
          rowNode(row).remove();
 
194
        }
 
195
        else {
 
196
          row.animationStep = - row.animationStep; // use symmetry
 
197
          row.animationPower = animationPower;
 
198
          rowsFadingOut.push(row);
 
199
          scheduleAnimation();
 
200
        }
 
201
      }
 
202
    }
 
203
 
 
204
    // newPosition is position after the row has been removed
 
205
    function moveRow(oldPosition, newPosition, animationPower) {
 
206
      animationPower = (animationPower === undefined ? 1 : animationPower); // linear is best
 
207
      var row = rowsPresent[oldPosition];
 
208
      if (row && oldPosition != newPosition) {
 
209
        var rowData = row.data;
 
210
        removeRow(oldPosition, animationPower);
 
211
        insertRow(newPosition, rowData, animationPower);
 
212
      }
 
213
    }
 
214
 
 
215
    function animateStep() {
 
216
      // animation must be symmetrical
 
217
      for(var i=rowsFadingIn.length-1;i>=0;i--) { // backwards to allow removal
 
218
        var row = rowsFadingIn[i];
 
219
        var step = ++row.animationStep;
 
220
        var animHeight = getAnimationHeight(step, row.animationPower);
 
221
        var node = rowNode(row);
 
222
        var baseOpacity = (row.opacity === undefined ? 1 : row.opacity);
 
223
        if (step <= -OPACITY_STEPS) {
 
224
          node.find("td").height(animHeight);
 
225
        }
 
226
        else if (step == -OPACITY_STEPS+1) {
 
227
          node.html(getUserRowHtml(animHeight, row.data)).find("td").css(
 
228
            'opacity', baseOpacity*1/OPACITY_STEPS);
 
229
          handleRowNode(node, row.data);
 
230
        }
 
231
        else if (step < 0) {
 
232
          node.find("td").css('opacity', baseOpacity*(OPACITY_STEPS-(-step))/OPACITY_STEPS).height(animHeight);
 
233
        }
 
234
        else if (step == 0) {
 
235
          // set HTML in case modified during animation
 
236
          node.html(getUserRowHtml(animHeight, row.data)).find("td").css(
 
237
            'opacity', baseOpacity*1).height(animHeight);
 
238
          handleRowNode(node, row.data);
 
239
          rowsFadingIn.splice(i, 1); // remove from set
 
240
        }
 
241
      }
 
242
      for(var i=rowsFadingOut.length-1;i>=0;i--) { // backwards to allow removal
 
243
        var row = rowsFadingOut[i];
 
244
        var step = ++row.animationStep;
 
245
        var node = rowNode(row);
 
246
        var animHeight = getAnimationHeight(step, row.animationPower);
 
247
        var baseOpacity = (row.opacity === undefined ? 1 : row.opacity);
 
248
        if (step < OPACITY_STEPS) {
 
249
          node.find("td").css('opacity', baseOpacity*(OPACITY_STEPS - step)/OPACITY_STEPS).height(animHeight);
 
250
        }
 
251
        else if (step == OPACITY_STEPS) {
 
252
          node.html(getEmptyRowHtml(animHeight));
 
253
        }
 
254
        else if (step <= ANIMATION_END) {
 
255
          node.find("td").height(animHeight);
 
256
        }
 
257
        else {
 
258
          rowsFadingOut.splice(i, 1); // remove from set
 
259
          node.remove();
 
260
        }
 
261
      }
 
262
 
 
263
      handleOtherUserInputs();
 
264
 
 
265
      return (rowsFadingIn.length > 0) || (rowsFadingOut.length > 0); // is more to do
 
266
    }
 
267
 
 
268
    var self = {
 
269
      insertRow: insertRow,
 
270
      removeRow: removeRow,
 
271
      moveRow: moveRow,
 
272
      updateRow: updateRow
 
273
    };
 
274
    return self;
 
275
  }()); ////////// rowManager
 
276
 
 
277
  var myUserInfo = {};
 
278
  var otherUsersInfo = [];
 
279
  var otherUsersData = [];
 
280
  var colorPickerOpen = false;
 
281
 
 
282
  function rowManagerMakeNameEditor(jnode, userId) {
 
283
    setUpEditable(jnode, function() {
 
284
      var existingIndex = findExistingIndex(userId);
 
285
      if (existingIndex >= 0) {
 
286
        return otherUsersInfo[existingIndex].name || '';
 
287
      }
 
288
      else {
 
289
        return '';
 
290
      }
 
291
    }, function(newName) {
 
292
      if (! newName) {
 
293
        jnode.addClass("editempty");
 
294
        jnode.val("unnamed");
 
295
      }
 
296
      else {
 
297
        jnode.attr('disabled', 'disabled');
 
298
        pad.suggestUserName(userId, newName);
 
299
      }
 
300
    });
 
301
  }
 
302
 
 
303
  function renderMyUserInfo() {
 
304
    if (myUserInfo.name) {
 
305
      $("#myusernameedit").removeClass("editempty").val(
 
306
        myUserInfo.name);
 
307
    }
 
308
    else {
 
309
      $("#myusernameedit").addClass("editempty").val(
 
310
        "< enter your name >");
 
311
    }
 
312
    if (colorPickerOpen) {
 
313
      $("#myswatchbox").addClass('myswatchboxunhoverable').removeClass(
 
314
        'myswatchboxhoverable');
 
315
    }
 
316
    else {
 
317
      $("#myswatchbox").addClass('myswatchboxhoverable').removeClass(
 
318
        'myswatchboxunhoverable');
 
319
    }
 
320
    $("#myswatch").css('background', pad.getColorPalette()[myUserInfo.colorId]);
 
321
  }
 
322
 
 
323
  function findExistingIndex(userId) {
 
324
    var existingIndex = -1;
 
325
    for(var i=0;i<otherUsersInfo.length;i++) {
 
326
      if (otherUsersInfo[i].userId == userId) {
 
327
        existingIndex = i;
 
328
        break;
 
329
      }
 
330
    }
 
331
    return existingIndex;
 
332
  }
 
333
 
 
334
  function setUpEditable(jqueryNode, valueGetter, valueSetter) {
 
335
    jqueryNode.bind('focus', function(evt) {
 
336
      var oldValue = valueGetter();
 
337
      if (jqueryNode.val() !== oldValue) {
 
338
        jqueryNode.val(oldValue);
 
339
      }
 
340
      jqueryNode.addClass("editactive").removeClass("editempty");
 
341
    });
 
342
    jqueryNode.bind('blur', function(evt) {
 
343
      var newValue = jqueryNode.removeClass("editactive").val();
 
344
      valueSetter(newValue);
 
345
    });
 
346
    padutils.bindEnterAndEscape(jqueryNode, function onEnter() {
 
347
      jqueryNode.blur();
 
348
    }, function onEscape() {
 
349
      jqueryNode.val(valueGetter()).blur();
 
350
    });
 
351
    jqueryNode.removeAttr('disabled').addClass('editable');
 
352
  }
 
353
 
 
354
  function showColorPicker() {
 
355
    if (! colorPickerOpen) {
 
356
      var palette = pad.getColorPalette();
 
357
      for(var i=0;i<palette.length;i++) {
 
358
        $("#mycolorpicker .n"+(i+1)+" .pickerswatch").css(
 
359
          'background', palette[i]);
 
360
      }
 
361
      $("#mycolorpicker").css('display', 'block');
 
362
      colorPickerOpen = true;
 
363
      renderMyUserInfo();
 
364
    }
 
365
    // this part happens even if color picker is already open
 
366
    $("#mycolorpicker .pickerswatchouter").removeClass('picked');
 
367
    $("#mycolorpicker .pickerswatchouter:eq("+(myUserInfo.colorId||0)+")").
 
368
      addClass('picked');
 
369
  }
 
370
  function getColorPickerSwatchIndex(jnode) {
 
371
    return Number(jnode.get(0).className.match(/\bn([0-9]+)\b/)[1])-1;
 
372
  }
 
373
  function closeColorPicker(accept) {
 
374
    if (accept) {
 
375
      var newColorId = getColorPickerSwatchIndex($("#mycolorpicker .picked"));
 
376
      if (newColorId >= 0) { // fails on NaN
 
377
        myUserInfo.colorId = newColorId;
 
378
        pad.notifyChangeColor(newColorId);
 
379
      }
 
380
    }
 
381
    colorPickerOpen = false;
 
382
    $("#mycolorpicker").css('display', 'none');
 
383
    renderMyUserInfo();
 
384
  }
 
385
 
 
386
  function updateInviteNotice() {
 
387
    if (otherUsersInfo.length == 0) {
 
388
      $("#otheruserstable").hide();
 
389
      $("#nootherusers").show();
 
390
    }
 
391
    else {
 
392
      $("#nootherusers").hide();
 
393
      $("#otheruserstable").show();
 
394
    }
 
395
  }
 
396
 
 
397
  var knocksToIgnore = {};
 
398
  var guestPromptFlashState = 0;
 
399
  var guestPromptFlash = padutils.makeAnimationScheduler(
 
400
    function () {
 
401
      var prompts = $("#guestprompts .guestprompt");
 
402
      if (prompts.length == 0) {
 
403
        return false; // no more to do
 
404
      }
 
405
 
 
406
      guestPromptFlashState = 1 - guestPromptFlashState;
 
407
      if (guestPromptFlashState) {
 
408
        prompts.css('background', '#ffa');
 
409
      }
 
410
      else {
 
411
        prompts.css('background', '#ffe');
 
412
      }
 
413
 
 
414
      return true;
 
415
    }, 1000);
 
416
 
 
417
  var self = {
 
418
    init: function(myInitialUserInfo) {
 
419
      self.setMyUserInfo(myInitialUserInfo);
 
420
 
 
421
      $("#otheruserstable tr").remove();
 
422
 
 
423
      if (pad.getUserIsGuest()) {
 
424
        $("#myusernameedit").addClass('myusernameedithoverable');
 
425
        setUpEditable($("#myusernameedit"),
 
426
                      function() {
 
427
                        return myUserInfo.name || '';
 
428
                      },
 
429
                      function(newValue) {
 
430
                        myUserInfo.name = newValue;
 
431
                        pad.notifyChangeName(newValue);
 
432
                        // wrap with setTimeout to do later because we get
 
433
                        // a double "blur" fire in IE...
 
434
                        window.setTimeout(function() {
 
435
                          renderMyUserInfo();
 
436
                        }, 0);
 
437
                      });
 
438
      }
 
439
 
 
440
      // color picker
 
441
      $("#myswatchbox").click(showColorPicker);
 
442
      $("#mycolorpicker .pickerswatchouter").click(function() {
 
443
        $("#mycolorpicker .pickerswatchouter").removeClass('picked');
 
444
        $(this).addClass('picked');
 
445
      });
 
446
      $("#mycolorpickersave").click(function() {
 
447
        closeColorPicker(true);
 
448
      });
 
449
      $("#mycolorpickercancel").click(function() {
 
450
        closeColorPicker(false);
 
451
      });
 
452
      //
 
453
 
 
454
    },
 
455
    setMyUserInfo: function(info) {
 
456
      myUserInfo = $.extend({}, info);
 
457
 
 
458
      renderMyUserInfo();
 
459
    },
 
460
    userJoinOrUpdate: function(info) {
 
461
      if ((! info.userId) || (info.userId == myUserInfo.userId)) {
 
462
        // not sure how this would happen
 
463
        return;
 
464
      }
 
465
 
 
466
      var userData = {};
 
467
      userData.color = pad.getColorPalette()[info.colorId];
 
468
      userData.name = info.name;
 
469
      userData.status = '';
 
470
      userData.activity = '';
 
471
      userData.id = info.userId;
 
472
      // Firefox ignores \n in title text; Safari does a linebreak
 
473
      userData.titleText = [info.userAgent||'', info.ip||''].join(' \n');
 
474
 
 
475
      var existingIndex = findExistingIndex(info.userId);
 
476
 
 
477
      var numUsersBesides = otherUsersInfo.length;
 
478
      if (existingIndex >= 0) {
 
479
        numUsersBesides--;
 
480
      }
 
481
      var newIndex = padutils.binarySearch(numUsersBesides, function(n) {
 
482
        if (existingIndex >= 0 && n >= existingIndex) {
 
483
          // pretend existingIndex isn't there
 
484
          n++;
 
485
        }
 
486
        var infoN = otherUsersInfo[n];
 
487
        var nameN = (infoN.name||'').toLowerCase();
 
488
        var nameThis = (info.name||'').toLowerCase();
 
489
        var idN = infoN.userId;
 
490
        var idThis = info.userId;
 
491
        return (nameN > nameThis) || (nameN == nameThis &&
 
492
                                      idN > idThis);
 
493
      });
 
494
 
 
495
      if (existingIndex >= 0) {
 
496
        // update
 
497
        if (existingIndex == newIndex) {
 
498
          otherUsersInfo[existingIndex] = info;
 
499
          otherUsersData[existingIndex] = userData;
 
500
          rowManager.updateRow(existingIndex, userData);
 
501
        }
 
502
        else {
 
503
          otherUsersInfo.splice(existingIndex, 1);
 
504
          otherUsersData.splice(existingIndex, 1);
 
505
          otherUsersInfo.splice(newIndex, 0, info);
 
506
          otherUsersData.splice(newIndex, 0, userData);
 
507
          rowManager.updateRow(existingIndex, userData);
 
508
          rowManager.moveRow(existingIndex, newIndex);
 
509
        }
 
510
      }
 
511
      else {
 
512
        otherUsersInfo.splice(newIndex, 0, info);
 
513
        otherUsersData.splice(newIndex, 0, userData);
 
514
        rowManager.insertRow(newIndex, userData);
 
515
      }
 
516
 
 
517
      updateInviteNotice();
 
518
    },
 
519
    userLeave: function(info) {
 
520
      var existingIndex = findExistingIndex(info.userId);
 
521
      if (existingIndex >= 0) {
 
522
        var userData = otherUsersData[existingIndex];
 
523
        userData.status = 'Disconnected';
 
524
        rowManager.updateRow(existingIndex, userData);
 
525
        if (userData.leaveTimer) {
 
526
          window.clearTimeout(userData.leaveTimer);
 
527
        }
 
528
        // set up a timer that will only fire if no leaves,
 
529
        // joins, or updates happen for this user in the
 
530
        // next N seconds, to remove the user from the list.
 
531
        var thisUserId = info.userId;
 
532
        var thisLeaveTimer = window.setTimeout(function() {
 
533
          var newExistingIndex = findExistingIndex(thisUserId);
 
534
          if (newExistingIndex >= 0) {
 
535
            var newUserData = otherUsersData[newExistingIndex];
 
536
            if (newUserData.status == 'Disconnected' &&
 
537
                newUserData.leaveTimer == thisLeaveTimer) {
 
538
              otherUsersInfo.splice(newExistingIndex, 1);
 
539
              otherUsersData.splice(newExistingIndex, 1);
 
540
              rowManager.removeRow(newExistingIndex);
 
541
              updateInviteNotice();
 
542
            }
 
543
          }
 
544
        }, 8000); // how long to wait
 
545
        userData.leaveTimer = thisLeaveTimer;
 
546
      }
 
547
      updateInviteNotice();
 
548
    },
 
549
    showGuestPrompt: function(userId, displayName) {
 
550
      if (knocksToIgnore[userId]) {
 
551
        return;
 
552
      }
 
553
 
 
554
      var encodedUserId = padutils.encodeUserId(userId);
 
555
 
 
556
      var actionName = 'hide-guest-prompt-'+encodedUserId;
 
557
      padutils.cancelActions(actionName);
 
558
 
 
559
      var box = $("#guestprompt-"+encodedUserId);
 
560
      if (box.length == 0) {
 
561
        // make guest prompt box
 
562
        box = $('<div id="guestprompt-'+encodedUserId+'" class="guestprompt"><div class="choices"><a href="javascript:void(paduserlist.answerGuestPrompt(\''+encodedUserId+'\',false))">Deny</a> <a href="javascript:void(paduserlist.answerGuestPrompt(\''+encodedUserId+'\',true))">Approve</a></div><div class="guestname"><strong>Guest:</strong> '+padutils.escapeHtml(displayName)+'</div></div>');
 
563
        $("#guestprompts").append(box);
 
564
      }
 
565
      else {
 
566
        // update display name
 
567
        box.find(".guestname").html('<strong>Guest:</strong> '+padutils.escapeHtml(displayName));
 
568
      }
 
569
      var hideLater = padutils.getCancellableAction(actionName, function() {
 
570
        self.removeGuestPrompt(userId);
 
571
      });
 
572
      window.setTimeout(hideLater, 15000); // time-out with no knock
 
573
 
 
574
      guestPromptFlash.scheduleAnimation();
 
575
    },
 
576
    removeGuestPrompt: function(userId) {
 
577
      var box = $("#guestprompt-"+padutils.encodeUserId(userId));
 
578
      // remove ID now so a new knock by same user gets new, unfaded box
 
579
      box.removeAttr('id').fadeOut("fast", function() {
 
580
        box.remove();
 
581
      });
 
582
 
 
583
      knocksToIgnore[userId] = true;
 
584
      window.setTimeout(function() {
 
585
        delete knocksToIgnore[userId];
 
586
      }, 5000);
 
587
    },
 
588
    answerGuestPrompt: function(encodedUserId, approve) {
 
589
      var guestId = padutils.decodeUserId(encodedUserId);
 
590
 
 
591
      var msg = {
 
592
        type: 'guestanswer',
 
593
        authId: pad.getUserId(),
 
594
        guestId: guestId,
 
595
        answer: (approve ? "approved" : "denied")
 
596
      };
 
597
      pad.sendClientMessage(msg);
 
598
 
 
599
      self.removeGuestPrompt(guestId);
 
600
    }
 
601
  };
 
602
  return self;
 
603
}());
 
604