feat: photo editor grid, image prefetching, and story strip improvements (#4)
* feat: use grain typeahead API for login autocomplete
Switch from bsky's public searchActorsTypeahead to grain's merged
endpoint that includes both grain and bsky profiles, so users on
non-bsky PDSes show up in login suggestions.
* feat: reorganize just recipes with sim/sim-local/test commands
* chore: add VSCode extensions config
* test: add DeepLink and LabelResolution unit tests
Add 27 new tests covering DeepLink URL parsing (grain:// and https://
schemes, invalid URLs, galleryUri property) and LabelResolution logic
(fallback definitions, server overrides, severity ordering, worst-wins).
Also fix GrainTests target to generate Info.plist automatically.
* test: add DateFormatting, FacetFeature coding, and AnyCodable tests
- DateFormattingTests: parse with/without fractional seconds, invalid
strings, relativeTime for minutes/hours/days/weeks
- FacetCodingTests: decode mention/link/tag variants, unknown type
throws, round-trip encoding, full Facet with ByteSlice
- AnyCodableTests: init from each type, JSON decode/encode round-trips,
dictValue property, nested structures, null fallback
* test: add PhotoModels, FeedEndpoint model, and StoryStatusCache tests
- PhotoModelsTests: cameraName, lensName, settingsLine formatting,
hasDisplayableData with various EXIF field combinations
- FeedEndpointModelTests: PinnedFeed feedName/feedValue computed
properties, defaults array, colon-separated value extraction
- StoryStatusCacheTests: update/replace cache, hasStory lookups,
author retrieval, didsWithStories set
* test: add ViewModel tests with MockURLProtocol infrastructure
- MockURLProtocol: URLProtocol subclass for intercepting network
requests with configurable JSON/error responses
- NotificationsViewModelTests: loadInitial, markAsSeen optimistic
update + rollback, pagination
- ProfileDetailViewModelTests: follow/unfollow optimistic updates,
rollback on failure, follower count clamping, guard clauses
* feat: add xcbeautify, SwiftFormat, and SwiftLint tooling
Pipe all xcodebuild commands through xcbeautify for formatted output
with test pass/fail counts. Add format, format-fix, lint, and lint-fix
just recipes.
* chore: reset story view on every app load for local builds
* chore: format only staged files in pre-commit hook
* test uses prod api
* fix: pre-commit hook stages only formatted files, not all modified
* test: add ViewedStoryStorage tests and fix date test edge cases
Cover markViewed, isViewed, hasViewedAll, and firstUnviewedIndex
with 14 unit tests. Fix flaky date formatting tests that used
exact boundary values.
* feat: own story always shows unread (colored) ring
Force the story ring to display as unviewed for the current user
across all views: GalleryCardView, GalleryDetailView,
NotificationsView, FollowListView, ProfileView, and SearchView.
Pass userDID as a prop to NotificationRow and CommentRow since
they lack AuthManager in their environment.
* feat: story viewer unread mode, animations, and strip overhaul
Unread-only mode:
- Auto-detect from starting author's viewed state (colored ring = unread mode)
- Start at first unread story, navigate only to authors with unreads
- Own story excluded from navigation queue, only viewable via "Your story"
- Own story always starts at index 0 (all stories treated as unread)
- Mark stories as viewed at 25% progress instead of 50%
Story strip:
- "Your story" shows colored ring when user has stories, tap to view
- Long press "Your story" to create when stories exist
- Own story filtered from regular author list
- Deferred sort: re-sort only on viewer dismiss with animated reorder
- Wave lift effect radiates from gap where read card departed
- Read cards slide under unreads via zIndex layering
Animations:
- Drag-to-dismiss throws story downward with velocity-based duration
- Author transitions: directional shrink + slide + grow on all switches
- Strip invalidation triggers ring refresh without pull-to-refresh
Refactoring:
- Extract startTimerIfSafe, canNavigate, presentStories, findAuthorIndex
- Remove unused startIndex init parameter
- Simplify authorHasUnreads (own-DID check was dead code)
* feat: prefetch carousel images with Nuke ImagePrefetcher
Preloads fullsize URLs for the first two images on appear and
the next two ahead of the current page on swipe, cancelling
in-flight requests when the card disappears.
* Update .gitignore
* feat: add blur-up thumb placeholders for images
Show a blurred thumbnail as placeholder while fullsize loads,
replacing the gray rectangle. Applied to gallery carousel and
story viewer.
* feat: tiered carousel prefetch with priority levels
Replace simple next-image prefetch with engagement-gated strategy:
page 0 prefetches first 2 fullsize, page 1 adds thumbs, page 2+
fetches all remaining. Uses Nuke ImageRequest priority levels.
* feat: add feed-level image prefetch and prefetch unit tests
Prefetch first image of next 3 galleries as user scrolls the feed.
Trigger loadMore 5 items from end instead of at the last item.
Add 19 unit tests for carousel, feed, and story prefetch planning.
* feat: add story image prefetching with priority queue
Prefetch upcoming story images based on engagement depth:
next 2 current author stories at high priority, first of next
author at high, rest of stack at normal, and look-ahead authors
at low priority.
* feat: add image cache size and clear button in settings
Show current Nuke disk cache size in human-readable format
and provide a clear button that wipes both memory and disk cache.
* chore: don't track generated code, default build to production API
Remove project.pbxproj from git (managed by xcodegen). Update
justfile so just build uses PRODUCTION_API by default, add
separate build-local for dev API builds.
Save and close the editor to continue.
* test: add priority correctness, boundary, and dedup tests
Verify priority levels are set correctly per tier, story prefetch
at last index still gets next author, and no duplicate URLs.
* fix: set visible images to veryHigh priority over prefetch
LazyImage was loading at default .normal priority while prefetch
requests fired at .high, starving the currently visible image.
Now visible images use .veryHigh so they always load first.
* feat: pause story timer until image loads, tap location chip to copy
Timer no longer starts until the full-resolution image has loaded.
A spinner overlays the blurred thumbnail during loading. The label
reveal callback also gates on image load.
Location chip: tap copies location name to clipboard with a brief
checkmark animation. Resets instantly on story/author change via
.id(story.uri) to avoid a lingering reverse animation.
* chore: add privacy manifest and photo library permission
- Add PrivacyInfo.xcprivacy declaring UserDefaults API access and
collected data types (photos, location, user content, name)
- Add NSPhotoLibraryUsageDescription to Info.plist and project.yml
to cover photo picker usage in gallery/story creation
* feat: add search spinner and refactor legal text in login view
- Show ProgressView while searching for handle suggestions
- Extract legal markdown to static constant, render via LocalizedStringKey
* feat: move Edit Profile button to profile page, remove from Settings
Self-view profile now shows [Edit Profile] [Germ DM] matching the
layout of the follow/DM row shown for other users. Saving refreshes
the profile in place. Removed the redundant Settings entry per HIG
guidance on keeping infrequently-changed options in settings.
* chore: restore story viewer status bar and remove local networking flag
- Remove .statusBarHidden() from StoryViewer so system status bar
remains visible during story playback (HIG compliance)
- Remove NSAllowsLocalNetworking from Info.plist and project.yml
* feat: add Privacy section to Settings with location, camera data, and suggested users toggles
* feat: add use-photo-location button to story creation
* feat: show EXIF per photo inline with thumbnail, add per-upload camera data toggle
* fix: skip wave animation when story strip order is unchanged
* feat: show progress bars and header overlay while stories are loading
* feat: tap own avatar to create story, long-press as shortcut, show plus badge
* feat: add LocalZoomableViewer, ReorderablePhotoGrid, and shared drag-to-reorder modifier
* feat: add PhotoEditor section, upgrade strip with selection and lift physics
* refactor: reorganize create gallery form — UUID selection, merged gallery section, section reorder
* test: add PhotoEditor tests for grid target index calculation and selection stability
* fix: keep create form scrollable when interacting with photo viewer and strip
* feat: split EXIF into fields and render two-row layout with focal/aperture right-aligned
Add alt text count row to gallery section.
* feat: add alt text indicator on grid thumbnails, use simultaneousGesture for tap
* chore: make team ID configurable via env var, add dev setup docs
- Load APPLE_TEAM_ID from .env (via dotenv-load in justfile)
- Extract sim_sign and team_id variables in justfile to reduce repetition
- Update DEVELOPMENT_TEAM in project.yml to 54P9BCDR92 with Automatic signing
- Add .env.example with instructions for contributors
- Add docs/development.md with full setup and build reference
- Add Grain-Debug.entitlements (empty, required for debug signing)
- Gitignore .env
* chore: add LFS post-checkout, post-commit, post-merge hooks
* Update GalleryCardView.swift
* fix: story viewer swipe-back in reads mode, drop phantom h3ToCity reference
* feat: add App Shortcuts via App Intents for Siri/Spotlight
Exposes 5 shortcuts discoverable at install time: Open Feed,
Search, Open Notifications, Open My Profile, Create Gallery.
Routing uses NotificationCenter so openAppWhenRun timing is reliable.
* perf: skip network tasks in Xcode preview canvas
Adds isPreview guard to all initial-load .task blocks so previews
render instantly instead of hanging on network timeouts.
* fix: use static let for openAppWhenRun to silence concurrency warning
static var on a global is nonisolated mutable state; static let
satisfies the same AppIntent protocol requirement without the warning.
Also adds #Preview to AvatarView, ExpandableDescriptionView, RichTextView.
* feat: add #Preview to all Views
Covers all 35 view files — components, create flow, feed, gallery,
notifications, profile, search, settings, and stories. Screen-level
views show loading/empty state; component views use inline sample data.
* fix: move GalleryCardView preview gallery init to top-level helper
@Previewable can't handle a multi-line inline initializer; calling a
private function satisfies the macro's single-expression requirement.
* feat: photo editor drag z-index, weighted animations, strip auto-scroll
- Dragged thumbnail renders on top via .zIndex(1) in both grid and strip
- Reorder spring response 0.28→0.45, damping 0.65→0.7 for heavier feel
- ReorderablePhotoStrip wraps in ScrollViewReader; scrolls to selected
photo with .easeInOut(0.3) whenever selectedPhotoID changes
* fix: show ProgressView in GalleryDetailView and ProfileView previews
When isPreview guard skips loading, viewModel data is nil and isLoading
stays false — the content gate renders nothing. Changed to an else
fallback so the canvas shows a spinner instead of a white screen.
* fix: use overlay for dragged grid cell to guarantee top z-index
LazyVGrid doesn't reliably respect .zIndex between sibling cells.
Render dragged item as a grid overlay instead — positioned by
computing its natural cell origin (col/row * step) + drag offset.
The in-grid copy is hidden (opacity 0) to preserve layout space.
Also updates PhotoEditor preview to 9 colored rects with first
item selected via .onAppear.
* feat: smooth grid reorder via matchedGeometryEffect
Uses matchedGeometryEffect to track each cell's frame in a shared
namespace. When items.move() is called, SwiftUI interpolates frames
smoothly between old and new positions — same mechanism as
UICollectionView move animations. Bump displacement spring to
response:0.35 dampingFraction:0.88 for a clean glide.
* chore: update PhotoEditor preview — 15 photos, icon, dark mode
15 colored rect thumbnails with germ-logo as first item (falls back
to color if asset missing). Dark mode color scheme by default.
* fix: move alt text bubble to bottom-right, mirror icon; drop germ-logo from preview
* fix: inject LabelDefinitionsCache into CreateGalleryView preview
* feat: full-width photo strip and grid in gallery create form
Remove list row insets from the photo strip and grid rows so both span
edge-to-edge. Strip content gets 16pt horizontal padding so thumbnails
don't start flush against the screen edge. Also consolidates preview
helpers to use PreviewData and fixes a MainActor isolation error in the
CreateGalleryView preview.
* feat: rich preview content across all major views
- Add PreviewData.swift with shared fake profiles, galleries, photos,
comments, and gradient UIImage generation
- ProfileView: inject mock profile + galleries when isPreview
- GalleryDetailView: inject mock gallery + comments when isPreview
- GalleryCardView: show two gallery cards with real fake metadata
- CreateGalleryView: preview wrapper shows form + PhotoEditor with 15
gradient photo items pre-populated
- PhotoEditor/Grid/Strip: use PreviewData.photoItems (gradient thumbs
with some alt text) instead of solid-color rects; dark mode default
* feat: enrich story strip, notifications, and story viewer previews
- PreviewData: add storyAuthors (5 users) and notifications (5 items
covering favorite, comment, follow, and mention reasons)
- StoryStripView: show 5 story authors instead of 1, with own userDid
- NotificationsView: pre-populate ViewModel with mock notifications
and unseenCount=3 so the badge and list are both visible
- StoryViewer: use PreviewData.storyAuthors instead of bare placeholder
* chore: configure DEVELOPMENT_ASSET_PATHS for Preview Content folder
* feat: load stock photos into PreviewData with file:// URLs
* feat: inject mock galleries into Feed/Hashtag/Location view previews
* chore: add stock photos to Preview Content (development assets only)
* chore: remove unassigned legacy app icon files
Delete 57.png and 114.png from AppIcon.appiconset — these pre-iOS 7 icon sizes have no slot in Contents.json and caused an Xcode "unassigned children" warning.
* feat: smooth story transitions with native SwiftUI slide animation
Use .id(story.uri) on the image ZStack and .transition(.asymmetric) to let SwiftUI animate story swaps as proper view insertions/removals. A spring(response: 0.28, dampingFraction: 0.88) drives the transition, with direction tracked via nextStoryFromTrailing so forward/back taps slide in from the correct edge.
* chore: remove deprecated 76x76@1x iPad app icon slot
* fix: restore includeExif state vars dropped during rebase
The Edit Profile removal commit was authored against an older branch
state without the EXIF preference feature, so its rebase auto-merged
a removal of @State includeExif and hasLoadedExifPref that left the
Photos section in SettingsView and the .task block in CreateGalleryView
referencing undeclared vars. Restore the declarations so both files
compile against origin/main's EXIF feature.
* fix: mark UITabBar badge appearance setup as @MainActor
Swift 6 strict concurrency flags UITabBarItemAppearance.normal/selected
as main-actor isolated, so mutating their badge properties from the
nonisolated static let initializer closure was an error. Annotating
the static let @MainActor is safe because it is only read from
MainTabView.body, which is already isolated to the main actor.
* Update MainTabView.swift
---------
Co-authored-by: Chad Miller <chadtmiller15@gmail.com>
authored by
This is a binary file and will not be displayed.
This is a binary file and will not be displayed.
This is a binary file and will not be displayed.