|
|
/*
|
|
|
@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;
|
|
|
|
|
|
var JS_AUTOUPDATE_PERIOD = 20000;
|
|
|
|
|
|
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');
|
|
|
|
|
|
/**
|
|
|
* 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) {
|
|
|
var blinkCount = 2;
|
|
|
|
|
|
var nodeToAnimate = node;
|
|
|
for (var i = 0; i < blinkCount; i++) {
|
|
|
nodeToAnimate = nodeToAnimate.fadeTo('fast', 0.5).fadeTo('fast', 1.0);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
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 = '* ';
|
|
|
if (unreadPosts > 0) {
|
|
|
newTitle += '[' + unreadPosts + '] ';
|
|
|
}
|
|
|
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);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 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) {
|
|
|
var errorList = $('<div class="form-errors">' + text + '<div>');
|
|
|
errorList.appendTo(form);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Run js methods that are usually run on the document, on the new post
|
|
|
*/
|
|
|
function processNewPost(post) {
|
|
|
addRefLinkPreview(post[0]);
|
|
|
highlightCode(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 + '/'
|
|
|
};
|
|
|
|
|
|
form.ajaxForm(options);
|
|
|
|
|
|
resetForm(form);
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
|