Skip to content

Commit a6be136

Browse files
committed
feat: enhance chat functionality and thread management
- Introduced a new `MessageCreated` struct to encapsulate message and participant data for broadcasting. - Refactored the `broadcast` function to utilize the new struct for improved clarity. - Added `ensure_participant` and `insert_message` private functions to streamline message handling. - Implemented `get_or_create_bounty_thread` to manage thread creation for bounties. - Updated `BountyLive` to integrate chat features, including message and participant management. - Added `bounty_id` field to the `Thread` schema and created a migration for the database update.
1 parent e2d1c3f commit a6be136

File tree

6 files changed

+135
-25
lines changed

6 files changed

+135
-25
lines changed

lib/algora/chat/chat.ex

Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,13 @@ defmodule Algora.Chat do
88
alias Algora.Chat.Thread
99
alias Algora.Repo
1010

11-
def broadcast(message) do
12-
Phoenix.PubSub.broadcast(Algora.PubSub, "chat:thread:#{message.thread_id}", message)
11+
defmodule MessageCreated do
12+
@moduledoc false
13+
defstruct message: nil, participant: nil
14+
end
15+
16+
def broadcast(%MessageCreated{} = event) do
17+
Phoenix.PubSub.broadcast(Algora.PubSub, "chat:thread:#{event.message.thread_id}", event)
1318
end
1419

1520
def subscribe(thread_id) do
@@ -23,7 +28,6 @@ defmodule Algora.Chat do
2328
|> Thread.changeset(%{title: "#{User.handle(user_1)} <> #{User.handle(user_2)}"})
2429
|> Repo.insert()
2530

