Skip to content

Commit 31ac7b5

Browse files
committed
Fixed infinite scroll
1 parent f85199f commit 31ac7b5

File tree

6 files changed

+148
-40
lines changed

6 files changed

+148
-40
lines changed

assets/css/app.css

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,34 @@ table.package-list .button {
408408
font-weight: 600;
409409
}
410410

411+
#loading-trigger {
412+
height: 60px;
413+
width: 100%;
414+
margin: 30px 0;
415+
display: flex;
416+
align-items: center;
417+
justify-content: center;
418+
color: #888;
419+
font-size: 14px;
420+
opacity: 0.8;
421+
}
422+
423+
#loading-trigger::before {
424+
content: '';
425+
width: 20px;
426+
height: 20px;
427+
border: 2px solid #f3f3f3;
428+
border-top: 2px solid #888;
429+
border-radius: 50%;
430+
animation: spin 1s linear infinite;
431+
margin-right: 10px;
432+
}
433+
434+
@keyframes spin {
435+
0% { transform: rotate(0deg); }
436+
100% { transform: rotate(360deg); }
437+
}
438+
411439

412440

413441
@media only screen

assets/js/app.js

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,40 @@ import "phoenix_html"
1414
import { Socket } from 'phoenix'
1515
import { LiveSocket } from "phoenix_live_view"
1616

17-
let liveSocket = new LiveSocket("/live", Socket, {})
17+
// Define hooks for LiveView
18+
window.Hooks = {}
19+
20+
window.Hooks.InfiniteScroll = {
21+
mounted() {
22+
this.pending = false
23+
24+
this.observer = new IntersectionObserver((entries) => {
25+
const target = entries[0]
26+
if (target.isIntersecting && !this.pending) {
27+
this.pending = true
28+
this.pushEvent("load-more", {})
29+
}
30+
}, {
31+
root: null,
32+
rootMargin: '100px',
33+
threshold: 0.1
34+
})
35+
36+
this.observer.observe(this.el)
37+
},
38+
39+
destroyed() {
40+
if (this.observer) {
41+
this.observer.disconnect()
42+
}
43+
},
44+
45+
updated() {
46+
this.pending = false
47+
}
48+
}
49+
50+
let liveSocket = new LiveSocket("/live", Socket, { hooks: window.Hooks })
1851
liveSocket.connect()
1952

