// ==UserScript==
// @name GoodTwitter 2 - Electric Boogaloo
// @version 0.0.44.1
// @description A try to make Twitter look good again.
// @author schwarzkatz
// @license MIT
// @match https://twitter.com/*
// @match https://mobile.twitter.com/*
// @exclude https://twitter.com/i/cards/*
// @exclude https://twitter.com/i/release_notes
// @exclude https://twitter.com/*/privacy
// @exclude https://twitter.com/*/tos
// @exclude https://twitter.com/account/access
// @grant GM_deleteValue
// @grant GM_getResourceText
// @grant GM_getResourceURL
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_info
// @grant GM_xmlhttpRequest
// @connect api.twitter.com
// @resource css https://github.com/Bl4Cc4t/GoodTwitter2/raw/master/twitter.gt2eb.style.css
// @resource emojiRegex https://github.com/Bl4Cc4t/GoodTwitter2/raw/master/data/emoji-regex.txt
// @resource pickrCss https://cdn.jsdelivr.net/npm/@simonwep/pickr/dist/themes/classic.min.css
// @require https://github.com/Bl4Cc4t/GoodTwitter2/raw/master/twitter.gt2eb.i18n.js
// @require https://github.com/Bl4Cc4t/GoodTwitter2/raw/master/twitter.gt2eb.polyfills.js
// @require https://code.jquery.com/jquery-3.5.1.min.js
// @require https://gist.github.com/raw/2625891/waitForKeyElements.js
// @require https://cdn.jsdelivr.net/npm/@simonwep/pickr/dist/pickr.es5.min.js
// @updateURL https://github.com/Bl4Cc4t/GoodTwitter2/raw/master/twitter.gt2eb.user.js
// @downloadURL https://github.com/Bl4Cc4t/GoodTwitter2/raw/master/twitter.gt2eb.user.js
// ==/UserScript==
(function($, waitForKeyElements) {
"use strict"
// do not execute on these pages
if (getPath().match(/^login(\?.*)?$/) || (!isLoggedIn() && getPath().match(/^(\?.*)?$/))) {
return
}
// redirect for mobile urls
if (window.location.host == "mobile.twitter.com") {
if (GM_getValue("opt_gt2").mobileRedirect) {
window.location.href = window.location.href.replace("//mobile.twitter.com", "//twitter.com")
} else return
}
// ###########################
// # convenience functions #
// ###########################
// seperate number with commas
Number.prototype.humanize = function() {
let t = this.toString().split("")
let out = ""
let c = 1
for (let i=t.length-1; i>=0; i--) {
out = `${t[i]}${out}`
if (c++ % 3 == 0 && i-1 >= 0) {
out = `,${out}`
}
}
return out
}
// shorter version: 1.4M, 23.4K, etc
Number.prototype.humanizeShort = function() {
let t = this.toString()
if (this >= 1000000) {
t = t.slice(0, -5)
return `${t.slice(0, -1)}${t.slice(-1) != 0 ? `.${t.slice(-1)}` : ""}M`
} else if (this >= 10000) {
t = t.slice(0, -2)
return `${t.slice(0, -1)}${t.slice(-1) != 0 ? `.${t.slice(-1)}` : ""}K`
} else return this.humanize()
}
// get kebab case (thisIsAString -> this-is-a-string)
String.prototype.toKebab = function() {
let arr = this.toString().split("")
return arr.map((e, i) => {
let add_dash = i > 0
&& ((!isNaN(e) && isNaN(arr[i-1]))
|| (isNaN(e) && !isNaN(arr[i-1]))
|| (isNaN(e) && e == e.toUpperCase()))
return `${add_dash ? "-" : ""}${e.toLowerCase()}`
}).join("")
}
String.prototype.replaceAt = function(index, length, text) {
return `${this.toString().slice(0, index)}${text}${this.toString().slice(index + length)}`
}
String.prototype.insertAt = function(index, text) {
return this.toString().replaceAt(index, 0, text)
}
const defaultAvatarUrl = "https://abs.twimg.com/sticky/default_profile_images/default_profile.png"
const emojiRegexp = new RegExp(`(${GM_getResourceText("emojiRegex")})`, "gu")
// get account information
let info = null
function getInfo() {
if (info)
return info
let user = null
try {
for (let e of Array.from(document.querySelectorAll("#react-root ~ script"))) {
if (e.textContent.includes("__INITIAL_STATE__")) {
let match = e.textContent.match(/__INITIAL_STATE__=(\{.*?\});window/)
if (match) {
let initialState = JSON.parse(match[1])
user = Object.values(initialState?.entities?.users?.entities)[0] ?? null
}
break
}
}
} catch (e) {
console.error(e)
}
if (user) {
info = {
bannerUrl: user.profile_banner_url,
avatarUrl: user.profile_image_url_https,
screenName: user.screen_name,
name: user.name,
id: user.id_str,
stats: {
tweets: user.statuses_count,
followers: user.followers_count,
following: user.friends_count
}
}
console.log("user info", info)
} else {
console.error("match of __INITIAL_STATE__ unsuccessful, falling back to default values")
info = {
bannerUrl: "",
avatarUrl: defaultAvatarUrl,
screenName: "youarenotloggedin",
name: "Anonymous",
id: "0",
stats: {
tweets: 0,
followers: 0,
following: 0
}
}
}
return info
}
// get current display language
function getLang() {
return $("html").attr("lang").trim()
}
// check if the user is logged in
function isLoggedIn() {
return document.cookie.match(/twid=u/)
}
// get localized version of a string.
// defaults to english version.
function getLocStr(key) {
let lang = getLang()
lang = Object.keys(i18n).includes(lang) ? lang : "en"
return i18n[Object.keys(i18n[lang]).includes(key) ? lang : "en"][key]
}
// current path
function getPath() {
return window.location.href.replace(/.*?twitter\.com\//, "")
}
// svg convenience
function getSvg(key) {
let svgs = {
lightning: ` `,
caret: ` `,
tick: ` `,
moon: ` `,
x: ` `,
google: ` `,
arrow: ` `,
location: ` `,
url: ` `,
calendar: ` `,
balloon: ` `,
}
return `
${svgs[key]}
`
}
// request headers
function getRequestHeaders(additionalHeaders) {
// found in https://abs.twimg.com/responsive-web/web/main.5c0baa34.js
let publicBearer = "AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA"
let csrf = window.document.cookie.match(/ct0=([^;]+)(;|$)/)[1]
let out = {
authorization: `Bearer ${publicBearer}`,
origin: "https://twitter.com",
referer: window.location.href,
"x-twitter-client-language": getLang(),
"x-csrf-token": csrf,
"x-twitter-active-user": "yes",
// "x-twitter-auth-type": "OAuth2Session"
}
Object.assign(out, additionalHeaders)
return out
}
function getRequestURL(base, param) {
let out = base
for (let [key, val] of Object.entries(param)) {
if (typeof val === "object") val = encodeURIComponent(JSON.stringify(val))
out += `&${key}=${val}`
}
return `${out.replace("&", "?")}`
}
// request a tweet
function requestTweet(id, cb) {
GM_xmlhttpRequest({
method: "GET",
url: getRequestURL("https://twitter.com/i/api/1.1/statuses/show.json", {
id,
tweet_mode: "extended",
trim_user: true,
include_cards: 1
}),
headers: getRequestHeaders(),
onload: function(res) {
if (res.status == "200") {
cb(JSON.parse(res.response))
} else {
console.warn(res)
}
}
})
}
// request tweet with content warnings
// the results from this api sometimes are missing the url entities, hence the new function.
function requestTweetCW(id, cb) {
GM_xmlhttpRequest({
method: "GET",
url: getRequestURL("https://twitter.com/i/api/graphql/bRL1YYMraLIBpo1PGLeFcw/TweetDetail", {
variables: {
focalTweetId: id,
includePromotedContent: false,
withBirdwatchNotes: false,
withDownvotePerspective: false,
withReactionsMetadata: false,
withReactionsPerspective: false,
withSuperFollowsTweetFields: false,
withSuperFollowsUserFields: false,
withVoice: false
}
}),
headers: getRequestHeaders(),
onload: function(res) {
if (res.status == "200") {
cb(
JSON.parse(res.response)
.data.threaded_conversation_with_injections.instructions
.find(e => e.type == "TimelineAddEntries").entries
.find(e => e.entryId.startsWith("tweet-"))
.content.itemContent.tweet_results.result.legacy
)
} else {
console.warn(res)
}
}
})
}
function requestUser(screenName, cb) {
GM_xmlhttpRequest({
method: "GET",
url: getRequestURL(`https://twitter.com/i/api/graphql/jMaTS-_Ea8vh9rpKggJbCQ/UserByScreenName`, {
variables: {
screen_name: screenName,
withHighlightedLabel: true
}
}),
headers: getRequestHeaders(),
onload: function(res) {
if (res.status == "200") {
cb(JSON.parse(res.response))
} else {
console.warn(res)
}
}
})
}
function blockUser(user_id, block, cb) {
GM_xmlhttpRequest({
method: "POST",
url: getRequestURL(`https://api.twitter.com/1.1/blocks/${block ? "create" : "destroy"}.json`, {
user_id,
skip_status: true
}),
headers: getRequestHeaders(),
onload: function(res) {
if (res.status == "200") {
cb()
} else {
console.warn(res)
}
}
})
}
// adds links from an entities object to a text
String.prototype.populateWithEntities = function(entities) {
let text = this.toString()
let out = text
let toReplace = []
// urls
if (entities.urls) {
for (let url of entities.urls) {
toReplace.push({
[url.indices[0]]: `${url.display_url} `
})
}
}
// users
if (entities.user_mentions) {
for (let user of entities.user_mentions) {
let x = text.slice(user.indices[0], user.indices[0]+1) == "@" ? 0 : 1
toReplace.push({
[user.indices[0]+x]: ``,
[user.indices[1]+x]: ` `
})
}
}
// hashtags
if (entities.hashtags) {
for (let hashtag of entities.hashtags) {
let x = text.slice(hashtag.indices[0], hashtag.indices[0]+1) == "#" ? 0 : 1
toReplace.push({
[hashtag.indices[0]+x]: ``,
[hashtag.indices[1]+x]: ` `
})
}
}
// change indices if emoji(s) appear before the entity
// reason: multiple > 0xFFFF codepoint emojis are counted wrong: all but the first emoji have their length reduced by 1.
// also, if any emoji > 0xFFFF precedes a url, the indices of the url are misaligned by -1.
let match
let counter = 0
while ((match = emojiRegexp.exec(text)) != null) {
let e = match[1]
if (e.codePointAt(0) < 0xFFFF) continue
counter++
for (let i in toReplace) {
let tmp = Object.entries(toReplace[i])
// skip if not url and first element
if (tmp[0][1] != ` e.url == match[1]).expanded_url}"`)
}
}
return out
}
// replace emojis with the twitter svgs
String.prototype.replaceEmojis = function() {
let text = this.toString()
.replace(/([\*#0-9])\s\u20E3/ug, "$1\u20E3")
.replace(/([\*#0-9])\uFE0F/ug, "$1")
let out = text
let match
let offset = 0
while ((match = emojiRegexp.exec(text)) != null) {
let e = match[1]
// get unicode of emoji
let uni = []
for (let i = 0; i < e.length; i++) {
uni.push(e.codePointAt(i).toString(16))
if (e.codePointAt(i) > 0xFFFF) i++
}
// remove fe0f from non joined emojis
if (uni.length > 1 && uni[1].match(/^FE0F$/i)) uni.pop()
// replace with image
// https://abs-0.twimg.com/emoji/v2/svg/1f647.svg
// https://abs-0.twimg.com/emoji/v2/svg/1f647-200d-2640-fe0f.svg
let img = ` `
out = out.replaceAt(match.index + offset, e.length, img)
offset += img.length - e.length
}
return out
}
// ###################
// # GT2 settings #
// ###################
// custom options and their default values
const opt_gt2 = {
// timeline
forceLatest: false,
biggerPreviews: false,
// tweets
hideTranslateTweetButton: false,
tweetIconsPullLeft: false,
hidePromoteTweetButton: false,
showMediaWithContentWarnings: false,
showMediaWithContentWarningsSel: 7,
hideTweetAnalytics: false,
// sidebars
stickySidebars: true,
smallSidebars: false,
hideTrends: false,
leftTrends: true,
show5Trends: false,
// profile
legacyProfile: false,
squareAvatars: false,
disableHexagonAvatars: false,
enableQuickBlock: false,
leftMedia: false,
profileMediaRedirect: false,
// global look
hideFollowSuggestions: false,
hideFollowSuggestionsSel: 7,
hideFollowSuggestionsLocSel: 3,
fontOverride: false,
fontOverrideValue: "Arial",
colorOverride: false,
colorOverrideValue: "85, 102, 68",
hideMessageBox: true,
rosettaIcons: false,
favoriteLikes: false,
birdIcon: true,
// other
updateNotifications: true,
expandTcoShortlinks: true,
mobileRedirect: true,
}
// set default options
if (GM_getValue("opt_gt2") == undefined) GM_setValue("opt_gt2", opt_gt2)
// add previously non existant options
if (JSON.stringify(Object.keys(GM_getValue("opt_gt2"))) != JSON.stringify(Object.keys(opt_gt2))) {
let old = GM_getValue("opt_gt2")
// remove default options that are modified
for (let k of Object.keys(opt_gt2)) {
if (Object.keys(old).includes(k)) delete opt_gt2[k]
}
// remove old options
for (let k of Object.keys(old)) {
if (Object.keys(opt_gt2).includes(k)) delete old[k]
}
Object.assign(old, opt_gt2)
GM_setValue("opt_gt2", old)
}
// toggle opt_gt2 value
function toggleGt2Opt(key) {
let x = GM_getValue("opt_gt2")
x[key] = !x[key]
GM_setValue("opt_gt2", x)
}
// insert the menu item
function addSettingsToggle() {
if (!$(".gt2-toggle-settings").length) {
$(`main section[aria-labelledby=root-header] div[role=tablist],
main > div > div > div > div:last-child > div[role=tablist],
main div[data-testid=loggedOutPrivacySection]`).append(`
GoodTwitter2
${getSvg("caret")}
`)
}
}
// toggle settings display
$("body").on("click", ".gt2-toggle-settings", function(event) {
event.preventDefault()
window.history.pushState({}, "", $(this).attr("href"))
addSettings()
changeSettingsTitle()
})
// disable settings display again when clicking on another menu item
$("body").on("click", `main section[aria-labelledby=root-header] div[role=tablist] a:not(.gt2-toggle-settings),
main section[aria-labelledby=root-header] div[data-testid=loggedOutPrivacySection] a:not(.gt2-toggle-settings)`, () => {
$(".gt2-settings-header, .gt2-settings").remove()
})
// get html for a gt2 toggle (checkbox)
function getSettingTogglePart(name, additionalHTML="") {
let d = `${name}Desc`
return `
${additionalHTML}
${getLocStr(d) ? `
${getLocStr(d)} ` : ""}
`
}
// add the settings to the display (does not yet work on screens smaller than 1050px)
function addSettings() {
if (!$(".gt2-settings").length) {
let elem = `
${getSettingTogglePart("forceLatest")}
${getSettingTogglePart("biggerPreviews")}
${getSettingTogglePart("hideTranslateTweetButton")}
${getSettingTogglePart("tweetIconsPullLeft")}
${getSettingTogglePart("hidePromoteTweetButton")}
${getSettingTogglePart("showMediaWithContentWarnings", `
${["Nudity", "Violence", "SensitiveContent"].map((e, i) => {
let x = Math.pow(2, i)
return `
${getLocStr(`contentWarning${e}`)}
`
}).join("")}
`)}
${getSettingTogglePart("hideTweetAnalytics")}
${getSettingTogglePart("stickySidebars")}
${getSettingTogglePart("smallSidebars")}
${getSettingTogglePart("hideTrends")}
${getSettingTogglePart("leftTrends")}
${getSettingTogglePart("show5Trends")}
${getSettingTogglePart("legacyProfile")}
${getSettingTogglePart("squareAvatars")}
${getSettingTogglePart("disableHexagonAvatars")}
${getSettingTogglePart("enableQuickBlock")}
${getSettingTogglePart("leftMedia")}
${getSettingTogglePart("profileMediaRedirect")}
${getSettingTogglePart("hideFollowSuggestions", `
${getLocStr("hideFollowSuggestionsBox").replace("$type$", `
${["topics", "users", "navLists"].map((e, i) => {
let x = Math.pow(2, i)
return `
`}).join("")}
`).replace("$location$", `
${["Timeline", "Sidebars"].map((e, i) => {
let x = Math.pow(2, i)
return `
${getLocStr(`settingsHeader${e}`)}
`}).join("")}
`)}
`)}
${getSettingTogglePart("fontOverride", `
`)}
${getSettingTogglePart("colorOverride", `
`)}
${getSettingTogglePart("hideMessageBox")}
${getSettingTogglePart("rosettaIcons")}
${getSettingTogglePart("favoriteLikes")}
${getSettingTogglePart("birdIcon")}
${getSettingTogglePart("updateNotifications")}
${getSettingTogglePart("expandTcoShortlinks")}
${getSettingTogglePart("mobileRedirect")}
`
let $s = $("main section[aria-labelledby=detail-header]")
if ($s.length) {
$s.prepend(elem)
} else {
$("main > div > div > div").append(`
`)
}
// add color pickr
Pickr.create({
el: ".gt2-pickr",
theme: "classic",
lockOpacity: true,
useAsButton: true,
appClass: "gt2-color-override-pickr",
inline: true,
default: `rgb(${GM_getValue("opt_gt2").colorOverrideValue})`,
components: {
preview: true,
hue: true,
interaction: {
hex: true,
rgba: true,
hsla: true,
hsva: true,
cmyk: true,
input: true
}
}
})
.on("change", e => {
let val = e.toRGBA().toString(0).slice(5, -4)
GM_setValue("opt_gt2", Object.assign(GM_getValue("opt_gt2"), { colorOverrideValue: val}))
document.documentElement.style.setProperty("--color-override", val)
})
disableTogglesIfNeeded()
}
}
// change the title to display GoodTwitter2
function changeSettingsTitle() {
let t = $("title").html()
$("title").html(`${t.startsWith("(") ? `${t.split(" ")[0]} ` : ""}GoodTwitter2 / Twitter`)
}
// handler for the toggles
$("body").on("click", ".gt2-setting-toggle:not(.gt2-disabled)", function() {
$(this).toggleClass("gt2-active")
if ($(this).is("[data-setting-name]")) {
let name = $(this).attr("data-setting-name").trim()
toggleGt2Opt(name)
$("body").toggleClass(`gt2-opt-${name.toKebab()}`)
}
// handle selector settings (hideFollowSuggestions, showMediaWithContentWarnings)
if ($(this).is("[data-sel]")) {
let sName = $(this).closest("[data-setting-name]").attr("data-setting-name")
let opt = GM_getValue("opt_gt2")
GM_setValue("opt_gt2", Object.assign(opt, { [sName]: opt[sName] ^ parseInt($(this).attr("data-sel")) }))
}
disableTogglesIfNeeded()
})
// handler for inputs
$("body").on("keyup", ".gt2-setting-input input", function() {
let name = $(this).parent().attr("data-setting-name").trim()
let val = $(this).val().trim()
GM_setValue("opt_gt2", Object.assign(GM_getValue("opt_gt2"), { [name]: val}))
document.documentElement.style.setProperty(`--${name.replace("Value", "").toKebab()}`, val)
})
function disableTogglesIfNeeded() {
// other trend related toggles are not needed when the trends are disabled
$("div[data-setting-name=leftTrends], div[data-setting-name=show5Trends]")
[GM_getValue("opt_gt2").hideTrends ? "addClass" : "removeClass"]("gt2-disabled")
// hide font input if fontOverride is disabled
$("[data-setting-name=fontOverrideValue]")
[GM_getValue("opt_gt2").fontOverride ? "removeClass" : "addClass"]("gt2-hidden")
// hide color input if colorOverride is disabled
$(".gt2-color-override-pickr")
[GM_getValue("opt_gt2").colorOverride ? "removeClass" : "addClass"]("gt2-hidden")
// hide follow suggestions
$("[data-setting-name=hideFollowSuggestionsBox]")
[GM_getValue("opt_gt2").hideFollowSuggestions ? "removeClass" : "addClass"]("gt2-hidden")
// showMediaWithContentWarnings
$("[data-setting-name=showMediaWithContentWarningsBox]")
[GM_getValue("opt_gt2").showMediaWithContentWarnings ? "removeClass" : "addClass"]("gt2-hidden")
}
// click on the back button
$("body").on("click", ".gt2-settings-back", () => window.history.back())
// #######################
// # various functions #
// #######################
// add navbar
function addNavbar() {
waitForKeyElements(`nav > a[href="/home"]`, () => {
if ($(".gt2-nav").length) return
$("main").before(`
${getLocStr("composeNewTweet")}
`)
// home, notifications, messages
for (let type of [
"Home",
"Notifications",
"Messages",
window.innerWidth < 1005 ? "Explore" : null
]) {
if (!type) continue
let origElemSel = `nav > a[href^="/${type.toLowerCase()}"]:not([data-testid=AppTabBar_Profile_Link]):not([href$="/lists"])`
let $e = document.querySelector(origElemSel)
if (!$e) continue
document.querySelector(".gt2-nav-left").insertAdjacentHTML("beforeend", $e.outerHTML)
document.querySelectorAll(`.gt2-nav-left [data-testid]`)
.forEach(e => {
e.addEventListener("click", event => {
if (!event.ctrlKey) {
event.preventDefault()
let testid = event.target.closest("[data-testid]").dataset.testid
document.querySelector(`nav [data-testid=${testid}]`).click()
}
})
})
watchForChanges(origElemSel, e => {
let navbarElem = document.querySelector(`.gt2-nav-left [data-testid=${e.dataset.testid}]`)
if (!navbarElem) return
navbarElem.innerHTML = e.innerHTML
navbarElem.firstElementChild.setAttribute("data-gt2-color-override-ignore", "")
navbarElem.firstElementChild.insertAdjacentHTML("beforeend", `
`)
})
// $e.appendTo(".gt2-nav-left")
$(`.gt2-nav a[href^="/${type.toLowerCase()}"] > div`)
.append(`
`)
.attr("data-gt2-color-override-ignore", "")
}
// highlight current location
$(`.gt2-nav a[href^='/${getPath().split("/")[0]}']`).addClass("active")
// twitter logo
$("h1 a[href='/home'] svg")
.appendTo(".gt2-nav-center a")
})
}
function watchForChanges(selector, callback) {
waitForKeyElements(selector, $element => {
let element = $element[0]
if (element) {
new MutationObserver(mut => {
mut.forEach(() => callback(element))
}).observe(element, {
attributes: true,
subtree: true,
childList: true
})
}
})
}
// add navbar
function addNavbarLoggedOut() {
waitForKeyElements("nav > a[data-testid=AppTabBar_Explore_Link]", () => {
if ($(".gt2-nav").length) return
$("body").prepend(`
`)
// explore and settings
$(`nav > a[data-testid=AppTabBar_Explore_Link],
nav > a[href="/settings"]`)
.appendTo(".gt2-nav-left")
$(`.gt2-nav a[data-testid=AppTabBar_Explore_Link] > div`)
.append(`
`)
$(`.gt2-nav a[href="/settings"] > div`)
.append(`
`)
// highlight current location
$(`.gt2-nav a[href^='/${getPath().split("/")[0]}']`).addClass("active")
// twitter logo
$("header h1 a[href='/'] svg")
.appendTo(".gt2-nav-center a")
})
}
// add search
function addSearch() {
let search = "div[data-testid=sidebarColumn] > div > div:nth-child(2) > div > div > div > div:nth-child(1)"
waitForKeyElements(`${search} input[data-testid=SearchBox_Search_Input]`, () => {
// remove if added previously
$(".gt2-search").empty()
// add search
$(search)
.prependTo(".gt2-search")
$("body").addClass("gt2-search-added")
})
}
// add element to sidebar
function addToSidebar(elements) {
let w = window.innerWidth
let insertAt = ".gt2-left-sidebar"
// insert into the right sidebar
if ((!GM_getValue("opt_gt2").smallSidebars && w <= 1350) ||
( GM_getValue("opt_gt2").smallSidebars && w <= 1230)) {
insertAt = "div[data-testid=sidebarColumn] > div > div:nth-child(2) > div > div > div"
}
elements.unshift(`
`)
waitForKeyElements(insertAt, () => {
if (!$(insertAt).find(".gt2-legacy-profile-info").length) {
for (let elem of elements.slice().reverse()) {
if (insertAt.startsWith(".gt2")) {
$(insertAt).prepend(elem)
} else {
$(`${insertAt} > div:empty:not(.gt2-legacy-profile-info)`).after(elem)
}
}
}
if ($(".gt2-dashboard-profile").length > 1) {
$(".gt2-dashboard-profile").last().remove()
}
})
}
// profile view left sidebar
function getDashboardProfile() {
let i = getInfo()
// console.log(`userInformation:\n${JSON.stringify(i, null, 2)}`)
let href = isLoggedIn() ? "href" : "data-href"
return `
${getSvg(isLoggedIn() ? "caret" : "moon")}
`
}
// gt2 update notice
function getUpdateNotice() {
let v = GM_info.script.version
return `
`
}
// recreate the legacy profile layout
function rebuildLegacyProfile() {
let currentScreenName = getPath().match(/^intent\/(user|follow)/)
? getPath().match(/screen_name=(\w+)/)[1]
: getPath().split("/")[0].split("?")[0].split("#")[0]
console.log(`rebuild: ${currentScreenName}`)
let profileSel = "div[data-testid=primaryColumn] > div > div:nth-last-child(1) > div > div > div:nth-child(1) > div:nth-child(2)"
waitForKeyElements([
`a[href="/${currentScreenName}/photo" i] img`,
`a[href="/${currentScreenName}/nft" i] img`,
`${profileSel} [data-testid=UserDescription] [href="https://support.twitter.com/articles/20169222"]`, // withheld in country
`${profileSel} [data-testid=UserDescription] [href="https://support.twitter.com/articles/20169199"]` // temporarily unavailable (Media Policy Violation)
].join(", "), (e) => {
// remove previously added profile
if ($(".gt2-legacy-profile-nav").length) {
$(".gt2-legacy-profile-banner, .gt2-legacy-profile-nav").remove()
$(".gt2-legacy-profile-info").empty()
}
// profile information
const i = {
banner: () => $("a[href$='/header_photo'] img"),
avatar: () => $(profileSel).find("a[href$='/photo'] img, a[href$='/nft'] img").first(),
screenName: () => $(profileSel).find("> [data-testid=UserName] > div:nth-child(1) > div [dir] > span:contains(@):not(:has(> *))").text().slice(1),
followsYou: () => $(profileSel).find("> [data-testid=UserName] > div:nth-child(1) > div > div:nth-child(2) > div:nth-child(2)"),
name: () => $(profileSel).find("> [data-testid=UserName] > div:nth-child(1) > div > div:nth-child(1) > div"),
automated: () => $(profileSel).find("> [data-testid=UserName] > div:nth-child(2)"),
joinDateHTML: () => $(profileSel).find("div[data-testid=UserProfileHeader_Items] > span:last-child").html(),
followingRnd: () => $(profileSel).find(`a[href$="/following"] > span:first-child, > div:not(:first-child) div:nth-child(1) > [role=button]:first-child:last-child > span:first-child`).first().text().trim(),
followersRnd: () => $(profileSel).find(`a[href$="/followers"] > span:first-child, > div:not(:first-child) div:nth-child(2) > [role=button]:first-child:last-child > span:first-child`).first().text().trim(),
// booleans
hasOnlyScreenName: () => $(profileSel).find("> [data-testid=UserName] > div:nth-child(1) > div > div").length == 1,
avatarIsHex: () => $(profileSel).find("a[href$='/nft']").length > 0,
// sidebar elements
description: () => $(profileSel).find("div[data-testid=UserDescription]"),
items: () => $(profileSel).find("div[data-testid=UserProfileHeader_Items]"),
fyk: () => $(profileSel).find("> div:last-child:not(:nth-child(2)) > div:last-child:first-child")
}
if (!$(".gt2-legacy-profile-banner").length) {
$("header").before(`
${i.banner().length ? `
` : ""}
${i.name().html()}
${i.hasOnlyScreenName() ? "" : `
@${i.screenName()}
`}
${i.followsYou().length ? i.followsYou().prop("outerHTML") : ""}
`)
}
// add like and tweet count
requestUser(i.screenName(), res => {
let profileData = res.data.user
let pleg = profileData.legacy
// profile id
waitForKeyElements(".gt2-legacy-profile-info > :first", e => $(e).parent().attr("data-profile-id", profileData.rest_id))
// change stats
for (let tmp of [
[i.screenName(), "statuses_count"],
["following", "friends_count"],
["followers", "followers_count"],
["likes", "favourites_count"]
]) {
$(`.gt2-legacy-profile-nav-center a[href$="/${tmp[0]}"]`)
.attr("title", pleg[tmp[1]].humanize())
.find("div:nth-child(2)").html(pleg[tmp[1]].humanizeShort())
}
// expand t.co links
if (GM_getValue("opt_gt2").expandTcoShortlinks) {
let urls = pleg.entities.description.urls.concat(pleg.entities.url ? pleg.entities.url.urls : [])
$(`.gt2-legacy-profile-info a[href^="https://t.co"]`).each(function() {
$(this).attr("href", urls.find(e => e.url == $(this).attr("href").split("?")[0]).expanded_url)
})
}
})
// sidebar profile information
waitForKeyElements(`[href="/${
getPath().match(/^intent\/(user|follow)/)
? getPath().match(/screen_name=(\w+)/)[1]
: getPath().split("/")[0].split("?")[0].split("#")[0]
}/following" i]`, () => {
$(".gt2-legacy-profile-info").data("alreadyFound", false)
waitForKeyElements(".gt2-legacy-profile-info", () => {
if (!$(".gt2-legacy-profile-info .gt2-legacy-profile-screen-name").length) {
$(".gt2-legacy-profile-info").append(`
${i.hasOnlyScreenName() ? "" : `
@${i.screenName()}
`}
${i.followsYou().length ? i.followsYou().prop("outerHTML") : ""}
${i.automated().length ? `${i.automated().prop("outerHTML")}
` : ""}
${i.description().length ? `${i.description().parent().html()}
` : ""}
${i.items().length ? i.items().html() : ""}
${i.fyk().length ? `${i.fyk().prop("outerHTML")}
` : ""}
`)
document.querySelector(".gt2-legacy-profile-info .gt2-legacy-profile-name")
.insertAdjacentHTML("afterbegin", i.name()[0].innerHTML)
document.querySelector(`.gt2-legacy-profile-info .gt2-legacy-profile-name [data-testid=icon-verified]`)
?.parentElement?.addEventListener("click", e => {
document.querySelector(`${profileSel} [data-testid=icon-verified]`)
?.dispatchEvent(new MouseEvent("click", {bubbles: true}))
// calculate position of the box
waitForKeyElements(`#layers > div:nth-child(2) > div > div > div:nth-child(2)`, $floatingBox => {
let floatingBox = $floatingBox[0]
let boxBcr = floatingBox.getBoundingClientRect()
let buttonBcr = e.target.getBoundingClientRect()
const pad = 20
let left = Math.max(pad, (buttonBcr.left + buttonBcr.width / 2 - boxBcr.width / 2))
let leftMax = innerWidth - pad
let topBoxBelow = buttonBcr.bottom + 10
let topBoxAbove = Math.max(pad, buttonBcr.top - 10 - boxBcr.height)
let topBoxBelowMax = innerHeight - pad
document.querySelector(".gt2-style-verification")?.remove()
document.head.insertAdjacentHTML("beforebegin", `
`)
})
})
GM_setValue("hasRun_InsertFYK", false)
waitForKeyElements(`a[href$="/followers_you_follow"] div[style*=background-image] + img`, e => {
if (!GM_getValue("hasRun_InsertFYK")) {
$(".gt2-legacy-profile-fyk").html($(e).parents(`a[href$="/followers_you_follow"]`).prop("outerHTML"))
GM_setValue("hasRun_InsertFYK", true)
}
})
}
})
})
// buttons
if (!$(".gt2-legacy-profile-nav-right > div").length) {
$(profileSel).find("> div:nth-child(1) > div:last-child").detach().appendTo(".gt2-legacy-profile-nav-right")
}
})
// profile suspended / not found
waitForKeyElements([
`body:not([data-gt2-path^="messages"]) [data-testid=empty_state_body_text] > *:not(a):first-child:last-child`, // not found
`[data-testid=emptyState] [href="https://help.twitter.com/rules-and-policies/twitter-rules"]` // suspended
].join(", "), () => {
let $tmp = $(profileSel).find("> div:nth-child(2) > div > div")
let i = {
screenName: () => $tmp.find("> div:nth-last-child(1)").text().trim().slice(1),
nameHTML: () => $tmp.find("> div").length > 1 ? $tmp.find("> div:nth-child(1)").html() : null
}
$("body").addClass("gt2-profile-not-found")
$("header").before(`
`)
waitForKeyElements(".gt2-legacy-profile-info", () => {
$(".gt2-legacy-profile-info").append(`
${i.nameHTML() ? i.nameHTML() : `@${i.screenName()}`}
${i.nameHTML() ? `
` : ""}
`)
})
})
}
// force latest tweets view.
function forceLatest() {
waitForKeyElements(`body:not([data-switched-to-latest]) [data-testid=ScrollSnap-List] > div:nth-child(2) > [href="/home"][aria-selected=false]`, e => {
e[0].click()
document.body.setAttribute("data-switched-to-latest", "")
})
}
// handle trends (hide, move, wrap)
function handleTrends() {
let w = window.innerWidth
let trends = `section:not(.gt2-trends-handled) div[data-testid=trend]:not(.gt2-trend-wrapped),
section[aria-labelledby^=accessible-list]:not(.gt2-trends-handled) a[href="/explore/tabs/for-you"] > div > span:not(.gt2-trend-wrapped)`
waitForKeyElements(trends, e => {
// actions for the whole container
if (!$(trends).parents("section").hasClass("gt2-trends-handled")
&& $(trends).parents("div[data-testid=sidebarColumn]").length
) {
// hide trends
if (GM_getValue("opt_gt2").hideTrends) {
$(trends).parents("section").parent().parent().remove()
return
}
// move trends
if (GM_getValue("opt_gt2").leftTrends
&& ((!GM_getValue("opt_gt2").smallSidebars && w > 1350)
|| (GM_getValue("opt_gt2").smallSidebars && w > 1230))) {
if ($(".gt2-trends").length) $(".gt2-trends").remove()
$(trends).parents("section").parent().parent()
.detach().addClass("gt2-trends")
.appendTo(".gt2-left-sidebar")
}
$(trends).parents("section").addClass("gt2-trends-handled")
}
// wrap trends in anchors
$(e).each(function() {
let $toWrap = $(this).find("> div > div:nth-child(2) > span [dir]")
if ($toWrap.length) {
$(this).addClass("gt2-trend-wrapped")
let txt = $toWrap.text()
let query = encodeURIComponent($toWrap.text().replace(/%/g, "%25"))
.replace(/'/g, "%27")
.replace(/(^\"|\"$)/g, "")
$toWrap.html(`${txt} `)
}
})
})
}
function getFollowersYouKnowHTML(screenName, profileID, callback) {
GM_xmlhttpRequest({
method: "GET",
url: getRequestURL("https://twitter.com/i/api/1.1/friends/following/list.json", {
include_profile_interstitial_type: 1,
include_blocking: 1,
include_blocked_by: 1,
include_followed_by: 1,
include_want_retweets: 1,
include_mute_edge: 1,
include_can_dm: 1,
include_can_media_tag: 1,
skip_status: 1,
cursor: -1,
user_id: profileID,
count: 3,
with_total_count: true
}),
headers: getRequestHeaders(),
onload: res => {
if (res.status == 200) {
// followers you know
let fyk = JSON.parse(res.response)
let fykText
if (fyk.total_count < 4) {
fykText = getLocStr(`followedBy${fyk.total_count}`)
.replace("$p1$", fyk.users.length > 0 ? fyk.users[0].name : "")
.replace("$p2$", fyk.users.length > 1 ? fyk.users[1].name : "")
.replace("$p3$", fyk.users.length > 2 ? fyk.users[2].name : "")
} else {
fykText = getLocStr("followedBy4Plus")
.replace("$p1$", fyk.users[0].name)
.replace("$p2$", fyk.users[1].name)
.replace("$nr$", fyk.total_count - 2)
}
let fykImg = ""
for (let u of fyk.users) {
fykImg += ` `
}
callback(`
${fykImg}
${fykText.replaceEmojis()}
`)
} else if (res.status == 401) {
callback("")
}
}
})
}
// display standard information for blocked profile
function displayBlockedProfileData() {
let screenName = getPath().split("/")[0].split("?")[0].split("#")[0]
$("body").addClass("gt2-page-profile-youre-blocked")
requestUser(screenName, res => {
let profileData = res.data.user
// get “x persons you follow follow this account” stuff
getFollowersYouKnowHTML(screenName, profileData.rest_id, fykHTML => {
let pleg = profileData.legacy
// join date
let joinDate = new Date(pleg.created_at)
let p = {
description: pleg.description
.populateWithEntities(pleg.entities.description)
.replaceEmojis(),
location: pleg.location != "" ? `
${getSvg("location")}
${pleg.location.replaceEmojis()}
` : null,
url: pleg.url ? `
${getSvg("url")}
${pleg.entities.url.urls[0].display_url}
` : null,
joinDate: `
${getSvg("calendar")}
${
getLocStr("joinDate")
.replace("$date$", joinDate.toLocaleDateString(getLang(), { month: "long", year: "numeric" }))
}
`,
birthday: profileData.legacy_extended_profile && profileData.legacy_extended_profile.birthdate ? (() => {
let bd = profileData.legacy_extended_profile.birthdate
let bdText
let date = new Date(Date.UTC(bd.year || 1970, bd.month || 1, bd.day || 1))
if (bd.year && !bd.month && !bd.day) {
bdText = getLocStr("bornYear").replace("$year$", date.toLocaleDateString(getLang(), { year: "numeric"}))
} else {
let opt = {}
if (bd.year) opt.year = "numeric"
if (bd.month) opt.month = "long"
if (bd.day) opt.day = "numeric"
bdText = getLocStr("bornDate").replace("$date$", date.toLocaleDateString(getLang(), opt))
}
return `
${getSvg("balloon")}
${bdText}
`
})() : null
}
// description: add links for mentioned users
for (let m of p.description.match(/(@[0-9A-Za-z_]+)/g) || []) {
p.description = p.description.replace(m, `${m} `)
}
// add profile info
$("a[href$='/header_photo'] + div > div:nth-child(2)").after(`
${p.description}
${p.location ? p.location : ""}
${p.url ? p.url : ""}
${p.birthday ? p.birthday : ""}
${p.joinDate}
`)
// add followers/following count
if (!$(`.gt2-blocked-profile-items + div [href$="/following"]`).length) {
$(".gt2-blocked-profile-items").after(`
`)
}
// followersYouKnow
$(".gt2-blocked-profile-items + div").after(fykHTML)
// add legacy sidebar profile information
waitForKeyElements(".gt2-legacy-profile-name", () => {
if (!$(".gt2-legacy-profile-info .gt2-legacy-profile-fyk").length) {
$(".gt2-legacy-profile-info .gt2-legacy-profile-items").append(`
${p.description ? `${p.description}
` : ""}
${p.location ? `${p.location}
` : ""}
${p.url ? `${p.url}
` : ""}
${p.birthday ? `${p.birthday}
` : ""}
${p.joinDate}
${fykHTML}
`)
}
})
// profile id
waitForKeyElements(".gt2-legacy-profile-info > :first", e => $(e).parent().attr("data-profile-id", profileData.rest_id))
})
})
}
// ##################################
// # translate tweets in timelime #
// ##################################
// add translate button
if (!GM_getValue("opt_gt2").hideTranslateTweetButton) {
waitForKeyElements(`[data-testid=tweet] [lang],
[data-testid=tweet] + div > div:nth-child(2) [role=link] [lang]`, function(e) {
let $e = $(e)
if ($e.siblings().length) return
let tweetLang = $e.attr("lang")
let userLang = getLang()
userLang = userLang == "en-GB" ? "en" : userLang
if (tweetLang != userLang && tweetLang != "und") {
$e.first().after(`
`)
}
})
}
// translate a tweet or LPL bio
$("body")[0].addEventListener("click", function(event) {
if (!$(event.target).is(".gt2-translate-tweet, .gt2-legacy-profile-info [data-testid=UserDescription] + [role=button] span")) return
event.preventDefault()
console.log("translating tweet");
let target = $(event.target).is(".gt2-translate-tweet") ? event.target : $(event.target).parents("[role=button]")[0]
// already translated
if ($(target).parent().find(".gt2-translated-tweet").length) {
$(target).addClass("gt2-hidden")
$(target).parent().find(".gt2-translated-tweet, .gt2-translated-tweet-info").removeClass("gt2-hidden")
return
}
let id = $(target).parents("article[data-testid=tweet]").length
? $(target).parents("article[data-testid=tweet]")
.find(`> div > div > div > div > div > div:nth-child(1) a[href*='/status/'],
div[data-testid=tweet] + div > div:nth-child(3) a[href*='/status/']`).attr("href").split("/")[3]
: null
// embedded tweet
if ($(target).parents("[role=link]").parents("article[data-testid=tweet]").length) {
requestTweet(id, res => translateTweet(target, res.quoted_status_id_str))
// normal tweet with embedded one
} else if ($(target).parents("article[data-testid=tweet]").find("[role=link] [lang]").length) {
requestTweet(id, res => translateTweet(target, id, res.quoted_status_id_str))
// normal tweet or bio
} else {
translateTweet(target, id)
}
}, true)
function translateTweet(e, id, quoteId) {
let isTweet = $(e).is(".gt2-translate-tweet")
GM_setValue("tmp_translatedTweetInfo", getLocStr("translatedTweetInfo"))
let url = `https://twitter.com/i/api/1.1/strato/column/None/${isTweet ? `tweetId=${id}` : `profileUserId=${$(".gt2-legacy-profile-info").data("profile-id")}`},destinationLanguage=None,translationSource=Some(Google),feature=None,timeout=None,onlyCached=None/translation/service/translate${isTweet ? "Tweet" : "Profile"}`
GM_xmlhttpRequest({
method: "GET",
url,
headers: getRequestHeaders(isTweet ? {
referer: `https://twitter.com/i/status/${id}`
} : {}),
onload: function(res) {
if (res.status == "200") {
let o = JSON.parse(res.response)
if (!isTweet) o = o.profileTranslation
console.log(o)
let out = o.translation
// handle entities in tweet
if (o.entities) {
// remove embedded url if applicable
if (quoteId && o.entities.urls) {
let tco = o.entities.urls.find(x => x.expanded_url.endsWith(quoteId))
if (tco) {
out = out.replace(` ${tco.url}`, "")
o.entities.urls = o.entities.urls.filter(x => !x.expanded_url.endsWith(quoteId))
}
}
out = out.populateWithEntities(o.entities)
}
$(e).addClass("gt2-hidden")
$(e).after(`
`)
} else {
console.error("Error occurred while translating.")
console.error(url)
console.error(res)
}
}
})
}
// hide translation
$("body")[0].addEventListener("click", function(event) {
if (!$(event.target).is(".gt2-translated-tweet-info")) return
event.preventDefault()
$(event.target).parent().find(".gt2-translated-tweet, .gt2-translated-tweet-info").addClass("gt2-hidden")
$(event.target).prevAll(".gt2-translate-tweet, [role=button]").removeClass("gt2-hidden")
}, true)
// ##########################
// # misc event handlers #
// ##########################
// compose tweet button
$("body").on("click", ".gt2-nav .gt2-compose", () => {
$("header a[href='/compose/tweet'] > div").click()
})
// add elements to navbar dropdow menu
$("body").on("click", ".gt2-toggle-navbar-dropdown", () => {
console.log("navbar toggled");
let i = getInfo()
$("header nav > div[data-testid=AppTabBar_More_Menu]").click()
let more = "div[role=menu][style^='max-height: calc'].r-ipm5af > div > div > div"
waitForKeyElements(`${more} `, e => {
if ($(more).find("a[href='/explore']").length) return
// separator line
let separatorHtml = e[0].querySelector("[role=separator]").parentElement.outerHTML
e[0].insertAdjacentHTML("afterbegin", separatorHtml)
// items from left menu to attach
let toAttach = [
{
sel: `a[href='/${i.screenName}']`,
name: "Profile"
}, {
sel: `a[href$='/lists']`,
name: "Lists"
}, {
sel: `a[href$='/bookmarks']`,
name: "Bookmarks"
}, {
sel: `a[href$='/communities']`,
name: "Communities"
}, {
sel: `a[href='/explore']`,
name: "Explore"
}
]
for (let e of toAttach.reverse()) {
if (!$("header nav").find(e.sel).length) continue
let $tmp = $("header nav").find(e.sel).clone()
$tmp.children().append(`${getLocStr(`nav${e.name}`)} `)
$tmp.prependTo(more)
}
// expand sections
document.querySelectorAll(`${more} [aria-expanded=false]`)
.forEach(e => {
e.click()
e.nextElementSibling.insertAdjacentHTML("afterend", separatorHtml)
})
$(`Logout `).appendTo(more)
})
})
// acc switcher dropdown
$("body").on("click", ".gt2-toggle-acc-switcher-dropdown", function() {
$("body").addClass("gt2-acc-switcher-active")
$("div[data-testid=SideNav_AccountSwitcher_Button]").click()
// change dropdown position
$(".gt2-style-acc-switcher-dropdown").remove()
let pos = $(".gt2-toggle-acc-switcher-dropdown")[0].getBoundingClientRect()
$("html").prepend(`
`)
})
// remove class on next click
$("body").on("click", `:not(.gt2-toggle-acc-switcher-dropdown):not(div[data-testid=SideNav_AccountSwitcher_Button])`, function(e) {
if (e.target.closest(`[d^="M22.25 12c0-1.43-.88"]`))
return
setTimeout(function () {
if (!$("a[href='/i/flow/login']").length) {
$("body").removeClass("gt2-acc-switcher-active")
document.querySelector(".gt2-style-verification")?.remove()
}
}, 2000)
})
// expand the “What’s happening?” tweet field (minimized by default)
$("body").on("click", "div[data-testid=primaryColumn] > div > div:nth-child(2)", e => $(e.currentTarget).addClass("gt2-compose-large"))
// loggedOut nightmode
$("body").on("click", ".gt2-toggle-lo-nightmode", () => {
let nm = document.cookie.match(/night_mode=1/) ? 0 : 1
// delete old cookie
document.cookie = "night_mode=; Max-Age=0;"
// create new cookie
let d = new Date()
d.setDate(d.getDate() + 500)
document.cookie = `night_mode=${nm}; expires=${d.toUTCString()}; path=/; domain=.twitter.com`
window.location.reload()
})
// close sidebar notice
$("body").on("click", ".gt2-sidebar-notice-close", function() {
if ($(this).parents(".gt2-sidebar-notice").hasClass("gt2-update-notice")) {
GM_setValue(`sb_notice_ack_update_${GM_info.script.version}`, true)
}
$(this).parents(".gt2-sidebar-notice").remove()
})
// remove blocked profile stuff on unblock
$("body").on("click", `div[data-testid=placementTracking] div[data-testid$="-unblock"]`, () => $("[class^=gt2-blocked-profile]").remove())
// [LPL] unusual activity button: make elements clickable again
$(document).on("click", `.gt2-profile-not-found [data-testid=primaryColumn] > div > div:nth-child(2) > div > div > div:nth-child(2) > div[role=button]`, () => $("body").removeClass("gt2-profile-not-found"))
// expand t.co shortlinks (tweets)
$(document).on("mouseover", `.gt2-opt-expand-tco-shortlinks div:not([data-testid=placementTracking]) > div > article[data-testid=tweet]:not(.gt2-tco-expanded),
.gt2-opt-expand-tco-shortlinks.gt2-page-tweet [data-testid=primaryColumn] section > h1 + div > div > div:nth-child(1) article:not(.gt2-tco-expanded)`, function() {
let $tweet = $(this)
$tweet.addClass("gt2-tco-expanded")
// exit if tweet has no links
if (!$tweet.find(`a[href^="http://t.co"], a[href^="https://t.co"], [data-testid="card.wrapper"]`).length) return
let id = !$tweet.find(`time`).length && $("body").is(".gt2-page-tweet")
? getPath().split("/")[2].split("?")[0].split("#")[0]
: $tweet.find(`time`).parent().attr("href").split("/status/")[1]
requestTweet(id, res => {
$tweet.find(`a[href^="http://t.co"], a[href^="https://t.co"]`).each(function() {
$(this).attr("href", res.entities.urls.find(e => e.url == $(this).attr("href").split("?")[0]).expanded_url)
})
$tweet.find(`[data-testid="card.layoutSmall.media"] + *:not(a)`).each(function() {
$(this).wrap(` `)
})
})
})
// expand t.co shortlinks (profile, not legacy)
$(document).on("mouseover", `.gt2-opt-expand-tco-shortlinks.gt2-page-profile:not(.gt2-opt-legacy-profile) [data-testid=primaryColumn] > div > div:nth-child(2) > div > div > div:nth-child(1):not(.gt2-tco-expanded), .gt2-opt-expand-tco-shortlinks [data-testid=UserCell]`, function() {
let $profile = $(this)
$profile.addClass("gt2-tco-expanded")
// exit if profile has no links
if (!$profile.find(`a[href^="http://t.co"], a[href^="https://t.co"]`).length) return
let screenName = $profile.is("[data-testid=UserCell]")
? $profile.find("> div > div:nth-child(2) > div:nth-child(1) a").attr("href").slice(1)
: getPath().split("/")[0].split("?")[0].split("#")[0]
requestUser(screenName, res => {
let ent = res.data.user.legacy.entities
let urls = []
if (ent.description) urls.push(...ent.description.urls)
if (ent.url) urls.push(...ent.url.urls)
$profile.find(`a[href^="http://t.co"], a[href^="https://t.co"]`).each(function() {
$(this).attr("href", urls.find(e => e.url == $(this).attr("href").split("?")[0].split("#")[0]).expanded_url)
})
})
})
// block/unblock account on holding follow button for 3 seconds
if (GM_getValue("opt_gt2").enableQuickBlock) {
let qbOffer
$("body").on("mouseover", `[data-testid$="-follow"]:not([data-gt2-qb-state])`, e => {
let $b = $(e.target).parents(`[data-testid$="-follow"]`)
$b.attr("data-gt2-qb-state", "offer-pending")
qbOffer = setTimeout(() => {
$b.attr("data-gt2-qb-state", "offer")
$b.find("> div > span").append(`
${getLocStr("qbBlock")}
${getLocStr("qbBlocked")}
${getLocStr("qbUnblock")}
`)
}, 3e3)
})
$("body").on("click", `[data-testid$="-follow"][data-gt2-qb-state=offer]`, e => {
e.stopImmediatePropagation()
let $b = $(e.target).parents(`[data-testid$="-follow"]`)
let user_id = $b.attr("data-testid").slice(0, -7)
blockUser(user_id, true, () => {
console.log(`quickblock: ${user_id}`)
$b.attr("data-gt2-qb-state", "blocked")
})
})
$("body").on("click", `[data-testid$="-follow"][data-gt2-qb-state=blocked]`, e => {
e.stopImmediatePropagation()
let $b = $(e.target).parents(`[data-testid$="-follow"]`)
let user_id = $b.attr("data-testid").slice(0, -7)
blockUser(user_id, false, () => {
console.log(`quickunblock: ${user_id}`)
$b.removeAttr("data-gt2-qb-state")
$b.find("[class^=gt2-qb]").remove()
})
})
$("body").on("mouseleave", `[data-testid$="-follow"][data-gt2-qb-state^=offer],
[data-testid$="-unfollow"][data-gt2-qb-state^=offer]`, e => {
let $b = $(e.target).parents(`[data-testid$="-follow"]`)
$b.removeAttr("data-gt2-qb-state")
$b.find("[class^=gt2-qb]").remove()
clearTimeout(qbOffer)
})
}
// fix coloring on clicking the follow button
$("body").on("click", `[data-testid$="-follow"]`, e => $(e.target).parents(`[data-testid$="-follow"]`).attr("data-gt2-just-clicked-follow", 1))
$("body").on("mouseleave", `[data-testid$="-unfollow"][data-gt2-just-clicked-follow]`, e => $(e.target).parents(`[data-testid$="-unfollow"]`).removeAttr("data-gt2-just-clicked-follow"))
// [LPL] enlarge profile image when clicking on it
$("body").on("click", ".gt2-legacy-profile-nav-avatar", () => $(`div[data-testid=primaryColumn] > div > div:nth-child(2) > div > div > div:nth-child(1) > div:nth-child(2)`).find(`a[href$="/photo"] img, a[href$="/nft"] img`).first().click())
// ########################
// # tweets #
// ########################
waitForKeyElements(`[data-testid=tweet] [href^="/"][href*="/photo/1"] [data-testid=tweetPhoto],
[data-testid=tweet] [data-testid=previewInterstitial]`, e => {
// showMediaWithContentWarnings
if (GM_getValue("opt_gt2").showMediaWithContentWarnings && GM_getValue("opt_gt2").showMediaWithContentWarningsSel < 7) {
let $tweet = $(e).closest("[data-testid=tweet]")
if ($(e).closest("[aria-labelledby]").find("> div > div > div > div:nth-child(2)").length) {
let id = $("body").is(".gt2-page-tweet")
? getPath().split("/")[2].split("?")[0].split("#")[0]
: $tweet.find("time").parent().attr("href").split("/status/")[1]
requestTweetCW(id, res => {
let score = res.extended_entities.media.filter(e => e.hasOwnProperty("sensitive_media_warning")).map(m => {
return ["adult_content", "graphic_violence", "other"].reduce((p, c, i) => {
return p + (m.sensitive_media_warning[c] ? Math.pow(2, i) : 0)
}, 0)
}).reduce((p, c) => p | c)
console.log(`cw id: ${id}, opt: ${GM_getValue("opt_gt2").showMediaWithContentWarningsSel} score: ${score}`)
if ((score & GM_getValue("opt_gt2").showMediaWithContentWarningsSel) == score) {
$tweet.attr("data-gt2-show-media", 1)
}
})
}
}
})
if (GM_getValue("opt_gt2").hideTweetAnalytics) {
waitForKeyElements(`[data-testid=tweet] [href$="/analytics"]`, e => e[0].parentElement.classList.add("gt2-hidden"))
}
// ########################
// # display settings #
// ########################
// high contrast
$("body").on("click", `[data-testid="accessibilityScreen"] > div:nth-child(3) label [aria-labelledby]`, function() {
GM_setValue("opt_display_highContrast", !$(this).find("input").is("[checked]"))
updateCSS()
})
// user color
waitForKeyElements(`body:not(.gt2-opt-color-override) [data-testid=SideNav_NewTweet_Button]`, e => {
let userColor = $(e).css("background-color")
if (userColor != GM_getValue("opt_display_userColor")) {
GM_setValue("opt_display_userColor", userColor)
updateCSS()
}
})
// background color
new MutationObserver(mut => {
mut.forEach(m => {
let bgColor = m.target[m.attributeName]["background-color"]
if (m.oldValue && bgColor != "") {
GM_setValue("opt_display_bgColor", bgColor)
updateCSS()
}
})
}).observe($("body")[0], {
attributes: true,
attributeOldValue: true,
attributeFilter: ["style"]
})
// font increment
new MutationObserver(mut => {
mut.forEach(m => {
let fs = m.target[m.attributeName]["font-size"]
let fsOld = m.oldValue?.match(/font-size: (\d+px);/)
if (fsOld && fs != "" && fs != fsOld[1]) {
GM_setValue("opt_display_fontSize", fs)
updateCSS()
}
})
}).observe($("html")[0], {
attributes: true,
attributeOldValue: true,
attributeFilter: ["style"]
})
// minimize DMDrawer if hideMessageBox is set
if (GM_getValue("opt_gt2").hideMessageBox) {
waitForKeyElements(`.gt2-opt-hide-message-box [data-testid=DMDrawer] path[d^="M12 19.344l-8.72"]`, e => {
console.log("Minimized DMDrawer")
$(e).parents("[role=button]").click()
})
}
// hide timeline follow suggestions
if (GM_getValue("opt_gt2").hideFollowSuggestions) {
function hideTLFS($p) {
if (!$p) return $p
if ($p.prev().length) {
$p = $p.prev()
if ($p.find("article").length) return
$p.addClass("gt2-hidden")
} else {
// if (window.scrollY < 500) return
setTimeout(() => {
$p = hideTLFS($p)
}, 100)
}
return $p
}
// big follow boxes
waitForKeyElements(
["topics/picker", "connect_people", "lists/suggested"]
.filter((e, i) => (GM_getValue("opt_gt2").hideFollowSuggestionsSel & Math.pow(2, i)) == Math.pow(2, i))
.map(e => `[data-testid=primaryColumn] section [href^="/i/${e}"]`)
.join(", "), e => {
let $p = $(e).parents("[data-testid=cellInnerDiv]").addClass("gt2-hidden")
if ($p.next().find("div > div:empty").length) $p.next().addClass("gt2-hidden")
for (let i=0; i < 6; i++) {
$p = hideTLFS($p)
}
})
}
// do not colorOverride these elements (reply/like/retweet/share on tweets and verified badge)
waitForKeyElements(`[data-testid=tweet] [role=group]`, e => $(e).find("[role=button] *").attr("data-gt2-color-override-ignore", ""))
waitForKeyElements(`path[d^="M22.5 12.5c0-1.58-.875"]`, e => $(e).parents("svg").attr("data-gt2-color-override-ignore", ""))
waitForKeyElements(`[data-gt2-path-modal="i/display"] div:nth-last-child(2) > div > [role=radiogroup],
[data-gt2-path="settings/display"] div:nth-last-child(2) > div > [role=radiogroup]`, e => {
let $e = $(e).parents("[aria-labelledby]")
$e.find("[name*=COLOR_PICKER]").parents("label").parent().find("*").attr("data-gt2-color-override-ignore", "")
$e.find("[dir]:nth-child(3) + div:not([dir]) > div > div > div[dir] + div *").attr("data-gt2-color-override-ignore", "")
})
// do not add dividers to tweet inline threads
waitForKeyElements(`[data-testid=cellInnerDiv] article,
[data-testid=cellInnerDiv] a[href^="/i/status/"]`, e => $(e).parents(`[data-testid=cellInnerDiv]`).children().attr("data-gt2-divider-add-ignore", ""))
// color notifications bell
waitForKeyElements(`path[d^="M23.61.15c-.375"]`, e => $(e).parents("[role=button]").attr("data-gt2-bell-full-color", ""))
waitForKeyElements(`path[d^="M23.24 3.26h-2.425V"]`, e => $(e).parents("[role=button]").removeAttr("data-gt2-bell-full-color", ""))
// ################
// # Update CSS #
// ################
// get scrollbar width (https://stackoverflow.com/q/8079187)
function getScrollbarWidth() {
if ($("html").is("[data-minimalscrollbar]")) {
return 0
}
let $t = $("
").css({
position: "absolute",
top: "-100px",
overflowX: "hidden",
overflowY: "scroll"
}).prependTo("body")
let out = $t[0].offsetWidth - $t[0].clientWidth
$t.remove()
return out
}
// update inserted CSS
function updateCSS() {
// bgColor schemes
let bgColors = {
// default (white)
"rgb(255, 255, 255)": {
bg: "#e6ecf0",
elem: "rgb(255, 255, 255)",
elemSel: "rgb(247, 249, 250)",
gray: "rgb(91, 112, 131)",
grayDark: "#e6ecf0",
grayDark2: "rgb(196, 207, 214)",
grayLight: "rgb(101, 119, 134)",
navbar: "#ffffff",
text: "rgb(20, 23, 26)",
text2: "white",
shadow: "rgba(101, 119, 134, 0.15)",
backdrop: "rgba(0, 0, 0, 0.4)"
},
// dim
"rgb(21, 32, 43)": {
bg: "#10171e",
elem: "rgb(21, 32, 43)",
elemSel: "rgb(25, 39, 52)",
gray: "rgb(101, 119, 134)",
grayDark: "#38444d",
grayDark2: "rgb(61, 84, 102)",
grayLight: "rgb(136, 153, 166)",
navbar: "#1c2938",
text: "rgb(255, 255, 255)",
text2: "white",
shadow: "rgba(136, 153, 166, 0.15)",
backdrop: "rgba(91, 112, 131, 0.4)"
},
// lightsOut
"rgb(0, 0, 0)": {
bg: "#000000",
elem: "#000000",
elemSel: "rgb(21, 24, 28)",
gray: "#657786",
grayDark: "#38444d",
grayDark2: "rgb(47, 51, 54)",
grayLight: "rgb(110, 118, 125)",
navbar: "rgb(21, 24, 28)",
text: "rgb(217, 217, 217)",
text2: "white",
shadow: "rgba(255, 255, 255, 0.15)",
backdrop: "rgba(91, 112, 131, 0.4)"
}
}
// high contrast color overrides
let bgColorsHC = {
// default (white)
"rgb(255, 255, 255)": {
gray: "rgb(59, 76, 92)",
grayDark: "rgb(170, 184, 194)",
grayLight: "rgb(59, 76, 92)",
text: "rgb(20, 29, 38)"
},
// dim
"rgb(21, 32, 43)": {
elemSel: "rgb(24, 36, 48)",
gray: "rgb(184, 203, 217)",
grayDark: "rgb(56, 68, 88)",
grayLight: "rgb(184, 203, 217)",
text2: "rgb(15, 20, 25)"
},
// lightsOut
"rgb(0, 0, 0)": {
bg: "rgb(5, 5, 5)",
elem: "rgb(5, 5, 5)",
elemSel: "rgb(14, 16, 18)",
gray: "rgb(146, 156, 166)",
grayDark: "rgb(61, 65, 69)",
grayLight: "rgb(146, 156, 166)",
text: "rgb(255, 255, 255)",
text2: "rgb(15, 20, 25)"
}
}
let baseColors = {
// normal white hc // dim/lo hc
blue: ["29, 161, 242", "38, 74, 157", "112, 200, 255"],
green: ["23, 191, 99", "9, 102, 51", "102, 211, 151"],
red: ["224, 36, 94", "159, 12, 58", "240, 152, 179"],
redDark: ["202, 32, 85", "169, 36, 78", "216, 137, 161"],
yellow: ["255, 173, 31", "121, 80, 11", "255, 203, 112"]
}
// initialize with the current settings
if (GM_getValue("gt2_initialized") == undefined && isLoggedIn()) {
waitForKeyElements(`h2 > a[href="/i/keyboard_shortcuts"] span`, () => {
GM_setValue("opt_display_userColor", $(`a[href="/i/keyboard_shortcuts"]`).css("color"))
GM_setValue("opt_display_bgColor", $("body").css("background-color"))
GM_setValue("opt_display_highContrast", false)
GM_setValue("opt_display_fontSize", $("html").css("font-size"))
GM_setValue("gt2_initialized", true)
window.location.reload()
})
} else {
// add gt2-options to body for the css to take effect
for (let [key, val] of Object.entries(GM_getValue("opt_gt2"))) {
if (val) $("body").addClass(`gt2-opt-${key.toKebab()}${typeof val === "number" ? `-${val}` : ""}`)
}
// remove unneeded classes
$("body").removeClass("gt2-acc-switcher-active")
// delete old stylesheet
if ($(".gt2-style").length) {
$(".gt2-style, .gt2-style-pickr").remove()
}
let opt_display_bgColor = GM_getValue("opt_display_bgColor")
let opt_display_highContrast = GM_getValue("opt_display_highContrast")
let opt_display_fontSize = GM_getValue("opt_display_fontSize")
let opt_display_userColor = GM_getValue("opt_display_userColor")
// options to set if not logged in
if (!isLoggedIn()) {
// get bgColor from cookie
opt_display_bgColor = document.cookie.match(/night_mode=1/)
? "rgb(21, 32, 43)"
: document.cookie.match(/night_mode=2/)
? "rgb(0, 0, 0)"
: "rgb(255, 255, 255)"
opt_display_highContrast = false
opt_display_fontSize = "15px"
opt_display_userColor = "rgb(29, 161, 242)"
}
// highContrast lightsOut
if (opt_display_bgColor == "rgb(5, 5, 5)") opt_display_bgColor = "rgb(0, 0, 0)"
// squareAvatars
if (GM_getValue("opt_gt2").disableHexagonAvatars) {
waitForKeyElements("#hex-hw-shapeclip-clipconfig path", e => $(e).parent().html(
GM_getValue("opt_gt2").squareAvatars
? ` `
: ` `
).attr("transform", "scale(0.005 0.005)"))
}
// insert new stylesheet
$("html").prepend(`
`
)
}
// add navbar
if (!$("gt2-nav").length) {
if (isLoggedIn()) {
addNavbar()
} else {
addNavbarLoggedOut()
}
}
}
// ##############
// # resizing #
// ##############
// things to do when resizing the window
$(window).on("resize", () => {
let w = window.innerWidth
if ((!GM_getValue("opt_gt2").smallSidebars && w <= 1350) ||
( GM_getValue("opt_gt2").smallSidebars && w <= 1230)) {
// move dash profile to right sidebar
$(".gt2-left-sidebar > *").each(function() {
$(this).attr("data-gt2-detached-from-left-sidebar", 1)
.detach().insertBefore("div[data-testid=sidebarColumn] > div > div:nth-child(2) > div > div > div > :last-child")
})
} else {
$("[data-gt2-detached-from-left-sidebar]").each(function() {
$(this).removeAttr("data-gt2-detached-from-left-sidebar")
.detach().appendTo(".gt2-left-sidebar")
})
}
})
// ###############
// # scrolling #
// ###############
// things to do when scrolling
;(function() {
let prev = window.pageYOffset
let bannerHeight = (window.innerWidth - getScrollbarWidth()) / 3 - 15
$(window).on("scroll", () => {
let curr = window.pageYOffset
// prevent auto scroll to top on /search and /explore
if (prev > 1500 && curr == 0 && getPath().match(/^(?:search\?|explore\/?$)/)) {
window.scroll(0, prev)
return
}
if (prev < curr) {
$("body").addClass("gt2-scrolled-down")
} else {
$("body").removeClass("gt2-scrolled-down")
}
prev = curr
// legacy profile banner parallax
if (curr > bannerHeight) {
$("body").addClass("gt2-scrolled-down-banner")
} else {
$("body").removeClass("gt2-scrolled-down-banner")
$(".gt2-legacy-profile-banner img").css("transform", `translate3d(0px, ${curr / bannerHeight * 42}%, 0px)`)
}
})
}())
// ################
// # URL change #
// ################
function beforeUrlChange(path) {
path = path.split("?")[0].split("#")[0]
// [LPL] reattach buttons to original position
if (!_isModal(path)) {
let $b = $("div[data-testid=primaryColumn] > div > div:nth-last-child(1) > div > div > div:nth-child(1) > div:nth-child(2) > div:nth-child(1)")
if (!$b.find("> div:last-child:not(:first-child)").length && $("body").attr("data-gt2-prev-path") != path) {
$(".gt2-legacy-profile-nav-right > div").appendTo($b)
}
}
}
// path helper functions
function _onPage(path, ...top) {
return top.some(e => e == path.split("/")[0])
}
function _onSubPage(path, top, sub) {
return (top == null ? true : _onPage(path, top)) && path.includes("/") && sub.some(e => e == (e.includes("/") ? path.split("/").slice(1).join("/") : path.split("/")[1]))
}
function _isModal(path) {
return _onSubPage(path, "i", ["display", "keyboard_shortcuts", "flow", "lists/add_member", "report"])
|| _onSubPage(path, "settings", ["trends", "profile"])
|| _onSubPage(path, "compose", ["tweet"])
|| _onSubPage(path, "account", ["add", "switch"])
|| _onPage(path, "search-advanced")
|| _onPage(path, "intent")
|| path.match(/\/(photo|video)\/\d\/?$/)
}
// stuff to do when url changes
function urlChange(changeType, changePath) {
let path = () => (changePath || getPath()).split("?")[0].split("#")[0]
let onPage = (...top) => _onPage(path(), ...top)
let onSubPage = (top, sub) => _onSubPage(path(), top, sub)
let isModal = _isModal(path())
console.log(`[${changeType}]${isModal ? " [modal]" : ""} ${path()}`)
$("body").attr(`data-gt2-path${isModal ? "-modal" : ""}`, path())
let $realPath = $("link[hreflang=default][data-rh=true]")
if ($realPath.length) $("body").attr("data-gt2-path", $realPath.attr("href"))
// do a reload on these pages
if (onPage("login") || (!isLoggedIn() && onPage(""))) {
window.location.reload()
}
// update css
if (!$("body").hasClass("gt2-css-inserted")) {
updateCSS()
$("body").addClass("gt2-css-inserted")
}
let mainView = "main > div > div > div"
waitForKeyElements(mainView, () => {
// insert left sidebar
if (!$(".gt2-left-sidebar").length) {
$(mainView).prepend(``)
}
// on error page
if ($(mainView).find("h1[data-testid=error-detail]").length && !path().startsWith("settings/gt2")) {
$("body").addClass("gt2-page-error")
} else if (!isModal) {
$("body").removeClass("gt2-page-error")
}
if (onPage("settings")) {
waitForKeyElements(`main a[href="/settings/about"]`, addSettingsToggle)
if (path().startsWith("settings/gt2")) {
addSettings()
changeSettingsTitle()
}
}
})
// add navbar
if ($("body").attr("data-gt2-prev-path") == "i/moment_maker") $(".gt2-nav").remove()
if (!$(".gt2-nav").length) {
if (isLoggedIn()) {
addNavbar()
} else {
addNavbarLoggedOut()
}
}
// highlight current location in left bar
if (!isModal) {
$(`.gt2-nav-left > a`).removeClass("active")
$(`.gt2-nav-left > a[href^='/${path().split("/")[0]}']`).addClass("active")
}
// hide/add search
if (onPage("search", "explore")) {
$(".gt2-search").empty()
$("body").removeClass("gt2-search-added")
} else if (!isModal) {
addSearch()
}
if (!isLoggedIn()) {
$("body").addClass("gt2-not-logged-in")
}
// handle stuff in sidebars
handleTrends()
if (GM_getValue("opt_gt2").hideFollowSuggestions && (GM_getValue("opt_gt2").hideFollowSuggestionsLocSel & 2) == 2) {
let sel = GM_getValue("opt_gt2").hideFollowSuggestionsSel
// topic suggestions
if ((sel & 1) == 1) waitForKeyElements(`div[data-testid=sidebarColumn] section [href^="/i/topics/"]`, e => $(e).parents("section").parent().parent().remove())
// user suggestions (Who to follow, You might like)
if ((sel & 2) == 2) waitForKeyElements(`div[data-testid=sidebarColumn] aside [data-testid=UserCell]`, e => $(e).parents("aside").parent().remove())
}
// settings
if (onPage("settings") && !isModal) {
if (path().startsWith("settings/gt2")) {
} else {
if (window.innerWidth < 1005) {
$("main section").remove()
}
$(".gt2-settings-header, .gt2-settings").remove()
}
} else if (!isModal) {
$(".gt2-settings-header, .gt2-settings").remove()
}
// tweet
if (onSubPage(null, ["status"]) || path().startsWith("i/web/status/")) {
$("body").addClass("gt2-page-tweet")
// add source
let m = location.pathname.match(/\/status\/(\d+)/)
if (m) {
requestTweet(m[1], res => {
waitForKeyElements(`[data-testid=tweet][tabindex="-1"] [href*="${m[1]}"] time`, e => {
// scroll up on load
window.scroll(0, window.pageYOffset - 75)
if (GM_getValue("opt_gt2").hideTweetAnalytics) {
e[0].parentElement.parentElement.querySelectorAll(":scope > span").forEach(e => e.classList.add("gt2-hidden"))
}
if (!res.source)
return
e[0].parentElement.insertAdjacentHTML("afterend", ``)
})
})
}
} else if (!isModal) {
$("body").removeClass("gt2-page-tweet")
}
// sidebar
let sidebarContent = []
// update changelog
if (!GM_getValue(`sb_notice_ack_update_${GM_info.script.version}`)
&& GM_getValue("opt_gt2").updateNotifications) {
sidebarContent.push(getUpdateNotice())
}
sidebarContent.push(getDashboardProfile())
// assume profile page
if (!isModal || onSubPage("intent", ["user", "follow"])) {
if (!(onPage("", "explore", "home", "hashtag", "i", "messages", "notifications", "places", "search", "settings", "404")
|| onSubPage(null, ["communities", "followers", "followers_you_follow", "following", "lists", "moments", "status", "topics"]))
|| onSubPage("intent", ["user", "follow"])) {
$("body").addClass("gt2-page-profile").removeClass("gt2-profile-not-found gt2-page-profile-youre-blocked")
$("[class^=gt2-blocked-profile-]").remove()
$(".gt2-tco-expanded").removeClass("gt2-tco-expanded")
if (GM_getValue("opt_gt2").legacyProfile) {
if ($("body").attr("data-gt2-prev-path") != path()) {
$("a[href$='/photo'] img").data("alreadyFound", false)
}
rebuildLegacyProfile()
}
// redirect to /media on profiles (without /intent)
if (GM_getValue("opt_gt2").profileMediaRedirect && path().split("/").length == 1 && (!document.body.dataset.hasOwnProperty("gt2PrevPath") || document.body.dataset.gt2PrevPath.split("/")[0] != path().split("/")[0])) {
waitForKeyElements(`[href$="/media"][aria-selected=false]`, e => e[0].click())
console.log("redirecting to /media")
}
// move left media
if (GM_getValue("opt_gt2").leftMedia
&& ((!GM_getValue("opt_gt2").smallSidebars && window.innerWidth > 1350)
|| (GM_getValue("opt_gt2").smallSidebars && window.innerWidth > 1230))) {
waitForKeyElements("[data-testid=sidebarColumn] a:nth-child(1) [data-testid=tweetPhoto]", e => {
if ($(".gt2-profile-media").length) $(".gt2-profile-media").remove()
let $mediaContainer = $(e).parents("a[role=link]").parent().parent().parent().parent().parent()
if ($mediaContainer.parent().children().length == 1) $mediaContainer = $mediaContainer.parent()
$mediaContainer.detach().addClass("gt2-profile-media")
.appendTo(".gt2-left-sidebar")
})
}
} else {
$("body").removeClass("gt2-page-profile")
$(`.gt2-legacy-profile-banner,
.gt2-legacy-profile-nav,
.gt2-legacy-profile-info`).remove()
}
}
// add elements to sidebar
addToSidebar(sidebarContent)
// own account is blocked by profile page
waitForKeyElements(`div[data-testid=placementTracking] div[data-testid$="-unblock"],
[data-testid=emptyState] [href="https://support.twitter.com/articles/20172060"]`, displayBlockedProfileData)
// home page
if (path().split("/")[0] == "home") {
if (GM_getValue("opt_gt2").forceLatest)
forceLatest()
} else {
document.body.removeAttribute("data-switched-to-latest")
}
if (!isModal) $("body").attr("data-gt2-prev-path", path())
}
urlChange("init")
// run urlChange() when history changes
// https://github.com/Bl4Cc4t/GoodTwitter2/issues/96
const exportFunc = typeof exportFunction === "function" ? exportFunction : (fn => fn)
const pageWindow = unsafeWindow.wrappedJSObject || unsafeWindow
const pageHistory = pageWindow.History.prototype
const origPush = exportFunc(pageHistory.pushState, pageWindow)
pageHistory.pushState = exportFunc(function () {
let path = arguments.length > 2 ? arguments[2].slice(1) : "???"
beforeUrlChange(path)
origPush.apply(this, arguments)
urlChange("push", path)
}, pageWindow)
const origRepl = exportFunc(pageHistory.replaceState, pageWindow)
pageHistory.replaceState = exportFunc(function () {
let path = arguments.length > 2 ? arguments[2].slice(1) : "???"
beforeUrlChange(path)
origRepl.apply(this, arguments)
urlChange("replace", path)
}, pageWindow)
window.addEventListener("popstate", function() {
beforeUrlChange(getPath())
urlChange("pop", getPath())
})
// remove "t" search parameter (probably used for tracking?)
// https://twitter.com/Outrojules/status/1543220843995619328?s=20&t=fCFEatQ_iAtlyiHQCWCxoQ
let _selectNodeContents = Range.prototype.selectNodeContents
Range.prototype.selectNodeContents = function() {
arguments[0].textContent = arguments[0].textContent.replace(/&t=.*$/, "")
_selectNodeContents.apply(this, arguments)
}
})(jQuery, waitForKeyElements)