##// END OF EJS Templates
Updated document
Updated document

File last commit:

r1467:02e3df39 default
r1570:74e32ed9 default
Show More
thread_update.js
446 lines | 12.2 KiB | application/javascript | JavascriptLexer
/*
@licstart The following is the entire license notice for the
JavaScript code in this page.
Copyright (C) 2013-2014 neko259
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.
*/
var CLASS_POST = '.post'
var POST_ADDED = 0;
var POST_UPDATED = 1;
// TODO These need to be syncronized with board settings.
var JS_AUTOUPDATE_PERIOD = 20000;
// TODO This needs to be the same for attachment download time limit.
var POST_AJAX_TIMEOUT = 30000;
var BLINK_SPEED = 500;
var ALLOWED_FOR_PARTIAL_UPDATE = [
'refmap',
'post-info'
];
var ATTR_CLASS = 'class';
var ATTR_UID = 'data-uid';
var wsUser = '';
var unreadPosts = 0;
var documentOriginalTitle = '';
// Thread ID does not change, can be stored one time
var threadId = $('div.thread').children(CLASS_POST).first().attr('id');
var blinkColor = $('<div class="post-blink"></div>').css('background-color');
/**
* Connect to websocket server and subscribe to thread updates. On any update we
* request a thread diff.
*
* @returns {boolean} true if connected, false otherwise
*/
function connectWebsocket() {
var metapanel = $('.metapanel')[0];
var wsHost = metapanel.getAttribute('data-ws-host');
var wsPort = metapanel.getAttribute('data-ws-port');
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
});
centrifuge.on('error', function(error_message) {
console.log("Error connecting to websocket server.");
console.log(error_message);
console.log("Using javascript update instead.");
// If websockets don't work, enable JS update instead
enableJsUpdate()
});
centrifuge.on('connect', function() {
var channelName = 'thread:' + threadId;
centrifuge.subscribe(channelName, function(message) {
getThreadDiff();
});
// For the case we closed the browser and missed some updates
getThreadDiff();
$('#autoupdate').hide();
});
centrifuge.connect();
return true;
} else {
return false;
}
}
/**
* 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() {
var all_posts = $('.post');
var uids = '';
var posts = all_posts;
for (var i = 0; i < posts.length; i++) {
uids += posts[i].getAttribute('data-uid') + ' ';
}
var data = {
uids: uids,
thread: threadId
};
var diffUrl = '/api/diff_thread/';
$.post(diffUrl,
data,
function(data) {
var updatedPosts = data.updated;
var addedPostCount = 0;
for (var i = 0; i < updatedPosts.length; i++) {
var postText = updatedPosts[i];
var post = $(postText);
if (updatePost(post) == POST_ADDED) {
addedPostCount++;
}
}
var hasMetaUpdates = updatedPosts.length > 0;
if (hasMetaUpdates) {
updateMetadataPanel();
}
if (addedPostCount > 0) {
updateBumplimitProgress(addedPostCount);
}
if (updatedPosts.length > 0) {
showNewPostsTitle(addedPostCount);
}
// TODO Process removed posts if any
$('.metapanel').attr('data-last-update', data.last_update);
},
'json'
)
}
/**
* Add or update the post on html page.
*/
function updatePost(postHtml) {
// This needs to be set on start because the page is scrolled after posts
// are added or updated
var bottom = isPageBottom();
var post = $(postHtml);
var threadBlock = $('div.thread');
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 + ']');
var type;
if (existingPosts.size() > 0) {
replacePartial(existingPosts.first(), post, false);
post = existingPosts.first();
type = POST_UPDATED;
} else {
post.appendTo(threadBlock);
if (bottom) {
scrollToBottom();
}
type = POST_ADDED;
}
processNewPost(post);
return type;
}
/**
* Initiate a blinking animation on a node to show it was updated.
*/
function blink(node) {
node.effect('highlight', { color: blinkColor }, BLINK_SPEED);
}
function isPageBottom() {
var scroll = $(window).scrollTop() / ($(document).height()
- $(window).height());
return scroll == 1
}
function enableJsUpdate() {
setInterval(getThreadDiff, JS_AUTOUPDATE_PERIOD);
return true;
}
function initAutoupdate() {
if (location.protocol === 'https:') {
return enableJsUpdate();
} else {
if (connectWebsocket()) {
return true;
} else {
return enableJsUpdate();
}
}
}
function getReplyCount() {
return $('.thread').children(CLASS_POST).length
}
function getImageCount() {
return $('.thread').find('img').length
}
/**
* Update post count, images count and last update time in the metadata
* panel.
*/
function updateMetadataPanel() {
var replyCountField = $('#reply-count');
var imageCountField = $('#image-count');
var replyCount = getReplyCount();
replyCountField.text(replyCount);
var imageCount = getImageCount();
imageCountField.text(imageCount);
var lastUpdate = $('.post:last').children('.post-info').first()
.children('.pub_time').first().html();
if (lastUpdate !== '') {
var lastUpdateField = $('#last-update');
lastUpdateField.html(lastUpdate);
blink(lastUpdateField);
}
blink(replyCountField);
blink(imageCountField);
$('#message-count-text').text(ngettext('message', 'messages', replyCount));
$('#image-count-text').text(ngettext('image', 'images', imageCount));
}
/**
* Update bumplimit progress bar
*/
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;
if (newPostsToLimit <= 0) {
$('.bar-bg').remove();
} else {
postsToLimitElement.text(newPostsToLimit);
progressBar.width((100 - postCount / bumplimit * 100.0) + '%');
}
}
}
/**
* Show 'new posts' text in the title if the document is not visible to a user
*/
function showNewPostsTitle(newPostCount) {
if (document.hidden) {
if (documentOriginalTitle === '') {
documentOriginalTitle = document.title;
}
unreadPosts = unreadPosts + newPostCount;
var newTitle = null;
if (unreadPosts > 0) {
newTitle = '[' + unreadPosts + '] ';
} else {
newTitle = '* ';
}
newTitle += documentOriginalTitle;
document.title = newTitle;
document.addEventListener('visibilitychange', function() {
if (documentOriginalTitle !== '') {
document.title = documentOriginalTitle;
documentOriginalTitle = '';
unreadPosts = 0;
}
document.removeEventListener('visibilitychange', null);
});
}
}
/**
* 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');
$('.file_wrap').find('.file-thumb').remove();
$('#preview-text').hide();
}
/**
* 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;
showAsErrors(form, '');
if (status === 'ok') {
resetFormPosition();
resetForm(form);
getThreadDiff();
scrollToBottom();
} else {
var errors = json.errors;
for (var i = 0; i < errors.length; i++) {
var fieldErrors = errors[i];
var error = fieldErrors.errors;
showAsErrors(form, error);
}
}
}
/**
* Run js methods that are usually run on the document, on the new post
*/
function processNewPost(post) {
addScriptsToPost(post);
blink(post);
}
function replacePartial(oldNode, newNode, recursive) {
if (!equalNodes(oldNode, newNode)) {
// Update parent node attributes
updateNodeAttr(oldNode, newNode, ATTR_CLASS);
updateNodeAttr(oldNode, newNode, ATTR_UID);
// Replace children
var children = oldNode.children();
if (children.length == 0) {
oldNode.replaceWith(newNode);
} else {
var newChildren = newNode.children();
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);
}
}
}
}
});
}
}
}
/**
* Compare nodes by content
*/
function equalNodes(node1, node2) {
return node1[0].outerHTML == node2[0].outerHTML;
}
/**
* 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);
}
}
$(document).ready(function() {
if (initAutoupdate()) {
// Post form data over AJAX
var threadId = $('div.thread').children('.post').first().attr('id');
var form = $('#form');
if (form.length > 0) {
var options = {
beforeSubmit: function(arr, form, options) {
showAsErrors(form, gettext('Sending message...'));
},
success: updateOnPost,
error: function() {
showAsErrors(form, gettext('Server error!'));
},
url: '/api/add_post/' + threadId + '/',
timeout: POST_AJAX_TIMEOUT
};
form.ajaxForm(options);
resetForm(form);
}
}
});