LiveViewで認証付きのチャットアプリを構築する(チャットボードの作成)

はじめに

前回までで、認証機能が加わりユーザーの区別ができるようになりました。
これで誰が発言したかを表示できるようになり、チャットっぽい感じになってきました。
今回は、チャットに必要なコンテキスト周りを作成していきます。
本記事でLiveViewでコメント表示をするための前準備が完了します。

本テーマについて

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

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

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

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

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

lv_chat

これから作るもの

シナリオ

以下のようなシナリオを想定します。

  • ユーザーはボードを作成
  • そのボードのオーナーになる
  • 認証済みユーザーはボードに対しコメントを行う
  • コメントはリアルタイムにボード参加者に反映される

関連付け

これから BoardComment を定義し、関連付けを行っていきます。
以下のような関連を作ります。

UserBoardComment1*

チャットボードの作成

まずはチャットボードを作ります。

スキーマについて

チャットボードのスキーマを以下のようにします。

メンバー備考
namestring
  • ボード名
descriptionstring
  • ボードの説明
user_idreferences:users
  • ボードの作成者

コンテキストの作成

上記スキーマに対して、コンテキストを Meeting として作成します。

$ mix phx.gen.html Meeting Board boards name:string description:string user_id:references:users

Boardモジュールの変更

自動生成されたスキーマに関連付けが行われるように変更します。

  schema "boards" do
    field :description, :string
    field :name, :string
-   field :user_id, :id
+   belongs_to :user, LvChat.Accounts.User

    timestamps()
  end

最終的に以下の様になります。

# /lib/lv_chat/meeting/board.ex
schema "boards" do
  field :description, :string
  field :name, :string

  belongs_to :user, LvChat.Accounts.User

  timestamps()
end

マイグレーションの実行

マイグレーションファイルは、生成されたものを使用します。
ということで、早速マイグレーションを行います。

$ mix ecto.migrate

RouterへAPIの追加

認証が必要なページとしてRouterに記述します。

# /lib/lv_chat_web/router.ex
scope "/", LvChatWeb do
  pipe_through [:browser, :authenticate_user]

  get "/", PageController, :index
  resources "/boards", BoardControllerend

チャットボードにユーザーを紐付ける

コンテキストの変更

チャットボード作成時にユーザーとの関連を追加します。
変更点は以下です。

  • 引数としてユーザーを受け取るようにする
  • ユーザーとチャットボードに関連付けを行う
# /lib/lv_chat_web/meeting.ex
alias LvChat.Accountsdef create_board(%Accounts.User{} = user, attrs \\ %{}) do
  %Board{}
  |> Board.changeset(attrs)  |> Ecto.Changeset.put_assoc(:user, user)
  |> Repo.insert()
end

BoardControllerの変更

current_userの引数渡し

BoardController において、関数に current_user が引数で渡ってくると便利です。
ということで、current_user が引数として渡るように細工します。
以下の記述を BoardController モジュールの先頭に記述します。

# /lib/lv_chat_web/controllers/board_controller.ex
def action(conn, _) do
  args = [conn, conn.params, conn.assigns.current_user]
  apply(__MODULE__, action_name(conn), args)
end

これで関数の定義を以下のように書けるようになります。

- def some_func(conn, params) do
+ def some_func(conn, params, current_user) do

これに伴い BoardController の関数すべてを some_func/2 から some_func/3 に変更する必要があります。

create関数の変更

create関数で、さきほど作成したMeeting.create/2 を使用するように変更します。

# /lib/lv_chat_web/controllers/board_controller.ex
def create(conn, %{"board" => board_params}, current_user) do
  case Meeting.create_board(current_user, board_params) do
    {:ok, board} ->
      conn
      |> put_flash(:info, "チャットボードの作成に成功しました。")
      |> redirect(to: Routes.board_path(conn, :show, board))

    {:error, %Ecto.Changeset{} = changeset} ->
      render(conn, "new.html", changeset: changeset)
  end
end

コメントの作成

次にコメントを作成します。

スキーマについて

コメントのスキーマを以下のようにします。

メンバー備考
bodytext
  • コメント本文
user_idreferences:users
  • コメント作成者
board_idreferences:boards
  • コメントされたチャットボード

スキーマの作成

以下のコマンドでスキーマを作成します。

$ mix phx.gen.schema Meeting.Comment comments body:text user_id:references:users board_id:references:boards

Commentモジュールの変更

自動生成されたスキーマに関連付けが行われるように変更します。

-  field :user_id, :id
-  field :board_id, :id

+  belongs_to :user, LvChat.Accounts.User
+  belongs_to :board, LvChat.Meeting.Board

changesetでデフォルト引数を指定します。

- def changeset(comment, attrs) do
+ def changeset(comment, attrs \\ %{}) do

最終的には以下のようになります。

# /lib/lv_chat/meeting/comment.ex
defmodule LvChat.Meeting.Comment do
  use Ecto.Schema
  import Ecto.Changeset

  schema "comments" do
    field :body, :string

    belongs_to :user, LvChat.Accounts.User    belongs_to :board, LvChat.Meeting.Board
    timestamps()
  end

  @doc false
  def changeset(comment, attrs \\ %{}) do    comment
    |> cast(attrs, [:body])
    |> validate_required([:body])
  end
end

Boardモジュールの変更

boardsスキーマに対し、has_manyを追加します。

# /lib/lv_chat/meeting/board.ex
schema "boards" do
  field :description, :string
  field :name, :string

  belongs_to :user, LvChat.Accounts.User
  has_many :comments, LvChat.Meeting.Comment
  timestamps()
end

Meetingモジュールの変更

コメント関連の関数を追加します。

alias LvChat.Meeting.Comment

def change_comment() do
  change_comment(%Comment{})
end

def change_comment(%Comment{} = comment, params \\ %{}) do
  Comment.changeset(comment, params)
end

def comment_to_board(%Accounts.User{id: user_id}, board_id, attrs) do
  %Comment{board_id: board_id, user_id: user_id}
  |> Comment.changeset(attrs)
  |> Repo.insert()
end

def list_comments(%Board{} = board) do
  Repo.all(
    from c in Ecto.assoc(board, :comments),
    order_by: [asc: c.id],
    limit: 500,
    preload: [:user]
  )
end

マイグレーションの実行

必要な変更が完了したので、マイグレーションを行います。

$ mix ecto.migrate

これでデータ関連の定義が終わりました。

終わりに

コンテキスト周りが完成しました。
いよいよチャットのメインの部分を作成していきます。

チャットボードの画面周り作成