🏠 Home 

Misskey Hashflags

TwitterのHashflagsをMisskeyに表示するやつ

// ==UserScript==
// @name         Misskey Hashflags
// @namespace    https://midra.me/
// @version      1.0.5
// @description  TwitterのHashflagsをMisskeyに表示するやつ
// @author       Midra
// @license      MIT
// @match        https://*/*
// @icon         https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png
// @run-at       document-body
// @grant        GM_getValue
// @grant        GM_setValue
// ==/UserScript==
// @ts-check
/**
* @typedef TwitterHashflag
* @property {string}  hashtag
* @property {string}  asset_url
* @property {number}  starting_timestamp_ms
* @property {number}  ending_timestamp_ms
* @property {boolean} is_hashfetti_enabled
*/
;(async () => {
'use strict'
const HASHFLAGS_UPDATE_INTERVAL = 2 * 60 * 60 * 1000
const isTwitter =
location.href.startsWith('https://twitter.com/') ||
location.href.startsWith('https://x.com/')
const isMisskey =
document
.querySelector('meta[name="application-name"]')
?.getAttribute('content') === 'Misskey'
if (!isTwitter && !isMisskey) return
/** @type {TwitterHashflag[]} */
const hashflags = GM_getValue('hashflags', [])
/** @type {TwitterHashflag[]} */
const activeHashflags = hashflags
.filter((v) => Date.now() < v.ending_timestamp_ms)
.map((v) => ((v.hashtag = v.hashtag.toLowerCase()), v))
const activeHashtags = activeHashflags.map((v) => v.hashtag)
/**
* @param {string} [hashtag]
* @returns {TwitterHashflag | undefined}
*/
const getHashflag = (hashtag) => {
if (!hashtag) return
const hashflag =
activeHashflags[activeHashtags.indexOf(hashtag.toLowerCase())]
if (
hashflag &&
hashflag.starting_timestamp_ms <= Date.now() &&
Date.now() < hashflag.ending_timestamp_ms
) {
return hashflag
}
}
/**
* @param {Element} target
*/
const addHashflags = (target) => {
if (activeHashflags.length === 0) return
/** @type {NodeListOf<HTMLAnchorElement>} */
const hashtags = target.querySelectorAll('a[href^="/tags/"]')
for (const tag of hashtags) {
if (tag.classList.contains('hasTwitterHashflag')) continue
const text = tag.textContent
if (!text?.startsWith('#')) continue
const hashflag = getHashflag(text.substring(1))
if (hashflag) {
const img = document.createElement('img')
img.classList.add('twitter_hashflag')
img.src = hashflag.asset_url
tag.appendChild(img)
tag.classList.add('hasTwitterHashflag')
}
}
}
// /**
//  * @param {Element} target
//  */
// const removeHashflags = (target) => {
//   for (const elm of target.getElementsByClassName('twitter_hashflag')) {
//     elm.remove()
//   }
//   for (const elm of target.getElementsByClassName('hasTwitterHashflag')) {
//     elm.classList.remove('hasTwitterHashflag')
//   }
// }
// Twitter (Hashflagsの取得・保存)
if (isTwitter) {
console.log('[Misskey Hashflags] Twitter')
const lastUpdated = GM_getValue('hashflags_lastupdated', 0)
if (HASHFLAGS_UPDATE_INTERVAL < Date.now() - lastUpdated) {
try {
const res = await fetch('https://twitter.com/i/api/1.1/hashflags.json')
/** @type {TwitterHashflag[]} */
const json = await res.json()
if (json && 0 < json.length) {
GM_setValue('hashflags', json)
GM_setValue('hashflags_lastupdated', Date.now())
console.log('[Misskey Hashflags] Hashflagsを保存しました')
}
} catch (e) {
console.error('[Misskey Hashflags]', e)
}
}
}
// Misskey
else if (isMisskey) {
console.log('[Misskey Hashflags] Misskey')
addHashflags(document.body)
/** @type {MutationObserverInit} */
const obs_options = {
childList: true,
subtree: true,
}
const obs = new MutationObserver((mutations) => {
obs.disconnect()
for (const mutation of mutations) {
if (!(mutation.target instanceof HTMLElement)) continue
if (0 < mutation.addedNodes.length) {
addHashflags(mutation.target)
}
}
obs.observe(document.body, obs_options)
})
obs.observe(document.body, obs_options)
// style
const style = document.createElement('style')
style.textContent = `
.twitter_hashflag {
display: inline-block;
height: 1.1em;
margin: 0 2px -0.15em;
}
`
document.body.appendChild(style)
}
})()