LoginSignup
12
1

Elixirのチャットアプリを実装解説〜②コード解説編〜

Last updated at Posted at 2023-12-10

この記事は、Elixir Advent Calendar 2023 シリーズ7の10日目です。


今年話題になったChatGPT。
ChatGPTクローンアプリをElixirで実装しようという動機がありました。

実装時にちょうどよい参考記事を見つけたので、数回に分けてマイペースに紹介&解説します。

概要

本記事では、コードと共にLiveview StreamsとPubSubを解説します。
前回はこちら

本記事で解説するコードは下記です。

準備

リポジトリをクローン

git clone https://github.com/SophieDeBenedetto/stream_chat.git

依存関係のインストール、マイグレーション、初期データの登録

cd stream_chat && mix setup

デフォルトで mix setupは下記のようになっています

mix.exs
  defp aliases do
    [
      setup: ["deps.get", "ecto.setup"],
      "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
      "ecto.reset": ["ecto.drop", "ecto.setup"],
      test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"],
      "assets.deploy": ["tailwind default --minify", "esbuild default --minify", "phx.digest"]
    ]
  end

コードだけ見る場合

GitHubのページを開いてキーボードで.を押してブラウザでVS Codeを開きます。

Liveview Stream

socketにチャットメッセージリストを保存

チャットアプリでは、チャットメッセージが大量に生まれるのでstreamに保存します。
とは言っても、assignの代わりに使うだけです。

lib/stream_chat_web/live/chat_live/root.ex#L72-78
  def assign_active_room_messages(socket) do
    messages = Chat.last_ten_messages_for(socket.assigns.room.id)

    socket
    |> stream(:messages, messages)
    |> assign(:oldest_message_id, List.first(messages).id)
  end

チャットメッセージを追加

このチャットアプリでは、下記のような流れでチャットメッセージを保存&ページに表示されています。

  1. メッセージをDBに保存
  2. DB保存後にPubSubで通知
  3. 通知を受け取ってsocketのstreamに追加

順番に解説します。

メッセージをDB保存

localhost/roomsを開いてページ下部のフォームからメッセージを送信すると、handle_event("save", _, socket)関数が呼び出されます。
ここでは、DBにデータを保存しているだけなのでまだページ中央のメッセージリストには反映されません。

lib/stream_chat_web/live/chat_live/message/form.ex#L47-55
  def handle_event("save", %{"message" => %{"content" => content}}, socket) do
    Chat.create_message(%{
      content: content,
      room_id: socket.assigns.room_id,
      sender_id: socket.assigns.sender_id
    })

    {:noreply, assign_changeset(socket)}
  end

DB保存後にPubSubで通知

PubSubの通知を送信する側はChat.create_message/1に実装されています。
room:#{message.room_id}の登録者にイベントnew_message、データ%{message: message}を送信します。

lib/stream_chat/chat.ex#L132-137
  def create_message(attrs \\ %{}) do
    %Message{}
    |> Message.changeset(attrs)
    |> Repo.insert()
    |> publish_message_created()
  end

  def publish_message_created({:ok, message} = result) do
    Endpoint.broadcast("room:#{message.room_id}", "new_message", %{message: message}) # ここ
    result
  end

通知を受け取ってsocketのstreamに追加

Pub Subの受信設定は、チャットルームを開いたときにEndpoint.subscribe("room:#{id}") で行われています。

lib/stream_chat_web/live/chat_live/root.ex#L16-24
  def handle_params(%{"id" => id}, _uri, %{assigns: %{live_action: :show}} = socket) do
    if connected?(socket), do: Endpoint.subscribe("room:#{id}") # ここ

    {:noreply,
     socket
     |> assign_active_room(id)
     |> assign_active_room_messages()
     |> assign_last_user_message()}
  end

受信後の処理は、handle_infoで実装されています。

lib/stream_chat_web/live/chat_live/root.ex#L28-33
  def handle_info(%{event: "new_message", payload: %{message: message}}, socket) do
    {:noreply,
     socket
     |> insert_new_message(message)
     |> assign_last_user_message(message)}
  end

assignとは違って、専用の追加関数が用意されています。
stream_insertを使ってメッセージを追加します。1

  def insert_new_message(socket, message) do
    socket
    |> stream_insert(:messages, Chat.preload_message_sender(message))
  end

このチャットアプリでは、 slackやDiscordのようにスクロールでメッセージを追加読み込みするところまで実装されています。
参考にしたリポジトリでは、Liveview 0.18時点の実装であり、Liveview v0.19で追加されたスクロールに関する機能追加は取り込まれていませんので、本記事では解説しません m(_ _)m

Liveview 0.19 リリースノート

終わりに

コードの解説をしました。
次回はスクロールによる追加読み込みを実装します。

  1. Chat.preload_message_senderは関連テーブルとして送信者のデータを取得しているだけです。

12
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
1