// ==UserScript==
// @name        Jira issue page updated notification
// @namespace   https://greasyfork.org/users/1047370
// @description Give Jira issue page an update button, in the top navigation bar, when the issue is updated while the page is shown.  Also update the title.
// @author      Marnix Klooster <[email protected]>
// @copyright   public domain
// @license     public domain
// @version     0.10
// @homepage    https://greasyfork.org/scripts/462479
// @include     /^https?://(jira\.[^/]*|[^/]*\.atlassian\.net)/(browse|projects/[^/]+/issues)//
// @require     https://cdn.jsdelivr.net/npm/[email protected]/build/global/luxon.min.js
// @grant       none
// ==/UserScript==
/// TODO list
///
///  * Bug report in Edge: After the button shows, and it has been clicked, then using browser 'back',
///    which goes from a URL ending in a '#' to a URL without it,
///    results in the 'refresh icon' shown again in the tab title bar.  (Edge bug?)
///
///  * Also support URLs like https://jira.example.com/secure/RapidBoard.jspa?...&selectedIssue=MYPROJ-98765&...
///
///  * Usability issue.  On 'Jira issue in search result' .../browse/SOMEPROJ-12345?jql=... pages,
///    the top navigation bar scrolls out of view when scrolling down,
///    making the 'update' button inaccessible, which is not helpful.
///
///    Consider moving the button into the issue header (<header id="stalker" class="issue-header"/>),
///    either in the primary toolbar on the left (`<div class="aui-toolbar2-primary">`)
///    or in the secondary toolbar on the right (`<div class="aui-toolbar2-secondary">`).
///
///    EXCEPT that on .../projects/SOMEPROJ/issues/... pages, it is the other way around:
///    There the top navigation bar remains visible, and the issue header scrolls out of view...
///
///    Options:
///     - Do nothing, the 'search result' user will see the 'refresh' circular arrow in the title,
///       and that triggers them to scroll up.
///     - Show two buttons always, both in top bar and in issue header.  Ugly.
///     - If the top bar button scrolls out of view, then show that button e.g. floating.
///       (Or add a second button in the issue header?)
///
///  * Try to get rid of `href="#"`, since the URL-changing when clicking the button is not nice.
///
///  * Perhaps: Also show the _type_ of changes, in the 'updated' button's tooltip.
///    (It seems this needs both its ...?field=&expand=changelog
///    and the issue's full .../comment list, and perhaps more.)
///      If this is built, it also would make sense to keep watching for issue updates,
///    and keep updating the tooltip (and perhaps the time, see other TODO item).
///
///    Additionally, if (at least) the Description changed, make the button red/bold/highlighted
///    since that other Description change will be lost.
///    (And/or make that a separate userscript, that only looks at Description collisions.)
///
///  * Idea: Switch to relative times, e.g. as follows:
///
///       changed 10 min. — 52 sec. ago
///
///    (using `DateTime.toRelative()` twice) or
///
///       changed yesterday 4:30 PM — 7:42 PM GMT+2
///
///    (using `DateTime.toRelativeCalendar()` and `DateTime.toLocaleString(DateTime.TIME_...)` twice),
///    together with the following algorithm to combine two strings into an 'interval string':
///     - Split start and end time string using `s.split(/\s+(?!(?:AM|PM))\b/)`, resulting in two arrays.
///     - Show the start, but leave out the suffix common to start and end.
///     - Show `"—"` (that is, an `&mdash`).
///     - Show the end, but leave out the prefix common to start and end.
///
///    If this is done, try hard to update the button also in case of a connection error.
///    (The button could even be made a different color in that case...)
///
///  * Bug/limitation: From a query result page (https://jira.infor.com/issues/?jql=...)
///    clicking on a specific issue, the page is updated 'in place',
///    so even though the address bar URL changes, this userscript is not activated.
///    See if there is a way to fix that.
///
///  * Robustness in case `aui-nav` element does not exist.
///
///  * Perhaps: Wait longer after an error response, to reduce server load?
/// END of TODO list
"use strict";
/// Configuration settings
/// (also look at @include and @match in the manifest above,
/// which you can usually override in your userscript browser extension configuration)
/// Setting the following too high will overload the Jira server;
/// setting it too low will make this userscript less useful.
var timeBetweenChecksInSeconds = 10;
/// END of Configuration settings
/// Helper functions
/// From https://stackoverflow.com/a/35385518/223837:
/// Construct an DOM element from the given HTML string.
/// The caller must ensure no injection occurs, e.g. using `stringToHTML()` below.
function htmlToElement(html) {
    var template = document.createElement('template');
    html = html.trim(); // Never return a text node of whitespace as the result
    template.innerHTML = html;
    return template.content.firstChild;
}
/// From https://stackoverflow.com/a/22706073/223837:
/// Convert a string to the equivalent HTML source code.
function stringToHTML(str){
    return new Option(str).innerHTML;
}
/// END of Helper functions
// Documentation about the Jira REST API that is used here can be found at
// https://docs.atlassian.com/software/jira/docs/api/REST/latest/
// (currently https://docs.atlassian.com/software/jira/docs/api/REST/9.7.0/),
// specifically
//
// - https://docs.atlassian.com/software/jira/docs/api/REST/latest/#api/2/issue-getIssue
// - https://docs.atlassian.com/software/jira/docs/api/REST/latest/#api/2/issue-getComments
//
// Documentation about the Javascript JIRA object's API doesn't seem to exist.
// There are mostly fragments floating around in forum questions and answers.
(function () {
    /// 'global' variables
    var theInterval = null;
    var issueFirstUpdatedAfterPage = null;
    function start() {
        if (theInterval) {
            console.log(`SOMETHING WENT WRONG.  Ignoring this call to start().`);
            return;
        }
        console.log(`STARTING regular check for issue updates`);
        theInterval = setInterval(checkForUpdates, timeBetweenChecksInSeconds * 1000);
    }
    /// (Note that this function is currently not used.)
    function stop() {
        if (!theInterval) {
            console.log(`SOMETHING WENT WRONG.  Ignoring this call to stop().`);
            return;
        }
        console.log(`STOPPING regular check for issue updates`);
        clearInterval(theInterval);
        theInterval = null;
    }
    function getPageLastUpdated() {
        // The 'best' implementation for this would be
        // ```
        // luxon.DateTime.fromISO(document.getElementById("updated-val").getElementsByTagName("time")[0].getAttribute("datetime"));
        // ```
        // but because of Jira 9 bug https://jira.atlassian.com/browse/JRASERVER-76257 (at least up until 9.10.1),
        // instead we take the latest <... class="livestamp" datetime="..."> we can find on the page.
        return luxon.DateTime.max(...Array.from(document.querySelectorAll('.livestamp[datetime]')).map(
            (t) => luxon.DateTime.fromISO(t.getAttribute("datetime"))
        ));
    }
    function checkForUpdates() {
        var issueNumber = JIRA.Issue.getIssueKey();
        if (issueNumber == null) {
            console.log(`No issue number found (yet?)...  trying again in a little while`);
            return;
        }
        console.log(`Checking whether issue ${issueNumber} has been recently updated`);
        fetch(new Request(`/rest/api/latest/issue/${issueNumber}?fields=updated`, {
            headers: {'Content-Type': 'application/json'}
        })).then((response) => {
            // technical handling of the response
            if (!response.ok) {
                console.log(`Something went wrong checking for updates of issue ${issueNumber}, will retry in a little while: ${response.statusText}`);
                return Promise.reject(response); // not handled in any way, no need
            }
            return response.json();
        }).then((responseJSON) => {
            // functional handling of the response
            // 'last updated' according to the Jira REST API
            var issueLastUpdated = luxon.DateTime.fromISO(responseJSON.fields.updated);
            // 'last updated' according to this page; could perhaps also be read via the JIRA object? could not find any documentation
            var pageLastUpdated = getPageLastUpdated();
            console.log(`Issue ${issueNumber} was last updated ${issueLastUpdated}`);
            console.log(`This page says its data is from ${pageLastUpdated}' (+/- 1 second)`);
            // remove any UI changes, preparing to make them again below if necessary.
            var updateButtonElement = document.getElementById('marnix_update_page_button');
            if (updateButtonElement) {
                // below always add a fresh button, so that we have the updated text
                updateButtonElement.remove();
            }
            const prefix = '\u21BB ';
            if (document.title.startsWith(prefix)) {
                document.title = document.title.substring(prefix.length);
            }
            if (pageLastUpdated.toMillis() + 1000 < issueLastUpdated.toMillis()) { // + 1000 since 'page last updated' has no millisecond information
                // issue was updated after the page information was refreshed
                if (!issueFirstUpdatedAfterPage) {
                    issueFirstUpdatedAfterPage = issueLastUpdated;
                }
                var issueLastUpdatedText = luxon.Interval.fromDateTimes(
                    issueFirstUpdatedAfterPage, issueLastUpdated).toLocaleString(luxon.DateTime.TIME_WITH_SHORT_OFFSET, {});
                console.log(`Concluding that issue was updated after last page refresh, just now at ${issueLastUpdatedText}: ensuring update button and updated title`);
                // We put the button as the last in the <ul class="aui-nav"> top navigation bar.
                // (The button is the same as the 'Create' button; `href="#"` is needed for the correct hover color.)
                updateButtonElement = htmlToElement(`
                            <li id="marnix_update_page_button">
                                <a
                                    href="#"
                                    class="aui-button aui-button-primary aui-style"
                                    title="Update page"
                                   >Update (changed ${stringToHTML(issueLastUpdatedText)})</a>
                            </li>
                        `);
                updateButtonElement.addEventListener("click", updateThisPage);
                document.getElementsByClassName("aui-nav")[0].appendChild(updateButtonElement);
                // prepend 'Clockwise Open Circle Arrow' character
                document.title = `${prefix}${document.title}`;
                return;
            }
            console.log(`Concluding that there was no recent issue ${issueNumber} update, will check again in a little while.`);
        });
    }
    function updateThisPage() {
        console.log(`Let this page update its information (which also reverts the title)`);
        JIRA.trigger(JIRA.Events.REFRESH_ISSUE_PAGE, [JIRA.Issue.getIssueId()]);
        // this event triggers an update, which raises an ISSUE_REFRESHED event,
        // which is caught by the event handler below, which will re-enable the regular check for issue updates
        // (or it triggers a full page reload, sometimes, it seems, e.g. if there is a network issue)
    }
    JIRA.bind(JIRA.Events.ISSUE_REFRESHED, function (e, context) {
        console.log(`Something triggered a refresh of this page`);
        if (!theInterval) {
            console.log(`We will start to look for issue updates again in a little while`);
            start();
        }
        var updateButtonElement = document.getElementById('marnix_update_page_button')
        if (updateButtonElement) {
            console.log(`We can remove the update button again`);
            updateButtonElement.remove();
            issueFirstUpdatedAfterPage = null;
            // no need to revert the document.title, the refresh that just happened has already done that
        }
    });
    // No need to updateThisPage() on JIRA.Events.INLINE_EDIT_SAVE_COMPLETE,
    // because every inline edit also updates the `Updated:` (id="updated-val") field,
    // and we already trigger on that.
    start();
})();