##// END OF EJS Templates
Use hightlight effect for post updates
neko259 -
r1411:e11ba5bb default
parent child Browse files
Show More
@@ -1,461 +1,457 b''
1 1 /*
2 2 @licstart The following is the entire license notice for the
3 3 JavaScript code in this page.
4 4
5 5
6 6 Copyright (C) 2013-2014 neko259
7 7
8 8 The JavaScript code in this page is free software: you can
9 9 redistribute it and/or modify it under the terms of the GNU
10 10 General Public License (GNU GPL) as published by the Free Software
11 11 Foundation, either version 3 of the License, or (at your option)
12 12 any later version. The code is distributed WITHOUT ANY WARRANTY;
13 13 without even the implied warranty of MERCHANTABILITY or FITNESS
14 14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
15 15
16 16 As additional permission under GNU GPL version 3 section 7, you
17 17 may distribute non-source (e.g., minimized or compacted) forms of
18 18 that code without the copy of the GNU GPL normally required by
19 19 section 4, provided you include this license notice and a URL
20 20 through which recipients can access the Corresponding Source.
21 21
22 22 @licend The above is the entire license notice
23 23 for the JavaScript code in this page.
24 24 */
25 25
26 26 var CLASS_POST = '.post'
27 27
28 28 var POST_ADDED = 0;
29 29 var POST_UPDATED = 1;
30 30
31 31 // TODO These need to be syncronized with board settings.
32 32 var JS_AUTOUPDATE_PERIOD = 20000;
33 33 // TODO This needs to be the same for attachment download time limit.
34 34 var POST_AJAX_TIMEOUT = 30000;
35 var BLINK_SPEED = 500;
35 36
36 37 var ALLOWED_FOR_PARTIAL_UPDATE = [
37 38 'refmap',
38 39 'post-info'
39 40 ];
40 41
41 42 var ATTR_CLASS = 'class';
42 43 var ATTR_UID = 'data-uid';
43 44
44 45 var wsUser = '';
45 46
46 47 var unreadPosts = 0;
47 48 var documentOriginalTitle = '';
48 49
49 50 // Thread ID does not change, can be stored one time
50 51 var threadId = $('div.thread').children(CLASS_POST).first().attr('id');
51 52
52 53 /**
53 54 * Connect to websocket server and subscribe to thread updates. On any update we
54 55 * request a thread diff.
55 56 *
56 57 * @returns {boolean} true if connected, false otherwise
57 58 */
58 59 function connectWebsocket() {
59 60 var metapanel = $('.metapanel')[0];
60 61
61 62 var wsHost = metapanel.getAttribute('data-ws-host');
62 63 var wsPort = metapanel.getAttribute('data-ws-port');
63 64
64 65 if (wsHost.length > 0 && wsPort.length > 0) {
65 66 var centrifuge = new Centrifuge({
66 67 "url": 'ws://' + wsHost + ':' + wsPort + "/connection/websocket",
67 68 "project": metapanel.getAttribute('data-ws-project'),
68 69 "user": wsUser,
69 70 "timestamp": metapanel.getAttribute('data-ws-token-time'),
70 71 "token": metapanel.getAttribute('data-ws-token'),
71 72 "debug": false
72 73 });
73 74
74 75 centrifuge.on('error', function(error_message) {
75 76 console.log("Error connecting to websocket server.");
76 77 console.log(error_message);
77 78 console.log("Using javascript update instead.");
78 79
79 80 // If websockets don't work, enable JS update instead
80 81 enableJsUpdate()
81 82 });
82 83
83 84 centrifuge.on('connect', function() {
84 85 var channelName = 'thread:' + threadId;
85 86 centrifuge.subscribe(channelName, function(message) {
86 87 getThreadDiff();
87 88 });
88 89
89 90 // For the case we closed the browser and missed some updates
90 91 getThreadDiff();
91 92 $('#autoupdate').hide();
92 93 });
93 94
94 95 centrifuge.connect();
95 96
96 97 return true;
97 98 } else {
98 99 return false;
99 100 }
100 101 }
101 102
102 103 /**
103 104 * Get diff of the posts from the current thread timestamp.
104 105 * This is required if the browser was closed and some post updates were
105 106 * missed.
106 107 */
107 108 function getThreadDiff() {
108 109 var all_posts = $('.post');
109 110
110 111 var uids = '';
111 112 var posts = all_posts;
112 113 for (var i = 0; i < posts.length; i++) {
113 114 uids += posts[i].getAttribute('data-uid') + ' ';
114 115 }
115 116
116 117 var data = {
117 118 uids: uids,
118 119 thread: threadId
119 120 };
120 121
121 122 var diffUrl = '/api/diff_thread/';
122 123
123 124 $.post(diffUrl,
124 125 data,
125 126 function(data) {
126 127 var updatedPosts = data.updated;
127 128 var addedPostCount = 0;
128 129
129 130 for (var i = 0; i < updatedPosts.length; i++) {
130 131 var postText = updatedPosts[i];
131 132 var post = $(postText);
132 133
133 134 if (updatePost(post) == POST_ADDED) {
134 135 addedPostCount++;
135 136 }
136 137 }
137 138
138 139 var hasMetaUpdates = updatedPosts.length > 0;
139 140 if (hasMetaUpdates) {
140 141 updateMetadataPanel();
141 142 }
142 143
143 144 if (addedPostCount > 0) {
144 145 updateBumplimitProgress(addedPostCount);
145 146 }
146 147
147 148 if (updatedPosts.length > 0) {
148 149 showNewPostsTitle(addedPostCount);
149 150 }
150 151
151 152 // TODO Process removed posts if any
152 153 $('.metapanel').attr('data-last-update', data.last_update);
153 154 },
154 155 'json'
155 156 )
156 157 }
157 158
158 159 /**
159 160 * Add or update the post on html page.
160 161 */
161 162 function updatePost(postHtml) {
162 163 // This needs to be set on start because the page is scrolled after posts
163 164 // are added or updated
164 165 var bottom = isPageBottom();
165 166
166 167 var post = $(postHtml);
167 168
168 169 var threadBlock = $('div.thread');
169 170
170 171 var postId = post.attr('id');
171 172
172 173 // If the post already exists, replace it. Otherwise add as a new one.
173 174 var existingPosts = threadBlock.children('.post[id=' + postId + ']');
174 175
175 176 var type;
176 177
177 178 if (existingPosts.size() > 0) {
178 179 replacePartial(existingPosts.first(), post, false);
179 180 post = existingPosts.first();
180 181
181 182 type = POST_UPDATED;
182 183 } else {
183 184 post.appendTo(threadBlock);
184 185
185 186 if (bottom) {
186 187 scrollToBottom();
187 188 }
188 189
189 190 type = POST_ADDED;
190 191 }
191 192
192 193 processNewPost(post);
193 194
194 195 return type;
195 196 }
196 197
197 198 /**
198 199 * Initiate a blinking animation on a node to show it was updated.
199 200 */
200 201 function blink(node) {
201 var blinkCount = 2;
202
203 var nodeToAnimate = node;
204 for (var i = 0; i < blinkCount; i++) {
205 nodeToAnimate = nodeToAnimate.fadeTo('fast', 0.5).fadeTo('fast', 1.0);
206 }
202 node.effect('highlight', {}, BLINK_SPEED);
207 203 }
208 204
209 205 function isPageBottom() {
210 206 var scroll = $(window).scrollTop() / ($(document).height()
211 207 - $(window).height());
212 208
213 209 return scroll == 1
214 210 }
215 211
216 212 function enableJsUpdate() {
217 213 setInterval(getThreadDiff, JS_AUTOUPDATE_PERIOD);
218 214 return true;
219 215 }
220 216
221 217 function initAutoupdate() {
222 218 if (location.protocol === 'https:') {
223 219 return enableJsUpdate();
224 220 } else {
225 221 if (connectWebsocket()) {
226 222 return true;
227 223 } else {
228 224 return enableJsUpdate();
229 225 }
230 226 }
231 227 }
232 228
233 229 function getReplyCount() {
234 230 return $('.thread').children(CLASS_POST).length
235 231 }
236 232
237 233 function getImageCount() {
238 234 return $('.thread').find('img').length
239 235 }
240 236
241 237 /**
242 238 * Update post count, images count and last update time in the metadata
243 239 * panel.
244 240 */
245 241 function updateMetadataPanel() {
246 242 var replyCountField = $('#reply-count');
247 243 var imageCountField = $('#image-count');
248 244
249 245 var replyCount = getReplyCount();
250 246 replyCountField.text(replyCount);
251 247 var imageCount = getImageCount();
252 248 imageCountField.text(imageCount);
253 249
254 250 var lastUpdate = $('.post:last').children('.post-info').first()
255 251 .children('.pub_time').first().html();
256 252 if (lastUpdate !== '') {
257 253 var lastUpdateField = $('#last-update');
258 254 lastUpdateField.html(lastUpdate);
259 255 blink(lastUpdateField);
260 256 }
261 257
262 258 blink(replyCountField);
263 259 blink(imageCountField);
264 260
265 261 $('#message-count-text').text(ngettext('message', 'messages', replyCount));
266 262 $('#image-count-text').text(ngettext('image', 'images', imageCount));
267 263 }
268 264
269 265 /**
270 266 * Update bumplimit progress bar
271 267 */
272 268 function updateBumplimitProgress(postDelta) {
273 269 var progressBar = $('#bumplimit_progress');
274 270 if (progressBar) {
275 271 var postsToLimitElement = $('#left_to_limit');
276 272
277 273 var oldPostsToLimit = parseInt(postsToLimitElement.text());
278 274 var postCount = getReplyCount();
279 275 var bumplimit = postCount - postDelta + oldPostsToLimit;
280 276
281 277 var newPostsToLimit = bumplimit - postCount;
282 278 if (newPostsToLimit <= 0) {
283 279 $('.bar-bg').remove();
284 280 } else {
285 281 postsToLimitElement.text(newPostsToLimit);
286 282 progressBar.width((100 - postCount / bumplimit * 100.0) + '%');
287 283 }
288 284 }
289 285 }
290 286
291 287 /**
292 288 * Show 'new posts' text in the title if the document is not visible to a user
293 289 */
294 290 function showNewPostsTitle(newPostCount) {
295 291 if (document.hidden) {
296 292 if (documentOriginalTitle === '') {
297 293 documentOriginalTitle = document.title;
298 294 }
299 295 unreadPosts = unreadPosts + newPostCount;
300 296
301 297 var newTitle = '* ';
302 298 if (unreadPosts > 0) {
303 299 newTitle += '[' + unreadPosts + '] ';
304 300 }
305 301 newTitle += documentOriginalTitle;
306 302
307 303 document.title = newTitle;
308 304
309 305 document.addEventListener('visibilitychange', function() {
310 306 if (documentOriginalTitle !== '') {
311 307 document.title = documentOriginalTitle;
312 308 documentOriginalTitle = '';
313 309 unreadPosts = 0;
314 310 }
315 311
316 312 document.removeEventListener('visibilitychange', null);
317 313 });
318 314 }
319 315 }
320 316
321 317 /**
322 318 * Clear all entered values in the form fields
323 319 */
324 320 function resetForm(form) {
325 321 form.find('input:text, input:password, input:file, select, textarea').val('');
326 322 form.find('input:radio, input:checkbox')
327 323 .removeAttr('checked').removeAttr('selected');
328 324 $('.file_wrap').find('.file-thumb').remove();
329 325 $('#preview-text').hide();
330 326 }
331 327
332 328 /**
333 329 * When the form is posted, this method will be run as a callback
334 330 */
335 331 function updateOnPost(response, statusText, xhr, form) {
336 332 var json = $.parseJSON(response);
337 333 var status = json.status;
338 334
339 335 showAsErrors(form, '');
340 336
341 337 if (status === 'ok') {
342 338 resetFormPosition();
343 339 resetForm(form);
344 340 getThreadDiff();
345 341 scrollToBottom();
346 342 } else {
347 343 var errors = json.errors;
348 344 for (var i = 0; i < errors.length; i++) {
349 345 var fieldErrors = errors[i];
350 346
351 347 var error = fieldErrors.errors;
352 348
353 349 showAsErrors(form, error);
354 350 }
355 351 }
356 352 }
357 353
358 354 /**
359 355 * Show text in the errors row of the form.
360 356 * @param form
361 357 * @param text
362 358 */
363 359 function showAsErrors(form, text) {
364 360 form.children('.form-errors').remove();
365 361
366 362 if (text.length > 0) {
367 363 var errorList = $('<div class="form-errors">' + text + '<div>');
368 364 errorList.appendTo(form);
369 365 }
370 366 }
371 367
372 368 /**
373 369 * Run js methods that are usually run on the document, on the new post
374 370 */
375 371 function processNewPost(post) {
376 372 addRefLinkPreview(post[0]);
377 373 highlightCode(post);
378 374 blink(post);
379 375 }
380 376
381 377 function replacePartial(oldNode, newNode, recursive) {
382 378 if (!equalNodes(oldNode, newNode)) {
383 379 // Update parent node attributes
384 380 updateNodeAttr(oldNode, newNode, ATTR_CLASS);
385 381 updateNodeAttr(oldNode, newNode, ATTR_UID);
386 382
387 383 // Replace children
388 384 var children = oldNode.children();
389 385 if (children.length == 0) {
390 386 oldNode.replaceWith(newNode);
391 387 } else {
392 388 var newChildren = newNode.children();
393 389 newChildren.each(function(i) {
394 390 var newChild = newChildren.eq(i);
395 391 var newChildClass = newChild.attr(ATTR_CLASS);
396 392
397 393 // Update only certain allowed blocks (e.g. not images)
398 394 if (ALLOWED_FOR_PARTIAL_UPDATE.indexOf(newChildClass) > -1) {
399 395 var oldChild = oldNode.children('.' + newChildClass);
400 396
401 397 if (oldChild.length == 0) {
402 398 oldNode.append(newChild);
403 399 } else {
404 400 if (!equalNodes(oldChild, newChild)) {
405 401 if (recursive) {
406 402 replacePartial(oldChild, newChild, false);
407 403 } else {
408 404 oldChild.replaceWith(newChild);
409 405 }
410 406 }
411 407 }
412 408 }
413 409 });
414 410 }
415 411 }
416 412 }
417 413
418 414 /**
419 415 * Compare nodes by content
420 416 */
421 417 function equalNodes(node1, node2) {
422 418 return node1[0].outerHTML == node2[0].outerHTML;
423 419 }
424 420
425 421 /**
426 422 * Update attribute of a node if it has changed
427 423 */
428 424 function updateNodeAttr(oldNode, newNode, attrName) {
429 425 var oldAttr = oldNode.attr(attrName);
430 426 var newAttr = newNode.attr(attrName);
431 427 if (oldAttr != newAttr) {
432 428 oldNode.attr(attrName, newAttr);
433 429 }
434 430 }
435 431
436 432 $(document).ready(function(){
437 433 if (initAutoupdate()) {
438 434 // Post form data over AJAX
439 435 var threadId = $('div.thread').children('.post').first().attr('id');
440 436
441 437 var form = $('#form');
442 438
443 439 if (form.length > 0) {
444 440 var options = {
445 441 beforeSubmit: function(arr, $form, options) {
446 442 showAsErrors($('#form'), gettext('Sending message...'));
447 443 },
448 444 success: updateOnPost,
449 445 error: function() {
450 446 showAsErrors($('#form'), gettext('Server error!'));
451 447 },
452 448 url: '/api/add_post/' + threadId + '/',
453 449 timeout: POST_AJAX_TIMEOUT
454 450 };
455 451
456 452 form.ajaxForm(options);
457 453
458 454 resetForm(form);
459 455 }
460 456 }
461 457 });
General Comments 0
You need to be logged in to leave comments. Login now