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.
当前为
// ==UserScript==
// @name Mastodon Notes
// @namespace FiXato's Mastodon Notes Extension
// @version 0.4
// @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/*
// @match https://octodon.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;
try {
if (typeof(Storage) === "undefined") {
console.error("Local Storage not supported");
return;
}
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();
}
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);
}
catch(err) { console.error('Mastodon Notes: error', err); }
// 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;
}
const head=document.querySelector('head');
var styles = `<style>button.notes { opacity: 0; visibility: hidden; transition: visibility 1s, opacity 1s linear; position: absolute; margin-top: -1em; margin-left: -2em;}
a:hover + button.notes, button.notes:hover { visibility: visible; opacity: 1; z-index: 998}
#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}
#notes_content {margin: auto 0; position: relative; background: rgb(30,30,30); border: 1px solid rgb(200,200,200); padding: 2em; margin: 2em;}
#notes_content form textarea, #notes_content form input {width: 100%;}
#notes_content form label, #notes_content form input {margin: 0.5em;}</style>`
head.appendChild(htmlToElement(styles));
function open_notes(event) {
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.profileUrl;
let new_element = htmlToElement(notes_template);
let body = document.querySelector('body');
body.insertBefore(new_element, body.firstElementChild);
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;
}
})();