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