thread_update.js
458 lines
| 12.5 KiB
| application/javascript
|
JavascriptLexer
neko259
|
r365 | /* | ||
@licstart The following is the entire license notice for the | ||||
JavaScript code in this page. | ||||
neko259
|
r895 | Copyright (C) 2013-2014 neko259 | ||
neko259
|
r365 | |||
The JavaScript code in this page is free software: you can | ||||
redistribute it and/or modify it under the terms of the GNU | ||||
General Public License (GNU GPL) as published by the Free Software | ||||
Foundation, either version 3 of the License, or (at your option) | ||||
any later version. The code is distributed WITHOUT ANY WARRANTY; | ||||
without even the implied warranty of MERCHANTABILITY or FITNESS | ||||
FOR A PARTICULAR PURPOSE. See the GNU GPL for more details. | ||||
As additional permission under GNU GPL version 3 section 7, you | ||||
may distribute non-source (e.g., minimized or compacted) forms of | ||||
that code without the copy of the GNU GPL normally required by | ||||
section 4, provided you include this license notice and a URL | ||||
through which recipients can access the Corresponding Source. | ||||
@licend The above is the entire license notice | ||||
for the JavaScript code in this page. | ||||
*/ | ||||
neko259
|
r1085 | var CLASS_POST = '.post' | ||
neko259
|
r1126 | var POST_ADDED = 0; | ||
var POST_UPDATED = 1; | ||||
neko259
|
r1410 | // TODO These need to be syncronized with board settings. | ||
neko259
|
r1146 | var JS_AUTOUPDATE_PERIOD = 20000; | ||
neko259
|
r1410 | // TODO This needs to be the same for attachment download time limit. | ||
var POST_AJAX_TIMEOUT = 30000; | ||||
neko259
|
r1411 | var BLINK_SPEED = 500; | ||
neko259
|
r1146 | |||
neko259
|
r1315 | var ALLOWED_FOR_PARTIAL_UPDATE = [ | ||
'refmap', | ||||
'post-info' | ||||
]; | ||||
var ATTR_CLASS = 'class'; | ||||
neko259
|
r1316 | var ATTR_UID = 'data-uid'; | ||
neko259
|
r1315 | |||
neko259
|
r853 | var wsUser = ''; | ||
neko259
|
r361 | |||
neko259
|
r725 | var unreadPosts = 0; | ||
neko259
|
r892 | var documentOriginalTitle = ''; | ||
neko259
|
r363 | |||
neko259
|
r853 | // Thread ID does not change, can be stored one time | ||
neko259
|
r1085 | var threadId = $('div.thread').children(CLASS_POST).first().attr('id'); | ||
neko259
|
r1412 | var blinkColor = $('<div class="post-blink"></div>').css('background-color'); | ||
neko259
|
r853 | |||
neko259
|
r895 | /** | ||
* Connect to websocket server and subscribe to thread updates. On any update we | ||||
* request a thread diff. | ||||
* | ||||
* @returns {boolean} true if connected, false otherwise | ||||
*/ | ||||
neko259
|
r853 | function connectWebsocket() { | ||
var metapanel = $('.metapanel')[0]; | ||||
var wsHost = metapanel.getAttribute('data-ws-host'); | ||||
var wsPort = metapanel.getAttribute('data-ws-port'); | ||||
neko259
|
r1147 | if (wsHost.length > 0 && wsPort.length > 0) { | ||
var centrifuge = new Centrifuge({ | ||||
"url": 'ws://' + wsHost + ':' + wsPort + "/connection/websocket", | ||||
"project": metapanel.getAttribute('data-ws-project'), | ||||
"user": wsUser, | ||||
"timestamp": metapanel.getAttribute('data-ws-token-time'), | ||||
"token": metapanel.getAttribute('data-ws-token'), | ||||
"debug": false | ||||
}); | ||||
neko259
|
r853 | |||
neko259
|
r1147 | centrifuge.on('error', function(error_message) { | ||
console.log("Error connecting to websocket server."); | ||||
console.log(error_message); | ||||
neko259
|
r1163 | console.log("Using javascript update instead."); | ||
// If websockets don't work, enable JS update instead | ||||
enableJsUpdate() | ||||
neko259
|
r853 | }); | ||
neko259
|
r1147 | centrifuge.on('connect', function() { | ||
var channelName = 'thread:' + threadId; | ||||
centrifuge.subscribe(channelName, function(message) { | ||||
getThreadDiff(); | ||||
}); | ||||
neko259
|
r853 | |||
neko259
|
r1147 | // For the case we closed the browser and missed some updates | ||
getThreadDiff(); | ||||
$('#autoupdate').hide(); | ||||
}); | ||||
neko259
|
r863 | |||
neko259
|
r1147 | centrifuge.connect(); | ||
return true; | ||||
} else { | ||||
return false; | ||||
} | ||||
neko259
|
r853 | } | ||
neko259
|
r892 | /** | ||
* Get diff of the posts from the current thread timestamp. | ||||
* This is required if the browser was closed and some post updates were | ||||
* missed. | ||||
*/ | ||||
function getThreadDiff() { | ||||
neko259
|
r1332 | var all_posts = $('.post'); | ||
neko259
|
r892 | |||
neko259
|
r1118 | var uids = ''; | ||
neko259
|
r1332 | var posts = all_posts; | ||
neko259
|
r1118 | for (var i = 0; i < posts.length; i++) { | ||
uids += posts[i].getAttribute('data-uid') + ' '; | ||||
} | ||||
neko259
|
r892 | |||
neko259
|
r1118 | var data = { | ||
neko259
|
r1191 | uids: uids, | ||
thread: threadId | ||||
neko259
|
r1340 | }; | ||
neko259
|
r892 | |||
neko259
|
r1191 | var diffUrl = '/api/diff_thread/'; | ||
neko259
|
r1085 | |||
neko259
|
r1118 | $.post(diffUrl, | ||
data, | ||||
function(data) { | ||||
neko259
|
r892 | var updatedPosts = data.updated; | ||
neko259
|
r1126 | var addedPostCount = 0; | ||
neko259
|
r892 | |||
for (var i = 0; i < updatedPosts.length; i++) { | ||||
var postText = updatedPosts[i]; | ||||
var post = $(postText); | ||||
neko259
|
r1126 | if (updatePost(post) == POST_ADDED) { | ||
addedPostCount++; | ||||
} | ||||
neko259
|
r892 | } | ||
neko259
|
r1118 | var hasMetaUpdates = updatedPosts.length > 0; | ||
neko259
|
r1074 | if (hasMetaUpdates) { | ||
updateMetadataPanel(); | ||||
} | ||||
neko259
|
r1073 | |||
neko259
|
r1127 | if (addedPostCount > 0) { | ||
updateBumplimitProgress(addedPostCount); | ||||
} | ||||
if (updatedPosts.length > 0) { | ||||
showNewPostsTitle(addedPostCount); | ||||
} | ||||
neko259
|
r1126 | |||
neko259
|
r892 | // TODO Process removed posts if any | ||
neko259
|
r895 | $('.metapanel').attr('data-last-update', data.last_update); | ||
neko259
|
r1118 | }, | ||
'json' | ||||
) | ||||
neko259
|
r892 | } | ||
/** | ||||
neko259
|
r895 | * Add or update the post on html page. | ||
neko259
|
r892 | */ | ||
neko259
|
r895 | function updatePost(postHtml) { | ||
neko259
|
r853 | // This needs to be set on start because the page is scrolled after posts | ||
// are added or updated | ||||
var bottom = isPageBottom(); | ||||
var post = $(postHtml); | ||||
neko259
|
r895 | var threadBlock = $('div.thread'); | ||
neko259
|
r853 | |||
neko259
|
r895 | var postId = post.attr('id'); | ||
// If the post already exists, replace it. Otherwise add as a new one. | ||||
var existingPosts = threadBlock.children('.post[id=' + postId + ']'); | ||||
neko259
|
r1126 | var type; | ||
neko259
|
r895 | if (existingPosts.size() > 0) { | ||
neko259
|
r1315 | replacePartial(existingPosts.first(), post, false); | ||
post = existingPosts.first(); | ||||
neko259
|
r1126 | |||
type = POST_UPDATED; | ||||
neko259
|
r895 | } else { | ||
neko259
|
r1085 | post.appendTo(threadBlock); | ||
neko259
|
r853 | |||
if (bottom) { | ||||
scrollToBottom(); | ||||
} | ||||
neko259
|
r1118 | |||
neko259
|
r1126 | type = POST_ADDED; | ||
neko259
|
r853 | } | ||
processNewPost(post); | ||||
neko259
|
r1126 | |||
return type; | ||||
neko259
|
r853 | } | ||
neko259
|
r892 | /** | ||
* Initiate a blinking animation on a node to show it was updated. | ||||
*/ | ||||
neko259
|
r363 | function blink(node) { | ||
neko259
|
r1412 | node.effect('highlight', { color: blinkColor }, BLINK_SPEED); | ||
neko259
|
r363 | } | ||
neko259
|
r361 | |||
neko259
|
r373 | function isPageBottom() { | ||
var scroll = $(window).scrollTop() / ($(document).height() | ||||
neko259
|
r855 | - $(window).height()); | ||
neko259
|
r373 | |||
return scroll == 1 | ||||
} | ||||
neko259
|
r1146 | function enableJsUpdate() { | ||
setInterval(getThreadDiff, JS_AUTOUPDATE_PERIOD); | ||||
return true; | ||||
} | ||||
neko259
|
r363 | function initAutoupdate() { | ||
neko259
|
r1146 | if (location.protocol === 'https:') { | ||
return enableJsUpdate(); | ||||
} else { | ||||
neko259
|
r1147 | if (connectWebsocket()) { | ||
return true; | ||||
} else { | ||||
return enableJsUpdate(); | ||||
} | ||||
neko259
|
r1146 | } | ||
neko259
|
r363 | } | ||
neko259
|
r391 | |||
function getReplyCount() { | ||||
neko259
|
r1085 | return $('.thread').children(CLASS_POST).length | ||
neko259
|
r391 | } | ||
function getImageCount() { | ||||
return $('.thread').find('img').length | ||||
} | ||||
neko259
|
r420 | |||
neko259
|
r892 | /** | ||
* Update post count, images count and last update time in the metadata | ||||
* panel. | ||||
*/ | ||||
neko259
|
r1073 | function updateMetadataPanel() { | ||
neko259
|
r536 | var replyCountField = $('#reply-count'); | ||
var imageCountField = $('#image-count'); | ||||
neko259
|
r1332 | var replyCount = getReplyCount(); | ||
replyCountField.text(replyCount); | ||||
var imageCount = getImageCount(); | ||||
imageCountField.text(imageCount); | ||||
neko259
|
r536 | |||
neko259
|
r1073 | var lastUpdate = $('.post:last').children('.post-info').first() | ||
.children('.pub_time').first().html(); | ||||
neko259
|
r536 | if (lastUpdate !== '') { | ||
var lastUpdateField = $('#last-update'); | ||||
neko259
|
r1017 | lastUpdateField.html(lastUpdate); | ||
neko259
|
r536 | blink(lastUpdateField); | ||
} | ||||
blink(replyCountField); | ||||
blink(imageCountField); | ||||
neko259
|
r1332 | |||
$('#message-count-text').text(ngettext('message', 'messages', replyCount)); | ||||
$('#image-count-text').text(ngettext('image', 'images', imageCount)); | ||||
neko259
|
r536 | } | ||
neko259
|
r429 | /** | ||
* Update bumplimit progress bar | ||||
*/ | ||||
neko259
|
r420 | function updateBumplimitProgress(postDelta) { | ||
var progressBar = $('#bumplimit_progress'); | ||||
if (progressBar) { | ||||
var postsToLimitElement = $('#left_to_limit'); | ||||
var oldPostsToLimit = parseInt(postsToLimitElement.text()); | ||||
var postCount = getReplyCount(); | ||||
var bumplimit = postCount - postDelta + oldPostsToLimit; | ||||
var newPostsToLimit = bumplimit - postCount; | ||||
neko259
|
r429 | if (newPostsToLimit <= 0) { | ||
$('.bar-bg').remove(); | ||||
neko259
|
r420 | } else { | ||
postsToLimitElement.text(newPostsToLimit); | ||||
progressBar.width((100 - postCount / bumplimit * 100.0) + '%'); | ||||
} | ||||
} | ||||
neko259
|
r427 | } | ||
neko259
|
r429 | |||
neko259
|
r451 | /** | ||
* Show 'new posts' text in the title if the document is not visible to a user | ||||
*/ | ||||
neko259
|
r468 | function showNewPostsTitle(newPostCount) { | ||
neko259
|
r451 | if (document.hidden) { | ||
neko259
|
r468 | if (documentOriginalTitle === '') { | ||
documentOriginalTitle = document.title; | ||||
} | ||||
unreadPosts = unreadPosts + newPostCount; | ||||
neko259
|
r1127 | |||
var newTitle = '* '; | ||||
if (unreadPosts > 0) { | ||||
newTitle += '[' + unreadPosts + '] '; | ||||
} | ||||
newTitle += documentOriginalTitle; | ||||
document.title = newTitle; | ||||
neko259
|
r451 | |||
document.addEventListener('visibilitychange', function() { | ||||
if (documentOriginalTitle !== '') { | ||||
document.title = documentOriginalTitle; | ||||
documentOriginalTitle = ''; | ||||
neko259
|
r468 | unreadPosts = 0; | ||
neko259
|
r451 | } | ||
document.removeEventListener('visibilitychange', null); | ||||
}); | ||||
} | ||||
neko259
|
r458 | } | ||
neko259
|
r533 | /** | ||
* Clear all entered values in the form fields | ||||
*/ | ||||
function resetForm(form) { | ||||
form.find('input:text, input:password, input:file, select, textarea').val(''); | ||||
form.find('input:radio, input:checkbox') | ||||
.removeAttr('checked').removeAttr('selected'); | ||||
neko259
|
r682 | $('.file_wrap').find('.file-thumb').remove(); | ||
neko259
|
r1217 | $('#preview-text').hide(); | ||
neko259
|
r533 | } | ||
neko259
|
r682 | /** | ||
* When the form is posted, this method will be run as a callback | ||||
*/ | ||||
function updateOnPost(response, statusText, xhr, form) { | ||||
var json = $.parseJSON(response); | ||||
var status = json.status; | ||||
neko259
|
r725 | showAsErrors(form, ''); | ||
neko259
|
r682 | |||
if (status === 'ok') { | ||||
neko259
|
r1059 | resetFormPosition(); | ||
neko259
|
r682 | resetForm(form); | ||
neko259
|
r923 | getThreadDiff(); | ||
neko259
|
r1057 | scrollToBottom(); | ||
neko259
|
r682 | } else { | ||
var errors = json.errors; | ||||
for (var i = 0; i < errors.length; i++) { | ||||
var fieldErrors = errors[i]; | ||||
var error = fieldErrors.errors; | ||||
neko259
|
r725 | showAsErrors(form, error); | ||
neko259
|
r682 | } | ||
} | ||||
} | ||||
neko259
|
r533 | |||
neko259
|
r709 | /** | ||
neko259
|
r725 | * Show text in the errors row of the form. | ||
* @param form | ||||
* @param text | ||||
*/ | ||||
function showAsErrors(form, text) { | ||||
form.children('.form-errors').remove(); | ||||
if (text.length > 0) { | ||||
neko259
|
r1005 | var errorList = $('<div class="form-errors">' + text + '<div>'); | ||
neko259
|
r725 | errorList.appendTo(form); | ||
} | ||||
} | ||||
/** | ||||
neko259
|
r709 | * Run js methods that are usually run on the document, on the new post | ||
*/ | ||||
function processNewPost(post) { | ||||
neko259
|
r710 | addRefLinkPreview(post[0]); | ||
neko259
|
r709 | highlightCode(post); | ||
neko259
|
r853 | blink(post); | ||
neko259
|
r709 | } | ||
neko259
|
r1315 | function replacePartial(oldNode, newNode, recursive) { | ||
if (!equalNodes(oldNode, newNode)) { | ||||
neko259
|
r1316 | // Update parent node attributes | ||
updateNodeAttr(oldNode, newNode, ATTR_CLASS); | ||||
updateNodeAttr(oldNode, newNode, ATTR_UID); | ||||
neko259
|
r1313 | |||
neko259
|
r1315 | // Replace children | ||
neko259
|
r1313 | var children = oldNode.children(); | ||
if (children.length == 0) { | ||||
oldNode.replaceWith(newNode); | ||||
} else { | ||||
var newChildren = newNode.children(); | ||||
neko259
|
r1315 | newChildren.each(function(i) { | ||
var newChild = newChildren.eq(i); | ||||
var newChildClass = newChild.attr(ATTR_CLASS); | ||||
// Update only certain allowed blocks (e.g. not images) | ||||
if (ALLOWED_FOR_PARTIAL_UPDATE.indexOf(newChildClass) > -1) { | ||||
var oldChild = oldNode.children('.' + newChildClass); | ||||
if (oldChild.length == 0) { | ||||
oldNode.append(newChild); | ||||
} else { | ||||
if (!equalNodes(oldChild, newChild)) { | ||||
if (recursive) { | ||||
replacePartial(oldChild, newChild, false); | ||||
} else { | ||||
oldChild.replaceWith(newChild); | ||||
} | ||||
} | ||||
} | ||||
} | ||||
neko259
|
r1313 | }); | ||
} | ||||
} | ||||
} | ||||
neko259
|
r1315 | /** | ||
* Compare nodes by content | ||||
*/ | ||||
function equalNodes(node1, node2) { | ||||
return node1[0].outerHTML == node2[0].outerHTML; | ||||
} | ||||
neko259
|
r1316 | /** | ||
* Update attribute of a node if it has changed | ||||
*/ | ||||
function updateNodeAttr(oldNode, newNode, attrName) { | ||||
var oldAttr = oldNode.attr(attrName); | ||||
var newAttr = newNode.attr(attrName); | ||||
if (oldAttr != newAttr) { | ||||
oldNode.attr(attrName, newAttr); | ||||
neko259
|
r1332 | } | ||
neko259
|
r1316 | } | ||
neko259
|
r458 | $(document).ready(function(){ | ||
neko259
|
r922 | if (initAutoupdate()) { | ||
// Post form data over AJAX | ||||
var threadId = $('div.thread').children('.post').first().attr('id'); | ||||
neko259
|
r534 | |||
neko259
|
r922 | var form = $('#form'); | ||
neko259
|
r682 | |||
neko259
|
r1024 | if (form.length > 0) { | ||
var options = { | ||||
beforeSubmit: function(arr, $form, options) { | ||||
neko259
|
r1323 | showAsErrors($('#form'), gettext('Sending message...')); | ||
neko259
|
r1024 | }, | ||
success: updateOnPost, | ||||
neko259
|
r1221 | error: function() { | ||
neko259
|
r1323 | showAsErrors($('#form'), gettext('Server error!')); | ||
neko259
|
r1221 | }, | ||
neko259
|
r1410 | url: '/api/add_post/' + threadId + '/', | ||
timeout: POST_AJAX_TIMEOUT | ||||
neko259
|
r1024 | }; | ||
neko259
|
r534 | |||
neko259
|
r1024 | form.ajaxForm(options); | ||
neko259
|
r534 | |||
neko259
|
r1024 | resetForm(form); | ||
} | ||||
neko259
|
r856 | } | ||
neko259
|
r458 | }); | ||