// ==UserScript==
// @name Plurk shadow block
// @name:zh-TW 噗浪隱形黑名單
// @description Shadow blocks user (only blocks on responses and timeline of yourself)
// @description:zh-TW 隱形封鎖使用者(只是會在回應和在河道上看不到被封鎖者的發文、轉噗,其他正常)
// @version 0.4.0
// @license MIT
// @namespace https://github.com/stdai1016
// @match https://www.plurk.com/*
// @exclude https://www.plurk.com/_*
// @require https://code.jquery.com/jquery-3.5.1.min.js
// @require https://greasyfork.org/scripts/432792-plurk-lib/code/plurk_lib.js?version=972862
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// ==/UserScript==
/* jshint esversion: 6 */
/* global $, plurklib */
(function () {
'use strict';
const LANG = {
DEFAULT: {
resp_btn_hide: 'Hide blocked responses',
resp_btn_show: 'Show blocked responses',
set_alert: 'Incorrect format of nick name!',
set_append: 'Append',
set_empty: 'There is no one in your blocklist.',
set_note:
'A blocked user will not be shown on responses and your timeline,' +
' but is still able to see your profile, follow you,' +
' respond to your plurks or befriend you.',
set_remove: 'Remove',
set_replurk: 'Block replurks',
set_response: 'Block responses',
set_tab: 'Shadow Block'
},
'zh-hant': {
resp_btn_hide: '隱藏被封鎖的回應',
resp_btn_show: '顯示被封鎖的回應',
set_alert: '帳號格式不正確',
set_append: '新增',
set_empty: '沒有任何人在黑名單中',
set_note: '在回應區和自己的河道上看不到被封鎖者的發文、轉噗;' +
'但對方仍可瀏覽您的個人檔案,關注、回應您的訊息,或加您為朋友。',
set_remove: '移除',
set_replurk: '封鎖轉噗',
set_response: '封鎖回應',
set_tab: '隱形黑名單'
}
};
let lang = LANG.DEFAULT;
const curLang = document.documentElement.getAttribute('lang') || '';
if (curLang.toLowerCase() in LANG) lang = LANG[curLang.toLowerCase()];
if (typeof plurklib === 'undefined') {
console.error('plurklib load failed!');
return;
}
const pageUserId = plurklib.getPageUserData()?.id;
const currUserId = plurklib.getUserData()?.id;
/* ======= storage ======= */
/** Struct in GM storage:
* {
* `u${currUserId}`: {
* `b${blockedUserId}`: {
* id: blockedUserId,
* nick_name: <string>, // for readability
* replurk: <bool>, // block his replurks
* response: <bool>, // block his responses
* date: <UTC_datetime_string>
* }
* }
* }
*/
function valueGetSet (val = null) {
if (val != null) GM_setValue(`u${currUserId}`, val);
return GM_getValue(`u${currUserId}`);
}
let _blockedUsers = valueGetSet();
if (typeof _blockedUsers !== 'object') _blockedUsers = valueGetSet({});
const blockedList = {
get: id => _blockedUsers[`b${id}`],
add: user => {
_blockedUsers[`b${user.id}`] = {
id: user.id,
nick_name: user.nick_name,
replurk: user.replurk ?? true,
response: user.response ?? true,
date: user.date ?? (new Date()).toUTCString()
};
valueGetSet(_blockedUsers);
},
remove: id => {
delete _blockedUsers[`b${id}`];
valueGetSet(_blockedUsers);
},
contains: user => {
switch (typeof user) {
case 'string':
for (const u in _blockedUsers) {
if (_blockedUsers[u].nick_name === user) return true;
}
break;
case 'number':
return !!_blockedUsers[`b${user}`];
case 'object':
return !!_blockedUsers[`b${user?.id}`];
}
return false;
},
forEach: callbackfn => {
for (const i in _blockedUsers) {
callbackfn(_blockedUsers[i], i, _blockedUsers);
}
},
get length () { return Object.keys(_blockedUsers).length; }
};
/* ============== */
GM_addStyle(
'.hide {display:none}' +
'.item_holder .user_item.user_shadow_blocked_users_item .user_info {' +
' width: calc(100% - 190px);}' +
'.friend_man.not_block {background-color:#999;}' +
'.friend_man.not_block:hover {background-color:#207298}' +
'.resp-hidden-show {background:#f5f5f9;color:#afb8cc;' +
' font-weight:normal;vertical-align:top;transform:scale(0.9);opacity:0;}' +
'.resp-hidden-show.show {opacity:1}' +
'.resp-hidden-show:not(.show) .onshow {display:none}' +
'.resp-hidden-show.show .onhide {display:none}' +
'.response-status:hover .resp-hidden-show {opacity:1}' +
'.resp-hidden-show:hover {background:#afb8cc;color:#fff}'
);
if (window.location.pathname === '/Friends/') {
$('<li><a void="">' + lang.set_tab + '</a></li>').on('click', function () {
window.history.pushState('', document.title, '/Friends/');
$('#pop-window-tabs>ul>li').removeClass('current');
this.classList.add('current');
const $content = $('#pop-window-inner-content .content_inner').empty();
$content.append(
'<div class="note">' + lang.set_note + '</div>',
'<div class="dashboard">' +
' <div class="search_box"><input>' +
' <button>' + lang.set_append + '</button></div>' +
' <div class="empty">' + lang.set_empty + '</div>' +
'</div>');
const $holder = $('<div class="item_holder"></div>').appendTo($content);
if (blockedList.length) {
$content.find('.dashboard .empty').addClass('hide');
}
blockedList.forEach(u => {
plurklib.fetchUserInfo(u.id).then(info => {
makeBlockedUserItem(info, $holder);
}).catch(e => {
console.info(`Cannot get info of "${u.nick_name}" (${e.message})`);
makeBlockedUserItem(u, $holder);
});
});
$content.find('.search_box>button').on('click', function () {
const m = this.parentElement.children[0].value.match(/^[A-Za-z]\w+$/);
if (m) {
this.parentElement.children[0].value = '';
$content.find('.dashboard .empty').addClass('hide');
plurklib.fetchUserInfo(m[0]).then(info => {
blockedList.add(info);
makeBlockedUserItem(info, $holder);
}).catch(e => {
window.alert(`Unknown user "${m[0]}"`);
});
} else { window.alert(lang.set_alert); }
});
}).appendTo('#pop-window-tabs>ul');
} else if (pageUserId === currUserId ||
window.location.pathname.match(/^\/p\/[0-9a-z]+$/)) {
makeButton($('#plurk_responses>.response_box'));
makeButton($('#form_holder>.response_box'));
makeButton($('#cbox_response>.response_box'));
const po = new plurklib.PlurkObserver(prs => prs.forEach(pr => {
pr.plurks.forEach(plurk => {
if (blockedList.contains(plurk.owner_id)) {
if (plurk.isResponse) {
plurk.target.classList.add('shadow-block');
const btn =
pr.target.parentElement.querySelector('.resp-hidden-show');
btn?.classList.remove('hide');
if (blockedList.get(plurk.owner_id).response) {
plurk.target.classList.add('hide');
console.debug(`block #m${plurk.id}`);
} else { btn?.classList.add('show'); }
} else {
plurk.target.classList.add('shadow-block', 'hide');
console.debug(`block #p${plurk.id}`);
}
} else if (blockedList.get(plurk.replurker_id)?.replurk) {
plurk.target.classList.add('shadow-block', 'hide');
console.debug(`block #p${plurk.id}`);
}
});
}));
po.observe({ plurk: true });
}
function makeButton ($responseBox) {
if (!$responseBox.length) return;
const $formBtn = $(
'<div><span class="onshow">' + lang.resp_btn_hide + '</span>' +
'<span class="onhide">' + lang.resp_btn_show + '</span></div>'
);
$formBtn.on('click', function () {
$formBtn.toggleClass('show');
const $blocks = $responseBox.children('.list').children('.shadow-block');
if ($formBtn.hasClass('show')) $blocks.removeClass('hide');
else $blocks.addClass('hide');
}).addClass(['resp-hidden-show', 'button', 'small-button', 'hide'])
.insertAfter($responseBox.find('.response-only-owner'));
(new MutationObserver(mrs => mrs.forEach(mr => {
if (!mr.target.querySelector('.handle-remove')) {
$('<div class="handle-remove hide"></div>').prependTo(mr.target);
}
mr.removedNodes.forEach(node => {
if (node.classList.contains('handle-remove')) {
$formBtn.removeClass('show').addClass('hide').text(lang.resp_btn_show);
}
});
}))).observe($responseBox.find('.list').first()[0], { childList: true });
}
function makeBlockedUserItem (info, holder) {
const user = blockedList.get(info.id);
if (info.nick_name && info.nick_name !== user.nick_name) {
user.nick_name = info.nick_name;
blockedList.add(user);
}
blockedList.add(user);
const $u = $('<div class="user_item user_shadow_blocked_users_item"></div>');
const img = info.has_profile_image
? `https://avatars.plurk.com/${info.id}-medium${info.avatar ?? ''}.gif`
: 'https://www.plurk.com/static/default_medium.jpg';
$u.append([
'<a class="user_avatar" target="_blank">',
` <img class="profile_pic" src="${img}"></img>`,
'</a>',
'<div class="user_info">',
' <a class="user_link" target="_blank"',
` style="color:#000">${info.display_name}</a>`,
` <span class="nick_name">@${info.nick_name}</span>`,
' <div class="more_info"><br></div>',
'</div>',
'<div class="user_action">',
` <a void="" data-switch="replurk" title="${lang.set_replurk}"`,
' class="friend_man icon_only pif-replurk',
` ${user.replurk ? 'has_block' : 'not_block'}"></a>`,
` <a void="" data-switch="response" title="${lang.set_response}"`,
' class="friend_man icon_only pif-message',
` ${user.response ? 'has_block' : 'not_block'}"></a>`,
` <a void="" data-remove="1" title="${lang.set_remove}"`,
' class="friend_man icon_only pif-user-blocked has_block"></a>',
'</div>'
].join(''));
$u.find('a:not(.icon_only)').attr('href', '/' + info.nick_name);
$u.find('a.icon_only').on('click', function () {
if (this.dataset.switch) {
user[this.dataset.switch] = !user[this.dataset.switch];
if (user[this.dataset.switch]) {
this.classList.add('has_block');
this.classList.remove('not_block');
} else {
this.classList.remove('has_block');
this.classList.add('not_block');
}
blockedList.add(user);
}
if (this.dataset.remove) {
blockedList.remove(user.id);
$u.remove();
}
});
$u.appendTo(holder);
}
})();