Powered by AppSignal & Oban Pro

Phoenix quick dirty chat

phoenix-dirty-chat.livemd

Phoenix quick dirty chat

Mix.install([
  {:phoenix_playground, "~> 0.1.3"}
])

Phoenix Playground

defmodule DemoTest do
  use Phoenix.LiveView

  def mount(_params, _session, socket) do
    if connected?(socket), do: MessageStorage.subscribe()
    messages = MessageStorage.get_messages()

    {:ok, assign(socket, messages: messages, message_value: "")}
  end

  def render(assigns) do
    ~H"""
    <head>
      <link href="https://cdn.jsdelivr.net/npm/daisyui@4.12.10/dist/full.min.css" rel="stylesheet" type="text/css" />
      <script src="https://cdn.tailwindcss.com"></script>
    </head>

    <body>

      <div class="h-screen flex items-center justify-center">

      <div class="mockup-phone border-primary">
        <div class="camera"></div>
        <div class="display">
          <div class="artboard artboard-demo phone-1">

            <div class="chat chat-end w-full">
              <div class="chat-bubble" :for={message <- @messages}><%= message %></div>
            </div>

            <form phx-submit="new_message" >
              <input type="text" name="text" value={@message_value} placeholder="Message..." autofocus>
              <input type="submit" />
            </form>

          </div>
        </div>
      </div>

    </div>

    </body>
    """
  end

  def handle_event("new_message", %{"text" => message}, socket) do
    MessageStorage.add_message(message)

    # works only once (input is in focus and due to that wont be updated)
    # https://github.com/phoenixframework/phoenix_live_view/issues/624
    # socket = assign(socket, :message_value, nil)

    {:noreply, socket}
  end

  def handle_info({:message_last_deleted}, socket) do
    if length(socket.assigns.messages) == 0 do
      {:noreply, socket}
    else
      [_|tail] = socket.assigns.messages
      {:noreply, assign(socket, messages: tail )}
    end
  end

  def handle_info({:message_created, message}, socket) do
    {:noreply, assign(socket, messages: socket.assigns.messages ++ [message] )}
  end

end

Our storage and topic logic

defmodule MessageStorage do
  use GenServer

  @cleanup_interval 5_000 # 5 seconds

  def start_link(_opts) do
    GenServer.start_link(__MODULE__, [], name: __MODULE__)
  end

  def init(_args) do
    schedule_cleanup()
    {:ok, []}
  end

  def add_message(message) do
    GenServer.cast(__MODULE__, {:add_message, message})

    broadcast({:ok, message}, :message_created)
  end

  def get_messages() do
    GenServer.call(__MODULE__, :get_messages)
  end

  def handle_cast({:add_message, message}, state) do
    {:noreply, [message | state]}
  end

  def handle_call(:get_messages, _from, state) do
    {:reply, Enum.reverse(state), state}
  end

  def handle_info(:cleanup, state) do
    new_state = clean_oldest_message(state)
    schedule_cleanup()

    Phoenix.PubSub.broadcast(:demo_pub_sub, "message_topic", {:message_last_deleted})

    {:noreply, new_state}
  end

  def subscribe, do: Phoenix.PubSub.subscribe(:demo_pub_sub, "message_topic")

  defp broadcast({:error, _reason} = error, _event), do: error
  defp broadcast({:ok, message}, event) do
    Phoenix.PubSub.broadcast(:demo_pub_sub, "message_topic", {event, message})
    {:ok, message}
  end

  defp schedule_cleanup do
    Process.send_after(self(), :cleanup, @cleanup_interval)
  end

  defp clean_oldest_message([]), do: []
  defp clean_oldest_message(state) do
    List.delete_at(state, -1)
  end
end

Start tree

children = [
  Supervisor.child_spec({Phoenix.PubSub, name: :demo_pub_sub}, id: :demo_pub_sub),
  MessageStorage,
]

PhoenixPlayground.start(live: DemoTest, child_specs: children)