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


この記事は、[Elixir Advent Calendar 2023 シリーズ7](https://qiita.com/advent-calendar/2023/elixir)の10日目です。

---

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

実装時にちょうどよい[参考記事](https://fly.io/phoenix-files/building-a-chat-app-with-liveview-streams/)を見つけたので、数回に分けてマイペースに紹介＆解説します。

# 概要
本記事では、コードと共にLiveview StreamsとPubSubを解説します。
前回は[こちら](https://fly.io/phoenix-files/building-a-chat-app-with-liveview-streams/)。

本記事で解説するコードは[下記](https://github.com/SophieDeBenedetto/stream_chat)です。

https://github.com/SophieDeBenedetto/stream_chat

# 準備

リポジトリをクローン
```bash
git clone https://github.com/SophieDeBenedetto/stream_chat.git
```

依存関係のインストール、マイグレーション、初期データの登録
```bash
cd stream_chat && mix setup
```

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

```elixir: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を開きます。

https://github.com/SophieDeBenedetto/stream_chat

# Liveview Stream

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

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

```elixir: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で通知
2. 通知を受け取ってsocketのstreamに追加

順番に解説します。

### メッセージをDB保存

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

```elixir: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}`を送信します。

```elixir: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}")
`で行われています。

```elixir: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`で実装されています。

```elixir: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]

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

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

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

[Liveview 0.19 リリースノート](https://www.phoenixframework.org/blog/phoenix-liveview-0.19-released)
:::


# 終わりに

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