Slack用 Chrome拡張機能を作った!#techtekt Advent Calendar 2022

TL;DR

Chrome 拡張機能を作るのいいぞぉ!

customize-slackを作りました!

もちべ

毎日 Slack を利用しているとこんな機能があったら良いのにな...と思うことが多々あります。

公式に無ければ、自分で作ればいいじゃない

[ソター・アントワネット]

ということで自分が求めている機能を自分で作るぞ!

前提条件

そもそも Slack っていじれるのか?

まずはじめに、残念なお知らせがあります。

なんと Slack公式のアプリでは機能を作ることができません...

Webhook 等を利用して、外部サービスからSlackへのメッセージ送受信等をすることはできますが、利用者が直接さわるUIの変更等はできません(できる方が珍しいですが)。

基本的にアプリ版の Slack を利用していた僕は諦めかけました。

でも...それでも諦めなかったのは Slack は本当に毎日使うものだからです。

業務でも使いますし、実は大学の同期とも Slack でやりとりをしています。

毎日使っているものを少しでも便利にできれば、毎日少しずつ時間等が短縮でき、結果的には大きなアドバンテージになるでしょう!

「楽をするためならば、どんな苦労もいとわない!」

をモットーとしてる僕としては諦めきれず、他の方法を模索しました。

そこで僕は Web 版の Slack に目をつけました。

Web 版の Slack とは?

名前の通り、Web で表示する Slack です。 ブラウザ版とも言いますね。

Slack はWeb版とアプリ版で2種類を提供しており、特に理由なくアプリ版を利用していましたが、 Web 版だと一筋の光が見えてくるのです。

それは何か? Web版ならば、ブラウザの拡張機能を利用すればいいんですね! ブラウザで見えているもの(DOM)ならばほぼ全て、拡張機能でいじれます。

そこで今回は Chrome の拡張機能を作っていこうと思います。

Web版をアプリっぽく使いたい

ちなみに Chrome には特定のサイトのアプリっぽく利用することができます。

これは実際にはブラウザで表示しますが、あたかも専用アプリのような見た目で利用できるためとても便利です。

左: Web版をアプリっぽくしたもの 右: アプリ版

やり方

  • Chrome で Slack のページを開く
  • ブラウザの右上にあるメニューを開く
  • 「その他のツール」を選択
  • 「ショートカットを作成...」を選択
  • 「ウィンドウとして開く」にチェックを入れて「作成」ボタンを押す

Chrome 拡張機能の作り方

さて、やることが決まったので実際に Chrome 拡張機能の作成方法について述べます。

ネットで調べるとさまざまなテンプレートが出てくるのですが、 今回 ReactTypeScript を利用したかった僕は martellaj/chrome-extension-react-typescript-boilerplate を利用することにしました。Star の数も多くて良さそうです。

続いて中身のコードの話をします。

manifest.json のバージョン

とりあえずクローンしたものをそのままビルドして Chrome 拡張機能として動かしてみました。

動きませんでした。

alu.jp

最初のハマりポイントですね。

Chrome さんは今年の1月から 拡張機能のバージョンを変更したみたいです。

今回利用しようとしているテンプレートではそれに対応してないみたいで、うぐぐぐ...となりました。

しかし、この修正はそんな大変なものではなく、manifest.json をちょっと書き換えるだけですみました。(お世話になった記事

めでたしめでたし。

JavaScript の実行タイミング

Chrome 拡張機能と言っても、実際に行っているのは事前に用意したJavaScript(以下、JS)を実行してるだけに過ぎません。

そのため、ある特定のページ(今回だとapp.slack.comのドメイン)にだけ、用意した JS を実行させるようにしてみました。

動きませんでした

alu.jp

今度は何が原因かというと対象としているページが原因のようです。

やったことを説明します。

僕は JS を実行させるために manifest.jsonに以下のように記述しました。

manifest.json

 "content_scripts": [
   {
     "matches": ["https://app.slack.com/*"],
     "js": ["content-script.js"]
   }
 ]

content-script.js

  const hogeBtn = document.querySelector(
    `#hoge`
  ) 

こうすることでhttps://app.slack.comのURLだった場合、hogeBtnの要素が取得できるはずなんですが...できず、undefined になってしまいました。

理由はSlack君がクライアントサイドレンダリングを利用してるからっぽいです。

クライアントサイドレンダリングとはクライアント側(ブラウザ)でJSを実行し、DOM構成を作成するレンダリング手法です。

これの特徴として、初期状態のHTMLはほとんど空という点があります。

中身のコンテンツはJSで記述されており、ブラウザ上でページの構成や配置をするというイメージです。

HTMLにコンテンツが書かれているシンプルな方法に比べて初期表示は遅いが、画面遷移が早いという特徴があります。

クライアントサイドレンダリングの例

今回の hogeBtn が取得できなかった理由ですが、DOMが構築される前にcontent-script.js が実行されてしまうのだと考えられます。

ないものはそりゃあ取れませんからね。

そのため今回はJSの実行タイミングをずらすため、chrome.webRequest.onCompletedを利用しました。

これはリクエストが正常に処理されたときに発生します。

これに処理を紐付けることによって、DOM更新後に処理を実行できます。

chrome.webRequest.onCompleted.addListener(
  function (details) {
    if (details.url.indexOf("https://app.slack.com") !== -1 && details.tabId > 0)  {
      chrome.scripting.executeScript({
        target: { tabId: details.tabId, allFrames: true },
        files: ["./js/content.js"],
      });
    }
  },
  { urls: ["https://app.slack.com/*"] }
);
});

