Skip to content

Instantly share code, notes, and snippets.

@mimonelu
Last active January 23, 2026 13:55
Show Gist options
  • Select an option

  • Save mimonelu/f0e35ce16631c13179df0124baff14eb to your computer and use it in GitHub Desktop.

Select an option

Save mimonelu/f0e35ce16631c13179df0124baff14eb to your computer and use it in GitHub Desktop.
Nicosky - Blueskyのポストをニコニコ風に表示するブックマークレット
(() => {
const JETSTREAM_URL = "wss://jetstream1.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post"
const NUMBER_OF_SLOTS = 15
const TARGET_LANG = "ja"
const FONT_SIZE = 5
const SLOT_HEIGHT = 6.25
const TRANSITION_DURATIONS = [100, 2500]
const CONTAINER_CLASS_NAME = "nicosky"
const slots = Array(NUMBER_OF_SLOTS).fill(false)
let numberOfMessages = 0
let numberOfLangPosts = 0
let numberOfDisplayPosts = 0
let container = document.querySelector(`.${CONTAINER_CLASS_NAME}`)
if (container) {
if (container.__nicoskySocket) {
try {
container.__nicoskySocket.close()
} catch {}
}
container.parentNode.removeChild(container)
return
} else {
container = document.createElement("div")
container.style.cssText = `
all: initial;
contain: layout paint style;
overflow: hidden;
pointer-events: none;
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100vh;
z-index: 65535;
`
container.classList.add(CONTAINER_CLASS_NAME)
document.body.appendChild(container)
}
const infoContainer = document.createElement("div")
infoContainer.style.cssText = `
${makeTextStyles()}
font-size: ${FONT_SIZE / 2}vh;
color: #c0c0c0;
position: absolute;
right: 0.5em;
bottom: 0.5em;
`
container.appendChild(infoContainer)
const socket = new WebSocket(JETSTREAM_URL)
container.__nicoskySocket = socket
socket.addEventListener("message", event => {
numberOfMessages ++
updateInfo()
if (event.data == null) {
return
}
const record = JSON.parse(event.data)?.commit?.record
if (
!record ||
!record.langs?.length ||
!record.text
) {
return
}
const hasTargetLang = record.langs.includes(TARGET_LANG)
if (!hasTargetLang) {
return
}
numberOfLangPosts ++
const hasFacets = record.facets != null
if (hasFacets) {
return
}
const hasEmbed = record.embed != null
if (hasEmbed) {
return
}
const slotIndex = slots.findIndex(slot => slot === false)
if (slotIndex === - 1) {
return
}
updateInfo()
slots[slotIndex] = true
const textContainer = document.createElement("div")
textContainer.addEventListener("transitionend", () => {
if (container && textContainer) {
container.removeChild(textContainer)
slots[slotIndex] = false
}
})
const textNode = document.createTextNode(record.text)
textContainer.appendChild(textNode)
container.appendChild(textContainer)
const isReply = record.reply != null
const duration = record.text.length * TRANSITION_DURATIONS[0] + TRANSITION_DURATIONS[1]
textContainer.style.cssText = `
${makeTextStyles()}
color: ${isReply ? "#c0e0ff" : "#ffffff" };
font-size: ${FONT_SIZE}vh;
position: absolute;
left: 0;
top: 0;
transform: translate3d(100vw, 0, 0);
transition: transform ${duration}ms linear;
`
requestAnimationFrame(() => {
const width = textContainer.clientWidth
const y = slotIndex * SLOT_HEIGHT
textContainer.style.cssText += `
top: ${y}vh;
transform: translate3d(-${width}px, 0, 0);
`
})
if (++ numberOfDisplayPosts >= 1000) {
socket.close()
}
})
let lastInfo = 0
function updateInfo () {
const now = performance.now()
if (now - lastInfo < 125) {
return
}
lastInfo = now
infoContainer.innerText = `${numberOfDisplayPosts} shown (${numberOfLangPosts} "${TARGET_LANG}" / ${numberOfMessages} total)`
}
function makeTextStyles () {
return `
font-family: monospace;
-webkit-font-smoothing: none;
font-weight: bold;
text-shadow: -1px -1px 0 #000000, 1px -1px 0 #000000, -1px 1px 0 #000000, 1px 1px 0 #000000;
user-select: none;
white-space: nowrap;
`
}
})()
@mimonelu
Copy link
Author

mimonelu commented Jan 23, 2026

ブックマークレット登録用圧縮版

javascript:(()=>{function updateInfo(){const now=performance.now();125>now-lastInfo||(lastInfo=now,infoContainer.innerText=`${numberOfDisplayPosts} shown (${numberOfLangPosts} "${TARGET_LANG}" / ${numberOfMessages} total)`)}function makeTextStyles(){return`font-family: monospace;-webkit-font-smoothing: none;font-weight: bold;text-shadow: -1px -1px 0 #000000, 1px -1px 0 #000000, -1px  1px 0 #000000, 1px  1px 0 #000000;user-select: none;white-space: nowrap;`}const JETSTREAM_URL="wss://jetstream1.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post",NUMBER_OF_SLOTS=15,TARGET_LANG="ja",FONT_SIZE=5,SLOT_HEIGHT=6.25,TRANSITION_DURATIONS=[100,2500],CONTAINER_CLASS_NAME="nicosky",slots=Array(NUMBER_OF_SLOTS).fill(!1);let numberOfMessages=0,numberOfLangPosts=0,numberOfDisplayPosts=0,container=document.querySelector(`.${CONTAINER_CLASS_NAME}`);if(container){if(container.__nicoskySocket)try{container.__nicoskySocket.close()}catch{}return void container.parentNode.removeChild(container)}container=document.createElement("div"),container.style.cssText=`all: initial;contain: layout paint style;overflow: hidden;pointer-events: none;position: fixed;left: 0;top: 0;width: 100%;height: 100vh;z-index: 65535;`,container.classList.add(CONTAINER_CLASS_NAME),document.body.appendChild(container);const infoContainer=document.createElement("div");infoContainer.style.cssText=`${makeTextStyles()}font-size: ${FONT_SIZE/2}vh;color: #c0c0c0;position: absolute;right: 0.5em;bottom: 0.5em;`,container.appendChild(infoContainer);const socket=new WebSocket("wss://jetstream1.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post");container.__nicoskySocket=socket,socket.addEventListener("message",event=>{if(numberOfMessages++,updateInfo(),null==event.data)return;const record=JSON.parse(event.data)?.commit?.record;if(!record||!record.langs?.length||!record.text)return;const hasTargetLang=record.langs.includes(TARGET_LANG);if(!hasTargetLang)return;numberOfLangPosts++;const hasFacets=null!=record.facets;if(hasFacets)return;const hasEmbed=null!=record.embed;if(hasEmbed)return;const slotIndex=slots.findIndex(slot=>!1===slot);if(-1===slotIndex)return;updateInfo(),slots[slotIndex]=!0;const textContainer=document.createElement("div");textContainer.addEventListener("transitionend",()=>{container&&textContainer&&(container.removeChild(textContainer),slots[slotIndex]=!1)});const textNode=document.createTextNode(record.text);textContainer.appendChild(textNode),container.appendChild(textContainer);const isReply=null!=record.reply,duration=record.text.length*TRANSITION_DURATIONS[0]+TRANSITION_DURATIONS[1];textContainer.style.cssText=`${makeTextStyles()}color: ${isReply?"#c0e0ff":"#ffffff"};font-size: ${FONT_SIZE}vh;position: absolute;left: 0;top: 0;transform: translate3d(100vw, 0, 0);transition: transform ${duration}ms linear;`,requestAnimationFrame(()=>{const width=textContainer.clientWidth,y=slotIndex*SLOT_HEIGHT;textContainer.style.cssText+=`top: ${y}vh;transform: translate3d(-${width}px, 0, 0);`}),1e3<=++numberOfDisplayPosts&&socket.close()});let lastInfo=0})();

@mimonelu
Copy link
Author

mimonelu commented Jan 23, 2026

説明

Blueskyのポストを某動画サイト風に表示するブックマークレットです。

  • 「ブックマークレット登録用圧縮版」をブックマークとして登録し、任意のWebページで実行します。再実行で終了
    • Webページによってはセキュリティの都合上、実行できない場合があります
  • サーバ負荷軽減のため、1000件表示で自動終了します
  • 表示するポストの条件
    • ポスト言語に ja を含むポスト
    • facets のないポスト
    • embed のないポスト
    • かつ、表示する場所があれば表示します
  • 薄い青色のテキストはリプライです。リポストや引用RPは対象外
  • 右下に簡易統計情報を表示します

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment