From 03c4bf266bc66641c2b28e86cb3eb37a0586a275 Mon Sep 17 00:00:00 2001
From: Krishna Penukonda
Date: Thu, 2 Jul 2026 09:51:53 +0000
Subject: [PATCH] feat: add recently edited section to /live page
- Broadcast edits on shared 'edits' PubSub topic
- Track recent edits in LiveLive with real-time updates
- Show up to 10 most recently edited non-live docs
- Fetch excerpts for recent docs on first edit broadcast
- Docs never appear in both live and recent sections
---
lib/unfinal/document_server.ex | 6 +++++
lib/unfinal/documents.ex | 3 +++
lib/unfinal_web/live/live_live.ex | 44 +++++++++++++++++++++++++++++++
3 files changed, 53 insertions(+)
diff --git a/lib/unfinal/document_server.ex b/lib/unfinal/document_server.ex
index b95f004..530d8ca 100644
--- a/lib/unfinal/document_server.ex
+++ b/lib/unfinal/document_server.ex
@@ -215,5 +215,11 @@ defmodule Unfinal.DocumentServer do
path,
%{content: doc.content, etag: doc.etag, revision: doc.revision}
})
+
+ Phoenix.PubSub.broadcast(Unfinal.PubSub, Documents.edit_topic(), {
+ :edit,
+ path,
+ System.system_time(:second)
+ })
end
end
diff --git a/lib/unfinal/documents.ex b/lib/unfinal/documents.ex
index 378e5f4..99f1b06 100644
--- a/lib/unfinal/documents.ex
+++ b/lib/unfinal/documents.ex
@@ -19,6 +19,9 @@ defmodule Unfinal.Documents do
def topic(path),
do: @topic_prefix <> Base.url_encode64(ContentStore.normalize_path(path), padding: false)
+ @spec edit_topic() :: String.t()
+ def edit_topic, do: "edits"
+
@spec get(path()) :: Document.t()
def get(path), do: path |> ContentStore.normalize_path() |> server_call(:get)
diff --git a/lib/unfinal_web/live/live_live.ex b/lib/unfinal_web/live/live_live.ex
index 34a5459..0b1ca33 100644
--- a/lib/unfinal_web/live/live_live.ex
+++ b/lib/unfinal_web/live/live_live.ex
@@ -17,6 +17,7 @@ defmodule UnfinalWeb.LiveLive do
if connected?(socket) do
Phoenix.PubSub.subscribe(Unfinal.PubSub, @topic)
+ Phoenix.PubSub.subscribe(Unfinal.PubSub, Documents.edit_topic())
active_paths = active_paths()
Enum.each(active_paths, &Phoenix.PubSub.subscribe(Unfinal.PubSub, Documents.topic(&1)))
@@ -25,6 +26,7 @@ defmodule UnfinalWeb.LiveLive do
active_paths: active_paths,
sorted_paths: sorted_paths(),
excerpts: excerpts(active_paths, %{}),
+ recent_edits: %{},
authenticated: authenticated,
user: user,
claimed_namespace: claimed_namespace,
@@ -37,6 +39,7 @@ defmodule UnfinalWeb.LiveLive do
active_paths: MapSet.new(),
sorted_paths: [],
excerpts: %{},
+ recent_edits: %{},
authenticated: authenticated,
user: user,
claimed_namespace: claimed_namespace,
@@ -72,6 +75,20 @@ defmodule UnfinalWeb.LiveLive do
end
end
+ def handle_info({:edit, path, timestamp}, socket) do
+ socket = update(socket, :recent_edits, &Map.put(&1, path, timestamp))
+
+ socket =
+ if not MapSet.member?(socket.assigns.active_paths, path) and
+ not Map.has_key?(socket.assigns.excerpts, path) do
+ update(socket, :excerpts, &Map.put(&1, path, Documents.get(path).content))
+ else
+ socket
+ end
+
+ {:noreply, socket}
+ end
+
@impl true
def handle_event("toggle_mobile_menu", _params, socket) do
{:noreply, assign(socket, mobile_menu_open: !socket.assigns.mobile_menu_open)}
@@ -120,6 +137,14 @@ defmodule UnfinalWeb.LiveLive do
defp document_href("/" <> path), do: "/n/" <> path
defp document_href(_path), do: "/n"
+ defp visible_recent(recent_edits, active_paths, limit \\ 10) do
+ recent_edits
+ |> Enum.reject(fn {path, _ts} -> MapSet.member?(active_paths, path) end)
+ |> Enum.sort_by(fn {_, ts} -> -ts end)
+ |> Enum.take(limit)
+ |> Enum.map(fn {path, _ts} -> path end)
+ end
+
@impl true
def render(assigns) do
~H"""
@@ -160,6 +185,25 @@ defmodule UnfinalWeb.LiveLive do
+
+ <% visible_recent = visible_recent(@recent_edits, @active_paths) %>
+
+