Mastodon - Collapse Media in Notifications By Default

Adds a collapsible toggle to posts in your notifications for media

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Mastodon - Collapse Media in Notifications By Default
// @namespace    http://tampermonkey.net/
// @version      1.1.1
// @description  Adds a collapsible toggle to posts in your notifications for media
// @author       LupoMikti
// @license      MIT
// @match        https://mastodon.social/*
// @match        https://mstdn.jp/*
// @match        https://mastodon.art/*
// @match        https://pawoo.net/*
// @match        https://baraag.net/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=mastodon.social
// @grant        none
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    let css = `.media-toggle {
    margin-top: 10px;
    margin-inline-start: 48px;
    width: calc(100% - 48px);
    & span {
        cursor: pointer;
    }
}

.notification-ungrouped:not(.notification-ungrouped--unread) .media-toggle span {
    color: #606984;
    filter: brightness(1.5);
}

article:not([data-toggle-open]) .notification-ungrouped .status > .media-gallery,
article:not([data-toggle-open]) .notification-ungrouped .status > .media-gallery__item,
article:not([data-toggle-open]) .notification-ungrouped .status div:has(.video-player),
article[data-toggle-open="false"] .notification-ungrouped .status > .media-gallery,
article[data-toggle-open="false"] .notification-ungrouped .status > .media-gallery__item,
article[data-toggle-open="false"] .notification-ungrouped .status div:has(.video-player) {
    display: none;
}

.notification-ungrouped .status-card.expanded .status-card__image {
    visibility: collapse;
}
`

    // This waits until we have the notifications response JSON then makes the script wait 1 second more for all DOM changes to take place
    var origOpen = XMLHttpRequest.prototype.open
    XMLHttpRequest.prototype.open = function(method, url) {
        this.addEventListener('load', function() {
            // console.log('XHR finished loading', method, this.status, url);
            if (url.includes('api/v2/notifications?') && window.location.pathname.includes(`/notifications`)) {
                return setTimeout(init, 1000)
            }
        })

        this.addEventListener('error', function() {
            console.log('XHR errored out', method, url)
        })
        origOpen.apply(this, arguments)
    }

    window.addEventListener('load', () => {
        let oldHref = document.location.href
        const bodyObserver = new MutationObserver((mutationList) => {
            if (oldHref !== document.location.href) {
                oldHref = document.location.href
                refreshScript()
            }
        })
        bodyObserver.observe(document.body, {childList: true, subtree: true})
    })

    // We cannot inject a style element without the nonce, so get it and use it
    let styleNonce = document.querySelector('meta[name=style-nonce]').content
    document.head.insertAdjacentHTML('beforeend',`<style id="notif-media-toggle-css" nonce="${styleNonce}">${css}</style>`)

    let notificationColumn

    window.addEventListener('popstate', refreshScript)

    function init() {
        notificationColumn = document.querySelector('.column[aria-label="Notifications"] .item-list')
        let initialPostsWithMedia = document.querySelectorAll(`.item-list article[style=""] .notification-ungrouped .status > .media-gallery,
            .item-list article[style=""] .notification-ungrouped .status > .media-gallery__item,
            .item-list article[style=""] .notification-ungrouped .status div:has(.video-player),
            .item-list article:not(article[style]) .notification-ungrouped .status > .media-gallery,
            .item-list article:not(article[style]) .notification-ungrouped .status > .media-gallery__item,
            .item-list article:not(article[style]) .notification-ungrouped .status div:has(.video-player)`)

        initialPostsWithMedia.forEach((mediaSection) => {insertToggle(mediaSection)})

        startObserving()
    }

    function refreshScript() {
        if (!document.location.pathname.includes(`/notifications`)) return
        return setTimeout(init, 500)
    }

    function getAncestorWithDataId(node) {
        while (node && Array.from(node.attributes).every(attr => attr.name !== 'data-id')) {
            node = node.parentNode
        }
        return node
    }

    // Inserts the toggle span into a post
    function insertToggle(mediaSection, parentElementId = null, wasKeptOpen = 'false') {
        if (!mediaSection) return
        if (mediaSection.parentNode.querySelector('.media-toggle')) return
        const showingMedia = (wasKeptOpen === 'true')
        if (!parentElementId) {
            parentElementId = getAncestorWithDataId(mediaSection)?.getAttribute('data-id')
        }
        if (!parentElementId) {
            console.error("Failed to retrieve the element with the corresponding data-id")
            return
        }
        const targetSelector = mediaSection.className ? mediaSection.className.split(' ').map(name => `.${name}`).join('') : 'div:has(>.video-player)'
        mediaSection.insertAdjacentHTML('beforebegin',
            `<div class="media-toggle" data-toggle-target="[data-id='${parentElementId}'] ${targetSelector}"><span>Click to ${showingMedia ? 'hide' : 'show'} media</span></div>`)
        mediaSection.parentNode.querySelector('.media-toggle').addEventListener('click', doToggle)
    }

    // Click handler for the toggle span
    function doToggle(event) {
        if(event.target.nodeName !== 'SPAN') return
        let toggle = event.currentTarget
        let toggleTargetSelector = toggle?.getAttribute('data-toggle-target')
        let parentElementSelector = toggleTargetSelector?.split(' ')[0]
        let toggleTarget = document.querySelector(toggleTargetSelector)
        let parentElement = document.querySelector(parentElementSelector)
        if (!toggleTarget.checkVisibility()) {
            parentElement.setAttribute('data-toggle-open', 'true')
            toggleTarget.style = toggleTarget.style.cssText + "display: grid;"
            toggle.childNodes[0].innerText = "Click to hide media"
        }
        else {
            parentElement.setAttribute('data-toggle-open', 'false')
            toggleTarget.style = toggleTarget.style.cssText.replace("display: grid;", "")
            toggle.childNodes[0].innerText = "Click to show media"
        }
    }

    // Mutation observer needed because page dynamicaly hides + removes/adds post content as you scroll
    function startObserving() {
        const observeColumn = (mutationList) => {
            for (const mutation of mutationList) {
                if (mutation.type === 'attributes') {
                    const article = mutation.target
                    if (article.nodeName !== 'ARTICLE') continue // if the target is not an article element skip
                    if (!mutation.oldValue) continue // if the target's old attribute value is empty, skip
                    if (article.style.cssText) continue // if the target article's style attribute is NOT being changed to empty, skip
                    insertToggle(article.querySelector('.notification-ungrouped .status > .media-gallery, .notification-ungrouped .status > .media-gallery__item, .notification-ungrouped .status div:has(.video-player)'),
                                 article.getAttribute('data-id'),
                                 article.getAttribute('data-toggle-open') || 'false')
                }

                if (mutation.type === 'childList') {
                    const parent = mutation.target
                    let movedNodes = []
                    if (mutation.addedNodes.length > 0) {
                        movedNodes = Array.from(mutation.addedNodes)
                        if (!(parent.matches('div.status') && parent.hasAttribute('data-id') && movedNodes.some(node => node.matches('.media-gallery, .media-gallery__item, div:has(.video-player)')))) continue // if the target is not the parent of the media container we want, skip
                        if (parent.hasAttribute('data-toggle-open') && parent.getAttribute('data-toggle-open') === 'true') {
                            let mediaNode = movedNodes.find(node => node.matches('.media-gallery, .media-gallery__item, div:has(.video-player)'))
                            mediaNode.style = mediaNode.style.cssText + " display: grid;"
                        }
                        insertToggle(parent.querySelector('.media-gallery, .media-gallery__item, div:has(.video-player)'),
                                 parent.getAttribute('data-id'),
                                 parent.getAttribute('data-toggle-open') || 'false')
                    }

                    if (mutation.removedNodes.length > 0) {
                        movedNodes = Array.from(mutation.removedNodes)
                        if (!(parent.matches('div.status') && parent.hasAttribute('data-id') && movedNodes.some(node => node.matches('.media-gallery, .media-gallery__item, div:has(.video-player)')))) continue // if the target is not the parent of the media container we want, skip
                        parent.querySelector('.media-toggle').replaceWith()
                    }
                }
            }
        }

        const observer = new MutationObserver(observeColumn)
        observer.observe(notificationColumn, {
            subtree: true,
            attributeFilter: ["style"],
            attributeOldValue: true
        })

        const observer2 = new MutationObserver(observeColumn)
        observer2.observe(notificationColumn, { subtree: true, childList: true })
    }
})();