26-
# Add participants
2731
for user <- [user_1, user_2] do
2832
%Participant{}
2933
|> Participant.changeset(%{
@@ -46,7 +50,7 @@ defmodule Algora.Chat do
4650
|> Repo.insert()
4751

4852
participants = Enum.uniq_by([user | admins], & &1.id)
49-
# Add participants
53+
5054
for u <- participants do
5155
%Participant{}
5256
|> Participant.changeset(%{
@@ -61,20 +65,41 @@ defmodule Algora.Chat do
6165
end)
6266
end
6367

68+
defp ensure_participant(thread_id, user_id) do
69+
case Repo.fetch_by(Participant, thread_id: thread_id, user_id: user_id) do
70+
{:ok, participant} ->
71+
{:ok, participant}
72+
73+
{:error, _} ->
74+
%Participant{}
75+
|> Participant.changeset(%{
76+
thread_id: thread_id,
77+
user_id: user_id,
78+
last_read_at: DateTime.utc_now()
79+
})
80+
|> Repo.insert()
81+
end
82+
end
83+
84+
defp insert_message(thread_id, sender_id, content) do
85+
%Message{}
86+
|> Message.changeset(%{
87+
thread_id: thread_id,
88+
sender_id: sender_id,
89+
content: content
90+
})
91+
|> Repo.insert()
92+
end
93+
6494
def send_message(thread_id, sender_id, content) do
65-
case %Message{}
66-
|> Message.changeset(%{
67-
thread_id: thread_id,
68-
sender_id: sender_id,
69-
content: content
70-
})
71-
|> Repo.insert() do
72-
{:ok, message} ->
73-
message |> Repo.preload(:sender) |> broadcast()
74-
{:ok, message}
75-
76-
{:error, changeset} ->
77-
{:error, changeset}
95+
with {:ok, participant} <- ensure_participant(thread_id, sender_id),
96+
{:ok, message} <- insert_message(thread_id, sender_id, content) do
97+
broadcast(%MessageCreated{
98+
message: Repo.preload(message, :sender),
99+
participant: Repo.preload(participant, :user)
100+
})
101+
102+
{:ok, message}
78103
end
79104
end
80105

@@ -107,6 +132,12 @@ defmodule Algora.Chat do
107132
|> Repo.all()
108133
end
109134

135+
def list_participants(thread_id) do
136+
Participant
137+
|> where(thread_id: ^thread_id)
138+
|> Repo.all()
139+
end
140+
110141
def mark_as_read(thread_id, user_id) do
111142
Participant
112143
|> where(thread_id: ^thread_id, user_id: ^user_id)
@@ -145,4 +176,16 @@ defmodule Algora.Chat do
145176
thread -> {:ok, thread}
146177
end
147178
end
179+
180+
def get_or_create_bounty_thread(bounty) do
181+
case Repo.fetch_by(Thread, bounty_id: bounty.id) do
182+
{:ok, thread} ->
183+
{:ok, thread}
184+
185+
{:error, _} ->
186+
%Thread{}
187+
|> Thread.changeset(%{title: "Contributor chat", bounty_id: bounty.id})
188+
|> Repo.insert()
189+
end
190+
end
148191
end

lib/algora/chat/schemas/thread.ex

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ defmodule Algora.Chat.Thread do
66

77
typed_schema "threads" do
88
field :title, :string
9-
9+
field :bounty_id, :string
1010
has_many :messages, Algora.Chat.Message
1111
has_many :participants, Algora.Chat.Participant
1212
has_many :activities, {"thread_activities", Activity}, foreign_key: :assoc_id
@@ -16,8 +16,9 @@ defmodule Algora.Chat.Thread do
1616

1717
def changeset(thread, attrs) do
1818
thread
19-
|> cast(attrs, [:title])
19+
|> cast(attrs, [:title, :bounty_id])
2020
|> validate_required([:title])
2121
|> generate_id()
22+
|> unique_constraint(:bounty_id)
2223
end
2324
end

lib/algora_web/live/bounty_live.ex

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ defmodule AlgoraWeb.BountyLive do
88
alias Algora.Bounties
99
alias Algora.Bounties.Bounty
1010
alias Algora.Bounties.LineItem
11+
alias Algora.Chat
1112
alias Algora.Repo
1213
alias Algora.Util
1314
alias Algora.Workspace
@@ -95,6 +96,14 @@ defmodule AlgoraWeb.BountyLive do
9596

9697
exclusive_changeset = ExclusiveBountyForm.changeset(%ExclusiveBountyForm{}, %{})
9798

99+
{:ok, thread} = Chat.get_or_create_bounty_thread(bounty)
100+
messages = thread.id |> Chat.list_messages() |> Repo.preload(:sender)
101+
participants = thread.id |> Chat.list_participants() |> Repo.preload(:user)
102+
103+
if connected?(socket) do
104+
Chat.subscribe(thread.id)
105+
end
106+
98107
{:ok,
99108
socket
100109
|> assign(:page_title, bounty.ticket.title)
@@ -107,7 +116,9 @@ defmodule AlgoraWeb.BountyLive do
107116
|> assign(:show_exclusive_modal, false)
108117
|> assign(:selected_context, nil)
109118
|> assign(:line_items, [])
110-
|> assign(:messages, [])
119+
|> assign(:thread, thread)
120+
|> assign(:messages, messages)
121+
|> assign(:participants, participants)
111122
|> assign(:reward_form, to_form(reward_changeset))
112123
|> assign(:exclusive_form, to_form(exclusive_changeset))
113124
|> assign_exclusives(bounty.shared_with)}
@@ -118,34 +129,76 @@ defmodule AlgoraWeb.BountyLive do
118129
{:noreply, socket}
119130
end
120131

132+
@impl true
121133
def handle_params(%{"context" => context_id}, _url, socket) do
122134
{:noreply, socket |> assign_selected_context(context_id) |> assign_line_items()}
123135
end
124136

137+
@impl true
125138
def handle_params(_params, _url, socket) do
126139
{:noreply, socket}
127140
end
128141

142+
@impl true
143+
def handle_info(%Chat.MessageCreated{message: message, participant: participant}, socket) do
144+
socket =
145+
if message.id in Enum.map(socket.assigns.messages, & &1.id) do
146+
socket
147+
else
148+
Phoenix.Component.update(socket, :messages, &(&1 ++ [message]))
149+
end
150+
151+
socket =
152+
if participant.id in Enum.map(socket.assigns.participants, & &1.id) do
153+
socket
154+
else
155+
Phoenix.Component.update(socket, :participants, &(&1 ++ [participant]))
156+
end
157+
158+
{:noreply, socket}
159+
end
160+
161+
@impl true
162+
def handle_event("send_message", %{"message" => content}, socket) do
163+
{:ok, message} =
164+
Chat.send_message(
165+
socket.assigns.thread.id,
166+
socket.assigns.current_user.id,
167+
content
168+
)
169+
170+
message = Repo.preload(message, :sender)
171+
172+
{:noreply,
173+
socket
174+
|> update(:messages, &(&1 ++ [message]))
175+
|> push_event("clear-input", %{selector: "#message-input"})}
176+
end
177+
129178
@impl true
130179
def handle_event("reward", _params, socket) do
131180
{:noreply, assign(socket, :show_reward_modal, true)}
132181
end
133182

183+
@impl true
134184
def handle_event("exclusive", _params, socket) do
135185
{:noreply, assign(socket, :show_exclusive_modal, true)}
136186
end
137187

188+
@impl true
138189
def handle_event("close_drawer", _params, socket) do
139190
{:noreply, close_drawers(socket)}
140191
end
141192

193+
@impl true
142194
def handle_event("validate_reward", %{"reward_bounty_form" => params}, socket) do
143195
{:noreply,
144196
socket
145197
|> assign(:reward_form, to_form(RewardBountyForm.changeset(%RewardBountyForm{}, params)))
146198
|> assign_line_items()}
147199
end
148200

201+
@impl true
149202
def handle_event("pay_with_stripe", %{"reward_bounty_form" => params}, socket) do
150203
changeset = RewardBountyForm.changeset(%RewardBountyForm{}, params)
151204

@@ -165,13 +218,15 @@ defmodule AlgoraWeb.BountyLive do
165218
end
166219
end
167220

221+
@impl true
168222
def handle_event("validate_exclusive", %{"exclusive_bounty_form" => params}, socket) do
169223
{:noreply,
170224
socket
171225
|> assign(:exclusive_form, to_form(ExclusiveBountyForm.changeset(%ExclusiveBountyForm{}, params)))
172226
|> assign_line_items()}
173227
end
174228

229+
@impl true
175230
def handle_event("share_exclusive", %{"exclusive_bounty_form" => params}, socket) do
176231
changeset = ExclusiveBountyForm.changeset(%ExclusiveBountyForm{}, params)
177232
bounty = socket.assigns.bounty
@@ -420,11 +475,11 @@ defmodule AlgoraWeb.BountyLive do
420475
Contributor chat
421476
</h2>
422477
<div class="relative flex -space-x-2">
423-
<%= for user <- @exclusives do %>
478+
<%= for participant <- @participants do %>
424479
<.avatar>
425-
<.avatar_image src={user.avatar_url} alt="Developer avatar" />
480+
<.avatar_image src={participant.user.avatar_url} alt="Developer avatar" />
426481
<.avatar_fallback>
427-
{Algora.Util.initials(@bounty.owner.name)}
482+
{Algora.Util.initials(participant.user.name)}
428483
</.avatar_fallback>
429484
</.avatar>
430485
<% end %>

lib/algora_web/live/chat/thread_live.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ defmodule AlgoraWeb.Chat.ThreadLive do
3838
end
3939

4040
@impl true
41-
def handle_info(%Message{} = message, socket) do
41+
def handle_info(%{message: message}, socket) do
4242
if message.id in Enum.map(socket.assigns.messages, & &1.id) do
4343
{:noreply, socket}
4444
else

lib/algora_web/live/contract/view_live.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -510,7 +510,7 @@ defmodule AlgoraWeb.Contract.ViewLive do
510510
end
511511
end
512512

513-
def handle_info(%Chat.Message{} = message, socket) do
513+
def handle_info(%Chat.MessageCreated{message: message}, socket) do
514514
if message.id in Enum.map(socket.assigns.messages, & &1.id) do
515515
{:noreply, socket}
516516
else
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
defmodule Algora.Repo.Migrations.AddBountyIdToThreads do
2+
use Ecto.Migration
3+
4+
def change do
5+
alter table(:threads) do
6+
add :bounty_id, :string, null: true
7+
end
8+
9+
create unique_index(:threads, [:bounty_id])
10+
end
11+
end

0 commit comments

Comments
 (0)