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.

feat: replace drafts modal with a toggleable panel

+324 -238
-106
docs/BUGS.md
··· 2 2 title: Bugs 3 3 updated: 2026-03-18 4 4 --- 5 - 6 - ## Checklist 7 - 8 - - [x] [1. Draft Save Redundancy — Cancel Prompts After Explicit Save](#1-draft-save-redundancy--cancel-prompts-after-explicit-save) 9 - - [ ] [2. Character Counter — No Initial State](#2-character-counter--no-initial-state) 10 - - [ ] [3. Composer Layout — Drafts Should Be Inline, Not Full-Screen](#3-composer-layout--drafts-should-be-inline-not-full-screen) 11 - 12 - ## 1. Draft Save Redundancy — Cancel Prompts After Explicit Save 13 - 14 - **Status:** Broken — user can save a draft, then immediately be asked to save again on 15 - cancel. 16 - 17 - **Problem:** The AppBar has both a "Save Draft" button (line 502) and a "Cancel" button 18 - (line 497). If the user taps "Save Draft" → draft is saved and a snackbar confirms it. 19 - If the user then taps "Cancel", `_handleBackNavigation` (line 426) checks `hasContent` 20 - (line 430), which is still `true` because the text/media haven't been cleared. The user 21 - is shown a "Save Draft?" dialog even though the draft was just saved moments ago. 22 - 23 - **Fix:** 24 - 25 - - Track whether the current content has been saved since the last edit. Add a 26 - `isDraftDirty` (or `hasUnsavedChanges`) flag to `ComposeState`. 27 - - Set `isDraftDirty: true` when text or media changes (`_onTextChanged`, 28 - `_onMediaChanged`, etc.). 29 - - Set `isDraftDirty: false` after a successful `DraftSaved` event. 30 - - In `_handleBackNavigation`, check `isDraftDirty` instead of (or in addition to) 31 - `hasContent`. If content exists but `isDraftDirty` is `false`, skip the dialog and 32 - pop immediately. 33 - 34 - **Files:** 35 - 36 - - Edit: `lib/features/compose/bloc/compose_state.dart` — add `isDraftDirty` field 37 - (default `true` for new compositions, `false` after draft load) 38 - - Edit: `lib/features/compose/bloc/compose_bloc.dart` — set `isDraftDirty: true` in 39 - `_onTextChanged` (line 62) and media-change handlers; set `isDraftDirty: false` in 40 - `_onDraftSaved` (line 214) and `_onDraftLoaded` (line 241) 41 - - Edit: `lib/features/compose/presentation/compose_screen.dart` — 42 - `_handleBackNavigation` (line 426): gate the dialog on `state.isDraftDirty` rather 43 - than just `hasContent` 44 - 45 - ## 2. Character Counter — No Initial State 46 - 47 - **Status:** Incomplete — counter ring starts empty and invisible until the user types. 48 - 49 - **Problem:** `_CharCounter` (compose_screen.dart, line 895) only shows the remaining 50 - character count text when `count > 0` (line 918). On an empty compose screen the user 51 - sees a bare progress ring at 0% with no text — there is no indication of the 300-character 52 - limit. When loading a draft, the counter jumps from nothing to whatever the draft's count 53 - is, which feels jarring. 54 - 55 - The progress ring itself also starts as just the background circle with no fill, giving 56 - no visual cue about what it represents. 57 - 58 - **Fix:** 59 - 60 - - Always show the remaining count text, even when `count == 0`. Remove the `if (count > 0)` 61 - guard so the counter displays `300` on an empty compose screen. 62 - - This gives users an immediate signal: "you have 300 characters" — matching the behavior 63 - of the official Bluesky app and Twitter/X composer. 64 - 65 - **Files:** 66 - 67 - - Edit: `lib/features/compose/presentation/compose_screen.dart` — `_CharCounter.build` 68 - (line 918): remove the `if (count > 0)` condition so the remaining count is always 69 - visible 70 - 71 - ## 3. Composer Layout — Drafts Should Be Inline, Not Full-Screen 72 - 73 - **Status:** UX issue — drafts open as a modal bottom sheet that covers the composer. 74 - 75 - **Problem:** Tapping the drafts button (line 805) calls `_showDraftsDialog` (line 252), 76 - which opens a `showModalBottomSheet` with a `DraggableScrollableSheet` taking 60–90% of 77 - the screen. This obscures the compose area entirely, breaking the user's context. The 78 - overall composer is also described as "colossal" — the full-screen layout with the modal 79 - drafts on top makes it feel heavy. 80 - 81 - The desired behavior is: drafts should appear inline, sharing the screen with the 82 - compose area, and be toggleable open/closed. 83 - 84 - **Fix:** 85 - 86 - - Replace the `showModalBottomSheet` drafts dialog with an inline, collapsible drafts 87 - panel that sits below the compose text field (or above the bottom toolbar). 88 - - Use an `AnimatedContainer` or `ExpansionTile`-style widget that expands/collapses 89 - when the drafts button is toggled. 90 - - When expanded, the drafts panel should take roughly half the available space, with the 91 - compose text field shrinking to accommodate it. The text field remains visible and 92 - editable above. 93 - - When collapsed, the panel is fully hidden and the compose area reclaims the space. 94 - - Add a toggle state (e.g. `_showDrafts` boolean in the screen's `State`) controlled by 95 - the existing drafts `IconButton` (line 804). 96 - - Keep the same drafts list UI (ListTile with content preview, time, delete button, tap 97 - to load) — just move it from a modal into the inline panel. 98 - 99 - **Files:** 100 - 101 - - Edit: `lib/features/compose/presentation/compose_screen.dart`: 102 - - Add `_showDrafts` state variable to `_ComposeScreenState` 103 - - Replace `_showDraftsDialog()` call on the drafts button (line 805) with a 104 - `setState(() => _showDrafts = !_showDrafts)` toggle 105 - - Add an inline drafts panel widget between the text field / media area and the 106 - bottom toolbar (around line 767), wrapped in an `AnimatedSize` or similar for 107 - smooth expand/collapse 108 - - Remove or repurpose `_showDraftsDialog()` (lines 252-374) — extract the list 109 - content into a reusable `_DraftsPanel` widget used by the inline panel 110 - - Fire `DraftsRequested` event when the panel is opened (same as current behavior)
+134 -130
lib/features/compose/presentation/compose_screen.dart
··· 40 40 class _ComposeScreenState extends State<ComposeScreen> { 41 41 late final _FacetHighlightController _textController; 42 42 final ImagePicker _imagePicker = ImagePicker(); 43 + bool _showDrafts = false; 43 44 44 45 @override 45 46 void initState() { ··· 249 250 } 250 251 } 251 252 252 - Future<void> _showDraftsDialog() async { 253 - final bloc = context.read<ComposeBloc>(); 254 - bloc.add(const DraftsRequested()); 255 - 256 - await showModalBottomSheet<void>( 257 - context: context, 258 - isScrollControlled: true, 259 - builder: (context) => BlocProvider.value( 260 - value: bloc, 261 - child: BlocBuilder<ComposeBloc, ComposeState>( 262 - builder: (context, state) { 263 - if (state.isLoadingDrafts) { 264 - return const SizedBox(height: 200, child: Center(child: CircularProgressIndicator())); 265 - } 266 - 267 - if (state.drafts.isEmpty) { 268 - return const SizedBox(height: 150, child: Center(child: Text('No drafts saved'))); 269 - } 270 - 271 - return DraggableScrollableSheet( 272 - initialChildSize: 0.6, 273 - minChildSize: 0.3, 274 - maxChildSize: 0.9, 275 - expand: false, 276 - builder: (context, scrollController) { 277 - return Column( 278 - children: [ 279 - Container( 280 - padding: const EdgeInsets.all(16), 281 - decoration: BoxDecoration( 282 - border: Border(bottom: BorderSide(color: _theme.dividerColor)), 283 - ), 284 - child: Row( 285 - mainAxisAlignment: MainAxisAlignment.spaceBetween, 286 - children: [ 287 - Text('Drafts', style: _theme.textTheme.titleLarge), 288 - Text( 289 - '${state.drafts.length} draft${state.drafts.length != 1 ? 's' : ''}', 290 - style: Theme.of( 291 - context, 292 - ).textTheme.bodyMedium?.copyWith(color: _theme.colorScheme.onSurfaceVariant), 293 - ), 294 - ], 295 - ), 296 - ), 297 - Expanded( 298 - child: ListView.builder( 299 - controller: scrollController, 300 - itemCount: state.drafts.length, 301 - itemBuilder: (context, index) { 302 - final draft = state.drafts[index]; 303 - return ListTile( 304 - title: Text( 305 - draft.content.isEmpty ? '(No text)' : draft.content, 306 - maxLines: 2, 307 - overflow: TextOverflow.ellipsis, 308 - ), 309 - subtitle: Row( 310 - children: [ 311 - Text(_formatDraftTime(draft.updatedAt), style: _theme.textTheme.bodySmall), 312 - if (draft.scheduledAt != null) ...[ 313 - const SizedBox(width: 8), 314 - Container( 315 - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), 316 - decoration: BoxDecoration( 317 - color: _theme.colorScheme.primaryContainer, 318 - borderRadius: BorderRadius.circular(4), 319 - ), 320 - child: Text( 321 - 'Scheduled', 322 - style: _theme.textTheme.bodySmall?.copyWith( 323 - color: _theme.colorScheme.onPrimaryContainer, 324 - ), 325 - ), 326 - ), 327 - ], 328 - ], 329 - ), 330 - trailing: IconButton( 331 - icon: Icon(Icons.delete_outline, color: _theme.colorScheme.error), 332 - onPressed: () { 333 - final bloc = context.read<ComposeBloc>(); 334 - final theme = _theme; 335 - showDialog<bool>( 336 - context: context, 337 - builder: (dialogContext) => AlertDialog( 338 - title: const Text('Delete Draft?'), 339 - content: const Text('This action cannot be undone.'), 340 - actions: [ 341 - TextButton( 342 - onPressed: () => Navigator.of(dialogContext).pop(false), 343 - child: const Text('Cancel'), 344 - ), 345 - TextButton( 346 - onPressed: () => Navigator.of(dialogContext).pop(true), 347 - child: Text('Delete', style: TextStyle(color: theme.colorScheme.error)), 348 - ), 349 - ], 350 - ), 351 - ).then((confirmed) { 352 - if (confirmed == true && mounted) { 353 - bloc.add(DraftDeleted(draft.id)); 354 - } 355 - }); 356 - }, 357 - ), 358 - onTap: () { 359 - Navigator.pop(context); 360 - context.read<ComposeBloc>().add(DraftLoaded(draft.id)); 361 - }, 362 - ); 363 - }, 364 - ), 365 - ), 366 - ], 367 - ); 368 - }, 369 - ); 370 - }, 371 - ), 372 - ), 373 - ); 253 + void _toggleDrafts() { 254 + final willShow = !_showDrafts; 255 + setState(() => _showDrafts = willShow); 256 + if (willShow) { 257 + context.read<ComposeBloc>().add(const DraftsRequested()); 258 + } 374 259 } 375 260 376 261 String _formatDraftTime(DateTime dateTime) { ··· 405 290 }; 406 291 } 407 292 293 + Widget _buildDraftsPanel() { 294 + return BlocBuilder<ComposeBloc, ComposeState>( 295 + builder: (context, state) { 296 + return Container( 297 + constraints: const BoxConstraints(maxHeight: 280), 298 + decoration: BoxDecoration( 299 + border: Border(top: BorderSide(color: _theme.dividerColor)), 300 + ), 301 + child: Column( 302 + mainAxisSize: MainAxisSize.min, 303 + children: [ 304 + Padding( 305 + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), 306 + child: Row( 307 + mainAxisAlignment: MainAxisAlignment.spaceBetween, 308 + children: [ 309 + Text('Drafts', style: _theme.textTheme.titleMedium), 310 + if (state.drafts.isNotEmpty) 311 + Text( 312 + '${state.drafts.length} draft${state.drafts.length != 1 ? 's' : ''}', 313 + style: _theme.textTheme.bodySmall?.copyWith(color: _theme.colorScheme.onSurfaceVariant), 314 + ), 315 + ], 316 + ), 317 + ), 318 + if (state.isLoadingDrafts) 319 + const Padding( 320 + padding: EdgeInsets.symmetric(vertical: 24), 321 + child: Center(child: CircularProgressIndicator()), 322 + ) 323 + else if (state.drafts.isEmpty) 324 + Padding( 325 + padding: const EdgeInsets.symmetric(vertical: 24), 326 + child: Center( 327 + child: Text('No drafts saved', style: _theme.textTheme.bodyMedium?.copyWith(color: _theme.colorScheme.onSurfaceVariant)), 328 + ), 329 + ) 330 + else 331 + Flexible( 332 + child: ListView.builder( 333 + shrinkWrap: true, 334 + itemCount: state.drafts.length, 335 + itemBuilder: (context, index) { 336 + final draft = state.drafts[index]; 337 + return ListTile( 338 + dense: true, 339 + title: Text( 340 + draft.content.isEmpty ? '(No text)' : draft.content, 341 + maxLines: 2, 342 + overflow: TextOverflow.ellipsis, 343 + ), 344 + subtitle: Row( 345 + children: [ 346 + Text(_formatDraftTime(draft.updatedAt), style: _theme.textTheme.bodySmall), 347 + if (draft.scheduledAt != null) ...[ 348 + const SizedBox(width: 8), 349 + Container( 350 + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), 351 + decoration: BoxDecoration( 352 + color: _theme.colorScheme.primaryContainer, 353 + borderRadius: BorderRadius.circular(4), 354 + ), 355 + child: Text( 356 + 'Scheduled', 357 + style: _theme.textTheme.bodySmall?.copyWith( 358 + color: _theme.colorScheme.onPrimaryContainer, 359 + ), 360 + ), 361 + ), 362 + ], 363 + ], 364 + ), 365 + trailing: IconButton( 366 + icon: Icon(Icons.delete_outline, color: _theme.colorScheme.error), 367 + onPressed: () { 368 + final bloc = context.read<ComposeBloc>(); 369 + final theme = _theme; 370 + showDialog<bool>( 371 + context: context, 372 + builder: (dialogContext) => AlertDialog( 373 + title: const Text('Delete Draft?'), 374 + content: const Text('This action cannot be undone.'), 375 + actions: [ 376 + TextButton( 377 + onPressed: () => Navigator.of(dialogContext).pop(false), 378 + child: const Text('Cancel'), 379 + ), 380 + TextButton( 381 + onPressed: () => Navigator.of(dialogContext).pop(true), 382 + child: Text('Delete', style: TextStyle(color: theme.colorScheme.error)), 383 + ), 384 + ], 385 + ), 386 + ).then((confirmed) { 387 + if (confirmed == true && mounted) { 388 + bloc.add(DraftDeleted(draft.id)); 389 + } 390 + }); 391 + }, 392 + ), 393 + onTap: () { 394 + setState(() => _showDrafts = false); 395 + context.read<ComposeBloc>().add(DraftLoaded(draft.id)); 396 + }, 397 + ); 398 + }, 399 + ), 400 + ), 401 + ], 402 + ), 403 + ); 404 + }, 405 + ); 406 + } 407 + 408 408 ThemeData get _theme => Theme.of(context); 409 409 410 410 void _submitPost() { ··· 764 764 ); 765 765 }, 766 766 ), 767 + AnimatedSize( 768 + duration: const Duration(milliseconds: 200), 769 + curve: Curves.easeInOut, 770 + child: _showDrafts ? _buildDraftsPanel() : const SizedBox.shrink(), 771 + ), 767 772 const SizedBox(height: 8), 768 773 Container( 769 774 decoration: BoxDecoration( ··· 802 807 }, 803 808 ), 804 809 IconButton( 805 - onPressed: _showDraftsDialog, 810 + onPressed: _toggleDrafts, 806 811 icon: Icon(Icons.drive_file_rename_outline, color: _theme.colorScheme.primary), 807 812 tooltip: 'Drafts', 808 813 ), ··· 915 920 return Row( 916 921 mainAxisSize: MainAxisSize.min, 917 922 children: [ 918 - if (count > 0) 919 - Text( 920 - '$remaining', 921 - style: Theme.of( 922 - context, 923 - ).textTheme.bodySmall?.copyWith(color: color, fontFeatures: const [FontFeature.tabularFigures()]), 924 - ), 923 + Text( 924 + '$remaining', 925 + style: Theme.of( 926 + context, 927 + ).textTheme.bodySmall?.copyWith(color: color, fontFeatures: const [FontFeature.tabularFigures()]), 928 + ), 925 929 const SizedBox(width: 8), 926 930 SizedBox( 927 931 width: 28,
+188
test/features/compose/presentation/compose_screen_test.dart
··· 1 + import 'package:bloc_test/bloc_test.dart'; 2 + import 'package:flutter/material.dart'; 3 + import 'package:flutter_bloc/flutter_bloc.dart'; 4 + import 'package:flutter_test/flutter_test.dart'; 5 + import 'package:lazurite/core/database/app_database.dart'; 6 + import 'package:lazurite/features/compose/bloc/compose_bloc.dart'; 7 + import 'package:lazurite/features/compose/presentation/compose_screen.dart'; 8 + import 'package:mocktail/mocktail.dart'; 9 + 10 + class MockComposeBloc extends MockBloc<ComposeEvent, ComposeState> implements ComposeBloc {} 11 + 12 + class FakeDraftsCompanion extends Fake implements DraftsCompanion {} 13 + 14 + DraftEntry _makeDraft({int id = 1, String content = 'Draft'}) => DraftEntry( 15 + id: id, 16 + accountDid: 'did:plc:test', 17 + content: content, 18 + mediaPaths: null, 19 + embedJson: null, 20 + replyUri: null, 21 + replyCid: null, 22 + rootUri: null, 23 + rootCid: null, 24 + createdAt: DateTime(2025, 1, 1), 25 + updatedAt: DateTime(2025, 1, 1), 26 + scheduledAt: null, 27 + ); 28 + 29 + void main() { 30 + late MockComposeBloc mockBloc; 31 + 32 + setUp(() { 33 + registerFallbackValue(FakeDraftsCompanion()); 34 + registerFallbackValue(const TextChanged('')); 35 + mockBloc = MockComposeBloc(); 36 + }); 37 + 38 + tearDown(() => mockBloc.close()); 39 + 40 + Widget buildSubject() => MaterialApp( 41 + home: BlocProvider<ComposeBloc>.value(value: mockBloc, child: const ComposeScreen()), 42 + ); 43 + 44 + void seedState(ComposeState state) { 45 + whenListen(mockBloc, Stream.value(state), initialState: state); 46 + } 47 + 48 + group('ComposeScreen', () { 49 + group('character counter (Bug #2)', () { 50 + testWidgets('shows 300 on empty compose screen', (tester) async { 51 + seedState(const ComposeState.ready()); 52 + 53 + await tester.pumpWidget(buildSubject()); 54 + await tester.pump(); 55 + 56 + expect(find.text('300'), findsOneWidget); 57 + }); 58 + 59 + testWidgets('shows updated remaining count when graphemeCount changes', (tester) async { 60 + seedState(const ComposeState.ready(graphemeCount: 5)); 61 + 62 + await tester.pumpWidget(buildSubject()); 63 + await tester.pump(); 64 + 65 + expect(find.text('295'), findsOneWidget); 66 + }); 67 + 68 + testWidgets('shows negative remaining count when over limit', (tester) async { 69 + seedState(const ComposeState.ready(graphemeCount: 305, isOverLimit: true)); 70 + 71 + await tester.pumpWidget(buildSubject()); 72 + await tester.pump(); 73 + 74 + expect(find.text('-5'), findsOneWidget); 75 + }); 76 + }); 77 + 78 + group('inline drafts panel (Bug #3)', () { 79 + testWidgets('drafts panel is hidden initially', (tester) async { 80 + seedState(const ComposeState.ready()); 81 + 82 + await tester.pumpWidget(buildSubject()); 83 + await tester.pump(); 84 + 85 + expect(find.text('Drafts'), findsNothing); 86 + }); 87 + 88 + testWidgets('tapping drafts button shows inline panel without BottomSheet', (tester) async { 89 + seedState(const ComposeState.ready()); 90 + 91 + await tester.pumpWidget(buildSubject()); 92 + await tester.pump(); 93 + 94 + await tester.tap(find.byIcon(Icons.drive_file_rename_outline)); 95 + await tester.pump(); 96 + await tester.pump(const Duration(milliseconds: 300)); 97 + 98 + expect(find.text('Drafts'), findsOneWidget); 99 + expect(find.byType(BottomSheet), findsNothing); 100 + }); 101 + 102 + testWidgets('tapping drafts button again hides panel', (tester) async { 103 + seedState(const ComposeState.ready()); 104 + 105 + await tester.pumpWidget(buildSubject()); 106 + await tester.pump(); 107 + 108 + await tester.tap(find.byIcon(Icons.drive_file_rename_outline)); 109 + await tester.pump(); 110 + await tester.pump(const Duration(milliseconds: 300)); 111 + expect(find.text('Drafts'), findsOneWidget); 112 + 113 + await tester.tap(find.byIcon(Icons.drive_file_rename_outline)); 114 + await tester.pump(); 115 + await tester.pump(const Duration(milliseconds: 300)); 116 + expect(find.text('Drafts'), findsNothing); 117 + }); 118 + 119 + testWidgets('shows empty state when no drafts loaded', (tester) async { 120 + seedState(const ComposeState.ready().copyWith(drafts: [], isLoadingDrafts: false)); 121 + 122 + await tester.pumpWidget(buildSubject()); 123 + await tester.pump(); 124 + 125 + await tester.tap(find.byIcon(Icons.drive_file_rename_outline)); 126 + await tester.pump(); 127 + await tester.pump(const Duration(milliseconds: 300)); 128 + 129 + expect(find.text('No drafts saved'), findsOneWidget); 130 + }); 131 + 132 + testWidgets('shows draft items when drafts are loaded', (tester) async { 133 + seedState(const ComposeState.ready().copyWith(drafts: [_makeDraft(content: 'My saved draft')], isLoadingDrafts: false)); 134 + 135 + await tester.pumpWidget(buildSubject()); 136 + await tester.pump(); 137 + 138 + await tester.tap(find.byIcon(Icons.drive_file_rename_outline)); 139 + await tester.pump(); 140 + await tester.pump(const Duration(milliseconds: 300)); 141 + 142 + expect(find.text('My saved draft'), findsOneWidget); 143 + }); 144 + 145 + testWidgets('shows loading indicator while drafts are loading', (tester) async { 146 + seedState(const ComposeState.ready().copyWith(isLoadingDrafts: true)); 147 + 148 + await tester.pumpWidget(buildSubject()); 149 + await tester.pump(); 150 + 151 + await tester.tap(find.byIcon(Icons.drive_file_rename_outline)); 152 + await tester.pump(); 153 + await tester.pump(const Duration(milliseconds: 300)); 154 + 155 + expect(find.byType(CircularProgressIndicator), findsOneWidget); 156 + }); 157 + 158 + testWidgets('fires DraftsRequested when panel opens', (tester) async { 159 + seedState(const ComposeState.ready()); 160 + 161 + await tester.pumpWidget(buildSubject()); 162 + await tester.pump(); 163 + 164 + await tester.tap(find.byIcon(Icons.drive_file_rename_outline)); 165 + await tester.pump(); 166 + 167 + verify(() => mockBloc.add(const DraftsRequested())).called(1); 168 + }); 169 + 170 + testWidgets('tapping a draft fires DraftLoaded and closes panel', (tester) async { 171 + seedState(const ComposeState.ready().copyWith(drafts: [_makeDraft(id: 7, content: 'Tap to load me')])); 172 + 173 + await tester.pumpWidget(buildSubject()); 174 + await tester.pump(); 175 + 176 + await tester.tap(find.byIcon(Icons.drive_file_rename_outline)); 177 + await tester.pump(); 178 + await tester.pump(const Duration(milliseconds: 300)); 179 + 180 + await tester.tap(find.text('Tap to load me')); 181 + await tester.pump(); 182 + 183 + verify(() => mockBloc.add(const DraftLoaded(7))).called(1); 184 + expect(find.text('Drafts'), findsNothing); 185 + }); 186 + }); 187 + }); 188 + }
+2 -2
test/features/feed/cubit/saved_posts_cubit_test.dart
··· 550 550 bookmarks: [ 551 551 BookmarkView( 552 552 subject: RepoStrongRef(uri: testUri, cid: 'cid1'), 553 - item: UBookmarkViewItem.unknown(data: {}), 553 + item: const UBookmarkViewItem.unknown(data: {}), 554 554 ), 555 555 ], 556 556 ), ··· 590 590 bookmarks: [ 591 591 BookmarkView( 592 592 subject: RepoStrongRef(uri: testUri, cid: 'cid1'), 593 - item: UBookmarkViewItem.unknown(data: {}), 593 + item: const UBookmarkViewItem.unknown(data: {}), 594 594 ), 595 595 ], 596 596 ),