Mastodon Notes

Adds 'notes' buttons to profile links to configured Mastodon sites, which can be used to add your own notes (to be displayed as 'title' attributes) to users' profile links.

目前為 2020-04-07 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Mastodon Notes
// @namespace    FiXato's Mastodon Notes Extension
// @version      0.2
// @description  Adds 'notes' buttons to profile links to configured Mastodon sites, which can be used to add your own notes (to be displayed as 'title' attributes) to users' profile links.
// @author       FiXato
// @match        https://hackers.town/*
// @match        https://mastodon.social/*
// @grant        none
// @run-at document-idle
// ==/UserScript==

(function() {
    'use strict';
    console.log('Mastodon Notes loaded');
    var users = {};
    var first_call;
    const max_time_between_calls = (1000 * 3); // 3 seconds delay to reduce lag from the function being called too often while new content loads.
    var restore_users_note_timer;
    var running = false;
    restore_users_notes();

    // Options for the observer (which mutations to observe)
    const config = { attributes: false, childList: true, subtree: true };

    // Callback function to execute when mutations are observed
    const callback = function(mutationsList, observer) {
        // Use traditional 'for loops' for IE 11
        for(let mutation of mutationsList) {
            if (mutation.type === 'childList') {
                //console.log('Mastodon Notes: A child node has been added or removed.');
                if (mutation.target && mutation.target.getAttribute('role') == 'feed' && mutation.target.classList.contains('item-list')) {
                    //console.log('Mastodon Notes: new item', mutation.target);
                    //FIXME: This should be called on only a subset of items, so it doesn't get repeatedly called on object that are already processed.
                    restore_users_notes();
                }
                console.log('Mastodon Notes: new item', mutation.target);
                if (mutation.target && mutation.target.classList.contains('activity-stream')) {
                    restore_users_notes();
                }
            }
        }
    };

    const targetNode = document.querySelector('body');

    // Create an observer instance linked to the callback function
    const observer = new MutationObserver(callback);

    // Start observing the target node for configured mutations
    observer.observe(targetNode, config);

    if (typeof(Storage) === "undefined") {
        console.error("Local Storage not supported");
        return;
    }

    // Function by Mark Amery from https://stackoverflow.com/a/35385518
    function htmlToElement(html) {
        let template = document.createElement('template');
        html = html.trim(); // Never return a text node of whitespace as the result
        template.innerHTML = html;
        return template.content.firstChild;
    }

    var head=document.querySelector('head');
    var styles = 'button.notes { opacity: 0; visibility: hidden; transition: visibility 1s, opacity 1s linear; position: absolute; margin-top: -1em; margin-left: -2em;}'
    styles += 'a:hover + button.notes, button.notes:hover { visibility: visible; opacity: 1; z-index: 998}'
    styles += '#notes_template {width: 100%; height: 100%; position: fixed; top: 0; left: 0; display: flex; align-items: center; justify-content: center; background: rgba(30,30,30, 0.8); color: #fff; z-index: 999}';
    styles += '#notes_content {margin: auto 0; position: relative; background: rgb(30,30,30); border: 1px solid rgb(200,200,200); padding: 2em; margin: 2em;}';
    styles += '#notes_content form textarea, #notes_content form input {width: 100%;}';
    styles += '#notes_content form label, #notes_content form input {margin: 0.5em;}';
    head.innerHTML+='<style>' + styles + '</style>';

    function open_notes() {
        console.log('opening notes interface');
        let notes_template = '<div id="notes_template"><div id="notes_content"><form><label for="notes_profile_url">Notes for:</label><input type="text" id="notes_profile_url" /><br /><label for="notes"><textarea id="notes" cols="70" rows="15"></textarea></label><input id="save_notes" type="submit" value="Save!"><input id="close_notes" type="reset" value="Reset & Close"></form></div></div>'
        let profile_url = this.dataset.profile_url;
        let body = document.querySelector('body');
        body.innerHTML += notes_template;
        document.querySelector('#notes_profile_url').value = profile_url;
        document.querySelector('textarea#notes').value = users[profile_url];
        document.querySelector('#notes_template form').addEventListener('submit', save_notes);
        document.querySelector('#notes_template form #save_notes').addEventListener('click', save_notes);
        document.querySelector('#notes_template form #close_notes').addEventListener('click', close_notes);
    }
    function close_notes(event) {
        console.log('closing notes');
        document.querySelector('#notes_template').remove();
        set_open_notes_event_handlers();
    }
    function save_notes(event) {
        event.preventDefault();
        let profile_url = document.querySelector('#notes_profile_url').value;
        let notes = document.querySelector('#notes').value;
        console.log('saving notes for ' + profile_url);
        if (store_user_notes(profile_url, notes) == true) {
            close_notes()
            restore_users_notes();
        }
    }

    function store_user_notes(profile_url, notes) {
        users[profile_url] = notes;
        localStorage.setItem('notes_for_' + profile_url, notes);
        return true;
    }

    // not sure why I have to call this multiple times, but otherwise I seem to lose the handler after editing the first note.
    function set_open_notes_event_handlers() {
        let note_buttons = document.querySelectorAll('button.notes');
        note_buttons.forEach((element) => {
            element.addEventListener('click', open_notes);
        });

    }

    function append_notes_to_title(profile_url, element) {
        if (users[profile_url] !== null) {
            if (element.title !== undefined && element.dataset.originalTitle === undefined) {
                element.setAttribute('data-original-title', element.title);
            }
            element.title = (element.dataset.originalTitle + (element.dataset.originalTitle && element.dataset.originalTitle.length > 0 ? "\n" : "") + "Notes: " + users[profile_url]);
        }
    }
    function add_buttons_to_link_elements(profile_link_elements) {
        let idx = 1;
        for(let element of profile_link_elements) {
            console.log('Mastodon Notes: element[' + idx + '/' + profile_link_elements.length + ']:', element);
            idx += 1;
            let profile_url = element.href;
            // Don't add notes buttons again if it is already present.
            if (!element.nextElementSibling || !element.nextElementSibling.classList.contains('notes')) {
                let new_element = htmlToElement('<button class="notes" data-profile_url="' + profile_url + '">Notes</button>');
                if (!element.nextElementSibling) {
                    element.parentNode.appendChild(new_element);
                } else {
                    element.parentNode.insertBefore(new_element, element.nextElementSibling);
                }
            }
            let notes = localStorage.getItem('notes_for_' + profile_url);
            if (notes !== "undefined") {
                users[profile_url] = notes;
                try {
                    append_notes_to_title(profile_url, element);
                }
                catch(err) {
                    console.error('Mastodon Notes: error', err);
                }
            }
        }
    }

    function restore_users_notes() {
        if (running) { return }
        if (first_call === undefined) { first_call = Date.now();}
        let delta_first_call = (Date.now() - first_call);

        if (restore_users_note_timer !== undefined) { clearTimeout(restore_users_note_timer); }

        if (delta_first_call < max_time_between_calls) {
            let time_remaining = (max_time_between_calls - delta_first_call);
            console.log('Mastodon Notes: restore_users_notes() delayed for ' + time_remaining + 'ms');

            restore_users_note_timer = setTimeout(() => { restore_users_notes(); }, time_remaining);
            return false;
        } else {
            first_call = Date.now();
            restore_users_note_timer = undefined;
        }
        console.log('Mastodon Notes: restore_users_notes()');
        running = true;
        let profile_link_elements = document.querySelectorAll('a.mention[href], a.status__display-name[href], a.avatar[href], a.detailed-status__display-name[href]');
        add_buttons_to_link_elements(profile_link_elements);

        set_open_notes_event_handlers();
        running = false;
    }

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址