Skip to content

Commit 031e35c

Browse files
committed
feat: add chat thread live view and update routing for chat functionality
1 parent dafe82d commit 031e35c

File tree

5 files changed

+214
-8
lines changed

5 files changed

+214
-8
lines changed

lib/algora/chat/chat.ex

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,15 @@ defmodule Algora.Chat do
8686
|> Repo.all()
8787
end
8888

89-
def list_threads(user_id) do
89+
def get_thread(thread_id) do
90+
Repo.get(Thread, thread_id)
91+
end
92+
93+
def list_threads(_user_id) do
9094
Thread
91-
|> join(:inner, [t], p in Participant, on: p.thread_id == t.id)
92-
|> where([_t, p], p.user_id == ^user_id)
93-
|> preload(:participants)
95+
# |> join(:inner, [t], p in Participant, on: p.thread_id == t.id)
96+
# |> where([_t, p], p.user_id == ^user_id)
97+
|> preload(participants: :user)
9498
|> Repo.all()
9599
end
96100

lib/algora_web/components/layouts/user.html.heex

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,14 +106,14 @@
106106
<.separator :if={index != length(@nav) - 1} class="mx-auto my-4 w-2/3" />
107107
<% end %>
108108

109-
<%= if @contacts != [] do %>
109+
<%= if assigns[:threads] && assigns[:threads] != [] do %>
110110
<.separator class="mx-auto my-4 w-2/3" />
111111
<ul role="list" class="space-y-1">
112-
<%= for contact <- @contacts do %>
112+
<%= for %{thread: thread, contact: contact, path: path} <- @threads do %>
113113
<li>
114114
<.link
115115
class="flex justify-center p-2"
116-
navigate={Algora.Accounts.User.url(contact)}
116+
navigate={path}
117117
title={Algora.Accounts.User.handle(contact)}
118118
>
119119
<div class="relative inline-block">

lib/algora_web/live/admin/nav.ex

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,28 @@
11
defmodule AlgoraWeb.Admin.Nav do
22
@moduledoc false
33
use Phoenix.Component
4+
use AlgoraWeb, :verified_routes
45

56
import Phoenix.LiveView
67

8+
alias Algora.Chat
9+
710
def on_mount(:default, _params, _session, socket) do
11+
threads =
12+
socket.assigns.current_user.id
13+
|> Chat.list_threads()
14+
|> Enum.flat_map(fn thread ->
15+
case Enum.find(thread.participants, fn p -> !p.user.is_admin end) do
16+
nil -> []
17+
contact -> [%{thread: thread, contact: contact.user, path: ~p"/admin/chat/#{thread.id}"}]
18+
end
19+
end)
20+
21+
dbg(threads)
22+
823
{:cont,
924
socket
10-
|> assign(:contacts, [])
25+
|> assign(:threads, threads)
1126
|> assign(:admin_page?, true)
1227
|> assign_nav_items()
1328
|> attach_hook(:active_tab, :handle_params, &handle_active_tab_params/3)}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
defmodule AlgoraWeb.Chat.ThreadLive do
2+
@moduledoc false
3+
use AlgoraWeb, :live_view
4+
5+
alias Algora.Chat
6+
alias Algora.Chat.Message
7+
alias Algora.Repo
8+
9+
@impl true
10+
def mount(%{"id" => thread_id}, _session, socket) do
11+
if connected?(socket) do
12+
Chat.subscribe(thread_id)
13+
end
14+
15+
thread = thread_id |> Chat.get_thread() |> Repo.preload(participants: :user)
16+
messages = thread_id |> Chat.list_messages() |> Repo.preload(:sender)
17+
18+
{:ok,
19+
socket
20+
|> assign(:thread, thread)
21+
|> assign(:thread_id, thread_id)
22+
|> assign(:messages, messages)}
23+
end
24+
25+
@impl true
26+
def handle_event("send_message", %{"message" => content}, socket) do
27+
{:ok, message} =
28+
Chat.send_message(
29+
socket.assigns.thread_id,
30+
socket.assigns.current_user.id,
31+
content
32+
)
33+
34+
{:noreply, push_event(socket, "clear-input", %{selector: "#message-input"})}
35+
end
36+
37+
@impl true
38+
def handle_info(%Message{} = message, socket) do
39+
if message.id in Enum.map(socket.assigns.messages, & &1.id) do
40+
{:noreply, socket}
41+
else
42+
{:noreply, assign(socket, :messages, socket.assigns.messages ++ [message])}
43+
end
44+
end
45+
46+
@impl true
47+
def render(assigns) do
48+
~H"""
49+
<div class="pr-80">
50+
<div class="flex flex-col h-[calc(100vh-4rem)]">
51+
<div class="flex-none border-b border-border bg-card/50 p-4 backdrop-blur supports-[backdrop-filter]:bg-background/60">
52+
<div class="flex items-center justify-between">
53+
<div class="flex items-center gap-3">
54+
<div class="flex -space-x-2">
55+
<%= for participant <- @thread.participants do %>
56+
<.avatar class="relative z-10 h-8 w-8 ring-2 ring-background">
57+
<.avatar_image src={participant.user.avatar_url} alt={participant.user.name} />
58+
<.avatar_fallback>
59+
{Algora.Util.initials(participant.user.name)}
60+
</.avatar_fallback>
61+
</.avatar>
62+
<% end %>
63+
</div>
64+
<div>
65+
<h2 class="text-lg font-semibold">{@thread.title}</h2>
66+
<p class="text-xs text-muted-foreground">
67+
{@thread.participants
68+
|> Enum.map(& &1.user.name)
69+
|> Algora.Util.format_name_list()}
70+
</p>
71+
</div>
72+
</div>
73+
</div>
74+
</div>
75+
76+
<.scroll_area
77+
class="flex flex-1 flex-col-reverse gap-6 p-4"
78+
id="messages-container"
79+
phx-hook="ScrollToBottom"
80+
>
81+
<div class="space-y-6">
82+
<%= for {date, messages} <- @messages
83+
|> Enum.group_by(fn msg ->
84+
case Date.diff(Date.utc_today(), DateTime.to_date(msg.inserted_at)) do
85+
0 -> "Today"
86+
1 -> "Yesterday"
87+
n when n <= 7 -> Calendar.strftime(msg.inserted_at, "%A")
88+
_ -> Calendar.strftime(msg.inserted_at, "%b %d")
89+
end
90+
end)
91+
|> Enum.sort_by(fn {_, msgs} -> hd(msgs).inserted_at end, Date) do %>
92+
<div class="flex items-center justify-center">
93+
<div class="rounded-full bg-background px-2 py-1 text-xs text-muted-foreground">
94+
{date}
95+
</div>
96+
</div>
97+
98+
<div class="flex flex-col gap-6">
99+
<%= for message <- Enum.sort_by(messages, & &1.inserted_at, Date) do %>
100+
<div class="group flex gap-3">
101+
<.avatar class="h-8 w-8">
102+
<.avatar_image src={message.sender.avatar_url} />
103+
<.avatar_fallback>
104+
{Algora.Util.initials(message.sender.name)}
105+
</.avatar_fallback>
106+
</.avatar>
107+
<div class="max-w-[80%] relative rounded-2xl rounded-tl-none bg-muted p-3">
108+
{message.content}
109+
<div class="text-[10px] mt-1 text-muted-foreground">
110+
{message.inserted_at
111+
|> DateTime.to_time()
112+
|> Time.to_string()
113+
|> String.slice(0..4)}
114+
</div>
115+
</div>
116+
</div>
117+
<% end %>
118+
</div>
119+
<% end %>
120+
</div>
121+
</.scroll_area>
122+
123+
<div class="flex-none bg-card/50 p-4 backdrop-blur supports-[backdrop-filter]:bg-background/60">
124+
<form phx-submit="send_message" class="flex items-center gap-2">
125+
<div class="relative flex-1">
126+
<.input
127+
id="message-input"
128+
type="text"
129+
name="message"
130+
value=""
131+
placeholder="Type a message..."
132+
autocomplete="off"
133+
class="flex-1 pr-24"
134+
phx-hook="ClearInput"
135+
/>
136+
<div class="absolute top-1/2 right-2 flex -translate-y-1/2 gap-1">
137+
<.button
138+
type="button"
139+
variant="ghost"
140+
size="icon-sm"
141+
phx-hook="EmojiPicker"
142+
id="emoji-trigger"
143+
>
144+
<.icon name="tabler-mood-smile" class="h-4 w-4" />
145+
</.button>
146+
</div>
147+
</div>
148+
<.button type="submit" size="icon">
149+
<.icon name="tabler-send" class="h-4 w-4" />
150+
</.button>
151+
</form>
152+
<div id="emoji-picker-container" class="bottom-[80px] absolute right-4 hidden">
153+
<emoji-picker></emoji-picker>
154+
</div>
155+
</div>
156+
</div>
157+
158+
<aside class="fixed top-[4rem] right-0 z-20 h-full w-72 border-l border-border bg-card/50 p-6 backdrop-blur supports-[backdrop-filter]:bg-background/60">
159+
<div class="space-y-6">
160+
<div>
161+
<h3 class="mb-4 text-sm font-medium">Participants</h3>
162+
<div class="space-y-4">
163+
<%= for participant <- @thread.participants do %>
164+
<div class="flex items-center gap-3">
165+
<.avatar class="h-10 w-10">
166+
<.avatar_image src={participant.user.avatar_url} alt={participant.user.name} />
167+
<.avatar_fallback>
168+
{Algora.Util.initials(participant.user.name)}
169+
</.avatar_fallback>
170+
</.avatar>
171+
<div>
172+
<p class="text-sm font-medium leading-none">{participant.user.name}</p>
173+
<p :if={participant.user.last_active_at} class="text-xs text-muted-foreground">
174+
Active {Algora.Util.time_ago(participant.user.last_active_at)}
175+
</p>
176+
</div>
177+
</div>
178+
<% end %>
179+
</div>
180+
</div>
181+
</div>
182+
</aside>
183+
</div>
184+
"""
185+
end
186+
end

lib/algora_web/router.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ defmodule AlgoraWeb.Router do
5050
on_mount: [{AlgoraWeb.UserAuth, :ensure_admin}, AlgoraWeb.Admin.Nav] do
5151
live "/", Admin.AdminLive
5252
live "/leaderboard", Admin.LeaderboardLive
53+
live "/chat/:id", Chat.ThreadLive
5354
end
5455

5556
oban_dashboard("/oban", resolver: AlgoraWeb.ObanDashboardResolver)

0 commit comments

Comments
 (0)