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

fix: profile, following/follower routing

* add QoL update to just recipes

Changed files
+261 -7
lib
src
app
features
profile
presentation
search
presentation
test
+4 -4
justfile
··· 7 7 flutter analyze 8 8 9 9 # Test with failures only to focus on failures and hanging tests 10 - test-quiet: 11 - flutter test --reporter=failures-only --timeout=90s 10 + test-quiet *paths='': 11 + flutter test {{ paths }} --reporter=failures-only --timeout=90s 12 12 13 13 # Run all tests 14 - test: 15 - flutter test --timeout=90s 14 + test *paths='': 15 + flutter test {{ paths }} --timeout=90s 16 16 17 17 # Run code gen 18 18 gen:
+66
lib/src/app/router.dart
··· 136 136 state: state, 137 137 controller: animationController, 138 138 ), 139 + routes: [ 140 + GoRoute( 141 + path: 'followers', 142 + name: '${AppRouteNames.home}_${AppRouteNames.followers}', 143 + pageBuilder: (context, state) => LazuritePageTransitions.build( 144 + child: FollowersPage(did: Uri.decodeComponent(state.pathParameters['did']!)), 145 + type: LazuriteTransitionType.sharedAxisHorizontal, 146 + state: state, 147 + controller: animationController, 148 + ), 149 + ), 150 + GoRoute( 151 + path: 'following', 152 + name: '${AppRouteNames.home}_${AppRouteNames.following}', 153 + pageBuilder: (context, state) => LazuritePageTransitions.build( 154 + child: FollowingPage(did: Uri.decodeComponent(state.pathParameters['did']!)), 155 + type: LazuriteTransitionType.sharedAxisHorizontal, 156 + state: state, 157 + controller: animationController, 158 + ), 159 + ), 160 + ], 139 161 ), 140 162 ], 141 163 ), ··· 176 198 state: state, 177 199 controller: animationController, 178 200 ), 201 + routes: [ 202 + GoRoute( 203 + path: 'followers', 204 + name: '${AppRouteNames.search}_${AppRouteNames.followers}', 205 + pageBuilder: (context, state) => LazuritePageTransitions.build( 206 + child: FollowersPage(did: Uri.decodeComponent(state.pathParameters['did']!)), 207 + type: LazuriteTransitionType.sharedAxisHorizontal, 208 + state: state, 209 + controller: animationController, 210 + ), 211 + ), 212 + GoRoute( 213 + path: 'following', 214 + name: '${AppRouteNames.search}_${AppRouteNames.following}', 215 + pageBuilder: (context, state) => LazuritePageTransitions.build( 216 + child: FollowingPage(did: Uri.decodeComponent(state.pathParameters['did']!)), 217 + type: LazuriteTransitionType.sharedAxisHorizontal, 218 + state: state, 219 + controller: animationController, 220 + ), 221 + ), 222 + ], 179 223 ), 180 224 ], 181 225 ), ··· 209 253 state: state, 210 254 controller: animationController, 211 255 ), 256 + routes: [ 257 + GoRoute( 258 + path: 'followers', 259 + name: '${AppRouteNames.notifications}_${AppRouteNames.followers}', 260 + pageBuilder: (context, state) => LazuritePageTransitions.build( 261 + child: FollowersPage(did: Uri.decodeComponent(state.pathParameters['did']!)), 262 + type: LazuriteTransitionType.sharedAxisHorizontal, 263 + state: state, 264 + controller: animationController, 265 + ), 266 + ), 267 + GoRoute( 268 + path: 'following', 269 + name: '${AppRouteNames.notifications}_${AppRouteNames.following}', 270 + pageBuilder: (context, state) => LazuritePageTransitions.build( 271 + child: FollowingPage(did: Uri.decodeComponent(state.pathParameters['did']!)), 272 + type: LazuriteTransitionType.sharedAxisHorizontal, 273 + state: state, 274 + controller: animationController, 275 + ), 276 + ), 277 + ], 212 278 ), 213 279 ], 214 280 ),
+24 -2
lib/src/features/profile/presentation/profile_screen.dart
··· 189 189 profile: profile, 190 190 onFollowersPressed: () { 191 191 final encodedDid = Uri.encodeComponent(profile.did); 192 - context.push('/profile/followers/$encodedDid'); 192 + if (widget.isCurrentUser) { 193 + context.push('/profile/followers/$encodedDid'); 194 + } else { 195 + final currentLocation = GoRouter.of(context).routerDelegate.currentConfiguration.uri.path; 196 + String basePath = AppRoutes.home; 197 + if (currentLocation.startsWith(AppRoutes.search)) { 198 + basePath = AppRoutes.search; 199 + } else if (currentLocation.startsWith(AppRoutes.notifications)) { 200 + basePath = AppRoutes.notifications; 201 + } 202 + context.push('$basePath/u/$encodedDid/followers'); 203 + } 193 204 }, 194 205 onFollowingPressed: () { 195 206 final encodedDid = Uri.encodeComponent(profile.did); 196 - context.push('/profile/following/$encodedDid'); 207 + if (widget.isCurrentUser) { 208 + context.push('/profile/following/$encodedDid'); 209 + } else { 210 + final currentLocation = GoRouter.of(context).routerDelegate.currentConfiguration.uri.path; 211 + String basePath = AppRoutes.home; 212 + if (currentLocation.startsWith(AppRoutes.search)) { 213 + basePath = AppRoutes.search; 214 + } else if (currentLocation.startsWith(AppRoutes.notifications)) { 215 + basePath = AppRoutes.notifications; 216 + } 217 + context.push('$basePath/u/$encodedDid/following'); 218 + } 197 219 }, 198 220 followButton: widget.isCurrentUser ? null : _followButton(profile), 199 221 ),
+2 -1
lib/src/features/search/presentation/search_screen.dart
··· 483 483 484 484 return InkWell( 485 485 onTap: () { 486 - GoRouter.of(context).push('/profile/${actor.did}'); 486 + final encodedDid = Uri.encodeComponent(actor.did); 487 + GoRouter.of(context).push('/search/u/$encodedDid'); 487 488 }, 488 489 child: Padding( 489 490 padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
+165
test/src/app/router_test.dart
··· 26 26 import 'package:lazurite/src/features/notifications/application/notifications_providers.dart'; 27 27 import 'package:lazurite/src/features/profile/application/profile_providers.dart'; 28 28 import 'package:lazurite/src/features/profile/infrastructure/profile_repository.dart'; 29 + import 'package:lazurite/src/features/profile/presentation/followers_page.dart'; 30 + import 'package:lazurite/src/features/profile/presentation/following_page.dart'; 31 + import 'package:lazurite/src/features/profile/presentation/profile_screen.dart'; 29 32 import 'package:lazurite/src/features/search/application/search_providers.dart'; 30 33 import 'package:lazurite/src/features/settings/domain/animation_preferences.dart'; 31 34 import 'package:lazurite/src/features/splash/presentation/splash_screen.dart'; ··· 352 355 await tester.pumpAndSettle(); 353 356 354 357 expect(find.byType(DevToolsHomePage), findsOneWidget); 358 + }); 359 + 360 + group('Profile detail routing regression', () { 361 + testWidgets('navigates to profile detail from search tab', (tester) async { 362 + await tester.pumpRouterApp( 363 + overrides: [ 364 + ...getTestOverrides(), 365 + authProvider.overrideWith(() => _TestAuthNotifier(testSession)), 366 + ], 367 + ); 368 + 369 + final app = tester.widget<MaterialApp>(find.byType(MaterialApp)); 370 + final router = app.routerConfig as GoRouter; 371 + 372 + final encodedDid = Uri.encodeComponent('did:plc:test123'); 373 + unawaited(router.push('/search/u/$encodedDid')); 374 + await tester.pumpAndSettle(); 375 + 376 + expect(find.byType(ProfilePage), findsOneWidget); 377 + final profilePage = tester.widget<ProfilePage>(find.byType(ProfilePage)); 378 + expect(profilePage.did, 'did:plc:test123'); 379 + }); 380 + 381 + testWidgets('navigates to profile detail from home tab', (tester) async { 382 + await tester.pumpRouterApp( 383 + overrides: [ 384 + ...getTestOverrides(), 385 + authProvider.overrideWith(() => _TestAuthNotifier(testSession)), 386 + ], 387 + ); 388 + 389 + final app = tester.widget<MaterialApp>(find.byType(MaterialApp)); 390 + final router = app.routerConfig as GoRouter; 391 + 392 + final encodedDid = Uri.encodeComponent('did:plc:test456'); 393 + unawaited(router.push('/home/u/$encodedDid')); 394 + await tester.pumpAndSettle(); 395 + 396 + expect(find.byType(ProfilePage), findsOneWidget); 397 + final profilePage = tester.widget<ProfilePage>(find.byType(ProfilePage)); 398 + expect(profilePage.did, 'did:plc:test456'); 399 + }); 400 + 401 + testWidgets('navigates to profile detail from notifications tab', (tester) async { 402 + await tester.pumpRouterApp( 403 + overrides: [ 404 + ...getTestOverrides(), 405 + authProvider.overrideWith(() => _TestAuthNotifier(testSession)), 406 + ], 407 + ); 408 + 409 + final app = tester.widget<MaterialApp>(find.byType(MaterialApp)); 410 + final router = app.routerConfig as GoRouter; 411 + 412 + final encodedDid = Uri.encodeComponent('did:plc:test789'); 413 + unawaited(router.push('/notifications/u/$encodedDid')); 414 + await tester.pumpAndSettle(); 415 + 416 + expect(find.byType(ProfilePage), findsOneWidget); 417 + final profilePage = tester.widget<ProfilePage>(find.byType(ProfilePage)); 418 + expect(profilePage.did, 'did:plc:test789'); 419 + }); 420 + 421 + testWidgets('navigates to followers from search profile detail', (tester) async { 422 + await tester.pumpRouterApp( 423 + overrides: [ 424 + ...getTestOverrides(), 425 + authProvider.overrideWith(() => _TestAuthNotifier(testSession)), 426 + ], 427 + ); 428 + 429 + final app = tester.widget<MaterialApp>(find.byType(MaterialApp)); 430 + final router = app.routerConfig as GoRouter; 431 + 432 + final encodedDid = Uri.encodeComponent('did:plc:testfollowers'); 433 + unawaited(router.push('/search/u/$encodedDid/followers')); 434 + await tester.pumpAndSettle(); 435 + 436 + expect(find.byType(FollowersPage), findsOneWidget); 437 + final followersPage = tester.widget<FollowersPage>(find.byType(FollowersPage)); 438 + expect(followersPage.did, 'did:plc:testfollowers'); 439 + }); 440 + 441 + testWidgets('navigates to following from home profile detail', (tester) async { 442 + await tester.pumpRouterApp( 443 + overrides: [ 444 + ...getTestOverrides(), 445 + authProvider.overrideWith(() => _TestAuthNotifier(testSession)), 446 + ], 447 + ); 448 + 449 + final app = tester.widget<MaterialApp>(find.byType(MaterialApp)); 450 + final router = app.routerConfig as GoRouter; 451 + 452 + final encodedDid = Uri.encodeComponent('did:plc:testfollowing'); 453 + unawaited(router.push('/home/u/$encodedDid/following')); 454 + await tester.pumpAndSettle(); 455 + 456 + expect(find.byType(FollowingPage), findsOneWidget); 457 + final followingPage = tester.widget<FollowingPage>(find.byType(FollowingPage)); 458 + expect(followingPage.did, 'did:plc:testfollowing'); 459 + }); 460 + 461 + testWidgets('navigates to followers from notifications profile detail', (tester) async { 462 + await tester.pumpRouterApp( 463 + overrides: [ 464 + ...getTestOverrides(), 465 + authProvider.overrideWith(() => _TestAuthNotifier(testSession)), 466 + ], 467 + ); 468 + 469 + final app = tester.widget<MaterialApp>(find.byType(MaterialApp)); 470 + final router = app.routerConfig as GoRouter; 471 + 472 + final encodedDid = Uri.encodeComponent('did:plc:notiffollowers'); 473 + unawaited(router.push('/notifications/u/$encodedDid/followers')); 474 + await tester.pumpAndSettle(); 475 + 476 + expect(find.byType(FollowersPage), findsOneWidget); 477 + final followersPage = tester.widget<FollowersPage>(find.byType(FollowersPage)); 478 + expect(followersPage.did, 'did:plc:notiffollowers'); 479 + }); 480 + 481 + testWidgets('navigates to followers from current user profile', (tester) async { 482 + await tester.pumpRouterApp( 483 + overrides: [ 484 + ...getTestOverrides(), 485 + authProvider.overrideWith(() => _TestAuthNotifier(testSession)), 486 + ], 487 + ); 488 + 489 + final app = tester.widget<MaterialApp>(find.byType(MaterialApp)); 490 + final router = app.routerConfig as GoRouter; 491 + 492 + final encodedDid = Uri.encodeComponent(testSession.did); 493 + unawaited(router.push('/profile/followers/$encodedDid')); 494 + await tester.pumpAndSettle(); 495 + 496 + expect(find.byType(FollowersPage), findsOneWidget); 497 + final followersPage = tester.widget<FollowersPage>(find.byType(FollowersPage)); 498 + expect(followersPage.did, testSession.did); 499 + }); 500 + 501 + testWidgets('navigates to following from current user profile', (tester) async { 502 + await tester.pumpRouterApp( 503 + overrides: [ 504 + ...getTestOverrides(), 505 + authProvider.overrideWith(() => _TestAuthNotifier(testSession)), 506 + ], 507 + ); 508 + 509 + final app = tester.widget<MaterialApp>(find.byType(MaterialApp)); 510 + final router = app.routerConfig as GoRouter; 511 + 512 + final encodedDid = Uri.encodeComponent(testSession.did); 513 + unawaited(router.push('/profile/following/$encodedDid')); 514 + await tester.pumpAndSettle(); 515 + 516 + expect(find.byType(FollowingPage), findsOneWidget); 517 + final followingPage = tester.widget<FollowingPage>(find.byType(FollowingPage)); 518 + expect(followingPage.did, testSession.did); 519 + }); 355 520 }); 356 521 }); 357 522