diff --git a/lib/models/comment.dart b/lib/models/comment.dart index c7a2f8b..371a567 100644 --- a/lib/models/comment.dart +++ b/lib/models/comment.dart @@ -66,6 +66,55 @@ class ThreadViewComment { final CommentView comment; final List? replies; final bool hasMore; + + /// Creates a copy with the given fields replaced. + /// + /// Used when merging a freshly fetched subtree (load-more replies) into + /// an existing comment tree without mutating the original nodes. + ThreadViewComment copyWith({ + CommentView? comment, + List? replies, + bool? hasMore, + }) { + return ThreadViewComment( + comment: comment ?? this.comment, + replies: replies ?? this.replies, + hasMore: hasMore ?? this.hasMore, + ); + } + + /// Returns this subtree with the node whose URI matches [replacement] + /// (this node or any descendant) replaced by [replacement]. Returns the + /// subtree unchanged when no node matches. + ThreadViewComment replaceDescendant(ThreadViewComment replacement) { + if (comment.uri == replacement.comment.uri) { + return replacement; + } + final currentReplies = replies; + if (currentReplies == null || currentReplies.isEmpty) { + return this; + } + return copyWith( + replies: + currentReplies + .map((reply) => reply.replaceDescendant(replacement)) + .toList(), + ); + } + + /// Depth-first search for a comment with the given [uri] in this subtree. + ThreadViewComment? findByUri(String uri) { + if (comment.uri == uri) { + return this; + } + for (final reply in replies ?? const []) { + final found = reply.findByUri(uri); + if (found != null) { + return found; + } + } + return null; + } } /// Record data for a comment, containing the actual content. diff --git a/lib/providers/comments_provider.dart b/lib/providers/comments_provider.dart index c23827a..93c0ea1 100644 --- a/lib/providers/comments_provider.dart +++ b/lib/providers/comments_provider.dart @@ -74,6 +74,9 @@ class CommentsProvider with ChangeNotifier { // Collapsed thread state - stores URIs of collapsed comments final Set _collapsedComments = {}; + // Comment URIs with an in-flight "load more replies" subtree fetch + final Set _loadingMoreReplies = {}; + // Scroll position state (replaces ScrollStateService for this post) double _scrollPosition = 0; @@ -114,6 +117,7 @@ class CommentsProvider with ChangeNotifier { String? get timeframe => _timeframe; ValueNotifier get currentTimeNotifier => _currentTimeNotifier; Set get collapsedComments => Set.unmodifiable(_collapsedComments); + Set get loadingMoreReplies => Set.unmodifiable(_loadingMoreReplies); double get scrollPosition => _scrollPosition; DateTime? get lastRefreshTime => _lastRefreshTime; @@ -331,6 +335,89 @@ class CommentsProvider with ChangeNotifier { await loadComments(); } + /// Load more replies for a specific comment ("Load more replies" button) + /// + /// Fetches the subtree rooted at [commentUri] via the getComments + /// `parentRkey` parameter and merges it into the in-memory comment tree. + /// This surfaces replies hidden by the per-parent sibling cap or the + /// nesting-depth cutoff of the original thread fetch. + /// + /// Returns the freshly fetched subtree so callers (e.g. the focused + /// thread screen) can render it even when the node is no longer present + /// in the top-level tree. Returns null if a fetch for the same comment is + /// already in flight or the server returned no subtree. + /// + /// Throws ApiException/AuthenticationException on network or auth errors. + Future loadMoreReplies(String commentUri) async { + if (_loadingMoreReplies.contains(commentUri)) { + return null; + } + + // rkey is the last path segment of the comment AT-URI. Note: Uri.parse + // cannot handle at:// URIs (the DID's colons look like an invalid port). + final segments = commentUri.split('/'); + final rkey = segments.length > 1 ? segments.last : ''; + if (rkey.isEmpty) { + if (kDebugMode) { + debugPrint('⚠️ loadMoreReplies: malformed comment URI: $commentUri'); + } + return null; + } + + _loadingMoreReplies.add(commentUri); + _safeNotifyListeners(); + + try { + final response = await _apiService.getComments( + postUri: _postUri, + sort: _sort, + timeframe: _timeframe, + parentRkey: rkey, + ); + + if (_isDisposed || response.comments.isEmpty) { + return null; + } + + // The response contains the subtree rooted at the requested comment as + // its sole top-level entry. The cursor paginates the parent's direct + // replies; if present there are more direct replies beyond this page. + final subtree = response.comments.first.copyWith( + hasMore: response.cursor != null, + ); + + _comments = _replaceNode(_comments, subtree); + + // Initialize vote state for the newly fetched replies + if (_authProvider.isAuthenticated && _voteProvider != null) { + _initializeCommentVoteState(subtree); + } + + if (kDebugMode) { + debugPrint( + '✅ Loaded replies subtree for $rkey ' + '(${subtree.replies?.length ?? 0} direct replies)', + ); + } + + return subtree; + } finally { + if (!_isDisposed) { + _loadingMoreReplies.remove(commentUri); + _safeNotifyListeners(); + } + } + } + + /// Returns a copy of [nodes] with the node matching [replacement]'s URI + /// replaced by [replacement]. Leaves the tree untouched when absent. + List _replaceNode( + List nodes, + ThreadViewComment replacement, + ) { + return nodes.map((node) => node.replaceDescendant(replacement)).toList(); + } + /// Change sort order /// /// Updates the sort option and triggers a refresh of comments. @@ -502,6 +589,21 @@ class CommentsProvider with ChangeNotifier { await Future.delayed(Duration(milliseconds: 400 * attempt)); await refreshComments(); } + + // Deep replies can sit past the per-parent sibling cap or the depth + // cutoff of the top-level refresh. Pull the parent's subtree so the + // new reply is merged into the tree at its correct position. + if (parentComment != null && !_treeContainsUri(_comments, response.uri)) { + try { + await loadMoreReplies(parentComment.comment.uri); + } on Exception catch (e) { + // The comment was created successfully; failing to hydrate the + // subtree is not fatal — the reply is reachable via load-more. + if (kDebugMode) { + debugPrint('⚠️ Failed to hydrate reply subtree: $e'); + } + } + } } on Exception catch (e) { if (kDebugMode) { debugPrint('❌ Failed to create comment: $e'); diff --git a/lib/screens/home/focused_thread_screen.dart b/lib/screens/home/focused_thread_screen.dart index 949313c..a5fa1a4 100644 --- a/lib/screens/home/focused_thread_screen.dart +++ b/lib/screens/home/focused_thread_screen.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -89,15 +90,44 @@ class _FocusedThreadBodyState extends State<_FocusedThreadBody> { final ScrollController _scrollController = ScrollController(); final GlobalKey _anchorKey = GlobalKey(); + /// Live subtree rooted at the anchor comment. + /// + /// Starts as the snapshot passed from the parent thread (which may be + /// truncated by the original fetch depth) and is refreshed from the + /// server so deep replies and newly posted replies show up. + late ThreadViewComment _thread; + @override void initState() { super.initState(); - // Scroll to anchor comment after build + _thread = widget.thread; + // Scroll to anchor comment after build, then hydrate the full subtree WidgetsBinding.instance.addPostFrameCallback((_) { _scrollToAnchor(); + _refreshSubtree(); }); } + /// Re-fetch the anchor's subtree from the server and update the view. + /// + /// Also merges the subtree into the parent CommentsProvider tree (when + /// the anchor is still present there), keeping the full thread in sync. + /// Failures are non-fatal: the current (possibly truncated) subtree + /// remains visible. + Future _refreshSubtree() async { + final provider = context.read(); + try { + final subtree = await provider.loadMoreReplies(_thread.comment.uri); + if (subtree != null && mounted) { + setState(() => _thread = subtree); + } + } on Exception catch (e) { + if (kDebugMode) { + debugPrint('⚠️ Failed to refresh focused subtree: $e'); + } + } + } + @override void dispose() { _scrollController.dispose(); @@ -142,13 +172,44 @@ class _FocusedThreadBodyState extends State<_FocusedThreadBody> { MaterialPageRoute( builder: (navigatorContext) => ReplyScreen( comment: comment, - onSubmit: (content, facets) => widget.onReply(content, facets, comment), + onSubmit: (content, facets) async { + await widget.onReply(content, facets, comment); + // Re-fetch the subtree so the new reply appears in this view + await _refreshSubtree(); + }, commentsProvider: context.read(), ), ), ); } + /// Load hidden replies for a nested comment inside the focused view + Future _onLoadMoreReplies(ThreadViewComment node) async { + final provider = context.read(); + final messenger = ScaffoldMessenger.of(context); + try { + final subtree = await provider.loadMoreReplies(node.comment.uri); + if (subtree != null && mounted) { + setState(() => _thread = _thread.replaceDescendant(subtree)); + } + } on Exception { + if (mounted) { + messenger.showSnackBar( + const SnackBar( + content: Text('Failed to load replies. Please try again.'), + behavior: SnackBarBehavior.floating, + ), + ); + } + } + } + + /// Delete a comment, then refresh both the full thread and this subtree + Future _onDelete(String uri) async { + await context.read().deleteComment(commentUri: uri); + await _refreshSubtree(); + } + /// Navigate deeper into a nested thread void _onContinueThread( ThreadViewComment thread, @@ -168,6 +229,9 @@ class _FocusedThreadBodyState extends State<_FocusedThreadBody> { @override Widget build(BuildContext context) { + // Rebuilds on provider changes (e.g. per-node reply-loading spinners) + final commentsProvider = context.watch(); + // Calculate minimum bottom padding to allow anchor to scroll to top final screenHeight = MediaQuery.of(context).size.height; final minBottomPadding = screenHeight * 0.6; @@ -210,9 +274,8 @@ class _FocusedThreadBodyState extends State<_FocusedThreadBody> { ), // Replies (if any) - if (widget.thread.replies != null && - widget.thread.replies!.isNotEmpty) - ...widget.thread.replies!.map((reply) { + if (_thread.replies != null && _thread.replies!.isNotEmpty) + ..._thread.replies!.map((reply) { return CommentThread( thread: reply, depth: 1, @@ -221,16 +284,16 @@ class _FocusedThreadBodyState extends State<_FocusedThreadBody> { collapsedComments: _collapsedComments, onCollapseToggle: _toggleCollapsed, onContinueThread: _onContinueThread, - ancestors: [widget.thread], - onDelete: (uri) => context - .read() - .deleteComment(commentUri: uri), + onLoadMoreReplies: _onLoadMoreReplies, + loadingMoreReplies: + commentsProvider.loadingMoreReplies, + ancestors: [_thread], + onDelete: _onDelete, ); }), // Empty state if no replies - if (widget.thread.replies == null || - widget.thread.replies!.isEmpty) + if (_thread.replies == null || _thread.replies!.isEmpty) _buildNoReplies(), // Bottom padding to allow anchor to scroll to top @@ -255,8 +318,7 @@ class _FocusedThreadBodyState extends State<_FocusedThreadBody> { child: CommentCard( comment: ancestor.comment, onTap: () => _openReplyScreen(ancestor), - onDelete: (uri) => - context.read().deleteComment(commentUri: uri), + onDelete: _onDelete, ), ); } @@ -276,15 +338,14 @@ class _FocusedThreadBodyState extends State<_FocusedThreadBody> { ), ), child: CommentCard( - comment: widget.thread.comment, - onTap: () => _openReplyScreen(widget.thread), - onLongPress: () => _toggleCollapsed(widget.thread.comment.uri), - isCollapsed: _collapsedComments.contains(widget.thread.comment.uri), - collapsedCount: _collapsedComments.contains(widget.thread.comment.uri) - ? widget.thread.comment.stats.replyCount + comment: _thread.comment, + onTap: () => _openReplyScreen(_thread), + onLongPress: () => _toggleCollapsed(_thread.comment.uri), + isCollapsed: _collapsedComments.contains(_thread.comment.uri), + collapsedCount: _collapsedComments.contains(_thread.comment.uri) + ? _thread.comment.stats.replyCount : 0, - onDelete: (uri) => - context.read().deleteComment(commentUri: uri), + onDelete: _onDelete, ), ); } diff --git a/lib/screens/home/post_detail_screen.dart b/lib/screens/home/post_detail_screen.dart index 712dd12..eb3968b 100644 --- a/lib/screens/home/post_detail_screen.dart +++ b/lib/screens/home/post_detail_screen.dart @@ -755,6 +755,23 @@ class _PostDetailScreenState extends State { ); } + /// Load hidden replies for a comment (per-parent cap / depth cutoff) + Future _onLoadMoreReplies(ThreadViewComment thread) async { + final messenger = ScaffoldMessenger.of(context); + try { + await _commentsProvider.loadMoreReplies(thread.comment.uri); + } on Exception { + if (mounted) { + messenger.showSnackBar( + const SnackBar( + content: Text('Failed to load replies. Please try again.'), + behavior: SnackBarBehavior.floating, + ), + ); + } + } + } + /// Build main content area Widget _buildContent() { // Use Consumer to rebuild when comments provider changes @@ -930,6 +947,9 @@ class _PostDetailScreenState extends State { commentsProvider.collapsedComments, onCollapseToggle: commentsProvider.toggleCollapsed, onContinueThread: _onContinueThread, + onLoadMoreReplies: _onLoadMoreReplies, + loadingMoreReplies: + commentsProvider.loadingMoreReplies, onDelete: (uri) => commentsProvider.deleteComment(commentUri: uri), ), @@ -991,6 +1011,8 @@ class _CommentItem extends StatelessWidget { this.collapsedComments = const {}, this.onCollapseToggle, this.onContinueThread, + this.onLoadMoreReplies, + this.loadingMoreReplies = const {}, this.onDelete, }); @@ -1001,6 +1023,8 @@ class _CommentItem extends StatelessWidget { final void Function(String uri)? onCollapseToggle; final void Function(ThreadViewComment, List)? onContinueThread; + final void Function(ThreadViewComment)? onLoadMoreReplies; + final Set loadingMoreReplies; final Future Function(String commentUri)? onDelete; @override @@ -1016,6 +1040,8 @@ class _CommentItem extends StatelessWidget { collapsedComments: collapsedComments, onCollapseToggle: onCollapseToggle, onContinueThread: onContinueThread, + onLoadMoreReplies: onLoadMoreReplies, + loadingMoreReplies: loadingMoreReplies, onDelete: onDelete, ); }, diff --git a/lib/services/coves_api_service.dart b/lib/services/coves_api_service.dart index 8b6d1ba..14bf73e 100644 --- a/lib/services/coves_api_service.dart +++ b/lib/services/coves_api_service.dart @@ -413,6 +413,10 @@ class CovesApiService { /// - [depth]: Maximum nesting depth for replies (default: 10) /// - [limit]: Number of comments per page (default: 50, max: 100) /// - [cursor]: Pagination cursor from previous response + /// - [parentRkey]: Record key of a comment within the post. When provided, + /// the response contains only the subtree rooted at that comment (the + /// comment itself as the sole top-level entry). Used to load more + /// replies past the per-parent sibling cap or the depth cutoff. Future getComments({ required String postUri, String sort = 'hot', @@ -420,6 +424,7 @@ class CovesApiService { int depth = 10, int limit = 50, String? cursor, + String? parentRkey, }) async { try { if (kDebugMode) { @@ -433,6 +438,10 @@ class CovesApiService { 'limit': limit, }; + if (parentRkey != null) { + queryParams['parentRkey'] = parentRkey; + } + if (timeframe != null) { queryParams['timeframe'] = timeframe; } diff --git a/lib/widgets/comment_thread.dart b/lib/widgets/comment_thread.dart index 278a576..c3310d2 100644 --- a/lib/widgets/comment_thread.dart +++ b/lib/widgets/comment_thread.dart @@ -29,6 +29,7 @@ class CommentThread extends StatelessWidget { this.maxDepth = 5, this.currentTime, this.onLoadMoreReplies, + this.loadingMoreReplies = const {}, this.onCommentTap, this.collapsedComments = const {}, this.onCollapseToggle, @@ -42,7 +43,13 @@ class CommentThread extends StatelessWidget { final int depth; final int maxDepth; final DateTime? currentTime; - final VoidCallback? onLoadMoreReplies; + + /// Callback when "Load more replies" is tapped for a comment with + /// unloaded replies (per-parent sibling cap or depth cutoff) + final void Function(ThreadViewComment thread)? onLoadMoreReplies; + + /// Set of comment URIs with an in-flight "load more replies" fetch + final Set loadingMoreReplies; /// Callback when a comment is tapped (for reply functionality) final void Function(ThreadViewComment)? onCommentTap; @@ -104,6 +111,7 @@ class CommentThread extends StatelessWidget { maxDepth: maxDepth, currentTime: currentTime, onLoadMoreReplies: onLoadMoreReplies, + loadingMoreReplies: loadingMoreReplies, onCommentTap: onCommentTap, collapsedComments: collapsedComments, onCollapseToggle: onCollapseToggle, @@ -275,6 +283,7 @@ class CommentThread extends StatelessWidget { Widget _buildLoadMoreButton(BuildContext context) { // Calculate left padding based on depth (align with replies) final leftPadding = 16.0 + ((depth + 1) * 12.0); + final isLoading = loadingMoreReplies.contains(thread.comment.uri); return Container( padding: EdgeInsets.fromLTRB(leftPadding, 8, 16, 8), @@ -282,27 +291,41 @@ class CommentThread extends StatelessWidget { border: Border(bottom: BorderSide(color: AppColors.border)), ), child: InkWell( - onTap: () { - if (onLoadMoreReplies != null) { - onLoadMoreReplies!(); - } else { - if (kDebugMode) { - debugPrint('Load more replies tapped (no handler provided)'); - } - } - }, + onTap: isLoading + ? null + : () { + if (onLoadMoreReplies != null) { + onLoadMoreReplies!(thread); + } else { + if (kDebugMode) { + debugPrint( + 'Load more replies tapped (no handler provided)', + ); + } + } + }, child: Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( children: [ - Icon( - Icons.add_circle_outline, - size: 16, - color: AppColors.primary.withValues(alpha: 0.8), - ), + if (isLoading) + SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + color: AppColors.primary.withValues(alpha: 0.8), + ), + ) + else + Icon( + Icons.add_circle_outline, + size: 16, + color: AppColors.primary.withValues(alpha: 0.8), + ), const SizedBox(width: 6), Text( - 'Load more replies', + isLoading ? 'Loading replies…' : 'Load more replies', style: TextStyle( color: AppColors.primary.withValues(alpha: 0.8), fontSize: 13, diff --git a/test/providers/comments_provider_test.dart b/test/providers/comments_provider_test.dart index 7bebb9a..bf4f5ef 100644 --- a/test/providers/comments_provider_test.dart +++ b/test/providers/comments_provider_test.dart @@ -1342,7 +1342,31 @@ void main() { ); // Create a parent comment to reply to - final parentComment = _createMockThreadComment('parent-comment'); + const nestedParentUri = + 'at://did:plc:author/social.coves.community.comment/prkey'; + final parentComment = _createMockThreadComment(nestedParentUri); + + // When the refreshed top-level tree doesn't contain the new reply + // (deep reply past the sibling cap / depth cutoff), the provider + // hydrates the parent's subtree via parentRkey. + final hydratedSubtree = _createMockThreadCommentWithViewer( + uri: nestedParentUri, + replies: [ + _createMockThreadComment('at://did:plc:test/comment/reply1'), + ], + ); + when( + mockApiService.getComments( + postUri: anyNamed('postUri'), + sort: anyNamed('sort'), + timeframe: anyNamed('timeframe'), + depth: anyNamed('depth'), + limit: anyNamed('limit'), + parentRkey: argThat(isNotNull, named: 'parentRkey'), + ), + ).thenAnswer( + (_) async => CommentsResponse(post: {}, comments: [hydratedSubtree]), + ); await providerWithCommentService.createComment( content: 'This is a nested reply', @@ -1354,11 +1378,24 @@ void main() { mockCommentService.createComment( rootUri: testPostUri, rootCid: testPostCid, - parentUri: 'parent-comment', - parentCid: 'cid-parent-comment', + parentUri: nestedParentUri, + parentCid: 'cid-$nestedParentUri', content: 'This is a nested reply', ), ).called(1); + + // The reply wasn't in the refreshed tree, so the parent subtree + // must have been fetched to hydrate it. + verify( + mockApiService.getComments( + postUri: testPostUri, + sort: anyNamed('sort'), + timeframe: anyNamed('timeframe'), + depth: anyNamed('depth'), + limit: anyNamed('limit'), + parentRkey: 'prkey', + ), + ).called(1); }); test('should trim content before sending', () async { @@ -1549,6 +1586,133 @@ void main() { ).called(1); }); }); + + group('loadMoreReplies', () { + const parentUri = + 'at://did:plc:author/social.coves.community.comment/parentrkey1'; + + setUp(() { + // Initial tree: one top-level comment with unloaded replies + final initialResponse = CommentsResponse( + post: {}, + comments: [ + ThreadViewComment( + comment: _createMockThreadComment(parentUri).comment, + hasMore: true, + ), + ], + ); + + when( + mockApiService.getComments( + postUri: anyNamed('postUri'), + sort: anyNamed('sort'), + timeframe: anyNamed('timeframe'), + depth: anyNamed('depth'), + limit: anyNamed('limit'), + cursor: anyNamed('cursor'), + ), + ).thenAnswer((_) async => initialResponse); + }); + + test('should fetch subtree by rkey and merge it into the tree', + () async { + await commentsProvider.loadComments(refresh: true); + expect(commentsProvider.comments.single.replies, isNull); + expect(commentsProvider.comments.single.hasMore, isTrue); + + final subtree = _createMockThreadCommentWithViewer( + uri: parentUri, + replies: [ + _createMockThreadCommentWithViewer( + uri: 'child-1', + replies: [_createMockThreadComment('grandchild-1')], + ), + _createMockThreadComment('child-2'), + ], + ); + when( + mockApiService.getComments( + postUri: anyNamed('postUri'), + sort: anyNamed('sort'), + timeframe: anyNamed('timeframe'), + depth: anyNamed('depth'), + limit: anyNamed('limit'), + parentRkey: argThat(equals('parentrkey1'), named: 'parentRkey'), + ), + ).thenAnswer( + (_) async => CommentsResponse(post: {}, comments: [subtree]), + ); + + final result = await commentsProvider.loadMoreReplies(parentUri); + + expect(result, isNotNull); + final merged = commentsProvider.comments.single; + expect(merged.comment.uri, parentUri); + expect(merged.replies, hasLength(2)); + expect(merged.replies!.first.comment.uri, 'child-1'); + expect( + merged.replies!.first.replies!.single.comment.uri, + 'grandchild-1', + ); + // No cursor in the subtree response -> no more direct replies + expect(merged.hasMore, isFalse); + expect(commentsProvider.loadingMoreReplies, isEmpty); + }); + + test('should keep hasMore when the subtree response has a cursor', + () async { + await commentsProvider.loadComments(refresh: true); + + final subtree = _createMockThreadCommentWithViewer( + uri: parentUri, + replies: [_createMockThreadComment('child-1')], + ); + when( + mockApiService.getComments( + postUri: anyNamed('postUri'), + sort: anyNamed('sort'), + timeframe: anyNamed('timeframe'), + depth: anyNamed('depth'), + limit: anyNamed('limit'), + parentRkey: argThat(isNotNull, named: 'parentRkey'), + ), + ).thenAnswer( + (_) async => CommentsResponse( + post: {}, + comments: [subtree], + cursor: 'more-direct-replies', + ), + ); + + await commentsProvider.loadMoreReplies(parentUri); + + expect(commentsProvider.comments.single.hasMore, isTrue); + }); + + test('should propagate fetch errors and clear loading state', () async { + await commentsProvider.loadComments(refresh: true); + + when( + mockApiService.getComments( + postUri: anyNamed('postUri'), + sort: anyNamed('sort'), + timeframe: anyNamed('timeframe'), + depth: anyNamed('depth'), + limit: anyNamed('limit'), + parentRkey: argThat(isNotNull, named: 'parentRkey'), + ), + ).thenThrow(ApiException('Network error')); + + await expectLater( + commentsProvider.loadMoreReplies(parentUri), + throwsA(isA()), + ); + expect(commentsProvider.loadingMoreReplies, isEmpty); + // Tree is untouched on failure + expect(commentsProvider.comments.single.replies, isNull); + }); + }); }); } diff --git a/test/providers/comments_provider_test.mocks.dart b/test/providers/comments_provider_test.mocks.dart index 00874e5..de11c88 100644 --- a/test/providers/comments_provider_test.mocks.dart +++ b/test/providers/comments_provider_test.mocks.dart @@ -4,19 +4,20 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i9; -import 'dart:typed_data' as _i12; +import 'dart:typed_data' as _i13; import 'dart:ui' as _i10; import 'package:coves_flutter/models/comment.dart' as _i3; import 'package:coves_flutter/models/community.dart' as _i4; import 'package:coves_flutter/models/post.dart' as _i2; +import 'package:coves_flutter/models/post_get_result.dart' as _i11; import 'package:coves_flutter/models/user_profile.dart' as _i5; import 'package:coves_flutter/providers/auth_provider.dart' as _i8; -import 'package:coves_flutter/providers/vote_provider.dart' as _i13; +import 'package:coves_flutter/providers/vote_provider.dart' as _i14; import 'package:coves_flutter/services/comment_service.dart' as _i7; import 'package:coves_flutter/services/coves_api_service.dart' as _i6; import 'package:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i11; +import 'package:mockito/src/dummies.dart' as _i12; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -51,38 +52,43 @@ class _FakeCommunitiesResponse_2 extends _i1.SmartFake : super(parent, parentInvocation); } -class _FakeCreatePostResponse_3 extends _i1.SmartFake +class _FakeCommunityView_3 extends _i1.SmartFake implements _i4.CommunityView { + _FakeCommunityView_3(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeCreatePostResponse_4 extends _i1.SmartFake implements _i4.CreatePostResponse { - _FakeCreatePostResponse_3(Object parent, Invocation parentInvocation) + _FakeCreatePostResponse_4(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); } -class _FakeCreateCommunityResponse_4 extends _i1.SmartFake +class _FakeCreateCommunityResponse_5 extends _i1.SmartFake implements _i4.CreateCommunityResponse { - _FakeCreateCommunityResponse_4(Object parent, Invocation parentInvocation) + _FakeCreateCommunityResponse_5(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); } -class _FakeUserProfile_5 extends _i1.SmartFake implements _i5.UserProfile { - _FakeUserProfile_5(Object parent, Invocation parentInvocation) +class _FakeUserProfile_6 extends _i1.SmartFake implements _i5.UserProfile { + _FakeUserProfile_6(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); } -class _FakeActorCommentsResponse_6 extends _i1.SmartFake +class _FakeActorCommentsResponse_7 extends _i1.SmartFake implements _i3.ActorCommentsResponse { - _FakeActorCommentsResponse_6(Object parent, Invocation parentInvocation) + _FakeActorCommentsResponse_7(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); } -class _FakeUpdateProfileResponse_7 extends _i1.SmartFake +class _FakeUpdateProfileResponse_8 extends _i1.SmartFake implements _i6.UpdateProfileResponse { - _FakeUpdateProfileResponse_7(Object parent, Invocation parentInvocation) + _FakeUpdateProfileResponse_8(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); } -class _FakeCreateCommentResponse_8 extends _i1.SmartFake +class _FakeCreateCommentResponse_9 extends _i1.SmartFake implements _i7.CreateCommentResponse { - _FakeCreateCommentResponse_8(Object parent, Invocation parentInvocation) + _FakeCreateCommentResponse_9(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); } @@ -250,6 +256,37 @@ class MockCovesApiService extends _i1.Mock implements _i6.CovesApiService { ) as _i9.Future<_i2.TimelineResponse>); + @override + _i9.Future<_i2.TimelineResponse> getCommunityFeed({ + required String? community, + String? sort = 'hot', + String? timeframe, + int? limit = 15, + String? cursor, + }) => + (super.noSuchMethod( + Invocation.method(#getCommunityFeed, [], { + #community: community, + #sort: sort, + #timeframe: timeframe, + #limit: limit, + #cursor: cursor, + }), + returnValue: _i9.Future<_i2.TimelineResponse>.value( + _FakeTimelineResponse_0( + this, + Invocation.method(#getCommunityFeed, [], { + #community: community, + #sort: sort, + #timeframe: timeframe, + #limit: limit, + #cursor: cursor, + }), + ), + ), + ) + as _i9.Future<_i2.TimelineResponse>); + @override _i9.Future<_i3.CommentsResponse> getComments({ required String? postUri, @@ -258,6 +295,7 @@ class MockCovesApiService extends _i1.Mock implements _i6.CovesApiService { int? depth = 10, int? limit = 50, String? cursor, + String? parentRkey, }) => (super.noSuchMethod( Invocation.method(#getComments, [], { @@ -267,6 +305,7 @@ class MockCovesApiService extends _i1.Mock implements _i6.CovesApiService { #depth: depth, #limit: limit, #cursor: cursor, + #parentRkey: parentRkey, }), returnValue: _i9.Future<_i3.CommentsResponse>.value( _FakeCommentsResponse_1( @@ -278,12 +317,38 @@ class MockCovesApiService extends _i1.Mock implements _i6.CovesApiService { #depth: depth, #limit: limit, #cursor: cursor, + #parentRkey: parentRkey, }), ), ), ) as _i9.Future<_i3.CommentsResponse>); + @override + _i9.Future> getPosts({ + required List? uris, + }) => + (super.noSuchMethod( + Invocation.method(#getPosts, [], {#uris: uris}), + returnValue: _i9.Future>.value( + <_i11.PostGetResult>[], + ), + ) + as _i9.Future>); + + @override + _i9.Future<_i11.PostGetResult> getPost(String? uri) => + (super.noSuchMethod( + Invocation.method(#getPost, [uri]), + returnValue: _i9.Future<_i11.PostGetResult>.value( + _i12.dummyValue<_i11.PostGetResult>( + this, + Invocation.method(#getPost, [uri]), + ), + ), + ) + as _i9.Future<_i11.PostGetResult>); + @override _i9.Future<_i4.CommunitiesResponse> listCommunities({ int? limit = 50, @@ -312,6 +377,19 @@ class MockCovesApiService extends _i1.Mock implements _i6.CovesApiService { ) as _i9.Future<_i4.CommunitiesResponse>); + @override + _i9.Future<_i4.CommunityView> getCommunity({required String? community}) => + (super.noSuchMethod( + Invocation.method(#getCommunity, [], {#community: community}), + returnValue: _i9.Future<_i4.CommunityView>.value( + _FakeCommunityView_3( + this, + Invocation.method(#getCommunity, [], {#community: community}), + ), + ), + ) + as _i9.Future<_i4.CommunityView>); + @override _i9.Future<_i4.CreatePostResponse> createPost({ required String? community, @@ -333,7 +411,7 @@ class MockCovesApiService extends _i1.Mock implements _i6.CovesApiService { #labels: labels, }), returnValue: _i9.Future<_i4.CreatePostResponse>.value( - _FakeCreatePostResponse_3( + _FakeCreatePostResponse_4( this, Invocation.method(#createPost, [], { #community: community, @@ -349,6 +427,15 @@ class MockCovesApiService extends _i1.Mock implements _i6.CovesApiService { ) as _i9.Future<_i4.CreatePostResponse>); + @override + _i9.Future deletePost({required String? uri}) => + (super.noSuchMethod( + Invocation.method(#deletePost, [], {#uri: uri}), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) + as _i9.Future); + @override _i9.Future<_i4.CreateCommunityResponse> createCommunity({ required String? name, @@ -364,7 +451,7 @@ class MockCovesApiService extends _i1.Mock implements _i6.CovesApiService { #visibility: visibility, }), returnValue: _i9.Future<_i4.CreateCommunityResponse>.value( - _FakeCreateCommunityResponse_4( + _FakeCreateCommunityResponse_5( this, Invocation.method(#createCommunity, [], { #name: name, @@ -382,7 +469,7 @@ class MockCovesApiService extends _i1.Mock implements _i6.CovesApiService { (super.noSuchMethod( Invocation.method(#getProfile, [], {#actor: actor}), returnValue: _i9.Future<_i5.UserProfile>.value( - _FakeUserProfile_5( + _FakeUserProfile_6( this, Invocation.method(#getProfile, [], {#actor: actor}), ), @@ -436,7 +523,7 @@ class MockCovesApiService extends _i1.Mock implements _i6.CovesApiService { #cursor: cursor, }), returnValue: _i9.Future<_i3.ActorCommentsResponse>.value( - _FakeActorCommentsResponse_6( + _FakeActorCommentsResponse_7( this, Invocation.method(#getActorComments, [], { #actor: actor, @@ -456,7 +543,7 @@ class MockCovesApiService extends _i1.Mock implements _i6.CovesApiService { #community: community, }), returnValue: _i9.Future.value( - _i11.dummyValue( + _i12.dummyValue( this, Invocation.method(#subscribeToCommunity, [], { #community: community, @@ -477,10 +564,54 @@ class MockCovesApiService extends _i1.Mock implements _i6.CovesApiService { ) as _i9.Future); + @override + _i9.Future blockUser({required String? actor}) => + (super.noSuchMethod( + Invocation.method(#blockUser, [], {#actor: actor}), + returnValue: _i9.Future.value( + _i12.dummyValue( + this, + Invocation.method(#blockUser, [], {#actor: actor}), + ), + ), + ) + as _i9.Future); + + @override + _i9.Future unblockUser({required String? actor}) => + (super.noSuchMethod( + Invocation.method(#unblockUser, [], {#actor: actor}), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) + as _i9.Future); + + @override + _i9.Future blockCommunity({required String? community}) => + (super.noSuchMethod( + Invocation.method(#blockCommunity, [], {#community: community}), + returnValue: _i9.Future.value( + _i12.dummyValue( + this, + Invocation.method(#blockCommunity, [], {#community: community}), + ), + ), + ) + as _i9.Future); + + @override + _i9.Future unblockCommunity({required String? community}) => + (super.noSuchMethod( + Invocation.method(#unblockCommunity, [], {#community: community}), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) + as _i9.Future); + @override _i9.Future<_i4.CreateCommunityResponse> updateCommunity({ required String? communityDid, - required _i12.Uint8List? imageBytes, + required _i13.Uint8List? imageBytes, required String? mimeType, }) => (super.noSuchMethod( @@ -490,7 +621,7 @@ class MockCovesApiService extends _i1.Mock implements _i6.CovesApiService { #mimeType: mimeType, }), returnValue: _i9.Future<_i4.CreateCommunityResponse>.value( - _FakeCreateCommunityResponse_4( + _FakeCreateCommunityResponse_5( this, Invocation.method(#updateCommunity, [], { #communityDid: communityDid, @@ -506,9 +637,9 @@ class MockCovesApiService extends _i1.Mock implements _i6.CovesApiService { _i9.Future<_i6.UpdateProfileResponse> updateProfile({ String? displayName, String? bio, - _i12.Uint8List? avatarBytes, + _i13.Uint8List? avatarBytes, String? avatarMimeType, - _i12.Uint8List? bannerBytes, + _i13.Uint8List? bannerBytes, String? bannerMimeType, }) => (super.noSuchMethod( @@ -521,7 +652,7 @@ class MockCovesApiService extends _i1.Mock implements _i6.CovesApiService { #bannerMimeType: bannerMimeType, }), returnValue: _i9.Future<_i6.UpdateProfileResponse>.value( - _FakeUpdateProfileResponse_7( + _FakeUpdateProfileResponse_8( this, Invocation.method(#updateProfile, [], { #displayName: displayName, @@ -536,6 +667,22 @@ class MockCovesApiService extends _i1.Mock implements _i6.CovesApiService { ) as _i9.Future<_i6.UpdateProfileResponse>); + @override + _i9.Future submitReport({ + required String? targetUri, + required String? reason, + String? explanation, + }) => + (super.noSuchMethod( + Invocation.method(#submitReport, [], { + #targetUri: targetUri, + #reason: reason, + #explanation: explanation, + }), + returnValue: _i9.Future.value(0), + ) + as _i9.Future); + @override void dispose() => super.noSuchMethod( Invocation.method(#dispose, []), @@ -546,7 +693,7 @@ class MockCovesApiService extends _i1.Mock implements _i6.CovesApiService { /// A class which mocks [VoteProvider]. /// /// See the documentation for Mockito's code generation for more information. -class MockVoteProvider extends _i1.Mock implements _i13.VoteProvider { +class MockVoteProvider extends _i1.Mock implements _i14.VoteProvider { MockVoteProvider() { _i1.throwOnMissingStub(this); } @@ -563,9 +710,9 @@ class MockVoteProvider extends _i1.Mock implements _i13.VoteProvider { ); @override - _i13.VoteState? getVoteState(String? postUri) => + _i14.VoteState? getVoteState(String? postUri) => (super.noSuchMethod(Invocation.method(#getVoteState, [postUri])) - as _i13.VoteState?); + as _i14.VoteState?); @override bool isLiked(String? postUri) => @@ -673,7 +820,7 @@ class MockCommentService extends _i1.Mock implements _i7.CommentService { #contentFacets: contentFacets, }), returnValue: _i9.Future<_i7.CreateCommentResponse>.value( - _FakeCreateCommentResponse_8( + _FakeCreateCommentResponse_9( this, Invocation.method(#createComment, [], { #rootUri: rootUri, @@ -687,4 +834,13 @@ class MockCommentService extends _i1.Mock implements _i7.CommentService { ), ) as _i9.Future<_i7.CreateCommentResponse>); + + @override + _i9.Future deleteComment({required String? uri}) => + (super.noSuchMethod( + Invocation.method(#deleteComment, [], {#uri: uri}), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) + as _i9.Future); } diff --git a/test/providers/feed_provider_test.mocks.dart b/test/providers/feed_provider_test.mocks.dart deleted file mode 100644 index cba6ee9..0000000 --- a/test/providers/feed_provider_test.mocks.dart +++ /dev/null @@ -1,349 +0,0 @@ -// Mocks generated by Mockito 5.4.6 from annotations -// in coves_flutter/test/providers/feed_provider_test.dart. -// Do not manually edit this file. - -// ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i5; -import 'dart:ui' as _i6; - -import 'package:coves_flutter/models/comment.dart' as _i3; -import 'package:coves_flutter/models/post.dart' as _i2; -import 'package:coves_flutter/providers/auth_provider.dart' as _i4; -import 'package:coves_flutter/providers/vote_provider.dart' as _i8; -import 'package:coves_flutter/services/coves_api_service.dart' as _i7; -import 'package:mockito/mockito.dart' as _i1; - -// ignore_for_file: type=lint -// ignore_for_file: avoid_redundant_argument_values -// ignore_for_file: avoid_setters_without_getters -// ignore_for_file: comment_references -// ignore_for_file: deprecated_member_use -// ignore_for_file: deprecated_member_use_from_same_package -// ignore_for_file: implementation_imports -// ignore_for_file: invalid_use_of_visible_for_testing_member -// ignore_for_file: must_be_immutable -// ignore_for_file: prefer_const_constructors -// ignore_for_file: unnecessary_parenthesis -// ignore_for_file: camel_case_types -// ignore_for_file: subtype_of_sealed_class -// ignore_for_file: invalid_use_of_internal_member - -class _FakeTimelineResponse_0 extends _i1.SmartFake - implements _i2.TimelineResponse { - _FakeTimelineResponse_0(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); -} - -class _FakeCommentsResponse_1 extends _i1.SmartFake - implements _i3.CommentsResponse { - _FakeCommentsResponse_1(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); -} - -/// A class which mocks [AuthProvider]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockAuthProvider extends _i1.Mock implements _i4.AuthProvider { - MockAuthProvider() { - _i1.throwOnMissingStub(this); - } - - @override - bool get isAuthenticated => - (super.noSuchMethod( - Invocation.getter(#isAuthenticated), - returnValue: false, - ) - as bool); - - @override - bool get isLoading => - (super.noSuchMethod(Invocation.getter(#isLoading), returnValue: false) - as bool); - - @override - bool get hasListeners => - (super.noSuchMethod(Invocation.getter(#hasListeners), returnValue: false) - as bool); - - @override - _i5.Future getAccessToken() => - (super.noSuchMethod( - Invocation.method(#getAccessToken, []), - returnValue: _i5.Future.value(), - ) - as _i5.Future); - - @override - _i5.Future initialize() => - (super.noSuchMethod( - Invocation.method(#initialize, []), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); - - @override - _i5.Future signIn(String? handle) => - (super.noSuchMethod( - Invocation.method(#signIn, [handle]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); - - @override - _i5.Future signOut() => - (super.noSuchMethod( - Invocation.method(#signOut, []), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); - - @override - _i5.Future refreshToken() => - (super.noSuchMethod( - Invocation.method(#refreshToken, []), - returnValue: _i5.Future.value(false), - ) - as _i5.Future); - - @override - void clearError() => super.noSuchMethod( - Invocation.method(#clearError, []), - returnValueForMissingStub: null, - ); - - @override - void addListener(_i6.VoidCallback? listener) => super.noSuchMethod( - Invocation.method(#addListener, [listener]), - returnValueForMissingStub: null, - ); - - @override - void removeListener(_i6.VoidCallback? listener) => super.noSuchMethod( - Invocation.method(#removeListener, [listener]), - returnValueForMissingStub: null, - ); - - @override - void dispose() => super.noSuchMethod( - Invocation.method(#dispose, []), - returnValueForMissingStub: null, - ); - - @override - void notifyListeners() => super.noSuchMethod( - Invocation.method(#notifyListeners, []), - returnValueForMissingStub: null, - ); -} - -/// A class which mocks [CovesApiService]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockCovesApiService extends _i1.Mock implements _i7.CovesApiService { - MockCovesApiService() { - _i1.throwOnMissingStub(this); - } - - @override - _i5.Future<_i2.TimelineResponse> getTimeline({ - String? sort = 'hot', - String? timeframe, - int? limit = 15, - String? cursor, - }) => - (super.noSuchMethod( - Invocation.method(#getTimeline, [], { - #sort: sort, - #timeframe: timeframe, - #limit: limit, - #cursor: cursor, - }), - returnValue: _i5.Future<_i2.TimelineResponse>.value( - _FakeTimelineResponse_0( - this, - Invocation.method(#getTimeline, [], { - #sort: sort, - #timeframe: timeframe, - #limit: limit, - #cursor: cursor, - }), - ), - ), - ) - as _i5.Future<_i2.TimelineResponse>); - - @override - _i5.Future<_i2.TimelineResponse> getDiscover({ - String? sort = 'hot', - String? timeframe, - int? limit = 15, - String? cursor, - }) => - (super.noSuchMethod( - Invocation.method(#getDiscover, [], { - #sort: sort, - #timeframe: timeframe, - #limit: limit, - #cursor: cursor, - }), - returnValue: _i5.Future<_i2.TimelineResponse>.value( - _FakeTimelineResponse_0( - this, - Invocation.method(#getDiscover, [], { - #sort: sort, - #timeframe: timeframe, - #limit: limit, - #cursor: cursor, - }), - ), - ), - ) - as _i5.Future<_i2.TimelineResponse>); - - @override - _i5.Future<_i3.CommentsResponse> getComments({ - required String? postUri, - String? sort = 'hot', - String? timeframe, - int? depth = 10, - int? limit = 50, - String? cursor, - }) => - (super.noSuchMethod( - Invocation.method(#getComments, [], { - #postUri: postUri, - #sort: sort, - #timeframe: timeframe, - #depth: depth, - #limit: limit, - #cursor: cursor, - }), - returnValue: _i5.Future<_i3.CommentsResponse>.value( - _FakeCommentsResponse_1( - this, - Invocation.method(#getComments, [], { - #postUri: postUri, - #sort: sort, - #timeframe: timeframe, - #depth: depth, - #limit: limit, - #cursor: cursor, - }), - ), - ), - ) - as _i5.Future<_i3.CommentsResponse>); - - @override - void dispose() => super.noSuchMethod( - Invocation.method(#dispose, []), - returnValueForMissingStub: null, - ); -} - -/// A class which mocks [VoteProvider]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockVoteProvider extends _i1.Mock implements _i8.VoteProvider { - MockVoteProvider() { - _i1.throwOnMissingStub(this); - } - - @override - bool get hasListeners => - (super.noSuchMethod(Invocation.getter(#hasListeners), returnValue: false) - as bool); - - @override - void dispose() => super.noSuchMethod( - Invocation.method(#dispose, []), - returnValueForMissingStub: null, - ); - - @override - _i8.VoteState? getVoteState(String? postUri) => - (super.noSuchMethod(Invocation.method(#getVoteState, [postUri])) - as _i8.VoteState?); - - @override - bool isLiked(String? postUri) => - (super.noSuchMethod( - Invocation.method(#isLiked, [postUri]), - returnValue: false, - ) - as bool); - - @override - bool isPending(String? postUri) => - (super.noSuchMethod( - Invocation.method(#isPending, [postUri]), - returnValue: false, - ) - as bool); - - @override - int getAdjustedScore(String? postUri, int? serverScore) => - (super.noSuchMethod( - Invocation.method(#getAdjustedScore, [postUri, serverScore]), - returnValue: 0, - ) - as int); - - @override - _i5.Future toggleVote({ - required String? postUri, - required String? postCid, - String? direction = 'up', - }) => - (super.noSuchMethod( - Invocation.method(#toggleVote, [], { - #postUri: postUri, - #postCid: postCid, - #direction: direction, - }), - returnValue: _i5.Future.value(false), - ) - as _i5.Future); - - @override - void setInitialVoteState({ - required String? postUri, - String? voteDirection, - String? voteUri, - }) => super.noSuchMethod( - Invocation.method(#setInitialVoteState, [], { - #postUri: postUri, - #voteDirection: voteDirection, - #voteUri: voteUri, - }), - returnValueForMissingStub: null, - ); - - @override - void clear() => super.noSuchMethod( - Invocation.method(#clear, []), - returnValueForMissingStub: null, - ); - - @override - void addListener(_i6.VoidCallback? listener) => super.noSuchMethod( - Invocation.method(#addListener, [listener]), - returnValueForMissingStub: null, - ); - - @override - void removeListener(_i6.VoidCallback? listener) => super.noSuchMethod( - Invocation.method(#removeListener, [listener]), - returnValueForMissingStub: null, - ); - - @override - void notifyListeners() => super.noSuchMethod( - Invocation.method(#notifyListeners, []), - returnValueForMissingStub: null, - ); -}