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

grain.social/grain-ios

1 collaborator
0
1
main
90 lines · 3.8 KB Raw History

Stories Feature Design

Goal: Add Instagram-style ephemeral stories to the native iOS app, matching the web client's functionality for viewing, browsing, and creating stories.

Scope: Story strip on feed, story viewer, profile story indicator, story creation. Story archive is out of scope.


1. Story Strip (Feed)

Horizontal ScrollView at the top of FeedView, above gallery cards.

  • First item: "+" button (opens StoryCreateView sheet) for authenticated users
  • Remaining items: circular avatars with gradient ring (purple > accent > cyan) for users with active stories (24h window)
  • Display name label below each avatar
  • Tapping an avatar opens StoryViewer as a fullscreen cover, starting at that author
  • Data: getStoryAuthors() endpoint, fetched on load and pull-to-refresh

New files:

  • Grain/Views/Stories/StoryStripView.swift — horizontal avatar strip component
  • Grain/ViewModels/StoryStripViewModel.swift — fetches story authors

Shared component:

  • Grain/Views/Stories/StoryRingView.swift — gradient ring wrapper, reused on profile avatar

2. Story Viewer

Wire up the existing StoryViewer.swift and present it from feed and profile.

  • Presented as .fullScreenCover
  • Receives full authors list + starting index to enable swiping between authors
  • Each author's stories fetched on demand via getStories(actor:)
  • Auto-advance: 5-second timer per story, animated progress bars at top
  • Navigation: tap left 1/3 = previous, tap right 2/3 = next
  • Swipe left/right to jump between authors
  • Dismisses after last story of last author
  • Delete button on own stories via deleteRecord
  • Status bar hidden for immersive experience

Modified files:

  • Grain/Views/Stories/StoryViewer.swift — enhance with auto-advance timer, author list navigation
  • Grain/Views/Feed/FeedView.swift — add fullscreen cover presentation
  • Grain/Views/Profile/ProfileView.swift — add fullscreen cover on avatar tap

3. Profile Story Indicator

When a user has active stories, their profile avatar shows a gradient ring and becomes tappable to open the viewer.

  • ProfileDetailViewModel already fetches stories — use viewModel.stories.isEmpty to determine ring visibility
  • Wrap AvatarView with StoryRingView when stories exist
  • Tap avatar to open StoryViewer as fullscreen cover
  • No ring and no tap action when no stories

Modified files:

  • Grain/Views/Profile/ProfileView.swift — conditional ring + tap gesture on avatar

4. Story Creation

Sheet presented from the "+" button in the story strip. Reuses photo processing from CreateGalleryView.

  • Single photo selection via PhotosPicker
  • Photo preview after selection
  • Image resize: max 2000px, max 5MB with JPEG quality binary search
  • Location auto-populated from EXIF GPS via reverse geocode (Nominatim), converted to H3 index
  • Editable location text field
  • Optional "Post to Bluesky" toggle
  • Post: uploadBlob then createRecord with collection social.grain.story
  • Record fields: media (blob ref), aspectRatio, location (H3 value + name), address, createdAt
  • On success: refresh story strip, dismiss sheet

New files:

  • Grain/Views/Stories/StoryCreateView.swift

Reused from gallery creation:

  • Photo processing / resize utilities
  • EXIF GPS extraction
  • Reverse geocoding
  • H3 conversion (SwiftyH3)

Existing Infrastructure

Already built and ready to use:

  • Models: GrainStory, GrainStoryAuthor, StoryRecord (in Grain/Models/)
  • API endpoints: getStories, getStory, getStoryArchive, getStoryAuthors (in Grain/API/Endpoints/StoryEndpoints.swift)
  • Viewer component: StoryViewer.swift (needs enhancement but base exists)
  • OAuth scope: repo:social.grain.story already included
  • Photo processing: resize, EXIF extraction, blob upload all exist in CreateGalleryView