// ==UserScript==
// @name ResetEra Live Thread
// @namespace http://madjoki.com
// @version 3.7
// @description Update threads without refreshing
// @author Madjoki
// @match https://www.resetera.com/threads/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
var globalSettings = {
rememberThreads: true,
updateTime: 30,
enabledByDefault: false,
useNewMessageMarker: true,
enableDebug: false,
};
var windowID = Math.floor(Math.random() * 100000);
var timeoptions = [
{
name: "5s",
value: 5,
},
{
name: "10s",
value: 10,
},
{
name: "15s",
value: 15,
},
{
name: "30s (Default)",
value: 30,
},
{
name: "1m",
value: 60,
},
{
name: "2m",
value: 120,
},
];
$('body').append('<style>\
#livethreadPanel {\
display: none;\
text-align: center; \
}\
#livethreadPanel ul {\
display: inline-block;\
margin-bottom: 15px;\
}\
#livethreadPanel ul li {\
display: block;\
text-align: left;\
}\
#updateTime {\
margin-left: 5px;\
padding: 0px;\
}\
#updateTimeDefault {\
margin-left: 5px;\
padding: 0px;\
}\
body.darktheme #livethreadPanel ul {\
color: #8e50be; /*dark theme only*/\
}\
.livethreadStatus {\
text-align: center; \
}\
.liveThread_enabled #AjaxProgress {\
display: none !important;\
}\
</style>');
var highlightScript = $('script:contains("data-author")').first().text();
function addoptions(el, values) {
$(el).find("option").remove();
$(values).each(function (i, o) {
$(el).append($("<option>", {text: o.name, value: o.value}));
})
}
// Read Global Settings
var settingsJson = localStorage.getItem("livethreadSettings");
if (settingsJson !== null)
{
globalSettings = JSON.parse(settingsJson);
// Update settings by defaults
globalSettings.updateTime = globalSettings.updateTime || 30;
globalSettings.enabledByDefault = globalSettings.enabledByDefault || false;
globalSettings.enableDebug = globalSettings.enableDebug || false;
if (globalSettings.useNewMessageMarker === undefined)
globalSettings.useNewMessageMarker = true;
}
var defaultThreadSettings = {
updateTime: globalSettings.updateTime,
enabled: globalSettings.enabledByDefault,
};
var pageType = "";
if (location.path == "/")
return;
pageType = "thread";
var threadID = parseInt($('[name="type[post][thread_id]"]').val(), 10);
var currentThreadSettings = getSettings(threadID);
var isRememberedThread = globalSettings.rememberThreads;
var pauseReason = null;
var hasFocus = true;
var newMessageMarkerLast;
var newMessageMarker;
if (currentThreadSettings === null)
{
isRememberedThread = globalSettings.rememberThreads && !hasSettings(threadID);
currentThreadSettings = defaultThreadSettings;
}
else
{
isRememberedThread = true
}
function calculateNextUpdate()
{
var timer = currentThreadSettings.updateTime;
// If we have more pages to load, use shorter timer
if (currentPage < lastPage)
timer = 5;
timeToNextUpdate = timer;
}
function hasSettings(thread)
{
var key = "livethread_" + thread;
return key in localStorage;
}
function getSettings(thread)
{
var stored = localStorage.getItem("livethread_" + thread);
if (stored === null)
return null;
return JSON.parse(stored);
}
function setSettings(thread, settings)
{
localStorage.setItem("livethread_" + thread, JSON.stringify(settings));
}
function saveSettings()
{
if (isRememberedThread)
setSettings(threadID, currentThreadSettings);
else
setSettings(threadID, null);
localStorage.setItem("livethreadSettings", JSON.stringify(globalSettings));
redraw();
}
var countNewLast = 0;
var errors = 0;
var updating = false;
var lastUrl = window.location;
var currentPage = $('div.PageNav[data-page]').first().data('page') || 1;
var lastLoadedPage = currentPage;
var lastPage = $('div.PageNav[data-page]').first().data('last') || 1;
var threadTitle = $('title').text();
var timeToNextUpdate = 60;
calculateNextUpdate();
if (currentPage != lastPage)
currentThreadSettings.enabled = false;
// Do not enable if no messages (ie. edit / reply page)
if ($('#messageList > li').length === 0)
return;
$('#messageList > li').each(function (i, el) {
var $el = $(el);
$el.data('livethread-page', currentPage);
});
var timeout = setInterval(timerTick, 1000);
function timerTick()
{
if (!currentThreadSettings.enabled)
return;
timeToNextUpdate--;
if (timeToNextUpdate === 0)
{
updateMessages();
timeToNextUpdate = currentThreadSettings.updateTime;
}
redraw();
}
// Inserts marker after last message
function insertNotifi(text)
{
var $el = $('<div>', {'class': "newMessagesNotice livethreadSeparator"});
$el.append($('<span>').text(text));
$el.append($('<a href="#" class="pull-right"><i class="fa fa-close"></a>').click(function (event) {
event.preventDefault();
$el.prevAll('div.newMessagesNotice').remove();
$el.prevAll('li.message').removeClass('livethread_unread').hide();
$el.remove();
}));
$("#messageList").append($el);
return $el;
}
// Get URL for current page
function getCurrentURL()
{
var pageNav = $('div.PageNav[data-page]').first();
currentPage = pageNav.data('page') || 1;
lastPage = pageNav.data('last') || 1;
if (pageNav.data('baseurl') === undefined)
return window.location;
if (lastPage > currentPage)
currentPage++;
return pageNav.data('baseurl').replace('{{sentinel}}', currentPage);
}
function updateMessages()
{
if (updating)
return;
updating = true;
redraw();
countNewLast = 0;
var thisUrl = getCurrentURL();
lastUrl = getCurrentURL();
$('body').addClass('liveThread_loading');
$.get(lastUrl, function (data) {
var pageOpenTime = (new Date().getTime() / 1000),
pageOpenLength = pageOpenTime - XenForo._pageLoadTime;
// If new page insert marker and update history
if (lastLoadedPage < currentPage)
{
window.history.pushState(null, null, thisUrl);
lastUrl = thisUrl;
insertNotifi("Page " + currentPage);
lastLoadedPage = currentPage;
}
var node = $($.parseHTML(data, document, true));
function decodeEmails()
{
if ($('[data-cfemail]').length > 0)
{
var emailDecoder = node.find('script[src*="email-decode"]').attr('src');
$.getScript(emailDecoder);
}
}
var topGroup = $('Div.pageNavLinkGroup').last();
var botGroup = $('Div.pageNavLinkGroup').first();
var newNav = node.find('div.PageNav').first();
if (newNav.length)
{
var topNav = topGroup.find('div.PageNav');
var botNav = botGroup.find('div.PageNav');
if (topNav.length)
topNav.replaceWith(newNav.clone());
else
topGroup.append(newNav.clone());
if (botNav.length)
botNav.replaceWith(newNav.clone());
else
botGroup.append(newNav.clone());
}
// To avoid reloading mesasges after posting
$('input[name="last_date"]').val(node.find('input[name="last_date"]').val());
$('input[name="last_known_date"]').val(node.find('input[name="last_known_date"]').val());
var anyChanges = false;
node.find('#messageList > li').each(function (i, el) {
var $el = $(el);
var id = $el.attr('id');
var $curr = $('#' + id);
var forchecks = $el.find('article').clone();
forchecks.find('iframe').remove();
forchecks.find('noscript').remove();
// Sometimes bbcode has "X Said: <" and other times "X Said:<".
// This makes them considered equal
// Also IDs
var newData = forchecks.html().replace(/\s+</g, "<").replace(/>\s+/g, ">").replace(/\W([a-f0-9]{4,})\W/g, "");
// Update old message if changed
if ($curr.length)
{
var newMessage = $el.find('.messageInfo');
var oldMessage = $curr.find('.messageInfo');
var postDateEl = newMessage.find('.postDate abbr');
var editDateEl = newMessage.find('.editDate abbr');
// XenForo Assumes timestamps are relative to page open time
postDateEl.data('diff', postDateEl.data('diff') - pageOpenLength);
if (editDateEl !== undefined)
editDateEl.data('diff', editDateEl.data('diff') - pageOpenLength);
var embeds = $curr.find('iframe');
var editTimeNew = $el.find('.editDate abbr').data('time');
var editTimeOld = $curr.find('.editDate abbr').data('time');
var hasDifferentContent = newData !== $curr.data('original') && $curr.data('original') !== undefined;
var hasEditTimeChanged = editTimeNew !== editTimeOld;
// For debugging:
if (globalSettings.enableDebug)
{
console.log("Message Updated. Before: ");
console.log($curr.data('original'));
console.log("After: ");
console.log(newData);
}
// Update if there's zero embeds
if (embeds.length == 0 && (hasDifferentContent || editTimeNew != editTimeOld))
{
anyChanges = true;
oldMessage.replaceWith(newMessage);
$curr.xfActivate();
}
else if ((hasDifferentContent) || (editTimeNew != editTimeOld))
{
anyChanges = true;
var edit = $('<a>', {'href': '#', 'class': 'refresh', 'text': 'Show Edited (Reset Embeds)'});
edit.click(function (ev) {
ev.preventDefault();
oldMessage.replaceWith(newMessage);
$curr.xfActivate();
decodeEmails();
});
$curr.find('.postDate .refresh').remove();
$curr.find('.postDate').append(edit);
}
$curr.data('original', newData);
}
// Insert new messages
else
{
anyChanges = true;
if (!hasFocus && globalSettings.useNewMessageMarker && newMessageMarker === undefined)
{
newMessageMarker = insertNotifi("");
newMessageMarker.data('count', 0);
}
$el.data('livethread-page', currentPage);
$el.addClass('livethread_unread');
var postDateEl = $el.find('.postDate abbr');
var editDateEl = $el.find('.editDate abbr');
// XenForo Assumes timestamps are relative to page open time
postDateEl.data('diff', postDateEl.data('diff') - pageOpenLength);
if (editDateEl !== undefined)
editDateEl.data('diff', editDateEl.data('diff') - pageOpenLength);
$el.xfInsert('appendTo', $("#messageList"));
$el.data('original', newData);
countNewLast++;
}
});
if (anyChanges)
{
try {
document.dispatchEvent(new CustomEvent("LiveThreadUpdate"));
} catch (ignore) {}
}
decodeEmails();
// Update message times
XenForo._TimestampRefresh.refresh(null, true);
// Highlight Own Messages
jQuery.globalEval(highlightScript);
var count = countNewLast;
if (newMessageMarker !== undefined)
{
count = newMessageMarker.data('count') + countNewLast;
newMessageMarker.data('count', count);
newMessageMarker.find('span').text(count + " new messages");
}
}).always(function () {
updating = false;
calculateNextUpdate();
redraw();
$('body').removeClass('liveThread_loading');
});
}
// Control Panel
function updateForm()
{
addoptions($("#updateTime"), timeoptions);
addoptions($("#updateTimeDefault"), timeoptions);
$("#updateTime option[value='" + currentThreadSettings.updateTime + "']").attr("selected", true);
$("#updateTimeDefault option[value='" + globalSettings.updateTime + "']").attr("selected", true);
$("#liveThread_remember").attr("checked", globalSettings.rememberThreads);
$("#liveThread_messageMarkers").attr("checked", globalSettings.useNewMessageMarker);
$("#liveThread_enableByDefault").attr("checked", globalSettings.enabledByDefault);
$('#liveThread_currentRemember').attr("checked", isRememberedThread);
$("#liveThread_debug").attr("checked", globalSettings.enableDebug);
}
function isvisible($ele) {
var lBound = $(window).scrollTop(),
uBound = lBound + $(window).height(),
top = $ele.offset().top,
bottom = top + $ele.outerHeight(true);
return (top > lBound && top < uBound) || (bottom > lBound && bottom < uBound) || (lBound >= top && lBound <= bottom) || (uBound >= top && uBound <= bottom);
}
function getStatusText()
{
var status = "";
if (updating)
status += "Updating";
else if (currentThreadSettings.enabled)
{
status += "Next Update In " + timeToNextUpdate + " seconds";
if (countNewLast > 0)
status += " - " + countNewLast + " New Messages!";
}
else
return pauseReason || "";
return status;
}
// Build Controls
$('a.postsRemaining').hide();
var controlsContainer = $('<div>', {class: 'linkGroup'});
var statusText = $('<a>', {href: '#', class: 'livethreadStatus livethreadRefresh postsRemaining'});
var startPauseBtn = $('<a>', {href: '#', class: 'livethreadStartPause'}).append($('<i>', {class: 'fa'}));
var settingsBtn = $('<a>', {href: '#', class: 'livethreadSettings'}).append($('<i>', {class: 'fa fa-cog'}));
var refreshBtn = $('<a>', {href: '#', class: 'livethreadRefresh'}).append($('<i>', {class: 'fa fa-refresh'}));
controlsContainer.append(statusText);
controlsContainer.append(startPauseBtn);
controlsContainer.append(refreshBtn);
controlsContainer.append(settingsBtn);
$('Div.pageNavLinkGroup').last().prepend(controlsContainer);
$('Div.pageNavLinkGroup').first().find('.linkGroup').after(controlsContainer.clone());
$('.livethreadStartPause').click(function (event) {
event.preventDefault();
pauseReason = "";
currentThreadSettings.enabled = !currentThreadSettings.enabled;
saveSettings();
redraw();
});
$('.livethreadRefresh').click(function (event) {
event.preventDefault();
updateMessages();
});
$('.livethreadSettings').click(function (event) {
event.preventDefault();
$('#livethreadPanel').toggle();
$('#livethreadPanel').scrollintoview();
});
// Update Controls
function updateControls()
{
$(".livethreadStartPause i").toggleClass('fa-pause', currentThreadSettings.enabled);
$(".livethreadStartPause i").toggleClass('fa-play', !currentThreadSettings.enabled);
$(".livethreadRefresh i").toggleClass('fa-spin', updating);
$(".livethreadStatus").text(getStatusText());
}
// Build Settings
$('Div.pageNavLinkGroup').last().after('\
<div id="livethreadPanel" class="DiscussionListOptions secondaryContent">\
<h2 class="heading h1">This Thread</h2>\
<ul>\
<li><label for="updateTime">Update Speed:</label> <select id="updateTime" class="textCtrl"></select></li>\
<li><label><input type="checkbox" id="liveThread_currentRemember" value="1"> Remember this thread</label></li>\
</ul>\
<h2 class="heading h1">Global Settings</h2>\
<ul>\
<li><label><input type="checkbox" id="liveThread_remember" value="1"> Remember New Threads by Default</label></li>\
<li><label><input type="checkbox" id="liveThread_enableByDefault" value="1"> Enable By Default</label></li>\
<li><label><input type="checkbox" id="liveThread_messageMarkers" value="1"> Insert Marker for New Messages</label></li>\
<li><label>Default Update Speed: <select id="updateTimeDefault" class="textCtrl"></select></label></li>\
<li><label><input type="checkbox" id="liveThread_debug" value="1"> Log Debug Data to Console (only for testing)</label></li>\
</ul>\
</div>');
$('#liveThread_currentRemember').change(function () {
isRememberedThread = $('#liveThread_currentRemember').is(':checked');
saveSettings();
});
$('#liveThread_enableByDefault').change(function () {
globalSettings.enabledByDefault = $('#liveThread_enableByDefault').is(':checked');
saveSettings();
});
$('#liveThread_messageMarkers').change(function () {
globalSettings.useNewMessageMarker = $('#liveThread_messageMarkers').is(':checked');
saveSettings();
});
$('#liveThread_remember').change(function () {
globalSettings.rememberThreads = $('#liveThread_remember').is(':checked');
saveSettings();
});
$('#liveThread_debug').change(function () {
globalSettings.enableDebug = $('#liveThread_debug').is(':checked');
saveSettings();
});
$('#updateTime').change(function () {
currentThreadSettings.updateTime = parseInt($('#updateTime').val());
saveSettings();
if (currentThreadSettings.updateTime < timeToNextUpdate)
calculateNextUpdate();
redraw();
});
function handleScroll()
{
$('.livethread_unread').each(function (i, el) {
var $el = $(el);
if (isvisible($el.find('div.messageMeta')))
{
$el.removeClass('livethread_unread');
$el.prevAll('.livethread_unread').removeClass('livethread_unread');
}
});
}
$('#updateTimeDefault').change(function () {
globalSettings.updateTime = parseInt($('#updateTimeDefault').val());
saveSettings();
});
$(window).scroll(function () {
handleScroll();
newMessageMarker = undefined;
redraw();
});
$(window).focus(function () {
// Reset new messages on focus
handleScroll();
redraw();
hasFocus = true;
newMessageMarker = undefined;
});
$(window).focusout(function () {
handleScroll();
redraw();
hasFocus = false;
newMessageMarker = undefined;
});
function redraw()
{
updateControls();
$('body').toggleClass('liveThread_enabled', currentThreadSettings.enabled);
var unreadMessages = $('.livethread_unread').length;
var newTitle = document.title;
if (unreadMessages > 0)
newTitle = "(" + unreadMessages + ") " + threadTitle;
else
newTitle = threadTitle;
if (newTitle != document.title)
document.title = newTitle;
}
redraw();
updateForm();
})();