Replace Python sx_call loops with data-driven SX defcomps using map

Move rendering logic from Python for-loops building sx_call strings into
SX defcomp components that use map/lambda over data dicts. Python now
serializes display data into plain dicts and passes them via a single
sx_call; the SX layer handles iteration and conditional rendering.

Covers orders (rows, items, calendar, tickets), federation (timeline,
search, actors, profile activities), and blog (cards, pages, filters,
snippets, menu items, tag groups, page search, nav OOB).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 16:03:29 +00:00
parent 0c9dbd6657
commit c1ad6fd8d4
12 changed files with 608 additions and 452 deletions

View File

@@ -53,3 +53,16 @@
(defcomp ~federation-profile-summary-text (&key text)
(p :class "mt-2" text))
;; Data-driven activities list (replaces Python loop in render_profile_page)
(defcomp ~federation-activities-from-data (&key activities)
(if (empty? (or activities (list)))
(~federation-activities-empty)
(~federation-activities-list
:items (<> (map (lambda (a)
(~federation-activity-card
:activity-type (get a "activity_type")
:published (get a "published")
:obj-type (when (get a "object_type")
(~federation-activity-obj-type :obj-type (get a "object_type")))))
activities)))))

View File

@@ -40,6 +40,47 @@
summary)
button))
;; Data-driven actor card (replaces Python _actor_card_sx loop)
(defcomp ~federation-actor-card-from-data (&key d has-actor csrf follow-url unfollow-url list-type)
(let* ((icon-url (get d "icon_url"))
(display-name (get d "display_name"))
(username (get d "username"))
(domain (get d "domain"))
(actor-url (get d "actor_url"))
(safe-id (get d "safe_id"))
(initial (or (get d "initial") "?"))
(avatar (~avatar
:src icon-url
:cls (if icon-url "w-12 h-12 rounded-full"
"w-12 h-12 rounded-full bg-stone-300 flex items-center justify-center text-stone-600 font-bold")
:initial (when (not icon-url) initial)))
(name-sx (if (get d "external_link")
(~federation-actor-name-link-external :href (get d "name_href") :name display-name)
(~federation-actor-name-link :href (get d "name_href") :name display-name)))
(summary-sx (when (get d "summary")
(~federation-actor-summary :summary (get d "summary"))))
(is-followed (get d "is_followed"))
(button (when has-actor
(if (or (= list-type "following") is-followed)
(~federation-unfollow-button :action unfollow-url :csrf csrf :actor-url actor-url)
(~federation-follow-button :action follow-url :csrf csrf :actor-url actor-url
:label (if (= list-type "followers") "Follow Back" "Follow"))))))
(~federation-actor-card
:cls "bg-white rounded-lg shadow-sm border border-stone-200 p-4 mb-3 flex items-center gap-4"
:id (str "actor-" safe-id)
:avatar avatar :name name-sx :username username :domain domain
:summary summary-sx :button button)))
;; Data-driven actor list (replaces Python _search_results_sx / _actor_list_items_sx loops)
(defcomp ~federation-actor-list-from-data (&key actors next-url has-actor csrf
follow-url unfollow-url list-type)
(<>
(map (lambda (d)
(~federation-actor-card-from-data :d d :has-actor has-actor :csrf csrf
:follow-url follow-url :unfollow-url unfollow-url :list-type list-type))
(or actors (list)))
(when next-url (~federation-scroll-sentinel :url next-url))))
(defcomp ~federation-search-info (&key cls text)
(p :class cls text))

View File

@@ -90,6 +90,65 @@
compose)
(div :id "timeline" timeline))
;; --- Data-driven post card (replaces Python _post_card_sx loop) ---
(defcomp ~federation-post-card-from-data (&key d has-actor csrf
like-url unlike-url
boost-url unboost-url)
(let* ((boosted-by (get d "boosted_by"))
(actor-icon (get d "actor_icon"))
(actor-name (get d "actor_name"))
(initial (or (get d "initial") "?"))
(avatar (~avatar
:src actor-icon
:cls (if actor-icon "w-10 h-10 rounded-full"
"w-10 h-10 rounded-full bg-stone-300 flex items-center justify-center text-stone-600 font-bold text-sm")
:initial (when (not actor-icon) initial)))
(boost (when boosted-by (~federation-boost-label :name boosted-by)))
(content-sx (if (get d "summary")
(~federation-content :content (get d "content") :summary (get d "summary"))
(~federation-content :content (get d "content"))))
(original (when (get d "original_url")
(~federation-original-link :url (get d "original_url"))))
(safe-id (get d "safe_id"))
(interactions (when has-actor
(let* ((oid (get d "object_id"))
(ainbox (get d "author_inbox"))
(target (str "#interactions-" safe-id))
(liked (get d "liked_by_me"))
(boosted-me (get d "boosted_by_me"))
(l-action (if liked unlike-url like-url))
(l-cls (str "flex items-center gap-1 " (if liked "text-red-500 hover:text-red-600" "hover:text-red-500")))
(l-icon (if liked "\u2665" "\u2661"))
(b-action (if boosted-me unboost-url boost-url))
(b-cls (str "flex items-center gap-1 " (if boosted-me "text-green-600 hover:text-green-700" "hover:text-green-600")))
(reply-url (get d "reply_url"))
(reply (when reply-url (~federation-reply-link :url reply-url)))
(like-form (~federation-like-form
:action l-action :target target :oid oid :ainbox ainbox
:csrf csrf :cls l-cls :icon l-icon :count (get d "like_count")))
(boost-form (~federation-boost-form
:action b-action :target target :oid oid :ainbox ainbox
:csrf csrf :cls b-cls :count (get d "boost_count"))))
(div :id (str "interactions-" safe-id)
(~federation-interaction-buttons :like like-form :boost boost-form :reply reply))))))
(~federation-post-card
:boost boost :avatar avatar
:actor-name actor-name :actor-username (get d "actor_username")
:domain (get d "domain") :time (get d "time")
:content content-sx :original original
:interactions interactions)))
;; Data-driven timeline items (replaces Python _timeline_items_sx loop)
(defcomp ~federation-timeline-items-from-data (&key items next-url has-actor csrf
like-url unlike-url boost-url unboost-url)
(<>
(map (lambda (d)
(~federation-post-card-from-data :d d :has-actor has-actor :csrf csrf
:like-url like-url :unlike-url unlike-url :boost-url boost-url :unboost-url unboost-url))
(or items (list)))
(when next-url (~federation-scroll-sentinel :url next-url))))
;; --- Compose ---
(defcomp ~federation-compose-reply (&key reply-to)

View File

@@ -147,62 +147,49 @@ def _interaction_buttons_sx(item: Any, actor: Any) -> str:
)
def _post_card_sx(item: Any, actor: Any) -> str:
"""Render a single timeline post card."""
# ---------------------------------------------------------------------------
# Post card data serializer
# ---------------------------------------------------------------------------
def _post_card_data(item: Any, actor: Any) -> dict:
"""Serialize a timeline item to a display-data dict for sx rendering."""
from quart import url_for
boosted_by = getattr(item, "boosted_by", None)
actor_icon = getattr(item, "actor_icon", None)
actor_name = getattr(item, "actor_name", "?")
actor_username = getattr(item, "actor_username", "")
actor_domain = getattr(item, "actor_domain", "")
content = getattr(item, "content", "")
summary = getattr(item, "summary", None)
published = getattr(item, "published", None)
oid = getattr(item, "object_id", "") or ""
safe_id = oid.replace("/", "_").replace(":", "_")
initial = actor_name[0].upper() if (not actor_icon and actor_name) else "?"
url = getattr(item, "url", None)
post_type = getattr(item, "post_type", "")
published = getattr(item, "published", None)
summary = getattr(item, "summary", None)
boost_sx = sx_call(
"federation-boost-label", name=str(escape(boosted_by)),
) if boosted_by else ""
initial = actor_name[0].upper() if (not actor_icon and actor_name) else "?"
avatar = sx_call(
"avatar", src=actor_icon or None,
cls="w-10 h-10 rounded-full" if actor_icon else "w-10 h-10 rounded-full bg-stone-300 flex items-center justify-center text-stone-600 font-bold text-sm",
initial=None if actor_icon else initial,
)
domain_str = f"@{escape(actor_domain)}" if actor_domain else ""
time_str = published.strftime("%b %d, %H:%M") if published else ""
if summary:
content_sx = sx_call(
"federation-content",
content=content, summary=str(escape(summary)),
)
else:
content_sx = sx_call("federation-content", content=content)
original_sx = ""
if url and post_type == "remote":
original_sx = sx_call("federation-original-link", url=url)
interactions_sx = ""
if actor:
oid = getattr(item, "object_id", "") or ""
safe_id = oid.replace("/", "_").replace(":", "_")
interactions_sx = f'(div :id {serialize(f"interactions-{safe_id}")} {_interaction_buttons_sx(item, actor)})'
return sx_call(
"federation-post-card",
boost=SxExpr(boost_sx) if boost_sx else None,
avatar=SxExpr(avatar),
d: dict[str, Any] = dict(
boosted_by=str(escape(boosted_by)) if boosted_by else None,
actor_icon=actor_icon or None,
actor_name=str(escape(actor_name)),
actor_username=str(escape(actor_username)),
domain=domain_str, time=time_str,
content=SxExpr(content_sx),
original=SxExpr(original_sx) if original_sx else None,
interactions=SxExpr(interactions_sx) if interactions_sx else None,
domain=f"@{escape(actor_domain)}" if actor_domain else "",
time=published.strftime("%b %d, %H:%M") if published else "",
content=getattr(item, "content", ""),
summary=str(escape(summary)) if summary else None,
original_url=url if url and post_type == "remote" else None,
object_id=oid,
author_inbox=getattr(item, "author_inbox", "") or "",
safe_id=safe_id,
initial=initial,
like_count=str(getattr(item, "like_count", 0) or 0),
boost_count=str(getattr(item, "boost_count", 0) or 0),
liked_by_me=bool(getattr(item, "liked_by_me", False)),
boosted_by_me=bool(getattr(item, "boosted_by_me", False)),
)
if actor and oid:
d["reply_url"] = url_for("social.defpage_compose_form", reply_to=oid)
return d
# ---------------------------------------------------------------------------
@@ -213,9 +200,11 @@ def _timeline_items_sx(items: list, timeline_type: str, actor: Any,
actor_id: int | None = None) -> str:
"""Render timeline items with infinite scroll sentinel."""
from quart import url_for
from shared.browser.app.csrf import generate_csrf_token
parts = [_post_card_sx(item, actor) for item in items]
item_dicts = [_post_card_data(item, actor) for item in items]
next_url = None
if items:
last = items[-1]
before = last.published.isoformat() if last.published else ""
@@ -223,22 +212,28 @@ def _timeline_items_sx(items: list, timeline_type: str, actor: Any,
next_url = url_for("social.actor_timeline_page", id=actor_id, before=before)
else:
next_url = url_for(f"social.{timeline_type}_timeline_page", before=before)
parts.append(sx_call("federation-scroll-sentinel", url=next_url))
return "(<> " + " ".join(parts) + ")" if parts else ""
kwargs: dict[str, Any] = dict(items=item_dicts, next_url=next_url)
if actor:
kwargs["has_actor"] = True
kwargs["csrf"] = generate_csrf_token()
kwargs["like_url"] = url_for("social.like")
kwargs["unlike_url"] = url_for("social.unlike")
kwargs["boost_url"] = url_for("social.boost")
kwargs["unboost_url"] = url_for("social.unboost")
return sx_call("federation-timeline-items-from-data", **kwargs)
# ---------------------------------------------------------------------------
# Search results (pagination fragment)
# ---------------------------------------------------------------------------
def _actor_card_sx(a: Any, actor: Any, followed_urls: set,
*, list_type: str = "search") -> str:
"""Render a single actor card with follow/unfollow button."""
from shared.browser.app.csrf import generate_csrf_token
def _actor_card_data(a: Any, followed_urls: set,
*, list_type: str = "search") -> dict:
"""Serialize an actor to a display-data dict for sx rendering."""
from quart import url_for
csrf = generate_csrf_token()
display_name = getattr(a, "display_name", None) or getattr(a, "preferred_username", "")
username = getattr(a, "preferred_username", "")
domain = getattr(a, "domain", "")
@@ -248,55 +243,43 @@ def _actor_card_sx(a: Any, actor: Any, followed_urls: set,
aid = getattr(a, "id", None)
safe_id = actor_url.replace("/", "_").replace(":", "_")
initial = (display_name or username)[0].upper() if (not icon_url and (display_name or username)) else "?"
avatar = sx_call(
"avatar", src=icon_url or None,
cls="w-12 h-12 rounded-full" if icon_url else "w-12 h-12 rounded-full bg-stone-300 flex items-center justify-center text-stone-600 font-bold",
initial=None if icon_url else initial,
is_followed = actor_url in (followed_urls or set())
d: dict[str, Any] = dict(
display_name=str(escape(display_name)),
username=str(escape(username)),
domain=str(escape(domain)),
icon_url=icon_url or None,
actor_url=actor_url,
summary=summary,
safe_id=safe_id,
initial=initial,
is_followed=is_followed,
)
# Name link
if (list_type in ("following", "search")) and aid:
name_sx = sx_call(
"federation-actor-name-link",
href=url_for("social.defpage_actor_timeline", id=aid),
name=str(escape(display_name)),
)
d["name_href"] = url_for("social.defpage_actor_timeline", id=aid)
else:
name_sx = sx_call(
"federation-actor-name-link-external",
href=f"https://{domain}/@{username}",
name=str(escape(display_name)),
)
d["name_href"] = f"https://{domain}/@{username}"
d["external_link"] = True
return d
summary_sx = sx_call("federation-actor-summary", summary=summary) if summary else ""
# Follow/unfollow button
button_sx = ""
if actor:
is_followed = actor_url in (followed_urls or set())
if list_type == "following" or is_followed:
button_sx = sx_call(
"federation-unfollow-button",
action=url_for("social.unfollow"), csrf=csrf, actor_url=actor_url,
)
else:
label = "Follow Back" if list_type == "followers" else "Follow"
button_sx = sx_call(
"federation-follow-button",
action=url_for("social.follow"), csrf=csrf, actor_url=actor_url, label=label,
)
def _actor_card_sx(a: Any, actor: Any, followed_urls: set,
*, list_type: str = "search") -> str:
"""Render a single actor card (used by follow/unfollow re-render)."""
from shared.browser.app.csrf import generate_csrf_token
from quart import url_for
d = _actor_card_data(a, followed_urls, list_type=list_type)
return sx_call(
"federation-actor-card",
cls="bg-white rounded-lg shadow-sm border border-stone-200 p-4 mb-3 flex items-center gap-4",
id=f"actor-{safe_id}",
avatar=SxExpr(avatar),
name=SxExpr(name_sx),
username=str(escape(username)), domain=str(escape(domain)),
summary=SxExpr(summary_sx) if summary_sx else None,
button=SxExpr(button_sx) if button_sx else None,
"federation-actor-card-from-data",
d=d,
has_actor=actor is not None,
csrf=generate_csrf_token() if actor else None,
follow_url=url_for("social.follow") if actor else None,
unfollow_url=url_for("social.unfollow") if actor else None,
list_type=list_type,
)
@@ -304,24 +287,38 @@ def _search_results_sx(actors: list, query: str, page: int,
followed_urls: set, actor: Any) -> str:
"""Render search results with pagination sentinel."""
from quart import url_for
from shared.browser.app.csrf import generate_csrf_token
parts = [_actor_card_sx(a, actor, followed_urls, list_type="search") for a in actors]
if len(actors) >= 20:
next_url = url_for("social.search_page", q=query, page=page + 1)
parts.append(sx_call("federation-scroll-sentinel", url=next_url))
return "(<> " + " ".join(parts) + ")" if parts else ""
actor_dicts = [_actor_card_data(a, followed_urls, list_type="search") for a in actors]
next_url = url_for("social.search_page", q=query, page=page + 1) if len(actors) >= 20 else None
return sx_call(
"federation-actor-list-from-data",
actors=actor_dicts, next_url=next_url, list_type="search",
has_actor=actor is not None,
csrf=generate_csrf_token() if actor else None,
follow_url=url_for("social.follow") if actor else None,
unfollow_url=url_for("social.unfollow") if actor else None,
)
def _actor_list_items_sx(actors: list, page: int, list_type: str,
followed_urls: set, actor: Any) -> str:
"""Render actor list items (following/followers) with pagination sentinel."""
from quart import url_for
from shared.browser.app.csrf import generate_csrf_token
parts = [_actor_card_sx(a, actor, followed_urls, list_type=list_type) for a in actors]
if len(actors) >= 20:
next_url = url_for(f"social.{list_type}_list_page", page=page + 1)
parts.append(sx_call("federation-scroll-sentinel", url=next_url))
return "(<> " + " ".join(parts) + ")" if parts else ""
actor_dicts = [_actor_card_data(a, followed_urls, list_type=list_type) for a in actors]
next_url = url_for(f"social.{list_type}_list_page", page=page + 1) if len(actors) >= 20 else None
return sx_call(
"federation-actor-list-from-data",
actors=actor_dicts, next_url=next_url, list_type=list_type,
has_actor=actor is not None,
csrf=generate_csrf_token() if actor else None,
follow_url=url_for("social.follow") if actor else None,
unfollow_url=url_for("social.unfollow") if actor else None,
)
# ---------------------------------------------------------------------------
@@ -683,23 +680,15 @@ async def render_profile_page(ctx: dict, actor: Any, activities: list,
"federation-profile-summary-text", text=str(escape(actor.summary)),
) if actor.summary else ""
activities_sx = ""
if activities:
parts = []
for a in activities:
published = a.published.strftime("%Y-%m-%d %H:%M") if a.published else ""
obj_type_sx = sx_call(
"federation-activity-obj-type", obj_type=a.object_type,
) if a.object_type else ""
parts.append(sx_call(
"federation-activity-card",
activity_type=a.activity_type, published=published,
obj_type=SxExpr(obj_type_sx) if obj_type_sx else None,
))
items_sx = "(<> " + " ".join(parts) + ")"
activities_sx = sx_call("federation-activities-list", items=SxExpr(items_sx))
else:
activities_sx = sx_call("federation-activities-empty")
activity_dicts = [
dict(
activity_type=a.activity_type,
published=a.published.strftime("%Y-%m-%d %H:%M") if a.published else "",
object_type=a.object_type,
)
for a in activities
]
activities_sx = sx_call("federation-activities-from-data", activities=activity_dicts)
content = sx_call(
"federation-profile-page",