🦊

Amazon Chime SDK React Component Libraryのデモにチャット機能を追加した

2020/11/30に公開

前回、Amazon Chime SDK React Component Libraryについて簡単な紹介記事を投稿しました。執筆時点での、このライブラリの最新のアップデートであるver1.5.0ではChat関連のコンポーネントが追加されました。今回、これを使ってAWS公式のデモにChat機能を追加してみようと思います。

具体的には、このようなものを作ります。

chat_overview

なお、今回はReactを使ったコーディングを使った少し込み入った話となります。
単純に動かしてみたいだけという方は、最後にリポジトリのURLを示しておりますので、そちらからコードを入手して動かしてみてください。

前提

今回は、公式が提供しているデモをベースにChat機能を追加します。まずは、前回の記事を参考にver1.5.0のサンプルが動く状態にしておいてください。

実現方針

Amazon Chime SDK React Component Libraryは、Amazon Chimeの機能を簡単に利用できるように、React のコンポーネントに加えて、そのコンポーネントの裏で動くフックやプロバイダ(Context API)も提供しています。

今回、Chat関連のコンポーネントが追加されたということで、Chat機能を実現するためのプロバイダなり、フックなりを提供してくれているだろうと期待したのですが、、、残念ながら、まだないようです。こちらのissueを見ると、自分でリアルタイムシグナリング用のデータメッセージを使って実装しろと言っております。リアルタイムシグナリング用のデータメッセージについては、以前の記事でご紹介したのでご参照ください。

ということで、リアルタイムシグナリング用のデータメッセージを使うプロバイダを自分で実装する方針で進めたいと思います。

RealitimeSubscribeChatStateProvider

今回作成するプロバイダを、RealitimeSubscribeChatStateProviderと名付けることとします。以下、プロバイダの内容を、重要と思われるところを抜粋して説明します。ソースコード全体はこちらをご覧ください。

このプロバイダのuseRealitimeSubscribeChatState()を呼び出すことで、Chatの機能とデータを参照できるようにします。

Stateの定義

useRealitimeSubscribeChatState()で提供されるState(Chat機能とデータ)を定義します。単純なChat機能を作るだけであれば、下記の2つの変数を定義すれば十分かと思います。

export interface RealitimeSubscribeChatStateValue {
    chatData: RealtimeData[]
    sendChatData: (mess: string) => void
}

なお、interface内で使われているRealtimeDataは、実際に送受信するデータです。今回は、下記のようなデータ構造を定義してみました。

export type RealtimeData = {
    uuid: string
    data: any
    createdDate: number
    senderName: string
    //<snip>
}

useRealitimeSubscribeChatState()

上記のStateを提供するメソッドは下記のようにしています。Context APIを使ってプロバイダを作成するときは、ほぼ定形だと思いますので説明は省略します。

export const useRealitimeSubscribeChatState = (): RealitimeSubscribeChatStateValue => {
    const state = useContext(RealitimeSubscribeChatStateContext)
    if (!state) {
       // handle exception
    }
    return state
}

Providerの定義

次はプロバイダを作っていきます。

リアルタイムシグナリング用のデータメッセージの送受信は、AudioVideoFacadeから実行します。このAudioVideoFacadeの参照はuseAudioVideo()により取得できます(1-1)。また、ユーザ名はuseAppState()により取得できます(1-2)。ChatのテキストデータはuseStateを使ってこのプロバイダで管理します(1-3)。

(2-1)では、リアルタイムシグナリング用のデータメッセージを使ったデータ送信関数を定義しています。audioVideo(AudioVideoFacade)のメソッドを呼び出して送信をしますが、このときtopicを指定できるので、"CHAT"と指定しておきます(2-2)。また、送信したデータは自分は受信できない仕様[1]なので、送信データをChatのテキストデータに追加しておきます(2-3)。

useEffectでリアルタイムシグナリング用のデータメッセージの受信用関数の登録(と削除)を行います(3-1), (3-2)。受信用の関数自体は(3-3)で示したとおり、受信したデータをパースして、Chatのテキストデータに追加するだけです。

export const RealitimeSubscribeChatStateProvider = ({ children }: Props) => {
    const audioVideo = useAudioVideo()           // <----- (1-1)
    const { localUserName } = useAppState()   // <----- (1-2)
    const [chatData, setChatData] = useState([] as RealtimeData[]) // <----- (1-3)

    const sendChatData = (text: string) => {  // <----- (2-1)
        const mess: RealtimeData = {
            uuid: v4(),
            data: text,
            createdDate: new Date().getTime(),
            senderName: localUserName
        }
        audioVideo!.realtimeSendDataMessage("CHAT" as DataMessageType, JSON.stringify(mess)) // <----- (2-2)
        setChatData([...chatData, mess]) // <----- (2-3)
    }

    const receiveChatData = (mess: DataMessage) => { // <----- (3-3)
        const data = JSON.parse(mess.text()) as RealtimeData
        setChatData([...chatData, data])
    }

    useEffect(() => {
        audioVideo!.realtimeSubscribeToReceiveDataMessage(    // <----- (3-1)
            "CHAT" as DataMessageType,
            receiveChatData
        )
        return () => {
            audioVideo!.realtimeUnsubscribeFromReceiveDataMessage("CHAT" as DataMessageType) // <----- (3-2)
        }
    })

    const providerValue = {
        chatData,
        sendChatData,
    }
    return (
        <RealitimeSubscribeChatStateContext.Provider value={providerValue}>
            {children}
        </RealitimeSubscribeChatStateContext.Provider>
    )
}

以上がRealitimeSubscribeChatStateProviderの実装になります。

GUI

次に、Chat画面を表示する部分を説明してきます。こちらも重要と思われる部分を抜粋して説明していきます。

RealitimeSubscribeStateProviderを追加

上記作成したプロバイダを会議室で利用できるようにMeetingViewのDOMを一部変更します。(1)の部分にRealitimeSubscribeStateProviderを追加します。これで、下位のViewでChat機能を使用できるようになります。

const MeetingView = () => {
  useMeetingEndRedirect();
  const { showNavbar, showRoster, showChat } = useNavigation();

  return (
    <UserActivityProvider>
      <StyledLayout showNav={showNavbar} showRoster={showRoster || showChat}>
        <RealitimeSubscribeStateProvider>          // <--- (1)
          <StyledContent>
            <MeetingMetrics />
            <VideoTileGrid
              className="videos"
              noRemoteVideoView={<MeetingDetails />}
            />
            <MeetingControls />
          </StyledContent>
          <NavigationControl />
        </RealitimeSubscribeStateProvider>
      </StyledLayout>
    </UserActivityProvider>
  );
};

ChatView

Chatの画面は次のようにしました。
(1)で先ほど作成したuseRealitimeSubscribeChatState()を用いてchatデータとデータ送信関数の参照を得ます。(2-1),(2-2)で、Amazon Chime SDK React Component LibraryのChatコンポーネントを使って、表示部分を生成しています(詳細は後述)。(3)は、chatデータの送信関数を呼び出しています。

const ChatView = () => {
  const { localUserName } = useAppState()
  const { closeChat } = useNavigation();
  const { chatData, sendChatData } = useRealitimeSubscribeChatState()  // <---- (1)
  const [ chatMessage, setChatMessage] = useState('');
  
  const attendeeItems = []
  for (let c of chatData) {                      // <---- (2-1)
    const senderName = c.senderName
    const text = c.data
    const time = (new Date(c.createdDate)).toLocaleTimeString('ja-JP')
    attendeeItems.push(
      <ChatBubbleContainer timestamp={time} key={time+senderName}>   // <---- (2-2)
        <ChatBubble
          variant= {localUserName === senderName ? "outgoing" : "incoming"}
          senderName={senderName}
          content={text}
          showTail={true}
          css={bubbleStyles}
        />
      </ChatBubbleContainer>
    )
  }

  return (

    <Roster className="roster">
      <RosterHeader title="Chat" onClose={()=>{closeChat}}>
      </RosterHeader>
      {attendeeItems}
      <br/>
      <Textarea
        //@ts-ignore
        onChange={e => setChatMessage(e.target.value)}
        value={chatMessage}
        placeholder="input your message"
        type="text"
        label=""
        style={{resize:"vertical",}}
      />
      <PrimaryButton 
        label="send" 
        onClick={e=>{
          setChatMessage("")
          sendChatData(chatMessage)    // <---- (3)
        }}
      />
    </Roster>
  );
}

このChatViewをnavibarから表示できるようにすれば完成です。
navibarからの表示は、今回は本質ではなく、また、細かい修正が複数入りますので、説明を省略します。下記のリポジトリのコミットログから変更部分を確認してみてください。

動作確認

では、動作を確認してみましょう。
こんな感じでメッセージを入力すると相手と自分の画面にメッセージが表示されると思います。
chat_overview

自分のメッセージ( variant属性がoutgoing)の場合は青色の吹き出しになります。他者のメッセージの場合は白の吹き出しになります。
chat1

吹き出しのtail(漫画的に言うと吹き出しの発生箇所を示すところ)の表示有無をtail属性で切り替えられますが、向きは変えられません。Lineになれ親しんでいる日本人にとっては少し使いづらいかもしれませんね。

chat2

その他、時刻を省略したり、アクションボタンをつけられたりします。
chat3

リポジトリ

今回作成したソースコードは次のリポジトリの"chat_feature"ブランチに置いてあります。前回の記事でのデモの動かし方と同じ方法で起動できますので、動作を確認してみてください。

https://github.com/w-okada/amazon-chime-sdk-component-library-react

まとめ

以上、Amazon Chime SDK React Component Library の公式デモにチャット機能を追加して見ました。少し生のAmazon Chime SDKを触る必要がありましたが、それほど手間なく、他のコンポーネントと統一感のあるチャットGUIが作成できました。

なお、下記のリポジトリでは、ここで紹介したチャット機能以外にも、ホワイトボード機能も搭載したバージョンを公開しています。またCognito連携や仮想背景も実装しています。下記のリポジトリに公開していますので、是非ご活用ください。

https://github.com/w-okada/flect-chime-sdk-demo

  • Virtual 背景
    vbg

  • Whiteboard
    whiteboard

追記

ちょうど先週末に、公式のリポジトリにもChatのデモが追加されたようですね。

脚注
  1. これは個人的には引っかかる仕様ですが。[参照](https://qiita.com/wok/items/83a8700d5c09cc1747d0 ↩︎

Discussion