Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

List users for chat #1302

Merged
merged 1 commit into from
Apr 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/ex338/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ defmodule Ex338.Application do
Ex338.Repo,
{Phoenix.PubSub, pubsub_options},
Ex338Web.Telemetry,
Ex338Web.Presence,
# Start the endpoint when the application starts
Ex338Web.Endpoint,
{Oban, Application.fetch_env!(:ex338, Oban)}
Expand Down
4 changes: 2 additions & 2 deletions lib/ex338/chats.ex
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,6 @@ defmodule Ex338.Chats do
Phoenix.PubSub.broadcast(Ex338.PubSub, topic(message), {__MODULE__, event})
end

defp topic(%Message{} = message), do: "chat:#{message.chat_id}"
defp topic(%Chat{} = chat), do: "chat:#{chat.id}"
def topic(%Message{} = message), do: "chat:#{message.chat_id}"
def topic(%Chat{} = chat), do: "chat:#{chat.id}"
end
103 changes: 29 additions & 74 deletions lib/ex338_web/live/championship_live/chat_component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -52,83 +52,38 @@ defmodule Ex338Web.ChampionshipLive.ChatComponent do
@impl true
def render(assigns) do
~H"""
<div class="overflow-hidden bg-white shadow sm:rounded-lg">
<div class="py-5 border-b border-gray-200">
<ul
id="messages"
phx-update="stream"
role="list"
phx-hook="ChatScrollToBottom"
class="flex flex-col h-[800px] overflow-y-auto overflow-x-hidden pb-6"
>
<.comment :for={{id, message} <- @messages} id={id} message={message} />
</ul>

<div class="flex gap-x-3 px-4 sm:px-6">
<.user_icon name={@current_user.name} class="!mt-0" />
<.form
id="create-message"
for={@form}
phx-target={@myself}
phx-change="validate"
phx-submit="save"
class="relative flex-auto"
>
<div class="overflow-hidden rounded-lg pb-12 shadow-sm ring-1 ring-inset ring-gray-300 focus-within:ring-2 focus-within:ring-indigo-600">
<label for="comment" class="sr-only">Add your comment</label>
<.input
field={@form[:content]}
phx-debounce="blur"
type="commenttextarea"
rows="2"
class="block w-full resize-none border-0 bg-transparent py-1.5 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm sm:leading-6"
placeholder="Add your comment..."
/>
</div>

<div class="absolute inset-x-0 bottom-0 flex justify-end py-2 pl-3 pr-2">
<button
type="submit"
class="rounded-md bg-white px-2.5 py-1.5 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
>
Comment
</button>
</div>
</.form>
<div class="flex gap-x-3 px-4 sm:px-6">
<.user_icon name={@current_user.name} class="!mt-0" />
<.form
id="create-message"
for={@form}
phx-target={@myself}
phx-change="validate"
phx-submit="save"
class="relative flex-auto"
>
<div class="overflow-hidden rounded-lg pb-12 shadow-sm ring-1 ring-inset ring-gray-300 focus-within:ring-2 focus-within:ring-indigo-600">
<label for="comment" class="sr-only">Add your comment</label>
<.input
field={@form[:content]}
phx-debounce="blur"
type="commenttextarea"
rows="2"
class="block w-full resize-none border-0 bg-transparent py-1.5 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm sm:leading-6"
placeholder="Add your comment..."
/>
</div>
</div>
</div>
"""
end

defp comment(%{message: %{user: nil}} = assigns) do
~H"""
<li id={@id} class="flex gap-x-4 hover:bg-gray-50 px-4 sm:px-6 py-2">
<div class="flex h-6 w-6 flex-none items-center justify-center">
<.icon name="hero-check-circle" class="h-6 w-6 text-indigo-600" />
</div>
<p class="flex-auto py-0.5 text-xs leading-5 text-gray-500">
<%= @message.content %>
</p>
</li>
"""
end

defp comment(assigns) do
~H"""
<li id={@id} class="flex gap-x-4 hover:bg-gray-50 px-4 sm:px-6 py-2">
<.user_icon name={@message.user.name} />
<div class="flex-auto">
<div class="flex justify-between items-start gap-x-4">
<div class="text-xs leading-5 font-medium text-gray-900">
<%= @message.user.name %>
</div>
<div class="absolute inset-x-0 bottom-0 flex justify-end py-2 pl-3 pr-2">
<button
type="submit"
class="rounded-md bg-white px-2.5 py-1.5 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
>
Comment
</button>
</div>
<p class="text-sm leading-6 text-gray-500">
<%= @message.content %>
</p>
</div>
</li>
</.form>
</div>
"""
end

Expand Down
141 changes: 136 additions & 5 deletions lib/ex338_web/live/championship_live/show.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ defmodule Ex338Web.ChampionshipLive.Show do
alias Ex338.FantasyLeagues
alias Ex338.InSeasonDraftPicks
alias Ex338Web.ChampionshipLive.ChatComponent
alias Ex338Web.Presence

@impl true
def mount(_params, _session, socket) do
Expand Down Expand Up @@ -50,18 +51,38 @@ defmodule Ex338Web.ChampionshipLive.Show do
Chats.subscribe(chat, socket.assigns.current_user)
end

current_user = socket.assigns.current_user

if chat && current_user do
Presence.track(
self(),
Chats.topic(chat),
current_user.id,
default_user_presence_payload(current_user)
)
end

