0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

X(Twitter)で余計なおすすめツイートを見えなくするChrome拡張機能を作ってみた

Posted at

概要

Xの火種の一つであるおすすめツイート機能を見れなくするChromeの拡張機能を作って使ってみました。Geminiのサポートもあり、簡単に望みの拡張機能が作れました!

背景

X(Twitter)は色々な情報を迅速に得られるので便利だったりするのですが、一方でユーザーの発言の質はお世辞にも良いとは言えず、しょうもない事で毎日のように争っていたりする、そんな環境です。この環境で何年も過ごしてきましたが、最近Xで有益な情報を得るよりも精神的に良くないポストの方が多く流れてくるようになってしまったため、一度アプリを削除して離れていました。
ただ、やはりほしい情報や興味深そうな情報を得ようとすると結構な確率で、X上で誰かが投げたポストに当たったりするので全く触らないというのも難しいかなと思っていました。
色々考えていたのですが、諸悪の根源の1つはおすすめツイートという機能ではないかと思い、これを物理的に見れない状態にすれば少しはXを閲覧する際に気が楽になるのではとふと思いました。
どうやって実装すればいいかと考えてGeminiに聞いたところ、「Chromeの拡張機能を自前で開発すれば楽に実装できるよ!」との事だったので作ってみる事にしました。

機能

現在の拡張機能では以下の2つの機能があります。

  • ”おすすめツイート”タブの非表示化
  • オプションページにて予め登録したリストに遷移する

おすすめツイートの非表示化

この拡張機能のメインの目的がおすすめツイートの非表示化です。個人的には見ていてストレスなので開かないようにしていましたが、ブラウザでXを開くと最初におすすめツイートが流れてくる事があった事、物理的に見れてしまうものは開いてしまう癖があるため、いっそのこと非表示化してしまおうという発想のもと実装しました。

指定したリストへの遷移

おすすめツイートの非表示化によりフォローユーザーのツイートだけがタイムラインで見られるようになりましたが、一方でフォローユーザーが多いと大量のツイートが流れてきます。登録したユーザーのツイートのみ閲覧できるリストを使いたくなる時があると思います。一方で作成したリストを見るためにはXの左のサイドバーの”もっと見る”を押し表示されるリストから作成したリストに遷移する必要があり、毎回Xを開くたびにこの作業を行うのはやや面倒です。
そのため、左サイドバーのメニューの一部を書き換えて、予め登録しておいたリストを1クリックで表示させるようにしました。

作成物

  • manifest.json
    拡張機能を作る上で必ず必要になるものです。ここで拡張機能の名前や説明、バージョン情報、拡張機能の本体となるスクリプトファイルを指定したりします。
manifest.json
{
  "manifest_version": 3,
  "name": "MoreComfortableX(JP)",
  "version": "1.0.1",
  "description": "おすすめポストのタブを削除し、自分のフォローユーザーのポストのみをタイムラインで閲覧するように変更します。またオプションでリストIDを設定する事でプレミアムボタンから指定したリストにすぐ移動できるように変更します。",
  "content_scripts": [
    {
      "matches": ["https://x.com/*"],
      "js": ["content.js"],
      "css": ["style.css"]
    }
  ],
  "permissions": [
    "storage"
  ],
  "options_page": "options.html",
  "icons": {
    "16": "icons/icon16.png",
    "48": "icons/icon48.png",
    "128": "icons/icon128.png"
}
}

content_scriptsで指定しているJavaScriptファイル、CSSファイルが、実際にX上で動作するものとなります。

  • content.js
    拡張機能のメインとなる部分です。
コードの中身
content.js
const LIST_SVG_PATH = `
  <g><path d="M3 4h18v2H3V4zm0 7h18v2H3v-2zm0 7h18v2H3v-2z"></path></g>
`;

const COMMUNITY_SVG_PATH = `<g><path d="M7.5 7c0-2.485 2.015-4.5 4.5-4.5s4.5 2.015 4.5 4.5-2.015 4.5-4.5 4.5-4.5-2.015-4.5-4.5zm6 0c0-0.828-0.672-1.5-1.5-1.5s-1.5 0.672-1.5 1.5 0.672 1.5 1.5 1.5 1.5-0.672 1.5-1.5zM21 21v-2c0-2.761-2.239-5-5-5H8c-2.761 0-5 2.239-5 5v2h2v-2c0-1.657 1.343-3 3-3h8c1.657 0 3 1.343 3 3v2h2z"></path></g>`;

let myScreenName = null;

function getScreenName() {
    if (myScreenName) return myScreenName;
    const profileLink = document.querySelector('a[data-testid="AppTabBar_Profile_Link"]');
    if (profileLink) {
        myScreenName = profileLink.getAttribute('href').replace('/', '');
        return myScreenName;
    }
    return null;
}

