Twitterの「いいね」を「LGTM」にするChrome Extensionを作ってみた


前置き

つい先日「Qiitaの「いいね」が「LGTM」に変わります」という発表があり、、

その中でも

技術情報が主題でなくても、ただ面白い記事であれば「いいね」が多く押されているような状況にもなっています。

チッ、、バレたか、、、
お、お、俺は、、もっと、、、いいn、、じゃなくてLGTMが欲しいんだ!!!

成果物

何はともあれこんな感じになります。

モサっとしているのは、載せるためにgifに変換したからで、実際はちゃんとクイックに動作します。
せっかくなのでLTGMしたら、twitterのハートの赤ではなくQiitaの緑色になるようにしてみました。

タイムラインの全体像はこんな感じになります。

twitterで

  • 「いいね」してない = LGTMアイコン
  • 「いいね」してる = チェックマークアイコン

という形でQiitaに揃えています。

導入

おふざけしてますが、どうやったかを載せておきます。

Chrome Extensionで実装する

作ったことがないので、テンプレートページを叩き台にしてコードを組んでいきました。

ちなみにですが、ストアには公開していない(申請もかかるので)お試ししたい方は上記からセットアップしていただき、リポジトリを参照していただきたいです。

javascriptが書ければ開発できるのでサクっとやっていきましょう!
少し凝った実装をしたい場合は、Chrome Extensionの独自APIを少し勉強する必要があります。

必要な実装

行ったことはおおまかに以下だけです

  1. 描画の判定
  2. クリック時の挙動制御
  3. タグの監視

あまり難しいことはしていないと思います。

1. 描画の判定

ごくごく当たり前ですが、「いいね」を している / していない を判定して描画を出しわけしています。

してない してる
アイコン

実装よりも、どのclass名を判定するための材料として使用するか、Twitter内部のユニークなclassを調べる方が面倒でした、、、。
内部的には以下のようなオブジェクトを定義して参照するようにしています。

var RESOURCES = RESOURCES || {};

RESOURCES = {

    TARGET: {
        WRAPPER_NAME: 'r-1mdbhws',
        ICON_NAME: "r-xoduu5"
    },

    IS_NOT_SELECTED: {
        NAME: 'r-1re7ezh',
        COLOR: '#657786',
        VIEW_BOX: '0 0 392.81 429',
        SVG: 'M14.19 5.4h53.86v149.45h90.05v44.87H14.19zM288.4 93.77h100.79q1.29 25-5.66 45.39a96.79 96.79 0 01-20.33 34.89 92 92 0 01-32.13 22.45 104 104 0 01-40.95 7.93 109.71 109.71 0 01-76-29.92 104.05 104.05 0 01-23-32.56 95.46 95.46 0 01-8.47-39.88 94.78 94.78 0 018.47-39.87 104.38 104.38 0 0123-32.42A107.71 107.71 0 01248.23 8a110.79 110.79 0 01118.48 22.49l-35.07 35.08a51.25 51.25 0 00-17.75-15 52.83 52.83 0 00-44.67-1.23 52.92 52.92 0 00-17 12 57.07 57.07 0 00-11.45 18.11 60 60 0 00-4.23 22.77 60 60 0 004.23 22.68 56.57 56.57 0 0011.45 18.19 52.62 52.62 0 0017 12 50.5 50.5 0 0020.9 4.36q20.19 0 31.07-7.51a35.75 35.75 0 0014.46-20.55h-47.39zM51.29 279.55H0v-44.86h156v44.86h-51.13V429H51.29zM283.36 381.71l-41.72-62V429h-53.86V234.69h47.47L290 312l55.9-77.29h46.9V429h-53.86V320.27l-42.43 61.44z'
    },

    IS_SELECTED: {
        NAME: 'r-daml9f',
        COLOR: '#55c500',
        VIEW_BOX: '0 0 271.61 199.3',
        SVG: 'M115.57 199.3L0 83.73l36-36L108.34 120l120-120 43.26 43.26z'
    }
}

TwitterのアイコンもQiitaのアイコンもどちらもSVGだったので、
SVGのパスと一緒にviewBoxも差し込むことで、大きさを合わせるように実装しています。

TARGETはアイコンの少し上階層にあるラッパークラス(親class)になります。

Twitterでは基本的に共通化されたclass名が付与されているため(流石ですね笑)、
アイコンを親の階層から参照しないと面倒だったので使用しています。
(もしかしたら、そうしなくてもできたかも、、?)

2. クリック時の挙動制御

「いいね」を している / していないを判定するための唯一のクラスが存在したので、
そのDOMが存在するかどうかで切り分けを判定しています。

