Help add "message history" & "participants" to simple Chat Room with GenServer

Hi. I am learning Elixir recently, decided to make a small simple pet project “Chat Room” to learn some OTP.
The general points:
Need to create a gen_server which will have a state which

  • store last 100 messages
  • have a list of chat room participants (using join/leave functions)
  • Send message which stores in last 100 messages history AND broadcasts message to a chat room members (using PID, of course)
  • Add a monitor by chat room on participants PID to handle room leave when client process dies.

I used the via_tuple example I found. At this point, I can create a chatroom and messages in it. Now the main problem for me, due to lack of experience, is how to create a structure to store message history, members and replace this inside the GenServer so that I can do join/leave inside the room.

I would like at least some example of implementation and I will pick up from there.

This is my code for now:
server.ex

Summary
defmodule Chat.Server do
  use GenServer

  # Client API
  def start_link(name) do 
    GenServer.start_link(__MODULE__, [], name: via_tuple(name))
  end

  def add_message(room_name, message) do 
    GenServer.cast(via_tuple(room_name), {:add_message, message})
  end
  def get_messages(room_name) do 
    GenServer.call(via_tuple(room_name), :get_messages)
  end

  defp via_tuple (room_name) do
    {:via, Chat.Registry, {:chat_room, room_name}}
  end

  # SERVER
  @impl true
  def init(messages) do 
    {:ok, messages} 
  end

  @impl true
  def handle_cast({:add_message, new_message}, messages) do 
    {:noreply, [new_message | messages]} 
  end

  @impl true
  def handle_call(:get_messages, _from, messages) do 
    {:reply, messages, messages} 
  end
end

supervisor.ex

Summary
defmodule Chat.Supervisor do
  use DynamicSupervisor

  def start_link do
    DynamicSupervisor.start_link(__MODULE__, [], name: :chat_supervisor)
  end

  def start_room(name) do
    spec = %{id: Chat.Server, start: {Chat.Server, :start_link, [name]}}
    DynamicSupervisor.start_child(:chat_supervisor, spec)
  end

  def init(_) do
    DynamicSupervisor.init(
      strategy: :one_for_one,
      extra_arguments: []
    )
  end
end

registry.ex

Summary
defmodule Chat.Registry do

  use GenServer

  # API

  def start_link do
    # register our registry, with a simple name,
    # just so to reference it in the other functions.
    GenServer.start_link(__MODULE__, nil, name: :registry)
  end

  def whereis_name(room_name) do
    GenServer.call(:registry, {:whereis_name, room_name})
  end

  def register_name(room_name, pid) do
    GenServer.call(:registry, {:register_name, room_name, pid})
  end

  def unregister_name(room_name) do
    GenServer.cast(:registry, {:unregister_name, room_name})
  end

  def send(room_name, message) do
    # If we try to send a message to a process
    # that is not registered, we return a tuple in the format
    # {:badarg, {process_name, error_message}}.
    # Otherwise, we just forward the message to the pid of this room.
    case whereis_name(room_name) do
      :undefined ->
        {:badarg, {room_name, message}}

      pid ->
        Kernel.send(pid, message)
        pid
    end
  end

  # SERVER

  def init(_) do
    {:ok, Map.new}
  end

  def handle_call({:whereis_name, room_name}, _from, state) do
    {:reply, Map.get(state, room_name, :undefined), state}
  end

  def handle_call({:register_name, room_name, pid}, _from, state) do
    case Map.get(state, room_name) do
      nil ->
        # When a new process is registered, we start monitoring it
        Process.monitor(pid)
        {:reply, :yes, Map.put(state, room_name, pid)}

      _ ->
        {:reply, :no, state}
    end
  end

  def handle_info({:DOWN, _, :process, pid, _}, state) do
    # When a monitored process dies, we will receive a `:DOWN` message
    # that we can use to remove the dead pid from our registry
    {:noreply, remove_pid(state, pid)}
  end

  def remove_pid(state, pid_to_remove) do
    # And here we just filter out the dead pid
    remove = fn {_key, pid} -> pid  != pid_to_remove end
    Enum.filter(state, remove) |> Enum.into(%{})
  end
end