Tweak New Twitter

Stay on the Latest Tweets timeline, reduce "engagement" and tone down some of Twitter's UI

当前为 2020-07-17 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Tweak New Twitter
  3. // @description Stay on the Latest Tweets timeline, reduce "engagement" and tone down some of Twitter's UI
  4. // @namespace https://github.com/insin/tweak-new-twitter/
  5. // @match https://twitter.com/*
  6. // @match https://mobile.twitter.com/*
  7. // @version 26
  8. // ==/UserScript==
  9.  
  10. //#region Config & variables
  11. /**
  12. * Default config enables all features.
  13. *
  14. * You'll need to edit the config object manually for now if you're using this
  15. * as a user script.
  16. */
  17. let config = {
  18. alwaysUseLatestTweets: true,
  19. fastBlock: true,
  20. hideAccountSwitcher: true,
  21. hideBookmarksNav: true,
  22. hideExploreNav: true,
  23. hideListsNav: true,
  24. hideMessagesDrawer: true,
  25. hideMoreTweets: true,
  26. hideSidebarContent: true,
  27. hideWhoToFollowEtc: true,
  28. navBaseFontSize: true,
  29. /** @type {'separate'|'hide'|'ignore'} */
  30. retweets: ('separate'),
  31. /** @type {'highlight'|'hide'|'ignore'} */
  32. verifiedAccounts: ('ignore'),
  33. }
  34.  
  35. config.enableDebugLogging = false
  36.  
  37. const HOME = 'Home'
  38. const LATEST_TWEETS = 'Latest Tweets'
  39. const MESSAGES = 'Messages'
  40. const TIMELINE_RETWEETS = 'Timeline Retweets'
  41.  
  42. const PROFILE_TITLE_RE = /\(@[a-z\d_]{1,15}\)$/i
  43. const TITLE_NOTIFICATION_RE = /^\(\d+\+?\) /
  44. const URL_TWEET_ID_RE = /\/status\/(\d+)$/
  45. const URL_PHOTO_RE = /photo\/\d$/
  46.  
  47. let Selectors = {
  48. ACCOUNT_SWITCHER: 'div[data-testid="SideNav_AccountSwitcher_Button"]',
  49. MESSAGES_DRAWER: 'div[data-testid="DMDrawer"]',
  50. NAV_HOME_LINK: 'a[data-testid="AppTabBar_Home_Link"]',
  51. PRIMARY_COLUMN: 'div[data-testid="primaryColumn"]',
  52. PRIMARY_NAV: 'nav[aria-label="Primary"]',
  53. PROMOTED_TWEET: '[data-testid="placementTracking"]',
  54. SIDEBAR_COLUMN: 'div[data-testid="sidebarColumn"]',
  55. TIMELINE_HEADING: 'h2[role="heading"]',
  56. TWEET: 'div[data-testid="tweet"]',
  57. VERIFIED_TICK: 'svg[aria-label="Verified account"]',
  58. }
  59.  
  60. Object.assign(Selectors, {
  61. SIDEBAR_FOOTER: `${Selectors.SIDEBAR_COLUMN} nav`,
  62. SIDEBAR_PEOPLE: `${Selectors.SIDEBAR_COLUMN} aside`,
  63. SIDEBAR_TRENDS: `${Selectors.SIDEBAR_COLUMN} section`,
  64. TIMELINE: `${Selectors.PRIMARY_COLUMN} section > h1 + div[aria-label] > div`,
  65. })
  66.  
  67. /** Title of the current page, without the ' / Twitter' suffix */
  68. let currentPage = ''
  69.  
  70. /** Notification count in the title (including trailing space), e.g. '(1) ' */
  71. let currentNotificationCount = ''
  72.  
  73. /** Current URL path */
  74. let currentPath = ''
  75.  
  76. /** Flag for a Home / Latest Tweets link having been clicked */
  77. let homeLinkClicked = false
  78.  
  79. /**
  80. * MutationObservers active on the current page
  81. * @type MutationObserver[]
  82. */
  83. let pageObservers = []
  84. //#endregion
  85.  
  86. //#region Utility functions
  87. function addStyle(css) {
  88. let $style = document.createElement('style')
  89. $style.dataset.insertedBy = 'tweak-new-twitter'
  90. $style.textContent = css
  91. document.head.appendChild($style)
  92. return $style
  93. }
  94.  
  95. /**
  96. * @returns {Promise<HTMLElement>}
  97. */
  98. function getElement(selector, {
  99. name = null,
  100. stopIf = null,
  101. target = document,
  102. timeout = Infinity,
  103. } = {}) {
  104. return new Promise((resolve) => {
  105. let rafId
  106. let timeoutId
  107.  
  108. function stop($element, reason) {
  109. if ($element == null) {
  110. log(`stopped waiting for ${name || selector} after ${reason}`)
  111. }
  112. if (rafId) {
  113. cancelAnimationFrame(rafId)
  114. }
  115. if (timeoutId) {
  116. clearTimeout(timeoutId)
  117. }
  118. resolve($element)
  119. }
  120.  
  121. if (timeout !== Infinity) {
  122. timeoutId = setTimeout(stop, timeout, null, `${timeout}ms timeout`)
  123. }
  124.  
  125. function queryElement() {
  126. let $element = target.querySelector(selector)
  127. if ($element) {
  128. stop($element)
  129. }
  130. else if (stopIf != null && stopIf() === true) {
  131. stop(null, 'stopIf condition met')
  132. }
  133. else {
  134. rafId = requestAnimationFrame(queryElement)
  135. }
  136. }
  137.  
  138. queryElement()
  139. })
  140. }
  141.  
  142. function log(...args) {
  143. if (config.enableDebugLogging) {
  144. console.log(`TWT${currentPage ? `(${currentPage})` : ''}`, ...args)
  145. }
  146. }
  147.  
  148. /**
  149. * Convenience wrapper for the MutationObserver API.
  150. *
  151. * The listener is called immediately to support using an observer and its
  152. * options as a trigger for any change, without looking at MutationRecords.
  153. *
  154. * @param {Node} $element
  155. * @param {MutationCallback} callback
  156. * @param {MutationObserverInit} options
  157. */
  158. function observeElement($element, callback, options = {childList: true}) {
  159. let observer = new MutationObserver(callback)
  160. callback([], observer)
  161. observer.observe($element, options)
  162. return observer
  163. }
  164.  
  165. function pageIsNot(page) {
  166. return () => page != currentPage
  167. }
  168.  
  169. function pathIsNot(path) {
  170. return () => path != currentPath
  171. }
  172.  
  173. function s(n) {
  174. return n == 1 ? '' : 's'
  175. }
  176. //#endregion
  177.  
  178. //#region Global observers
  179. function observeHtmlFontSize() {
  180. let $html = document.querySelector('html')
  181. let $style = addStyle('')
  182. let lastFontSize = ''
  183.  
  184. log('observing html style attribute for font-size changes')
  185. let observer = observeElement($html, () => {
  186. if ($html.style.fontSize != lastFontSize) {
  187. lastFontSize = $html.style.fontSize
  188. log(`setting nav font size to ${lastFontSize}`)
  189. $style.textContent = [
  190. `${Selectors.PRIMARY_NAV} div[dir="auto"] span { font-size: ${lastFontSize}; font-weight: normal; }`,
  191. `${Selectors.PRIMARY_NAV} div[dir="auto"] { margin-top: -4px; }`
  192. ].join('\n')
  193. }
  194. }, {
  195. attributes: true,
  196. attributeFilter: ['style']
  197. })
  198.  
  199. return {
  200. disconnect() {
  201. $style.remove()
  202. observer.disconnect()
  203. }
  204. }
  205. }
  206.  
  207. async function observeTitle() {
  208. let $title = await getElement('title', {name: '<title>'})
  209. log('observing <title>')
  210. return observeElement($title, () => onTitleChange($title.textContent), {
  211. childList: true,
  212. })
  213. }
  214.  
  215. async function observePopups() {
  216. let $keyboardWrapper = await getElement('[data-at-shortcutkeys]', {
  217. name: 'keyboard wrapper',
  218. })
  219. log('observing popups')
  220. observeElement($keyboardWrapper.previousElementSibling, (mutations) => {
  221. mutations.forEach((mutation) => {
  222. // The first popup takes another tick to render content
  223. mutation.addedNodes.forEach($el => requestAnimationFrame(() => onPopup($el)))
  224. })
  225. })
  226. }
  227. //#endregion
  228.  
  229. //#region Page observers
  230. async function observeSidebarAppearance(page) {
  231. let $primaryColumn = await getElement(Selectors.PRIMARY_COLUMN, {
  232. name: 'primary column',
  233. stopIf: pageIsNot(page),
  234. })
  235. log('observing responsive sidebar')
  236. pageObservers.push(
  237. observeElement($primaryColumn.parentNode, (mutations) => {
  238. mutations.forEach((mutation) => {
  239. mutation.addedNodes.forEach((el) => {
  240. if (/** @type {HTMLElement} */ (el).dataset.testid == 'sidebarColumn') {
  241. log('sidebar appeared')
  242. hideSidebarContents(page)
  243. }
  244. })
  245. })
  246. })
  247. )
  248. }
  249.  
  250. async function observeTimeline(page) {
  251. let $timeline = await getElement(Selectors.TIMELINE, {
  252. name: 'timeline',
  253. stopIf: pageIsNot(page),
  254. })
  255. if ($timeline == null) {
  256. return
  257. }
  258.  
  259. // On 2020-04-03 Twitter switched to a new way of rendering the timeline which replaces an initial
  260. // container with the real element which holds timeline tweets and reduces the number of elements
  261. // wrapping the timeline.
  262. //
  263. // v1.9 was released to handle this.
  264. //
  265. // On 2020-04-05 they switched back to the old method.
  266. //
  267. // This attempts to support both approaches in case they keeping switching between the two.
  268.  
  269. // The "new" inital timeline element is a placeholder which doesn't have a style attribute
  270. // The "old" timeline has 2 wrapper divs which apply padding via the DOM .style object
  271. if ($timeline.hasAttribute('style')) {
  272. // The "old" timeline is nested one level deeper and the initial container has padding-bottom
  273. // <div aria-label="Timeline: Your Home Timeline">
  274. // <div style="padding-bottom: 0px"> <!-- current $timeline -->
  275. // <div style="padding-top: ...px; padding-bottom: ...px"> <!-- we want to observe this -->
  276. // <div> <!-- tweet elements are at this level -->
  277. // ...
  278. if ($timeline.style.paddingBottom) {
  279. $timeline = /** @type {HTMLElement} */ ($timeline.firstElementChild)
  280. log('observing "old" timeline', {$timeline})
  281. }
  282. else {
  283. log('observing "new" timeline', {$timeline})
  284. }
  285. pageObservers.push(
  286. observeElement($timeline, () => onTimelineChange($timeline, page))
  287. )
  288. }
  289. else {
  290. log('waiting for real "new" timeline')
  291. pageObservers.push(
  292. observeElement($timeline.parentNode, (mutations) => {
  293. mutations.forEach((mutation) => {
  294. mutation.addedNodes.forEach(($timeline) => {
  295. log('observing "new" timeline', {$timeline})
  296. pageObservers.push(
  297. observeElement($timeline, () => onTimelineChange($timeline, page))
  298. )
  299. })
  300. })
  301. })
  302. )
  303. }
  304. }
  305. //#endregion
  306.  
  307. //#region Tweak functions
  308. async function addRetweetsHeader(page) {
  309. let $timelineTitle = await getElement('main h2', {
  310. name: 'timeline title',
  311. stopIf: pageIsNot(page),
  312. })
  313. if ($timelineTitle != null &&
  314. document.querySelector('#twt_retweets') == null) {
  315. log('inserting Retweets header')
  316. let div = document.createElement('div')
  317. div.innerHTML = $timelineTitle.parentElement.outerHTML
  318. let $retweets = div.firstElementChild
  319. $retweets.querySelector('h2').id = 'twt_retweets'
  320. $retweets.querySelector('span').textContent = 'Retweets'
  321. // This script assumes navigation has occurred when the document title changes,
  322. // so by changing the title to "Retweets" we effectively fake navigation to a
  323. // non-existent Retweets page.
  324. $retweets.addEventListener('click', () => {
  325. if (!document.title.startsWith(TIMELINE_RETWEETS)) {
  326. setTitle(TIMELINE_RETWEETS)
  327. }
  328. window.scrollTo({top: 0})
  329. })
  330. $timelineTitle.parentElement.parentElement.insertAdjacentElement('afterend', $retweets)
  331. // Go back to the main timeline from Retweets when the Latest Tweets / Home heading is clicked
  332. $timelineTitle.parentElement.addEventListener('click', () => {
  333. if (!document.title.startsWith(page)) {
  334. homeLinkClicked = true
  335. setTitle(page)
  336. }
  337. })
  338. // Go back to the main timeline from Retweets when the Home nav link is clicked
  339. document.querySelector(Selectors.NAV_HOME_LINK).addEventListener('click', () => {
  340. homeLinkClicked = true
  341. if (location.pathname == '/home' && !document.title.startsWith(page)) {
  342. setTitle(page)
  343. }
  344. })
  345. }
  346. }
  347.  
  348. function addStaticCss() {
  349. var cssRules = []
  350. var hideCssSelectors = []
  351. if (config.hideSidebarContent) {
  352. hideCssSelectors.push(
  353. Selectors.SIDEBAR_TRENDS,
  354. Selectors.SIDEBAR_PEOPLE,
  355. Selectors.SIDEBAR_FOOTER
  356. )
  357. }
  358. if (config.hideExploreNav) {
  359. hideCssSelectors.push(`${Selectors.PRIMARY_NAV} a[href="/explore"]`)
  360. }
  361. if (config.hideBookmarksNav) {
  362. hideCssSelectors.push(`${Selectors.PRIMARY_NAV} a[href="/i/bookmarks"]`)
  363. }
  364. if (config.hideListsNav) {
  365. hideCssSelectors.push(`${Selectors.PRIMARY_NAV} a[href*="/lists"]`)
  366. }
  367. if (config.hideAccountSwitcher) {
  368. hideCssSelectors.push(Selectors.ACCOUNT_SWITCHER)
  369. }
  370. if (config.hideMessagesDrawer) {
  371. hideCssSelectors.push(Selectors.MESSAGES_DRAWER)
  372. }
  373. if (hideCssSelectors.length > 0) {
  374. cssRules.push(`${hideCssSelectors.join(', ')} { display: none !important; }`)
  375. }
  376. if (cssRules.length > 0) {
  377. addStyle(cssRules.join('\n'))
  378. }
  379. }
  380.  
  381. function getTweetType($tweet) {
  382. if ($tweet.closest(Selectors.PROMOTED_TWEET)) {
  383. return 'PROMOTED_TWEET'
  384. }
  385. if ($tweet.previousElementSibling != null &&
  386. $tweet.previousElementSibling.textContent.includes('Retweeted')) {
  387. return 'RETWEET'
  388. }
  389. return 'TWEET'
  390. }
  391.  
  392. async function hideSidebarContents(page) {
  393. let trends = getElement(Selectors.SIDEBAR_TRENDS, {
  394. name: 'sidebar trends',
  395. stopIf: pageIsNot(page),
  396. timeout: 4000,
  397. }).then(($trends) => {
  398. if ($trends == null) {
  399. return false
  400. }
  401. let $trendsModule = $trends.parentElement.parentElement.parentElement
  402. $trendsModule.style.display = 'none'
  403. // Hide surrounding elements which draw separators between modules
  404. if ($trendsModule.previousElementSibling &&
  405. $trendsModule.previousElementSibling.childElementCount == 0) {
  406. /** @type {HTMLElement} */ ($trendsModule.previousElementSibling).style.display = 'none'
  407. }
  408. if ($trendsModule.nextElementSibling &&
  409. $trendsModule.nextElementSibling.childElementCount == 0) {
  410. /** @type {HTMLElement} */ ($trendsModule.nextElementSibling).style.display = 'none'
  411. }
  412. return true
  413. })
  414.  
  415. let people = getElement(Selectors.SIDEBAR_PEOPLE, {
  416. name: 'sidebar people',
  417. stopIf: pageIsNot(page),
  418. timeout: 4000,
  419. }).then(($people) => {
  420. if ($people == null) {
  421. return false
  422. }
  423. let $peopleModule
  424. if ($people.getAttribute('aria-label') == 'Relevant people') {
  425. // "Relevant people" section when viewing a Tweet/thread
  426. $peopleModule = $people.parentElement
  427. }
  428. else {
  429. // "Who to follow" section
  430. $peopleModule = $people.parentElement
  431. }
  432. $peopleModule.style.display = 'none'
  433. return true
  434. })
  435.  
  436. let [hidTrends, hidPeople] = await Promise.all([trends, people])
  437. log(hidTrends == true && hidPeople == true
  438. ? 'hid all sidebar content'
  439. : 'stopped waiting for sidebar content')
  440. }
  441.  
  442. function onPopup($topLevelElement) {
  443. // Block button
  444. let $confirmButton = $topLevelElement.querySelector('div[data-testid="confirmationSheetConfirm"]')
  445. if ($confirmButton && $confirmButton.innerText == 'Block') {
  446. if (config.fastBlock) {
  447. log('Fast blocking')
  448. $confirmButton.click()
  449. }
  450. return
  451. }
  452. }
  453.  
  454. /** @typedef {'TWEET'|'RETWEET'|'PROMOTED_TWEET'|'HEADING'} TimelineItemType */
  455.  
  456. function onTimelineChange($timeline, page) {
  457. log(`processing ${$timeline.children.length} timeline item${s($timeline.children.length)}`)
  458. /** @type {HTMLElement} */
  459. let $previousItem = null
  460. /** @type {?TimelineItemType} */
  461. let previousTimelineItemType = null
  462. for (let $item of $timeline.children) {
  463. /** @type {?TimelineItemType} */
  464. let timelineItemType = null
  465. let hideItem = null
  466. let $tweet = $item.querySelector(Selectors.TWEET)
  467.  
  468. if ($tweet != null) {
  469. timelineItemType = getTweetType($tweet)
  470. if (page == LATEST_TWEETS || page == TIMELINE_RETWEETS || page == HOME) {
  471. hideItem = shouldHideTweet(timelineItemType, page)
  472. }
  473. }
  474.  
  475. if (timelineItemType == null && config.hideWhoToFollowEtc) {
  476. // "Who to follow", "Follow some Topics" etc. headings
  477. if ($item.querySelector(Selectors.TIMELINE_HEADING)) {
  478. timelineItemType = 'HEADING'
  479. hideItem = true
  480. // Also hide the divider above the heading
  481. if ($previousItem && $previousItem.innerText == '') {
  482. /** @type {HTMLElement} */ ($previousItem.firstElementChild).style.display = 'none'
  483. }
  484. }
  485. }
  486.  
  487. if (timelineItemType == null) {
  488. // Assume a non-identified item following an identified item is related to it
  489. // "Who to follow" users and "Follow some Topics" topics appear in subsequent items
  490. // "Show this thread" and "Show more" links appear in subsequent items
  491. if (previousTimelineItemType != null) {
  492. hideItem = previousTimelineItemType == 'HEADING' || shouldHideTweet(previousTimelineItemType, page)
  493. }
  494. // The first item in the timeline is sometimes an empty placeholder <div>
  495. else if ($item !== $timeline.firstElementChild) {
  496. // We're probably also missing some spacer / divider nodes
  497. log('unhandled timeline item', $item)
  498. }
  499. }
  500.  
  501. if (hideItem !== true &&
  502. config.verifiedAccounts === 'hide' &&
  503. $item.querySelector(Selectors.VERIFIED_TICK)) {
  504. hideItem = true
  505. }
  506.  
  507. if (hideItem != null) {
  508. /** @type {HTMLElement} */ ($item.firstElementChild).style.display = hideItem ? 'none' : ''
  509. // Log these out as they can't be reliably triggered for testing
  510. if (timelineItemType == 'HEADING' || previousTimelineItemType == 'HEADING') {
  511. log(`hid a ${previousTimelineItemType == 'HEADING' ? 'post-' : ''}heading item`, $item)
  512. }
  513. }
  514.  
  515. if (hideItem !== true &&
  516. config.verifiedAccounts === 'highlight' &&
  517. $item.querySelector(Selectors.VERIFIED_TICK)) {
  518. $item.style.backgroundColor = 'rgba(29, 161, 242, 0.25)'
  519. }
  520.  
  521. $previousItem = $item
  522. // If we hid a heading, keep hiding everything after it until we hit a tweet
  523. if (!(previousTimelineItemType == 'HEADING' && timelineItemType == null)) {
  524. previousTimelineItemType = timelineItemType
  525. }
  526. }
  527. }
  528.  
  529. function onTitleChange(title) {
  530. // Ignore any leading notification counts in titles, e.g. '(1) Latest Tweets / Twitter'
  531. let notificationCount = ''
  532. if (TITLE_NOTIFICATION_RE.test(title)) {
  533. notificationCount = TITLE_NOTIFICATION_RE.exec(title)[0]
  534. title = title.replace(TITLE_NOTIFICATION_RE, '')
  535. }
  536.  
  537. let homeLinkWasClicked = homeLinkClicked
  538. homeLinkClicked = false
  539.  
  540. // Ignore Flash of Uninitialised Title when navigating to a screen for the
  541. // first time.
  542. if (title == 'Twitter') {
  543. log('ignoring Flash of Uninitialised Title')
  544. return
  545. }
  546.  
  547. // Only allow the same page to re-process if the "Customize your view" dialog
  548. // is currently open.
  549. let newPage = title.split(' / ')[0]
  550. if (newPage == currentPage && location.pathname != '/i/display') {
  551. log('ignoring duplicate title change')
  552. currentNotificationCount = notificationCount
  553. return
  554. }
  555.  
  556. // Stay on the Retweets timeline when…
  557. if (currentPage == TIMELINE_RETWEETS &&
  558. // …the title has changed back to the main timeline…
  559. (newPage == LATEST_TWEETS || newPage == HOME) &&
  560. // …the Home nav or Latest Tweets / Home header _wasn't_ clicked and…
  561. !homeLinkWasClicked &&
  562. (
  563. // …the user viewed a photo.
  564. URL_PHOTO_RE.test(location.pathname) ||
  565. // …the user stopped viewing a photo.
  566. URL_PHOTO_RE.test(currentPath) ||
  567. // …the user opened or used the "Customize your view" dialog.
  568. location.pathname == '/i/display' ||
  569. // …the user closed the "Customize your view" dialog.
  570. currentPath == '/i/display' ||
  571. // …the user opened the "Send via Direct Message" dialog.
  572. location.pathname == '/messages/compose' ||
  573. // …the user closed the "Send via Direct Message" dialog.
  574. currentPath == '/messages/compose' ||
  575. // …the user opened the compose Tweet dialog.
  576. location.pathname == '/compose/tweet' ||
  577. // …the user closed the compose Tweet dialog.
  578. currentPath == '/compose/tweet' ||
  579. // …the notification count in the title changed.
  580. notificationCount != currentNotificationCount
  581. )) {
  582. log('ignoring title change on Retweets timeline')
  583. currentNotificationCount = notificationCount
  584. currentPath = location.pathname
  585. setTitle(TIMELINE_RETWEETS)
  586. return
  587. }
  588.  
  589. // Assumption: all non-FOUT, non-duplicate title changes are navigation, which
  590. // need the screen to be re-processed.
  591.  
  592. if (pageObservers.length > 0) {
  593. log(`disconnecting ${pageObservers.length} page observer${s(pageObservers.length)}`)
  594. pageObservers.forEach(observer => observer.disconnect())
  595. pageObservers = []
  596. }
  597.  
  598. currentPage = newPage
  599. currentNotificationCount = notificationCount
  600. currentPath = location.pathname
  601.  
  602. log('processing new page')
  603.  
  604. if (config.alwaysUseLatestTweets && currentPage == HOME) {
  605. return switchToLatestTweets(currentPage)
  606. }
  607.  
  608. if (config.retweets == 'separate') {
  609. document.body.classList.toggle('Home', currentPage == HOME)
  610. document.body.classList.toggle('LatestTweets', currentPage == LATEST_TWEETS)
  611. document.body.classList.toggle('TimelineRetweets', currentPage == TIMELINE_RETWEETS)
  612. updateThemeColor()
  613. }
  614.  
  615. if (config.retweets == 'separate' && (currentPage == LATEST_TWEETS || currentPage == TIMELINE_RETWEETS || currentPage == HOME)) {
  616. addRetweetsHeader(currentPage)
  617. }
  618.  
  619. if ((config.retweets != 'ignore' || config.verifiedAccounts != 'ignore' || config.hideWhoToFollowEtc) && (currentPage == LATEST_TWEETS || currentPage == TIMELINE_RETWEETS || currentPage == HOME) ||
  620. (config.verifiedAccounts != 'ignore' || config.hideWhoToFollowEtc) && PROFILE_TITLE_RE.test(currentPage)) {
  621. observeTimeline(currentPage)
  622. }
  623.  
  624. if (config.hideSidebarContent && currentPage != MESSAGES) {
  625. hideSidebarContents(currentPage)
  626. observeSidebarAppearance(currentPage)
  627. }
  628.  
  629. if (config.hideMoreTweets && URL_TWEET_ID_RE.test(currentPath) && location.search.startsWith('?ref_src')) {
  630. hideMoreTweetsSection(currentPath)
  631. }
  632. }
  633.  
  634. /**
  635. * Automatically click the "Show this thread" link to get rid of the "More Tweets" section if the
  636. * user is viewing a tweet from an external link with a ?ref_src= URL.
  637. */
  638. async function hideMoreTweetsSection(path) {
  639. let id = URL_TWEET_ID_RE.exec(path)[1]
  640. let $link = await getElement(`a[href$="/status/${id}"]`, {
  641. name: '"Show this thread" link',
  642. stopIf: pathIsNot(path),
  643. })
  644. if ($link != null) {
  645. log('clicking "Show this thread" link')
  646. $link.click()
  647. }
  648. }
  649.  
  650. /**
  651. * Sets the page name in <title>, retaining any current notification count.
  652. * @param {string} page
  653. */
  654. function setTitle(page) {
  655. document.title = `${currentNotificationCount}${page} / Twitter`
  656. }
  657.  
  658. function shouldHideTweet(tweetType, page) {
  659. if (tweetType == 'RETWEET' && config.retweets == 'ignore') {
  660. return false
  661. }
  662. return tweetType != (page == TIMELINE_RETWEETS ? 'RETWEET' : 'TWEET')
  663. }
  664.  
  665. async function switchToLatestTweets(page) {
  666. let $switchButton = await getElement('div[aria-label="Top Tweets on"]', {
  667. name: 'timeline switch button',
  668. stopIf: pageIsNot(page),
  669. })
  670.  
  671. if ($switchButton == null) {
  672. return false
  673. }
  674.  
  675. $switchButton.click()
  676.  
  677. let $seeLatestTweetsInstead = await getElement('div[role="menu"] div[role="menuitem"]', {
  678. name: '"See latest Tweets instead" menu item',
  679. stopIf: pageIsNot(page),
  680. })
  681.  
  682. if ($seeLatestTweetsInstead == null) {
  683. return false
  684. }
  685.  
  686. /** @type {HTMLElement} */ ($seeLatestTweetsInstead.closest('div[tabindex="0"]')).click()
  687. return true
  688. }
  689.  
  690. let updateThemeColor = (function() {
  691. let $style = addStyle('')
  692. let lastThemeColor = null
  693.  
  694. return async function updateThemeColor() {
  695. // Only try to update if the "Customize your view" dialog is open or we
  696. // haven't set an inital color yet.
  697. if (location.pathname !== '/i/display' && lastThemeColor != null) {
  698. return
  699. }
  700.  
  701. let $tweetButton = await getElement('a[data-testid="SideNav_NewTweet_Button"]', {
  702. name: 'Tweet button'
  703. })
  704.  
  705. let themeColor = getComputedStyle($tweetButton).backgroundColor
  706. if (themeColor === lastThemeColor) {
  707. return
  708. }
  709. log(`setting theme color to ${themeColor}`)
  710. lastThemeColor = themeColor
  711. $style.textContent = [
  712. 'body.Home main h2:not(#twt_retweets)',
  713. 'body.LatestTweets main h2:not(#twt_retweets)',
  714. 'body.TimelineRetweets #twt_retweets',
  715. ].join(', ') + ` { color: ${lastThemeColor}; }`
  716. }
  717. })()
  718. //#endregion
  719.  
  720. //#region Main
  721. function main() {
  722. log('config', config)
  723.  
  724. addStaticCss()
  725.  
  726. if (config.fastBlock) {
  727. observePopups()
  728. }
  729.  
  730. if (config.navBaseFontSize) {
  731. observeHtmlFontSize()
  732. }
  733.  
  734. if (config.hideMoreTweets ||
  735. config.hideSidebarContent ||
  736. config.hideWhoToFollowEtc ||
  737. config.retweets != 'ignore' ||
  738. config.verifiedAccounts != 'ignore') {
  739. observeTitle()
  740. }
  741. }
  742.  
  743. if (typeof chrome != 'undefined' && typeof chrome.storage != 'undefined') {
  744. chrome.storage.local.get((storedConfig) => {
  745. Object.assign(config, storedConfig)
  746. main()
  747. })
  748. }
  749. else {
  750. main()
  751. }
  752. //#endregion