1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# 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`