2053
/*

lib/diff/storage/local.ex

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,19 @@ defmodule Diff.Storage.Local do
8181
|> String.replace_prefix(prefix, "")
8282
|> String.replace_suffix(".html", "")
8383
end)
84-
|> Enum.sort()
84+
|> Enum.sort_by(fn patch_id ->
85+
# Extract numeric part for proper sorting (e.g., "patch-10" -> 10)
86+
case String.split(patch_id, "-") do
87+
["patch", num_str] ->
88+
case Integer.parse(num_str) do
89+
{num, _} -> num
90+
:error -> 0
91+
end
92+
93+
_ ->
94+
0
95+
end
96+
end)
8597

8698
{:ok, patch_ids}
8799
else

lib/diff_web/live/diff_live_view.ex

Lines changed: 73 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -61,22 +61,13 @@ defmodule DiffWeb.DiffLiveView do
6161
<div class="ghd-container" id="patch-list">
6262
<%= for patch_id <- @loaded_patches do %>
6363
<div id={"patch-#{patch_id}"}>
64-
<%= case Map.get(@patch_contents, patch_id) do %>
65-
<% nil -> %>
66-
<div class="message-container">
67-
<div class="message">Loading patch <%= patch_id %>...</div>
68-
</div>
69-
<% content -> %>
70-
<%= raw(content) %>
71-
<% end %>
64+
<%= raw(Map.get(@patch_contents, patch_id, "")) %>
7265
</div>
7366
<% end %>
7467
7568
<%= if @has_more_patches do %>
76-
<div class="message-container">
77-
<button phx-click="load-more" class="diff-button button">
78-
Load more patches
79-
</button>
69+
<div id="loading-trigger" phx-hook="InfiniteScroll" data-loaded={length(@loaded_patches)}>
70+
Loading more patches...
8071
</div>
8172
<% end %>
8273
</div>
@@ -215,18 +206,17 @@ defmodule DiffWeb.DiffLiveView do
215206
defp resolve_latest_version(_package, from, to), do: {:ok, from, to}
216207

217208
def handle_event("load-more", _params, socket) do
218-
batch_size = 10
209+
batch_size = 5
219210
{next_batch, remaining} = Enum.split(socket.assigns.remaining_patches, batch_size)
220211

221212
socket =
222213
socket
223214
|> assign(
224-
loaded_patches: socket.assigns.loaded_patches ++ next_batch,
225215
remaining_patches: remaining,
226216
has_more_patches: length(remaining) > 0
227217
)
228218

229-
send(self(), {:load_patches, next_batch})
219+
send(self(), {:load_patches_and_update, next_batch})
230220

231221
{:noreply, socket}
232222
end
@@ -292,22 +282,60 @@ defmodule DiffWeb.DiffLiveView do
292282
{:noreply, assign(socket, error: "Invalid diff", generating: false)}
293283
end
294284

295-
def handle_info({:load_patches, patch_ids}, socket) do
285+
def handle_info({:load_patches_and_update, patch_ids}, socket) do
296286
%{package: package, from: from, to: to} = socket.assigns
297287

298288
loaded_contents =
299289
patch_ids
300290
|> Enum.reduce(%{}, fn patch_id, acc ->
301291
case Diff.Storage.get_patch(package, from, to, patch_id) do
302292
{:ok, content} ->
303-
Map.put(acc, patch_id, content)
293+
sanitized_content = sanitize_utf8(content)
294+
Map.put(acc, patch_id, sanitized_content)
304295

305296
{:error, reason} ->
306297
Logger.error("Failed to load patch #{patch_id}: #{inspect(reason)}")
307298
Map.put(acc, patch_id, "<div class='patch-error'>Failed to load patch</div>")
308299
end
309300
end)
310301

302+
# Update BOTH loaded_patches AND patch_contents in ONE DOM update
303+
new_loaded_patches = socket.assigns.loaded_patches ++ patch_ids
304+
305+
socket =
306+
socket
307+
|> assign(
308+
loaded_patches: new_loaded_patches,
309+
patch_contents: Map.merge(socket.assigns.patch_contents, loaded_contents),
310+
loading: false
311+
)
312+
313+
{:noreply, socket}
314+
end
315+
316+
def handle_info({:load_patches, patch_ids}, socket) do
317+
%{package: package, from: from, to: to} = socket.assigns
318+
319+
Logger.info("📦 Actually loading patch content for: #{inspect(patch_ids)}")
320+
321+
loaded_contents =
322+
patch_ids
323+
|> Enum.reduce(%{}, fn patch_id, acc ->
324+
case Diff.Storage.get_patch(package, from, to, patch_id) do
325+
{:ok, content} ->
326+
# Ensure content is valid UTF-8 to prevent JSON encoding errors
327+
sanitized_content = sanitize_utf8(content)
328+
Logger.info("✅ Successfully loaded patch #{patch_id} (#{byte_size(content)} bytes)")
329+
Map.put(acc, patch_id, sanitized_content)
330+
331+
{:error, reason} ->
332+
Logger.error("❌ Failed to load patch #{patch_id}: #{inspect(reason)}")
333+
Map.put(acc, patch_id, "<div class='patch-error'>Failed to load patch</div>")
334+
end
335+
end)
336+
337+
Logger.info("🎯 Loaded #{map_size(loaded_contents)} patches, updating socket assigns")
338+
311339
socket =
312340
socket
313341
|> assign(
@@ -336,6 +364,7 @@ defmodule DiffWeb.DiffLiveView do
336364
if patch.chunks != [] do
337365
patch_html =
338366
Phoenix.View.render_to_string(DiffWeb.RenderView, "patch.html", patch: patch)
367+
|> sanitize_utf8()
339368

340369
patch_id = "patch-#{index}"
341370

@@ -360,6 +389,7 @@ defmodule DiffWeb.DiffLiveView do
360389
{:too_large, file_path} ->
361390
too_large_html =
362391
Phoenix.View.render_to_string(DiffWeb.RenderView, "too_large.html", file: file_path)
392+
|> sanitize_utf8()
363393

364394
patch_id = "patch-#{index}"
365395

@@ -440,6 +470,32 @@ defmodule DiffWeb.DiffLiveView do
440470
defp parse_version(""), do: {:ok, :latest}
441471
defp parse_version(input), do: Version.parse(input)
442472

473+
defp sanitize_utf8(content) when is_binary(content) do
474+
# Replace invalid UTF-8 bytes with replacement character
475+
case String.valid?(content) do
476+
true ->
477+
content
478+
479+
false ->
480+
# Convert to UTF-8, replacing invalid bytes
481+
content
482+
|> :unicode.characters_to_binary(:latin1, :utf8)
483+
|> case do
484+
result when is_binary(result) ->
485+
result
486+
487+
_ ->
488+
# If conversion fails, scrub invalid bytes
489+
content
490+
|> String.codepoints()
491+
|> Enum.filter(&String.valid?/1)
492+
|> Enum.join()
493+
end
494+
end
495+
end
496+
497+
defp sanitize_utf8(content), do: content
498+
443499
defp parse_diff(diff) do
444500
case String.split(diff, ":", trim: true) do
445501
[app, from, to] -> {app, from, to, build_url(app, from, to)}

priv/gettext/en/LC_MESSAGES/errors.po

Lines changed: 0 additions & 11 deletions
This file was deleted.

priv/gettext/errors.pot

Lines changed: 0 additions & 10 deletions
This file was deleted.

0 commit comments

Comments
 (0)