314 lines
11 kB
1
defmodule BlogWeb.ThinkerStreamLive do
2
use BlogWeb, :live_view
3
require Logger
4
5
@emoji_options ["โค๏ธ", "๐", "๐", "๐", "๐ญ", "๐ฅ", "๐", "๐ฅบ", "โจ", "๐ค", "๐", "๐", "๐ฅฐ", "๐ฉ", "๐ค", "๐", "๐ณ", "๐", "๐", "โ
", "๐ฏ", "๐", "๐", "๐ซถ", "๐"]
6
@max_skeets 50
7
8
def mount(_params, _session, socket) do
9
if connected?(socket) do
10
# Subscribe to the general skeet feed
11
Phoenix.PubSub.subscribe(Blog.PubSub, "skeet_feed")
12
end
13
14
{:ok,
15
assign(socket,
16
page_title: "Thinker Stream",
17
selected_emojis: [],
18
emoji_options: @emoji_options,
19
skeets: [],
20
max_skeets: @max_skeets,
21
selected_profile: nil
22
)}
23
end
24
25
def handle_event("toggle_emoji", %{"emoji" => emoji}, socket) do
26
selected_emojis = socket.assigns.selected_emojis
27
28
updated_emojis = if emoji in selected_emojis do
29
# Remove emoji if already selected
30
List.delete(selected_emojis, emoji)
31
else
32
# Add emoji to selection
33
[emoji | selected_emojis]
34
end
35
36
# Reset skeets when changing filters
37
{:noreply, assign(socket, selected_emojis: updated_emojis, skeets: [])}
38
end
39
40
def handle_event("clear_filters", _params, socket) do
41
{:noreply, assign(socket, selected_emojis: [], skeets: [])}
42
end
43
44
def handle_event("view_profile", %{"id" => skeet_id}, socket) do
45
# Find the skeet with the given ID
46
case Enum.find(socket.assigns.skeets, fn skeet -> skeet.id == skeet_id end) do
47
nil ->
48
{:noreply, socket}
49
50
skeet ->
51
# Extract profile information from the skeet
52
profile = extract_profile_info(skeet.text)
53
54
{:noreply, assign(socket, selected_profile: profile)}
55
end
56
end
57
58
def handle_event("close_profile", _params, socket) do
59
{:noreply, assign(socket, selected_profile: nil)}
60
end
61
62
def handle_info({:new_post, skeet}, socket) do
63
selected_emojis = socket.assigns.selected_emojis
64
65
# If no emojis are selected or the skeet doesn't contain any of the selected emojis, do nothing
66
if Enum.empty?(selected_emojis) or not contains_any_emoji?(skeet, selected_emojis) do
67
{:noreply, socket}
68
else
69
# Add the new skeet to our collection with a unique ID
70
skeet_id = generate_id()
71
72
updated_skeets = [%{
73
id: skeet_id,
74
text: skeet,
75
timestamp: DateTime.utc_now(),
76
username: extract_username(skeet),
77
matched_emojis: find_matching_emojis(skeet, selected_emojis)
78
} | socket.assigns.skeets]
79
|> Enum.take(socket.assigns.max_skeets) # Keep only the most recent skeets
80
81
{:noreply, assign(socket, skeets: updated_skeets)}
82
end
83
end
84
85
defp contains_any_emoji?(text, emojis) do
86
Enum.any?(emojis, fn emoji -> String.contains?(text, emoji) end)
87
end
88
89
defp find_matching_emojis(text, emojis) do
90
Enum.filter(emojis, fn emoji -> String.contains?(text, emoji) end)
91
end
92
93
defp generate_id do
94
:crypto.strong_rand_bytes(8) |> Base.encode16(case: :lower)
95
end
96
97
defp extract_username(skeet) do
98
# Try to extract a username from the skeet text
99
case Regex.run(~r/@([a-zA-Z0-9_]+)/, skeet) do
100
[_, username] -> username
101
_ -> "unknown_user"
102
end
103
end
104
105
defp extract_profile_info(skeet) do
106
# In a real implementation, you would make an API call to Bluesky
107
# to get the actual profile information
108
username = extract_username(skeet)
109
110
%{
111
username: username,
112
display_name: String.capitalize(username),
113
bio: "This is a mock bio for #{username}. In a real implementation, this would come from the Bluesky API.",
114
avatar_url: "https://ui-avatars.com/api/?name=#{username}&background=random",
115
follower_count: :rand.uniform(1000),
116
following_count: :rand.uniform(500)
117
}
118
end
119
120
# Helper function to highlight emojis in the text
121
defp highlight_emojis(text, emojis) do
122
Enum.reduce(emojis, text, fn emoji, acc ->
123
String.replace(acc, emoji, "<span class=\"text-2xl\">#{emoji}</span>")
124
end)
125
end
126
127
def render(assigns) do
128
~H"""
129
<div class="min-h-screen bg-black text-white p-4 font-mono">
130
<div class="max-w-4xl mx-auto">
131
<div class="mb-8 border-b border-gray-800 pb-4">
132
<h1 class="text-4xl font-bold mb-2 glitch" data-text="THINKER STREAM">Skeet Vibe Filter</h1>
133
<p class="text-gray-400">Select emojis to see matching skeets in real-time</p>
134
</div>
135
136
<div class="mb-8">
137
<div class="flex flex-wrap gap-4 mb-4">
138
<%= for emoji <- @emoji_options do %>
139
<button
140
phx-click="toggle_emoji"
141
phx-value-emoji={emoji}
142
class={"text-3xl p-2 rounded-lg transition-all #{if emoji in @selected_emojis, do: "bg-blue-700 scale-110", else: "bg-gray-800 hover:bg-gray-700"}"}
143
>
144
<%= emoji %>
145
</button>
146
<% end %>
147
</div>
148
149
<div class="flex justify-between items-center">
150
<div>
151
<%= if Enum.empty?(@selected_emojis) do %>
152
<p class="text-gray-400">No filters selected</p>
153
<% else %>
154
<div class="flex items-center gap-2">
155
<p class="text-gray-400">Showing skeets with:</p>
156
<div class="flex gap-1">
157
<%= for emoji <- @selected_emojis do %>
158
<span class="text-2xl"><%= emoji %></span>
159
<% end %>
160
</div>
161
</div>
162
<% end %>
163
</div>
164
165
<%= if not Enum.empty?(@selected_emojis) do %>
166
<button
167
phx-click="clear_filters"
168
class="text-sm px-3 py-1 bg-gray-800 hover:bg-gray-700 rounded"
169
>
170
Clear filters
171
</button>
172
<% end %>
173
</div>
174
</div>
175
176
<%= if Enum.empty?(@selected_emojis) do %>
177
<div class="flex items-center justify-center h-64 border-2 border-dashed border-gray-700 rounded-lg">
178
<p class="text-2xl text-gray-500">Select emojis above to start seeing skeets</p>
179
</div>
180
<% else %>
181
<div class="space-y-4">
182
<%= if Enum.empty?(@skeets) do %>
183
<div class="flex items-center justify-center h-64 border-2 border-dashed border-gray-700 rounded-lg">
184
<p class="text-xl text-gray-500">Waiting for skeets containing your selected emojis...</p>
185
</div>
186
<% else %>
187
<%= for {skeet, index} <- Enum.with_index(@skeets) do %>
188
<div
189
id={"skeet-#{index}"}
190
class="border border-gray-800 p-4 rounded-lg hover:border-gray-600 transition-all cursor-pointer"
191
phx-mounted={JS.transition("fade-in-scale", time: 500)}
192
phx-click="view_profile"
193
phx-value-id={skeet.id}
194
>
195
<div class="flex justify-between items-start mb-2">
196
<div class="text-blue-400">
197
@<%= skeet.username %>
198
</div>
199
<div class="text-gray-400 text-sm">
200
<%= Calendar.strftime(skeet.timestamp, "%Y-%m-%d %H:%M:%S") %>
201
</div>
202
</div>
203
<p class="whitespace-pre-wrap"><%= raw(highlight_emojis(skeet.text, skeet.matched_emojis)) %></p>
204
205
<div class="mt-2 flex justify-between items-center">
206
<div class="flex gap-1">
207
<%= for emoji <- skeet.matched_emojis do %>
208
<span class="text-xl"><%= emoji %></span>
209
<% end %>
210
</div>
211
<div class="text-sm text-gray-500">Click to view profile</div>
212
</div>
213
</div>
214
<% end %>
215
<% end %>
216
</div>
217
<% end %>
218
219
<%= if @selected_profile do %>
220
<div class="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
221
<div class="bg-gray-900 border border-gray-700 rounded-lg max-w-md w-full p-6">
222
<div class="flex justify-between items-start mb-4">
223
<h2 class="text-2xl font-bold">Profile</h2>
224
<button
225
class="text-gray-400 hover:text-white"
226
phx-click="close_profile"
227
>
228
<.icon name="hero-x-mark-solid" class="h-6 w-6" />
229
</button>
230
</div>
231
232
<div class="flex items-center space-x-4 mb-4">
233
<img
234
src={@selected_profile.avatar_url}
235
alt={@selected_profile.username}
236
class="w-16 h-16 rounded-full border-2 border-blue-500"
237
/>
238
<div>
239
<div class="font-bold text-xl"><%= @selected_profile.display_name %></div>
240
<div class="text-blue-400">@<%= @selected_profile.username %></div>
241
</div>
242
</div>
243
244
<div class="mb-4 text-gray-300">
245
<%= @selected_profile.bio %>
246
</div>
247
248
<div class="flex space-x-4 text-sm text-gray-400">
249
<div><span class="font-bold text-white"><%= @selected_profile.following_count %></span> Following</div>
250
<div><span class="font-bold text-white"><%= @selected_profile.follower_count %></span> Followers</div>
251
</div>
252
</div>
253
</div>
254
<% end %>
255
</div>
256
</div>
257
258
<style>
259
.glitch {
260
position: relative;
261
text-shadow: 0.05em 0 0 #00fffc, -0.03em -0.04em 0 #fc00ff,
262
0.025em 0.04em 0 #fffc00;
263
animation: glitch 725ms infinite;
264
}
265
266
@keyframes glitch {
267
0% {
268
text-shadow: 0.05em 0 0 #00fffc, -0.03em -0.04em 0 #fc00ff,
269
0.025em 0.04em 0 #fffc00;
270
}
271
15% {
272
text-shadow: 0.05em 0 0 #00fffc, -0.03em -0.04em 0 #fc00ff,
273
0.025em 0.04em 0 #fffc00;
274
}
275
16% {
276
text-shadow: -0.05em -0.025em 0 #00fffc, 0.025em 0.035em 0 #fc00ff,
277
-0.05em -0.05em 0 #fffc00;
278
}
279
49% {
280
text-shadow: -0.05em -0.025em 0 #00fffc, 0.025em 0.035em 0 #fc00ff,
281
-0.05em -0.05em 0 #fffc00;
282
}
283
50% {
284
text-shadow: 0.05em 0.035em 0 #00fffc, 0.03em 0 0 #fc00ff,
285
0 -0.04em 0 #fffc00;
286
}
287
99% {
288
text-shadow: 0.05em 0.035em 0 #00fffc, 0.03em 0 0 #fc00ff,
289
0 -0.04em 0 #fffc00;
290
}
291
100% {
292
text-shadow: -0.05em 0 0 #00fffc, -0.025em -0.04em 0 #fc00ff,
293
-0.04em -0.025em 0 #fffc00;
294
}
295
}
296
297
.fade-in-scale {
298
animation: fadeInScale 0.5s ease-out;
299
}
300
301
@keyframes fadeInScale {
302
from {
303
opacity: 0;
304
transform: scale(0.95);
305
}
306
to {
307
opacity: 1;
308
transform: scale(1);
309
}
310
}
311
</style>
312
"""
313
end
314
end
315