Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add comment search #887

Merged
merged 9 commits into from
Nov 25, 2023
10 changes: 9 additions & 1 deletion lib/core/enums/fab_action.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ enum PostFabAction {
backToTop(),
changeSort(),
replyToPost(),
refresh();
refresh(),
search();

IconData getIcon({IconData? override, bool postLocked = false}) {
if (override != null) {
Expand All @@ -81,6 +82,8 @@ enum PostFabAction {
return Icons.reply_rounded;
case PostFabAction.refresh:
return Icons.refresh_rounded;
case PostFabAction.search:
return Icons.search_rounded;
}
}

Expand All @@ -99,6 +102,8 @@ enum PostFabAction {
return AppLocalizations.of(context)!.replyToPost;
case PostFabAction.refresh:
return AppLocalizations.of(context)!.refresh;
case PostFabAction.search:
return AppLocalizations.of(context)!.search;
}
}

Expand All @@ -121,6 +126,9 @@ enum PostFabAction {
break;
case PostFabAction.refresh:
context?.read<PostBloc>().add(GetPostEvent(postView: postView, postId: postId, selectedCommentId: selectedCommentId, selectedCommentPath: selectedCommentPath));
case PostFabAction.search:
// Invoked via override
break;
}
}
}
1 change: 1 addition & 0 deletions lib/core/enums/local_settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ enum LocalSettings {
postFabEnableChangeSort(name: 'setting_post_fab_enable_change_sort', label: 'Change Sort'),
postFabEnableReplyToPost(name: 'setting_post_fab_enable_reply_to_post', label: 'Reply to Post'),
postFabEnableRefresh(name: 'setting_post_fab_enable_refresh', label: 'Refresh'),
postFabEnableSearch(name: 'setting_post_fab_enable_search', label: 'Search'),
feedFabSinglePressAction(name: 'settings_feed_fab_single_press_action', label: ''),
feedFabLongPressAction(name: 'settings_feed_fab_long_press_action', label: ''),
postFabSinglePressAction(name: 'settings_post_fab_single_press_action', label: ''),
Expand Down
6 changes: 5 additions & 1 deletion lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@
"@emptyUri": {
"description": "Error message for empty link."
},
"endSearch": "End Search",
"errorDownloadingMedia": "Could not download the media file to share: {errorMessage}",
"@errorDownloadingMedia": {},
"exceptionProcessingUri": "An error occurred while processing the link. It may not be available on your instance.",
Expand Down Expand Up @@ -282,6 +283,7 @@
"noPosts": "No posts found",
"@noPosts": {},
"noPostsFound": "No posts found",
"noResultsFound": "No results found.",
"noUserBlocks": "No blocked users.",
"@noUserBlocks": {},
"noUsersFound": "No users found",
Expand Down Expand Up @@ -407,13 +409,15 @@
"@search": {},
"searchByText": "Search by text",
"searchByUrl": "Search by URL",
"searchComments": "Search Comments",
"searchCommentsFederatedWith": "Search for comments federated with {instance}",
"searchCommunitiesFederatedWith": "Search for communities federated with {instance}",
"@searchCommunitiesFederatedWith": {},
"searchInstance": "Search {instance}",
"@searchInstance": {},
"searchPostSearchType": "Select Post Search Type",
"searchPostsFederatedWith": "Search for posts federated with {instance}",
"searchTerm": "Search term",
"searchUsersFederatedWith": "Search for users federated with {instance}",
"selectCommunity": "Select a community",
"@selectCommunity": {},
Expand Down Expand Up @@ -563,7 +567,7 @@
"username": "Username",
"@username": {},
"users": "Users",
"viewAllComments": "View all comments.",
"viewAllComments": "View all comments",
"@viewAllComments": {},
"visitCommunity": "Visit Community",
"@visitCommunity": {},
Expand Down
103 changes: 100 additions & 3 deletions lib/post/bloc/post_bloc.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:math';

import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:collection/collection.dart';
import 'package:flutter/cupertino.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
Expand Down Expand Up @@ -77,6 +78,15 @@ class PostBloc extends Bloc<PostEvent, PostState> {
on<NavigateCommentEvent>(
_navigateCommentEvent,
);
on<StartCommentSearchEvent>(
_startCommentSearchEvent,
);
on<ContinueCommentSearchEvent>(
_continueCommentSearchEvent,
);
on<EndCommentSearchEvent>(
_endCommentSearchEvent,
);
on<ReportCommentEvent>(
_reportCommentEvent,
transformer: throttleDroppable(throttleDuration),
Expand Down Expand Up @@ -212,6 +222,8 @@ class PostBloc extends Bloc<PostEvent, PostState> {

/// Event to fetch more comments from a post
Future<void> _getPostCommentsEvent(GetPostCommentsEvent event, emit) async {
bool searchWasInProgress = state.status == PostStatus.searchInProgress;

int attemptCount = 0;

SharedPreferences prefs = await SharedPreferences.getInstance();
Expand Down Expand Up @@ -263,7 +275,8 @@ class PostBloc extends Bloc<PostEvent, PostState> {
state.copyWith(
selectedCommentId: null,
selectedCommentPath: null,
status: PostStatus.success,
newlyCreatedCommentId: state.newlyCreatedCommentId,
status: searchWasInProgress ? PostStatus.searchInProgress : PostStatus.success,
comments: commentTree,
commentResponseMap: responseMap,
commentPage: 1,
Expand All @@ -286,7 +299,7 @@ class PostBloc extends Bloc<PostEvent, PostState> {
}
return;
}
emit(state.copyWith(status: PostStatus.refreshing));
emit(state.copyWith(status: PostStatus.refreshing, newlyCreatedCommentId: state.newlyCreatedCommentId));

GetCommentsResponse getCommentsResponse = await lemmy
.run(GetComments(
Expand Down Expand Up @@ -326,9 +339,10 @@ class PostBloc extends Bloc<PostEvent, PostState> {
// We'll add in a edge case here to stop fetching comments after theres no more comments to be fetched
return emit(state.copyWith(
sortType: sortType,
status: PostStatus.success,
status: searchWasInProgress ? PostStatus.searchInProgress : PostStatus.success,
selectedCommentPath: null,
selectedCommentId: null,
newlyCreatedCommentId: state.newlyCreatedCommentId,
comments: commentViewTree,
commentResponseMap: state.commentResponseMap,
commentPage: event.commentParentId != null ? 1 : state.commentPage + 1,
Expand Down Expand Up @@ -614,4 +628,87 @@ class PostBloc extends Bloc<PostEvent, PostState> {
return emit(state.copyWith(status: PostStatus.success, navigateCommentIndex: event.targetIndex, navigateCommentId: state.navigateCommentId + 1));
}
}

Future<void> _startCommentSearchEvent(StartCommentSearchEvent event, Emitter<PostState> emit) async {
if (event.commentMatches.isEmpty) {
return;
}

// Find the parent comment of the match
Comment? parentComment = findParent(event.commentMatches.first);

return emit(state.copyWith(
status: PostStatus.searchInProgress,
postView: null,
newlyCreatedCommentId: event.commentMatches.first.id,
commentMatches: event.commentMatches,
navigateCommentIndex: parentComment == null ? null : state.comments.indexOf(state.comments.firstWhere((c) => c.commentView?.comment.id == parentComment.id)) + 1,
navigateCommentId: state.navigateCommentId + 1,
));
}

Future<void> _continueCommentSearchEvent(ContinueCommentSearchEvent event, Emitter<PostState> emit) async {
if (state.commentMatches?.isNotEmpty != true) {
return;
}

int newSelectedCommentId = state.commentMatches!.first.id;
Comment? parentComment = findParent(state.commentMatches!.first);

// Try to select and navigate to the next match
Comment? existingSelectedComment = state.commentMatches!.firstWhereOrNull((c) => c.id == state.newlyCreatedCommentId);
if (state.newlyCreatedCommentId != null && existingSelectedComment != null) {
int index = state.commentMatches!.indexOf(existingSelectedComment);
if (index + 1 < state.commentMatches!.length && index + 1 >= 0) {
newSelectedCommentId = state.commentMatches![index + 1].id;

// Find the parent comment of the match
parentComment = findParent(state.commentMatches![index + 1]);
}
}

return emit(state.copyWith(
status: PostStatus.searchInProgress,
postView: null,
newlyCreatedCommentId: newSelectedCommentId,
navigateCommentIndex: parentComment == null ? null : state.comments.indexOf(state.comments.firstWhere((c) => c.commentView?.comment.id == parentComment!.id)) + 1,
navigateCommentId: state.navigateCommentId + 1,
));
}

Future<void> _endCommentSearchEvent(EndCommentSearchEvent event, Emitter<PostState> emit) async {
return emit(state.copyWith(
status: PostStatus.success,
newlyCreatedCommentId: null,
commentMatches: null,
));
}

/// Finds the parent [CommentViewTree] from the current [state]
/// which contains the given [comment] anywhere in its descendents.
Comment? findParent(Comment comment) {
/// Recursive function which checks if any child has the given [comment].
bool childrenContains(CommentViewTree commentViewTree, Comment comment) {
if (commentViewTree.replies.firstWhereOrNull((cvt) => cvt.commentView?.comment.id == comment.id) != null) {
return true;
} else {
for (CommentViewTree child in commentViewTree.replies) {
if (childrenContains(child, comment)) {
return true;
}
}
}

return false;
}

// Only iterate through top-level comments.
for (CommentViewTree commentViewTree in state.comments) {
if (commentViewTree.commentView!.comment.id == comment.id || childrenContains(commentViewTree, comment)) {
return commentViewTree.commentView!.comment;
}
}

return null;
}
}
14 changes: 14 additions & 0 deletions lib/post/bloc/post_event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,20 @@ class NavigateCommentEvent extends PostEvent {
const NavigateCommentEvent({required this.targetIndex, required this.direction});
}

class StartCommentSearchEvent extends PostEvent {
final List<Comment> commentMatches;

const StartCommentSearchEvent({required this.commentMatches});
}

class ContinueCommentSearchEvent extends PostEvent {
const ContinueCommentSearchEvent();
}

class EndCommentSearchEvent extends PostEvent {
const EndCommentSearchEvent();
}

class ReportCommentEvent extends PostEvent {
final int commentId;
final String message;
Expand Down
15 changes: 14 additions & 1 deletion lib/post/bloc/post_state.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
part of 'post_bloc.dart';

enum PostStatus { initial, loading, refreshing, success, empty, failure }
enum PostStatus {
initial,
loading,
refreshing,
success,
empty,
failure,
searchInProgress,
}

class PostState extends Equatable {
const PostState({
Expand All @@ -25,6 +33,7 @@ class PostState extends Equatable {
this.viewAllCommentsRefresh = false,
this.navigateCommentIndex = 0,
this.navigateCommentId = 0,
this.commentMatches,
});

final PostStatus status;
Expand Down Expand Up @@ -57,6 +66,7 @@ class PostState extends Equatable {
final String? errorMessage;

final int navigateCommentIndex;
final List<Comment>? commentMatches;

// This exists purely for forcing the bloc to refire
// even if the comment index doesn't change
Expand Down Expand Up @@ -84,6 +94,7 @@ class PostState extends Equatable {
bool? viewAllCommentsRefresh = false,
int? navigateCommentIndex,
int? navigateCommentId,
List<Comment>? commentMatches,
}) {
return PostState(
status: status,
Expand All @@ -107,6 +118,7 @@ class PostState extends Equatable {
viewAllCommentsRefresh: viewAllCommentsRefresh ?? false,
navigateCommentIndex: navigateCommentIndex ?? 0,
navigateCommentId: navigateCommentId ?? 0,
commentMatches: commentMatches ?? this.commentMatches,
);
}

Expand All @@ -132,5 +144,6 @@ class PostState extends Equatable {
moddingCommentId,
navigateCommentIndex,
navigateCommentId,
commentMatches,
];
}
Loading
Loading