socket
|> assign(:chat, chat)
|> assign(:message, %Message{})
|> stream(:messages, chat.messages)
|> assign(:users, Presence.list_presences(Chats.topic(chat)))
else
_ ->
socket
|> assign(:chat, nil)
|> assign(:users, [])
|> stream(:messages, [])
end
end

defp default_user_presence_payload(user) do
%{
name: user.name,
user_id: user.id
}
end

@impl true
def handle_info(:refresh, socket) do
championship = Championships.update_next_in_season_pick(socket.assigns.championship)
Expand Down Expand Up @@ -112,6 +133,16 @@ defmodule Ex338Web.ChampionshipLive.Show do
{:noreply, stream_insert(socket, :messages, message)}
end

def handle_info(
%{event: "presence_diff", payload: _payload},
%{assigns: %{chat: chat}} = socket
) do
users =
Presence.list_presences(Chats.topic(chat))

{:noreply, assign(socket, users: users)}
end

# Implementations

defp schedule_refresh do
Expand Down Expand Up @@ -312,14 +343,14 @@ defmodule Ex338Web.ChampionshipLive.Show do
<.section_header>
Draft Chat
</.section_header>
<.live_component
module={ChatComponent}
id="chat"
<.chat_list
championship={@championship}
chat={@chat}
current_user={@current_user}
fantasy_league={@fantasy_league}
messages={@streams.messages}
message={@message}
current_user={@current_user}
patch={~p"/fantasy_leagues/#{@fantasy_league.id}/championships/#{@championship.id}"}
users={@users}
/>
</div>
<% end %>
Expand Down Expand Up @@ -552,6 +583,106 @@ defmodule Ex338Web.ChampionshipLive.Show do
"""
end

attr :chat, :map, required: true
attr :championship, :map, required: true
attr :current_user, :map, required: true
attr :fantasy_league, :map, required: true
attr :message, :map, required: true
attr :messages, :list, required: true
attr :users, :list, required: true

def chat_list(assigns) do
~H"""
<div class="overflow-hidden bg-white shadow sm:rounded-lg">
<div class="py-5 border-b border-gray-200">
<ul
id="messages"
phx-update="stream"
role="list"
phx-hook="ChatScrollToBottom"
class="flex flex-col h-[800px] overflow-y-auto overflow-x-hidden pb-6"
>
<.comment :for={{id, message} <- @messages} id={id} message={message} />
</ul>

<.live_component
module={ChatComponent}
id="chat"
chat={@chat}
message={@message}
current_user={@current_user}
patch={~p"/fantasy_leagues/#{@fantasy_league.id}/championships/#{@championship.id}"}
/>
<div class="mt-4 px-4 sm:px-6 ">
<h3>Online Users</h3>
<%= for user <- @users do %>
<p id={"online-user-#{user.user_id}"}>
<span class="inline-block h-2 w-2 flex-shrink-0 rounded-full bg-green-400">
<span class="sr-only">Online</span>
</span>
<span class="ml-1 text-xs leading-5 font-medium text-gray-900">
<%= user.name %>
</span>
</p>
<% end %>
</div>
</div>
</div>
"""
end

defp comment(%{message: %{user: nil}} = assigns) do
~H"""
<li id={@id} class="flex gap-x-4 hover:bg-gray-50 px-4 sm:px-6 py-2">
<div class="flex h-6 w-6 flex-none items-center justify-center">
<.icon name="hero-check-circle" class="h-6 w-6 text-indigo-600" />
</div>
<p class="flex-auto py-0.5 text-xs leading-5 text-gray-500">
<%= @message.content %>
</p>
</li>
"""
end

defp comment(assigns) do
~H"""
<li id={@id} class="flex gap-x-4 hover:bg-gray-50 px-4 sm:px-6 py-2">
<.user_icon name={@message.user.name} />
<div class="flex-auto">
<div class="flex justify-between items-start gap-x-4">
<div class="text-xs leading-5 font-medium text-gray-900">
<%= @message.user.name %>
</div>
</div>
<p class="text-sm leading-6 text-gray-500">
<%= @message.content %>
</p>
</div>
</li>
"""
end

attr :name, :string, required: true
attr :class, :string, default: nil

defp user_icon(assigns) do
~H"""
<div class={[
"h-6 w-6 flex flex-shrink-0 items-center justify-center bg-gray-600 rounded-full text-xs font-medium text-white",
@class
]}>
<%= get_initials(@name) %>
</div>
"""
end

defp get_initials(name) do
name
|> String.split(" ")
|> Enum.take(2)
|> Enum.map_join("", &String.at(&1, 0))
end

defp get_team_name(%{fantasy_player: %{roster_positions: [position]}}) do
position.fantasy_team.team_name
end
Expand Down
16 changes: 16 additions & 0 deletions lib/ex338_web/presence.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
defmodule Ex338Web.Presence do
@moduledoc false
use Phoenix.Presence,
otp_app: :ex338,
pubsub_server: Ex338.PubSub

alias Ex338Web.Presence

def list_presences(topic) do
topic
|> Presence.list()
|> Enum.map(fn {_user_id, data} ->
List.first(data[:metas])
end)
end
end
1 change: 1 addition & 0 deletions test/ex338_web/live/championship_live/show_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@ defmodule Ex338Web.ChampionshipLive.ShowTest do

assert has_element?(view, "h3", "Draft")
assert has_element?(view, "p", "hello world!")
assert has_element?(view, "p#online-user-#{user.id}", user.name)

long_comment = """
In a quiet town nestled between rolling hills and dense forests, a small but spirited
Expand Down
Loading