Last active
November 1, 2018 13:50
-
-
Save unarist/91da256246fb438811f2e1a05785abad to your computer and use it in GitHub Desktop.
Mastodon - 期限付きミュート
This file contains 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 - 期限付きミュート | |
// @namespace https://github.com/unarist/ | |
// @version 0.11 | |
// @author unarist | |
// @match https://*/web/* | |
// @grant none | |
// @downloadURL https://gist.github.com/unarist/91da256246fb438811f2e1a05785abad/raw/mastodon-timed-mute.user.js | |
// ==/UserScript== | |
/* | |
標準のミュート機能で期限を指定できるようにするUserScriptです。 | |
具体的には「いつ誰を解除するか」をlocalStorageに記憶しておき、 | |
その時間を過ぎた適当なタイミングにミュート解除を行います。 | |
ミュート期限はミュートしたブラウザに記憶されるので、 | |
・期限を過ぎていてもそのブラウザでWebUIを開かなければ解除されません | |
・「閲覧履歴の削除」「プライベートモード」などでは期限情報が消え、自動解除が行われない場合があります | |
自動解除された後はリロードしなくても当該トゥートが表示されるようになりますが、 | |
既に読み込まれていたTLの分はそのままです。(つまり新しく読み込んだ分だけ) | |
ページ表示時にミュート解除された場合も、初回読み込み分では非表示のままです。 | |
v0.11: "無期限"が即時解除になっていたバグを修正 (thanks: @mitarashi_dango) | |
v0.10: consoleに処理結果を流すように、エラーメッセージに(空の)statusTextではなくstatusを使うように | |
v0.9: Mastodon1.5対応、モバイルレイアウトで崩れないように「期限:」のラベルを一旦削除 | |
*/ | |
(function() { | |
'use strict'; | |
const log = s => console.log('mastodon-timed-mute.user.js: ' + s); | |
const options = [ | |
/* [label, minutes] */ | |
['5分', 5], | |
['30分', 30], | |
['1時間', 60], | |
['2時間', 120], | |
['6時間', 360], | |
['12時間', 720], | |
['無期限', 0] | |
].map(([l, m]) => [l, m * 60 * 1000]); | |
if (!document.querySelector('.app-holder[data-react-class="Mastodon"], #mastodon')) return; | |
const token = JSON.parse(document.getElementById('initial-state').textContent).meta.access_token; | |
const root = document.querySelector('.modal-root'), | |
tag = (e,p) => Object.assign(document.createElement(e), p || {}), | |
load = () => JSON.parse(localStorage.getItem('timed-mute')) || {}, | |
save = obj => localStorage.setItem('timed-mute', JSON.stringify(obj)); | |
function respAsJson(resp) { | |
if (!resp.ok) throw new Error(new URL(resp.url).pathname + ' ' + resp.status); | |
return resp.json(); | |
} | |
/* core */ | |
let timerId = null; | |
function migrate() { | |
let state = load(); | |
const v = state.v; | |
if(v === undefined) { | |
state = {v:2, default: 0, items: state }; | |
} | |
save(state); | |
} | |
function reload(timeout = false) { | |
if (timeout && timerId !== null) clearTimeout(timerId); | |
const t = Date.now(); | |
const state = load(); | |
const items = state.items; | |
for (const uid in items) { | |
if(items[uid].expires < t) { | |
unmute(items[uid].acct, uid); | |
delete items[uid]; | |
} | |
} | |
save(state); | |
timerId = setTimeout(reload, 5 * 60, true); | |
return state; | |
} | |
function unmute(acct, uid) { | |
fetch(location.origin + '/api/v1/accounts/' + uid + '/unmute', { | |
method: 'post', | |
headers: { | |
'Authorization': 'Bearer ' + token | |
} | |
}) | |
.then(respAsJson) | |
.then(() => log(`unmuted ${uid} (${acct})`)) | |
.catch(msg => { | |
log(`failed to unmute - acct: ${acct}, uid: ${uid}, msg: ${msg}`); | |
alert(acct + 'の自動ミュート解除に失敗しました。\n' + msg); | |
}); | |
} | |
function addEntry(acct, expires) { | |
fetch(location.origin + '/api/v1/accounts/search?q=' + acct, { | |
headers: { | |
'Authorization': 'Bearer ' + token | |
} | |
}) | |
.then(respAsJson) | |
.then(json => { | |
if (json.length === 0) throw new Error('対象のアカウントが見つかりませんでした'); | |
const uid = json[0].id; | |
const state = reload(); | |
state.items[uid] = {acct, expires}; | |
save(state); | |
log(`registered ${uid} (${acct}) to unmute at ${expires}`); | |
}) | |
.catch(msg => { | |
log(`failed to register - acct: ${acct}, expires: ${expires}, msg: ${msg}`); | |
alert('期限付きミュートの設定に失敗しました。\n' + msg); | |
}); | |
} | |
/* ミュート設定 */ | |
new MutationObserver(() => { | |
const button = [...root.querySelectorAll('.button')].find(e => e.textContent.match(/ミュート|Mute/)); | |
if (!button || button.classList.contains('---timed-mute')) return; | |
button.classList.add('---timed-mute'); | |
const acct = root.querySelector('strong').textContent.slice(1); | |
const container = button.parentElement.insertBefore(tag('div', {/*textContent: '期限: '*/}), button); | |
const select = container.appendChild(tag('select')); | |
const defaultDuration = load().default; | |
for (const [label, duration] of options) | |
select.appendChild(tag('option', {value: duration, textContent: label, selected: duration === defaultDuration})); | |
button.addEventListener('click', () => { | |
const selectedValue = Number(select.options[select.selectedIndex].value); | |
save(Object.assign(load(), { default: selectedValue })); | |
if (selectedValue > 0) { | |
const expires = Date.now() + selectedValue; | |
addEntry(acct, expires); | |
} | |
}); | |
}).observe(root, { childList: true, subtree: true }); | |
migrate(); | |
reload(); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment