Chatwork+ChromeExtensionで縁の下の力持ちにもスポットライトがあたるように

この記事は、株式会社エイチームフィナジーの[Advent Calendar 2020](https://qiita.com/advent-calendar/2020/ateam-finergy) 14 日目の記事になります。
テクニカルソリューション部エンジニアの@andmorefineが担当いたします。

先日、社内の子会社を跨いだグループ横断でのハッカソンが開催されました。
それに参加し自分自身どんなことを考えて、どんな行動ができたのかをかいつまんでお話しをします。

結論から先にお伝えすると、参加してみて自分自身成長する機会をいただきました。
期間が短く限られた中で、即席だったチームにも関わらず、最終的に成果物を出せて達成感を味わうことができました。

ただ、残念ながら結果にはつながりませんでした。
がしかし、ゼロベースから物を作り上げることや、自分たちのアイディアで今の課題を解決していくことを経験できたことはとてもいい学びとなりました。
また、チームメンバーやハッカソンに関わってくださった関係者の方々に、この場をお借りして感謝申し上げます。
ありがとうございました。

## グループ横断ハッカソン概要

グループ横断ハッカソンの概要を簡単に説明します。
制作期間は約一ヶ月で、テーマは「在宅勤務によって生じているマネジメント課題の解決ツール」です。

チームによって人数はバラバラでしたが、一チーム4〜6人名ほどの構成です。
各チームにはマネージャーが1名、基本デザイナー＆エンジニアという構成です。

私のチームは、マネージャー1名、デザイナー2名、エンジニア3名のチーム構成でした。

チームメンバーとは、ほとんど話したことがない人たちばかり＆どんな人たちかまったく知らない状況でしたので、一ヶ月の期間で果たして本当にできるのかとひやひやしたものです。
結果的にそんな思いは杞憂に終わりました。

## 解決するマネジメント課題

まず、最初にやったことはマネージャーに今抱えているマネジメント課題のヒアリングを行いました。

マネージャーから提示されたものは二つありましたが、解決するマネジメント課題は以下に決まりました。

**今まで以上に皆んなの行動が見えて、縁の下の力持ちにもスポットライトがあたるようにしたい！**

リモートワークになる前はメンバーの日々の何気ない行動を見ており気にとめてメモしていたが、
在宅勤務になることで、より一層、特に縁の下の力持ちのメンバーの行動が見えずらくなったとのこと。

マネージャーから話しを伺って、成果だけではなくプロセスにも気にとめていただいているのだなと改めて思った次第です。

## 課題解決ツール作成

作成したツールは下記です。

**Chatworkのリアクションに、複数回押せる「いいね」のような機能を追加して、普段の行動や気付き、発信がみんなにどれだけ良い影響を与えているか可視化するツール**

ツールが使われなくなる懸念があったので、普段使っているChatworkに機能を追加することで使われなくなることを防止しようと考えました。
また、どんなコメントに対して良い影響を与えたかをマネージャーが把握することができるようにするために管理画面を用意しました。

マネージャーは管理画面を通じて、誰がどれだけ「いいね」をもらっているかがわかります。
「いいね」だけではどんな良いことをしたかわからないので、Chatworkのコメントをまとめて表示したり、Chatworkへのリンクを紐付けて、時系列もわかるようにしました。

管理画面ではマネージャーは、配下のメンバーのコメントを見ることができます。
マネージャー以外の一般社員も管理画面を利用することはできますが、自分の情報だけしか見ることができません。

継続して使用できるようにエンターテイメント性を追加。
ランキングや、一定数いいねを獲得した社員には称号のようなものを与えるように考えました。

プロダクト名は、「ぴーぶる！」に決定。
「いいね」も`ぴーぶる`＋`いいね`で「ぴぷいね」とチーム内で呼ぶようになりました。
ネーミングセンスとデザインは個人的に好みでした。

![people-250.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42432/2309e000-10d0-c676-357a-b9604d296f7a.png)

## システム概要

![1920 Web – 2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42432/cce603f3-7e46-f122-7ff6-41bbd5cf5c78.png)

## Chatwork＋ChromeExtension作成

私が担当した箇所は、主にフロント側のChromeExtension作成です。
アイコン、デザイン、インフラ周りはチームの方々に制作していただきました。

### ChromeExtensionで用いたもの

#### ■ 技術

- JavaScript
- React.js
- gulp
- babel

#### ■ 機能

- 管理画面を表示する
- Chatworkの各メッセージに「ぴぷいね（いいね）」を送れるボタンを設ける
- ルームを開いたときに「ぴぷいね（いいね）」数を取得する

#### ■ 画面

![1920 Web – 1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42432/a3389e8b-80cc-5d04-85fc-ad3410dcb8df.png)

### 実装内容

#### Chatworkの各メッセージに「ぴぷいね（いいね）」を送れるボタンを設ける

ChatworkはReactベースで作成されているため、各メッセージ内にボタンを追加させるためにはDOMの変更をキャッチする必要がありました。
[window.onload](https://developer.mozilla.org/ja/docs/Web/API/GlobalEventHandlers/onload)では、動的に変化をキャッチできず、仮にメッセージを上にスクロールしてしまうとボタンが表示されないので、常時DOMの変更を監視することが最低限必要でした。
また、左側のルーム部屋をクリックしてもボタンが表示されるようにするにはどこでフックするべきなのかを考慮する必要がありました。

そこで使用したものは、WebAPIのインターフェイスのひとつである[MutationObserver](https://developer.mozilla.org/ja/docs/Web/API/MutationObserver)です。

こちらは、DOMの変更を検知するオブザーバを生成することができます。
恥ずかしながら私自身このAPIを知ったのは最近のことでして、こんな便利な機能があるのだと驚いたものです。

実際に動かしたコードを一部抜粋します。

```js
// 対象とするノードを取得
const target = document.head

// Chatwork設定
const chatworkConfig = {
  accountId: '',
  roomId: '',
  messageIdList: [],
  likeCount: {},
}

// オブザーバインスタンスを作成
const observer = new MutationObserver((mutations) => {
  // ACCOUNT_ID
  if (document.querySelector('#_accountId') != null) {
    const accountId = document.querySelector('#_accountId').textContent
    chatworkConfig.accountId = accountId
  }

  // ROOM_ID
  if (document.querySelector('._roomHeadPin') != null) {
    const roomHeadPin = document.querySelector('._roomHeadPin')
    chatworkConfig.roomId = roomHeadPin.dataset.roomid
  }
  .
  .
  .
});

// オブザーバの設定
const config = { attributes: true, childList: true, characterData: true }
 
// 対象ノードとオブザーバの設定を渡す
observer.observe(target, config);
```

重要な部分は「__対象とするノードを取得__」の部分です。

```js
// 対象とするノードを取得
const target = document.head
```

`head`にしてあります。なぜbodyではなく、headにする必要があったのか。
`body`では、左側のチャットのルームをクリックしたり、新しい通知が来たタイミングではDOMの変更をキャッチできませんでした。
どこが変更するのかをチェックしていたところ、`<title>`が変更されていることに気付き、今回はこちらで対応することにしました。
主に、`<head></head>`下層の`<title></title>`の変更を常時監視するようにしました。
そうすることで、上記であった問題は解決できました。

#### ルームを開いたときに「ぴぷいね（いいね）」数を取得する

もうすでにカオスな実装になっている自負はあるのですが、ハマったポイントをご紹介します。
こちらも一部抜粋します。

```js
const getMessageLikeCounts = async () => {
  const idList = chatworkConfig.messageIdList.reverse()
  const latestList = idList.slice(0, 10); // 最新10件取得

  const params = { messageIds: latestList.join(',') }
  try {
    const { data } = await axios.get(`${URL}/chatwork_rooms/${chatworkConfig.roomId}`, { params })
    if (data.length > 0) {
      data.forEach((el) => {
        chatworkConfig.likeCount[el['message_id']] = el['like_count']
      });
    }
    createMessage()
  } catch (err) {
    console.error(err);
  }
}

const setAttributes = (el, attrs) => {
  for (var key in attrs) {
    el.setAttribute(key, attrs[key]);
  }
}

const createMessage = () => {
  // messageItems
  if (document.querySelector('._message') != null) {
    const messageItems = document.querySelectorAll("._message")
    messageItems.forEach((message, _i) => {
      const messageId = message.dataset.mid
      const messageText = (message.querySelector('pre') != null) ? message.querySelector('pre').textContent : ''
      const avatarId = (message.querySelector('._avatarHoverTip') != null) ? message.querySelector('._avatarHoverTip').dataset.aid : ''
      const likeCount = chatworkConfig.likeCount[messageId]

      // Node重複チェック
      if (message.firstChild != null && message.firstChild.className != `message_btn_${messageId}`) {
        const btnMessage = document.createElement('div')
        setAttributes(btnMessage, { "class": `message_btn_${messageId}` })
        message.insertBefore(btnMessage, message.firstChild)
        if (document.querySelector(`.message_btn_${messageId}`) != null) {
          ReactDOM.render(
            <Message chatworkConfig={chatworkConfig} messageId={messageId} avatarId={avatarId} messageText={messageText} likeCount={likeCount} />,
            document.querySelector(`.message_btn_${messageId}`)
          )
        }
      }
    })
  }
}
```

今回、ルームを開いて「ぴぷいね（いいね）」を投稿(POST)&取得(GET)する際に必要な情報として、下記の情報をどこから取得してくるのかが大きなポイントでした。

- いいねした人のID
- いいねされた人のID
- Chatwork部屋のID
- ChatworkメッセージID
- Chatworkメッセージテキスト
- 投稿日時

本来であればバックエンド側のAPIから取得してくるものを、フロントのレンダリングされたDOMから取得してくる必要がありました。
もちろん、Chatworkだと、[Chatwork API](https://developer.chatwork.com/ja/index.html)なるものが用意されておりますが、今回は独自のリアクション追加になるためにこちらは利用できませんでした。

しかも、上記の取得されたIDなどは、DivタグなどのID名やClass名で取得していたため、仮にChatworkがアップデートされ、その名前に変更があった場合は動作しなくなってしまうという危惧もありました。
アップデートの追従をしないといけないかも。。なんてことを考えておりました。

```js
const latestList = idList.slice(0, 10); // 最新10件取得
```

Chatworkではルームを開いた際、40個のメッセージを取得しており、当初は40個のメッセージIDの配列で取得しようと試みましたが、Firestoreの制限で同時に取得可能なのは10個ということがわかり、最新10件を取得するようにしました。

10x4で複数回に分けて取得できていれば、不安定な動作と思われることもなかったかもしれないと悔やまれるばかりです。

```js
if (message.firstChild != null && message.firstChild.className != `message_btn_${messageId}`) {
```

また、メッセージを削除した場合のリアクションボタンが表示されない不具合にも苦しめられました。
上記の、`message.firstChild != null`が必要だったのですね。

メンバーから、ボタンが表示されないルームがあるよと教えてくれたものの自分では原因がつかめず、結局のところメンバーのお力を借りてことなきを得ました。


## 反省点

今回は、いろいろと反省点しかありませんでしたが、自分への戒めを込めて展開します。

**反省点①**

- TypeScriptを使いたかったのに使用していない
  - [Chatworkブログ](https://creators-note.chatwork.com/entry/2020/11/05/100922)から知る限り公式でも使っていたのでTypeScript使いたかった
  - 完全に技術選定ミス
    - なぜgulpのタスクランナーを使用してしまったのか
      - 安易に[ChromeExtensionの作成できるnpm](https://www.npmjs.com/package/generator-chrome-extension)に手を出してしまった
- ChromeExtensionの[公式サイト](https://developer.chrome.com/docs/extensions/mv3/getstarted/)を事前に確認しておくべきだった
  - webpackでコンパイルするべきだった

**反省点②**

- ChromeExtension動作が不安定
  - API取得時のエラーハンドリングが正確にできていなかった
  - 動作確認の際に、メンバーにもっと頼ってもよかったかもしれない
  - 一通り動いていて安心してしまった

**反省点③**

- 自分はFirestoreデータの確認ぐらい
- インフラ構築といったことやデータ設計というところにまったく着手することができなかった
  - データ設計周りはお手伝いできたかもしれない
- 今回使用したバックエンドおよびインフラ
  - Firebase
    - Authentication(認証機能)
    - Firestore(データベース)
    - Cloud Functions for Firebase(Web API)
    - Cloud Storage(クラウド上のストレージ)
    - Remote Config(フロントエンドの設定値をリモート制御)

## まとめ

冒頭でもお話ししましたが、反省点は数多くあったもののハッカソンに参加してよかったです。

完全リモートで、なかなか全員が集まれる時間が取れなかったものの、最終的な成果物が出来たことは喜びでした。

参加してみて、行動することの大切さを改めて感じることが出来ました。
私自身当初不安との戦いでしたが、間違ってもいいんだという安心感や、無知をさらけ出す勇気をもらえました。
そのような環境で制作を進めることができたのは、メンバーの方々のおかげです。
ありがとうございました。

また一緒に働きたい＆一緒のチームでよかったと思えてもらえるように、これからも日々努めていきたいなと思いました。

明日は、同じ部署の先輩、@taiteamさんの記事になります！

乞うご期待です！