この記事は個人ブログと同じ内容です
輪読会の読み順をランダムで決める chrome 拡張機能を作る for Google Meet [React + TypeScript]
最近弊社の開発メンバーでブログを書こうという運動があります。 ネタ探しをしていたらよさげな記事を見つけたのでアイデアをお借りします。 shohei さんありがとう🙏 ※決してパ○リではありません https://techblog.roxx.co.jp/entry/2021/04/23/064837
毎週チーム内での読書会や、部署をまたいだエンジニアでの輪読会を行っているのですが、都度読み順を決めるのが面倒なので、参加メンバーをシャッフルしてリストで返す chrome 拡張機能を作ってみました。 なお、弊社の読書会は Google Meet で行うことが多いため必然的に Google Meet 用の拡張機能となっておりますのでご理解ください。
作ったもの
現在 Meet に参加しているメンバーをランダムに並び替えて一覧表示する拡張機能を作りました ※store で公開はしていない
こちらで公開しているのでぜひ覗いてみてください github.com
技術選定
UI の構築を含むので書き慣れたフレームワークを使いたかったので React + TypeScript を採用しました。
chrome extension をつくるにあたって、以下のテンプレートを使えば React + TypeScript が入った状態でサクッと始められそうだったので使わせていただきました。 https://github.com/chibat/chrome-extension-typescript-starter
パッケージ類
Chrome 拡張機能の型定義を読み込むために @types/chrome を使いました。
popup.tsx と content_script.tsx
上記の 2 箇所でデータをやりとりするためには以下の chrome の API を活用します
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
まとめ
参加してるメンバーを集計する目的でも利用できるのでわりと便利かと思うのでぜひ使ってみてください。 今後、公開まで試してみたい。