[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.

block button in posts

+101 -8
+4 -4
lib/src/core/ui/widgets/options_panel.dart
··· 29 29 children: [ 30 30 if (onDelete != null) 31 31 ListTile( 32 - leading: const Icon(Icons.delete_outline, color: Colors.red), 33 - title: Text(l10n.optionsPanelDelete, style: const TextStyle(color: Colors.red)), 32 + leading: Icon(Icons.delete_outline, color: textColor), 33 + title: Text(l10n.optionsPanelDelete, style: TextStyle(color: textColor)), 34 34 onTap: () { 35 35 Navigator.of(context).pop(); 36 36 onDelete(); ··· 38 38 ), 39 39 if (onBlock != null) 40 40 ListTile( 41 - leading: const Icon(Icons.block, color: Colors.red), 41 + leading: Icon(Icons.block, color: textColor), 42 42 title: Text( 43 43 isBlocked ? l10n.optionsPanelUnblock : l10n.optionsPanelBlock, 44 - style: const TextStyle(color: Colors.red), 44 + style: TextStyle(color: textColor), 45 45 ), 46 46 onTap: () { 47 47 Navigator.of(context).pop();
-1
lib/src/features/comments/ui/widgets/comment_item.dart
··· 82 82 actions: [ 83 83 TextButton(onPressed: () => context.router.maybePop(), child: const Text('Cancel')), 84 84 TextButton( 85 - style: TextButton.styleFrom(foregroundColor: Colors.red), 86 85 onPressed: () async { 87 86 try { 88 87 await ref
+22
lib/src/features/feed/providers/feed_provider.dart
··· 402 402 state = state.copyWith(loadedPosts: updatedPosts); 403 403 } 404 404 405 + /// Removes a post at the specified index from the feed 406 + /// and adjusts the current index if necessary. 407 + void removePostAtIndex(int index) { 408 + if (index < 0 || index >= state.length) return; 409 + 410 + final postToRemove = state.loadedPosts[index]; 411 + final updatedPosts = [...state.loadedPosts]..removeAt(index); 412 + 413 + // Clean up extraInfo for the removed post 414 + final updatedExtraInfo = LinkedHashMap<AtUri, ({List<Label> postLabels})>.from(state.extraInfo)..remove(postToRemove.uri); 415 + 416 + // Adjust current index: if we removed a post before current position, 417 + // decrement index to stay on the same visual post 418 + final newIndex = state.index > index ? state.index - 1 : state.index; 419 + 420 + state = state.copyWith( 421 + loadedPosts: updatedPosts, 422 + extraInfo: updatedExtraInfo, 423 + index: newIndex, 424 + ); 425 + } 426 + 405 427 /// Checks if a post should be hidden based on its labels and user preferences 406 428 Future<bool> _shouldHidePost(AtUri uri, List<Label> postLabels) async { 407 429 final settings = ref.read(settingsProvider.notifier);
+20 -2
lib/src/features/feed/ui/pages/feed_page.dart
··· 40 40 super.dispose(); 41 41 } 42 42 43 + /// Scrolls to the next post and removes the current one from the feed. 44 + /// This allows users to quickly advance through their feed while 45 + /// removing posts they've already seen. 46 + Future<void> scrollToNextAndRemovePrevious() async { 47 + final state = ref.read(feedProvider(widget.feed)); 48 + final notifier = ref.read(feedProvider(widget.feed).notifier); 49 + final currentIndex = state.index; 50 + 51 + // Check if there's a next post to scroll to 52 + if (currentIndex >= state.length - 1) return; 53 + 54 + // Remove the current post. Since the PageController is at currentIndex, 55 + // and we're removing the item at currentIndex, the next post naturally 56 + // takes its place at the same index. The PageView will rebuild with 57 + // the new item at this index, effectively showing the "next" post. 58 + notifier.removePostAtIndex(currentIndex); 59 + } 60 + 43 61 @override 44 62 Widget build(BuildContext context) { 45 63 super.build(context); // Required for AutomaticKeepAliveClientMixin ··· 146 164 if (shouldBeActive) { 147 165 return Stack( 148 166 children: [ 149 - FeedPostWidget(index: index, feed: widget.feed), 167 + FeedPostWidget(index: index, feed: widget.feed, onBlockAndAdvance: scrollToNextAndRemovePrevious), 150 168 const Positioned( 151 169 bottom: 10, 152 170 left: 10, ··· 175 193 : const DecoratedBox(decoration: BoxDecoration(color: AppColors.black)); 176 194 } else { 177 195 if (shouldBeActive) { 178 - return FeedPostWidget(index: index, feed: widget.feed); 196 + return FeedPostWidget(index: index, feed: widget.feed, onBlockAndAdvance: scrollToNextAndRemovePrevious); 179 197 } else { 180 198 // Return SizedBox to maintain scroll position but hide content 181 199 return const DecoratedBox(decoration: BoxDecoration(color: AppColors.black));
+45
lib/src/features/feed/ui/widgets/action_buttons/side_action_bar.dart
··· 2 2 import 'package:auto_route/auto_route.dart'; 3 3 import 'package:flutter/material.dart'; 4 4 import 'package:flutter_riverpod/flutter_riverpod.dart'; 5 + import 'package:get_it/get_it.dart'; 5 6 import 'package:sparksocial/src/core/design_system/components/organisms/side_action_bar.dart'; 6 7 import 'package:sparksocial/src/core/network/atproto/atproto.dart'; 7 8 import 'package:sparksocial/src/core/routing/app_router.dart'; 8 9 import 'package:sparksocial/src/core/ui/widgets/options_panel.dart'; 9 10 import 'package:sparksocial/src/core/ui/widgets/report_dialog.dart'; 11 + import 'package:sparksocial/src/core/utils/blocking_utils.dart'; 10 12 import 'package:sparksocial/src/features/feed/providers/feed_provider.dart'; 11 13 import 'package:sparksocial/src/features/feed/providers/like_post.dart'; 12 14 import 'package:sparksocial/src/features/feed/providers/repost_post.dart'; ··· 24 26 this.profileImageUrl, 25 27 this.isImage = false, 26 28 this.onProfilePressed, 29 + this.onBlockAndAdvance, 27 30 }); 28 31 final Feed? feed; 29 32 final String likeCount; ··· 34 37 final PostView post; 35 38 final bool isImage; 36 39 final VoidCallback? onProfilePressed; 40 + 41 + /// Callback invoked after successfully blocking a user, typically to advance to the next post 42 + final VoidCallback? onBlockAndAdvance; 37 43 38 44 @override 39 45 ConsumerState<SideActionBar> createState() => SideActionBarState(); ··· 287 293 ); 288 294 } 289 295 296 + Future<void> _handleBlock() async { 297 + final currentPost = _currentPost ?? widget.post; 298 + final author = currentPost.author; 299 + final wasBlocked = isBlocking(author.viewer); 300 + 301 + try { 302 + final graphRepository = GetIt.instance<SprkRepository>().graph; 303 + await graphRepository.toggleBlock( 304 + author.did, 305 + author.viewer?.blocking, 306 + ); 307 + 308 + if (mounted) { 309 + ScaffoldMessenger.of(context).showSnackBar( 310 + SnackBar( 311 + content: Text(wasBlocked ? 'User unblocked' : 'User blocked'), 312 + backgroundColor: Colors.green, 313 + ), 314 + ); 315 + } 316 + 317 + // If blocking (not unblocking) and callback is provided, advance to next post 318 + if (!wasBlocked && widget.onBlockAndAdvance != null) { 319 + widget.onBlockAndAdvance!(); 320 + } 321 + } catch (e) { 322 + if (mounted) { 323 + ScaffoldMessenger.of(context).showSnackBar( 324 + SnackBar( 325 + content: Text('Failed to ${wasBlocked ? 'unblock' : 'block'} user: $e'), 326 + backgroundColor: Colors.red, 327 + ), 328 + ); 329 + } 330 + } 331 + } 332 + 290 333 // Future<void> _handleCurate() async { 291 334 // // For now, this is a placeholder for curate functionality 292 335 // // In the future, this could add the post to a custom feed or collection ··· 316 359 onOptions: () => OptionsPanel.show( 317 360 context: context, 318 361 onReport: _handleReport, 362 + onBlock: _handleBlock, 363 + isBlocked: isBlocking(currentPost.author.viewer), 319 364 ), 320 365 likeCount: _likeCount.toString(), 321 366 commentCount: commentCount.toString(),
+5 -1
lib/src/features/feed/ui/widgets/post/feed_post_widget.dart
··· 18 18 import 'package:sparksocial/src/features/home/providers/navigation_provider.dart'; 19 19 20 20 class FeedPostWidget extends ConsumerStatefulWidget { 21 - const FeedPostWidget({required this.index, required this.feed, super.key}); 21 + const FeedPostWidget({required this.index, required this.feed, super.key, this.onBlockAndAdvance}); 22 22 23 23 final int index; 24 24 final Feed feed; 25 + 26 + /// Callback invoked after successfully blocking a user, typically to advance to the next post 27 + final VoidCallback? onBlockAndAdvance; 25 28 26 29 @override 27 30 ConsumerState<FeedPostWidget> createState() => _FeedPostWidgetState(); ··· 267 270 ), 268 271 ); 269 272 }, 273 + onBlockAndAdvance: widget.onBlockAndAdvance, 270 274 ), 271 275 ), 272 276 ],
+5
lib/src/features/feed/ui/widgets/post/post_overlay.dart
··· 15 15 this.onProfilePressed, 16 16 this.onUsernameTap, 17 17 this.labels = const [], 18 + this.onBlockAndAdvance, 18 19 }); 19 20 20 21 final PostView post; ··· 23 24 final VoidCallback? onProfilePressed; 24 25 final VoidCallback? onUsernameTap; 25 26 final List<Label> labels; 27 + 28 + /// Callback invoked after successfully blocking a user, typically to advance to the next post 29 + final VoidCallback? onBlockAndAdvance; 26 30 27 31 @override 28 32 Widget build(BuildContext context) { ··· 113 117 profileImageUrl: post.author.avatar.toString(), 114 118 isImage: post.media is MediaViewImages || post.media is MediaViewBskyImages, 115 119 onProfilePressed: onProfilePressed, 120 + onBlockAndAdvance: onBlockAndAdvance, 116 121 ), 117 122 ), 118 123 ],