Tweak New Twitter

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

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

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