quark_zju / phab.mercurial-scm.org customization

// ==UserScript==
// @name         phab.mercurial-scm.org customization
// @namespace    http://tampermonkey.net/
// @version      0.2
// @description  Make reviewing a bit easier
// @author       Jun Wu
// @match        https://phab.mercurial-scm.org/D*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    //// Utilities

    // Select an outer element by text of an inner element
    let selectByInner = (outer, inner, text) => {
        let results = [];
        document.querySelectorAll(outer).forEach((outer) => {
            if (inner) {
                outer.querySelectorAll(inner).forEach((inner) => {
                    if (inner.textContent === text) {
                        results.push(outer);
                    }
                });
            } else {
                if (outer.textContent.includes(text)) {
                    results.push(outer);
                }
            }
        });
        return results;
    };

    let hide = (e) => { e.style.display = 'none'; };

    let toArray = (l) => Array.prototype.slice.call(l);

    // Find Differential Revision number in an element
    let drevRegex = /\/D([1-9][0-9]*)$/;
    let findDRevNum = (e) => {
        let result = null;
        if (e) {
            e.querySelectorAll('a').forEach((a) => {
                let match = drevRegex.exec(a.href);
                if (match) result = match[1];
            });
        }
        return result;
    };

    //// Actual interface tweaks

    // Hide "Authored by ..." - duplicated with "... created this revision"
    // document.querySelectorAll('.phui-two-column-subheader .phui-head-thing-view').forEach(hide);

    // Hide "Details" label
    selectByInner('.phui-header-shell', '.phui-header-header', 'Details').forEach(hide);

    // Hide "Reviewers"
    selectByInner('.phui-property-list-container', null, 'Reviewers').forEach(hide);

    // Hide "SUMMARY" label
    selectByInner('.phui-property-list-section-header', null, 'Summary').forEach(hide);

    // Hide "Diff Detail"
    selectByInner('.phui-box.phui-object-box.phui-box-border', '.phui-header-header', 'Diff Detail').forEach(hide);

    // Simplify side column
    let toRemove = /(Update Diff|Edit Revision|Download Raw Diff|Edit Related Objects|Automatically Subscribed|Flag For Later)/;
    document.querySelectorAll('.phabricator-action-view').forEach((e) => {
        if (toRemove.exec(e.textContent)) hide(e);
    });

    // Hide "Tags" or "Subscribers" if it's "None"
    ['Subscribers', 'Tags'].forEach((name) => {
        selectByInner('.phui-curtain-panel', '.phui-curtain-panel-header', name).forEach((e) => {
            if (e.textContent.includes('None')) hide(e);
        });
    });

    // Hide actions
    document.querySelectorAll('.phui-timeline-shell').forEach((e) => {
        let needHide = false;
        // Hide Herald
        if (!needHide && toArray(e.querySelectorAll('a')).some((a) => a.textContent === 'Herald')) needHide = true;
        // Hide "added a dependent revision" actions
        if (!needHide && /(added a dependent revision|created this revision)/.exec(e.textContent)) needHide = true;
        // Hide non-human text like "This revision now requires changes to proceed"
        if (!needHide && e.querySelectorAll('.phui-timeline-title .phui-link-person').length === 0) needHide = true;
        if (needHide) {
            hide(e);
            // Hide the spacer too
            let next = e.nextElementSibling;
            if (next && next.classList.contains('phui-timeline-spacer')) hide(next);
        }
    });

    // Hide "Revision Contents" label
    selectByInner('.phui-header-shell', '.phui-header-header', 'Revision Contents').forEach(hide);

    // Hide "Lint was skipped ..." warning box
    selectByInner('.phui-info-view.phui-info-severity-warning', null, 'Lint was skipped').forEach(hide);

    // Get some information about the stack
    let prevDRev, nextDRev, stackSize = 0, stackIndex;
    document.querySelectorAll('.object-graph-table').forEach((e) => {
        e.querySelectorAll('tr.highlighted, tr.alt-highlighted').forEach((currRow) => {
            if (!stackIndex) {
                let rows = toArray(e.querySelector('tbody').children).slice(1); // remove table header
                stackSize = rows.length;
                stackIndex = stackSize - rows.indexOf(currRow);
            }
            if (!nextDRev) nextDRev = findDRevNum(currRow.previousElementSibling);
            if (!prevDRev) prevDRev = findDRevNum(currRow.nextElementSibling);
        });
    });

    // Show "x of y" in subtitle
    if (stackSize > 0) {
        document.querySelectorAll('.phui-header-row .phui-header-subheader').forEach((e) => {
            let span = document.createElement('span');
            span.innerHTML = '<span class="phui-tag-core">' + stackIndex + ' of ' + stackSize + '</span>';
            span.className = 'phui-tag-view phui-tag-type-shade phui-tag-shade';
            e.appendChild(span);
        });
    }

    // Use a smaller title with "Patch X of Y" information
    let title = null;
    document.querySelectorAll('.phui-two-column-header .phui-header-header').forEach((e) => { title = e.textContent; });
    if (title && stackSize > 0) {
        title = '[' + stackIndex + ' of ' + stackSize + '] ' + title;
    }
    if (title) {
        let header = document.querySelector('.phui-two-column-subheader > .phui-head-thing-view.head-thing-small');
        if (!header) return;
        // Change the small title and hide the large title
        let e = document.createElement('a');
        e.appendChild(document.createTextNode(title));
        e.style.fontWeight = 'bold';
        // Insert the title after the profile picture
        header.insertBefore(e, header.childNodes[1]);
        // Change "Authored by" to " authored by "
        header.childNodes[2].textContent = ' authored by ';
        // Hide main header
        document.querySelectorAll('.phui-two-column-header').forEach(hide);
    }

    // Register more keyboard shortcuts
    if (JX.KeyboardShortcut) {
        // Register "[", "]" to navigate to previous/next patch
        if (prevDRev) {
            let k = new JX.KeyboardShortcut('[', 'Navigate to previous patch in the stack');
            k.setHandler(() => { window.location.href = '/D' + prevDRev; }).register();
        }
        if (nextDRev) {
            let k = new JX.KeyboardShortcut(']', 'Navigate to next patch in the stack');
            k.setHandler(() => { window.location.href = '/D' + nextDRev; }).register();
        }
        // Register "a" for triggering a custom protocol: action://...
        // To register an non-web custom protocol handler, see:
        // Firefox: http://kb.mozillazine.org/Register_protocol
        // Windows: https://customurl.codeplex.com/
        // Linux: https://stackoverflow.com/questions/32064229
        // Also see https://secure.phabricator.com/book/phabricator/article/external_editor
        let k = new JX.KeyboardShortcut('A', 'Trigger an "action" protocol for this URL');
        k.setHandler(() => {
            let url = 'action://open?url=' + encodeURIComponent(window.location.href);
            console.log('Trigger! ' + url);
            window.location.href = url;
        }).register();
    }
})();