Skip to content

Instantly share code, notes, and snippets.

@unarist
Last active November 13, 2022 09:19
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save unarist/a5998d24c5bcb207376ab22c05f24e5c to your computer and use it in GitHub Desktop.
Save unarist/a5998d24c5bcb207376ab22c05f24e5c to your computer and use it in GitHub Desktop.
Mastodon - Open status/account on dropping URLs
// ==UserScript==
// @name Mastodon - Open status/account on droping URLs
// @description Open status/account in WebUI when you drop those URLs
// @namespace https://github.com/unarist/
// @downloadURL https://gist.github.com/unarist/a5998d24c5bcb207376ab22c05f24e5c/raw/mastodon-allow-drop-urls.user.js
// @version 0.6.0
// @author unarist
// @match https://*/web/*
// @match https://mstdn.maud.io/*
// @license MIT License https://opensource.org/licenses/MIT
// @grant none
// @run-at document-idle
// ==/UserScript==
/*
Note:
This script will be executed only when you visit "https://example.com/web/...", not "https://example.com/.
If you don't like this behavior, please add include/match rule for your instances (by forking script, Tampermonkey feature, etc.)
Changelog:
v0.6.0: Mastodon4.0.0rc3とかに対応してみた
v0.5.0: 色々直した
*/
(async function() {
'use strict';
const app_root = document.querySelector('#mastodon');
if (!app_root) return;
const wait = t => new Promise(r => setTimeout(r, t));
const waitForSelector = async (q, t, max_t) => {
const until = Date.now() + max_t;
do {
const v = document.querySelector(q);
if (v) return v;
await wait(t);
} while (Date.now() < until);
throw new Error(`waiting for query ${q} has been timed out in ${max_t}ms`);
};
// Reactのマウントを待つ
await waitForSelector(".ui", 200, 2000);
const tag = (name, props = {}, children = []) => {
const e = Object.assign(document.createElement(name), props);
if (typeof props.style === "object") Object.assign(e.style, props.style);
(children.forEach ? children : [children]).forEach(c => e.appendChild(c));
return e;
};
const defaultOptions = {
headers: {
'Authorization': 'Bearer ' + JSON.parse(document.getElementById('initial-state').textContent).meta.access_token
}
};
const api = (path, options) =>
fetch(location.origin + path, Object.assign({}, defaultOptions, options)) // should be deepMerge
.then(resp => { if (!resp.ok) throw new Error(new URL(resp.url).pathname + ' ' + resp.status); return resp.json(); });
const getHistory = () => {
try {
const v16_root_node_prop = Object.keys(app_root).find(k => k.startsWith("__reactContainer"));
if (v16_root_node_prop) {
// >= v16: to descendant
let current_node = app_root[v16_root_node_prop];
while (current_node) {
const history = current_node.memoizedProps?.history;
if (history) return history;
current_node = current_node.child;
}
} else {
// < v16: to ancestor
const root_instance = Object.entries(app_root.firstElementChild).find(prop=>prop[0].startsWith('__reactInternalInstance'))[1];
let current_node = root_instance._currentElement._owner;
while (current_node) {
const history = current_node._instance.props.history;
if (history) return history;
current_node = current_node._currentElement._owner;
}
}
} catch (e) {
console.log('mastodon-open-link-in-webui: Failed to get History instance: ', e);
return null;
}
};
let refs = {};
refs.container = tag('div', {
style: 'position:fixed; top:0; width:100%; height:100%; background: rgba(0,0,0,0.8); display:none; z-index: 999;'
}, [
refs.content = tag('div', {
style: `
display: flex; align-items: center; justify-content: center;
border: 2px dashed #606984; border-radius: 4px;
background: #282c37; color: #d9e1e8; font-size: 18px; font-weight: 500; word-break: break-word;
width: 320px; height: 160px; max-width: 90vw; max-height: 80vh;
position: absolute; top: 0; left: 0; right: 0; bottom: 0; margin: auto; padding: 1em;`
})
]);
document.body.appendChild(refs.container);
const parseUrl = str => { try { return new URL(str); } finally {} };
let isLocal = false;
let enteredElement = new Set();
let history;
const handlers = {
dragstart: e => isLocal = true,
dragend: e => isLocal = false,
dragenter: e => {
if (!isLocal && e.dataTransfer.types.some(t => ['text/x-moz-url', 'text/uri-list'].includes(t))) {
e.preventDefault();
e.stopPropagation();
refs.content.textContent = 'Drop here status/account URL to open in WebUI';
refs.container.style.display = 'block';
enteredElement.add(e.target);
}
},
dragover: e => {
if (enteredElement.size) {
e.preventDefault();
e.stopPropagation();
e.dataTransfer.dropEffect = 'move';
}
},
dragleave: e => {
if (enteredElement.size) {
e.preventDefault();
e.stopPropagation();
enteredElement.delete(e.target);
if (!enteredElement.size) {
refs.container.style.display = 'none';
}
}
},
drop: e => {
if (!enteredElement.size) return;
e.preventDefault();
e.stopPropagation();
const url = parseUrl(e.dataTransfer.getData('text/plain'));
if (!url || url.protocol !== 'https:') {
refs.container.style.display = 'none';
return;
}
refs.content.textContent = `Fetching...\n${url.href}`;
api('/api/v2/search?' + new URLSearchParams({q: url.href, resolve: true}))
.then(json => {
if (json.statuses.length) {
getHistory().push('/statuses/' + json.statuses[0].id);
} else if (json.accounts.length) {
getHistory().push('/accounts/' + json.accounts[0].id);
} else
throw Error(`no result for ${url.href}`);
})
.catch(e => console.warn('mastodon-allow-drop-urls: ' + e))
.then(() => refs.container.style.display = 'none');
}
};
Object.entries(handlers).forEach(([event, handler]) => window.addEventListener(event, handler, true));
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment