Tweak New Twitter

Hide retweets, and other UI tweaks for New Twitter

当前为 2019-07-24 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        Tweak New Twitter
// @description Hide retweets, and other UI tweaks for New Twitter
// @namespace   https://github.com/insin/tweak-new-twitter/
// @match       https://twitter.com/*
// @version     3
// ==/UserScript==

const HOME = 'Home'
const LATEST_TWEETS = 'Latest Tweets'

let Selectors = {
  PRIMARY_COLUMN: 'div[data-testid="primaryColumn"]',
  PRIMARY_NAV: 'nav[aria-label="Primary"]',
  SIDEBAR_COLUMN: 'div[data-testid="sidebarColumn"]',
  TIMELINE: 'div[aria-label^="Timeline"]',
  TWEET: 'div[data-testid="tweet"]',
}

Object.assign(Selectors, {
  SIDEBAR_FOOTER: `${Selectors.SIDEBAR_COLUMN} nav`,
  SIDEBAR_PEOPLE: `${Selectors.SIDEBAR_COLUMN} aside`,
  SIDEBAR_TRENDS: `${Selectors.SIDEBAR_COLUMN} section`,
})

/** Title of the current page, without the ' / Twitter' suffix */
let currentPage = ''

/** MutationObservers active on the current page */
let pageObservers = []

/**
  * Default config enables all features.
  *
  * You'll need to edit the config object manually for now if you're using this
  * as a user script.
  */
let config = {
  enableDebugLogging: false,
  hideBookmarksNav: true,
  hideExploreNav: true,
  hideListsNav: true,
  hideRetweets: true,
  hideSidebarContent: true,
  navBaseFontSize: true,
}

function addStyle(css) {
  let $style = document.createElement('style')
  $style.dataset.insertedBy = 'tweak-new-twitter'
  $style.textContent = css
  document.head.appendChild($style)
  return $style
}

function log(...args) {
  if (config.enableDebugLogging) {
    console.log(`TWT${currentPage ? `(${currentPage})` : ''}`, ...args)
  }
}

function s(n) {
  return n == 1 ? '' : 's'
}

function applyCss() {
  var cssRules = []
  var hideCssSelectors = []
  if (config.hideSidebarContent) {
    hideCssSelectors.push(
      Selectors.SIDEBAR_TRENDS,
      Selectors.SIDEBAR_PEOPLE,
      Selectors.SIDEBAR_FOOTER
    )
  }
  if (config.hideExploreNav) {
    hideCssSelectors.push(`${Selectors.PRIMARY_NAV} a[href="/explore"]`)
  }
  if (config.hideExploreNav) {
    hideCssSelectors.push(`${Selectors.PRIMARY_NAV} a[href="/i/bookmarks"]`)
  }
  if (config.hideListsNav) {
    hideCssSelectors.push(`${Selectors.PRIMARY_NAV} a[href*="/lists"]`)
  }
  if (hideCssSelectors.length > 0) {
    cssRules.push(`${hideCssSelectors.join(', ')} { display: none !important; }`)
  }
  if (cssRules.length > 0) {
    addStyle(cssRules.join('\n'))
  }
}

function observeTitle() {
  let $title = document.querySelector('title')

  if ($title == null) {
    log('waiting for title')
    return setTimeout(observeTitle, 1000 / 60 * 3)
  }

  function processPage(title) {
    // Ignore Flash of Uninitialised Title navigating to a screen for the first time
    if (title == 'Twitter') {
      return
    }

    // Assumption: all non-FOUT page title changes are navigation / redraws

    if (pageObservers.length > 0) {
      log(`disconnecting ${pageObservers.length} page observer${s(pageObservers.length)}`)
      pageObservers.forEach(observer => observer.disconnect())
      pageObservers = []
    }

    currentPage = title.split(' / ')[0]

    log('navigation occurred')

    if (currentPage == HOME || currentPage == LATEST_TWEETS) {
      if (config.hideRetweets) {
        observeTimeline(currentPage)
      }
    }
    if (config.hideSidebarContent) {
       hideSidebarContents(currentPage)
       observeSidebar(currentPage)
    }
  }

  processPage($title.textContent)

  // Watch for page navigation
  log('observing <title> text')
  new MutationObserver((mutations) => {
    processPage($title.textContent)
  }).observe($title, {
    characterData: true,
    childList: true,
  })
}

