]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/Realtime/realtimeupdate.js
Localisation updates from http://translatewiki.net.
[quix0rs-gnu-social.git] / plugins / Realtime / realtimeupdate.js
1 /*
2  * StatusNet - a distributed open-source microblogging tool
3  * Copyright (C) 2009-2011, StatusNet, Inc.
4  *
5  * Add a notice encoded as JSON into the current timeline
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU Affero General Public License as published by
9  * the Free Software Foundation, either version 3 of the License, or
10  * (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU Affero General Public License for more details.
16  *
17  * You should have received a copy of the GNU Affero General Public License
18  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
19  *
20  * @category  Plugin
21  * @package   StatusNet
22  * @author    Evan Prodromou <evan@status.net>
23  * @author    Sarven Capadisli <csarven@status.net>
24  * @copyright 2009-2011 StatusNet, Inc.
25  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
26  * @link      http://status.net/
27  */
28
29 /**
30  * This is the UI portion of the Realtime plugin base class, handling
31  * queueing up and displaying of notices that have been received through
32  * other code in one of the subclassed plugin implementations such as
33  * Meteor or Orbited.
34  *
35  * Notices are passed in as JSON objects formatted per the Twitter-compatible
36  * API.
37  *
38  * @todo Currently we duplicate a lot of formatting and layout code from
39  *       the PHP side of StatusNet, which makes it very difficult to maintain
40  *       this package. Internationalization as well as newer features such
41  *       as location data, customized source links for OStatus profiles,
42  *       and image thumbnails are not yet supported in Realtime yet because
43  *       they have not been implemented here.
44  */
45 RealtimeUpdate = {
46      _userid: 0,
47      _showurl: '',
48      _keepaliveurl: '',
49      _closeurl: '',
50      _updatecounter: 0,
51      _maxnotices: 50,
52      _windowhasfocus: true,
53      _documenttitle: '',
54      _paused:false,
55      _queuedNotices:[],
56
57      /**
58       * Initialize the Realtime plugin UI on a page with a timeline view.
59       *
60       * This function is called from a JS fragment inserted by the PHP side
61       * of the Realtime plugin, and provides us with base information
62       * needed to build a near-replica of StatusNet's NoticeListItem output.
63       *
64       * Once the UI is initialized, a plugin subclass will need to actually
65       * feed data into the RealtimeUpdate object!
66       *
67       * @param {int} userid: local profile ID of the currently logged-in user
68       * @param {String} showurl: URL for shownotice action, used when fetching formatting notices.
69       *                            This URL contains a stub value of 0000000000 which will be replaced with the notice ID.
70       *
71       * @access public
72       */
73      init: function(userid, showurl)
74      {
75         RealtimeUpdate._userid = userid;
76         RealtimeUpdate._showurl = showurl;
77
78         RealtimeUpdate._documenttitle = document.title;
79
80         $(window).bind('focus', function() {
81           RealtimeUpdate._windowhasfocus = true;
82
83           // Clear the counter on the window title when we focus in.
84           RealtimeUpdate._updatecounter = 0;
85           RealtimeUpdate.removeWindowCounter();
86         });
87
88         $(window).bind('blur', function() {
89           $('#notices_primary .notice').removeClass('mark-top');
90
91           $('#notices_primary .notice:first').addClass('mark-top');
92
93           // While we're in the background, received messages will increment
94           // a counter that we put on the window title. This will cause some
95           // browsers to also flash or mark the tab or window title bar until
96           // you seek attention (eg Firefox 4 pinned app tabs).
97           RealtimeUpdate._windowhasfocus = false;
98
99           return false;
100         });
101      },
102
103      /**
104       * Accept a notice in a Twitter-API JSON style and either show it
105       * or queue it up, depending on whether the realtime display is
106       * active.
107       *
108       * The meat of a Realtime plugin subclass is to provide a substrate
109       * transport to receive data and shove it into this function. :)
110       *
111       * Note that the JSON data is extended from the standard API return
112       * with additional fields added by RealtimePlugin's PHP code.
113       *
114       * @param {Object} data: extended JSON API-formatted notice
115       *
116       * @access public
117       */
118      receive: function(data)
119      {
120           if (RealtimeUpdate.isNoticeVisible(data.id)) {
121               // Probably posted by the user in this window, and so already
122               // shown by the AJAX form handler. Ignore it.
123               return;
124           }
125           if (RealtimeUpdate._paused === false) {
126               RealtimeUpdate.purgeLastNoticeItem();
127
128               RealtimeUpdate.insertNoticeItem(data);
129           }
130           else {
131               RealtimeUpdate._queuedNotices.push(data);
132
133               RealtimeUpdate.updateQueuedCounter();
134           }
135
136           RealtimeUpdate.updateWindowCounter();
137      },
138
139      /**
140       * Add a visible representation of the given notice at the top of
141       * the current timeline.
142       *
143       * If the notice is already in the timeline, nothing will be added.
144       *
145       * @param {Object} data: extended JSON API-formatted notice
146       *
147       * @fixme while core UI JS code is used to activate the AJAX UI controls,
148       *        the actual production of HTML (in makeNoticeItem and its subs)
149       *        duplicates core code without plugin hook points or i18n support.
150       *
151       * @access private
152       */
153      insertNoticeItem: function(data) {
154         // Don't add it if it already exists
155         if (RealtimeUpdate.isNoticeVisible(data.id)) {
156             return;
157         }
158
159         RealtimeUpdate.makeNoticeItem(data, function(noticeItem) {
160             // Check again in case it got shown while we were waiting for data...
161             if (RealtimeUpdate.isNoticeVisible(data.id)) {
162                 return;
163             }
164             var noticeItemID = $(noticeItem).attr('id');
165
166             var list = $("#notices_primary .notices:first")
167             var prepend = true;
168
169             var threaded = list.hasClass('threaded-notices');
170             if (threaded && data.in_reply_to_status_id) {
171                 // aho!
172                 var parent = $('#notice-' + data.in_reply_to_status_id);
173                 if (parent.length == 0) {
174                     // @todo fetch the original, insert it, and finish the rest
175                 } else {
176                     // Check the parent notice to make sure it's not a reply itself.
177                     // If so, use it's parent as the parent.
178                     var parentList = parent.closest('.notices');
179                     if (parentList.hasClass('threaded-replies')) {
180                         parent = parentList.closest('.notice');
181                     }
182                     list = parent.find('.threaded-replies');
183                     if (list.length == 0) {
184                         list = $('<ul class="notices threaded-replies xoxo"></ul>');
185                         parent.append(list);
186                         SN.U.NoticeInlineReplyPlaceholder(parent);
187                     }
188                     prepend = false;
189                 }
190             }
191
192             var newNotice = $(noticeItem);
193             if (prepend) {
194                 list.prepend(newNotice);
195             } else {
196                 var placeholder = list.find('li.notice-reply-placeholder')
197                 if (placeholder.length > 0) {
198                     newNotice.insertBefore(placeholder)
199                 } else {
200                     newNotice.appendTo(list);
201                 }
202             }
203             newNotice.css({display:"none"}).fadeIn(1000);
204
205             SN.U.NoticeReplyTo($('#'+noticeItemID));
206             SN.U.NoticeWithAttachment($('#'+noticeItemID));
207         });
208      },
209
210      /**
211       * Check if the given notice is visible in the timeline currently.
212       * Used to avoid duplicate processing of notices that have been
213       * displayed by other means.
214       *
215       * @param {number} id: notice ID to check
216       *
217       * @return boolean
218       *
219       * @access private
220       */
221      isNoticeVisible: function(id) {
222         return ($("#notice-"+id).length > 0);
223      },
224
225      /**
226       * Trims a notice off the end of the timeline if we have more than the
227       * maximum number of notices visible.
228       *
229       * @access private
230       */
231      purgeLastNoticeItem: function() {
232         if ($('#notices_primary .notice').length > RealtimeUpdate._maxnotices) {
233             $("#notices_primary .notice:last").remove();
234         }
235      },
236
237      /**
238       * If the window/tab is in background, increment the counter of newly
239       * received notices and append it onto the window title.
240       *
241       * Has no effect if the window is in foreground.
242       *
243       * @access private
244       */
245      updateWindowCounter: function() {
246           if (RealtimeUpdate._windowhasfocus === false) {
247               RealtimeUpdate._updatecounter += 1;
248               document.title = '('+RealtimeUpdate._updatecounter+') ' + RealtimeUpdate._documenttitle;
249           }
250      },
251
252      /**
253       * Clear the background update counter from the window title.
254       *
255       * @access private
256       *
257       * @fixme could interfere with anything else trying similar tricks
258       */
259      removeWindowCounter: function() {
260           document.title = RealtimeUpdate._documenttitle;
261      },
262
263      /**
264       * Builds a notice HTML block from JSON API-style data;
265       * loads data from server, so runs async.
266       *
267       * @param {Object} data: extended JSON API-formatted notice
268       * @param {function} callback: function(DOMNode) to receive new code
269       *
270       * @access private
271       */
272      makeNoticeItem: function(data, callback)
273      {
274          var url = RealtimeUpdate._showurl.replace('0000000000', data.id);
275          $.get(url, {ajax: 1}, function(data, textStatus, xhr) {
276              var notice = $('li.notice:first', data);
277              if (notice.length) {
278                  var node = document._importNode(notice[0], true);
279                  callback(node);
280              }
281          });
282      },
283
284      /**
285       * Creates a favorite button.
286       *
287       * @param {number} id: notice ID to work with
288       * @param {String} session_key: session token for form CSRF protection
289       * @return {String} HTML fragment
290       *
291       * @fixme this replicates core StatusNet code, making maintenance harder
292       * @fixme sloppy HTML building (raw concat without escaping)
293       * @fixme no i18n support
294       *
295       * @access private
296       */
297      makeFavoriteForm: function(id, session_key)
298      {
299           var ff;
300
301           ff = "<form id=\"favor-"+id+"\" class=\"form_favor\" method=\"post\" action=\""+RealtimeUpdate._favorurl+"\">"+
302                 "<fieldset>"+
303                "<legend>Favor this notice</legend>"+
304                "<input name=\"token-"+id+"\" type=\"hidden\" id=\"token-"+id+"\" value=\""+session_key+"\"/>"+
305                "<input name=\"notice\" type=\"hidden\" id=\"notice-n"+id+"\" value=\""+id+"\"/>"+
306                "<input type=\"submit\" id=\"favor-submit-"+id+"\" name=\"favor-submit-"+id+"\" class=\"submit\" value=\"Favor\" title=\"Favor this notice\"/>"+
307                 "</fieldset>"+
308                "</form>";
309           return ff;
310      },
311
312      /**
313       * Creates a reply button.
314       *
315       * @param {number} id: notice ID to work with
316       * @param {String} nickname: nick of the user to whom we are replying
317       * @return {String} HTML fragment
318       *
319       * @fixme this replicates core StatusNet code, making maintenance harder
320       * @fixme sloppy HTML building (raw concat without escaping)
321       * @fixme no i18n support
322       *
323       * @access private
324       */
325      makeReplyLink: function(id, nickname)
326      {
327           var rl;
328           rl = "<a class=\"notice_reply\" href=\""+RealtimeUpdate._replyurl+"?replyto="+nickname+"\" title=\"Reply to this notice\">Reply <span class=\"notice_id\">"+id+"</span></a>";
329           return rl;
330      },
331
332      /**
333       * Creates a repeat button.
334       *
335       * @param {number} id: notice ID to work with
336       * @param {String} session_key: session token for form CSRF protection
337       * @return {String} HTML fragment
338       *
339       * @fixme this replicates core StatusNet code, making maintenance harder
340       * @fixme sloppy HTML building (raw concat without escaping)
341       * @fixme no i18n support
342       *
343       * @access private
344       */
345      makeRepeatForm: function(id, session_key)
346      {
347           var rf;
348           rf = "<form id=\"repeat-"+id+"\" class=\"form_repeat\" method=\"post\" action=\""+RealtimeUpdate._repeaturl+"\">"+
349                "<fieldset>"+
350                "<legend>Repeat this notice?</legend>"+
351                "<input name=\"token-"+id+"\" type=\"hidden\" id=\"token-"+id+"\" value=\""+session_key+"\"/>"+
352                "<input name=\"notice\" type=\"hidden\" id=\"notice-"+id+"\" value=\""+id+"\"/>"+
353                "<input type=\"submit\" id=\"repeat-submit-"+id+"\" name=\"repeat-submit-"+id+"\" class=\"submit\" value=\"Yes\" title=\"Repeat this notice\"/>"+
354                "</fieldset>"+
355                "</form>";
356
357           return rf;
358      },
359
360      /**
361       * Creates a delete button.
362       *
363       * @param {number} id: notice ID to create a delete link for
364       * @return {String} HTML fragment
365       *
366       * @fixme this replicates core StatusNet code, making maintenance harder
367       * @fixme sloppy HTML building (raw concat without escaping)
368       * @fixme no i18n support
369       *
370       * @access private
371       */
372      makeDeleteLink: function(id)
373      {
374           var dl, delurl;
375           delurl = RealtimeUpdate._deleteurl.replace("0000000000", id);
376
377           dl = "<a class=\"notice_delete\" href=\""+delurl+"\" title=\"Delete this notice\">Delete</a>";
378
379           return dl;
380      },
381
382      /**
383       * Adds a control widget at the top of the timeline view, containing
384       * pause/play and popup buttons.
385       *
386       * @param {String} url: full URL to the popup window variant of this timeline page
387       * @param {String} timeline: string key for the timeline (eg 'public' or 'evan-all')
388       * @param {String} path: URL to the base directory containing the Realtime plugin,
389       *                       used to fetch resources if needed.
390       *
391       * @todo timeline and path parameters are unused and probably should be removed.
392       *
393       * @access private
394       */
395     initActions: function(url, timeline, path, keepaliveurl, closeurl)
396      {
397         $('#notices_primary').prepend('<ul id="realtime_actions"><li id="realtime_playpause"></li><li id="realtime_timeline"></li></ul>');
398
399         RealtimeUpdate._pluginPath = path;
400         RealtimeUpdate._keepaliveurl = keepaliveurl;
401         RealtimeUpdate._closeurl = closeurl;
402
403
404          // On unload, let the server know we're no longer listening
405          $(window).unload(function() {
406             $.ajax({
407                 type: 'POST',
408                 url: RealtimeUpdate._closeurl});
409          });
410
411         setInterval(function() {
412             $.ajax({
413                 type: 'POST',
414                 url: RealtimeUpdate._keepaliveurl});
415
416         }, 15 * 60 * 1000 ); // every 15 min; timeout in 30 min
417
418         RealtimeUpdate.initPlayPause();
419         RealtimeUpdate.initAddPopup(url, timeline, RealtimeUpdate._pluginPath);
420      },
421
422      /**
423       * Initialize the state of the play/pause controls.
424       *
425       * If the browser supports the localStorage interface, we'll attempt
426       * to retrieve a pause state from there; otherwise we default to paused.
427       *
428       * @access private
429       */
430      initPlayPause: function()
431      {
432         if (typeof(localStorage) == 'undefined') {
433             RealtimeUpdate.showPause();
434         }
435         else {
436             if (localStorage.getItem('RealtimeUpdate_paused') === 'true') {
437                 RealtimeUpdate.showPlay();
438             }
439             else {
440                 RealtimeUpdate.showPause();
441             }
442         }
443      },
444
445      /**
446       * Switch the realtime UI into paused state.
447       * Uses SN.msg i18n system for the button label and tooltip.
448       *
449       * State will be saved and re-used next time if the browser supports
450       * the localStorage interface (via setPause).
451       *
452       * @access private
453       */
454      showPause: function()
455      {
456         RealtimeUpdate.setPause(false);
457         RealtimeUpdate.showQueuedNotices();
458         RealtimeUpdate.addNoticesHover();
459
460         $('#realtime_playpause').remove();
461         $('#realtime_actions').prepend('<li id="realtime_playpause"><button id="realtime_pause" class="pause"></button></li>');
462         $('#realtime_pause').text(SN.msg('realtime_pause'))
463                             .attr('title', SN.msg('realtime_pause_tooltip'))
464                             .bind('click', function() {
465             RealtimeUpdate.removeNoticesHover();
466             RealtimeUpdate.showPlay();
467             return false;
468         });
469      },
470
471      /**
472       * Switch the realtime UI into play state.
473       * Uses SN.msg i18n system for the button label and tooltip.
474       *
475       * State will be saved and re-used next time if the browser supports
476       * the localStorage interface (via setPause).
477       *
478       * @access private
479       */
480      showPlay: function()
481      {
482         RealtimeUpdate.setPause(true);
483         $('#realtime_playpause').remove();
484         $('#realtime_actions').prepend('<li id="realtime_playpause"><span id="queued_counter"></span> <button id="realtime_play" class="play"></button></li>');
485         $('#realtime_play').text(SN.msg('realtime_play'))
486                            .attr('title', SN.msg('realtime_play_tooltip'))
487                            .bind('click', function() {
488             RealtimeUpdate.showPause();
489             return false;
490         });
491      },
492
493      /**
494       * Update the internal pause/play state.
495       * Do not call directly; use showPause() and showPlay().
496       *
497       * State will be saved and re-used next time if the browser supports
498       * the localStorage interface.
499       *
500       * @param {boolean} state: true = paused, false = not paused
501       *
502       * @access private
503       */
504      setPause: function(state)
505      {
506         RealtimeUpdate._paused = state;
507         if (typeof(localStorage) != 'undefined') {
508             localStorage.setItem('RealtimeUpdate_paused', RealtimeUpdate._paused);
509         }
510      },
511
512      /**
513       * Go through notices we have previously received while paused,
514       * dumping them into the timeline view.
515       *
516       * @fixme long timelines are not trimmed here as they are for things received while not paused
517       *
518       * @access private
519       */
520      showQueuedNotices: function()
521      {
522         $.each(RealtimeUpdate._queuedNotices, function(i, n) {
523             RealtimeUpdate.insertNoticeItem(n);
524         });
525
526         RealtimeUpdate._queuedNotices = [];
527
528         RealtimeUpdate.removeQueuedCounter();
529      },
530
531      /**
532       * Update the Realtime widget control's counter of queued notices to show
533       * the current count. This will be called after receiving and queueing
534       * a notice while paused.
535       *
536       * @access private
537       */
538      updateQueuedCounter: function()
539      {
540         $('#realtime_playpause #queued_counter').html('('+RealtimeUpdate._queuedNotices.length+')');
541      },
542
543      /**
544       * Clear the Realtime widget control's counter of queued notices.
545       *
546       * @access private
547       */
548      removeQueuedCounter: function()
549      {
550         $('#realtime_playpause #queued_counter').empty();
551      },
552
553      /**
554       * Set up event handlers on the timeline view to automatically pause
555       * when the mouse is over the timeline, as this indicates the user's
556       * desire to interact with the UI. (Which is hard to do when it's moving!)
557       *
558       * @access private
559       */
560      addNoticesHover: function()
561      {
562         $('#notices_primary .notices').hover(
563             function() {
564                 if (RealtimeUpdate._paused === false) {
565                     RealtimeUpdate.showPlay();
566                 }
567             },
568             function() {
569                 if (RealtimeUpdate._paused === true) {
570                     RealtimeUpdate.showPause();
571                 }
572             }
573         );
574      },
575
576      /**
577       * Tear down event handlers on the timeline view to automatically pause
578       * when the mouse is over the timeline.
579       *
580       * @fixme this appears to remove *ALL* event handlers from the timeline,
581       *        which assumes that nobody else is adding any event handlers.
582       *        Sloppy -- we should only remove the ones we add.
583       *
584       * @access private
585       */
586      removeNoticesHover: function()
587      {
588         $('#notices_primary .notices').unbind();
589      },
590
591      /**
592       * UI initialization, to be called from Realtime plugin code on regular
593       * timeline pages.
594       *
595       * Adds a button to the control widget at the top of the timeline view,
596       * allowing creation of a popup window with a more compact real-time
597       * view of the current timeline.
598       *
599       * @param {String} url: full URL to the popup window variant of this timeline page
600       * @param {String} timeline: string key for the timeline (eg 'public' or 'evan-all')
601       * @param {String} path: URL to the base directory containing the Realtime plugin,
602       *                       used to fetch resources if needed.
603       *
604       * @todo timeline and path parameters are unused and probably should be removed.
605       *
606       * @access public
607       */
608      initAddPopup: function(url, timeline, path)
609      {
610          $('#realtime_timeline').append('<button id="realtime_popup"></button>');
611          $('#realtime_popup').text(SN.msg('realtime_popup'))
612                              .attr('title', SN.msg('realtime_popup_tooltip'))
613                              .bind('click', function() {
614                 window.open(url,
615                          '',
616                          'toolbar=no,resizable=yes,scrollbars=yes,status=no,menubar=no,personalbar=no,location=no,width=500,height=550');
617
618              return false;
619          });
620      },
621
622      /**
623       * UI initialization, to be called from Realtime plugin code on popup
624       * compact timeline pages.
625       *
626       * Sets up links in notices to open in a new window.
627       *
628       * @fixme fails to do the same for UI links like context view which will
629       *        look bad in the tiny chromeless window.
630       *
631       * @access public
632       */
633      initPopupWindow: function()
634      {
635          $('.notices .entry-title a, .notices .entry-content a').bind('click', function() {
636             window.open(this.href, '');
637
638             return false;
639          });
640
641          $('#showstream .entity_profile').css({'width':'69%'});
642      }
643 }
644