/// アイコンの親クラスを参照
const elements = document.getElementsByClassName(RESOURCES.TARGET.WRAPPER_NAME);

/// 画面上にある全てのアイコンをLGTMに書き換える
while(elements.length != 0) {
    const wrapperDom = elements[0];
    if(!wrapperDom) { return; }

    const likeDom = wrapperDom.children[2]; // 1つ目はコメント, 2つ目はリツイート, 3つ目がいいね
    if(!likeDom) { return; }

    const svgDom = likeDom.querySelector('svg');
    const pathDom = likeDom.querySelector('path');
    if(!svgDom || !pathDom) { return; }

    /// ボタンが「いいね」を している / していない なのかを探す
    const nonSelectedDom = likeDom.getElementsByClassName(RESOURCES.IS_NOT_SELECTED.NAME)[0];
    const selectedDom = likeDom.getElementsByClassName(RESOURCES.IS_SELECTED.NAME)[0];

    /// RESOURCESから適切なものをセット
    if (nonSelectedDom) {
        svgDom.setAttribute('viewBox', RESOURCES.IS_NOT_SELECTED.VIEW_BOX);
        pathDom.setAttribute('d', RESOURCES.IS_NOT_SELECTED.SVG);
        nonSelectedDom.style.color = RESOURCES.IS_NOT_SELECTED.COLOR;
        nonSelectedDom.addEventListener('click', function() { onClick(nonSelectedDom); });
    } else if (selectedDom) {
        svgDom.setAttribute('viewBox', RESOURCES.IS_SELECTED.VIEW_BOX);
        pathDom.setAttribute('d', RESOURCES.IS_SELECTED.SVG);
        selectedDom.style.color = RESOURCES.IS_SELECTED.COLOR;
        selectedDom.addEventListener('click', function() { onClick(selectedDom); });
    }

    /// 処理が終わったクラスからは、そのクラスを消す(ことで次のwhileへと進む)
    wrapperDom.classList.remove(RESOURCES.TARGET.WRAPPER_NAME);
}

クリック時のメソッドはざっくりですがこんな感じです。

function onClick(btn) {
    /// クリック時の色変更
    const isSelected = (toHex(btn.style.color) == RESOURCES.IS_SELECTED.COLOR);
    btn.style.color = (isSelected ? RESOURCES.IS_NOT_SELECTED.COLOR : RESOURCES.IS_SELECTED.COLOR);    

    // その他、アイコンやアニメーションの制御などを実行
}

jsを書くのが久しぶりなので、もっと良い書き方があればご指摘いただけると幸いですmm

3. タグの監視

ツイートは

  • 遡ったりしての追加読み込み
  • 新規読み込み

でどんどん画面上に増えてきます

なので、後から表示されるアイコンも動的に差し替える必要があります。
そのために、MutationObserverを使ってタグを監視して、メソッドを発火することでそれを可能にしています。

/// 書き換えメソッド
func replace() { /* 略 */ }

function execObserve() {
    /// 新しいツイートが画面上に追加されたかどうかを0.5秒ごとに判定
    const target = document.getElementsByClassName(RESOURCES.TARGET.WRAPPER_NAME)[0];
    if(!target) {
        window.setTimeout(execObserve, 500);
        return;
    }

    /// ターゲットが見つかれば監視を開始
    observer.observe(target, { attributes: true, childList: true });

    /// LGTMに書き換えを実行
    replace();

    /// 次の対象を探しに再起する
    execObserve();
}

const observer = new MutationObserver(replace);

/// 初回の実行
execObserve();

一部省略しているとはいえ、コードはほぼこれだけです。

その他

Chrome Extensionとして作成しているので、インストールするとこんな感じの簡易な物は用意しています。

こちらは別途この画面用にhtml、css、jsを用意する必要があります。
当然カスタマイズも可能です。

問題点

とはいえ、昨日の今日でとっかんで作ったのでまだまだ課題も、、

①詳細画面のハートが変わらない
②読み込みが遅いと一瞬ハートがちらつく

などなど。。。

①に関しては、どうやらユニークなクラス名が違うようです、、。

こちらにプルリクお待ちしております!
ちなみに、今後も公開する気はないので、ストアに公開されたい方がいればどうぞどうぞです笑

後書き

Qiita運営としても、流れてきた記事に「いいね」するのではなく、しっかりその内容を読んで、その内容が少なくとも自分にとって良いものだと思ったときに「LGTM(Looks Good To Me)」と言ってもらった方が良いと考えました。

というわけでみんな!
しっかりと記事を読んで、良いと思ったものにしっかりLGTMするんだぞ!!!