[READ ONLY MIRROR] Open Source TikTok alternative built on AT Protocol github.com/sprksocial/client
flutter atproto video dart
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

rm snackbars, add alert dialogs for destructive actions

+140 -467
+8 -2
lib/src/core/routing/app_router.dart
··· 116 116 117 117 // Deep linking routes or routes that will be pushed on top of everything 118 118 AutoRoute(page: StandalonePostRoute.page, path: '/post/:postUri'), 119 - AutoRoute(page: StandaloneProfileFeedRoute.page, path: '/profile-feed'), 120 - AutoRoute(page: StandaloneRepostsFeedRoute.page, path: '/reposts-feed'), 121 119 AutoRoute( 122 120 page: ProfileRoute.page, 123 121 path: '/profile/:did', 122 + ), 123 + AutoRoute( 124 + page: StandaloneProfileFeedRoute.page, 125 + path: '/profile/:did/feed', 126 + ), 127 + AutoRoute( 128 + page: StandaloneRepostsFeedRoute.page, 129 + path: '/profile/:did/reposts', 124 130 ), 125 131 AutoRoute(page: UserListRoute.page, path: '/profile/:did/users'), 126 132 AutoRoute(page: VideoReviewRoute.page, path: '/video-review'),
+1 -3
lib/src/features/auth/ui/pages/onboarding_page.dart
··· 98 98 context.router.replaceAll([const MainRoute()]); 99 99 } catch (e) { 100 100 if (!mounted) return; 101 - ScaffoldMessenger.of( 102 - context, 103 - ).showSnackBar(SnackBar(content: Text('Error completing profile: $e'))); 101 + // Error handling - snackbar removed 104 102 } finally { 105 103 if (mounted) { 106 104 setState(() {
-3
lib/src/features/comments/providers/comment_input_provider.dart
··· 81 81 final currentImageCount = state.selectedImages.length; 82 82 if (currentImageCount >= maxImages) { 83 83 if (!context.mounted) return; 84 - ScaffoldMessenger.of(context).showSnackBar( 85 - const SnackBar(content: Text('Replies can only have 1 image.')), 86 - ); 87 84 return; 88 85 } 89 86
+7 -18
lib/src/features/comments/ui/widgets/comment_item.dart
··· 15 15 import 'package:spark/src/core/ui/widgets/options_panel.dart'; 16 16 import 'package:spark/src/core/ui/widgets/report_dialog.dart'; 17 17 import 'package:spark/src/core/ui/widgets/user_avatar.dart'; 18 + import 'package:spark/src/core/utils/utils.dart'; 18 19 import 'package:spark/src/features/comments/providers/comment_provider.dart'; 19 20 import 'package:spark/src/features/comments/providers/comment_state.dart'; 20 21 import 'package:spark/src/features/comments/providers/comments_page_provider.dart'; ··· 34 35 35 36 class _CommentItemState extends ConsumerState<CommentItem> { 36 37 late CommentState commentState; 38 + late final SparkLogger _logger = GetIt.instance<LogService>().getLogger( 39 + 'CommentItem', 40 + ); 37 41 38 42 @override 39 43 void initState() { ··· 51 55 } 52 56 53 57 void _handleReportComment() { 54 - final scaffoldMessenger = ScaffoldMessenger.of(context); 55 58 final sprkRepository = GetIt.instance<SprkRepository>(); 56 59 showDialog( 57 60 context: context, ··· 60 63 postCid: commentState.thread.post.cid, 61 64 onSubmit: (subject, reasonType, reason) async { 62 65 try { 63 - final result = await sprkRepository.repo.createReport( 66 + await sprkRepository.repo.createReport( 64 67 input: ModerationCreateReportInput( 65 68 subject: subject, 66 69 reasonType: reasonType, 67 70 reason: reason, 68 71 ), 69 72 ); 70 - 71 - if (result) { 72 - scaffoldMessenger.showSnackBar( 73 - const SnackBar(content: Text('Report submitted successfully')), 74 - ); 75 - } 76 73 } catch (e) { 77 - scaffoldMessenger.showSnackBar( 78 - SnackBar(content: Text('Error submitting report: $e')), 79 - ); 74 + _logger.e('Error creating report', error: e); 80 75 } 81 76 }, 82 77 ), ··· 84 79 } 85 80 86 81 void _handleDeleteComment() { 87 - final scaffoldMessenger = ScaffoldMessenger.of(context); 88 - 89 82 // Confirm deletion 90 83 showDialog( 91 84 context: context, ··· 114 107 await context.router.maybePop(); // to close the menu below 115 108 } 116 109 } catch (e) { 117 - if (mounted) { 118 - scaffoldMessenger.showSnackBar( 119 - SnackBar(content: Text('Failed to delete comment: $e')), 120 - ); 121 - } 110 + _logger.e('Error deleting comment', error: e); 122 111 } 123 112 }, 124 113 child: Text(AppLocalizations.of(context).buttonDelete),
+2 -27
lib/src/features/feed/ui/widgets/action_buttons/share_panel.dart
··· 41 41 } 42 42 }); 43 43 44 - final theme = Theme.of(context); 45 - ScaffoldMessenger.of(context).showSnackBar( 46 - SnackBar( 47 - content: Row( 48 - children: [ 49 - Icon(Icons.check_circle, color: theme.colorScheme.onPrimary), 50 - const SizedBox(width: 12), 51 - Text(isLink ? 'Video link copied!' : 'Embed code copied!'), 52 - ], 53 - ), 54 - backgroundColor: theme.colorScheme.primary, 55 - behavior: SnackBarBehavior.floating, 56 - width: MediaQuery.of(context).size.width * 0.9, 57 - duration: const Duration(seconds: 2), 58 - ), 59 - ); 44 + // Snackbar removed 60 45 61 46 Future.delayed(const Duration(seconds: 2), () { 62 47 if (mounted) { ··· 242 227 onPressed: (_selectedConvoId == null || _sending) 243 228 ? null 244 229 : () async { 245 - final messenger = ScaffoldMessenger.of(context); 246 230 final navigator = Navigator.of(context); 247 231 248 232 setState(() => _sending = true); ··· 263 247 embed: widget.atUri, 264 248 ); 265 249 266 - messenger.showSnackBar( 267 - const SnackBar( 268 - content: Text('Shared to conversation'), 269 - ), 270 - ); 271 250 navigator.maybePop(); 272 251 } catch (e) { 273 - messenger.showSnackBar( 274 - SnackBar( 275 - content: Text('Failed to share: $e'), 276 - ), 277 - ); 252 + // Error handling - snackbar removed 278 253 logger.d( 279 254 'Failed to share video to conversation', 280 255 error: e,
+33 -51
lib/src/features/feed/ui/widgets/action_buttons/side_action_bar.dart
··· 163 163 _likeCount += wasLiked ? 1 : -1; 164 164 }); 165 165 166 - // Show error to user 167 - if (mounted) { 168 - ScaffoldMessenger.of(context).showSnackBar( 169 - SnackBar( 170 - content: Text('Failed to ${wasLiked ? 'unlike' : 'like'} post: $e'), 171 - ), 172 - ); 173 - } 166 + // Error handling - snackbar removed 174 167 } 175 168 } 176 169 ··· 238 231 _repostCount += wasReposted ? 1 : -1; 239 232 }); 240 233 241 - // Show error to user 242 - if (mounted) { 243 - ScaffoldMessenger.of( 244 - context, 245 - ).showSnackBar( 246 - SnackBar( 247 - content: Text( 248 - 'Failed to ${wasReposted ? 'unrepost' : 'repost'} post: $e', 249 - ), 250 - ), 251 - ); 252 - } 234 + // Error handling - snackbar removed 253 235 } 254 236 } 255 237 ··· 350 332 351 333 Future<void> _handleDeletePost() async { 352 334 final currentPost = _currentPost ?? widget.post; 353 - final scaffoldMessenger = ScaffoldMessenger.of(context); 354 335 final confirmed = await showDialog<bool>( 355 336 context: context, 356 337 builder: (context) => AlertDialog( ··· 392 373 ..invalidate(profileFeedProvider(profileUri, false)) 393 374 ..invalidate(profileFeedProvider(profileUri, true)); 394 375 } 395 - 396 - if (mounted) { 397 - scaffoldMessenger.showSnackBar( 398 - const SnackBar(content: Text('Post deleted')), 399 - ); 400 - } 401 376 } catch (e) { 402 - if (mounted) { 403 - scaffoldMessenger.showSnackBar( 404 - SnackBar(content: Text('Failed to delete post: $e')), 405 - ); 406 - } 377 + // Error handling - snackbar removed 407 378 } 408 379 } 409 380 ··· 412 383 final author = currentPost.author; 413 384 final wasBlocked = isBlocking(author.viewer); 414 385 386 + // Show confirmation dialog 387 + final confirmed = await showDialog<bool>( 388 + context: context, 389 + builder: (context) => AlertDialog( 390 + title: Text(wasBlocked ? 'Unblock User' : 'Block User'), 391 + content: Text( 392 + wasBlocked 393 + ? 'Are you sure you want to unblock this user?' 394 + : 'Are you sure you want to block this user? ' 395 + 'You will no longer see their posts.', 396 + ), 397 + actions: [ 398 + TextButton( 399 + onPressed: () => Navigator.of(context).pop(false), 400 + child: const Text('Cancel'), 401 + ), 402 + TextButton( 403 + onPressed: () => Navigator.of(context).pop(true), 404 + style: TextButton.styleFrom( 405 + foregroundColor: wasBlocked ? null : Colors.red, 406 + ), 407 + child: Text(wasBlocked ? 'Unblock' : 'Block'), 408 + ), 409 + ], 410 + ), 411 + ); 412 + 413 + if (confirmed != true) return; 414 + 415 415 try { 416 416 final graphRepository = GetIt.instance<SprkRepository>().graph; 417 417 await graphRepository.toggleBlock( ··· 419 419 author.viewer?.blocking, 420 420 ); 421 421 422 - if (mounted) { 423 - ScaffoldMessenger.of(context).showSnackBar( 424 - SnackBar( 425 - content: Text(wasBlocked ? 'User unblocked' : 'User blocked'), 426 - backgroundColor: Colors.green, 427 - ), 428 - ); 429 - } 430 - 431 422 // If blocking and we have a feed, use the action controller to advance 432 423 if (!wasBlocked && widget.feed != null) { 433 424 final controller = ref.read( ··· 436 427 controller?.onAdvanceAndRemove(); 437 428 } 438 429 } catch (e) { 439 - if (mounted) { 440 - ScaffoldMessenger.of(context).showSnackBar( 441 - SnackBar( 442 - content: Text( 443 - 'Failed to ${wasBlocked ? 'unblock' : 'block'} user: $e', 444 - ), 445 - backgroundColor: Colors.red, 446 - ), 447 - ); 448 - } 430 + // Error handling - snackbar removed 449 431 } 450 432 } 451 433
+21 -19
lib/src/features/feed/ui/widgets/images/image_carousel.dart
··· 158 158 ), 159 159 Positioned( 160 160 // Position dots above the post overlay content area 161 - // When known interactions exist, they add ~48px (bar height + 12px spacing) 161 + // When known interactions exist, they add ~48px (bar height + 12px) 162 162 // Base position is 180px, reduced by 48px when no known interactions 163 163 bottom: widget.hasKnownInteractions ? 180 : 132, 164 164 left: 0, ··· 193 193 class _ScrollingDotIndicatorState extends State<_ScrollingDotIndicator> 194 194 with SingleTickerProviderStateMixin { 195 195 static const int _maxVisibleDots = 5; 196 - static const double _dotSize = 6.0; 197 - static const double _dotSpacing = 4.0; 196 + static const double _dotSize = 6; 197 + static const double _dotSpacing = 4; 198 198 static const double _dotTotalWidth = _dotSize + _dotSpacing; 199 199 200 200 // Dot positions (indices 0-4 for 5 dots) ··· 281 281 final newOffset = _calculateScrollOffset(index); 282 282 if (newOffset != _targetScrollOffset) { 283 283 // Get the current visual position - use the animation value if animating, 284 - // otherwise use the stored offset. This prevents jumps when new animations 284 + // otherwise use the stored offset to prevent jumps when new animations 285 285 // start while previous ones are still in progress. 286 286 final currentVisualPosition = _controller.isAnimating 287 287 ? _scrollAnimation.value ··· 291 291 // This ensures continuity when rapid swipes occur 292 292 _currentScrollOffset = currentVisualPosition; 293 293 294 - _scrollAnimation = Tween<double>( 295 - begin: currentVisualPosition, 296 - end: newOffset, 297 - ).animate( 298 - CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic), 299 - ); 294 + _scrollAnimation = 295 + Tween<double>( 296 + begin: currentVisualPosition, 297 + end: newOffset, 298 + ).animate( 299 + CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic), 300 + ); 300 301 _targetScrollOffset = newOffset; 301 302 _controller 302 303 ..reset() ··· 323 324 child: AnimatedBuilder( 324 325 animation: _scrollAnimation, 325 326 builder: (context, child) { 326 - final scrollOffset = 327 - _controller.isAnimating 328 - ? _scrollAnimation.value 329 - : _targetScrollOffset; 327 + final scrollOffset = _controller.isAnimating 328 + ? _scrollAnimation.value 329 + : _targetScrollOffset; 330 330 331 331 return SizedBox( 332 332 width: visibleWidth, ··· 406 406 if (relativePosition < edgeZone) { 407 407 if (relativePosition < 0) { 408 408 // Dot is outside visible area (entering/exiting on left) 409 - // Scale from 0 (at -1) to edgeScale or 1.0 (at 0) depending on hasMoreBefore 409 + // Scale from 0 (at -1) to edgeScale or 1 (at 0) 410 + // depending on hasMoreBefore 410 411 final targetScale = hasMoreBefore ? edgeScale : 1.0; 411 412 return ((1 + relativePosition) * targetScale).clamp(0.0, 1.0); 412 413 } else if (hasMoreBefore) { ··· 418 419 } 419 420 420 421 // Right side: dots entering/exiting or at edge 421 - final rightEdgeStart = _maxVisibleDots - 1 - edgeZone; 422 + const rightEdgeStart = _maxVisibleDots - 1 - edgeZone; 422 423 if (relativePosition > rightEdgeStart) { 423 424 final distanceFromRight = _maxVisibleDots - 1 - relativePosition; 424 425 425 426 if (relativePosition > _maxVisibleDots - 1) { 426 427 // Dot is outside visible area (entering/exiting on right) 427 - // Scale from 0 (at _maxVisibleDots) to edgeScale or 1.0 (at _maxVisibleDots-1) 428 + // Scale from 0 (at _maxVisibleDots) 429 + // to edgeScale or 1 (at _maxVisibleDots-1) 428 430 final targetScale = hasMoreAfter ? edgeScale : 1.0; 429 431 return ((1 + distanceFromRight) * targetScale).clamp(0.0, 1.0); 430 432 } else if (hasMoreAfter) { 431 433 // Dot is in the right edge zone with more dots after 432 - // Scale from edgeScale (at _maxVisibleDots-1) to 1.0 (at rightEdgeStart) 434 + // Scale from edgeScale (at _maxVisibleDots-1) to 1 (at rightEdgeStart) 433 435 return (edgeScale + (distanceFromRight / edgeZone) * (1 - edgeScale)) 434 436 .clamp(0.0, 1.0); 435 437 } 436 438 } 437 439 438 440 // Middle dots are full size 439 - return 1.0; 441 + return 1; 440 442 } 441 443 } 442 444
+1 -5
lib/src/features/messages/ui/pages/chat_page.dart
··· 63 63 ); 64 64 await chatService.sendMessage(widget.conversationId, content); 65 65 } catch (e) { 66 - if (mounted) { 67 - ScaffoldMessenger.of( 68 - context, 69 - ).showSnackBar(SnackBar(content: Text('Failed to send message: $e'))); 70 - } 66 + // Error handling - snackbar removed 71 67 } 72 68 } 73 69
+1 -3
lib/src/features/messages/ui/pages/new_chat_search_page.dart
··· 200 200 ); 201 201 } catch (e) { 202 202 if (!mounted) return; 203 - ScaffoldMessenger.of( 204 - context, 205 - ).showSnackBar(SnackBar(content: Text('Unable to start chat: $e'))); 203 + // Error handling - snackbar removed 206 204 } 207 205 } 208 206
+1 -16
lib/src/features/posting/ui/pages/image_review_page.dart
··· 104 104 }); 105 105 } catch (e) { 106 106 if (!mounted) return; 107 - ScaffoldMessenger.of( 108 - context, 109 - ).showSnackBar( 110 - SnackBar( 111 - content: Text('Failed to select images: $e'), 112 - backgroundColor: Colors.red, 113 - ), 114 - ); 107 + // Error handling - snackbar removed 115 108 } 116 109 } 117 110 ··· 156 149 setState(() { 157 150 _isPosting = false; 158 151 }); 159 - ScaffoldMessenger.of( 160 - context, 161 - ).showSnackBar( 162 - SnackBar( 163 - content: Text('Failed to create post: $e'), 164 - backgroundColor: Colors.red, 165 - ), 166 - ); 167 152 } 168 153 return null; 169 154 }
-17
lib/src/features/posting/ui/pages/recording_page.dart
··· 41 41 final recordingState = ref.read(recordingProvider); 42 42 43 43 if (cameraAsync.hasError) { 44 - _showError('Camera error: ${cameraAsync.error}'); 45 44 return; 46 45 } 47 46 ··· 49 48 if (cameraState == null || 50 49 !cameraState.isInitialized || 51 50 cameraState.controller == null) { 52 - _showError('Camera not ready'); 53 51 return; 54 52 } 55 53 ··· 69 67 final success = await cameraNotifier.startVideoRecording(); 70 68 if (success) { 71 69 recordingNotifier.startRecording(); 72 - } else { 73 - _showError('Failed to start recording'); 74 70 } 75 71 } 76 72 ··· 93 89 setState(() { 94 90 _isProcessing = false; 95 91 }); 96 - _showError('Failed to stop recording'); 97 92 return; 98 93 } 99 94 ··· 145 140 setState(() { 146 141 _isProcessing = false; 147 142 }); 148 - _showError('Failed to process video: $e'); 149 143 } 150 144 } 151 145 } ··· 155 149 await cameraNotifier.flipCamera(); 156 150 } 157 151 158 - void _showError(String message) { 159 - if (!mounted) return; 160 - ScaffoldMessenger.of(context).showSnackBar( 161 - SnackBar( 162 - content: Text(message), 163 - backgroundColor: Colors.red, 164 - ), 165 - ); 166 - } 167 - 168 152 @override 169 153 Widget build(BuildContext context) { 170 154 final cameraAsync = ref.watch(cameraProvider); ··· 239 223 maxDuration: recordingState.maxDuration, 240 224 onBack: () { 241 225 if (recordingState.isRecording) { 242 - _showError('Please stop recording before going back'); 243 226 return; 244 227 } 245 228 context.router.pop();
-18
lib/src/features/posting/ui/pages/video_review_page.dart
··· 115 115 ..invalidate(profileFeedProvider(AtUri.parse('at://$did'), true)); 116 116 } 117 117 if (postRef == null) { 118 - ScaffoldMessenger.of(context).showSnackBar( 119 - const SnackBar( 120 - content: Text('Failed to post video. Please try again.'), 121 - ), 122 - ); 123 118 return; 124 119 } else { 125 - ScaffoldMessenger.of(context).showSnackBar( 126 - const SnackBar(content: Text('Video posted successfully!')), 127 - ); 128 120 if (!widget.storyMode) { 129 121 context.router.push( 130 122 StandalonePostRoute(postUri: postRef.uri.toString()), ··· 137 129 setState(() { 138 130 _isPosting = false; 139 131 }); 140 - 141 - // Show error without blocking UI 142 - ScaffoldMessenger.of( 143 - context, 144 - ).showSnackBar( 145 - SnackBar( 146 - content: Text('Failed to upload video: $e'), 147 - backgroundColor: Colors.red, 148 - ), 149 - ); 150 132 } 151 133 } 152 134 return;
+1 -7
lib/src/features/profile/ui/pages/edit_profile_page.dart
··· 77 77 78 78 if (!mounted) return; 79 79 80 - ScaffoldMessenger.of(context).showSnackBar( 81 - const SnackBar(content: Text('Profile updated successfully!')), 82 - ); 83 - 84 80 // Go back to previous screen 85 81 context.router.pop(); 86 82 } catch (e) { 87 83 if (!mounted) return; 88 - ScaffoldMessenger.of( 89 - context, 90 - ).showSnackBar(SnackBar(content: Text('Error updating profile: $e'))); 84 + // Error handling - snackbar removed 91 85 } finally { 92 86 if (mounted) { 93 87 setState(() {
+42 -126
lib/src/features/profile/ui/pages/profile_page.dart
··· 17 17 import 'package:spark/src/core/ui/widgets/options_panel.dart'; 18 18 import 'package:spark/src/core/ui/widgets/report_dialog.dart'; 19 19 import 'package:spark/src/core/utils/blocking_utils.dart'; 20 - import 'package:spark/src/core/utils/error_messages.dart'; 21 20 import 'package:spark/src/core/utils/logging/log_service.dart'; 22 21 import 'package:spark/src/core/utils/logging/logger.dart'; 23 22 import 'package:spark/src/core/utils/text_formatter.dart'; ··· 271 270 ) { 272 271 if (updated == true) { 273 272 notifier.refreshProfile(); 274 - if (context.mounted) { 275 - ScaffoldMessenger.of(context).showSnackBar( 276 - const SnackBar( 277 - content: Text('Profile updated successfully'), 278 - ), 279 - ); 280 - } 281 273 } 282 274 }); 283 275 }, 284 276 onFollowTap: () async { 285 277 try { 286 278 await notifier.toggleFollow(); 287 - final latestProfileState = ref 288 - .read(profileProvider(did: widget.did)) 289 - .asData 290 - ?.value; 291 - 292 - if (latestProfileState != null && 293 - !latestProfileState.showAuthPrompt) { 294 - if (context.mounted) { 295 - ScaffoldMessenger.of(context).showSnackBar( 296 - const SnackBar( 297 - content: Text('Followed successfully'), 298 - backgroundColor: Colors.green, 299 - ), 300 - ); 301 - } 302 - } 303 279 } catch (e) { 304 - if (context.mounted) { 305 - ScaffoldMessenger.of(context).showSnackBar( 306 - SnackBar( 307 - content: Text( 308 - ErrorMessages.getOperationErrorMessage('follow', e), 309 - ), 310 - backgroundColor: Colors.red, 311 - ), 312 - ); 313 - } 280 + _logger.e('Error unfollowing profile', error: e); 314 281 } 315 282 }, 316 283 onUnfollowTap: () async { 317 284 try { 318 285 await notifier.toggleFollow(); 319 - final latestProfileState = ref 320 - .read(profileProvider(did: widget.did)) 321 - .asData 322 - ?.value; 323 - 324 - if (latestProfileState != null && 325 - !latestProfileState.showAuthPrompt) { 326 - if (context.mounted) { 327 - ScaffoldMessenger.of(context).showSnackBar( 328 - const SnackBar( 329 - content: Text('Unfollowed successfully'), 330 - backgroundColor: Colors.green, 331 - ), 332 - ); 333 - } 334 - } 335 286 } catch (e) { 336 - if (context.mounted) { 337 - ScaffoldMessenger.of(context).showSnackBar( 338 - SnackBar( 339 - content: Text( 340 - ErrorMessages.getOperationErrorMessage('unfollow', e), 341 - ), 342 - backgroundColor: Colors.red, 343 - ), 344 - ); 345 - } 287 + _logger.e('Error unfollowing profile', error: e); 346 288 } 347 289 }, 348 290 onUnblockTap: () async { 349 291 try { 350 292 await notifier.toggleBlock(); 351 - if (context.mounted) { 352 - ScaffoldMessenger.of(context).showSnackBar( 353 - const SnackBar( 354 - content: Text('User unblocked'), 355 - backgroundColor: Colors.green, 356 - ), 357 - ); 358 - } 359 293 } catch (e) { 360 - if (context.mounted) { 361 - ScaffoldMessenger.of(context).showSnackBar( 362 - SnackBar( 363 - content: Text( 364 - ErrorMessages.getOperationErrorMessage('unblock', e), 365 - ), 366 - backgroundColor: Colors.red, 367 - ), 368 - ); 369 - } 294 + _logger.e('Error unblocking profile', error: e); 370 295 } 371 296 }, 372 297 onShareTap: () => ··· 401 326 postCid: profile.did, 402 327 onSubmit: (subject, reasonType, reason) async { 403 328 try { 404 - final success = await notifier.createReport( 329 + await notifier.createReport( 405 330 did: profile.did, 406 331 reasonType: reasonType, 407 332 reason: reason, 408 333 ); 409 - if (success && context.mounted) { 410 - ScaffoldMessenger.of(context).showSnackBar( 411 - const SnackBar( 412 - content: Text( 413 - 'Report submitted successfully', 414 - ), 415 - ), 416 - ); 417 - } 418 334 } catch (e) { 419 - if (context.mounted) { 420 - ScaffoldMessenger.of(context).showSnackBar( 421 - SnackBar( 422 - content: Text( 423 - ErrorMessages.getOperationErrorMessage( 424 - 'report', 425 - e, 426 - ), 427 - ), 428 - ), 429 - ); 430 - } 335 + _logger.e('Error creating report', error: e); 431 336 } 432 337 }, 433 338 ), 434 339 ), 435 340 onBlock: () async { 341 + final wasBlocked = isBlocking(profile.viewer); 342 + 343 + // Show confirmation dialog 344 + final confirmed = await showDialog<bool>( 345 + context: context, 346 + builder: (context) => AlertDialog( 347 + title: Text( 348 + wasBlocked ? 'Unblock User' : 'Block User', 349 + ), 350 + content: Text( 351 + wasBlocked 352 + ? 'Are you sure you want to unblock this user?' 353 + : 'Are you sure you want to block this user? ' 354 + 'You will no longer see their posts.', 355 + ), 356 + actions: [ 357 + TextButton( 358 + onPressed: () => Navigator.of(context).pop(false), 359 + child: const Text('Cancel'), 360 + ), 361 + TextButton( 362 + onPressed: () => Navigator.of(context).pop(true), 363 + style: TextButton.styleFrom( 364 + foregroundColor: wasBlocked ? null : Colors.red, 365 + ), 366 + child: Text(wasBlocked ? 'Unblock' : 'Block'), 367 + ), 368 + ], 369 + ), 370 + ); 371 + 372 + if (confirmed != true) return; 373 + 436 374 try { 437 - final wasBlocked = isBlocking(profile.viewer); 438 375 await notifier.toggleBlock(); 439 - if (context.mounted) { 440 - ScaffoldMessenger.of(context).showSnackBar( 441 - SnackBar( 442 - content: Text( 443 - wasBlocked ? 'User unblocked' : 'User blocked', 444 - ), 445 - backgroundColor: Colors.green, 446 - ), 447 - ); 448 - } 449 376 } catch (e) { 450 - if (context.mounted) { 451 - ScaffoldMessenger.of(context).showSnackBar( 452 - SnackBar( 453 - content: Text( 454 - ErrorMessages.getOperationErrorMessage( 455 - isBlocking(profile.viewer) 456 - ? 'unblock' 457 - : 'block', 458 - e, 459 - ), 460 - ), 461 - backgroundColor: Colors.red, 462 - ), 463 - ); 464 - } 377 + _logger.e( 378 + 'Error blocking/unblocking profile', 379 + error: e, 380 + ); 465 381 } 466 382 }, 467 383 isBlocked: isBlocking(profile.viewer),
+7 -5
lib/src/features/profile/ui/pages/standalone_profile_feed_page.dart
··· 16 16 @RoutePage() 17 17 class StandaloneProfileFeedPage extends ConsumerStatefulWidget { 18 18 const StandaloneProfileFeedPage({ 19 - required this.profileUri, 19 + @PathParam('did') required this.did, 20 20 required this.videosOnly, 21 21 required this.initialPostIndex, 22 22 super.key, 23 23 }); 24 - final String profileUri; 24 + final String did; 25 25 final bool videosOnly; 26 26 final int initialPostIndex; 27 27 ··· 40 40 @override 41 41 void initState() { 42 42 super.initState(); 43 - profileAtUri = AtUri.parse(widget.profileUri); 43 + profileAtUri = AtUri.parse('at://${widget.did}'); 44 44 _currentIndex = widget.initialPostIndex; 45 45 pageController = PageController(initialPage: widget.initialPostIndex); 46 46 } ··· 57 57 _hasInitializedIndex = true; 58 58 WidgetsBinding.instance.addPostFrameCallback((_) { 59 59 ref 60 - .read(profileFeedIndexProvider(widget.profileUri).notifier) 60 + .read(profileFeedIndexProvider(profileAtUri.toString()).notifier) 61 61 .setIndex(widget.initialPostIndex); 62 62 }); 63 63 } ··· 99 99 }); 100 100 ref 101 101 .read( 102 - profileFeedIndexProvider(widget.profileUri).notifier, 102 + profileFeedIndexProvider( 103 + profileAtUri.toString(), 104 + ).notifier, 103 105 ) 104 106 .setIndex(index); 105 107 // Load more posts when approaching the end
+9 -9
lib/src/features/profile/ui/pages/standalone_reposts_feed_page.dart
··· 16 16 @RoutePage() 17 17 class StandaloneRepostsFeedPage extends ConsumerStatefulWidget { 18 18 const StandaloneRepostsFeedPage({ 19 - required this.actor, 19 + @PathParam('did') required this.did, 20 20 required this.initialPostIndex, 21 21 super.key, 22 22 }); 23 - final String actor; 23 + final String did; 24 24 final int initialPostIndex; 25 25 26 26 @override ··· 53 53 _hasInitializedIndex = true; 54 54 WidgetsBinding.instance.addPostFrameCallback((_) { 55 55 ref 56 - .read(profileFeedIndexProvider('reposts:${widget.actor}').notifier) 56 + .read(profileFeedIndexProvider('reposts:${widget.did}').notifier) 57 57 .setIndex(widget.initialPostIndex); 58 58 }); 59 59 } 60 60 61 - final repostsState = ref.watch(profileRepostsProvider(widget.actor)); 61 + final repostsState = ref.watch(profileRepostsProvider(widget.did)); 62 62 final bottomPadding = MediaQuery.of(context).padding.bottom; 63 63 64 64 return Scaffold( ··· 94 94 ref 95 95 .read( 96 96 profileFeedIndexProvider( 97 - 'reposts:${widget.actor}', 97 + 'reposts:${widget.did}', 98 98 ).notifier, 99 99 ) 100 100 .setIndex(index); ··· 102 102 if (index >= filteredUris.length - 3 && 103 103 !state.isEndOfNetwork) { 104 104 ref 105 - .read(profileRepostsProvider(widget.actor).notifier) 105 + .read(profileRepostsProvider(widget.did).notifier) 106 106 .loadMore(); 107 107 } 108 108 }, 109 109 itemBuilder: (context, index) { 110 110 final postUri = filteredUris[index]; 111 111 final post = state.postViews[postUri]; 112 - // Create a profile URI from the actor for the post widget 113 - final profileUri = AtUri.parse('at://${widget.actor}'); 112 + // Create a profile URI from the did for the post widget 113 + final profileUri = AtUri.parse('at://${widget.did}'); 114 114 return ProfileFeedPostWidget( 115 115 postUri: postUri, 116 116 profileUri: profileUri, ··· 143 143 ElevatedButton( 144 144 onPressed: () { 145 145 ref 146 - .read(profileRepostsProvider(widget.actor).notifier) 146 + .read(profileRepostsProvider(widget.did).notifier) 147 147 .refresh(); 148 148 }, 149 149 child: const Text('Retry'),
+1 -1
lib/src/features/profile/ui/widgets/profile_grid_tab.dart
··· 26 26 if (postIndex != -1) { 27 27 context.router.push( 28 28 StandaloneProfileFeedRoute( 29 - profileUri: profileUri.toString(), 29 + did: profileUri.hostname, 30 30 videosOnly: false, 31 31 initialPostIndex: postIndex, 32 32 ),
+1 -1
lib/src/features/profile/ui/widgets/profile_reposts_tab.dart
··· 30 30 if (postIndex != -1) { 31 31 context.router.push( 32 32 StandaloneRepostsFeedRoute( 33 - actor: actor, 33 + did: actor, 34 34 initialPostIndex: postIndex, 35 35 ), 36 36 );
+4 -41
lib/src/features/settings/ui/pages/feed_list_page.dart
··· 106 106 .read(settingsProvider.notifier) 107 107 .reorderFeed(actualOldIndex, actualNewIndex); 108 108 } catch (e) { 109 - if (context.mounted) { 110 - ScaffoldMessenger.of(context).showSnackBar( 111 - SnackBar(content: Text('Failed to reorder feeds: $e')), 112 - ); 113 - } 109 + // Error handling - snackbar removed 114 110 } 115 111 }, 116 112 proxyDecorator: (child, index, animation) { ··· 172 168 await ref 173 169 .read(settingsProvider.notifier) 174 170 .removeFeed(feed); 175 - if (context.mounted) { 176 - ScaffoldMessenger.of(context).showSnackBar( 177 - const SnackBar(content: Text('Feed removed')), 178 - ); 179 - } 180 171 } catch (e) { 181 - if (context.mounted) { 182 - ScaffoldMessenger.of(context).showSnackBar( 183 - SnackBar( 184 - content: Text('Failed to remove feed: $e'), 185 - ), 186 - ); 187 - } 172 + // Error handling - snackbar removed 188 173 } 189 174 } 190 175 : null, ··· 230 215 await ref 231 216 .read(settingsProvider.notifier) 232 217 .likeFeed(feed); 233 - if (context.mounted) { 234 - ScaffoldMessenger.of(context).showSnackBar( 235 - const SnackBar(content: Text('Feed liked')), 236 - ); 237 - } 238 218 } catch (e) { 239 - if (context.mounted) { 240 - ScaffoldMessenger.of(context).showSnackBar( 241 - SnackBar( 242 - content: Text('Failed to like feed: $e'), 243 - ), 244 - ); 245 - } 219 + // Error handling - snackbar removed 246 220 } 247 221 } 248 222 : null, ··· 252 226 await ref 253 227 .read(settingsProvider.notifier) 254 228 .unlikeFeed(feed); 255 - if (context.mounted) { 256 - ScaffoldMessenger.of(context).showSnackBar( 257 - const SnackBar(content: Text('Feed unliked')), 258 - ); 259 - } 260 229 } catch (e) { 261 - if (context.mounted) { 262 - ScaffoldMessenger.of(context).showSnackBar( 263 - SnackBar( 264 - content: Text('Failed to unlike feed: $e'), 265 - ), 266 - ); 267 - } 230 + // Error handling - snackbar removed 268 231 } 269 232 } 270 233 : null,
-5
lib/src/features/settings/ui/pages/labeler_label_settings_page.dart
··· 289 289 } 290 290 } catch (e) { 291 291 _logger.e('Error updating label preference: $e'); 292 - if (mounted) { 293 - ScaffoldMessenger.of(context).showSnackBar( 294 - SnackBar(content: Text('Failed to update preference: $e')), 295 - ); 296 - } 297 292 } 298 293 } 299 294
-46
lib/src/features/settings/ui/pages/labeler_management_page.dart
··· 66 66 _logger.d('Loaded ${labelerDids.length} labelers'); 67 67 } catch (e) { 68 68 _logger.e('Error loading labelers: $e'); 69 - if (mounted) { 70 - ScaffoldMessenger.of(context).showSnackBar( 71 - SnackBar(content: Text('Failed to load labelers: $e')), 72 - ); 73 - } 74 69 setState(() => _isLoading = false); 75 70 } 76 71 } ··· 154 149 // If it's a handle, try to resolve it to a DID 155 150 if (did.startsWith('@')) { 156 151 // TODO: add handle resolution in the future 157 - if (mounted) { 158 - ScaffoldMessenger.of(context).showSnackBar( 159 - const SnackBar(content: Text('Please enter a DID (did:plc:...)')), 160 - ); 161 - } 162 152 return; 163 153 } 164 154 165 155 // Validate DID format 166 156 if (!did.startsWith('did:')) { 167 - if (mounted) { 168 - ScaffoldMessenger.of(context).showSnackBar( 169 - const SnackBar( 170 - content: Text('Invalid DID format. Must start with did:'), 171 - ), 172 - ); 173 - } 174 157 return; 175 158 } 176 159 ··· 179 162 180 163 // Refresh the list 181 164 await _loadLabelers(); 182 - 183 - if (mounted) { 184 - ScaffoldMessenger.of(context).showSnackBar( 185 - const SnackBar(content: Text('Labeler added successfully')), 186 - ); 187 - } 188 165 } catch (e) { 189 166 _logger.e('Error adding labeler: $e'); 190 - if (mounted) { 191 - ScaffoldMessenger.of(context).showSnackBar( 192 - SnackBar(content: Text('Failed to add labeler: $e')), 193 - ); 194 - } 195 167 } finally { 196 168 didController.dispose(); 197 169 } ··· 199 171 200 172 Future<void> _removeLabeler(String did) async { 201 173 if (did == _defaultModServiceDid) { 202 - if (mounted) { 203 - ScaffoldMessenger.of(context).showSnackBar( 204 - const SnackBar( 205 - content: Text('Cannot remove the default mod service labeler'), 206 - ), 207 - ); 208 - } 209 174 return; 210 175 } 211 176 ··· 235 200 236 201 // Refresh the list 237 202 await _loadLabelers(); 238 - 239 - if (mounted) { 240 - ScaffoldMessenger.of(context).showSnackBar( 241 - const SnackBar(content: Text('Labeler removed successfully')), 242 - ); 243 - } 244 203 } catch (e) { 245 204 _logger.e('Error removing labeler: $e'); 246 - if (mounted) { 247 - ScaffoldMessenger.of(context).showSnackBar( 248 - SnackBar(content: Text('Failed to remove labeler: $e')), 249 - ); 250 - } 251 205 } 252 206 } 253 207
-39
lib/src/features/settings/ui/pages/profile_settings_page.dart
··· 45 45 // Close loading dialog if it's open 46 46 if (mounted) { 47 47 context.router.maybePop(); 48 - 49 - // Show error message 50 - ScaffoldMessenger.of(context).showSnackBar( 51 - SnackBar( 52 - content: Text('Logout failed: $e'), 53 - backgroundColor: Colors.red, 54 - ), 55 - ); 56 48 } 57 49 } 58 50 } ··· 190 182 // Close loading dialog 191 183 if (mounted) { 192 184 context.router.maybePop(); 193 - 194 - // Show success message 195 - if (oldPosts.isEmpty) { 196 - ScaffoldMessenger.of(context).showSnackBar( 197 - const SnackBar( 198 - content: Text( 199 - 'No old posts found. All posts are already in the new format.', 200 - ), 201 - backgroundColor: Colors.green, 202 - ), 203 - ); 204 - } else { 205 - ScaffoldMessenger.of(context).showSnackBar( 206 - SnackBar( 207 - content: Text( 208 - 'Updated $successCount/${oldPosts.length} old posts to new format${errorCount > 0 ? ' ($errorCount failed)' : ''}.', 209 - ), 210 - backgroundColor: successCount == oldPosts.length 211 - ? Colors.green 212 - : Colors.orange, 213 - ), 214 - ); 215 - } 216 185 } 217 186 } catch (e) { 218 187 // Close loading dialog if it's open 219 188 if (mounted) { 220 189 context.router.maybePop(); 221 - 222 - // Show error message 223 - ScaffoldMessenger.of(context).showSnackBar( 224 - SnackBar( 225 - content: Text('Failed to fetch records: $e'), 226 - backgroundColor: Colors.red, 227 - ), 228 - ); 229 190 } 230 191 logger.e('Error fetching Spark records', error: e); 231 192 }
-5
lib/src/features/stories/ui/pages/story_manager_page.dart
··· 56 56 false; 57 57 if (!shouldDelete) return; 58 58 await notifier.deleteStory(story); 59 - if (context.mounted) { 60 - ScaffoldMessenger.of( 61 - context, 62 - ).showSnackBar(const SnackBar(content: Text('Story deleted'))); 63 - } 64 59 } 65 60 66 61 @override