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.

fix: compose route handling

+116 -31
+5 -3
docs/TODO.md
··· 6 6 7 7 ## UX 8 8 9 + ### Posts 10 + 11 + - Holding the quote/repost button should show the quote/repost menu (it does so on the thread 12 + screen but not others) 13 + 9 14 ### Composer 10 15 11 16 - Odd behavior when saving drafts: can save draft via the button but hitting cancel 12 17 prompts to save or discard. 13 - - Character count doesn't have "initial state" (completely full) 14 - - Composer is collosal -> We should show drafts on half the screen, with the option 15 - to toggle it closed. 16 18 17 19 ### Dev Tools 18 20
+50 -11
lib/core/router/app_router.dart
··· 59 59 import 'package:lazurite/features/settings/presentation/settings_screen.dart'; 60 60 import 'package:lazurite/features/settings/presentation/video_upload_limits_screen.dart'; 61 61 62 + ComposeRouteArgs parseComposeRouteExtra(Object? extra) { 63 + if (extra is ComposeRouteArgs) { 64 + return extra; 65 + } 66 + 67 + if (extra is Map) { 68 + String? readString(String key) { 69 + final value = extra[key]; 70 + return value is String ? value : null; 71 + } 72 + 73 + int? readInt(String key) { 74 + final value = extra[key]; 75 + if (value is int) { 76 + return value; 77 + } 78 + if (value is String) { 79 + return int.tryParse(value); 80 + } 81 + return null; 82 + } 83 + 84 + return ComposeRouteArgs( 85 + replyParentUri: readString('replyParentUri'), 86 + replyParentCid: readString('replyParentCid'), 87 + replyRootUri: readString('replyRootUri'), 88 + replyRootCid: readString('replyRootCid'), 89 + replyAuthorHandle: readString('replyAuthorHandle'), 90 + quoteUri: readString('quoteUri'), 91 + quoteCid: readString('quoteCid'), 92 + quoteAuthorHandle: readString('quoteAuthorHandle'), 93 + draftId: readInt('draftId'), 94 + initialText: readString('initialText'), 95 + ); 96 + } 97 + 98 + return const ComposeRouteArgs(); 99 + } 100 + 62 101 class AppRouter { 63 102 AppRouter({required this.authBloc, this.navigatorObserver}); 64 103 final AuthBloc authBloc; ··· 95 134 path: '/compose', 96 135 parentNavigatorKey: _rootNavigatorKey, 97 136 builder: (context, state) { 98 - final args = state.extra as ComposeRouteArgs?; 137 + final args = parseComposeRouteExtra(state.extra); 99 138 return BlocProvider( 100 139 create: (_) => ComposeBloc( 101 140 composeRepository: ComposeRepository(bluesky: context.read<Bluesky>()), ··· 103 142 accountDid: context.read<String>(), 104 143 ), 105 144 child: ComposeScreen( 106 - replyParentUri: args?.replyParentUri, 107 - replyParentCid: args?.replyParentCid, 108 - replyRootUri: args?.replyRootUri, 109 - replyRootCid: args?.replyRootCid, 110 - replyAuthorHandle: args?.replyAuthorHandle, 111 - quoteUri: args?.quoteUri, 112 - quoteCid: args?.quoteCid, 113 - quoteAuthorHandle: args?.quoteAuthorHandle, 114 - draftId: args?.draftId, 115 - initialText: args?.initialText, 145 + replyParentUri: args.replyParentUri, 146 + replyParentCid: args.replyParentCid, 147 + replyRootUri: args.replyRootUri, 148 + replyRootCid: args.replyRootCid, 149 + replyAuthorHandle: args.replyAuthorHandle, 150 + quoteUri: args.quoteUri, 151 + quoteCid: args.quoteCid, 152 + quoteAuthorHandle: args.quoteAuthorHandle, 153 + draftId: args.draftId, 154 + initialText: args.initialText, 116 155 ), 117 156 ); 118 157 },
+9 -8
lib/features/feed/presentation/post_thread_screen.dart
··· 11 11 import 'package:intl/intl.dart'; 12 12 import 'package:lazurite/core/logging/app_logger.dart'; 13 13 import 'package:lazurite/features/connectivity/cubit/connectivity_cubit.dart'; 14 + import 'package:lazurite/features/compose/presentation/compose_route_args.dart'; 14 15 import 'package:lazurite/features/feed/cubit/post_action_cubit.dart'; 15 16 import 'package:lazurite/features/feed/cubit/post_thread_cubit.dart'; 16 17 import 'package:lazurite/features/feed/cubit/saved_posts_cubit.dart'; ··· 799 800 800 801 context.push( 801 802 '/compose', 802 - extra: { 803 - 'replyParentUri': post.uri.toString(), 804 - 'replyParentCid': post.cid, 805 - 'replyRootUri': root.$1, 806 - 'replyRootCid': root.$2, 807 - 'replyAuthorHandle': post.author.handle, 808 - }, 803 + extra: ComposeRouteArgs( 804 + replyParentUri: post.uri.toString(), 805 + replyParentCid: post.cid, 806 + replyRootUri: root.$1, 807 + replyRootCid: root.$2, 808 + replyAuthorHandle: post.author.handle, 809 + ), 809 810 ); 810 811 } 811 812 ··· 815 816 816 817 context.push( 817 818 '/compose', 818 - extra: {'quoteUri': post.uri.toString(), 'quoteCid': post.cid, 'quoteAuthorHandle': post.author.handle}, 819 + extra: ComposeRouteArgs(quoteUri: post.uri.toString(), quoteCid: post.cid, quoteAuthorHandle: post.author.handle), 819 820 ); 820 821 } 821 822
+8 -7
lib/features/feed/presentation/widgets/post_card_with_actions.dart
··· 8 8 import 'package:flutter_bloc/flutter_bloc.dart'; 9 9 import 'package:go_router/go_router.dart'; 10 10 import 'package:lazurite/features/connectivity/cubit/connectivity_cubit.dart'; 11 + import 'package:lazurite/features/compose/presentation/compose_route_args.dart'; 11 12 import 'package:lazurite/features/feed/cubit/post_action_cache.dart'; 12 13 import 'package:lazurite/features/feed/cubit/post_action_cubit.dart'; 13 14 import 'package:lazurite/features/feed/cubit/saved_posts_cubit.dart'; ··· 191 192 192 193 context.push( 193 194 '/compose', 194 - extra: { 195 - 'replyParentUri': post.uri.toString(), 196 - 'replyParentCid': post.cid, 197 - 'replyRootUri': rootUri, 198 - 'replyRootCid': rootCid, 199 - 'replyAuthorHandle': post.author.handle, 200 - }, 195 + extra: ComposeRouteArgs( 196 + replyParentUri: post.uri.toString(), 197 + replyParentCid: post.cid, 198 + replyRootUri: rootUri, 199 + replyRootCid: rootCid, 200 + replyAuthorHandle: post.author.handle, 201 + ), 201 202 ); 202 203 } 203 204
+2 -2
lib/features/profile/presentation/follow_audit_screen.dart
··· 13 13 @override 14 14 Widget build(BuildContext context) { 15 15 return Scaffold( 16 - appBar: AppBar(title: const Text('Clean Follows')), 16 + appBar: AppBar(title: const Text('Audit Followers')), 17 17 body: BlocBuilder<FollowAuditCubit, FollowAuditState>( 18 18 builder: (context, state) { 19 19 final visibleEntries = _visibleEntries(state); ··· 145 145 crossAxisAlignment: CrossAxisAlignment.start, 146 146 children: [ 147 147 Text( 148 - 'CLEAN FOLLOWS', 148 + 'AUDIT FOLLOWERS', 149 149 style: Theme.of(context).textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w700, letterSpacing: 1.1), 150 150 ), 151 151 const SizedBox(height: 6),
+41
test/core/router/compose_route_extra_parser_test.dart
··· 1 + import 'package:flutter_test/flutter_test.dart'; 2 + import 'package:lazurite/core/router/app_router.dart'; 3 + import 'package:lazurite/features/compose/presentation/compose_route_args.dart'; 4 + 5 + void main() { 6 + group('parseComposeRouteExtra', () { 7 + test('returns args unchanged when already ComposeRouteArgs', () { 8 + const args = ComposeRouteArgs(replyParentUri: 'at://parent', replyParentCid: 'cid-parent'); 9 + 10 + final parsed = parseComposeRouteExtra(args); 11 + 12 + expect(parsed.replyParentUri, 'at://parent'); 13 + expect(parsed.replyParentCid, 'cid-parent'); 14 + }); 15 + 16 + test('parses legacy map payload used by reply actions', () { 17 + final parsed = parseComposeRouteExtra({ 18 + 'replyParentUri': 'at://parent', 19 + 'replyParentCid': 'cid-parent', 20 + 'replyRootUri': 'at://root', 21 + 'replyRootCid': 'cid-root', 22 + 'replyAuthorHandle': 'alice.bsky.social', 23 + }); 24 + 25 + expect(parsed.replyParentUri, 'at://parent'); 26 + expect(parsed.replyParentCid, 'cid-parent'); 27 + expect(parsed.replyRootUri, 'at://root'); 28 + expect(parsed.replyRootCid, 'cid-root'); 29 + expect(parsed.replyAuthorHandle, 'alice.bsky.social'); 30 + }); 31 + 32 + test('returns empty args for unsupported payload types', () { 33 + final parsed = parseComposeRouteExtra(42); 34 + 35 + expect(parsed.replyParentUri, isNull); 36 + expect(parsed.replyParentCid, isNull); 37 + expect(parsed.quoteUri, isNull); 38 + expect(parsed.initialText, isNull); 39 + }); 40 + }); 41 + }
+1
test/features/profile/presentation/follow_audit_screen_test.dart
··· 78 78 testWidgets('initial state renders Scan button', (tester) async { 79 79 await tester.pumpWidget(_buildSubject(cubit)); 80 80 81 + expect(find.text('Audit Followers'), findsOneWidget); 81 82 expect(find.byKey(const Key('follow_audit_scan_button')), findsOneWidget); 82 83 expect(find.text('Scan'), findsOneWidget); 83 84 });