realtimeupdate.js 22 KB

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