Tweak New Twitter

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

当前为 2020-06-24 提交的版本,查看 最新版本

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