mobile bluesky app made with flutter lazurite.stormlightlabs.org/
mobile bluesky flutter
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

docs: gallery & typeahead

+777 -36
+295
docs/specs/gallery.md
··· 1 + --- 2 + title: Gallery Viewing Spec 3 + updated: 2026-04-25 4 + --- 5 + 6 + ## Summary 7 + 8 + Add two immersive viewing modes for feeds containing media: 9 + 10 + 1. **Slideshow** - full-screen image carousel for image-heavy feeds 11 + 2. **Short-form vertical** - TikTok-style vertical swipe for video or mixed feeds 12 + 13 + These modes activate from any feed context (timeline, profile, search, list feed, 14 + hashtag, saved posts) when the feed's content qualifies. 15 + 16 + ## Content Classification 17 + 18 + Before entering gallery mode, classify the filtered feed to determine which viewer 19 + to offer. Only posts with image or video embeds participate. 20 + 21 + | Embed type | Classification | 22 + | ----------------------------------- | -------------- | 23 + | `embedImagesView` | image | 24 + | `embedVideoView` | video | 25 + | `embedRecordWithMediaView` (images) | image | 26 + | `embedRecordWithMediaView` (video) | video | 27 + | `embedExternalView`, text-only | excluded | 28 + 29 + A `GalleryMediaItem` model normalises these into a uniform structure: 30 + 31 + ```dart 32 + class GalleryMediaItem { 33 + final FeedViewPost feedViewPost; 34 + final GalleryMediaType type; // image, video 35 + final List<ImageViewerItem> images; // populated for image type 36 + final VideoPlayerRouteArgs? video; // populated for video type 37 + } 38 + ``` 39 + 40 + ### Feed filtering 41 + 42 + Extract media items from a `List<FeedViewPost>` via a utility: 43 + 44 + ```dart 45 + List<GalleryMediaItem> extractGalleryItems(List<FeedViewPost> posts); 46 + ``` 47 + 48 + This walks each post's embed (and `recordWithMedia.media`), skipping posts without 49 + qualifying embeds. Moderation filtering applies - `contentMedia` blur/filter 50 + decisions are respected. 51 + 52 + ## Slideshow Mode (Images) 53 + 54 + ### Entry point 55 + 56 + A "Gallery" icon button in the feed app bar (or FAB) that appears when 57 + `extractGalleryItems` returns at least one item. Tapping it opens 58 + `GalleryScreen` with the extracted items, starting at the first item (or the 59 + item nearest the current scroll position). 60 + 61 + ### Screen: `GalleryScreen` 62 + 63 + Full-screen `PageView` with `PageController`. Each page renders based on 64 + `GalleryMediaType`: 65 + 66 + **Image pages:** `PhotoViewGallery` (reuse existing `photo_view` dependency) 67 + showing all images from the post. Multi-image posts use a nested horizontal 68 + `PageView` within the vertical page, with a dot indicator. Pinch-to-zoom and 69 + pan via `PhotoView`. Hero animation from the feed thumbnail when entering from 70 + a specific post. 71 + 72 + **Video pages:** Inline `VideoPlayerController` + `Chewie` (reuse existing 73 + dependencies). Auto-play when the page is visible, pause when swiped away. 74 + GIF-style videos (`presentation: "gif"`) loop muted with no controls. 75 + 76 + ### Navigation 77 + 78 + - **Vertical swipe** (default): swipe up/down to move between posts - TikTok 79 + style. Uses `PageView` with `scrollDirection: Axis.vertical`. 80 + - **Horizontal swipe** (slideshow): swipe left/right. Configurable via a toggle 81 + in the gallery toolbar. 82 + - Swipe-to-dismiss: vertical drag past threshold when in horizontal mode (or 83 + horizontal drag in vertical mode) pops the screen. 84 + 85 + ### Chrome overlay 86 + 87 + Semi-transparent top and bottom bars, auto-hiding after 3 seconds of 88 + inactivity. Tap anywhere to toggle. 89 + 90 + **Top bar:** 91 + 92 + - Close button (X) 93 + - Post author avatar + handle (tap → navigate to profile) 94 + - Page counter (e.g. "3 / 12") 95 + 96 + **Bottom bar:** 97 + 98 + - Post text snippet (first 2 lines, tap to expand) 99 + - Like / repost / reply action row (reuse `PostActionBar` pattern via 100 + `PostActionCubit`) 101 + - Download / share buttons (reuse `MediaActions`) 102 + - Alt text badge when present (tap to show full alt text in a sheet) 103 + 104 + ### State management 105 + 106 + `GalleryCubit` manages: 107 + 108 + ```dart 109 + class GalleryState { 110 + final List<GalleryMediaItem> items; 111 + final int currentIndex; 112 + final bool chromeVisible; 113 + final Axis scrollDirection; 114 + } 115 + ``` 116 + 117 + Events: `PageChanged`, `ChromeToggled`, `DirectionToggled`. 118 + 119 + The cubit receives the pre-filtered list of `GalleryMediaItem` - no additional 120 + API calls. Pagination piggybacks on the parent feed's `FeedBloc` cursor: when 121 + the user reaches the last few pages, the cubit signals the parent to load more, 122 + and new items are appended. 123 + 124 + ### Preloading 125 + 126 + Preload adjacent pages to reduce perceived latency: 127 + 128 + - Images: `precacheImage` for ±1 pages 129 + - Videos: initialise `VideoPlayerController` for +1 page, dispose -2 pages 130 + 131 + ## Short-Form Vertical Video Mode 132 + 133 + For feeds that are primarily video, the gallery defaults to vertical scroll 134 + direction with video-first UX: 135 + 136 + - Full-bleed video fills the screen (respect `aspectRatio` from embed, pillarbox 137 + for non-9:16 content) 138 + - Auto-play on visibility, auto-pause on swipe-away 139 + - Tap to pause/resume (no explicit controls unless user taps) 140 + - Double-tap right side → like, double-tap left side → rewind 5s 141 + - Long-press → playback speed options (1x, 1.5x, 2x) 142 + - Progress bar at bottom (thin, YouTube Shorts style) 143 + 144 + ### Video lifecycle 145 + 146 + Use `VisibilityDetector` pattern (or `PageView.onPageChanged`) to: 147 + 148 + 1. Pause video leaving viewport 149 + 2. Play video entering viewport 150 + 3. Dispose controllers for pages > 2 positions away 151 + 4. Pre-initialise controller for the next page 152 + 153 + ## Mixed feeds 154 + 155 + When a feed has both images and videos, gallery mode interleaves them in feed 156 + order. Each page adapts its renderer based on `GalleryMediaType`. The bottom 157 + bar shows a media type icon (camera/video) so users know what's coming next. 158 + 159 + ## Entry points 160 + 161 + | Context | Trigger | 162 + | ---------------------- | ------------------------------------------------- | 163 + | Home feed tabs | Gallery icon in `LazuriteAppBar` actions | 164 + | Profile feed tab | Gallery icon in profile feed section | 165 + | Search results (Posts) | Gallery icon in search app bar when results shown | 166 + | Hashtag screen | Gallery icon in hashtag app bar | 167 + | List feed | Gallery icon in list detail feed tab | 168 + | Saved posts | Gallery icon in saved posts app bar | 169 + | Post thread | Tap post media opens gallery seeded at that post | 170 + 171 + For thread context, tapping a post's media now opens gallery mode seeded with 172 + just that thread's media items, starting at the tapped item. 173 + 174 + ## Routing 175 + 176 + ```dart 177 + GoRoute( 178 + path: '/gallery', 179 + parentNavigatorKey: _rootNavigatorKey, 180 + builder: (context, state) { 181 + final args = state.extra as GalleryRouteArgs; 182 + return GalleryScreen(args: args); 183 + }, 184 + ), 185 + ``` 186 + 187 + `GalleryRouteArgs`: 188 + 189 + ```dart 190 + class GalleryRouteArgs { 191 + final List<GalleryMediaItem> items; 192 + final int initialIndex; 193 + final Axis initialDirection; 194 + } 195 + ``` 196 + 197 + ## Packages 198 + 199 + No new dependencies. Reuses: 200 + 201 + - `photo_view` - image zoom/pan 202 + - `video_player` + `chewie` - video playback 203 + - `dio` + `gal` - download 204 + - `share_plus` - sharing 205 + - `permission_handler` - gallery save permissions 206 + 207 + ## Moderation 208 + 209 + All gallery items pass through the existing `ModerationService` pipeline. 210 + `contentMedia` blur overlays render atop the gallery page. `noOverride` blurs 211 + cannot be dismissed. Posts filtered at `contentList` level are excluded from the 212 + gallery item list entirely. 213 + 214 + ## Infinite Scroll 215 + 216 + Gallery mode supports continuous scrolling beyond the initially loaded posts. 217 + When the user reaches the last 3 pages, `GalleryCubit` signals the parent 218 + feed's bloc (or cubit) to load the next page via its existing cursor-based 219 + pagination. New posts are filtered through `extractGalleryItems` and appended 220 + to the gallery's item list. A loading shimmer renders on the final page while 221 + the fetch is in progress. When the cursor is exhausted, the gallery shows an 222 + "end of feed" indicator on the last page. 223 + 224 + The gallery receives a callback (`onLoadMore`) and a stream/listener for new 225 + posts from the parent. This keeps the gallery decoupled from specific feed 226 + sources - it works identically whether backed by `FeedBloc`, `HashtagCubit`, 227 + `ListFeedBloc`, or `SearchBloc`. 228 + 229 + ## DM & Notification Media 230 + 231 + ### Notifications 232 + 233 + Notifications that reference posts with media (likes, reposts, quotes, replies 234 + on media posts) are gallery-eligible. `NotificationBloc` already hydrates 235 + referenced posts. `extractGalleryItems` can process the `subjectPost` or 236 + `reasonSubject` from notification views to extract media. 237 + 238 + Add a gallery entry point in the notifications screen when loaded notifications 239 + contain media-bearing posts. The gallery is seeded with media items extracted 240 + from notification subjects. 241 + 242 + ### DMs 243 + 244 + The `chat.bsky.convo` lexicon currently supports text-only messages - no image 245 + or video embeds. Gallery mode for DMs is not applicable until the AT Protocol 246 + adds media message support. When/if `chat.bsky.convo.defs#messageView` gains 247 + an `embed` field, the same `extractGalleryItems` pattern applies by adapting the 248 + extractor to accept message views alongside feed views. 249 + 250 + ## Offline Gallery 251 + 252 + Gallery mode works offline using cached feed data from the Drift database. 253 + 254 + **Sources:** 255 + 256 + | Cache source | Table | Contains embeds? | 257 + |--------------------|--------------------|------------------| 258 + | Feed first pages | `CachedFeedPages` | Yes (full JSON) | 259 + | Saved posts | `SavedPosts` | Yes (`postJson`) | 260 + | Liked posts | `LikedPosts` | Yes (`postJson`) | 261 + 262 + When the `ConnectivityCubit` reports offline, the gallery entry point still 263 + appears if cached data contains media items. `extractGalleryItems` processes 264 + deserialised `FeedViewPost` objects from cached JSON the same way it handles 265 + live data. 266 + 267 + Offline gallery disables actions requiring network (like, repost, reply) - these 268 + buttons show a disabled state with a tooltip ("Offline"). Download/share actions 269 + are disabled for images/videos not already in the device cache. 270 + 271 + Pagination is unavailable offline - the gallery shows only what's cached, with 272 + an "Offline - showing cached posts" indicator when the user reaches the end. 273 + 274 + ## Media-Only Feed Filtering 275 + 276 + No server-side "media-only" feed filter exists in the AT Protocol. Gallery mode 277 + filters client-side, which means the ratio of media-to-text posts in the 278 + underlying feed affects gallery density. To mitigate: 279 + 280 + - **Aggressive prefetch:** When gallery mode is active, the parent feed fetches 281 + with a higher `limit` (100 vs the default 50) to increase the pool of 282 + candidate posts. 283 + - **Skip ratio indicator:** The chrome overlay shows the media density 284 + (e.g. "12 media posts from 47 loaded") so users understand the scope. 285 + - **Feed generator hint:** For profile feeds, use `filter: postsWithMedia` 286 + (`FeedFilter.postsWithMedia`) which is supported by `getAuthorFeed` - this 287 + returns only posts containing images or video, making gallery mode fully 288 + dense for profile contexts. 289 + 290 + ## Accessibility 291 + 292 + - Gallery pages announce post author + media type via `Semantics` 293 + - Alt text is exposed to screen readers for every image/video 294 + - Chrome overlay elements are focusable and labeled 295 + - Physical keyboard: arrow keys navigate pages, Escape dismisses
+14 -14
docs/specs/testing.md
··· 15 15 | ----------------------- | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- | 16 16 | `_initials(String)` | Generates initials from display name | 10 files (post_card, grid_post_card, post_embed_view, notification items, suggested_follows, labeler_detail, moderation_settings, search, hashtag) | 17 17 | `_formatCount(int)` | Formats numbers with K/M suffixes | 5 files (post_action_bar, post_card_footer, starter_pack_card, starter_pack_detail, profile_screen) | 18 - | `_formatTime(DateTime)` | Relative time strings ("2h ago") | 4 files (notification items, search, hashtag) — overlaps with `formatPostTime()` in post_card_footer | 18 + | `_formatTime(DateTime)` | Relative time strings ("2h ago") | 4 files (notification items, search, hashtag) - overlaps with `formatPostTime()` in post_card_footer | 19 19 20 20 **Target**: Extract to `lib/shared/utils/format_utils.dart`. 21 21 22 22 ### 2. Duplicated Widgets 23 23 24 - **Avatar display** — 14 files repeat `CircleAvatar` / `ModeratedAvatar` with 24 + **Avatar display** - 14 files repeat `CircleAvatar` / `ModeratedAvatar` with 25 25 identical styling. Extract to a configurable `ProfileAvatar` widget. 26 26 27 - **Author name + handle** — Repeated two-line text widget (displayName / @handle) 27 + **Author name + handle** - Repeated two-line text widget (displayName / @handle) 28 28 in post_card, grid_post_card, post_embed_view, convo_list_item. Extract to 29 29 `ActorNameWidget`. 30 30 31 - **Greyscale color filter** — Identical 4x5 color matrix defined in 31 + **Greyscale color filter** - Identical 4x5 color matrix defined in 32 32 grid_post_card and profile_screen. Extract to `lib/core/theme/color_filters.dart`. 33 33 34 - **Notification reason icon** — Large switch statement mapping notification 34 + **Notification reason icon** - Large switch statement mapping notification 35 35 reasons to icons/colors duplicated identically in notification_list_item and 36 36 grouped_notification_list_item. Extract to `NotificationIconMapper`. 37 37 38 38 ### 3. Bottom Sheets & Dialogs 39 39 40 - **Modal bottom sheets** — 9 files repeat `showModalBottomSheet` with ListTile 40 + **Modal bottom sheets** - 9 files repeat `showModalBottomSheet` with ListTile 41 41 option lists. Create `OptionsSheet` builder. 42 42 43 - **Confirmation dialogs** — 26 occurrences of AlertDialog with 43 + **Confirmation dialogs** - 26 occurrences of AlertDialog with 44 44 title/content/cancel/confirm. Create `ConfirmationDialog(title, content, 45 45 confirmLabel, onConfirm)`. 46 46 47 47 ### 4. State Handling Patterns 48 48 49 - **Loading** — `Center(child: CircularProgressIndicator())` in 8+ screens. 50 - **Error with retry** — Center + error message + retry button in 8+ screens. 51 - **Empty state** — Center + message + optional action in multiple screens. 49 + **Loading** - `Center(child: CircularProgressIndicator())` in 8+ screens. 50 + **Error with retry** - Center + error message + retry button in 8+ screens. 51 + **Empty state** - Center + message + optional action in multiple screens. 52 52 53 53 Create `LoadingState`, `ErrorState(message, onRetry)`, `EmptyState(message, 54 54 icon, action)` widgets in `lib/shared/presentation/widgets/`. ··· 120 120 Reasons: 121 121 122 122 - Requires building/maintaining a separate Widgetbook app with use-case 123 - definitions for every widget — high overhead for a small team 123 + definitions for every widget - high overhead for a small team 124 124 - The project's gap is golden tests and integration tests, not a design catalog 125 125 - No dedicated designer reviewing components, so collaborative review value is 126 126 unrealized ··· 131 131 132 132 ### Recommended Approach 133 133 134 - **Golden Toolkit** (`golden_toolkit` package) — add visual regression to 134 + **Golden Toolkit** (`golden_toolkit` package) - add visual regression to 135 135 existing widget tests with minimal overhead: 136 136 137 137 - One or two lines per existing test to capture golden snapshots ··· 139 139 - Works with existing `pumpWidget` patterns 140 140 - `multiScreenGolden` for multi-device-size snapshots 141 141 142 - **Patrol** — consider later if native feature testing (permissions, deep links) 142 + **Patrol** - consider later if native feature testing (permissions, deep links) 143 143 becomes important. 144 144 145 - **Built-in integration tests** — add end-to-end flow tests using 145 + **Built-in integration tests** - add end-to-end flow tests using 146 146 `integration_test` package for critical paths (compose, auth, navigation). 147 147 148 148 ### Platform Rendering Note
+259
docs/specs/typeahead.md
··· 1 + --- 2 + title: Configurable Typeahead 3 + updated: 2026-04-25 4 + --- 5 + 6 + ## Summary 7 + 8 + Replace the current single-source typeahead with a configurable system that 9 + supports multiple backends. Two integration points: 10 + 11 + 1. **Login** - server/handle resolution during OAuth sign-in 12 + 2. **Search** - actor autocomplete in search, jump-to-profile, list member 13 + add, and starter pack member add 14 + 15 + ## Typeahead backends 16 + 17 + ### 1. Bluesky Official (default) 18 + 19 + Existing endpoint already used by `SearchRepository.searchActorsTypeahead`. 20 + 21 + - **Endpoint:** `app.bsky.actor.searchActorsTypeahead` 22 + - **SDK:** `_bluesky.actor.searchActorsTypeahead(q:, limit:)` 23 + - **Auth:** Required (session token) 24 + - **Response:** `List<ProfileViewBasic>` - `did`, `handle`, `displayName`, 25 + `avatar`, `labels` 26 + - **Rate limit:** Standard Bluesky XRPC limits 27 + 28 + ### 2. waow.tech Community Typeahead 29 + 30 + Community-run drop-in replacement. Useful for unauthenticated contexts (login) 31 + and as a faster/broader index. 32 + 33 + - **Endpoint:** `GET https://typeahead.waow.tech/xrpc/app.bsky.actor.searchActorsTypeahead` 34 + - **Params:** `q` (required), `limit` (optional, 1–100, default 10) 35 + - **Headers:** `X-Client: lazurite` (identifies app for traffic stats) 36 + - **Auth:** None required 37 + - **Response:** Same `actors` array shape as Bluesky, minus `viewer` field 38 + - **Rate limit:** 60 req/min per IP, 60s result cache 39 + - **Discovery:** Auto-indexes via Jetstream monitoring; on-demand backfill for 40 + unknown accounts 41 + - **Moderation:** Respects Bluesky `!hide`/`!takedown`/`!suspend`/`spam` labels; 42 + filters slur handles 43 + 44 + ### Response normalisation 45 + 46 + Both backends return actors in a compatible shape. The waow.tech response omits 47 + `viewer` (requires auth). Normalise into a common model: 48 + 49 + ```dart 50 + class TypeaheadResult { 51 + final String did; 52 + final String handle; 53 + final String? displayName; 54 + final String? avatarUrl; 55 + final List<Label> labels; 56 + } 57 + ``` 58 + 59 + Parse from `ProfileViewBasic` (Bluesky) or raw JSON (waow.tech) via a factory. 60 + 61 + ## Configuration 62 + 63 + ### Settings model 64 + 65 + Add a `typeaheadProvider` field to the settings table: 66 + 67 + | Key | Type | Values | Default | 68 + | -------------------- | ------ | ---------------------- | --------- | 69 + | `typeahead_provider` | string | `bluesky`, `community` | `bluesky` | 70 + 71 + Exposed via `SettingsCubit` as `state.typeaheadProvider`. 72 + 73 + ### Settings UI 74 + 75 + Add a "Typeahead Provider" option in Settings under a "Search" section: 76 + 77 + - **Bluesky** - official endpoint, requires login 78 + - **Community (waow.tech)** - faster, works pre-login, community-run 79 + 80 + Show a brief description for each option. The community option notes it's a 81 + third-party service. 82 + 83 + ## TypeaheadRepository 84 + 85 + Central abstraction that delegates to the configured backend: 86 + 87 + ```dart 88 + class TypeaheadRepository { 89 + TypeaheadRepository({ 90 + required Bluesky? bluesky, 91 + required String provider, 92 + ModerationService? moderationService, 93 + }); 94 + 95 + Future<List<TypeaheadResult>> search({ 96 + required String query, 97 + int limit = 10, 98 + }); 99 + } 100 + ``` 101 + 102 + When `provider == 'bluesky'`: 103 + 104 + - Delegates to `bluesky.actor.searchActorsTypeahead` 105 + - Passes moderation headers 106 + - Filters via `ModerationService` 107 + 108 + When `provider == 'community'`: 109 + 110 + - HTTP GET to `https://typeahead.waow.tech/xrpc/app.bsky.actor.searchActorsTypeahead` 111 + - Uses the existing `http` package (already a dependency) 112 + - Adds `X-Client: lazurite` header 113 + - Parses `actors` array from JSON response 114 + - Applies local moderation filtering (labels are included in response) 115 + 116 + ### Fallback 117 + 118 + If the community endpoint fails (timeout, rate limit, 5xx), fall back to the 119 + Bluesky endpoint when a session is available. Log the fallback via `AppLogger`. 120 + 121 + ## Integration: Login Screen 122 + 123 + ### Current state 124 + 125 + The login screen has a `TextFormField` for handle/DID entry with no 126 + autocomplete. Users must type their full handle. 127 + 128 + ### New behaviour 129 + 130 + Add typeahead suggestions below the handle field as the user types: 131 + 132 + 1. User types ≥ 2 characters 133 + 2. Debounce 300ms 134 + 3. Call `TypeaheadRepository.search` - **always uses community backend** on the 135 + login screen since no session exists yet (override the setting for this context) 136 + 4. Show results in a `ListView` overlay anchored below the text field 137 + 5. Each result shows: avatar, display name, handle 138 + 6. Tapping a result fills the handle field and triggers the OAuth flow 139 + 140 + ### Widget: `TypeaheadTextField` 141 + 142 + Reusable widget combining `TextFormField` + overlay suggestions: 143 + 144 + ```dart 145 + class TypeaheadTextField extends StatefulWidget { 146 + final TextEditingController controller; 147 + final TypeaheadRepository repository; 148 + final ValueChanged<TypeaheadResult> onSelected; 149 + final InputDecoration? decoration; 150 + final int debounceMs; 151 + final int minChars; 152 + final int limit; 153 + } 154 + ``` 155 + 156 + Uses `OverlayEntry` positioned via `LayerLink` + `CompositedTransformFollower` 157 + for correct placement. The overlay follows the text field and adapts to keyboard 158 + presence. 159 + 160 + ### Login-specific flow 161 + 162 + On the login screen, the `TypeaheadRepository` is created without a `Bluesky` 163 + instance and forces `provider: 'community'`. This allows handle discovery 164 + before authentication. 165 + 166 + ## Integration: Search Screen 167 + 168 + ### Current state 169 + 170 + `SearchBloc` has `TypeaheadRequested` / `TypeaheadResultsLoaded` events that 171 + call `SearchRepository.searchActorsTypeahead` on debounced text input. Results 172 + are stored in `state.typeaheadActors` and shown in the search screen. 173 + 174 + ### Changes 175 + 176 + Replace the direct `SearchRepository.searchActorsTypeahead` call in 177 + `SearchBloc._onTypeaheadRequested` with `TypeaheadRepository.search`. The bloc 178 + receives a `TypeaheadRepository` instead of calling `SearchRepository` for 179 + typeahead. 180 + 181 + The typeahead provider setting determines the backend. Users who prefer the 182 + community index get it everywhere (search, jump-to-profile, list member add, 183 + starter pack member add). 184 + 185 + ### Existing typeahead consumers 186 + 187 + All these currently call `SearchRepository.searchActorsTypeahead` and should 188 + migrate to `TypeaheadRepository`: 189 + 190 + | Location | Context | 191 + | ---------------------------------- | ---------------------------- | 192 + | `SearchBloc._onTypeaheadRequested` | Search screen autocomplete | 193 + | Jump-to-profile dialog | Search screen FAB | 194 + | List member add screen | `searchActorsTypeahead` call | 195 + | Starter pack member search | Create/edit starter pack | 196 + 197 + ## TypeaheadCubit 198 + 199 + Shared cubit for typeahead state, usable by any screen: 200 + 201 + ```dart 202 + class TypeaheadCubit extends Cubit<TypeaheadState> { 203 + TypeaheadCubit({required TypeaheadRepository repository}); 204 + 205 + void onQueryChanged(String query); // debounced 206 + void clear(); 207 + } 208 + 209 + class TypeaheadState { 210 + final List<TypeaheadResult> results; 211 + final bool isLoading; 212 + final String? error; 213 + } 214 + ``` 215 + 216 + This replaces the typeahead-related events in `SearchBloc`, keeping search 217 + concerns separate from typeahead concerns. 218 + 219 + ## Debouncing & rate limiting 220 + 221 + - 300ms debounce on text changes (existing pattern in `SearchBloc`) 222 + - Cancel in-flight requests when a new query arrives 223 + - Community backend: 60 req/min limit - debounce alone keeps usage well under 224 + this (user would need to change input 60 times in a minute past debounce) 225 + - Empty/whitespace queries return empty results immediately (no API call) 226 + 227 + ## Moderation 228 + 229 + Bluesky backend: moderation headers are included; server-side filtering applies. 230 + Results also pass through local `ModerationService.shouldFilterProfileBasicInList`. 231 + 232 + Community backend: response includes labels. Apply local moderation filtering 233 + using the same `ModerationService` logic. The community service already hides 234 + `!hide`/`!takedown`/`!suspend` server-side, but local filtering catches 235 + user-specific label preferences. 236 + 237 + ## Bloc architecture 238 + 239 + ```text 240 + SettingsCubit.typeaheadProvider 241 + 242 + 243 + TypeaheadRepository ──► Bluesky SDK (authenticated) 244 + │ HTTP client (community, unauthenticated) 245 + 246 + TypeaheadCubit ──► TypeaheadState { results, isLoading, error } 247 + 248 + 249 + TypeaheadTextField (Login, Search, Lists, Starter Packs) 250 + ``` 251 + 252 + ## Limitations 253 + 254 + - Community backend has no `viewer` field - cannot show follow status in 255 + typeahead results (degrade gracefully: hide follow badge) 256 + - Community backend caches for 60s - very recent handle changes may lag 257 + - Login typeahead cannot fall back to Bluesky (no session) 258 + - DID entry (not handles) bypasses typeahead entirely - typeahead is handle/name 259 + search only
+105
docs/tasks/gallery.md
··· 1 + # Gallery Viewing Milestones 2 + 3 + ## M0 - Data Layer & Models 4 + 5 + - [ ] Create `lib/features/gallery/data/gallery_media_item.dart` - `GalleryMediaItem` model with `feedViewPost`, `type` enum (`image`/`video`), `images` list, `video` args 6 + - [ ] Create `lib/features/gallery/data/gallery_utils.dart` - `extractGalleryItems(List<FeedViewPost>)` utility 7 + - Walk `embed.isEmbedImagesView`, `embed.isEmbedVideoView`, `embed.isEmbedRecordWithMediaView` 8 + - Skip external-only and text-only posts 9 + - Apply moderation filtering (`contentMedia` blur/filter) 10 + - [ ] Create `lib/features/gallery/data/gallery_route_args.dart` - `GalleryRouteArgs` with items, initial index, initial direction 11 + - [ ] Unit tests for `extractGalleryItems` - image-only, video-only, mixed, text-only, moderated, record-with-media edge cases 12 + 13 + ## M1 - GalleryCubit 14 + 15 + - [ ] Create `lib/features/gallery/cubit/gallery_cubit.dart` 16 + - [ ] Create `lib/features/gallery/cubit/gallery_state.dart` 17 + - State: `items`, `currentIndex`, `chromeVisible`, `scrollDirection` 18 + - Methods: `onPageChanged`, `toggleChrome`, `toggleDirection`, `appendItems` 19 + - [ ] Unit tests for cubit - page transitions, chrome toggle, direction toggle, item append 20 + 21 + ## M2 - Gallery Screen (Images) 22 + 23 + - [ ] Create `lib/features/gallery/presentation/gallery_screen.dart` 24 + - Full-screen `PageView` with `scrollDirection` from cubit 25 + - Image pages: `PhotoViewGallery` (reuse `photo_view`) for single/multi-image posts 26 + - Nested horizontal `PageView` + dot indicator for multi-image posts within vertical scroll 27 + - Swipe-to-dismiss gesture detection 28 + - Background opacity fade on drag 29 + - [ ] Chrome overlay - auto-hiding top/bottom bars with 3s timer 30 + - Top: close, author avatar+handle, page counter 31 + - Bottom: post text snippet, action buttons, download/share, alt text badge 32 + - [ ] Preloading: `precacheImage` for ±1 adjacent image pages 33 + - [ ] Register `/gallery` route in `app_router.dart` 34 + - [ ] Widget tests for gallery screen - page swipe, chrome toggle, dismiss gesture 35 + 36 + ## M3 - Gallery Screen (Videos) 37 + 38 + - [ ] Video page renderer within `GalleryScreen` 39 + - `VideoPlayerController` + `Chewie` for video items 40 + - Auto-play on page visible, auto-pause on swipe away 41 + - GIF handling: loop + mute + no controls 42 + - Full-bleed with pillarbox for non-9:16 aspect ratios 43 + - [ ] Video lifecycle management 44 + - Pause leaving viewport, play entering viewport 45 + - Dispose controllers >2 pages away, pre-init +1 page 46 + - [ ] Short-form interactions 47 + - Tap to pause/resume 48 + - Double-tap right → like animation + action 49 + - Double-tap left → rewind 5s 50 + - Long-press → speed selector (1x, 1.5x, 2x) 51 + - Thin progress bar at bottom 52 + - [ ] Widget tests for video pages - play/pause, lifecycle, gesture handling 53 + 54 + ## M4 - Feed Entry Points 55 + 56 + - [ ] Add gallery icon button to `LazuriteAppBar` actions in `HomeFeedScreen` 57 + - Visible when `extractGalleryItems` returns ≥1 item from loaded posts 58 + - Tap opens `/gallery` with extracted items 59 + - [ ] Add gallery icon to `ProfileScreen` feed tab section 60 + - [ ] Add gallery icon to `SearchScreen` app bar (when post results have media) 61 + - [ ] Add gallery icon to `HashtagScreen` app bar 62 + - [ ] Add gallery icon to `ListDetailScreen` feed tab 63 + - [ ] Add gallery icon to `SavedPostsScreen` app bar 64 + - [ ] Post thread: tapping media opens gallery seeded with thread's media items 65 + - [ ] Widget tests for entry point visibility - shown/hidden based on media presence 66 + 67 + ## M5 - Infinite Scroll & Pagination 68 + 69 + - [ ] Add `onLoadMore` callback and post stream/listener to `GalleryCubit` 70 + - Decoupled from specific feed source - works with `FeedBloc`, `HashtagCubit`, `ListFeedBloc`, `SearchBloc` 71 + - When user reaches last 3 pages, invoke callback to trigger parent feed load-more 72 + - [ ] `GalleryCubit.appendItems` - filter new posts through `extractGalleryItems`, merge into item list 73 + - [ ] Loading shimmer on final page while fetch is in progress 74 + - [ ] End-of-feed indicator when cursor is exhausted 75 + - [ ] For profile feeds, use `FeedFilter.postsWithMedia` to maximise gallery density 76 + - [ ] Higher `limit` (100) on feed fetches when gallery mode is active 77 + - [ ] Integration tests: gallery pagination triggers feed load-more, cursor exhaustion handled 78 + 79 + ## M6 - Notification Media & Offline Gallery 80 + 81 + - [ ] Extract media from notification subjects 82 + - Walk `subjectPost` / `reasonSubject` from notification views through `extractGalleryItems` 83 + - Add gallery entry point in `NotificationsScreen` when media-bearing notifications exist 84 + - [ ] Offline gallery support 85 + - Parse `CachedFeedPages.payload` JSON into `FeedViewPost` objects for `extractGalleryItems` 86 + - Parse `SavedPosts.postJson` and `LikedPosts.postJson` for gallery items 87 + - Gallery entry point visible when `ConnectivityCubit.isOffline` and cached data has media 88 + - Disable like/repost/reply buttons (disabled state + "Offline" tooltip) 89 + - Disable download/share for uncached media 90 + - Show "Offline - showing cached posts" indicator at end of gallery 91 + - Pagination unavailable offline - only show cached items 92 + - [ ] Unit tests: offline gallery from cached data, disabled actions, end indicator 93 + - [ ] Widget tests: notification gallery entry point, offline gallery rendering 94 + 95 + ## M7 - Media Density & Polish 96 + 97 + - [ ] Media density indicator in chrome overlay (e.g. "12 media from 47 loaded") 98 + - [ ] `Semantics` labels on gallery pages (author + media type) 99 + - [ ] Alt text exposed to screen readers for images and videos 100 + - [ ] Chrome overlay elements focusable and labeled 101 + - [ ] Keyboard navigation: arrow keys for pages, Escape to dismiss 102 + - [ ] Haptic feedback on like (double-tap) 103 + - [ ] Smooth page transition animations 104 + - [ ] `flutter analyze` clean 105 + - [ ] Full test suite passes
+13 -13
docs/tasks/testing.md
··· 1 1 # Testing Milestones 2 2 3 - ## M0 — Shared Utilities Extraction 3 + ## M0 - Shared Utilities Extraction 4 4 5 5 - [ ] Create `lib/shared/utils/format_utils.dart` with `formatInitials`, `formatCount`, `formatRelativeTime` 6 6 - [ ] Replace `_initials` in: ··· 27 27 - `lib/features/search/presentation/hashtag_screen.dart` 28 28 - [ ] Unit tests for all format functions (edge cases: empty string, zero, negative, boundary values) 29 29 30 - ## M1 — Shared State Widgets 30 + ## M1 - Shared State Widgets 31 31 32 32 - [ ] Create `lib/shared/presentation/widgets/loading_state.dart` 33 - - [ ] Create `lib/shared/presentation/widgets/error_state.dart` — `ErrorState(message, onRetry)` 34 - - [ ] Create `lib/shared/presentation/widgets/empty_state.dart` — `EmptyState(message, icon, action)` 33 + - [ ] Create `lib/shared/presentation/widgets/error_state.dart` - `ErrorState(message, onRetry)` 34 + - [ ] Create `lib/shared/presentation/widgets/empty_state.dart` - `EmptyState(message, icon, action)` 35 35 - [ ] Replace loading/error/empty states in: 36 36 - `lib/features/feed/presentation/home_feed_screen.dart` 37 37 - `lib/features/feed/presentation/feed_management_screen.dart` ··· 43 43 - `lib/features/notifications/presentation/notifications_screen.dart` 44 44 - [ ] Widget tests for each state widget 45 45 46 - ## M2 — Dialog & Sheet Consolidation 46 + ## M2 - Dialog & Sheet Consolidation 47 47 48 48 - [ ] Create `lib/shared/presentation/widgets/confirmation_dialog.dart` 49 49 - [ ] Create `lib/shared/presentation/widgets/options_sheet.dart` 50 - - [ ] Create `lib/shared/presentation/helpers/snackbar_helper.dart` — `showAppSnackBar` 50 + - [ ] Create `lib/shared/presentation/helpers/snackbar_helper.dart` - `showAppSnackBar` 51 51 - [ ] Replace confirmation dialogs in: 52 52 - `lib/features/profile/presentation/widgets/profile_action_buttons.dart` (5 dialogs) 53 53 - `lib/features/compose/presentation/compose_screen.dart` (6 dialogs) ··· 74 74 - `lib/features/settings/presentation/settings_screen.dart` 75 75 - [ ] Tests for dialog/sheet/snackbar helpers 76 76 77 - ## M3 — Theme & Spacing Constants 77 + ## M3 - Theme & Spacing Constants 78 78 79 - - [ ] Create `lib/core/theme/theme_extensions.dart` — `BuildContext` extension for `colorScheme` access 79 + - [ ] Create `lib/core/theme/theme_extensions.dart` - `BuildContext` extension for `colorScheme` access 80 80 - [ ] Create `lib/core/theme/spacing.dart` with padding/margin constants 81 - - [ ] Create `lib/core/theme/color_filters.dart` — extract greyscale matrix from: 81 + - [ ] Create `lib/core/theme/color_filters.dart` - extract greyscale matrix from: 82 82 - `lib/features/feed/presentation/widgets/grid_post_card.dart` 83 83 - `lib/features/profile/presentation/profile_screen.dart` 84 84 - [ ] Refactor files to use new constants/extensions (27 files use `Theme.of(context).colorScheme`) 85 85 - [ ] Tests for theme extension 86 86 87 - ## M4 — Widget Extraction 87 + ## M4 - Widget Extraction 88 88 89 89 - [ ] Create `lib/shared/presentation/widgets/profile_avatar.dart` (configurable size, shape, fallback) 90 90 - [ ] Create `lib/shared/presentation/widgets/actor_name_widget.dart` (displayName + handle) ··· 114 114 - `lib/features/notifications/presentation/widgets/grouped_notification_list_item.dart` 115 115 - [ ] Widget tests for extracted widgets 116 116 117 - ## M5 — Navigation & Haptics Helpers 117 + ## M5 - Navigation & Haptics Helpers 118 118 119 119 - [ ] Create `lib/shared/presentation/helpers/navigation_helpers.dart` (`navigateToProfile`, `navigateToPost`) 120 120 - [ ] Create `lib/shared/presentation/helpers/haptic_helper.dart` ··· 125 125 - [ ] Replace haptic feedback call sites (17 occurrences across 6 files) 126 126 - [ ] Tests for navigation helpers 127 127 128 - ## M6 — Golden Testing Setup 128 + ## M6 - Golden Testing Setup 129 129 130 130 - [ ] Add `golden_toolkit` to dev_dependencies 131 131 - [ ] Configure golden test threshold for CI tolerance ··· 136 136 - [ ] CI pipeline step for golden test comparison 137 137 - [ ] Document golden update workflow (`flutter test --update-goldens`) 138 138 139 - ## M7 — Integration Tests 139 + ## M7 - Integration Tests 140 140 141 141 - [ ] Add `integration_test` to dev_dependencies 142 142 - [ ] Auth flow end-to-end test
+82
docs/tasks/typeahead.md
··· 1 + # Typeahead Milestones 2 + 3 + ## M0 - Data Layer 4 + 5 + - [ ] Create `lib/features/typeahead/data/typeahead_result.dart` - `TypeaheadResult` model (`did`, `handle`, `displayName`, `avatarUrl`, `labels`) 6 + - Factory `fromProfileViewBasic` for Bluesky backend 7 + - Factory `fromJson` for community backend raw JSON 8 + - [ ] Create `lib/features/typeahead/data/typeahead_repository.dart` 9 + - Constructor takes optional `Bluesky`, required `provider` string, optional `ModerationService` 10 + - `search(query, limit)` dispatches to Bluesky SDK or HTTP based on provider 11 + - Bluesky path: `bluesky.actor.searchActorsTypeahead(q:, limit:)` + moderation headers + filtering 12 + - Community path: HTTP GET to `https://typeahead.waow.tech/xrpc/app.bsky.actor.searchActorsTypeahead?q=&limit=` with `X-Client: lazurite` header 13 + - Fallback: community failure → Bluesky endpoint (when session available), log via `AppLogger` 14 + - [ ] Unit tests for `TypeaheadRepository` 15 + - Bluesky provider delegates to SDK, applies moderation filtering 16 + - Community provider makes HTTP request, parses JSON, applies local moderation 17 + - Fallback triggers on community error when Bluesky available 18 + - Fallback does not trigger when no session (login context) 19 + 20 + ## M1 - Settings Integration 21 + 22 + - [ ] Add `typeahead_provider` column to settings table (Drift migration) 23 + - Type: text, default: `'bluesky'`, allowed: `'bluesky'` | `'community'` 24 + - [ ] Add `typeaheadProvider` to `SettingsState` 25 + - [ ] Add `setTypeaheadProvider(String)` to `SettingsCubit` 26 + - [ ] Settings UI: add "Typeahead Provider" option under a "Search" section 27 + - Radio/segmented control: Bluesky / Community (waow.tech) 28 + - Brief description for each, note community is third-party 29 + - [ ] Unit test: `SettingsCubit` persists and restores typeahead provider 30 + - [ ] Widget test: settings screen shows typeahead provider selector 31 + 32 + ## M2 - TypeaheadCubit 33 + 34 + - [ ] Create `lib/features/typeahead/cubit/typeahead_cubit.dart` 35 + - [ ] Create `lib/features/typeahead/cubit/typeahead_state.dart` 36 + - State: `results`, `isLoading`, `error` 37 + - Methods: `onQueryChanged(String)` (300ms debounce), `clear()` 38 + - Cancel in-flight on new query 39 + - Empty/whitespace → emit empty results immediately 40 + - [ ] Unit tests: debounce fires, cancel-on-new-query, empty input handling 41 + 42 + ## M3 - TypeaheadTextField Widget 43 + 44 + - [ ] Create `lib/features/typeahead/presentation/typeahead_text_field.dart` 45 + - Wraps `TextFormField` + `OverlayEntry` for suggestions dropdown 46 + - Uses `LayerLink` + `CompositedTransformFollower` for positioning 47 + - Props: `controller`, `repository`, `onSelected`, `decoration`, `debounceMs`, `minChars`, `limit` 48 + - Each result row: avatar (CircleAvatar), display name, @handle 49 + - Keyboard-aware: overlay repositions when keyboard shows/hides 50 + - Tap result → calls `onSelected`, clears overlay 51 + - Tap outside → dismisses overlay 52 + - [ ] Widget tests: overlay appears on input, results render, tap selects, tap-outside dismisses 53 + 54 + ## M4 - Login Integration 55 + 56 + - [ ] Modify `LoginScreen` to use `TypeaheadTextField` for the handle field 57 + - Create `TypeaheadRepository` without `Bluesky`, force `provider: 'community'` 58 + - `onSelected` fills handle controller + triggers `_onOAuthLogin` 59 + - `minChars: 2`, `debounceMs: 300`, `limit: 8` 60 + - [ ] Preserve existing validation (`validator`, `TextInputAction.next`) 61 + - [ ] Preserve debug app-password form (unaffected) 62 + - [ ] Widget test: login typeahead shows community results, selecting triggers login flow 63 + - [ ] Integration test: type handle → see suggestions → tap → OAuth initiates 64 + 65 + ## M5 - Search Integration 66 + 67 + - [ ] Inject `TypeaheadRepository` into `SearchBloc` (replace direct `SearchRepository.searchActorsTypeahead` usage) 68 + - [ ] Update `SearchBloc._onTypeaheadRequested` to call `TypeaheadRepository.search` 69 + - [ ] Map `TypeaheadResult` back to `ProfileViewBasic` for `state.typeaheadActors` compatibility (or migrate state to `TypeaheadResult`) 70 + - [ ] Update jump-to-profile dialog to use `TypeaheadCubit` + `TypeaheadTextField` 71 + - [ ] Update list member add screen to use `TypeaheadRepository` 72 + - [ ] Update starter pack member search to use `TypeaheadRepository` 73 + - [ ] Unit tests: `SearchBloc` typeahead delegates to `TypeaheadRepository` 74 + - [ ] Widget tests: search typeahead renders results from configured provider 75 + 76 + ## M6 - Polish & Validation 77 + 78 + - [ ] Verify rate limiting: 300ms debounce keeps community usage well under 60 req/min 79 + - [ ] Graceful degradation: community results without `viewer` → hide follow badge 80 + - [ ] Error handling: network timeout → show inline error, not crash 81 + - [ ] `flutter analyze` clean 82 + - [ ] Full test suite passes
+9 -9
docs/tasks/ui-refactor.md
··· 1 1 # UI Refactor Milestones 2 2 3 - ## M0 — Foundation & Layout Settings Persistence 3 + ## M0 - Foundation & Layout Settings Persistence 4 4 5 5 Completed 2026-03-20 6 6 7 - ## M1 — Navigation Chrome 7 + ## M1 - Navigation Chrome 8 8 9 9 Completed 2026-03-20 10 10 11 - ## M2 — Post Card Variants 11 + ## M2 - Post Card Variants 12 12 13 13 Completed 2026-03-20 14 14 15 - ## M3 — Home Feed Grid Layout 15 + ## M3 - Home Feed Grid Layout 16 16 17 17 Completed 2026-03-20 18 18 19 - ## M4 — Profile Screen Refactor 19 + ## M4 - Profile Screen Refactor 20 20 21 21 Completed 2026-03-20 22 22 23 - ## M5 — Layout Settings Screen 23 + ## M5 - Layout Settings Screen 24 24 25 - - [ ] UI Density selector — three radio-style cards with schematic icons 26 - - [ ] Feed Architecture selector — two square toggle cards 25 + - [ ] UI Density selector - three radio-style cards with schematic icons 26 + - [ ] Feed Architecture selector - two square toggle cards 27 27 - [ ] Viewport Preview wireframe that updates live with selections 28 28 - [ ] Settings screen entry point (new section or drawer link) 29 29 - [ ] Persist selections to Drift on change 30 30 - [ ] Tests for settings screen interactions and persistence round-trip 31 31 32 - ## M6 — Collapsible Threaded Replies 32 + ## M6 - Collapsible Threaded Replies 33 33 34 34 Completed [2026-03-21](../../CHANGELOG.md#2026-03-21)