Skip to content

Support permissioned feeds - #178

Merged
rishibalakrishnan merged 23 commits into
mainfrom
feat/spaces-private-feeds
Aug 30, 2026
Merged

Support permissioned feeds#178
rishibalakrishnan merged 23 commits into
mainfrom
feat/spaces-private-feeds

Conversation

@rishibalakrishnan

@rishibalakrishnan rishibalakrishnan commented Aug 26, 2026

Copy link
Copy Markdown

Summary

  • support reading, posting, replying, quoting, liking, deleting, and linking permissioned-space posts
  • route private-post liked-by and quotes pages through permission-aware AppView endpoints
  • preserve standard public and legacy community-only engagement routing unchanged
  • retain the space context through suffix routes, notification hydration, and post permalinks
  • keep reposts unsupported for private-space posts without falling through to the public endpoint

Validation

  • focused client suites passed, including space engagement routing, notification subjects, post routing, and permalink parsing
  • ESLint, TypeScript, Prettier, and git diff --check passed
  • locally validated the private feed and liked-by flow end to end

No Dockerfile, Metro, webpack, package-script, or dependency changes are included in the final diff.

The target was only resolved for top-level posts, so a reply into a
community feed lost the feed it was replying within.
Adds the space write path: records go to the account's own PDS, which
routes com.atproto.space.* to the space host, so the request carries no
atproto-proxy header — proxying would send private content to the appview,
which is not where it lives.

A like is written into the permissioned repo, never the public one. A
public like whose subject is a space URI would announce that the private
post exists and to whom; a test pins that the call is space.createRecord
and never repo.createRecord.

Records are built as raw JSON rather than through typed builders, because
a like or a reply inside a space refers to space URIs, which are not valid
at-uris and would fail SDK-side validation. The space-URI parser is
vendored from the appview verbatim and stays dependency-free so both
copies can remain identical.

Feed config gains `space`; its presence is the sole discriminator.
`visibility: 'gated'` remains an invariant the config must satisfy to
parse, not an input to any decision — access is always the space
authority's answer.

A 404 from the space methods is read as "this PDS has no space support",
which is how a foreign-PDS account will be offered migration rather than a
generic failure.

Also drops the dead `contentStore` proxy target from the submitPost call.
It named a per-feed appview for the retired multi-tenant path and was
always undefined on the Blacksky feed, so that path is unchanged.
post() sent every community feed to postCommunity, which submits to the
appview and writes a public stub. A feed whose config names a space keeps
its content in the author's permissioned repo instead, so it branches to
postToSpace before that routing.

Also drops submitBody.feed: the appview's submitPost no longer accepts it
after the multi-tenant retirement, so sending it broke the Blacksky write.
Likes on a permissioned post go into the author's space repo, never the
public one: a public like whose subject is a space URI would announce that
the private post exists and who can see it. Self-delete learns the same
path, checked before AtUri sees a URI it would misparse.

The routing sits in space-write as three helpers answering null for
anything that is not a space record, so the mutation hooks read as
`spaceX(...) ?? publicX(...)` and the decision is testable without them.
Reposts and threadgates/postgates are both deferred on space feeds, so the
composer drops the gate button when the target names a space and the post
controls disable repost and quote. Quoting is disabled with repost: a
public quote record naming a space URI leaks the private post's existence
even though the appview would decline to hydrate it.

isCommunityPostUri now recognises space records structurally, which also
takes bookmarks off private posts, and moves to lib/api so it can be tested
without the component tree.
A space write against a server that has never heard of the space methods
answers 404, which is how an account hosted elsewhere is recognised. The
composer now names that reason and links to move.blacksky.community rather
than reporting a failure the user cannot act on. Reads work cross-PDS; only
writes need an account hosted here.
A space record cannot be addressed by any of the standard read methods, so
threads route to community.blacksky.feed.getCommunityThread and single-post
reads to getCommunityPost, both of which take the uri as a plain string.

Post links keep their existing shape with the space attached:
/profile/{authorDid}/post/{rkey}?space=at://... — the path already carries
two of the six segments. Stripping the query yields a public post url that
does not resolve, so a truncated link cannot confirm that a space exists.

One postPermalink helper replaces eight copies of the same AtUri branch,
each of which misparsed a space uri. The thread screen resolves a handle to
a DID before assembling the uri, since that form admits only DIDs, and the
liked-by / reposted-by / quotes stats render unlinked on space posts: those
screens have no endpoint that would accept the uri.
Replies: resolveReply learns space records, reading the parent back through
the appview — the only read path a client has into a space — and taking the
thread root from the parent's own record. postToSpace seeds its chain from
that instead of ignoring the reply target, and the composer recognises a
space parent as a forced community target.

Quotes inside a feed: a space post now survives the composer's url round
trip. postUriToRelativePath emits the ?space= form and resolveLink rebuilds
the record uri from it, so the existing quote pipeline works unchanged and
the embed is a strongRef to the space record. Quoting a space post into a
public post stays refused.

Mutes: thread mute/unmute on a space thread goes to setThreadMute.
The post view now exposes communitySpace, a plain string, because a space URI
is not a valid at-uri and a feed is only a view over the space where content
lives. Replying to or quoting a space post therefore inherits the space and
writes straight to it, with no feed to resolve first.
@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown
Old size New size Diff
9.14 MB 9.16 MB 28.93 KB (0.31%)

@rishibalakrishnan

Copy link
Copy Markdown
Author

@github-actions ota

@github-actions

github-actions Bot commented Aug 29, 2026

Copy link
Copy Markdown

Your requested OTA deployment was unsuccessful. See action logs for more details.

Generated by PR labeler 🤖

@rishibalakrishnan

Copy link
Copy Markdown
Author

@github-actions ota

@github-actions

github-actions Bot commented Aug 29, 2026

Copy link
Copy Markdown

Your requested OTA deployment was published to channel pull-request-178.

Note

On-device apply works on TestFlight and development builds only. Production builds keep the anti-bricking safety net (disableAntiBrickingMeasures is off), so the deep link below is a no-op there.

blacksky://intent/apply-ota?channel=pull-request-178

Generated by PR labeler 🤖

@rishibalakrishnan

Copy link
Copy Markdown
Author

@github-actions ota

@github-actions

github-actions Bot commented Aug 29, 2026

Copy link
Copy Markdown

Your requested OTA deployment was published to channel pull-request-178.

Note

On-device apply works on TestFlight and development builds only. Production builds keep the anti-bricking safety net (disableAntiBrickingMeasures is off), so the deep link below is a no-op there.

blacksky://intent/apply-ota?channel=pull-request-178

Generated by PR labeler 🤖

@rishibalakrishnan
rishibalakrishnan merged commit 39c8763 into main Aug 30, 2026
15 of 16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant