輪読会の読み順をランダムで決める chrome 拡張機能を作る for Google Meet [React + TypeScript]

この記事は個人ブログと同じ内容です

輪読会の読み順をランダムで決める chrome 拡張機能を作る for Google Meet [React + TypeScript]

最近弊社の開発メンバーでブログを書こうという運動があります。 ネタ探しをしていたらよさげな記事を見つけたのでアイデアをお借りします。 shohei さんありがとう🙏 ※決してパ○リではありません https://techblog.roxx.co.jp/entry/2021/04/23/064837

毎週チーム内での読書会や、部署をまたいだエンジニアでの輪読会を行っているのですが、都度読み順を決めるのが面倒なので、参加メンバーをシャッフルしてリストで返す chrome 拡張機能を作ってみました。 なお、弊社の読書会は Google Meet で行うことが多いため必然的に Google Meet 用の拡張機能となっておりますのでご理解ください。

作ったもの

現在 Meet に参加しているメンバーをランダムに並び替えて一覧表示する拡張機能を作りました ※store で公開はしていない

image

こちらで公開しているのでぜひ覗いてみてください https://github.com/SotaYamaguchi/member-sort-extension-for-google-meet

技術選定

UI の構築を含むので書き慣れたフレームワークを使いたかったので React + TypeScript を採用しました。

chrome extension をつくるにあたって、以下のテンプレートを使えば React + TypeScript が入った状態でサクッと始められそうだったので使わせていただきました。 https://github.com/chibat/chrome-extension-typescript-starter

パッケージ類

Chrome 拡張機能の型定義を読み込むために @types/chrome を使いました。

image

popup.tsx と content_script.tsx

  • popup.tsx
  • content_script.tsx
    • 開いているタブで実行できる script

上記の 2 箇所でデータをやりとりするためには以下の chromeAPI を活用します

chrome.tabs.sendMessage() chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {} )

https://developer.chrome.com/docs/extensions/reference/tabs/ https://developer.chrome.com/docs/extensions/reference/runtime/#event-onMessage

popup 側の実装

ユーザー一覧 drawer の表示

ユーザー一覧 drawer が表示されないと参加メンバー名を持った DOM が生成されないため、 初回レンダリング時に sendMessage でcontent_script 側でイベントを発火させます

参加メンバー一覧の取得処理

一覧取得ボタンをoukajini getMemberList() が走り、 sendMessage を送信することで content_script 側で script を実行し、参加メンバー一覧を文字列で取得します それをよしなに加工して view 側で表示させてあげることで参加メンバー一覧を表示しています

  const currentChromeTab = (callback: (tabId: number) => void) => {
    chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
      // 現在表示しているタブを取得
      const tab = tabs[0];
      if (tab.id) {
        callback(tab.id);
      }
    });
  };

  const getMemberList = () => {
    currentChromeTab((tabId) => {
      chrome.tabs.sendMessage(
        tabId,
        undefined, // message は不要なため undefined とする
        (msg) => {
          if (typeof msg === "string") {
            const shuffledMembers = shuffle(msg.split(","));
            setCurrentTime(new Date());
            setMembers(shuffledMembers);
          }
        }
      );
    });
  };

  // init
  useEffect(() => {
    currentChromeTab((tabId) => {
      // ユーザー一覧 drawer を表示させる
      chrome.tabs.sendMessage(tabId, undefined);
    });
  }, []);

content_script 側の実装

ユーザー一覧 drawer の表示判定

初回表示時にユーザー一覧 drawer の表示判定を行い、未表示だった場合は自動で開くようにしました

参加メンバーの名前一覧を取得

cylMye というクラス名を持った DOM の子孫に参加メンバー名が格納されていたので取得します

const openAllUserDrawer = () => {
  /*
   * 各ボタンの aria-label 属性にラベルに表示するボタン名が格納されている
   * 全てのボタンを DOM から取得して全員を表示ボタンを探す
   */
  const ariaLabelElems = document.querySelectorAll("[aria-label]");
  for (let i = 0; i < ariaLabelElems.length; i++) {
    if (
      ariaLabelElems[i].getAttribute("aria-label") === ALL_USER_BUTTON_LABEL
    ) {
      const chatOpenButton = ariaLabelElems[i] as HTMLButtonElement;
      chatOpenButton.click();
    }
  }
};

const getUserNameList = (sendResponse: (response?: any) => void) => {
  let names: string[] = [];
  const elems = document.querySelectorAll(".cylMye");
  if (!elems.length) {
    // .cylMye が存在しない = ユーザー一覧 drawer が表示されていない
    openAllUserDrawer();
    return;
  }
  for (let i = 0; i < elems.length; i++) {
    names = [...names, findUserNameFromElm(elems[i])];
  }
  // 重複した名前を省く
  names = Array.from(new Set(names));
  sendResponse(names.join(","));
};

chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  getUserNameList(sendResponse);
});

メンバーをシャッフルする関数

今回はランダムの偏りをなくすためフィッシャーイェーツのシャッフル という方法をアルゴリズムに採用しました

const shuffle = (value: string[]) => {
  for (let i = value.length - 1; i >= 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [value[i], value[j]] = [value[j], value[i]];
  }
  return value;
};

export default shuffle;

.zip ファイルを生成する

Github Actions で master ブランチへ push 時に build を実行 + 実行結果を .zip に圧縮しリリースに含めるように build.yml に記載

      - name: yarn install & build
        run: |
          yarn
          yarn build --if-present
      - name: zip output
        run: |
          cd dist
          zip release *.*
      ・
      ・
      ・
      - name: upload Release Asset
        id: upload-release-asset
        uses: actions/upload-release-asset@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          upload_url: ${{ steps.create_release.outputs.upload_url }}
          asset_path: ./dist/release.zip
          asset_name: chrome-extention.zip
          asset_content_type: application/zip

まとめ

参加してるメンバーを集計する目的でも利用できるのでわりと便利かと思うのでぜひ使ってみてください。 今後、公開まで試してみたい。