厳密にはリクエストの後、描画をして、描画終わりに処理を実行したほうが良いと思うのですが、できるかわからないのと、これでちゃんと動いているので

JS でやってること

今回やりたいこと

達成目標を説明します。

今回は Slack の未読ページにおいて、スクロールだけで既読をつけたいと思います。

完成図↓

次に実際になにをやっているのかを説明します。

まず正規表現を利用してURLを確認します。

これが実行される前に、ドメイン側のチェックは完了していますが、サブディレクトリ側のチェックはやってません。

そのためまずサブディレクトリのチェックを行い、app.slack.com/unreads*だったらプログラムを続けます。

// 未読ページか確認
const re = /\/client\/.+?\/unreads.?/;
if (re.test(location.pathname)) {
  setUnreadParentObs();
  setEmptyObs();
}

次に setUnreadParentObs 関数です。

この関数ではまず、既読にする要素の親要素を探します。

その後、対象となる子要素に既読にするイベントを子要素にアタッチします(bindAutoReadEvent

// 未読コンテナの追加を監視するオブザーバをセット
function setUnreadParentObs() {
  // 未読コンテナが追加される親コンポーネントを取得
  const unreadParent = document.querySelector(
    `#unreads_view > div [role=list]`
  );

  if (unreadParent) {
    // 未読コンテナにイベントをバインドする
    for (const child of unreadParent.childNodes as unknown as HTMLElement[]) {
      bindAutoReadEvent(child);
    }
    // 続きます

これで完了かと思われますが、そうではありません。

Slack は初期に表示される未読要素は全てとは限らないからです。

一部分だけ初期に描画し、スクロールして下から出てくる要素はその都度新しく作成されています。

そのため新しく作成される要素にもイベントをアタッチしなければなりません。

そこでオブザーバーというものを利用します。

これは任意の要素の変化を監視するものです。

今回は取得した親要素を監視し、もし要素が追加されたら、bindAutoReadEventを実行するようにしています。

    // 続きます
  
    // 未読コンテナ追加時にイベントをバインドする
    // オブザーバーの作成
    const unreadParentObs = new MutationObserver((mutationsList) => {
      for (const mutation of mutationsList) {
        // 追加された対象ノードに、イベントをバインドする
        bindAutoReadEvent(mutation.addedNodes[0]);
      }
    });
    // 監視を開始
    unreadParentObs.observe(unreadParent, {
      childList: true,
    });
  }
}

bindAutoReadEventでやっていることは、対象となる要素( id が unreads_view_spacer-bottom-hogehoge )がスクロールで画面外に行くと既読ボタンをクリックするイベントを作成し、そのイベントをスクロール時に実行するというものです。

画面外かどうかは要素の場所で判断しています。ウィンドウの上端からの距離を計算し、160px(ヘッダー + 未読 + 未読ヘッダー + container高さ)よりも小さい場合はスクロールして視界範囲の超えたとして既読ボタンをクリックします。

function bindAutoReadEvent(targetNode) {
  if (!targetNode) return;
  else if (targetNode.id.indexOf("unreads_view_spacer-bottom-") === 0) {
    const unreadId = targetNode.id.split("-").slice(-1)[0];
    let observing = true;

    // スクロール時に非表示になったら既読ボタンクリック
    // TODO: イベントリスナーを複数回追加するのをやめる
    document.addEventListener(
      "scroll",
      function () {
        const containerTop = targetNode.getBoundingClientRect().top;
        if (containerTop === 0) return;

        // TODO: 160 を可変にできるように、
        // ヘッダー + 未読 + 未読ヘッダー + container高さ(unreadBottom.clientHeight) = 160くらい
        if (containerTop < 160 - targetNode.clientHeight && observing) {
          observing = false;
          const readBtn = document.querySelector(
            `#unreads_view_header-${unreadId} > div > span > button`
          ) as HTMLElement;
          if (readBtn) readBtn.click();
        }
      },
      { capture: true }
    );
  }

この他にもコードはありますが、GitHubに公開してるので御覧ください。

また実際に Chromeウェブストアにも公開しています。

(まだ説明欄とかテンプレのままになってますが)

おわりに

楽しかったです! いやー、これからの Slack がちょっと便利になると考えるとウキウキですね!

加えて拡張機能を作る勉強にもなって良かったです。

もっともっと便利にしたいと考えており、今の所以下の機能をつくりたいなぁと思っています。

(できるかは置いてといて)

  • 未読ページで後で見るボタン作成(スクロールで既読にしない)
  • 似てるスタンプをピックアップする機能
  • slack上での絵文字作成機能
  • ピン留めスタンプを増やす
  • 読み終わったらボケて画像を出す
  • スレッド閉じるボタンの追加
  • 全スタンプ一斉押下ボタンの追加

はい、ということで、ここまで読んでくださりありがとうございました!

この記事が何かの役に立つことを願って終わろうと思います!

そーたでした!

エンジニアリング統括部 サービス開発部 第2グループ 渡辺 爽太

渡辺 爽太 Sota Watanabe

エンジニアリング統括部 サービス開発部 第2グループ エンジニア

趣味はゲーム、アニメ、漫画です。イカになって塗装作業に勤しんだり、おじさんになって大乱闘を繰り広げてます。 何事もたのしく!

※2022年12月現在の情報です。