@@ -3713,9 +3713,8 @@ defmodule Vutuv.Posts do
37133713 # answer with nothing to read above it used to be.
37143714 #
37153715 # It rides `remote_reply_ref`, already preloaded with its account
3716- # (`post_preloads/0`), so this costs no lookup — only the same three batch
3717- # reads a remote card needs anywhere (its pictures, the reader's marks, the
3718- # reader's follow), run once for the page. `taken` keeps a post that already
3716+ # (`post_preloads/0`), so finding one costs no lookup; what it costs beyond
3717+ # that is `decorate_remote_parents/2` below. `taken` keeps a post that already
37193718 # has a card **further up the feed** from getting a second one; a post whose
37203719 # own row is on *this* page is not in it — that row steps aside instead, see
37213720 # `settle_remote_cards/5`.
@@ -3727,42 +3726,81 @@ defmodule Vutuv.Posts do
37273726 Vutuv.Fediverse . subject_key ( parent ) not in taken ,
37283727 do: % { post_id: post . id , remote_post: parent }
37293728
3730- case Enum . uniq_by ( parents , & parent_key / 1 ) do
3729+ case decorate_remote_parents ( parents , viewer ) do
3730+ empty when empty == % { } -> entries
3731+ by_post -> Enum . map ( entries , & claim_remote_parents ( & 1 , by_post ) )
3732+ end
3733+ end
3734+
3735+ @ doc """
3736+ The cached post each of these posts answers (issue #1165), as the card the
3737+ renderer draws above it: `%{post_id => card}`, and `%{}` where none of them
3738+ answers anything out there.
3739+
3740+ What `attach_remote_parents/3` builds for the feed, for a caller holding a
3741+ plain list of posts instead of feed entries — the permalink's conversation
3742+ (`VutuvWeb.PostLive.Thread`), which is the page a link to such an answer
3743+ lands on and was the one place that showed the answer without what it
3744+ answers.
3745+ """
3746+ def remote_parents ( posts , viewer ) do
3747+ parents =
3748+ for post <- posts ,
3749+ % RemotePost { } = parent <- [ remote_parent ( post ) ] ,
3750+ do: % { post_id: post . id , remote_post: parent }
3751+
3752+ decorate_remote_parents ( parents , viewer )
3753+ end
3754+
3755+ # The shared tail of both callers: one card per cached post, only the ones
3756+ # this reader may read, and the batch reads a remote card needs, run once for
3757+ # the page rather than once per card. Nothing to draw means no read at all.
3758+ #
3759+ # **One card per cached post per page**, the rule `dedupe_remote/1`,
3760+ # `collapse_reposts/1` and `attach_thread_notes/3` already hold for the other
3761+ # kinds — and here it is not about repetition. The card's action bar is a
3762+ # LiveComponent keyed by the cached post
3763+ # (`RemoteActionsComponent.dom_id(:remote_post, id)`), so a second card emits
3764+ # a duplicate LiveView id, which raises inside `render_pending_components/6`
3765+ # during the **static** render: the page 500s rather than degrading. Two
3766+ # members answering the same post out there is ordinary behaviour, and the
3767+ # first such pair in the database took /feed down for every reader who had
3768+ # both answers on one page (2026-08-28). So the deduped list decides where
3769+ # the card renders as well as which reads run: the answers that do not claim
3770+ # it keep their "Replying to …" line, which is what this feature replaced and
3771+ # still beats an error page.
3772+ defp decorate_remote_parents ( parents , viewer ) do
3773+ case parents |> Enum . uniq_by ( & parent_key / 1 ) |> readable_parents ( viewer ) do
37313774 [ ] ->
3732- entries
3775+ % { }
37333776
37343777 unique ->
3735- # **One card per cached post per page**, the rule `dedupe_remote/1`,
3736- # `collapse_reposts/1` and `attach_thread_notes/3` already hold for the
3737- # other kinds — and here it is not about repetition. The card's action
3738- # bar is a LiveComponent keyed by the cached post
3739- # (`RemoteActionsComponent.dom_id(:remote_post, id)`), so a second card
3740- # emits a duplicate LiveView id, which raises inside
3741- # `render_pending_components/6` during the **static** render: the page
3742- # 500s rather than degrading. Two members answering the same post out
3743- # there is ordinary behaviour, and the first such pair in the database
3744- # took /feed down for every reader who had both answers on one page
3745- # (2026-08-28).
3746- #
3747- # `unique` therefore decides where the card renders as well as which
3748- # batch reads run, and the placement is built from the decorated list
3749- # itself — the older version kept a second list to re-expand from, and
3750- # a comment saying the repeats "must not pay for the batch reads, not
3751- # the places they render" is exactly how the trap was rationalised.
3752- # The answers that do not claim it keep their "Replying to …" line,
3753- # which is what this feature replaced and still beats an error page.
3754- by_post =
3755- unique
3756- |> attach_remote_images ( )
3757- |> attach_remote_quotes ( )
3758- |> attach_remote_follows ( viewer )
3759- |> attach_remote_likes ( viewer )
3760- |> Map . new ( & { & 1 . post_id , & 1 } )
3761-
3762- Enum . map ( entries , & claim_remote_parents ( & 1 , by_post ) )
3778+ unique
3779+ |> attach_remote_images ( )
3780+ |> attach_remote_quotes ( )
3781+ |> attach_remote_follows ( viewer )
3782+ |> attach_remote_likes ( viewer )
3783+ |> Map . new ( & { & 1 . post_id , & 1 } )
37633784 end
37643785 end
37653786
3787+ # Whose audience lets *this* reader see it — never the audience the answer's
3788+ # author had. Answering a followers-only post in public is refused
3789+ # (`Fediverse.check_remote_post_reply/2`), but an audience narrows *after* the
3790+ # answer often enough that the composer re-reads the row for exactly that, and
3791+ # a card drawn above a public answer would then hand the post to every reader
3792+ # of that answer. The permalink is a public page, so the question is
3793+ # `Fediverse.publicly_readable_remote_post_ids/2` rather than the signed-in
3794+ # one, and an open parent — nearly all of them — costs no query either way.
3795+ defp readable_parents ( parents , viewer ) do
3796+ readable =
3797+ parents
3798+ |> Enum . map ( & & 1 . remote_post )
3799+ |> Vutuv.Fediverse . publicly_readable_remote_post_ids ( viewer )
3800+
3801+ Enum . filter ( parents , & MapSet . member? ( readable , & 1 . remote_post . id ) )
3802+ end
3803+
37663804 defp parent_key ( % { remote_post: post } ) , do: Vutuv.Fediverse . subject_key ( post )
37673805
37683806 defp claim_remote_parents ( entry , by_post ) do
0 commit comments