LiveViewで認証付きのチャットアプリを構築する(入力ステータスの表示)

はじめに

Facebookメッセンジャー等を見ると、相手がメッセージを入力しているときに入力状況を示すアニメーションが表示されます。
そのような機能を本チャットボードにも追加します。
本チャットボードでは、入力中に入力しているユーザーの横に「(typing...)」という表示を行うようにします。

本テーマについて

本テーマは7記事構成になっています。
7記事それぞれで取り扱う内容は以下です。

  1. プロジェクト作成からログイン処理まで
  2. 認証機能の作成
  3. チャットボードのコンテキスト周り作成
  4. チャットボードの画面周り作成
  5. Phoenix.PubSubを使用したリアルタイム更新
  6. Phoenix.Presenceを使用したアクティブユーザー表示
  7. 入力ステータスの表示(本記事)

本テーマは以下の方針で記述します。

  • 機能実装は本テーマに沿った必要最小限のみ
  • UIもほぼそのまま
  • 機能へのアクセスはURLで直に指定を想定

完成版の画面イメージは以下のようになっています。

lv_chat

入力状態表示の実装

追跡情報の追加

typing に入力ステータスをもたせることにします。
それを追跡情報に追加します。
ハイライトされている箇所が今回追加したところです。

# /lib/lv_chat_web/views/board_live_view.ex
def mount(session = %{current_user: user, board: board}, socket) do
  Presence.track_presence(
    self(),
    board.id |> topicId,
    user.id,
    %{
      name: user.name,
      id: user.id,
      typing: false    }
  )
  ...
end

入力状態の定義

入力状態を以下のように定義します。

項目
入力中inputに変化がある
入力中ではないinputからフォーカスが外れている

実際のイベントとあわせて状態遷移図にすると以下のようになります。

NotTypingTypingmountphx_changephx_changephx_blur

イベントバインディング

イベントの指定

状態遷移図の通りにイベントを指定します。
formとinputそれぞれの要素に対して指定します。

# /lib/lv_chat_web/templates/board/show.html.leex
<div class="form-group">
  <%= f = form_for @comment, "#", [phx_submit: :add_comment, phx_change: :typing] %>    <%= text_input f, :body, phx_blur: "stop_typing" %>    <%= submit "Send" %>
  </form>
</div>

typingイベントハンドラの実装

入力中の処理です。
タイピングしているユーザーのIDと %{typing: true} を指定しています。
ポイントとなる箇所をハイライトしています。

# /lib/lv_chat_web/views/board_live_view.ex
def handle_event("typing", _value, socket = %{assigns: %{board: board, current_user: user}}) do
  topic = board.id |> topicId
  key = user.id
  payload = %{typing: true}
  Presence.update_presence(self(), topic, key, payload)
  {:noreply, socket}
end

ここで update_presence は以下のように実装しています。

# /lib/lv_chat_web/channels/presence.ex
def update_presence(pid, topic, key, payload) do
  metas =
    Presence.get_by_key(topic, key)
    |> extract_metadata
    |> Map.merge(payload)

  Presence.update(pid, topic, key, metas)
end

extract_metadata についてですが、もとのデータは以下のような形で取得されます。

%{metas: [%{id: 1, name: "freddie", phx_ref: "OoxOA/GE9hY=", typing: false}]}

変換後は以下のようになります。

%{id: 1, name: "freddie", phx_ref: "OoxOA/GE9hY=", typing: false}

複数のユーザーがそのチャットボードにアクセスしているときは、リストとして取得されます。

[
  %{id: 1, name: "freddie", phx_ref: "OoxOA/GE9hY=", typing: false},
  %{id: 2, name: "brian", phx_ref: "aUeSKX6opX0=", typing: false}
]

stop_typingイベントハンドラの実装

入力を終了したときの処理です。
タイプング状態をクリアしています。
ポイントとなる箇所をハイライトしています。

# /lib/lv_chat_web/views/board_live_view.ex
def handle_event(
      "stop_typing",
      %{"value" => val},
      socket = %{assigns: %{board: board, current_user: user}}
    ) do
  topic = board.id |> topicId
  key = user.id
  payload = %{typing: false}
  comment = LvChat.Meeting.change_comment(%Meeting.Comment{},%{body: val})
  Presence.update_presence(self(), topic, key, payload)
  {:noreply, assign(socket, comment: comment)}end

入力ステータスの表示

typingtrue の場合には「(typing...)」と表示するようにしています。

# /lib/lv_chat_web/templates/board/show.html.leex
<h3>Users</h3>
<ul id="user-container">
  <%= for user <- @users do %>
    <li id="<%= user.phx_ref %>"><%= user.name %><%= if user.typing, do: "(typing...)" %></li>  <% end %>
</ul>

全体

全体的には、以下のようになります。

# /lib/lv_chat_web/templates/board/show.html.leex
<div class="form-group">
  <%= f = form_for @comment, "#", [phx_submit: :add_comment, phx_change: :typing] %>    <%= text_input f, :body, phx_blur: "stop_typing" %>    <%= submit "Send" %>
  </form>
</div>

<div id="msg-container" phx-update="replace" style="height: 200px; overflow-y: scroll;" phx-hook="NewComment">
  <ul style="list-style: none;">
  <%= for comment <- @comments do %>
    <li id="<%= comment.id %>"><%= comment.user.name %> :  <%= comment.body %> </li>
  <% end %>
</ul>
</div>

<h3>Users</h3>
<ul id="user-container">
  <%= for user <- @users do %>
    <li id="<%= user.phx_ref %>"><%= user.name %><%= if user.typing, do: "(typing...)" %></li>  <% end %>
</ul>

これで入力ステータスの表示は完了です。

終わりに

一連の記事で、Phoenix.LiveView を用いたそこそこ実用的なアプリを作りました。
ほぼ JavaScript のコードを書くことなく(一部DOM操作で書いた)、ElixirのみでリアルタイムなWebアプリを作成できるのは便利ですね。
ちなみに、今回は見た目的には全く考慮していませんのであしからず。

一連のソースコードは以下に置いてあります。

ソースコード