mobile bluesky app made with flutter
lazurite.stormlightlabs.org/
mobile
bluesky
flutter
title: Phase 5 Task Breakdown updated: 2026-04-01#
Phase 5 Milestones#
M20 - Starter Pack Search#
Core#
-
SearchRepository.searchStarterPacks()- callbluesky.graph.searchStarterPacks(q:, limit:, cursor:), return result withList<StarterPackViewBasic>and cursor - Add
starterPacksvalue toSearchTabenum, updateSearchTabLabelextension
Cubit#
-
SearchBloc- handle starter packs tab: dispatch search on tab switch if query present, handleLoadMoreRequestedwith cursor pagination -
SearchState- addstarterPackslist andstarterPacksCursorfields
UI#
- Search screen UI - add third "Starter Packs" tab pill in
_buildTabrow - Starter pack result tile widget - show name, creator handle, member count, joined stats; reuse pattern from profile starter packs tab
- Tap result → navigate to existing starter pack detail screen (
/starter-pack?uri=) - Infinite scroll pagination for starter packs tab
Tests#
- Unit tests:
SearchRepository.searchStarterPacks, bloc events for new tab, pagination - Widget tests: third tab renders, results display, empty state, tap navigation
M21 - Suggested Follows Sheet#
Core#
-
ProfileRepository.getSuggestedFollows()- callbluesky.graph.getSuggestedFollowsByActor(actor:), returnList<ProfileView>
Cubit#
-
SuggestedFollowsCubit-load(actor:)fetches suggestions, exposes loaded/loading/error states
UI#
- Suggested follows sheet widget -
DraggableScrollableSheetlistingProfileViewtiles with follow/unfollow toggle buttons - Profile screen overflow menu - add "Suggested Follows"
ListTileentry; hide when viewing own profile - Tap entry → create cubit, show sheet with
BlocProvider.value, close cubit on sheet dismiss via.whenComplete - Tap profile tile → pop sheet, navigate to profile screen
- Empty state when no suggestions returned
Tests#
- Unit tests: repository method, cubit state transitions
- Widget tests: sheet renders profiles, follow button toggles, own-profile menu hides entry, empty state
M22 - Video Upload Limits#
Core#
-
VideoRepository(or extend settings repository) -getUploadLimits()callingbluesky.video.getUploadLimits(), return typed result
Cubit#
-
VideoUploadLimitsCubit- fetch on init, exposecanUpload, remaining counts, message/error
UI#
- Settings screen - new tile in Account section: "Video Upload Limits"
- Tile UI - show remaining daily video count, remaining bytes formatted as MB/GB,
canUploadstatus badge - Loading state while fetching, error state if request fails
- Display server
messageif present; showerrortext with warning styling ifcanUploadis false
Tests#
- Unit tests: repository method, cubit state transitions and formatting
- Widget tests: tile renders limits, loading indicator, error state, message display
M23 - Profile Context (Constellation)#
Core - Constellation Client#
-
ConstellationClient- thin HTTP client (httppackage) targeting configurable base URL (defaulthttps://constellation.microcosm.blue), 10s timeout,User-Agent: lazurite -
Settings- addconstellation_urlkey with default value; expose in Settings screen under "Advanced" -
getBacklinksCount(subject, source)→inttotal -
getDistinct(subject, source, {limit, cursor})→({int total, List<String> dids, String? cursor}) -
getBacklinks(subject, source, {limit, cursor})→({int total, List<ConstellationLinkRecord> records, String? cursor}) -
getManyToMany(subject, source, pathToOther, {limit, cursor})→({List<ManyToManyItem> items, String? cursor}) -
ConstellationLinkRecordmodel -did,collection,rkey -
ManyToManyItemmodel -linkRecord: ConstellationLinkRecord,otherSubject: String
Core - Profile Context Repository#
-
ProfileContextRepository- depends onConstellationClient+Bluesky -
getBlockedByCount(did)- callsgetBacklinksCount(did, 'app.bsky.graph.block:subject') -
getBlockedByProfiles(did, {cursor})- callsgetDistinct, hydrates DIDs viabluesky.actor.getProfiles(batched 25), returns({List<ProfileView> profiles, String? cursor, int total}) -
getBlockingProfiles(did, {cursor})- callscom.atproto.repo.listRecords(repo: did, collection: 'app.bsky.graph.block'), extracts subject DIDs, hydrates viagetProfiles, returns same shape -
getListsOn(did, {cursor})- callsgetManyToMany(did, 'app.bsky.graph.listitem:subject', 'list'), derives list AT-URIs fromotherSubject, hydrates viabluesky.graph.getList, returns({List<ListView> lists, String? cursor, int total})
Cubit#
-
ProfileContextCubit- manages tab state, loads counts on init for all three tabs -
ProfileContextState- fields:blockedByCount,blockingCount,listsOnCount, per-tabstatus(initial/loading/loaded/error), per-tab item list + cursor -
loadBlockedBy({cursor})- fetches page of blocked-by profiles, appends to state -
loadBlocking({cursor})- fetches page of blocking profiles, appends to state -
loadListsOn({cursor})- fetches page of lists, appends to state - Handle own-profile vs other-profile: blocking tab only available for own profile
UI#
- Profile screen overflow menu - add "Profile Context" entry (available for all profiles)
- Route:
/profile-context?did={DID}inapp_router.dart -
ProfileContextScreen-AppBar(title + handle subtitle),TabBarwith 3 tabs,BlocProvidercreating cubit - Blocked By tab - count header, "Show accounts" expand, paginated profile tiles (avatar, name, handle), tap → profile navigation, contextualizing note text
- Blocking tab - same layout; hidden or explanatory text when viewing other profiles
- Lists tab - list cards (name, owner, purpose badge, member count, description), grouped by purpose, tap →
/list?uri= - Per-tab states: skeleton shimmer (loading), contextual empty state, inline error with retry
- Pull-to-refresh per tab
- Infinite scroll pagination per tab
Tests#
- Unit tests:
ConstellationClient- each endpoint method, error handling, timeout, URL construction - Unit tests:
ProfileContextRepository- DID hydration batching, list URI derivation, cursor passthrough - Unit tests:
ProfileContextCubit- state transitions for each tab, own-profile vs other-profile logic, pagination appending - Widget tests: screen renders 3 tabs, blocked-by count + expand, profile tiles render and navigate, list cards render and navigate, empty states, error + retry, blocking tab hidden for non-own profiles