322 lines
12 kB
1
defmodule BlogWeb.AllowedChatsLive do
2
use BlogWeb, :live_view
3
require Logger
4
5
alias Blog.Chat.MessageStore
6
alias Blog.Chat.Presence
7
8
@impl true
9
def mount(_params, session, socket) do
10
# Generate a unique user ID for this session if not present
11
user_id = Map.get(session, "user_id", generate_user_id())
12
13
# Load the global allowed words from ETS
14
allowed_words = MessageStore.get_allowed_words()
15
16
# Load recent messages from ETS
17
messages = MessageStore.get_recent_messages()
18
19
# Get initial online users count (safely)
20
online_count = get_online_count()
21
22
if connected?(socket) do
23
# Subscribe to the chat topic for real-time updates
24
Phoenix.PubSub.subscribe(Blog.PubSub, MessageStore.topic())
25
26
# Subscribe to the presence topic for user presence updates
27
Phoenix.PubSub.subscribe(Blog.PubSub, Presence.topic())
28
29
# Track this user's presence (safely)
30
track_user_presence(user_id)
31
32
# Get an updated count after tracking
33
online_count = get_online_count()
34
end
35
36
{:ok,
37
socket
38
|> assign(:page_title, "Community Allowed Chats")
39
|> assign(:meta_attrs, [
40
%{name: "title", content: "Community Allowed Chats"},
41
%{name: "description", content: "Chat with community-managed allowed words filtering"},
42
%{property: "og:title", content: "Community Allowed Chats"},
43
%{property: "og:description", content: "Chat with community-managed allowed words filtering"},
44
%{property: "og:type", content: "website"}
45
])
46
|> assign(:user_id, user_id)
47
|> assign(:allowed_words, allowed_words)
48
|> assign(:messages, calculate_message_visibility(messages, allowed_words))
49
|> assign(:add_word_form, to_form(%{"word" => ""}))
50
|> assign(:message_form, to_form(%{"content" => ""}))
51
|> assign(:online_count, online_count)}
52
end
53
54
@impl true
55
def handle_event("add_word", %{"word" => word}, socket) when is_binary(word) and word != "" do
56
# Add the word to the global allowed_words set
57
word = String.downcase(String.trim(word))
58
# Use the new function for global word addition
59
MessageStore.add_allowed_word(word)
60
61
# We'll get updated words through the PubSub broadcast
62
{:noreply, assign(socket, :add_word_form, to_form(%{"word" => ""}))}
63
end
64
65
@impl true
66
def handle_event("remove_word", %{"word" => word}, socket) do
67
# Remove the word from the global allowed_words set
68
MessageStore.remove_allowed_word(word)
69
70
# We'll get updated words through the PubSub broadcast
71
{:noreply, socket}
72
end
73
74
@impl true
75
def handle_event("send_message", %{"content" => content}, socket) when is_binary(content) and content != "" do
76
user_id = socket.assigns.user_id
77
78
# Create a new message map (without is_visible - we'll calculate it dynamically)
79
new_message = %{
80
id: System.unique_integer([:positive]),
81
content: content,
82
timestamp: DateTime.utc_now(),
83
user_id: user_id
84
}
85
86
# Store the message in ETS
87
MessageStore.store_message(new_message)
88
89
# Updates will come through the PubSub channel
90
{:noreply, assign(socket, :message_form, to_form(%{"content" => ""}))}
91
end
92
93
@impl true
94
def handle_event("validate_add_word", %{"word" => word}, socket) do
95
{:noreply, assign(socket, :add_word_form, to_form(%{"word" => word}))}
96
end
97
98
@impl true
99
def handle_event("validate_message", %{"content" => content}, socket) do
100
{:noreply, assign(socket, :message_form, to_form(%{"content" => content}))}
101
end
102
103
@impl true
104
def handle_info({:new_message, _message}, socket) do
105
# When a new message is broadcast, update the messages list
106
messages = MessageStore.get_recent_messages()
107
108
# Calculate visibility for each message based on the current allowed words
109
messages_with_visibility = calculate_message_visibility(messages, socket.assigns.allowed_words)
110
111
{:noreply, assign(socket, :messages, messages_with_visibility)}
112
end
113
114
@impl true
115
def handle_info({:allowed_words_updated, updated_words}, socket) do
116
# With shared words, we update for all users regardless of user_id
117
# Recalculate message visibility with the updated allowed words
118
messages = calculate_message_visibility(socket.assigns.messages, updated_words)
119
120
{:noreply,
121
socket
122
|> assign(:allowed_words, updated_words)
123
|> assign(:messages, messages)}
124
end
125
126
@impl true
127
def handle_info(%Phoenix.Socket.Broadcast{event: "presence_diff"}, socket) do
128
# Update the online count when presence changes
129
online_count = get_online_count()
130
{:noreply, assign(socket, :online_count, online_count)}
131
end
132
133
# Helper function to calculate visibility for a list of messages
134
defp calculate_message_visibility(messages, allowed_words) do
135
Enum.map(messages, fn message ->
136
{is_visible, matching_words} = message_visible_with_words(message.content, allowed_words)
137
138
# Create a new message map with visibility information
139
Map.merge(message, %{
140
is_visible: is_visible,
141
matching_words: matching_words
142
})
143
end)
144
end
145
146
# Enhanced helper function to check if a message is visible based on the allowed words
147
# Returns a tuple of {is_visible, matching_words}
148
defp message_visible_with_words(content, allowed_words) do
149
# Skip the check if there are no allowed words
150
if MapSet.size(allowed_words) == 0 do
151
{false, []}
152
else
153
# Split the content into words
154
words = content
155
|> String.downcase()
156
|> String.split(~r/\s+/)
157
|> Enum.map(&String.trim/1)
158
159
# Find all matching words
160
matching_words =
161
words
162
|> Enum.filter(fn word -> MapSet.member?(allowed_words, word) end)
163
|> Enum.uniq()
164
165
{length(matching_words) > 0, matching_words}
166
end
167
end
168
169
# For backward compatibility with older messages
170
defp message_visible?(content, allowed_words) do
171
{is_visible, _matching_words} = message_visible_with_words(content, allowed_words)
172
is_visible
173
end
174
175
# Generate a unique user ID
176
defp generate_user_id do
177
System.unique_integer([:positive]) |> to_string()
178
end
179
180
# Helper functions for presence
181
defp track_user_presence(user_id) do
182
try do
183
Presence.track_user(user_id)
184
rescue
185
_ ->
186
# If tracking fails, log it but continue
187
Logger.warn("Failed to track user presence for user: #{user_id}")
188
:error
189
end
190
end
191
192
defp get_online_count do
193
try do
194
Presence.count_online_users()
195
rescue
196
_ ->
197
# If presence counting fails, return 1 (at least this user)
198
1
199
end
200
end
201
202
@impl true
203
def render(assigns) do
204
~H"""
205
<div class="min-h-screen bg-gray-100 p-6">
206
<div class="max-w-4xl mx-auto">
207
<div class="flex justify-between items-center mb-6">
208
<h1 class="text-3xl font-bold">Community Chat</h1>
209
<div class="bg-white rounded-full px-4 py-2 shadow flex items-center">
210
<div class="w-3 h-3 bg-green-500 rounded-full mr-2 animate-pulse"></div>
211
<span class="text-sm font-medium"><%= @online_count %> online</span>
212
</div>
213
</div>
214
<div class="text-sm text-gray-500 mb-6">Your session ID: <%= @user_id %></div>
215
216
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
217
<!-- Left sidebar: Allowed words -->
218
<div class="md:col-span-1">
219
<div class="bg-white rounded-lg shadow p-4">
220
<h2 class="text-xl font-semibold mb-4">Community Allowed Words</h2>
221
<p class="text-sm text-gray-600 mb-4">These words are shared by all users. Any message containing these words will be visible to everyone.</p>
222
223
<.form for={@add_word_form} phx-submit="add_word" phx-change="validate_add_word" class="mb-4">
224
<div class="flex gap-2">
225
<.input field={@add_word_form[:word]} placeholder="Enter a word" class="flex-grow" />
226
<button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
227
Add
228
</button>
229
</div>
230
</.form>
231
232
<div class="mt-4">
233
<div class="flex flex-wrap gap-2">
234
<%= for word <- @allowed_words do %>
235
<span class="px-2 py-1 bg-blue-100 text-blue-800 rounded text-sm group relative">
236
<%= word %>
237
<button
238
phx-click="remove_word"
239
phx-value-word={word}
240
class="ml-1 text-blue-500 hover:text-red-500 focus:outline-none"
241
aria-label={"Remove #{word}"}
242
>
243
×
244
</button>
245
</span>
246
<% end %>
247
</div>
248
<%= if Enum.empty?(@allowed_words) do %>
249
<p class="text-gray-500 text-sm italic">No community allowed words yet. Add some!</p>
250
<% end %>
251
</div>
252
</div>
253
</div>
254
255
<!-- Main content: Messages -->
256
<div class="md:col-span-2">
257
<div class="bg-white rounded-lg shadow p-4 mb-4">
258
<h2 class="text-xl font-semibold mb-4">Messages</h2>
259
260
<.form for={@message_form} phx-submit="send_message" phx-change="validate_message">
261
<div class="flex gap-2">
262
<.input field={@message_form[:content]} placeholder="Type a message..." class="flex-grow" />
263
<button type="submit" class="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700">
264
Send
265
</button>
266
</div>
267
</.form>
268
</div>
269
270
<div class="bg-white rounded-lg shadow p-4">
271
<h3 class="text-lg font-semibold mb-4">Chat History</h3>
272
273
<div class="space-y-4">
274
<%= if Enum.empty?(@messages) do %>
275
<p class="text-gray-500 text-center py-4">No messages yet. Start the conversation!</p>
276
<% else %>
277
<%= for message <- @messages do %>
278
<div class={[
279
"p-3 rounded-lg",
280
if(message.is_visible, do: "bg-green-50 border border-green-200", else: "bg-red-50 border border-red-200")
281
]}>
282
<div class="flex justify-between items-start">
283
<div class="flex-1">
284
<%= if message.is_visible do %>
285
<p class="text-gray-800"><%= message.content %></p>
286
<%= if message[:matching_words] && length(message.matching_words) > 0 do %>
287
<p class="text-xs text-green-600 mt-1">
288
Allowed by:
289
<%= for {word, i} <- Enum.with_index(message.matching_words) do %>
290
<span class="font-semibold"><%= word %></span><%= if i < length(message.matching_words) - 1, do: ", " %>
291
<% end %>
292
</p>
293
<% end %>
294
<% else %>
295
<p class="text-gray-400 italic">This message is hidden (no allowed words found)</p>
296
<% end %>
297
<p class="text-xs text-gray-500 mt-1">
298
<%= Calendar.strftime(message.timestamp, "%B %d, %Y at %I:%M %p") %>
299
<%= if Map.get(message, :user_id) == @user_id do %>
300
<span class="ml-2 text-blue-500">(You)</span>
301
<% end %>
302
</p>
303
</div>
304
<span class={[
305
"text-xs px-2 py-1 rounded-full",
306
if(message.is_visible, do: "bg-green-200 text-green-800", else: "bg-red-200 text-red-800")
307
]}>
308
<%= if message.is_visible, do: "Visible", else: "Hidden" %>
309
</span>
310
</div>
311
</div>
312
<% end %>
313
<% end %>
314
</div>
315
</div>
316
</div>
317
</div>
318
</div>
319
</div>
320
"""
321
end
322
end
323