// ヘッダー(フォロー中)を注入する関数
function injectFollowingHeader() {
    const currentPath = window.location.pathname;
    if (currentPath !== '/home' && currentPath !== '/') return;
    const headerContainer = document.querySelector('[data-testid="primaryColumn"]');
    if (headerContainer && !document.getElementById('custom-following-header')) {
        const container = document.createElement('div');
        container.id = 'custom-following-header';
        
        container.style.cssText = `
            display: flex;
            justify-content: center;
            align-items: center;
            padding: 12px 0;
            background-color: transparent;
            border-bottom: 1px solid rgb(56, 68, 77);
            position: relative;
        `;

        // クリックイベント用のラッパーを作成
        const clickWrapper = document.createElement('div');
        clickWrapper.style.cssText = `
            display: flex;
            align-items: center;
            cursor: pointer;
            padding: 4px 12px;
            border-radius: 9999px;
            transition: background-color 0.2s;
        `;

        clickWrapper.innerHTML = `
            <span style="font-size: 17px; font-weight: 800; color: #e7e9ea;">フォロー中</span>
            <span id="header-arrow" style="margin-left: 4px; font-size: 14px; color: #71767b; transform: translateY(1px);">▼</span>
        `;

        // ホバーエフェクト
        clickWrapper.onmouseover = () => clickWrapper.style.backgroundColor = 'rgba(231, 233, 234, 0.1)';
        clickWrapper.onmouseout = () => clickWrapper.style.backgroundColor = 'transparent';

        // クリック時の挙動
        clickWrapper.onclick = (e) => {
            const realFollowingTab = Array.from(document.querySelectorAll('[role="tab"]'))
                .find(tab => tab.innerText.includes('フォロー中') || tab.innerText.includes('Following'));
            
            if (realFollowingTab) {
                realFollowingTab.click();
            }
        };

        container.appendChild(clickWrapper);
        headerContainer.prepend(container);
    }
}

function mainLoop() {
    const currentPath = window.location.pathname;
    if (currentPath === '/home' || currentPath === '/') {
        // 公式タブを「フォロー中」にするロジック
        const allTabs = Array.from(document.querySelectorAll('[role="tab"]'));
        const followingTab = allTabs.find(tab => 
            tab.innerText.includes('フォロー中') || tab.innerText.includes('Following')
        );

        if (followingTab && followingTab.getAttribute('aria-selected') === 'false') {
            followingTab.click();
        }
        
        // 新しいヘッダーの注入
        injectFollowingHeader();
        document.body.classList.add('hide-tabs');
    } else {
        document.body.classList.remove('hide-tabs');
    }

    const screenName = getScreenName();
    if (screenName) {
        const commBtn = document.querySelector('nav a[href$="/premium_sign_up"]:not([data-is-custom-link="true"])');
        if (commBtn) {
            chrome.storage.sync.get(['targetListId'], (result) => {
                const listId = result.targetListId;
                let listLink = "";
                if (listId) {
                    listLink = `https://x.com/i/lists/${listId}`;
                } else {
                    listLink = `https://x.com/${screenName}/lists`;
                }
                const svg = commBtn.querySelector('svg');
                if (svg && !svg.dataset.modified) {
                    svg.innerHTML = LIST_SVG_PATH;
                    svg.dataset.modified = "true";
                }
                commBtn.setAttribute('href', listLink);
                commBtn.setAttribute('aria-label', 'リスト');
                commBtn.onclick = (e) => {
                    e.preventDefault();
                    // 既にそのURLにいる場合はリロードを防ぐ
                    if (window.location.href !== listLink) {
                    window.location.href = listLink;
                    }
                }
            });
        }
    }
}

const observer = new MutationObserver(mainLoop);
observer.observe(document.body, { childList: true, subtree: true });
mainLoop();
  • style.css
    CSS関係のファイルです。
style.css
/* 右側のトレンド・おすすめユーザーを非表示 */
[data-testid="sidebarColumn"] {
    display: none !important;
}

/* タイムラインの幅を調整(サイドバーが消えた分、中央に寄せる) */
main {
    align-items: center !important;
}

/* ホーム画面の「おすすめ/フォロー中」のタブ選択バーを非表示にする */

.hide-tabs [data-testid="ScrollSnap-List"] {
    display: none !important;
}

#my-order-switcher button:hover {
    background-color: rgba(29, 155, 240, 0.1) !important;
}

できた拡張機能

拡張機能を有効にした上でXにアクセスするとこんな感じでおすすめツイートタブが非表示になっており、さらに右サイドのトレンドも表示されなくなっています。

extension_pic1.png

ヘッダーのフォロー中と書かれた部分を押すと画面左側に表示されるメニューからフォロー中のユーザーのポスト表示順を切り替える事が出来ます。

extension_pic2.png

extension_pic4.png

リストの登録

この拡張機能では指定したリストを左サイドバーのメニューの”プレミアム”ボタンを置き換える事でそのリストにジャンプする事が出来ます。

まずは指定したいリストのURLから/i/lists以下の数字(リストID)をコピーします。

extension_pic5.png

続いて拡張機能のオプションを押します。

extension_pic7.png

オプションページの入力欄に先ほどコピーしたリストIDをペーストして保存ボタンを押します。

extension_pic6.png

extension_pic8.png

保存ボタンを押して画面を更新すると現在のリストIDの設定値が表示されます。

この状態でXに戻って図の赤丸で囲った部分(元々プレミアムボタンがあったところ)を押すと先ほど指定したリストへジャンプする事が出来ます。

extension_pic9.png

この機能を使う事で、左サイドバーからメニューを開く→リスト一覧を表示する→見たいリストを表示するという手間が省けます。

拡張機能の公開

こちらを参考にChromeの拡張機能を公開しました。

簡単な流れとしては

  • GoogleアカウントをChromeのWeb Developerに登録する(要5ドル)
  • 作成した拡張機能のファイルをzipに圧縮、アップロードする
  • ストア上で拡張機能の説明、権限の用途、プライバシーポリシーへのリンクなどを書く
  • 審査を待つ(数日で結果が分かります)

お金を払うのは最初の1回だけで大丈夫です。
審査に合格すればステータスに公開済みと表示されます。

extension_pic10.png

プライバシーポリシーはGithub pagesを使いました。
中身はこんな感じです。

拡張機能のコードの中身は私のGithubリポジトリに置いてます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?