commits
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a Delete Account row at the bottom of Settings, Account that calls
the new social.grain.unspecced.deleteAccount xrpc, signs the user out,
and dismisses the settings sheet. Uses a destructive alert so the user
can't trip the action accidentally. Error surface in the section footer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
iPad support required a 13" iPad screenshot for App Store submission,
which we don't have a design polished for v1. Drop iPad from the device
family so App Store Connect stops requiring the iPad screenshot.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reads the new locationDisplay field from gallery and story views so the
UI renders one pre-formatted string instead of piecing together name and
address fields client-side. Falls back to location.name when an older
server hasn't shipped the field yet.
Also fixes two legacy issues that left county embedded in stored records:
- LocationServices.reverseGeocode no longer appends "county" to the
fallback name when Nominatim omits a POI name, matching the web's
geocoder output. New records won't have county in location.name.
- BlueskyPost.buildPostText handles legacy community.lexicon.location.
hthree records (no structured address) by using location.name as-is
instead of stripping after the first comma.
Adds NominatimResultTests covering the county exclusion and extends
BlueskyPostTests with legacy-record, county-embedded, and state-abbrev
cases.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When Nominatim returned a formatted fallback name ("New York, New York,
United States"), the old logic appended region and country on top,
producing "New York, New York, United States, New York, US". Now we take
the first comma-separated chunk of the name as the primary label and
append locality/region/country while skipping case-insensitive adjacent
duplicates — preserving POI context ("Blue Bottle Coffee, Oakland,
California, US") while collapsing city-fallback redundancy
("New York, US").
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: stop tracking generated Info.plist, clean .gitignore
Info.plist is emitted by XcodeGen from project.yml on every just
generate — tracking it caused silent drift when edits were made
directly to Info.plist instead of project.yml. Also removes a
leftover merge conflict marker around .zed/sim.
* feat: add Create Story app intent
Adds a CreateStoryIntent alongside the existing CreateGalleryIntent
and reorders the AppShortcuts list so the two create actions surface
first in Spotlight and the Shortcuts app. FeedView's showStoryCreate
is lifted to a Binding so MainTabView can toggle it in response to
the intent.
* chore: refresh App Shortcuts index on every launch
Calls GrainShortcuts.updateAppShortcutParameters() from GrainApp so
iOS re-reads the AppShortcutsProvider regardless of auth state.
Without this, order/intent changes stay stale until the app is
deleted and reinstalled. Runs at .background priority so it doesn't
compete with launch-critical work.
* feat: add home-screen quick actions for Create Story and Create Post
Declares two static UIApplicationShortcutItems in project.yml so they
appear on long-press of the app icon. GrainSceneDelegate handles both
cold-launch (scene:willConnectTo:options:) and warm activation
(windowScene:performActionFor:) by posting into the existing
grainShortcutAction notification pipeline, reusing the same dispatch
that Siri/Spotlight intents already use.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add Int.compactCount extension (1K, 1.2M) and apply to gallery fav
counts, comment counts, and comment fav counts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add heart button to CommentRow for favoriting comments with optimistic
updates. Add comment-favorite notification reason with proper icon,
text, grouping, and navigation to parent gallery/story.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Glass post button and iMessage-style comment input
- Solid pink-red heart color across gallery cards, stories, notifications
- Replace custom image cache with NukeUI LazyImage for reliable loading
- Avatar border rings removed, notification avatar sizes matched
- Story + icon: white on indigo accent
- Profile buttons: glass style with rounded rectangle shape
- Context menu on logged-in user's story avatar
- Collapsible exif section when no metadata present
- Scrollable alt text overlay for panoramic images
- Location search disambiguation with Nominatim display_name
- AccentColor asset catalog properly wired
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wraps alt text in a ScrollView that centers short text and scrolls long
text. Tapping the overlay background dismisses it.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Animate height to zero when swiping to a photo without camera data
- Only render settings row (aperture, ISO, etc.) when at least one value exists
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add .frame(maxWidth: .infinity) to cached and lazy-loaded thumbnail
images in StoryViewer to match the fullsize image layout. Without this,
thumbnails could render at their intrinsic size leaving black gaps.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add .accessibilityLabel to icon-only buttons (like, comment, share,
close, delete, more options, pin/unpin, send, etc.) across 15 view
files. Mark decorative icons as .accessibilityHidden (avatar fallbacks,
particle animations, reply indicators). Add .accessibilityElement to
story strip bubbles. Fix "Your story" bubble alignment with other
author bubbles by adding matching padding. Nudge "+" badge position
on story avatar.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The .task block only called loadInitial when notifications were empty,
so returning to the tab with new notifications showed stale data.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When profile.viewer was nil (never-interacted-with users), the optional
chain silently skipped the optimistic UI update. Now creates a fresh
ActorViewerState before setting the pending follow.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Opens all links on the login screen (atmosphere account, legal links) in
an in-app Safari sheet instead of leaving the app. Standardizes heading
to "Sign in" for consistency with button and legal text.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Make delete gallery and block button red (icon + text)
- Make mute button non-destructive
- Remove double-tap-to-fav on stories
- Fix story ring not clearing after deleting last story
- Re-fetch story strip on viewer dismiss
- Filter out authors with 0 stories from strip and cache
- Rename settings: Appearance → Feeds, Upload Defaults → Privacy
- Add "Defaults for new uploads" section header
- Add long-press to copy handle/DID on account settings
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix heart icon carrying over filled state when swiping between stories
- Open comment sheet to medium detent only (not full screen)
- Improve reply banner visibility with larger text and proper hit area
- Swipe up on story to open comment sheet
- Remove tap-to-navigate on gallery title
- Resume timer when swiping right on first story
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: anchor upload arrow scale transition to trailing edge
Center-anchored .scale combined with HStack layout shift caused the
send button to appear from the diagonal lower-right. Anchoring to
.trailing keeps the animation on a straight axis.
* fix: send button slides in from right with opacity and offset transition
Use asymmetric offset+opacity transition with clipped HStack so the
button translates in from off-screen while the text field shrinks,
producing one fluid animation instead of a diagonal pop-in.
The settings cleanup PR removed the shared toast component. Move it
into ProfileView where it's actually used.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Redesign settings with cleaner layout and subpages
Remove icons and section labels for a minimal settings design. Add
Account, Appearance, and Upload Defaults subpages. Legal links now open
in-app via SFSafariViewController. Profile grid thumbnails use sync
cache reads to prevent flash. Add "Manage your data" link to Account
page opening pdsls.dev with user's DID.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix notifications UI and thumbnail rendering
Replace text-only marquee with actual app logo images from the
atmosphere ecosystem. Logos are rendered via Canvas for performance.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Replace ~140-line UIKit UIViewRepresentable with a simple HStack using
negative spacing and the existing AvatarView component.
Use systemGray4/systemGray2 instead of transparent gray so fallback
avatars don't bleed the background. Add fallback to UIKit
OverlappingAvatarsUIView (notifications grouped rows). DRY LoginView
avatar via AvatarView. Nil two preview profiles to exercise fallback.
CachedThumbnailView now uses a Nuke resize processor so the decoded
cache stores 44pt bitmaps instead of full-res images. Prefetcher
uses matching resized requests. Preview thumb assets simulate the
bsky avatar CDN preset (1000×1000 center-crop).
The thumb CDN image is high enough resolution to serve as a clean
placeholder — the blur just added visual noise and caused a
clear→blurred→sharp flash on first render.
Change inline timestamp from .tertiary to .secondary so it remains
readable in both light and dark mode.
Extract Date overload for relativeTime, remove duplicate in
StoryViewer, and replace hardcoded preview notification dates
with dynamic offsets so previews always show relative timestamps.
Every user re-auths on next launch — scope migration forces logout for tokens missing the new story / bsky-post scopes
Cold-start is measurably faster — MainTabView startup fetches parallelized via async let, Nuke DataCache init moved off the main thread, AuthManager @Observable setters no longer cascade a full GrainApp.body rebuild on every token refresh
API connection preheat shaves ~150–200ms off cold-network TFP — fire-and-forget /_health GET from GrainApp.init() warms DNS/TCP/TLS in parallel with the UIKit launch window so the first real XRPC calls hit an already-open connection pool
Feed and favorites render instantly on re-open — new FeedCache synchronous JSON-on-disk cache pre-populates the first page before first SwiftUI body eval
Story viewer visual bugs fixed — fullsize cache lookup memoized (fixes clear→blurred→sharp flash on first render), blur+spinner placeholder dropped, StoryStatusCache now expires entries 24h after latestAt
Fullsize images never disk-cached — thumbs and avatars own the 150 MB Nuke budget; fullsize is memory-only. Same 150 MB cache now holds ~1,150 images instead of ~75 (~130 KB thumb/avatar vs ~2 MB fullsize) — roughly ~15× capacity worst-case. 1000px thumbnails are sharp enough for feed/grid/story display; fullsize only matters for pinch-zoom, which still hits the memory cache within-session
Profile overhaul — new ProfileGalleryFeedView with galleries/favorites source routing, avatar overlay rewritten on ZoomableImage with pinch+drag-to-dismiss, unified profileContextMenu(...) across FollowList/StoryStrip/Search
Swipe-back anywhere — rightward swipe outside the carousel dismisses ProfileGallery, Hashtag, Location, and Camera feed views
Cold-start fully instrumented — OSSignposter intervals on every sync and async launch phase, visible in Instruments
Replace fragile scrollPosition/viewAligned approach with ScrollViewReader
+ scrollTo for bulletproof initial gallery positioning. Hide content until
scroll lands to eliminate transition flash. Add sync Nuke cache reads for
profile grid thumbnails to prevent grey flash on back navigation. Clip
horizontal tab pages to prevent tap bleed between galleries/favorites grids.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fires a fire-and-forget GET to /_health from a detached task in
GrainApp.init(), so DNS + TCP + TLS handshake runs in parallel with
the ~580ms UIKit launch window. By the time the real XRPC calls
fire, the connection pool to grain.social is already warm.
Measured impact on cold-network cold launch: ~150-200ms off TFP.
The ConnectionPreheat signpost interval shows how cold the handshake
was (500-650ms observed on cold runs, ~100ms when already warm).
A prior 'many changes' commit dropped showFeedsManagement, the Create
Gallery sheet, the deepLinkStory cover, and the comment/report/delete
modifiers from FeedView. Wires them all back so the My Feeds menu, +
button, comment sheet, report flow, and delete confirmation work again.
The onChange(of: auth.avatarImage) handler at MainTabView already
assigns avatarTabImage whenever the avatar transitions to non-nil,
making the post-avatarFetch assignment block redundant in the common
case where the avatar was nil at launch.
ProfileViewModelInit, SearchViewModelInit, ProfileViewBodyBegin, and
SearchViewBodyBegin were launch-path instrumentation that outlived their
measurement window. Removing the signposters keeps their view init paths
focused on actual setup.
Adds a minimal ImagePipelineDelegate whose willCache hook emits a
DiskCacheWrite os_signpost event with the image's last path component
and byte count. Verifies the fullsize-never-cached discipline holds:
after the refactor every event should fall in the thumb/avatar byte
range (tens to a few hundred KB), never the fullsize range (1-5 MB).
Fullsizes were eating the 150 MB Nuke disk budget and crowding out the
thumbs that actually drive feed/grid snappiness. Sets
.disableDiskCacheWrites on every fullsize ImageRequest — ZoomableImage,
StoryViewer, and all three branches of ImagePrefetchPlanning (carousel,
feed, stories). Thumbs and avatars still disk-cache normally.
The in-memory cache is untouched, so within a session zoom/re-open still
hits memory. Cross-session re-opens of the same fullsize re-fetch from
network, which is fine — the user is looking at one specific image, not
scrolling past hundreds.
Adds a Delete Account row at the bottom of Settings, Account that calls
the new social.grain.unspecced.deleteAccount xrpc, signs the user out,
and dismisses the settings sheet. Uses a destructive alert so the user
can't trip the action accidentally. Error surface in the section footer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reads the new locationDisplay field from gallery and story views so the
UI renders one pre-formatted string instead of piecing together name and
address fields client-side. Falls back to location.name when an older
server hasn't shipped the field yet.
Also fixes two legacy issues that left county embedded in stored records:
- LocationServices.reverseGeocode no longer appends "county" to the
fallback name when Nominatim omits a POI name, matching the web's
geocoder output. New records won't have county in location.name.
- BlueskyPost.buildPostText handles legacy community.lexicon.location.
hthree records (no structured address) by using location.name as-is
instead of stripping after the first comma.
Adds NominatimResultTests covering the county exclusion and extends
BlueskyPostTests with legacy-record, county-embedded, and state-abbrev
cases.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When Nominatim returned a formatted fallback name ("New York, New York,
United States"), the old logic appended region and country on top,
producing "New York, New York, United States, New York, US". Now we take
the first comma-separated chunk of the name as the primary label and
append locality/region/country while skipping case-insensitive adjacent
duplicates — preserving POI context ("Blue Bottle Coffee, Oakland,
California, US") while collapsing city-fallback redundancy
("New York, US").
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: stop tracking generated Info.plist, clean .gitignore
Info.plist is emitted by XcodeGen from project.yml on every just
generate — tracking it caused silent drift when edits were made
directly to Info.plist instead of project.yml. Also removes a
leftover merge conflict marker around .zed/sim.
* feat: add Create Story app intent
Adds a CreateStoryIntent alongside the existing CreateGalleryIntent
and reorders the AppShortcuts list so the two create actions surface
first in Spotlight and the Shortcuts app. FeedView's showStoryCreate
is lifted to a Binding so MainTabView can toggle it in response to
the intent.
* chore: refresh App Shortcuts index on every launch
Calls GrainShortcuts.updateAppShortcutParameters() from GrainApp so
iOS re-reads the AppShortcutsProvider regardless of auth state.
Without this, order/intent changes stay stale until the app is
deleted and reinstalled. Runs at .background priority so it doesn't
compete with launch-critical work.
* feat: add home-screen quick actions for Create Story and Create Post
Declares two static UIApplicationShortcutItems in project.yml so they
appear on long-press of the app icon. GrainSceneDelegate handles both
cold-launch (scene:willConnectTo:options:) and warm activation
(windowScene:performActionFor:) by posting into the existing
grainShortcutAction notification pipeline, reusing the same dispatch
that Siri/Spotlight intents already use.
- Glass post button and iMessage-style comment input
- Solid pink-red heart color across gallery cards, stories, notifications
- Replace custom image cache with NukeUI LazyImage for reliable loading
- Avatar border rings removed, notification avatar sizes matched
- Story + icon: white on indigo accent
- Profile buttons: glass style with rounded rectangle shape
- Context menu on logged-in user's story avatar
- Collapsible exif section when no metadata present
- Scrollable alt text overlay for panoramic images
- Location search disambiguation with Nominatim display_name
- AccentColor asset catalog properly wired
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add .accessibilityLabel to icon-only buttons (like, comment, share,
close, delete, more options, pin/unpin, send, etc.) across 15 view
files. Mark decorative icons as .accessibilityHidden (avatar fallbacks,
particle animations, reply indicators). Add .accessibilityElement to
story strip bubbles. Fix "Your story" bubble alignment with other
author bubbles by adding matching padding. Nudge "+" badge position
on story avatar.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Make delete gallery and block button red (icon + text)
- Make mute button non-destructive
- Remove double-tap-to-fav on stories
- Fix story ring not clearing after deleting last story
- Re-fetch story strip on viewer dismiss
- Filter out authors with 0 stories from strip and cache
- Rename settings: Appearance → Feeds, Upload Defaults → Privacy
- Add "Defaults for new uploads" section header
- Add long-press to copy handle/DID on account settings
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix heart icon carrying over filled state when swiping between stories
- Open comment sheet to medium detent only (not full screen)
- Improve reply banner visibility with larger text and proper hit area
- Swipe up on story to open comment sheet
- Remove tap-to-navigate on gallery title
- Resume timer when swiping right on first story
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: anchor upload arrow scale transition to trailing edge
Center-anchored .scale combined with HStack layout shift caused the
send button to appear from the diagonal lower-right. Anchoring to
.trailing keeps the animation on a straight axis.
* fix: send button slides in from right with opacity and offset transition
Use asymmetric offset+opacity transition with clipped HStack so the
button translates in from off-screen while the text field shrinks,
producing one fluid animation instead of a diagonal pop-in.
Remove icons and section labels for a minimal settings design. Add
Account, Appearance, and Upload Defaults subpages. Legal links now open
in-app via SFSafariViewController. Profile grid thumbnails use sync
cache reads to prevent flash. Add "Manage your data" link to Account
page opening pdsls.dev with user's DID.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Every user re-auths on next launch — scope migration forces logout for tokens missing the new story / bsky-post scopes
Cold-start is measurably faster — MainTabView startup fetches parallelized via async let, Nuke DataCache init moved off the main thread, AuthManager @Observable setters no longer cascade a full GrainApp.body rebuild on every token refresh
API connection preheat shaves ~150–200ms off cold-network TFP — fire-and-forget /_health GET from GrainApp.init() warms DNS/TCP/TLS in parallel with the UIKit launch window so the first real XRPC calls hit an already-open connection pool
Feed and favorites render instantly on re-open — new FeedCache synchronous JSON-on-disk cache pre-populates the first page before first SwiftUI body eval
Story viewer visual bugs fixed — fullsize cache lookup memoized (fixes clear→blurred→sharp flash on first render), blur+spinner placeholder dropped, StoryStatusCache now expires entries 24h after latestAt
Fullsize images never disk-cached — thumbs and avatars own the 150 MB Nuke budget; fullsize is memory-only. Same 150 MB cache now holds ~1,150 images instead of ~75 (~130 KB thumb/avatar vs ~2 MB fullsize) — roughly ~15× capacity worst-case. 1000px thumbnails are sharp enough for feed/grid/story display; fullsize only matters for pinch-zoom, which still hits the memory cache within-session
Profile overhaul — new ProfileGalleryFeedView with galleries/favorites source routing, avatar overlay rewritten on ZoomableImage with pinch+drag-to-dismiss, unified profileContextMenu(...) across FollowList/StoryStrip/Search
Swipe-back anywhere — rightward swipe outside the carousel dismisses ProfileGallery, Hashtag, Location, and Camera feed views
Cold-start fully instrumented — OSSignposter intervals on every sync and async launch phase, visible in Instruments
Replace fragile scrollPosition/viewAligned approach with ScrollViewReader
+ scrollTo for bulletproof initial gallery positioning. Hide content until
scroll lands to eliminate transition flash. Add sync Nuke cache reads for
profile grid thumbnails to prevent grey flash on back navigation. Clip
horizontal tab pages to prevent tap bleed between galleries/favorites grids.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fires a fire-and-forget GET to /_health from a detached task in
GrainApp.init(), so DNS + TCP + TLS handshake runs in parallel with
the ~580ms UIKit launch window. By the time the real XRPC calls
fire, the connection pool to grain.social is already warm.
Measured impact on cold-network cold launch: ~150-200ms off TFP.
The ConnectionPreheat signpost interval shows how cold the handshake
was (500-650ms observed on cold runs, ~100ms when already warm).
Adds a minimal ImagePipelineDelegate whose willCache hook emits a
DiskCacheWrite os_signpost event with the image's last path component
and byte count. Verifies the fullsize-never-cached discipline holds:
after the refactor every event should fall in the thumb/avatar byte
range (tens to a few hundred KB), never the fullsize range (1-5 MB).
Fullsizes were eating the 150 MB Nuke disk budget and crowding out the
thumbs that actually drive feed/grid snappiness. Sets
.disableDiskCacheWrites on every fullsize ImageRequest — ZoomableImage,
StoryViewer, and all three branches of ImagePrefetchPlanning (carousel,
feed, stories). Thumbs and avatars still disk-cache normally.
The in-memory cache is untouched, so within a session zoom/re-open still
hits memory. Cross-session re-opens of the same fullsize re-fetch from
network, which is fine — the user is looking at one specific image, not
scrolling past hundreds.