// ==UserScript==
// @name Twitter - Add notes to the user
// @name:zh-CN Twitter - 为用户添加备注(别名/标签)
// @name:zh-TW Twitter - 為使用者新增備註(別名/標籤)
// @namespace https://gf.qytechs.cn/zh-CN/users/193133-pana
// @homepage https://gf.qytechs.cn/zh-CN/users/193133-pana
// @icon data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0cHgiIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAwIDI0IDI0IiBhcmlhLWxhYmVsbGVkYnk9Im5ld0ljb25UaXRsZSIgc3Ryb2tlPSJyZ2JhKDI5LDE2MSwyNDIsMS4wMCkiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InNxdWFyZSIgc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIgZmlsbD0ibm9uZSIgY29sb3I9InJnYmEoMjksMTYxLDI0MiwxLjAwKSI+IDx0aXRsZSBpZD0ibmV3SWNvblRpdGxlIj5OZXc8L3RpdGxlPiA8cGF0aCBkPSJNMTkgMTRWMjJIMi45OTk5N1Y0SDEzIi8+IDxwYXRoIGQ9Ik0xNy40NjA4IDQuMDM5MjFDMTguMjQxOCAzLjI1ODE3IDE5LjUwODIgMy4yNTgxNiAyMC4yODkyIDQuMDM5MjFMMjAuOTYwOCA0LjcxMDc5QzIxLjc0MTggNS40OTE4NCAyMS43NDE4IDYuNzU4MTcgMjAuOTYwOCA3LjUzOTIxTDExLjU4NTggMTYuOTE0MkMxMS4yMTA3IDE3LjI4OTMgMTAuNzAyIDE3LjUgMTAuMTcxNiAxNy41TDcuNSAxNy41TDcuNSAxNC44Mjg0QzcuNSAxNC4yOTggNy43MTA3MSAxMy43ODkzIDguMDg1NzkgMTMuNDE0MkwxNy40NjA4IDQuMDM5MjFaIi8+IDxwYXRoIGQ9Ik0xNi4yNSA1LjI1TDE5Ljc1IDguNzUiLz4gPC9zdmc+
// @version 6.0.0
// @description Add notes (aliases/tags) for users to help identify and search
// @description:zh-CN 为用户添加备注(别名/标签)功能,以帮助识别和搜索
// @description:zh-TW 為使用者新增備註(別名/標籤)功能,以幫助識別和搜尋
// @author pana
// @license GNU General Public License v3.0 or later
// @compatible chrome
// @compatible firefox
// @match *://*twitter.com/*
// @require https://gcore.jsdelivr.net/npm/[email protected]/minified/arrive.min.js
// @require https://gcore.jsdelivr.net/gh/LightAPIs/greasy-fork-library@0c1077067cce825e0ea31a92264338aebdba6ffc/Note_Obj.js
// @noframes
// @grant GM_info
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_listValues
// @grant GM_openInTab
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_addValueChangeListener
// @grant GM_removeValueChangeListener
// ==/UserScript==
(function () {
'use strict';
const UPDATED = '2023-02-25';
const TWITTER_ICON = {
NOTE_GRAY: 'url(data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0cHgiIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAwIDI0IDI0IiBhcmlhLWxhYmVsbGVkYnk9Im5ld0ljb25UaXRsZSIgc3Ryb2tlPSJyZ2IoMTAxLCAxMTksIDEzNCkiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InNxdWFyZSIgc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIgZmlsbD0ibm9uZSIgY29sb3I9InJnYigxMDEsIDExOSwgMTM0KSI+IDx0aXRsZSBpZD0ibmV3SWNvblRpdGxlIj5OZXc8L3RpdGxlPiA8cGF0aCBkPSJNMTkgMTRWMjJIMi45OTk5N1Y0SDEzIi8+IDxwYXRoIGQ9Ik0xNy40NjA4IDQuMDM5MjFDMTguMjQxOCAzLjI1ODE3IDE5LjUwODIgMy4yNTgxNiAyMC4yODkyIDQuMDM5MjFMMjAuOTYwOCA0LjcxMDc5QzIxLjc0MTggNS40OTE4NCAyMS43NDE4IDYuNzU4MTcgMjAuOTYwOCA3LjUzOTIxTDExLjU4NTggMTYuOTE0MkMxMS4yMTA3IDE3LjI4OTMgMTAuNzAyIDE3LjUgMTAuMTcxNiAxNy41TDcuNSAxNy41TDcuNSAxNC44Mjg0QzcuNSAxNC4yOTggNy43MTA3MSAxMy43ODkzIDguMDg1NzkgMTMuNDE0MkwxNy40NjA4IDQuMDM5MjFaIi8+IDxwYXRoIGQ9Ik0xNi4yNSA1LjI1TDE5Ljc1IDguNzUiLz4gPC9zdmc+)',
NOTE_BLUE: 'url(data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0cHgiIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAwIDI0IDI0IiBhcmlhLWxhYmVsbGVkYnk9Im5ld0ljb25UaXRsZSIgc3Ryb2tlPSJyZ2JhKDI5LDE2MSwyNDIsMS4wMCkiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InNxdWFyZSIgc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIgZmlsbD0ibm9uZSIgY29sb3I9InJnYmEoMjksMTYxLDI0MiwxLjAwKSI+IDx0aXRsZSBpZD0ibmV3SWNvblRpdGxlIj5OZXc8L3RpdGxlPiA8cGF0aCBkPSJNMTkgMTRWMjJIMi45OTk5N1Y0SDEzIi8+IDxwYXRoIGQ9Ik0xNy40NjA4IDQuMDM5MjFDMTguMjQxOCAzLjI1ODE3IDE5LjUwODIgMy4yNTgxNiAyMC4yODkyIDQuMDM5MjFMMjAuOTYwOCA0LjcxMDc5QzIxLjc0MTggNS40OTE4NCAyMS43NDE4IDYuNzU4MTcgMjAuOTYwOCA3LjUzOTIxTDExLjU4NTggMTYuOTE0MkMxMS4yMTA3IDE3LjI4OTMgMTAuNzAyIDE3LjUgMTAuMTcxNiAxNy41TDcuNSAxNy41TDcuNSAxNC44Mjg0QzcuNSAxNC4yOTggNy43MTA3MSAxMy43ODkzIDguMDg1NzkgMTMuNDE0MkwxNy40NjA4IDQuMDM5MjFaIi8+IDxwYXRoIGQ9Ik0xNi4yNSA1LjI1TDE5Ljc1IDguNzUiLz4gPC9zdmc+)',
};
const TWITTER_STYLE = `
.note-obj-twitter-blue-tag {
background-color: #3c81df;
color: #fff;
display: inline-flex;
align-items: center;
padding: 2px 10px;
line-height: 100%;
border-radius: 50px;
}
.note-obj-twitter-note-btn {
background-image: ${TWITTER_ICON.NOTE_GRAY};
background-repeat: no-repeat;
background-position: center;
background-color: rgba(0, 0, 0, 0);
border-bottom-left-radius: 9999px;
border-bottom-right-radius: 9999px;
border-top-left-radius: 9999px;
border-top-right-radius: 9999px;
transition-property: background-color, box-shadow;
transition-duration: 0.2s;
}
.note-obj-twitter-note-btn:hover {
background-image: ${TWITTER_ICON.NOTE_BLUE};
background-color: rgba(29, 161, 242, .1);
}
.note-obj-twitter-panel-btn {
height: 32px;
width: 32px;
margin: 5px 0px 0px 0px;
background-size: 28px auto;
cursor: pointer !important;
border-radius: 0px;
}
.note-obj-twitter-panel-btn:hover::after {
content: "";
display: flex;
position: relative;
background-color: rgba(29, 161, 242, .1);
width: 48px;
height: 48px;
top: -8px;
left: -8px;
border-radius: 99px;
}
.note-obj-twitter-before-follow-note-btn {
height: 36px;
width: 36px;
background-image: ${TWITTER_ICON.NOTE_BLUE};
background-repeat: no-repeat;
background-size: 19px auto;
background-position: center;
margin-bottom: 12px;
margin-right: 12px;
cursor: pointer;
border: 1px solid rgba(29, 161, 242, 1);
border-bottom-left-radius: 9999px;
border-bottom-right-radius: 9999px;
border-top-left-radius: 9999px;
border-top-right-radius: 9999px;
background-color: rgba(0, 0, 0, 0);
transition-property: background-color, box-shadow;
transition-duration: 0.2s;
}
.note-obj-twitter-before-follow-note-btn:hover {
background-color: rgba(29, 161, 242, .1);
}
.note-obj-twitter-base-tool-bar-btn {
height: 18px;
width: 18px;
margin: 0px -40px 0px 0px;
background-size: 20px auto;
border-radius: 0px;
}
.note-obj-twitter-base-tool-bar-btn:hover::after {
content: "";
position: absolute;
background-color: rgba(29, 161, 242, .1);
width: 34px;
height: 34px;
top: -8px;
left: -8px;
border-radius: 99px;
}
.note-obj-twitter-comment-tool-bar-btn {
height: 24px;
width: 24px;
margin: 12px 0px 0px 0px;
background-size: 24px auto;
border-radius: 0px;
cursor: pointer;
}
.note-obj-twitter-comment-tool-bar-btn:hover::after {
content: "";
position: absolute;
background-color: rgba(29, 161, 242, .1);
width: 38px;
height: 38px;
top: -8px;
left: -8px;
border-radius: 99px;
}
.note-obj-twitter-left-box {
height: 50%;
}`;
const selector = {
root: '#react-root',
homepage: {
id: 'div[data-testid="User-Names"] a[role="link"] > div[dir] > span',
article: 'article',
toolBar: '.css-1dbjc4n.r-18u37iz.r-1wtj0ep.r-1mdbhws',
showName: 'div[data-testid="User-Names"] a[role="link"] > div > div[dir] > span',
reprintA: '.css-1dbjc4n.r-1habvwh.r-16y2uox a',
reprintName: ':scope > span:first-of-type > span',
at: 'a.css-4rbku5.css-18t94o4.css-901oao.css-16my406.r-1loqt21.r-bcqeeo.r-qvutc0',
userFrame: '.css-18t94o4.css-1dbjc4n.r-1loqt21.r-1wbh5a2.r-dnmrzs.r-1ny4l3l',
blockquote: 'div[aria-labelledby][id] > .css-1dbjc4n > div[role="link"]',
blockquoteId: ':scope div[tabindex] .css-1dbjc4n > div[dir] > span',
blockquoteShowName: 'div[data-testid="UserAvatar-Container-unknown"]',
},
userpage: {
main: '.css-1dbjc4n.r-1ifxtd0.r-ymttw5.r-ttdzmv',
id: '[data-testid="UserName"] .css-1dbjc4n.r-18u37iz.r-1wbh5a2 span',
showName: '[data-testid="UserName"] .css-901oao.r-1vr29t4.r-bcqeeo.r-qvutc0 > span',
follow: '.css-1dbjc4n.r-obd0qt.r-18u37iz.r-1w6e6rj.r-1h0z5md.r-dnmrzs',
},
comment: {
toolBar: '.css-1dbjc4n.r-1oszu61.r-1efd50x.r-5kkj8d.r-18u37iz.r-a2tzq0',
},
hover: {
panel: 'div[data-testid="HoverCard"] .r-nsbfu8',
followBtn: '.css-1dbjc4n.r-bcqeeo',
id: 'a[role="link"]',
showName: 'a[role="link"] > div > [dir] > span',
},
modal: {
cell: '[aria-labelledby="modal-header"] [data-testid="UserCell"]',
id: 'a[role="link"]',
showName: 'a[role="link"] > div > [dir] > span',
},
follow: {
cell: '[data-testid="cellInnerDiv"] [data-testid="UserCell"]',
id: 'a[role="link"]',
showName: 'a[role="link"] > div > [dir] > span',
},
rightRecommended: {
cell: '[role="complementary"] [data-testid="UserCell"]',
id: 'a[role="link"]',
showName: 'a[role="link"] > div > [dir]',
},
};
const noteObj = new Note_Obj({
id: 'myTwitterNote',
script: {
author: {
name: 'pana',
homepage: 'https://gf.qytechs.cn/zh-CN/users/193133-pana',
},
url: 'https://gf.qytechs.cn/scripts/404587',
updated: UPDATED,
library: [
{
name: 'arrive.js',
version: '2.4.1',
url: 'https://github.com/uzairfarooq/arrive',
},
],
},
style: selector.homepage.showName + ', ' + selector.modal.showName + ' { white-space: normal; }\n' + TWITTER_STYLE,
changeEvent: changeEvent,
settings: {
showToolbarButton: {
type: 'checkbox',
lang: {
en: 'Display the "Note" button in the toolbar below each tweet (if there is no such button in the user\'s hover information panel, this option can be turned on)',
zhHans: '在每条推特下方的工具栏里显示"备注"按钮 (如果在用户的悬停信息面板里没有此按钮时,可以打开此选项)',
zhHant: '在每條推特下方的工具欄裡顯示"備註"按鈕 (如果在使用者的懸停資訊面板裡沒有此按鈕時,可以開啟此選項)',
},
default: false,
event: insertToolbarButtonEvent,
},
disableInTweets: {
type: 'checkbox',
lang: {
en: 'Disable replacing @user with @note in tweets',
zhHans: '禁用将推文中的 @user 替换为 @note',
zhHant: '禁用將推文中的 @user 替換為 @note',
},
default: false,
event: disableInTweetsEvent,
},
},
});
function atFilter(text) {
return text.replace(/^@/, '');
}
function hrefComparator(href) {
return /^[^/]+$/i.test(href);
}
function toolBarNoteButton(ele, state) {
const eleId = Note_Obj.fn.getText(ele, selector.homepage.id, 'error', atFilter);
if (eleId) {
const eleName = Note_Obj.fn.getText(ele, selector.homepage.showName, 'info');
const homepageToolBar = Note_Obj.fn.query(ele, selector.homepage.toolBar, 'info');
const commentToolBar = Note_Obj.fn.query(ele, selector.comment.toolBar, 'info');
if (homepageToolBar) {
const homepageToolBarBtn = Note_Obj.fn.query(homepageToolBar, '.' + Note_Obj.btnClassName, 'none');
if (state) {
!homepageToolBarBtn &&
homepageToolBar.appendChild(noteObj.createNoteBtn(eleId, eleName, ['note-obj-twitter-note-btn', 'note-obj-twitter-base-tool-bar-btn', 'css-1dbjc4n']));
}
else {
homepageToolBarBtn && homepageToolBarBtn.remove();
}
}
if (commentToolBar) {
const commentToolBarBtn = Note_Obj.fn.query(commentToolBar, '.' + Note_Obj.btnClassName, 'none');
if (state) {
!commentToolBarBtn &&
commentToolBar.appendChild(noteObj.createNoteBtn(eleId, eleName, ['note-obj-twitter-note-btn', 'note-obj-twitter-comment-tool-bar-btn', 'css-1dbjc4n']));
}
else {
commentToolBarBtn && commentToolBarBtn.remove();
}
}
}
}
function homepageNote(ele, curId) {
const eleId = Note_Obj.fn.getText(ele, selector.homepage.id, 'error', atFilter);
if (eleId) {
if (curId) {
curId == eleId &&
noteObj.handler(eleId, ele, selector.homepage.showName, {
add: 'span',
className: ['note-obj-twitter-blue-tag'],
});
}
else {
const eleName = Note_Obj.fn.getText(ele, selector.homepage.showName, 'info');
noteObj.judgeUsers(eleId) &&
noteObj.handler(eleId, ele, selector.homepage.showName, {
add: 'span',
className: ['note-obj-twitter-blue-tag'],
}, eleName);
}
}
}
function reprintANote(ele, curId) {
const reprintA = Note_Obj.fn.queryAnchor(ele, selector.homepage.reprintA, 'info');
if (reprintA) {
const eleId = Note_Obj.fn.getIdFromUrl(reprintA.href);
if ((curId && curId == eleId) || (!curId && noteObj.judgeUsers(eleId))) {
noteObj.handler(eleId, reprintA, selector.homepage.reprintName, {
add: 'span',
className: ['note-obj-twitter-blue-tag'],
offsetWidth: 30,
});
}
}
}
function blockquoteNote(ele, curId) {
const blockquote = Note_Obj.fn.query(ele, selector.homepage.blockquote, 'info');
if (blockquote) {
const blockquoteUser = Note_Obj.fn.query(blockquote, selector.homepage.blockquoteShowName);
if (blockquoteUser) {
const bn = blockquoteUser.nextElementSibling;
if (bn) {
const eleId = Note_Obj.fn.getText(blockquote, selector.homepage.blockquoteId, 'error', atFilter);
if ((curId && curId == eleId) || (!curId && noteObj.judgeUsers(eleId))) {
noteObj.handler(eleId, bn, undefined, {
add: 'span',
className: ['note-obj-twitter-blue-tag'],
});
}
}
}
}
}
function homepageAtNote(ele, state, userId) {
for (const atUser of Note_Obj.fn.queryAllAnchor(ele, selector.homepage.at, 'info')) {
if (hrefComparator(atUser.href)) {
const atUserId = Note_Obj.fn.getIdFromUrl(atUser.href);
if ((userId && userId == atUserId) || (!userId && noteObj.judgeUsers(atUserId))) {
noteObj.handler(atUserId, atUser, undefined, {
prefix: '@',
restore: state,
});
}
}
}
}
function userpageNote(ele, userId) {
const eleId = Note_Obj.fn.getText(ele, selector.userpage.id, 'error', atFilter);
if (userId) {
userId == eleId &&
noteObj.handler(eleId, ele, selector.userpage.showName, {
add: 'span',
className: ['note-obj-twitter-blue-tag'],
});
}
else {
const eleName = Note_Obj.fn.getText(ele, selector.userpage.showName, 'info');
noteObj.judgeUsers(eleId) &&
noteObj.handler(eleId, ele, selector.userpage.showName, {
add: 'span',
className: ['note-obj-twitter-blue-tag'],
}, eleName);
}
}
function followNote(ele, userId) {
spanItemNote(ele, selector.follow.id, selector.follow.showName, userId);
}
function rightRecommendedNote(ele, userId) {
spanItemNote(ele, selector.rightRecommended.id, selector.rightRecommended.showName, userId);
}
function modalNote(ele, userId) {
spanItemNote(ele, selector.modal.id, selector.modal.showName, userId);
}
function spanItemNote(ele, idSelector, nameSelector, userId) {
const eleId = Note_Obj.fn.getUrlId(ele, idSelector);
if ((userId && userId == eleId) || (!userId && noteObj.judgeUsers(eleId))) {
noteObj.handler(eleId, ele, nameSelector, {
add: 'span',
className: ['note-obj-twitter-blue-tag'],
});
}
}
function disableInTweetsEvent(status) {
Note_Obj.fn.docQueryAll(selector.homepage.article, 'none').forEach(ele => {
homepageAtNote(ele, status);
});
}
function insertToolbarButtonEvent(status) {
Note_Obj.fn.docQueryAll(selector.homepage.article, 'none').forEach(ele => {
toolBarNoteButton(ele, status);
});
}
function changeEvent(userId) {
Note_Obj.fn.docQueryAll(selector.homepage.article, 'none').forEach(ele => {
homepageNote(ele, userId);
reprintANote(ele, userId);
blockquoteNote(ele, userId);
homepageAtNote(ele, noteObj.getOtherConfig().disableInTweets == true, userId);
});
Note_Obj.fn.docQueryAll(selector.userpage.main).forEach(ele => {
userpageNote(ele, userId);
});
Note_Obj.fn.docQueryAll(selector.follow.cell, 'info').forEach(ele => {
followNote(ele, userId);
});
Note_Obj.fn.docQueryAll(selector.rightRecommended.cell).forEach(ele => {
rightRecommendedNote(ele, userId);
});
Note_Obj.fn.docQueryAll(selector.modal.cell, 'info').forEach(ele => {
modalNote(ele, userId);
});
}
function init() {
const arriveOption = {
fireOnAttributesModification: true,
existing: true,
};
const rootDom = Note_Obj.fn.docQuery(selector.root);
if (rootDom === null) {
return;
}
rootDom.arrive(selector.homepage.article, arriveOption, ele => {
toolBarNoteButton(ele, noteObj.getOtherConfig().showToolbarButton == true);
homepageNote(ele);
reprintANote(ele);
blockquoteNote(ele);
const disableInTweets = noteObj.getOtherConfig().disableInTweets == true;
if (!disableInTweets) {
homepageAtNote(ele, disableInTweets);
}
});
rootDom.arrive(selector.userpage.main, arriveOption, ele => {
const eleId = Note_Obj.fn.getText(ele, selector.userpage.id, 'error', atFilter);
if (eleId) {
const eleName = Note_Obj.fn.getText(ele, selector.userpage.showName, 'info');
let followNoteBtn;
const userpageFollow = Note_Obj.fn.query(ele, selector.userpage.follow);
if (userpageFollow) {
followNoteBtn = noteObj.createNoteBtn(eleId, eleName, ['note-obj-twitter-before-follow-note-btn', 'css-901oao']);
userpageFollow.insertAdjacentElement('afterbegin', followNoteBtn);
}
const userIdChange = new MutationObserver(() => {
const newUserId = Note_Obj.fn.getText(ele, selector.userpage.id, 'error', atFilter);
if (newUserId) {
noteObj.handler('', ele, selector.userpage.showName, {
add: 'span',
className: ['note-obj-twitter-blue-tag'],
});
const newUserName = Note_Obj.fn.getText(ele, selector.userpage.showName, 'info');
if (followNoteBtn) {
followNoteBtn.remove();
followNoteBtn = noteObj.createNoteBtn(newUserId, newUserName, ['note-obj-twitter-before-follow-note-btn', 'css-901oao']);
userpageFollow && userpageFollow.insertAdjacentElement('afterbegin', followNoteBtn);
}
noteObj.judgeUsers(newUserId) &&
noteObj.handler(newUserId, ele, selector.userpage.showName, {
add: 'span',
className: ['note-obj-twitter-blue-tag'],
}, newUserName);
}
});
const obId = Note_Obj.fn.query(ele, selector.userpage.id);
obId &&
userIdChange.observe(obId, {
subtree: true,
characterData: true,
});
}
userpageNote(ele);
});
rootDom.arrive(selector.follow.cell, arriveOption, ele => {
followNote(ele);
});
rootDom.arrive(selector.rightRecommended.cell, arriveOption, ele => {
rightRecommendedNote(ele);
});
rootDom.arrive(selector.modal.cell, arriveOption, ele => {
modalNote(ele);
});
rootDom.arrive(selector.hover.panel, arriveOption, ele => {
const eleId = Note_Obj.fn.getUrlId(ele, selector.hover.id);
if (eleId) {
const userShowNameText = Note_Obj.fn.getText(ele, selector.hover.showName, 'info');
const hoverFollowBtn = Note_Obj.fn.query(ele, selector.hover.followBtn);
hoverFollowBtn &&
hoverFollowBtn.insertAdjacentElement('beforebegin', noteObj.createNoteBtn(eleId, userShowNameText, ['note-obj-twitter-note-btn', 'note-obj-twitter-panel-btn']));
noteObj.judgeUsers(eleId) &&
noteObj.handler(eleId, ele, selector.hover.showName, {
add: 'span',
className: ['note-obj-twitter-blue-tag'],
}, userShowNameText);
}
});
}
init();
})();