function observeTimeline(page) {
  var $timeline = document.querySelector(Selectors.TIMELINE)
  var $firstTweet = document.querySelector(Selectors.TWEET)

  if ($timeline == null ||
      $timeline.firstElementChild == null ||
      $timeline.firstElementChild.firstElementChild == null ||
      $firstTweet == null) {
    if (currentPage != page) {
      return log(`stopped waiting for ${page} timeline`)
    }
    return setTimeout(observeTimeline, 1000 / 60 * 5, page)
  }

  function processTweets() {
    let tweets = document.querySelectorAll(Selectors.TWEET)
    log(`processing ${tweets.length} tweet${s(tweets.length)}`)
    for (let $tweet of tweets) {
      let isRetweet = $tweet.previousElementSibling &&
                      $tweet.previousElementSibling.textContent.includes('Retweeted')
      if (isRetweet) {
        $tweet.parentNode.parentNode.parentNode.style.display = 'none'
      }
    }
  }

  // Watch for new tweets being rendered
  log('observing timeline childList')
  processTweets()
  let $tweetContainer = $timeline.firstElementChild.firstElementChild
  let observer = new MutationObserver(processTweets)
  observer.observe($tweetContainer, {
    childList: true
  })
  pageObservers.push(observer)
}

function observeSidebar(page) {
  let $primaryColumn = document.querySelector(Selectors.PRIMARY_COLUMN)

  if ($primaryColumn == null) {
    if (currentPage != page) {
      return log(`stopped waiting for ${page} primary column`)
    }
    return setTimeout(observeSidebar, 1000 / 60 * 3, page)
  }

  // Watch for sidebar appearing when the responsive breakpoint is hit
  log('observing sidebar')
  let observer = new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
      mutation.addedNodes.forEach((el) => {
        if (el.dataset.testid == 'sidebarColumn') {
          log('sidebar appeared')
          hideSidebarContents(page)
        }
      })
    })
  })
  observer.observe($primaryColumn.parentNode, {
    childList: true
  })
  pageObservers.push(observer)
}

function hideSidebarContents(page, attempts = 1) {
  let $trends = document.querySelector(Selectors.SIDEBAR_TRENDS)
  if ($trends) {
    let $trendsModule = $trends.parentNode.parentNode.parentNode
    $trendsModule.style.display = 'none'
    // Hide surrounding elements which draw separators between modules
    if ($trendsModule.previousElementSibling &&
        $trendsModule.previousElementSibling.childElementCount == 0) {
      $trendsModule.previousElementSibling.style.display = 'none'
    }
    if ($trendsModule.nextElementSibling &&
        $trendsModule.nextElementSibling.childElementCount == 0) {
      $trendsModule.nextElementSibling.style.display = 'none'
    }
  }

  let $people = document.querySelector(Selectors.SIDEBAR_PEOPLE)
  if ($people) {
     let $peopleModule
     if ($people.getAttribute('aria-label') == 'Relevant people') {
       // "Relevant people" section when viewing a Tweet/thread
       $peopleModule = $people.parentNode
     } else {
       // "Who to follow" section
       $peopleModule = $people.parentNode.parentNode
     }
     $peopleModule.style.display = 'none'
  }

  if ($trends == null || $people == null) {
    if (currentPage != page) {
      return log(`stopped waiting for ${page} sidebar`)
    }
    if (attempts == 10) {
      return log(`stopped waiting for sidebar content after ${attempts} attempts`)
    }
    return setTimeout(hideSidebarContents, 333, page, attempts + 1)
  }

  log('hid all sidebar content')
}

function observeHtmlFontSize() {
  let $html = document.querySelector('html')
  let $style = addStyle('')
  let lastFontSize = ''

  function setCss(fontSize) {
    log(`setting nav font size to ${fontSize}`)
    lastFontSize = fontSize
    $style.textContent = [
      `${Selectors.PRIMARY_NAV} div[dir="auto"] span { font-size: ${fontSize}; font-weight: normal; }`,
      `${Selectors.PRIMARY_NAV} div[dir="auto"] { margin-top: -4px; }`
    ].join('\n')
  }

  log('observing <html> style attribute')
  if ($html.style.fontSize) {
    setCss($html.style.fontSize)
  }
  new MutationObserver(() => {
    if ($html.style.fontSize != lastFontSize) {
      setCss($html.style.fontSize)
    }
  }).observe($html, {
    attributes: true,
    attributeFilter: ['style']
  })
}

function main() {
  log('config', config)
  applyCss()
  if (config.navBaseFontSize) {
    observeHtmlFontSize()
  }
  if (config.hideRetweets || config.hideSidebarContents) {
    observeTitle()
  }
}

main()