ResetEra Live Thread

Update threads without refreshing

2018-07-02 या दिनांकाला. सर्वात नवीन आवृत्ती पाहा.

// ==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();
})();