ninalanyon / SW_Enhancer

// ==UserScript==
// @name     SW_Enhancer
// @version  0.94.3
// @author   Nina Lanyon
// @grant    GM_getValue
// @grant    GM_setValue
// @grant    GM_log
// @require  https://openuserjs.org/src/libs/sizzle/GM_config.js
// @include  https://similarworlds.com/*
// @updateURL https://openuserjs.org/meta/ninalanyon/SW_Enhancer.meta.js
// @downloadURL https://openuserjs.org/install/ninalanyon/SW_Enhancer.user.js
// @copyright 2021, Nina Lanyon (https://openuserjs.org/users/ninalanyon)
// @license MIT
// ==/UserScript==

/*
Changes:
- 0.94.3 Added let to for statements.

- 0.94.2 Removed some logging that was reading properties before they
         existed.

- 0.94.1 SW implemented scrolling of the highlighted respons into
         view so I have disabled my function.

- 0.93.4 SW changed the name of the visibility element.

- 0.93.3 SW changed the classname of active notifications causing
         hiding of inactive notifications to fail.  Fixed.

- 0.93.2 Fixed bug in hideUnreadableReplyAndThread which hides the
         comments replying to a deleted comment.

- 0.93.1 Fixed bug in SetVisibility using window.eval to call and SW
         function.

- 0.93.0 In progress: automatically follow people when responding.

- 0.92.0 Hide read mail in https://similarworlds.com/messages

- 0.90.3 Fixed the function that hides inactive notifications to hide
         content box divs when all of the content is hidden.

- 0.90.1 Ignore mutations caused by the leechblock addon.

- 0.90 Added option to show only unread notifications

- 0.82 Added option to hide the Add downvote reaction button.

- 0.8 Fixed error in function that hides the motivational quotes, it
      was hiding other elements as well.

- 0.7 First published version.

*/

/* ----------------
Define the configuration dialog.
*/


(async function () {
    'use strict';

    // A few bits of shorthand
    function getElementById(id) {
        return document.getElementById(id);
    }

    function getElementsByClassName(name) {
        return document.getElementsByClassName(name);
    }

    function createElement(elementType) {
        return document.createElement(elementType);
    }

    // Name some of the fields, field types, etc. to assist in
    // preventing typos.
    const SExpandCollapsedPosts = 'expandCollapsedPosts';
    const SExpandCollapsedReplies = 'expandCollapsedReplies';
    const SHideUnreadableRepliesAndThread = 'hideUnreadableRepliesAndThread';
    const SHideUnwantedPostsInFeed = 'hideUnwantedPostsInFeed';
    const SMatchUnwantedPostsInFeed = 'matchUnwantedPostsInFeed';
    const SIgnoreCaseWhenMatching = "ignoreCaseWhenMatching";
    const SHidePosQuotes = "hidePosQuotes";
    const SEnableSWEnhancer = "enableSWEnhancer";
    const SHideSWReactionsNeg = "hideSWReactionsNeg";
    const SShowOnlyUnreadNotifications = "showOnlyUnreadNotifications";
    const SShowOnlyUnreadMail = "showOnlyUnreadMail";

    const SSetVisibility = "setVisibility";

    const VVisibilityDoNotSet = "Do not set";
    const VVisibilityLimited = "Limited";
    const VVisibilityPublic = "Public";
    const VVisibilityPrivate = "Private";

    function makeConfiguration() {
        console.log("a makeConfiguration");

        const TCheckBox = "checkbox";
        const TTextArea = "textarea";
        const TRadio = "radio";

        const KLabel = "label";
        const KType = "type"
        const KDefault = "default";
        const KTitle = "title";
        const KSection = "section";
        const KRows = "rows";
        const KCols = "cols";
        const KOptions = "options";

        console.log("SW m");

        var fieldDefs = {
            [SExpandCollapsedPosts]: {
                [KSection]: ["Hiding and revealing parts of a post"],
                [KLabel]: "Expand collapsed posts immediately",
                [KTitle]: "Pictures in posts are hidden by default, ticking this box makes them show always.",
                [KType]: TCheckBox,
                [KDefault]: false
            },
            [SExpandCollapsedReplies]: {
                [KLabel]: "Expand collapsed replies immediately",
                [KTitle]: "Replies with sensitive content are collapsed by default on SW, ticking this box saves you the bother of clicking each one yourself.",
                [KType]: TCheckBox,
                [KDefault]: false
            },
            [SHideUnreadableRepliesAndThread]: {
                [KLabel]: "Hide any replies that are marked as deleted together with all the replies to them",
                [KType]: TCheckBox,
                [KTitle]: "Once a reply has been deleted replies often become unfathomable and any you can't reply to them.  Ticking this box hides the deleted item and all the replies to it.",
                [KDefault]: false
            },
            [SHideUnwantedPostsInFeed]: {
                [KSection]: ["Feed related settings"],
                [KLabel]: 'Hide posts in the feed that match a regular expression',
                [KType]: TCheckBox,
                [KTitle]: "A simple example would be 'spank|diaper|poop' (without the quotation marks.  This would hide any feed items that contain any of those words.",
                [KDefault]: false
            },
            [SMatchUnwantedPostsInFeed]: {
                [KLabel]: "Enter terms below separated by a pipe symbol: |.  " +
                "If any of these terms appear in post extracts or post titles in the feed then the post that contains them will be hidden.  " +
                "Note that this applies to the posts and the comments.",
                [KType]: TTextArea,
                [KTitle]: "A simple example would be 'spank|diaper|poop' (without the quotation marks.  This would hide any feed items that contain any of those words.",
                [KDefault]: "",
                [KCols]: 60,
                [KRows]: 4
            },
            [SIgnoreCaseWhenMatching]: {
                [KLabel]: "Case insensitive matching.",
                [KType]: TCheckBox,
                [KTitle]: "For instance match all of these: Spank, SPANK, spank",
                [KDefault]: true
            },
            [SEnableSWEnhancer]: {
                [KSection]: ["Miscellaneous settings"],
                [KLabel]: 'Enable this tool',
                [KType]: TCheckBox,
                [KTitle]: "Disabling still allows you to change the settings.",
                [KDefault]: true
            },
            [SHidePosQuotes]: {
                [KLabel]: 'Hide the silly motivational quotes',
                [KType]: TCheckBox,
                [KTitle]: "I find them irritating, I would rather that they weren't there.",
                [KDefault]: false
            },
            [SHideSWReactionsNeg]: {
                [KLabel]: 'Hide the Downvote button in the Add Reaction popup',
                [KType]: TCheckBox,
                [KTitle]: "Makes it impossible to accidentally downvote and removes the irritating thing from my sight!",
                [KDefault]: true
            },
            [SShowOnlyUnreadNotifications]: {
                [KLabel]: "Hide notifications that you have already clicked on.",
                [KType]: TCheckBox,
                [KTitle]: "Makes it harder to forget a notification.",
                [KDefault]: true
            },
            [SShowOnlyUnreadMail]: {
                [KLabel]: "Hide mail that you have already clicked on.",
                [KType]: TCheckBox,
                [KTitle]: "Makes it harder to forget a message.",
                [KDefault]: true
            },
            [SSetVisibility]: {
                [KLabel]: 'Post visibility',
                [KType]: TRadio,
                [KOptions]: [VVisibilityDoNotSet, VVisibilityPublic, VVisibilityLimited, VVisibilityPrivate],
                [KTitle]: "When creating a new post or editing one set the visibility.",
                [KDefault]: VVisibilityDoNotSet
            }

        };

        GM_config.init({
            id: 'SWEnhancerConfig',
            title: 'SW Enhancer Settings',
            fields: fieldDefs,
        });
        console.log("SW bp ");
        //console.log("b " + GM_config.get(SExpandCollapsedPosts));
    }

    makeConfiguration();
    console.log("cp ");
    //console.log("c " + GM_config.get(SExpandCollapsedPosts));
    /*try {
    console.log("d " + GM_config.get(SExpandCollapsedReplies));
    }
    catch (error) {
    console.error(error);
    }
  */

    function showConfiguration() {
        GM_config.open();
    }

    console.log("SW F");

    let menu = getElementById("sw-header-profile-drop");
    if (menu != null) {
        let sep = createElement("div");
        sep.className = "header-dropsep";
        menu.appendChild(sep);
        console.log("SW G");

        let item = createElement("div");
        menu.appendChild(item);

        let a = createElement("a");
        a.className = "opt";
        a.onclick = function () {
            showConfiguration();
        };
        console.log("SW E ");

        a.innerHTML = '<i class="sw-icon sw-i-edit-small"></i>User Settings';
        item.appendChild(a);
    }

    // See
    // https://plainjs.com/javascript/styles/get-the-position-of-an-element-relative-to-the-document-24/
    function offset(el) {
        const
        rect = el.getBoundingClientRect(),
              scrollLeft = window.pageXOffset || document.documentElement.scrollLeft,
              scrollTop = window.pageYOffset || document.documentElement.scrollTop;
        return {
            left: rect.left + scrollLeft,
            top: rect.top + scrollTop
        };
    }

    var highlightedElementTop = 0;
    var allowScroll = true;
    var lastUrl = "";
    var postEntryHeight = 0;
    var cancelScrollTimerId = 0;

    function cancelScroll() {
        allowScroll = false;
    }

    // Return false if we should not scroll the highlighted comment
    // into view.  To determine this we check to see if the user has
    // expanded the text areas for data entry.
    function allowedToScroll() {
        console.log("SW e allowedToScroll: ", allowScroll);
        // SW has implemented this themselves so we should not do it as the two fight each other.
        allowScroll = false
        return allowScroll

        if (lastUrl !== document.location.href) {
            // SW sets the page url without 'navigating' to it.  Make
            // sure we start again if we are on a new page.
            console.log("e url changed");
            allowScroll = true;
            postEntryHeight = 0;
            lastUrl = document.location.href;
        }
        if (allowScroll) {
            // Check to see if we have entered the text entry box.  If
            // so cancel scrolling.
            const textEntries = getElementsByClassName("form-textarea");
            const textEntry = textEntries.item(0);
            const h = textEntry.clientHeight;
            console.log("f: ", h);
            if (postEntryHeight == 0) {
                console.log("g peh was 0");
                postEntryHeight = h;
            }
            if (postEntryHeight != h) {
                console.log("h peh changed");
                postEntryHeight = h;
                allowScroll = false;
            }
        }
        return allowScroll;

    }

    function scrollToHighlightedComment() {
        if (!allowedToScroll()) {
            return;
        }
        const divs = getElementsByClassName("cmt-hghlt");
        console.log("i scrollToHighlightedComment: ", divs);
        if (divs.length !== 0) {
            let div = divs.item(0);
            let rect = offset(div);
            // Round to integer to avoid scrolling because of changes
            // in the umpteenth decimal place.
            let newTop = Math.round(rect.top);
            console.log("j highlightedElementTop: ", highlightedElementTop);
            console.log("k rect: ", rect.top);
            if (newTop != highlightedElementTop) {
                // position has changed.
                highlightedElementTop = newTop;
                divs.item(0).scrollIntoView({
                    behavior: 'smooth',
                    block: "end",
                    inline: "nearest"
                });
                clearTimeOut(cancelScrollTimeoutId);
                cancelScrollimerId = setTimeout(cancelScroll, 1000)
            }
        }
    }

    function clickButtons(className) {
        console.log("SW f");
        const divs = getElementsByClassName(className);
        for (let i = 0; i < divs.length; i++) {
            divs.item(i).getElementsByClassName("small-button").item(0).click();
        }
    }

    function unBlurInMessages() {
        console.log("SW g");
        const anchors = getElementsByClassName("small-button blur-btn active");
        [].forEach.call(anchors, function (a) {
            if (a.style.display != "none") {
                a.click();
            }
        })
    }

    function clickStoryCompressed() {
        console.log("SW h");
        const a = getElementById("story-compressed");
        if (a !== null) {
            a.click();
        }
    }

    const rootUrl = "https://similarworlds.com/";

    const feedsUrls = [rootUrl,
                       rootUrl + "top",
                       rootUrl + "new"
                      ];

    function getParent(element, rel) {
        if (rel === 0) {
            return element;
        }
        else {
            return getParent(element.parentElement, rel - 1);
        }
    }

    function hideContentBoxIfContentsHidden(contentBox) {
        console.log("i");
        let active =
            contentBox.getElementsByClassName("sw-notif-active");
        if (active.length == 0) {
            contentBox.style.display = "none";
        }
    }

    function hideItemsRelative(elements, parentRel) {
        "use strict";
        console.log("k");
        let contentBoxes = new Set();
        try {
            for (let i = 0; i < elements.length; i++) {
                let element = elements[i];
                let parent = getParent(element, parentRel);
                parent.style.display = "none";
                contentBoxes.add(parent.parentElement);
            }
        }
        catch (ex) {
            alert('b ex: ' + ex.toString());
        }
        contentBoxes.forEach(hideContentBoxIfContentsHidden);
    }

    function hideItems(className, parentRel, matcher) {
        "use strict";
        console.log("SW j");
        var elements = document.getElementsByClassName(className),
            element,
            text,
            i;

        try {
            for (let i = 0; i < elements.length; i++) {
                element = elements[i];
                if (element) {
                    text = element.textContent;
                    if (typeof text !== 'undefined') {
                        if (text.search(matcher) !== -1) {
                            let parent = getParent(element, parentRel);
                            parent.style.display = "none";
                            //parent.visibility = "hidden";
                            //element.style.display = "none";
                            //element.visibility = "hidden";
                        }
                    }
                }
            }
        }
        catch (ex) {
            alert('b ex: ' + ex.toString());
        }
    }

    function hideUnwantedPostsInFeed() {
        let matchSrc = GM_config.get(SMatchUnwantedPostsInFeed);
        let matchInsensitive = GM_config.get(SIgnoreCaseWhenMatching);
        let flags = matchInsensitive ? "i" : "";
        let matcher = new RegExp(matchSrc, flags);

        hideItems("swcmnt", 3, matcher);
        hideItems("cmnt-post-prevw", 3, matcher);
        hideItems("pstbx-group", 2, matcher);
        hideItems("rctrpl-comment", 3, matcher);
    }

    function runClickButtons() {
        // click the view more replies button
        console.log("d ");
        //alert("runClickButtons");
        if (GM_config.get(SExpandCollapsedReplies)) {
            clickButtons("nested-comments-getmore");
        }
        clickButtons("cmtbx-showhdn");
        unBlurInMessages();
        console.log("e ");
        if (GM_config.get(SExpandCollapsedPosts)) {
            clickStoryCompressed();
        }
        // TODO: reinstate when fixed:
        //scrollToHighlightedComment();

    }

    function hideClass(className) {
        "use strict";
        var elements = document.getElementsByClassName(className);
        console.log("SW l  " + className + " " + elements.length);
        for (var i = 0; i < elements.length; i++) {
            let element = elements[i];
            element.style.display = "none";
        }
    }

    function hidePosQuotes() {
        "use strict";
        console.log("SW p hidePosQuotes");
        hideClass("pos-quote");
        hideClass("sw-body-1-aa");
        hideClass("pos-quote-in");
        //hideClass("cmtbx-bx-body");
        //hideClass("cmtbx-elem");
        let e = getElementById("sw-right-side-aa");
        e.style.display = "none";
    }

    function hideSWReactionsNeg() {
        hideClass("sw-reactions neg");
    }

    function hideOldNotifications() {
        let active = getElementsByClassName("sw-notif active");
        if (active.length == 0) {
            // No active items.  So we should not hide the inactive ones either.
            return;
        }
        let inactive = document.querySelectorAll('.sw-notif:not(.active)');
        hideItemsRelative(inactive, 0);
    }

    function hideOldMail() {
        let active = document.querySelectorAll('.message-box:not(.read)');
        if (active.length == 0) {
            // No active items.  So we should not hide the inactive ones either.
            return;
        }
        hideClass('message-box read');
    }

    function hideUnreadableReplyAndThread(element) {
        "use strict";
        element.style.display = "none";
        let next = element.nextElementSibling;
        if (next == null) {
            return;
        }
        next.style.display = "none";
        /*let comslst = next.firstElementChild;
    let firstComment = comslst.firstElementChild;
    if (firstComment.classList.contains("cmt-nst")){
        comslst.style.display = "none";
    }
    */

    }

    function hideUnreadableRepliesAndThread() {
        "use strict";
        var elements = document.getElementsByClassName("comment-deleted");
        console.log("SW sa  " + elements.length);
        for (var i = 0; i < elements.length; i++) {
            hideUnreadableReplyAndThread(elements[i]);
        }

    }

    // function setVisibility(){
    //   let postseg_3 = getElementById("postseg-3");
    //   if (postseg_3 == null || postseg_3.style.display == "none"){
    //     // Not ready to be set yet or not on the right page.
    //     return;
    //   }
    //   let newVisibility = GM_config.get(SSetVisibility);
    //   let code = [VVisibilityDoNotSet,
    //                 VVisibilityPublic,
    //                 VVisibilityPrivate,
    //                 VVisibilityLimited].indexOf(newVisibility);
    //     if (code == 0){
    //       return;
    //     }
    //   let visibilitySpan = getElementById("postentry-visible" + code);
    //   if (visibilitySpan.style.display == "none"){
    //       window.eval("postEditVisibility('0', " + code + ", 'postentry-visible');");
    //   }

    // }

    function setVisibility() {
        var visibilitySpan;
        for (let code = 1; code <= 3; code++) {
            visibilitySpan = getElementById("postentry-visible" + code);
            if (visibilitySpan != null && visibilitySpan.style.display != "none") {
                break;
            }
        }
        if (visibilitySpan == null || visibilitySpan.style.display == "none") {
            // Not ready to be set yet or not on the right page.
            return;
        }
        let newVisibility = GM_config.get(SSetVisibility);
        let code = [VVisibilityDoNotSet,
                    VVisibilityPublic,
                    VVisibilityPrivate,
                    VVisibilityLimited
                   ].indexOf(newVisibility);
        if (code == 0) {
            return;
        }
        visibilitySpan = getElementById("postentry-visible" + code);
        if (visibilitySpan.style.display == "none") {
            window.eval("postEditVisibility('0', " + code + ", 'postentry-visible');");
        }

    }

    function runFixUp() {
        console.log("SW q runFixUp");

        //alert("SW a " + window.location.href);
        if (feedsUrls.includes(window.location.href)) {
            if (GM_config.get(SHideUnwantedPostsInFeed)) {
                hideUnwantedPostsInFeed();
            }
        }
        else {
            runClickButtons();
        }
        if (GM_config.get(SHidePosQuotes, false)) {
            hidePosQuotes();
        }
        if (GM_config.get(SHideSWReactionsNeg, true)) {
            hideSWReactionsNeg();
        }
        if (window.location.href === "https://similarworlds.com/notifications" &&
            GM_config.get(SShowOnlyUnreadNotifications, true)) {
            hideOldNotifications();
        }
        if (window.location.href === "https://similarworlds.com/messages" &&
            GM_config.get(SShowOnlyUnreadMail, true)) {
            hideOldMail();
        }
        if (GM_config.get(SHideUnreadableRepliesAndThread, true)) {
            hideUnreadableRepliesAndThread();
        }
        setVisibility();
    }

    function run() {
        console.log("SW n run");
        //alert("SW B");
        let timerId = 0;
        const observer = new MutationObserver((mutationList, observer) => {
            if (mutationList.length == 1 &&
                mutationList[0].target.className == "leechblock-timer") {
                // Leech block places a countdown in the top left
                // corner which causes mutations that we aren't
                // interested in, just ignore them.
                return;
            }
            console.log("SW o observer", mutationList);
            // cancel the timeout if it is running so that we don't run
            // until the cascade of mutations has died down.

            clearTimeout(timerId);

            // click the buttons a little while after the last mutation event
            timerId = setTimeout(runFixUp, 100)
        });

        // observe everything
        observer.observe(
            document.body, {
                childList: true, // observe direct children
                subtree: true, // and lower descendants too
                attributes: true
            }
        );
    }


    console.log("SW D");
    //alert("SW D ");
    //if (GM_config.get(SEnableSWEnhancer)) {
    if (true) {
        console.log("SW l");
        //alert("SW C");
        run();
    }

})()