Twitter Lists Sidebar

Show your Twitter Lists in sidebar

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name        Twitter Lists Sidebar
// @description Show your Twitter Lists in sidebar
// @version     0.3
// @grant       GM.xmlHttpRequest
// @include     https://twitter.com/*
// @namespace https://greasyfork.org/users/173161
// ==/UserScript==

// Note: Avoid using jQuery as it may not be avialble on page load yet
(function(){
  // Cache data using localStorage or sessionStorage
  // class definion have to be placed before its usage
  class Cache {
    constructor(){
      // Key name prefix. Avoid clashes with Twitter itself
      this.prefix = 'lists-sidebar:';
      // Use sessionStorage or localStorage
      this.storage = window.sessionStorage;
    }
    get(name) {
      return this.storage.getItem(this.prefix + name);
    }
    set(name, value) {
      return this.storage.setItem(this.prefix + name, value);
    }
    clear() {
      for (let key in this.storage) {
        if (key.indexOf(this.prefix) === 0) {
          this.storage.removeItem(key);
        }
      }
    }
  }

  let usernameTag = document.querySelector('.DashUserDropdown-userInfo .username b');
  const username = usernameTag && usernameTag.innerText;
  // Not login
  if (!username) {
    return;
  }

  const cache = new Cache();
  // Clear cache if user changed
  if (cache.get('username') && cache.get('username') !== username) {
    cache.clear();
  }

  let sidebar;

  // Execute once on page load
  pageChanged();
  // Poll to detect page navigation.
  // Twitter.com is an SPA and thus document ready event is only fired once.
  // history.onpopstate event is not reliable as history.pushState does not trigger it
  let currentPathname = location.pathname;
  setInterval(function(){
    if (location.pathname !== currentPathname) {
      currentPathname = location.pathname;
      pageChanged();
    }
  }, 300);

  function ajaxRequest(url, callback) {
    // "Referer" header is required for the request to succeed,
    // but native XMLHttpRequest does not alllow to set this header
    // https://wiki.greasespot.net/GM.xmlHttpRequest
    GM.xmlHttpRequest({
      method: 'GET',
      url: url,
      timeout: 3000,
      headers: {
        'Accept': 'application/json, text/javascript, */*; q=0.01',
        'Referer': 'https://twitter.com/',
        'X-Push-State-Request': 'true',
        'X-Twitter-Active-User': 'yes',
        'X-Requested-With': 'XMLHttpRequest',
      },
      onload: function(details) {
        callback(JSON.parse(details.responseText).page.replace(/\\n|\\/g, ''));
      },
      onerror: function() {
        callback(null);
      },
      ontimeout: function() {
        callback(null);
      }
    });
  }

  // Retrive lists by requesting lists page and parsing returned HTML
  function retriveLists(username, callback) {
    const pattern = /<a class="ProfileListItem-name[^>]*href="([^"]+)">([^<]*)<\/a>/g;
    ajaxRequest(
      'https://twitter.com/' + username + '/lists',
      function(html) {
        if (!html) {
          return;
        }
        let lists = [];
        let match = null;
        while (match = pattern.exec(html)) {
          lists.push({
            url: match[1],
            name: match[2],
          });
        }
        // Sort by name
        lists.sort(function(a, b){
          return a.name > b.name ? 1 : (a.name < b.name ? -1 : 0);
        });
        callback && callback(lists.length > 0 ? lists : null);
      }
    );
  }

  function createSidebar(lists) {
    if (!lists || !lists.length) {
      return;
    }
    sidebar = document.createElement('sidebar');
    sidebar.id = 'lists-sidebar';
    // Reuse existing style for consistent color and backgroud-color
    sidebar.className = 'DashboardProfileCard'

    let title = document.createElement('h3');
    title.innerHTML = 'Lists';
    sidebar.appendChild(title);

    let ul = document.createElement('ul');
    ul.id = 'sidebar-lists';
    sidebar.appendChild(ul);

    for (let i = 0; i < lists.length; i++) {
      let li = document.createElement('li');
      let a = document.createElement('a');
      a.className = 'js-nav u-textUserColor';
      a.href = lists[i].url;
      a.innerHTML = lists[i].name;
      li.appendChild(a);
      ul.appendChild(li);
    }

    document.body.appendChild(sidebar);
    addSidebarStyle();
  }

  function addSidebarStyle() {
    if (document.getElementById('lists-sidebar-style')) {
      return;
    }
    let style = document.createElement('style');
    style.id = 'lists-sidebar-style';
    style.innerHTML = `
      #lists-sidebar {
        position: fixed;
        left: 20px;
        top: 30%;
        padding: 1em 1.5em;
        line-height: 1.5;
        z-index: 1000000;
      }

      #lists-sidebar h3 {
        margin-bottom: 0.5em;
        text-align: center;
      }

      #lists-sidebar ul {
        margin: 0;
        padding: 0;
        list-style-type: disc;
        list-style-position: inside;
      }
    `;
    document.head.appendChild(style);
  }

  function updateSidebar() {
    // Cache only valid for 10 minutes
    let lastUpdate = cache.get('lastUpdate');
    if (lastUpdate && new Date() - new Date(lastUpdate) > 600000) {
      cache.clear();
      removeSidebar();
    }

    // Already exists and do not need to update
    if (sidebar) {
      return;
    }

    let lists = JSON.parse(cache.get('lists'));
    if (lists) {
      createSidebar(lists);
    } else {
      retriveLists(username, function(lists) {
        if (lists && lists.length > 0) {
          cache.set('lists', JSON.stringify(lists));
          cache.set('username', username);
          cache.set('lastUpdate', new Date().toString());
          createSidebar(lists);
        }
      });
    }
  }

  function removeSidebar() {
    if (sidebar) {
      sidebar.remove();
      sidebar = null;
    }
  }

  // Create or remove sidebar when page changed
  // Only show sidebar on homepage and lists related pages
  function pageChanged() {
    if (location.pathname === '/' || /^\/[^/]+\/lists/.test(location.pathname)) {
      updateSidebar();
    } else {
      removeSidebar();
    }
  }
}());