Last active
January 24, 2025 17:26
-
-
Save unarist/a5998d24c5bcb207376ab22c05f24e5c to your computer and use it in GitHub Desktop.
Mastodon - Open status/account on dropping URLs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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.7.0 | |
// @author unarist | |
// @match https://*/web/* | |
// @match https://*/deck/* | |
// @match https://mstdn.maud.io/* | |
// @license MIT License https://opensource.org/licenses/MIT | |
// @grant none | |
// @run-at document-idle | |
// @noframes | |
// ==/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.7.0: @noframesつけて、いまさら /deck/ をmatchに足した | |
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