最終更新者を常に表示するTamperMonkey用UserScript
code:js
// ==UserScript==
// @name Scrapbox Who
// @version 1.0.0
// @description 各行に最終更新者を表示する
// @author gosyujin
// @grant none
// ==/UserScript==
(function () {
'use strict';
window.addEventListener('load', () => {
// scrapboxオブジェクトが存在しない環境では動かさない
if (typeof scrapbox === 'undefined') return;
const showTelomereDoms = new Map();
let userMap = new Map(); // userId -> name
let userIconMap = new Map(); // userId -> icon
async function fetchUsers() {
if (window.scrapbox.Project.name.includes('gosyujin')) return;
const projectName = scrapbox.Project.name;
const res = await fetch(https://scrapbox.io/api/projects/${projectName});
const json = await res.json();
for (const user of json.users) {
userMap.set(user.id, user.name);
userIconMap.set(user.id, https://scrapbox.io/api/pages/${projectName}/${user.name}/icon);
}
}
scrapbox.on('project:changed', async () => {
await fetchUsers();
showTelomere();
});
scrapbox.on('page:changed', async () => {
await fetchUsers();
showTelomere();
});
fetchUsers().then(() => showTelomere());
function ensureStyle() {
if (document.getElementById('show-telomere-style')) return;
const style = document.createElement('style');
style.id = 'show-telomere-style';
style.textContent = `
.show-telomere {
position: absolute;
top: 0;
right: -40px;
white-space: pre;
font-size: 10px;
color: grey;
}
@media screen and (max-width: 768px) {
.show-telomere {
right: 0px;
}
}
`;
document.head.appendChild(style);
}
function createShowTelomereDom() {
ensureStyle();
const div = document.createElement('div');
div.classList.add('show-telomere');
return div;
}
function buildLabel(line) {
const userName = userMap.get(line.userId) ?? line.userId;
const userIcon = userIconMap.get(line.userId) ?? line.userId;
return <span class=\"name\">${userName}</span><img class=\"icon\" alt=\"${userName}\" src=\"${userIcon}\" />; //
}
function showTelomere() {
if (window.scrapbox.Project.name.includes('gosyujin')) return;
if (!scrapbox.Page.lines) return;
for (const line of scrapbox.Page.lines) {
const lineEl = document.querySelector(#L${line.id});
if (!lineEl) continue;
if (!showTelomereDoms.has(line.id)) {
showTelomereDoms.set(line.id, createShowTelomereDom());
}
const dom = showTelomereDoms.get(line.id);
if (dom.parentNode !== lineEl) {
lineEl.appendChild(dom);
}
dom.innerHTML = buildLabel(line);
}
// ページから消えた行のDOMをクリーンアップ
const currentIds = new Set(scrapbox.Page.lines.map(l => l.id));
if (!currentIds.has(lineId)) {
dom.parentNode?.removeChild(dom);
showTelomereDoms.delete(lineId);
}
}
}
});
})();
やりたいこと
/forum-jpのような複数人が参加しているプロジェクトで、その行を書いたユーザーを見たい 外部プロジェクトでこそ使いたいのでUserScriptでなくTamperMonkeyで実行できるようにした
https://scrapbox.io/api/projects/${projectName}からプロジェクト情報を取得し、users[i].idとusers[i].nameを紐づける
code:response.json
"users": [
{ "id": "...", "name": "...", },
{ "id": "...", "name": "...", },
]
scrapbox.Page.linesのline.userIdと一致したユーザーを行に表示
code:response.json
{ id: '6a2905310000000000cd7a77', text: 'code:js', userId: '...', created: 1781073201, updated: 1781073233, }
この仕組み自体は、2015年くらいに公式で検証されていたが、以下の理由で却下されたみたい
行毎に編集者を自動表示すると、小人さん的な活動ができなくなる
リンクを付けてあげる、誤字脱字の修正など
勝手に表示されるとあまり楽しくない
自分で書くと楽しい
自分で適用する分には良いかという感じ
すぐ整合性は崩れる前提で、あくまで参考程度に使う感じ
小人さん的な活動をすると崩れる
ページを切り出すと切り出した人のuserIdになるのですべて崩れる
取得できる情報から組み立てるため厳密でなくて良い
というか、それ以上わからない