~ knotless Search/
↑↓ move↵ open⇧↵ all results

grain.social/grain-ios

1 collaborator
0
1

Calls reach grain-serve by its own method names

Merged
Merged chadtmiller.com asked to merge into main from grain.social/grain-ios:feature/rust-calls

What changes

Every call names a method grain-serve serves. The app's 24 social.grain.unspecced.* methods (25 call sites, typeahead twice) take their names from the map in grain's lexicons/README.md, and its 8 dev.hatk.* methods (12 call sites) their replacements. Nothing in the app, its tests or its strings names hatk now. No call sends viewer: grain-serve reads it from the call's own auth.

Feeds are social.grain.feed.getFeed, asked for by URI: at://<grain-serve's DID>/social.grain.feed.generator/<name>, as describeFeeds lists them. The other parameters are unchanged.

Preferences are social.grain.actor.getPreferences and putPreferences, typed (social.grain.actor.defs#preferences) and written back whole. Changing one kind reads the set, changes that kind and writes it all back, so the other kinds stay as they were, the web's themePref among them. Only the four kinds grain-serve takes are sent.

Notifications are listNotifications, getUnreadCount (where countOnly was) and updateSeen (where putPreference lastSeenNotifications was). The list loads with the count beside it, and falls back to counting the page's isRead: false if the count fails.

Push is social.grain.notification.registerPush and unregisterPush, naming grain-serve's DID as serviceDid, ios and the bundle id.

Reports are com.atproto.moderation.createReport. The reasonType is the one grain-serve files under the chosen label, from the same table the web uses (REPORT_REASONS). The call carries atproto-proxy: <grain DID>#grain_appview, which the rpc scope already covers, so the PDS forwards it to grain and not to its own moderation service.

Records are read with com.atproto.repo.getRecord at the account's PDS. Its answer carries value; the profile editor reads the avatar from it.

Deleting a gallery happens at the PDS, since grain-serve's deleteGallery is the web's alone. It mirrors grain-serve's GOING_SQL: the gallery's items, the photos only it places and their exif, and the account's own favorites and comments on it, then the gallery. They go children first in applyWrites commits of at most 200, and each commit is indexed.

Deleting the account deletes every record in each social.grain.* collection describeRepo lists, at the PDS, without indexing. It then calls social.grain.server.deleteAccount {did}, which forgets the account once the PDS lists none.

A grain profile is seeded from Bluesky's after sign-in, as grain-serve seeds one for the web (login/profile.rs), which never sees the app's sign-in. It runs in the background (ProfileSeed) and never blocks or fails the sign-in. If social.grain.actor.profile/self is missing or has no displayName, the app reads app.bsky.actor.profile/self and writes a grain profile with its displayName, description and avatar (the blob ref as is), keeping any createdAt. An existing record is replaced with putRecord, swapped on the CID that was read; otherwise createRecord makes one at rkey self. The write is then indexed through the proxy.

Label definitions come from social.grain.moderation.describeLabels, read once through the proxy into LabelDefinitionsCache (policies.labelValueDefinitions). They are kept in UserDefaults for the next launch. The copy the app carries (LabelDefinition.all, as the web's LABEL_DEFINITIONS) is only the offline fallback: the cache holds it until the first answer arrives, and keeps it when the call fails. The report sheet lists the cache's definitions.

Why

grain-serve serves social.grain.* only, so hatk's names stop working at cutover. Each replacement follows grain-serve's lexicons and handlers in crates/grain-serve/src/ on feature/rust. Where grain-serve refuses a service-token caller (deleteGallery) or works by a different contract (deleteAccount), the app does what the README says the apps do: it deletes at its own PDS first.

How it was checked

Run on the iPhone 17 Pro Max simulator, one run at a time: the test classes this changes, plus every class that reads the fixtures it changes. That is EndpointContractTests, GalleryServiceTests, GalleryDeleteTests, GalleryPublishTests, NotificationsViewModelTests, NotificationsListRenderTests, FeedPreferencesViewModelTests, LabelDefinitionsCacheTests, LabelResolutionTests, NetworkSideEffectTests, XRPCClientTests, PDSCallTests, StoryViewSyncTests and ProfileDetailViewModelTests, and 22 render and routing classes over Fixtures.routes: 447 tests, all passing. The whole target builds. The full suite was not run, and the repo has no CI.

New tests cover the feed URI, the typed preference round trip (other kinds and the theme kept), the report's reasonType and proxy, getRecord's parameters, the gallery delete (what goes and what stays, one commit, indexed), the account delete (grain collections only, then deleteAccount, nothing indexed), the unread count, updateSeen and the push device. ProfileSeedTests covers the profile seed: a named profile is left alone, no Bluesky profile means no write, create at self versus put with swapRecord, createdAt kept, the avatar passed as is, and a failed write staying inside the background run. ProfileSeedTests, OAuthSignInTests, AuthManagerAccountTests and EndpointContractTests were run again on the simulator after it (86 tests, passing). LabelDefinitionsCacheTests covers the label switch: it starts on the bundled copy, reads describeLabels once, keeps the result for the next launch, and on a failure keeps the copy and asks again. After that change, LabelDefinitionsCacheTests, LabelResolutionTests, EndpointContractTests, ProfileSeedTests, ListViewRenderTests (the report sheet) and 8 render classes were run again (194 tests, passing).

Not tried against a running grain-serve.

Shortcomings

Gaps, with no grain-serve method for a service-token caller:

  • social.grain.gallery.deleteGallery refuses a service token, so the app works out at the PDS what goes with a gallery. That means listing its gallery items, exif, favorites and comments, which takes many pages for a large account. If the PDS's records differ from what grain-serve indexed, the two can disagree about what goes.

Also:

  • A preference change is read-modify-write, with no compare-and-swap, so two devices changing different kinds at once can lose one change.
  • docs/plans/2026-03-29-grain-native-ios-design.md still lists hatk's methods. It is a dated design record and is left as written.

Review

Start with Grain/API/Endpoints/RecordEndpoints.swift (the two deletes) and FeedEndpoints.swift (the preference coding). Stacked on the sign-in pull, at://did:plc:bcgltzqazw5tb6k2g3ttenbj/dev.knotless.pull/3mzbahapdzh2h.

🤖 Generated with Claude Code

Approved

This moves every call to grain-serve's social.grain.* names. It adds the typed preferences read-modify-write, the PDS-side gallery and account deletes, the profile seed, the report proxy and reasonType, and the label-definitions cache. I found nothing that must change.

  • Preferences: UserPreferences decodes and encodes the typed array, keeps theme, and drops kinds the app doesn't know. That matches the body's claim that only the four kinds grain-serve takes are sent. The lost-update race across devices is already listed under Shortcomings.
  • Deletes: deleteGallery removes children first, spares photos that another gallery still uses, and goes in applyWrites commits of at most 200. deleteAccount removes only social.grain.* collections, doesn't index, and then calls deleteAccount. Pagination stops when a page is empty or has no cursor.
  • Profile seed: it runs detached after sign-in and logs failures without throwing. It puts with swapRecord when a profile exists and creates at self otherwise.
  • Report: XRPCClient.proxy(for:) special-cases createReport, so it carries the grain atproto-proxy header even though it is a com.atproto.* method.
  • Report sheet: ReportView reads LabelDefinitionsCache from the environment, and GrainApp provides it.

I couldn't check the mirrored GOING_SQL and reasonType table against grain-serve's source, and the body says the changes weren't tried against a running grain-serve.

I read the diffs of RecordEndpoints, FeedEndpoints, ProfileSeed, XRPCClient, ModerationEndpoints, PushManager, AuthManager, NotificationEndpoints, GalleryService, LabelDefinitionsCache, NotificationsViewModel and ReportView. I did not read the remaining endpoint renames, the call-site edits in views and view models, the tests, DEVELOPMENT.md, justfile or project.yml. They are mostly mechanical renames and test updates, and the body says the tests were run.

chadtmiller.com added 1 commit
Approved

Carried forward: the change this pull makes at a26a1206582bb507d06787019e55bac32113d234 is the one reviewed at e1e077c8e66b02a78411a493e69b1593385d884a, so that review stands. What is new comes from its base.

Merged as ca67f6a