My Interactions notification bubble

Add a notification bubble to the sidebar for outstanding Interactions assigned to you.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

// ==UserScript==
// @name         My Interactions notification bubble
// @namespace    https://github.com/nate-kean/
// @version      20260311
// @description  Add a notification bubble to the sidebar for outstanding Interactions assigned to you.
// @author       Nate Kean
// @match        https://jamesriver.fellowshiponego.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=fellowshiponego.com
// @grant        none
// @license      MIT
// @run-at       document-end
// ==/UserScript==

(async function() {
    document.head.insertAdjacentHTML("beforeend", `
        <style id="nates-my-interactions-notification-bubble-css">
            .notification-count-holder {
                pointer-events: none;
            }

            .side-nav-sub-itm-holder[data-linktype="interactions"] {
                position: relative;
            }

            #nates-my-interactions-notification-bubble {
                position: absolute;
                top: 9px;
                left: 12px;
            }
        </style>
    `);

    async function getMOICount() {
        if (window.location.pathname === "/interactions") {
            // Don't request the My Interactions page if you're already there
            return parseInt(document.querySelector(".group-grand-total").textContent);
        }

        const response = await fetch("https://jamesriver.fellowshiponego.com/interactions");
        if (response.status !== 200) {
            return 0;
        }
        const htmlText = await response.text();
        const indexStart = htmlText.indexOf('group-grand-total">') + 'group-grand-total">'.length;
        if (indexStart === -1) {
            console.error("Failed to parse My Interactions page: couldn't find start of count.");
            return 0;
        }
        const indexEnd = htmlText.indexOf("</span>", indexStart);
        if (indexEnd === -1 || indexEnd - indexStart > 3) {
            // Assuming, hopefully, that it looking like there are more than 999 outstanding
            // interactions assigned to you would always be an error
            console.error("Failed to parse My Interactions page: couldn't find end of count.");
            return 0;
        }
        const count = parseInt(htmlText.substring(indexStart, indexEnd));
        window.localStorage.setItem("ndkMyOutstandingInteractionsCount", count);
        return count;
    }

    const cachedCount = window.localStorage.getItem("ndkMyOutstandingInteractionsCount") ?? 0;

    const bubble = document.createElement("div");
    bubble.id = "nates-my-interactions-notification-bubble";
    bubble.classList.add("notification-count-holder", "invisible");
    const countEl = document.createElement("div");
    countEl.classList.add("notification-count");
    countEl.textContent = cachedCount;
    bubble.appendChild(countEl);

    if (cachedCount > 0) {
        bubble.classList.remove("invisible");
    }
    const category = document.querySelector("#main-side-item-interactions");
    if (category === null) return;
    category.prepend(bubble);

    const chevron = category.querySelector(".rightCheveronIcon");

    const dropdown = document.querySelector("#open-side-menu-interactions");
    dropdown.setAttribute("style", `
        position: relative;
    `);
    const menuItemBubblePlaceholder = document.createElement("div");
    menuItemBubblePlaceholder.id = "nates-my-interactions-notification-bubble-animation-placeholder";
    menuItemBubblePlaceholder.setAttribute("style", `
        position: absolute;
        top: 7px;
        left: -8px;
    `);
    dropdown.prepend(menuItemBubblePlaceholder);

    let firstTime = true;
    let firstTimePlusStartedOpen = category.getAttribute("data-lowersecshowing") === "true";
    function updatePosition(evt) {
        if (dropdown.classList.contains("collapsing")) return;
        const isDropdownVisible = category.getAttribute("data-lowersecshowing") === "true";
        const isSelfClicked = evt === undefined || chevron === evt.target;
        const target = ((isDropdownVisible || firstTime || !isSelfClicked) && !firstTimePlusStartedOpen)
            ? category
            : menuItemBubblePlaceholder;
        target.prepend(bubble);
        firstTimePlusStartedOpen = false;
        firstTime = false;
    }

    updatePosition();
    for (const otherChevron of document.querySelectorAll(".side-nav-holder .rightCheveronIcon")) {
        otherChevron.addEventListener("click", updatePosition, { passive: true });
    }

    const newCount = await getMOICount();
    countEl.textContent = newCount;
    if (newCount > 0) {
        bubble.classList.remove("invisible");
    } else {
        bubble.classList.add("invisible");
    }
})();