feat(journal): audience-aware social feed with remote activities (§8+§9)
8.1/8.2: listSocialFeed is now a UNION ALL of local and remote branches, sorted on COALESCE(remote_published_at, created_at): - local rows: visibility='public' from accepted local follows — and the previously missing accepted_at filter is added (the spec's 'Pending follows contribute nothing' scenario) - remote rows: gated structurally by joining the viewer's OWN accepted follow against the originating actor — which is exactly the followers-only audience rule (a row reaches only viewers whose follow brought it in); attribution from the remote_actors cache; cards link outward to the canonical origin page (no local detail page for remote rows) 9.1: annotated — local Pending button shipped with locked accounts; remote Pending lives on /follows/outgoing. 9.2: already enforced + tested since §3 (actor/webfinger 404). 9.3 hardening: deliver-activity now re-checks the OWNER's profile visibility at send time, closing the enqueue→delivery flip window (enqueue-side and inbound-side gates already existed). Integration tests: the §8 audience-leak guard (A sees followers-only, B does not), public remote attribution + outward link, pending contributes nothing, mixed local/remote COALESCE ordering. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f8b24f58ef
commit
bf2d8bfbd4
6 changed files with 245 additions and 27 deletions
|
|
@ -31,6 +31,11 @@ export async function loadFeed(request: Request) {
|
|||
geojson: a.geojson ?? null,
|
||||
ownerUsername: a.ownerUsername,
|
||||
ownerDisplayName: a.ownerDisplayName,
|
||||
// Remote (federated) attribution — only the followed view can
|
||||
// contain remote rows; the public view is local-only.
|
||||
ownerDomain: "ownerDomain" in a ? a.ownerDomain : null,
|
||||
externalUrl: "externalUrl" in a ? a.externalUrl : null,
|
||||
remote: "remote" in a ? a.remote : false,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,7 +95,11 @@ export default function Feed({ loaderData }: Route.ComponentProps) {
|
|||
{activities.map((a) => (
|
||||
<li key={a.id}>
|
||||
<a
|
||||
href={`/activities/${a.id}`}
|
||||
// Remote (federated) activities link to their canonical
|
||||
// page on the origin instance — there is no local detail
|
||||
// page for them.
|
||||
href={a.remote && a.externalUrl ? a.externalUrl : `/activities/${a.id}`}
|
||||
{...(a.remote ? { target: "_blank", rel: "noopener noreferrer" } : {})}
|
||||
className="block rounded-lg border border-gray-200 p-4 hover:bg-gray-50"
|
||||
>
|
||||
<div className="flex gap-4">
|
||||
|
|
@ -112,13 +116,22 @@ export default function Feed({ loaderData }: Route.ComponentProps) {
|
|||
<div>
|
||||
<h3 className="text-base font-medium text-gray-900">{a.name}</h3>
|
||||
<div className="mt-1 text-sm text-gray-500">
|
||||
<a
|
||||
href={`/users/${a.ownerUsername}`}
|
||||
className="hover:text-gray-700 hover:underline"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{a.ownerDisplayName ?? a.ownerUsername}
|
||||
</a>
|
||||
{a.remote ? (
|
||||
<span>
|
||||
{a.ownerDisplayName ?? a.ownerUsername ?? a.ownerDomain}
|
||||
{a.ownerUsername && a.ownerDomain && (
|
||||
<span className="text-gray-400"> @{a.ownerUsername}@{a.ownerDomain}</span>
|
||||
)}
|
||||
</span>
|
||||
) : (
|
||||
<a
|
||||
href={`/users/${a.ownerUsername}`}
|
||||
className="hover:text-gray-700 hover:underline"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{a.ownerDisplayName ?? a.ownerUsername}
|
||||
</a>
|
||||
)}
|
||||
{" · "}
|
||||
<ClientDate iso={a.startedAt ?? a.createdAt} />
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue