// ==UserScript== // @name Mastodon Notes // @namespace FiXato's Mastodon Notes Extension // @version 0.5.1 // @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 // @downloadURL none // ==/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; } if (get_notes_auto_status()) { 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' && mutation.addedNodes && mutation.addedNodes.length > 0) { //console.log("Mastodon Notes:", mutation); if (get_notes_auto_status()) { if (mutation.target && mutation.target.getAttribute('role') == 'feed' && mutation.target.classList.contains('item-list')) { //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 notes_control_panel = htmlToElement(`

Mastodon Notes:


`); const head=document.querySelector('head'); var styles = `` head.appendChild(htmlToElement(styles)); add_control_panel(); function get_notes_status() { let notes_status = localStorage.getItem('notes_enabled'); if (notes_status === null) {notes_status = "true";} // work around string type of localStorage. notes_status = (notes_status == "true" ? true : false); return notes_status; } function get_notes_auto_status() { let notes_auto_status = localStorage.getItem('notes_auto_apply'); if (notes_auto_status === null) {notes_auto_status = "true";} // work around string type of localStorage. notes_auto_status = (notes_auto_status == "true" ? true : false); return notes_auto_status; } function toggle_notes() { let notes_status = !get_notes_status(); localStorage.setItem('notes_enabled', notes_status); let notes_status_toggle = document.querySelector('#notes_status_toggle'); if (notes_status_toggle === null) { console.error('Mastodon Notes: could not find notes status toggle'); return false; } notes_status_toggle.checked = notes_status; if (notes_status) {restore_users_notes();} } function toggle_auto_apply() { let notes_auto_status = !get_notes_auto_status(); localStorage.setItem('notes_auto_apply', notes_auto_status); let notes_status_toggle = document.querySelector('#notes_auto_toggle'); if (notes_status_toggle === null) { console.error('Mastodon Notes: could not find notes auto-apply status toggle'); return false; } notes_status_toggle.checked = notes_auto_status; if (notes_auto_status) {restore_users_notes();} } function add_control_panel() { let beforeElement = document.querySelector('.drawer .search, .column-1 .public-account-bio'); if (beforeElement === null) { console.log('Mastodon Notes: Drawer with search not yet present. Will retry'); setTimeout(() => { add_control_panel(); }, 500); return false; } let notes_control_panel_el = document.querySelector('body #notes_control_panel'); if(notes_control_panel_el === null) { console.log('Mastodon Notes: Adding control panel'); beforeElement.parentElement.insertBefore(notes_control_panel, beforeElement); let notes_status = get_notes_status(); let notes_status_toggle = document.querySelector('#notes_status_toggle'); if (notes_status_toggle !== null) { notes_status_toggle.checked = notes_status; notes_status_toggle.addEventListener('click', toggle_notes); } let notes_auto_status = get_notes_auto_status(); let notes_auto_toggle = document.querySelector('#notes_auto_toggle'); if (notes_auto_toggle !== null) { notes_auto_toggle.checked = notes_auto_status; notes_auto_toggle.addEventListener('click', toggle_auto_apply); } let reapply_button = document.querySelector('#reapply_notes'); if (reapply_button !== null) { reapply_button.addEventListener('click', restore_users_notes); } } } function open_notes(event) { console.log('opening notes interface'); let notes_template = '

' 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(''); 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 (!get_notes_status()) { console.warn('Mastodon Notes: Cannot restore users notes as Mastodon Notes is currently disabled. Please toggle it on first.'); return false; } if (running) { return false; } 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: restoring 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; } })();