defmodule SlaxWeb.ChatRoomLive do @moduledoc false use SlaxWeb, :live_view require Logger import SlaxWeb.RoomComponents alias Slax.Accounts alias Slax.Accounts.User alias Slax.Chat alias Slax.Chat.Message alias Slax.Chat.Room alias SlaxWeb.OnlineUsers @impl Phoenix.LiveView def render(assigns) do ~H"""

Slax

<.toggler on_click={toggle_rooms()} dom_id="rooms-toggler" text="Rooms" />
<.room_link :for={room <- @rooms} room={room} active={room.id == @room.id} />
<.toggler on_click={toggle_users()} dom_id="users-toggler" text="Users" />
<.user :for={user <- @users} user={user} online={OnlineUsers.online?(@online_users, user.id)} />

#{@room.name} <.link :if={@joined?} class="font-normal text-xs text-blue-600 hover:text-blue-700" navigate={~p"/rooms/#{@room}/edit"} > Edit

<%= if @hide_topic? do %> [Topic hidden] <% else %> {@room.topic} <% end %>
<%= for {dom_id, message} <- @streams.messages do %> <%= if message == :unread_marker do %>
New
<% else %> <.message current_user={@current_user} dom_id={dom_id} message={message} timezone={@timezone} /> <% end %> <% end %>
<.form id="new-message-form" for={@new_message_form} phx-change="validate-message" phx-submit="submit-message" class="flex items-center border-2 border-slate-300 rounded-sm p-1" >

{@room.name}

{@room.topic}

Join Room
<.link navigate={~p"/rooms"} href="#" class="text-sm text-slate-500 underline hover:text-slate-600" > Back to All Rooms
<.modal id="new-room-modal" show={@live_action == :new} on_cancel={JS.navigate(~p"/rooms/#{@room}")} > <.header>New chat room <.room_form form={@new_room_form} /> """ end @impl Phoenix.LiveView def mount(_params, _session, socket) do rooms = Chat.list_joined_rooms_with_unread_count(socket.assigns.current_user) users = Accounts.list_users() timezone = get_connect_params(socket)["timezone"] if connected?(socket) do OnlineUsers.track(self(), socket.assigns.current_user) end OnlineUsers.subscribe() Enum.each(rooms, &Chat.subscribe_to_room/1) socket |> assign( hide_topic?: false, online_users: OnlineUsers.list(), rooms: rooms, timezone: timezone, users: users ) |> assign_room_form(Chat.change_room(%Room{})) |> stream_configure(:messages, dom_id: fn %Message{id: id} -> "messages-#{id}" :unread_marker -> "messages-unread-marker" end ) |> ok() end @impl Phoenix.LiveView def handle_params(params, _session, socket) do room = case Map.fetch(params, "id") do {:ok, id} -> Chat.get_room!(id) :error -> Chat.get_first_room!() end last_read_id = Chat.get_last_read_id(room, socket.assigns.current_user) messages = room |> Chat.list_messages_in_room() |> maybe_insert_unread_marker(last_read_id) Chat.update_last_read_id(room, socket.assigns.current_user) socket |> assign( hide_topic?: false, joined?: Chat.joined?(room, socket.assigns.current_user), page_title: "# #{room.name}", room: room ) |> stream(:messages, messages, reset: true) |> assign_message_form(Chat.change_message(%Message{})) |> push_event("scroll_messages_to_bottom", %{}) |> update(:rooms, fn rooms -> room_id = room.id Enum.map(rooms, fn %Room{id: ^room_id} = room -> %Room{room | unread_message_count: 0} other_room -> other_room end) end) |> noreply() end @impl Phoenix.LiveView def handle_event("toggle-topic", _params, socket) do {:noreply, update(socket, :hide_topic?, &(!&1))} end @impl Phoenix.LiveView def handle_event("validate-message", %{"message" => message_params}, socket) do changeset = Chat.change_message(%Message{}, message_params) {:noreply, assign_message_form(socket, changeset)} end @impl Phoenix.LiveView def handle_event("validate-room", %{"room" => room_params}, socket) do changeset = socket.assigns.room |> Chat.change_room(room_params) |> Map.put(:action, :validate) {:noreply, assign_room_form(socket, changeset)} end @impl Phoenix.LiveView def handle_event("submit-message", %{"message" => message_params}, socket) do %{current_user: user, room: room} = socket.assigns socket = if Chat.joined?(room, user) do case Chat.create_message(room, user, message_params) do {:ok, _message} -> assign_message_form(socket, Chat.change_message(%Message{})) {:error, changeset} -> assign_message_form(socket, changeset) end else socket end {:noreply, socket} end @impl Phoenix.LiveView def handle_event("delete-message", %{"id" => message_id}, socket) do Chat.delete_message_by_id(message_id, socket.assigns.current_user) {:noreply, socket} end @impl Phoenix.LiveView def handle_event("join-room", _, socket) do current_user = socket.assigns.current_user room = socket.assigns.room Chat.join_room!(room, current_user) Chat.subscribe_to_room(room) socket = assign(socket, joined?: true, rooms: Chat.list_joined_rooms_with_unread_count(current_user)) {:noreply, socket} end @impl Phoenix.LiveView def handle_event("save-room", %{"room" => room_params}, socket) do case Chat.create_room(room_params) do {:ok, room} -> Chat.join_room!(room, socket.assigns.current_user) {:noreply, socket |> put_flash(:info, "Created room") |> push_navigate(to: ~p"/rooms/#{room}")} {:error, changeset} -> {:noreply, assign_room_form(socket, changeset)} end end @impl Phoenix.LiveView def handle_info({:new_message, message}, socket) do room = socket.assigns.room socket = cond do message.room_id == room.id -> Chat.update_last_read_id(room, socket.assigns.current_user) socket |> stream_insert(:messages, message) |> push_event("scroll_messages_to_bottom", %{}) message.user_id != socket.assigns.current_user.id -> update(socket, :rooms, fn rooms -> room_id = message.room_id Enum.map(rooms, fn %Room{id: ^room_id} = room -> %Room{room | unread_message_count: room.unread_message_count + 1} other_room -> other_room end) end) true -> socket end {:noreply, socket} end @impl Phoenix.LiveView def handle_info({:deleted_message, message}, socket) do {:noreply, stream_delete(socket, :messages, message)} end @impl Phoenix.LiveView def handle_info(%{event: "presence_diff", payload: diff}, socket) do online_users = OnlineUsers.update(socket.assigns.online_users, diff) {:noreply, assign(socket, online_users: online_users)} end defp assign_room_form(socket, changeset) do assign(socket, :new_room_form, to_form(changeset)) end attr :active, :boolean, required: true attr :room, Room, required: true @spec room_link(Phoenix.LiveView.Socket.assigns()) :: Phoenix.LiveView.Rendered.t() defp room_link(assigns) do ~H""" <.link class={[ "flex items-center h-8 text-sm pl-8 pr-3", (@active && "bg-slate-300") || "hover:bg-slate-300" ]} patch={~p"/rooms/#{@room}"} > <.icon name="hero-hashtag" class="h-4 w-4" /> {@room.name} <.unread_message_count count={@room.unread_message_count} /> """ end attr :count, :integer, required: true defp unread_message_count(assigns) do ~H""" 0} class="flex items-center justifiy-center bg-blue-500 rounded-full font-medium h-5 px-2 ml-auto text-xs text-white" > {@count} """ end attr :user, User, required: true attr :online, :boolean, default: false defp user(assigns) do ~H""" <.link class="flex items-center h-8 hover:bg-gray-300 text-sm pl-8 pr-3" href="#">
<%= if @online do %> <% else %> <% end %>
{@user.username} """ end attr :dom_id, :string, required: true attr :on_click, JS, required: true attr :text, :string, required: true defp toggler(assigns) do ~H""" """ end defp toggle_rooms do JS.toggle(to: "#rooms-toggler-chevron-down") |> JS.toggle(to: "#rooms-toggler-chevron-right") |> JS.toggle(to: "#rooms-list") end defp toggle_users do JS.toggle(to: "#users-toggler-chevron-down") |> JS.toggle(to: "#users-toggler-chevron-right") |> JS.toggle(to: "#users-list") end attr :current_user, User, required: true attr :dom_id, :string, required: true attr :message, Message, required: true attr :timezone, :string, required: true defp message(assigns) do ~H"""
<.link class="text-sm font-semibold hover:underline"> {@message.user.username} {message_timestamp(@message, @timezone)}

{@message.body}

""" end defp message_timestamp(message, timezone) do message.inserted_at |> Timex.Timezone.convert(timezone) |> Timex.format!("%-l:%M %p", :strftime) end defp assign_message_form(socket, changeset) do assign(socket, :new_message_form, to_form(changeset)) end defp maybe_insert_unread_marker(messages, nil) do messages end defp maybe_insert_unread_marker(messages, last_read_id) do {read, unread} = Enum.split_while(messages, &(&1.id <= last_read_id)) if unread == [] do read else read ++ [:unread_marker | unread] end end end