···11----
22-title: Bugs & Inconsistencies
33-updated: 2026-03-18
44----
55-66-77-Saved posts should be a tabbed view for local & ATProto/BSky saved posts.
+1
docs/TODO.md
···1616 link to dev tools and follow audits.
1717- Constellation URL should remain configurable internally but the option to change the
1818 URL should be removed from the UI.
1919+- Saved posts should be a tabbed view for local & ATProto/BSky saved posts.
19202021## UX
2122
-60
docs/copywith.md
···11-# Nullable `copyWith` Rule
22-33-## Problem
44-55-A common state bug occurs when `copyWith` uses `field ?? this.field` for nullable fields.
66-That pattern cannot distinguish:
77-88-- "keep current value"
99-- "set this field to null"
1010-1111-This breaks flows where `null` is meaningful (for example clearing cursors, `likeUri`, `repostUri`, or `errorMessage`).
1212-1313-## Required Pattern
1414-1515-For nullable fields in immutable state objects, use a sentinel parameter default:
1616-1717-```dart
1818-static const Object _unset = Object();
1919-2020-State copyWith({
2121- Object? nullableField = _unset,
2222-}) {
2323- return State(
2424- nullableField: identical(nullableField, _unset)
2525- ? this.nullableField
2626- : nullableField as String?,
2727- );
2828-}
2929-```
3030-3131-## Where Applied
3232-3333-- `SearchState.copyWith` (cursor and nullable metadata fields)
3434-- `FeedState.copyWith` (cursor and error fields)
3535-- `PostActionState.copyWith` (`likeUri`, `repostUri`, `error`)
3636-- `MessageState.copyWith` (`cursor`, `convoId`, `errorMessage`)
3737-- `ConvoListState.copyWith` (`cursor`, `errorMessage`)
3838-- `NotificationState.copyWith` (`cursor`, `errorMessage`)
3939-- `AuthState.copyWith` (`tokens`, `errorMessage`)
4040-- `AccountSwitcherState.copyWith` (`activeDid`)
4141-- `ProfileState.copyWith` (`profile`, `errorMessage`)
4242-- `AddToListState.copyWith` (`targetDid`, `errorMessage`)
4343-4444-## Already Using Sentinel Pattern
4545-4646-- `ComposeState` / `VideoAttachment` / `MediaAttachment`
4747-- `ListState`
4848-- `ListFeedState`
4949-- `MyListsState`
5050-- `ActorStarterPacksState`
5151-- `StarterPackState`
5252-- `DevToolsState`
5353-- `LogViewerState`
5454-- `ProfileContextState`
5555-- `SettingsState` (for `threadAutoCollapseDepth`)
5656-5757-## Review Checklist
5858-5959-- If a field is nullable and needs to be clearable, do not use `??` in `copyWith`.
6060-- Add/keep tests that assert nullable fields can be explicitly cleared to `null`.
+123
docs/dev/patterns.md
···11+# Lazurite Code Patterns
22+33+This document captures recurring architectural and implementation patterns in the Lazurite codebase.
44+55+## Code Organization
66+77+```sh
88+lib/
99+ ├── core/ # cross-cutting app/platform concerns (router, theme, logging, network, database)
1010+ ├── features/ # feature-first modules
1111+ │ └── {feature} # one vertical slice per domain
1212+ │ ├── data/ # repositories, DTO mapping, persistence/network access
1313+ │ ├── bloc or cubit/ # state orchestration and business flow
1414+ │ └── presentation/ # screens, widgets, and view-specific helpers
1515+ └── shared/ # reusable widgets/helpers/utils not tied to one feature
1616+test/ # mirrors production layout (core/features/shared) for unit/widget tests
1717+docs/ # specs, tasks, operational docs, and developer references
1818+```
1919+2020+## State and UI Patterns
2121+2222+- Prefer immutable `Equatable` state with explicit `copyWith` semantics.
2323+- Model state transitions in `Bloc`/`Cubit` layers; keep presentation widgets focused on rendering and user interactions.
2424+- Use `sealed class` events and typed state classes to keep transitions explicit and testable.
2525+- For repeated UI states, use shared widgets instead of ad-hoc `Center(...)` blocks:
2626+ - `LoadingState`
2727+ - `ErrorState`
2828+ - `EmptyState`
2929+3030+## Extracted Reuse Patterns
3131+3232+### Utilities
3333+3434+`lib/shared/utils/format_utils.dart` is the canonical source for display primitives (initials, compact counts, relative time labels).
3535+The extraction replaced repeated local formatters from feed/profile/search/notification surfaces with one tested implementation (`test/shared/utils/format_utils_test.dart`).
3636+3737+### Dialogs, Sheets, and Snackbars
3838+3939+Transient UI interaction patterns were consolidated into shared helpers and widgets:
4040+4141+- `showConfirmationDialog` for confirm/cancel flows
4242+- `showOptionsSheet` for action menus
4343+- `showAppSnackBar` for user feedback
4444+4545+This keeps behavior and semantics consistent across features and is covered with focused widget tests in `test/shared/presentation/...`.
4646+4747+### Theme Access and Tokens
4848+4949+Theme access should go through `lib/core/theme/theme_extensions.dart` (`context.theme`, `context.colorScheme`, `context.textTheme`) rather than repeated `Theme.of(...)` lookups.
5050+5151+For layout rhythm, reuse spacing tokens from `lib/core/theme/spacing.dart`.
5252+5353+For visual consistency in image treatment, use shared filters from `lib/core/theme/color_filters.dart`.
5454+5555+### Reusable Presentation Widgets
5656+5757+Repeated avatar/name/icon logic from the audit was extracted into shared primitives. Prefer:
5858+5959+- `ProfileAvatar` (`lib/shared/presentation/widgets/profile_avatar.dart`) for all profile/list/feed avatar rendering, including fallback and moderation-aware masking behavior.
6060+- `ActorNameWidget` (`lib/shared/presentation/widgets/actor_name_widget.dart`) for standardized display-name + handle rows (used in feed cards, embedded records, and conversation items).
6161+- `NotificationIconMapper` (`lib/shared/presentation/helpers/notification_icon_mapper.dart`) for mapping notification reason to icon + color style, instead of local switch blocks.
6262+6363+### Navigation and Haptics
6464+6565+Navigation/haptic side effects also follow shared helper entry points.
6666+Use `navigateToProfile` and `navigateToPost` for common entity navigation, and route tactile feedback through `HapticHelper` instead of direct `HapticFeedback` calls in feature widgets.
6767+6868+## Other Recurring Codebase Patterns
6969+7070+- Feature-first vertical slices under `lib/features/{feature}/{data,bloc|cubit,presentation}` keep business logic close to owning UI while preserving cross-feature boundaries.
7171+- Dependency injection is explicit at composition boundaries (`RepositoryProvider`/`BlocProvider` in app and screen roots), which makes screens and blocs easy to test in isolation.
7272+- Repository interfaces return typed results (often with cursor pagination) instead of raw maps, keeping API edges explicit and testable.
7373+- Defensive fallback behavior is preferred for unstable integrations; for example, repository logic that falls back on alternative endpoints when one API path fails is covered by contract tests.
7474+- Shared moderation-aware rendering is treated as a first-class cross-feature concern (`ModerationUI`, `ModerationBadgeRow`, `ModeratedBlurOverlay`, `ProfileAvatar` moderation hooks).
7575+- Use fully-qualified package imports (`package:lazurite/...`) for internal modules.
7676+7777+## Moderation-Aware Rendering Pattern
7878+7979+- For actor/content media that can be moderated, request moderation UI state via moderation service helpers and pass it into rendering widgets.
8080+- `ProfileAvatar` supports moderated fallback/masking through `ModerationUI`.
8181+- Compose moderation badges/overlays (`ModerationBadgeRow`, `ModeratedBlurOverlay`) at feature widget boundaries.
8282+8383+## Nullable `copyWith` Pattern (Required)
8484+8585+For nullable fields in immutable state, **do not** use `field ?? this.field` when `null` is a meaningful explicit update.
8686+8787+Use a sentinel default to distinguish:
8888+8989+- keep current value
9090+- set value to `null`
9191+9292+```dart
9393+static const Object _unset = Object();
9494+9595+State copyWith({
9696+ Object? nullableField = _unset,
9797+}) {
9898+ return State(
9999+ nullableField: identical(nullableField, _unset)
100100+ ? this.nullableField
101101+ : nullableField as String?,
102102+ );
103103+}
104104+```
105105+106106+### Review Checklist
107107+108108+- For clearable nullable fields, avoid `??` fallback in `copyWith`.
109109+- Add tests proving nullable fields can be explicitly cleared.
110110+111111+## Testing Patterns
112112+113113+- Per-file harness builders are standard (`buildSubject(...)`, plus helpers like `openSheet(...)`) so test bodies focus on behavior, not setup.
114114+- Widget tests usually mount through `MaterialApp`/`Scaffold` for component tests, and `MaterialApp.router` + `GoRouter` for navigation tests.
115115+- Interaction flow follows a consistent shape:
116116+ `pumpWidget` -> input (`tap`, `enterText`) -> `pump`/`pumpAndSettle` -> assertions on visible UI and side effects.
117117+- BLoC/Cubit widget tests rely on `mocktail` + `bloc_test` (`MockBloc`, `MockCubit`, `when`, `whenListen`, `verify`, `verifyNever`), with `registerFallbackValue` in `setUpAll` when needed.
118118+- Router behavior is verified by capturing the pushed route URI and asserting path/query params
119119+- Complex service/repository tests use fit-for-purpose doubles:
120120+ - `MockClient` from `http/testing.dart` for HTTP-level contracts
121121+ - Small local fake implementations when protocol surfaces are complex
122122+- Async UI timing is made explicit where animations/debounce are relevant (`pump(const Duration(...))` in sheet/search tests), and responsive behavior is exercised with `setSurfaceSize` in router/shell tests.
123123+- Defensive assertions are common for robustness: boundary-value checks in utility tests, null/empty/error-path repository tests, and `expect(tester.takeException(), isNull)` guards in navigation/router tests.
docs/smoke-test.md
docs/dev/smoke-test.md
-155
docs/specs/testing.md
···11-# Testing Strategy
22-33-Audit of the presentation layer and evaluation of visual testing tooling to
44-improve test coverage, reduce code duplication, and establish a golden testing
55-baseline.
66-77-## Presentation Layer Audit
88-99-A full audit of `lib/features/*/presentation/` identified significant
1010-duplication across 40+ files. The findings group into ten categories.
1111-1212-### 1. Duplicated Utility Functions
1313-1414-| Function | Pattern | Occurrences |
1515-| ----------------------- | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
1616-| `_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) |
1717-| `_formatCount(int)` | Formats numbers with K/M suffixes | 5 files (post_action_bar, post_card_footer, starter_pack_card, starter_pack_detail, profile_screen) |
1818-| `_formatTime(DateTime)` | Relative time strings ("2h ago") | 4 files (notification items, search, hashtag) - overlaps with `formatPostTime()` in post_card_footer |
1919-2020-**Target**: Extract to `lib/shared/utils/format_utils.dart`.
2121-2222-### 2. Duplicated Widgets
2323-2424-**Avatar display** - 14 files repeat `CircleAvatar` / `ModeratedAvatar` with
2525-identical styling. Extract to a configurable `ProfileAvatar` widget.
2626-2727-**Author name + handle** - Repeated two-line text widget (displayName / @handle)
2828-in post_card, grid_post_card, post_embed_view, convo_list_item. Extract to
2929-`ActorNameWidget`.
3030-3131-**Greyscale color filter** - Identical 4x5 color matrix defined in
3232-grid_post_card and profile_screen. Extract to `lib/core/theme/color_filters.dart`.
3333-3434-**Notification reason icon** - Large switch statement mapping notification
3535-reasons to icons/colors duplicated identically in notification_list_item and
3636-grouped_notification_list_item. Extract to `NotificationIconMapper`.
3737-3838-### 3. Bottom Sheets & Dialogs
3939-4040-**Modal bottom sheets** - 9 files repeat `showModalBottomSheet` with ListTile
4141-option lists. Create `OptionsSheet` builder.
4242-4343-**Confirmation dialogs** - 26 occurrences of AlertDialog with
4444-title/content/cancel/confirm. Create `ConfirmationDialog(title, content,
4545-confirmLabel, onConfirm)`.
4646-4747-### 4. State Handling Patterns
4848-4949-**Loading** - `Center(child: CircularProgressIndicator())` in 8+ screens.
5050-**Error with retry** - Center + error message + retry button in 8+ screens.
5151-**Empty state** - Center + message + optional action in multiple screens.
5252-5353-Create `LoadingState`, `ErrorState(message, onRetry)`, `EmptyState(message,
5454-icon, action)` widgets in `lib/shared/presentation/widgets/`.
5555-5656-### 5. SnackBar Display
5757-5858-`ScaffoldMessenger.of(context).showSnackBar(SnackBar(..., behavior:
5959-floating))` appears in 14 files. Create `showAppSnackBar(context, message,
6060-{isError})` helper.
6161-6262-### 6. Theme Access Boilerplate
6363-6464-`Theme.of(context).colorScheme.*` appears 142 times across 27 files.
6565-`Border.all(color: colorScheme.outlineVariant)` is the most common repeated
6666-decoration. Consider a `BuildContext` extension:
6767-6868-```dart
6969-extension ThemeX on BuildContext {
7070- ColorScheme get colorScheme => Theme.of(this).colorScheme;
7171-}
7272-```
7373-7474-### 7. Spacing Constants
7575-7676-`EdgeInsets.symmetric(horizontal: 16)` appears 34+ times. `SizedBox(height: 8)`
7777-and variants repeat throughout. Define constants in
7878-`lib/core/theme/spacing.dart`.
7979-8080-### 8. Navigation Helpers
8181-8282-Profile navigation (`GoRouter.maybeOf(context)?.push('/profile/view?actor=...')`)
8383-repeated across post cards and notification items. Extract to route helper
8484-functions.
8585-8686-### 9. Haptic Feedback
8787-8888-17 occurrences of `HapticFeedback.mediumImpact()` across 6 files. Minor, but a
8989-`HapticHelper` would centralize.
9090-9191-### 10. List Item Tiles
9292-9393-23 occurrences of ListTile with avatar/title/subtitle across features. A
9494-`BaseListItemTile` could reduce this, though diversity of layouts may limit
9595-reuse.
9696-9797-## Visual Testing Evaluation
9898-9999-### Current State
100100-101101-- 122 test files across 16 feature modules
102102-- Stack: `flutter_test`, `bloc_test`, `mocktail`
103103-- Test types: unit (cubits, repos, services) + widget (screens, widgets)
104104-- **No golden tests, no integration tests, no visual regression testing**
105105-106106-### Widgetbook
107107-108108-Component catalog + visual testing platform for Flutter (v3.22.0 stable,
109109-v4.0.0-beta.3).
110110-111111-| Capability | Detail |
112112-| ----------------- | --------------------------------------------------------- |
113113-| Component catalog | Render widgets in isolation with configurable knobs |
114114-| Visual regression | `widgetbook_golden_test` generates goldens from use cases |
115115-| Widgetbook Cloud | CI visual diffs, platform-independent rendering (paid) |
116116-| Addons | Multi-theme, locale, text scale, device frame testing |
117117-118118-**Verdict: Not recommended for Lazurite at this time.**
119119-120120-Reasons:
121121-122122-- Requires building/maintaining a separate Widgetbook app with use-case
123123- definitions for every widget - high overhead for a small team
124124-- The project's gap is golden tests and integration tests, not a design catalog
125125-- No dedicated designer reviewing components, so collaborative review value is
126126- unrealized
127127-- Converting 122 existing widget tests into Widgetbook use cases is low ROI
128128-129129-**When to reconsider**: If a shared design system emerges, a designer joins, or
130130-PR visual review becomes a bottleneck.
131131-132132-### Recommended Approach
133133-134134-**Golden Toolkit** (`golden_toolkit` package) - add visual regression to
135135-existing widget tests with minimal overhead:
136136-137137-- One or two lines per existing test to capture golden snapshots
138138-- No separate app to maintain
139139-- Works with existing `pumpWidget` patterns
140140-- `multiScreenGolden` for multi-device-size snapshots
141141-142142-**Patrol** - consider later if native feature testing (permissions, deep links)
143143-becomes important.
144144-145145-**Built-in integration tests** - add end-to-end flow tests using
146146-`integration_test` package for critical paths (compose, auth, navigation).
147147-148148-### Platform Rendering Note
149149-150150-Golden tests produce platform-dependent pixel output (macOS dev vs Linux CI).
151151-Mitigations:
152152-153153-- Tolerance thresholds in comparisons
154154-- CI-only golden generation with committed baselines
155155-- Or Widgetbook Cloud (if budget allows) for platform-independent rendering
-75
docs/tasks/testing.md
···11-# Testing Milestones
22-33-## M0 - Shared Utilities Extraction
44-55-- [x] Create `lib/shared/utils/format_utils.dart` with `formatInitials`, `formatCount`, `formatRelativeTime`
66-- [x] Replace `_initials`
77-- [x] Replace `_formatCount`
88-- [x] Consolidate `_formatTime` with existing `formatPostTime` in `post_card_footer.dart`
99-- [x] Unit tests for all format functions (edge cases: empty string, zero, negative, boundary values)
1010-1111-## M1 - Shared State Widgets
1212-1313-- [x] Create `lib/shared/presentation/widgets/loading_state.dart`
1414-- [x] Create `lib/shared/presentation/widgets/error_state.dart` - `ErrorState(message, onRetry)`
1515-- [x] Create `lib/shared/presentation/widgets/empty_state.dart` - `EmptyState(message, icon, action)`
1616-- [x] Replace loading/error/empty states across the app with new widgets
1717-- [x] Widget tests for each state widget
1818-1919-## M2 - Dialog & Sheet Consolidation
2020-2121-- [x] Create `lib/shared/presentation/widgets/confirmation_dialog.dart`
2222-- [x] Create `lib/shared/presentation/widgets/options_sheet.dart`
2323-- [x] Create `lib/shared/presentation/helpers/snackbar_helper.dart` - `showAppSnackBar`
2424-- [x] Replace confirmation dialogs
2525-- [x] Replace modal bottom sheets
2626-- [x] Replace SnackBar patterns
2727-- [x] Tests for dialog/sheet/snackbar helpers
2828-2929-## M3 - Theme & Spacing Constants
3030-3131-- [x] Create `lib/core/theme/theme_extensions.dart` - `BuildContext` extension for `colorScheme` access
3232-- [x] Create `lib/core/theme/spacing.dart` with padding/margin constants
3333-- [x] Create `lib/core/theme/color_filters.dart` - extract greyscale matrix
3434-- [x] Refactor files to use new constants/extensions
3535-- [x] Tests for theme extension
3636-3737-## M4 - Widget Extraction
3838-3939-- [x] Create `lib/shared/presentation/widgets/profile_avatar.dart` (configurable size, shape, fallback)
4040-- [x] Create `lib/shared/presentation/widgets/actor_name_widget.dart` (displayName + handle)
4141-- [x] Create `lib/shared/presentation/helpers/notification_icon_mapper.dart`
4242-- [x] Replace avatar patterns
4343-- [x] Replace author name patterns
4444-- [x] Replace notification icon switch
4545-- [x] Widget tests for extracted widgets
4646-4747-## M5 - Navigation & Haptics Helpers
4848-4949-- [ ] Create `lib/shared/presentation/helpers/navigation_helpers.dart` (`navigateToProfile`, `navigateToPost`)
5050-- [ ] Create `lib/shared/presentation/helpers/haptic_helper.dart`
5151-- [ ] Replace navigation patterns in:
5252- - `lib/features/feed/presentation/widgets/post_card.dart`
5353- - `lib/features/feed/presentation/widgets/grid_post_card.dart`
5454- - `lib/features/notifications/presentation/widgets/grouped_notification_list_item.dart`
5555-- [ ] Replace haptic feedback call sites (17 occurrences across 6 files)
5656-- [ ] Tests for navigation helpers
5757-5858-## M6 - Golden Testing Setup
5959-6060-- [ ] Add `golden_toolkit` to dev_dependencies
6161-- [ ] Configure golden test threshold for CI tolerance
6262-- [ ] Add golden tests for shared widgets (M1-M4 extractions)
6363-- [ ] Add golden tests for post card variants (linear, grid)
6464-- [ ] Add golden tests for profile screen states
6565-- [ ] Add multi-device-size goldens for key screens
6666-- [ ] CI pipeline step for golden test comparison
6767-- [ ] Document golden update workflow (`flutter test --update-goldens`)
6868-6969-## M7 - Integration Tests
7070-7171-- [ ] Add `integration_test` to dev_dependencies
7272-- [ ] Auth flow end-to-end test
7373-- [ ] Compose + post flow test
7474-- [ ] Navigation flow test (tab switching, drawer, profile)
7575-- [ ] CI pipeline step for integration tests