diff --git a/comments/constants.py b/comments/constants.py index 1413794396..ca7799f74c 100644 --- a/comments/constants.py +++ b/comments/constants.py @@ -4,3 +4,10 @@ class CommentReportType(models.TextChoices): SPAM = "spam" VIOLATION = "violation" + + +class TimeWindow(models.TextChoices): + ALL_TIME = "all_time" + PAST_WEEK = "past_week" + PAST_MONTH = "past_month" + PAST_YEAR = "past_year" diff --git a/comments/migrations/0024_add_denormalized_fields.py b/comments/migrations/0024_add_denormalized_fields.py index 4636620ce7..dbb5860c9a 100644 --- a/comments/migrations/0024_add_denormalized_fields.py +++ b/comments/migrations/0024_add_denormalized_fields.py @@ -3,7 +3,50 @@ import django.contrib.postgres.indexes import django.contrib.postgres.search from django.conf import settings -from django.db import migrations, models +from django.db import connection, migrations, models + + +def backfill_vote_score(apps, schema_editor): + with connection.cursor() as cursor: + cursor.execute(""" + UPDATE comments_comment c + SET vote_score = COALESCE(sub.score, 0) + FROM ( + SELECT comment_id, SUM(direction) AS score + FROM comments_commentvote + GROUP BY comment_id + ) sub + WHERE c.id = sub.comment_id + """) + print(f"\n vote_score: {cursor.rowcount} rows updated") + + +def backfill_cmm_count(apps, schema_editor): + with connection.cursor() as cursor: + cursor.execute(""" + UPDATE comments_comment c + SET cmm_count = sub.cnt + FROM ( + SELECT comment_id, COUNT(*) AS cnt + FROM comments_changedmymindentry + GROUP BY comment_id + ) sub + WHERE c.id = sub.comment_id + """) + print(f"\n cmm_count: {cursor.rowcount} rows updated") + + +def backfill_text_original_search_vector(apps, schema_editor): + with connection.cursor() as cursor: + cursor.execute(""" + UPDATE comments_comment + SET text_original_search_vector = to_tsvector('english', COALESCE(text_original, '')) + WHERE text_original IS NOT NULL + AND text_original != '' + AND is_private = false + AND is_soft_deleted = false + """) + print(f"\n text_original_search_vector: {cursor.rowcount} rows updated") class Migration(migrations.Migration): @@ -45,4 +88,9 @@ class Migration(migrations.Migration): name="comment_text_search_vector_idx", ), ), + migrations.RunPython(backfill_vote_score, migrations.RunPython.noop), + migrations.RunPython(backfill_cmm_count, migrations.RunPython.noop), + migrations.RunPython( + backfill_text_original_search_vector, migrations.RunPython.noop + ), ] diff --git a/comments/models.py b/comments/models.py index 9a8a71128b..cac77d8efa 100644 --- a/comments/models.py +++ b/comments/models.py @@ -18,7 +18,6 @@ from django.db.models.functions import Coalesce from django.db.models.lookups import Exact from django.utils import timezone -from sql_util.aggregates import SubqueryAggregate from posts.models import Post from projects.models import Project @@ -28,15 +27,6 @@ class CommentQuerySet(models.QuerySet): - def annotate_vote_score(self): - return self.annotate( - annotated_vote_score=Coalesce( - SubqueryAggregate("comment_votes__direction", aggregate=Sum), - 0, - output_field=IntegerField(), - ) - ) - def annotate_user_vote(self, user: User): """ Annotates queryset with the user's vote option @@ -180,12 +170,14 @@ def update_vote_score(self): ] self.vote_score = score self.save(update_fields=["vote_score"]) + return score def update_cmm_count(self): count = self.changedmymindentry_set.count() self.cmm_count = count self.save(update_fields=["cmm_count"]) + return count diff --git a/comments/serializers/common.py b/comments/serializers/common.py index fac39366d3..255cd9f5a7 100644 --- a/comments/serializers/common.py +++ b/comments/serializers/common.py @@ -5,6 +5,7 @@ from rest_framework import serializers from rest_framework.exceptions import ValidationError +from comments.constants import TimeWindow from comments.models import Comment, KeyFactor, CommentsOfTheWeekEntry from comments.utils import comments_extract_user_mentions_mapping from posts.models import Post @@ -27,6 +28,18 @@ class CommentFilterSerializer(serializers.Serializer): is_private = serializers.BooleanField(required=False, allow_null=True) include_deleted = serializers.BooleanField(required=False, allow_null=True) last_viewed_at = serializers.DateTimeField(required=False, allow_null=True) + time_window = serializers.ChoiceField( + choices=TimeWindow.choices, + required=False, + allow_null=True, + ) + search = serializers.CharField(required=False, allow_null=True, min_length=3) + exclude_bots = serializers.BooleanField(required=False, default=False) + post_status = serializers.ChoiceField( + choices=Post.CurationStatus.choices, + required=False, + allow_null=True, + ) def validate_post(self, value: int): try: @@ -34,6 +47,14 @@ def validate_post(self, value: int): except Post.DoesNotExist: raise ValidationError("Post Does not exist") + def validate(self, attrs): + sort = attrs.get("sort") + search = attrs.get("search") + if sort == "relevance" and not search: + raise ValidationError({"sort": "Relevance sort requires a search query."}) + + return attrs + class CommentSerializer(serializers.ModelSerializer): author = BaseUserSerializer() @@ -196,8 +217,6 @@ def serialize_comment_many( qs = qs.select_related( "included_forecast__question", "author", "on_post" ).prefetch_related("key_factors") - qs = qs.annotate_vote_score() - if current_user: qs = qs.annotate_user_vote(current_user) diff --git a/comments/services/common.py b/comments/services/common.py index f1bfe1c3a0..3a5a65fb0f 100644 --- a/comments/services/common.py +++ b/comments/services/common.py @@ -1,7 +1,7 @@ import datetime import difflib -from django.db import transaction +from django.db import IntegrityError, transaction from django.db.models import ( F, Sum, @@ -267,6 +267,40 @@ def compute_comment_score( return score +def vote_comment(comment: Comment, user: User, direction: int | None) -> int: + try: + with transaction.atomic(): + CommentVote.objects.filter(user=user, comment=comment).delete() + + if direction: + CommentVote.objects.create( + user=user, comment=comment, direction=direction + ) + except IntegrityError: + pass + + return comment.update_vote_score() + + +def toggle_cmm(comment: Comment, user: User, enabled: bool) -> bool | None: + """Returns True if created, False if deleted, None if no-op.""" + try: + with transaction.atomic(): + cmm = ChangedMyMindEntry.objects.filter(user=user, comment=comment) + + if not enabled and cmm.exists(): + cmm.delete() + comment.update_cmm_count() + return False + + if enabled and not cmm.exists(): + ChangedMyMindEntry.objects.create(user=user, comment=comment) + comment.update_cmm_count() + return True + except IntegrityError: + pass + + def set_comment_excluded_from_week_top(comment: Comment, excluded: bool = True): entry = comment.comments_of_the_week_entry if entry: diff --git a/comments/services/feed.py b/comments/services/feed.py index 654f5d032a..2bd03546c3 100644 --- a/comments/services/feed.py +++ b/comments/services/feed.py @@ -1,8 +1,18 @@ -from datetime import datetime +from datetime import datetime, timedelta -from django.db.models import Q, Case, When, Value, IntegerField, Exists, OuterRef +from django.contrib.postgres.search import SearchQuery, SearchRank +from django.db.models import F, Q, Case, When, Value, IntegerField, Exists, OuterRef +from django.utils import timezone +from comments.constants import TimeWindow from comments.models import Comment +from posts.models import Post + +TIME_WINDOW_DELTAS = { + TimeWindow.PAST_WEEK: timedelta(days=7), + TimeWindow.PAST_MONTH: timedelta(days=30), + TimeWindow.PAST_YEAR: timedelta(days=365), +} def get_comments_feed( @@ -15,16 +25,19 @@ def get_comments_feed( sort=None, is_private=None, focus_comment_id: int = None, - include_deleted=False, + include_deleted: bool | None = None, last_viewed_at: datetime = None, + time_window: str = None, + search: str = None, + exclude_bots: bool = False, + post_status: Post.CurationStatus | None = None, ): user = user if user and user.is_authenticated else None sort = sort or "-created_at" order_by_args = [] - # Require at least one filter - if not post and not author and not (is_private and user): - return qs.none() + if post_status: + qs = qs.filter(on_post__curation_status=post_status) if parent_isnull is not None: qs = qs.filter(parent=None) @@ -83,7 +96,10 @@ def get_comments_feed( else: qs = qs.filter(is_private=False) - if not include_deleted: + if exclude_bots: + qs = qs.filter(author__is_bot=False) + + if include_deleted is None: qs = qs.filter( Q(is_soft_deleted=False) | Exists( @@ -91,6 +107,23 @@ def get_comments_feed( ) ) + if include_deleted is False: + qs = qs.filter(is_soft_deleted=False) + + # Time window filter + if time_window and time_window in TIME_WINDOW_DELTAS: + cutoff = timezone.now() - TIME_WINDOW_DELTAS[time_window] + qs = qs.filter(created_at__gte=cutoff) + + # Full-text search using stored search vector + if search: + query = SearchQuery(search, search_type="websearch", config="english") + qs = qs.filter(text_original_search_vector=query) + if sort == "relevance": + qs = qs.annotate( + search_rank=SearchRank(F("text_original_search_vector"), query) + ) + # Filter comments located under Posts current user is allowed to see qs = qs.filter_by_user_permission(user=user) @@ -124,11 +157,10 @@ def get_comments_feed( order_by_args.insert(pinned_idx, "-is_focused_comment") if sort: - if "vote_score" in sort: - qs = qs.annotate_vote_score() - sort = sort.replace("vote_score", "annotated_vote_score") - - order_by_args.append(sort) + if sort == "relevance": + order_by_args.append("-search_rank") + else: + order_by_args.append(sort) if order_by_args: qs = qs.order_by(*order_by_args) diff --git a/comments/views/common.py b/comments/views/common.py index a3e5bc4ff0..1b24345e31 100644 --- a/comments/views/common.py +++ b/comments/views/common.py @@ -11,7 +11,6 @@ from comments.constants import CommentReportType from comments.models import ( - ChangedMyMindEntry, Comment, CommentVote, CommentsOfTheWeekEntry, @@ -31,6 +30,8 @@ unpin_comment, soft_delete_comment, update_comment, + vote_comment, + toggle_cmm, ) from comments.services.feed import get_comments_feed from comments.services.key_factors.common import create_key_factors @@ -38,7 +39,7 @@ from posts.services.common import get_post_permission_for_user from projects.permissions import ObjectPermission from users.models import User -from utils.paginator import LimitOffsetPagination +from utils.paginator import LimitOffsetPagination, CountlessLimitOffsetPagination class RootCommentsPagination(LimitOffsetPagination): @@ -99,7 +100,7 @@ def comments_list_api_view(request: Request): paginator = ( RootCommentsPagination() if use_root_comments_pagination - else LimitOffsetPagination() + else CountlessLimitOffsetPagination() ) paginated_comments = paginator.paginate_queryset(comments, request) @@ -199,57 +200,40 @@ def comment_edit_api_view(request: Request, pk: int): @api_view(["POST"]) -@transaction.atomic def comment_vote_api_view(request: Request, pk: int): comment = get_object_or_404(Comment, pk=pk) + user: User = request.user - permission = get_post_permission_for_user(comment.on_post, user=request.user) + permission = get_post_permission_for_user(comment.on_post, user=user) ObjectPermission.can_view(permission, raise_exception=True) - if comment.author_id == request.user.pk: + if comment.author_id == user.id: raise ValidationError("You can not vote your own comment.") direction = serializers.ChoiceField( required=False, allow_null=True, choices=CommentVote.VoteDirection.choices ).run_validation(request.data.get("vote")) - # Deleting existing vote - CommentVote.objects.filter(user=request.user, comment=comment).delete() - - if direction: - CommentVote.objects.create( - user=request.user, comment=comment, direction=direction - ) - - score = comment.update_vote_score() + score = vote_comment(comment, user, direction) return Response({"score": score}) @api_view(["POST"]) @permission_classes([IsAuthenticated]) -@transaction.atomic def comment_toggle_cmm_view(request, pk=int): enabled = request.data.get("enabled", False) comment = get_object_or_404(Comment, pk=pk) - user = request.user - cmm = ChangedMyMindEntry.objects.filter(user=user, comment=comment) - - if not enabled and cmm.exists(): - cmm.delete() - comment.update_cmm_count() - return Response(status=status.HTTP_200_OK) + result = toggle_cmm(comment, request.user, enabled) - if not cmm.exists(): - ChangedMyMindEntry.objects.create(user=user, comment=comment) - comment.update_cmm_count() - return Response(status=status.HTTP_200_OK) + if result is None: + return Response( + {"error": "Already set as changed my mind"}, + status=status.HTTP_400_BAD_REQUEST, + ) - return Response( - {"error": "Already set as changed my mind"}, - status=status.HTTP_400_BAD_REQUEST, - ) + return Response(status=status.HTTP_200_OK) @api_view(["POST"]) diff --git a/front_end/messages/cs.json b/front_end/messages/cs.json index b890493692..854d2a39bb 100644 --- a/front_end/messages/cs.json +++ b/front_end/messages/cs.json @@ -2071,5 +2071,22 @@ "openInAggregationExplorer": "Otevřít v Průzkumníku agregací", "switchBackToSlidersHint": "přepněte zpět na posuvníky pro plynulé přizpůsobení", "view": "Zobrazit", - "thousandsOfOpenQuestions": "20 000+ otevřených otázek" + "thousandsOfOpenQuestions": "20 000+ otevřených otázek", + "commentsFeed": "Comments", + "commentsFeedTitle": "Comments Feed", + "sortRecent": "Recent", + "sortMostUpvoted": "Most Upvoted", + "sortMostMindsChanged": "Most Minds Changed", + "sortRelevance": "Relevance", + "timeWindow": "Časové období", + "bots": "Boti", + "timeWindowAllTime": "All Time", + "timeWindowPastWeek": "Past Week", + "timeWindowPastMonth": "Past Month", + "timeWindowPastYear": "Past Year", + "excludeBots": "Vyloučit boty", + "includeBots": "Zahrnout boty", + "searchComments": "Search comments...", + "loadMoreComments": "Load More", + "noCommentsFound": "No comments found" } diff --git a/front_end/messages/en.json b/front_end/messages/en.json index 8c167b41a2..293cfa78ba 100644 --- a/front_end/messages/en.json +++ b/front_end/messages/en.json @@ -2064,5 +2064,22 @@ "publicForecasters": "public forecasters", "yourInternalExperts": "your internal experts", "view": "View", - "feedTileSummaryPlaceholder": "Optional: Enter a custom summary text to display on feed tiles (if not provided, a summary will be auto-generated from the notebook content)" + "feedTileSummaryPlaceholder": "Optional: Enter a custom summary text to display on feed tiles (if not provided, a summary will be auto-generated from the notebook content)", + "commentsFeed": "Comments", + "commentsFeedTitle": "Comments Feed", + "sortRecent": "Recent", + "sortMostUpvoted": "Most Upvoted", + "sortMostMindsChanged": "Most Minds Changed", + "sortRelevance": "Relevance", + "timeWindow": "Time Window", + "bots": "Bots", + "timeWindowAllTime": "All Time", + "timeWindowPastWeek": "Past Week", + "timeWindowPastMonth": "Past Month", + "timeWindowPastYear": "Past Year", + "excludeBots": "Exclude Bots", + "includeBots": "Include Bots", + "searchComments": "Search comments...", + "loadMoreComments": "Load More", + "noCommentsFound": "No comments found" } diff --git a/front_end/messages/es.json b/front_end/messages/es.json index 781d7c3a65..012bb6990e 100644 --- a/front_end/messages/es.json +++ b/front_end/messages/es.json @@ -2071,5 +2071,22 @@ "openInAggregationExplorer": "Abrir en el Explorador de Agregación", "switchBackToSlidersHint": "vuelve a los deslizadores para un ajuste suave", "view": "Ver", - "thousandsOfOpenQuestions": "20,000+ preguntas abiertas" + "thousandsOfOpenQuestions": "20,000+ preguntas abiertas", + "commentsFeed": "Comments", + "commentsFeedTitle": "Comments Feed", + "sortRecent": "Recent", + "sortMostUpvoted": "Most Upvoted", + "sortMostMindsChanged": "Most Minds Changed", + "sortRelevance": "Relevance", + "timeWindow": "Período de tiempo", + "bots": "Bots", + "timeWindowAllTime": "All Time", + "timeWindowPastWeek": "Past Week", + "timeWindowPastMonth": "Past Month", + "timeWindowPastYear": "Past Year", + "excludeBots": "Excluir Bots", + "includeBots": "Incluir Bots", + "searchComments": "Search comments...", + "loadMoreComments": "Load More", + "noCommentsFound": "No comments found" } diff --git a/front_end/messages/pt.json b/front_end/messages/pt.json index 7d46728750..eb43aff821 100644 --- a/front_end/messages/pt.json +++ b/front_end/messages/pt.json @@ -2069,5 +2069,22 @@ "openInAggregationExplorer": "Abrir no Explorador de Agregações", "switchBackToSlidersHint": "volte para os controles deslizantes para um ajuste suave", "view": "Visualizar", - "thousandsOfOpenQuestions": "20.000+ perguntas abertas" + "thousandsOfOpenQuestions": "20.000+ perguntas abertas", + "commentsFeed": "Comments", + "commentsFeedTitle": "Comments Feed", + "sortRecent": "Recent", + "sortMostUpvoted": "Most Upvoted", + "sortMostMindsChanged": "Most Minds Changed", + "sortRelevance": "Relevance", + "timeWindow": "Período de tempo", + "bots": "Bots", + "timeWindowAllTime": "All Time", + "timeWindowPastWeek": "Past Week", + "timeWindowPastMonth": "Past Month", + "timeWindowPastYear": "Past Year", + "excludeBots": "Excluir Bots", + "includeBots": "Incluir Bots", + "searchComments": "Search comments...", + "loadMoreComments": "Load More", + "noCommentsFound": "No comments found" } diff --git a/front_end/messages/zh-TW.json b/front_end/messages/zh-TW.json index 2132a14e32..1a57cd5c74 100644 --- a/front_end/messages/zh-TW.json +++ b/front_end/messages/zh-TW.json @@ -2068,5 +2068,22 @@ "openInAggregationExplorer": "在聚合探索器中開啟", "switchBackToSlidersHint": "切換回滑桿以平滑調整", "view": "檢視", - "thousandsOfOpenQuestions": "20,000+ 開放問題" + "thousandsOfOpenQuestions": "20,000+ 開放問題", + "commentsFeed": "Comments", + "commentsFeedTitle": "Comments Feed", + "sortRecent": "Recent", + "sortMostUpvoted": "Most Upvoted", + "sortMostMindsChanged": "Most Minds Changed", + "sortRelevance": "Relevance", + "timeWindow": "時間範圍", + "bots": "機器人", + "timeWindowAllTime": "All Time", + "timeWindowPastWeek": "Past Week", + "timeWindowPastMonth": "Past Month", + "timeWindowPastYear": "Past Year", + "excludeBots": "排除機器人", + "includeBots": "包含機器人", + "searchComments": "Search comments...", + "loadMoreComments": "Load More", + "noCommentsFound": "No comments found" } diff --git a/front_end/messages/zh.json b/front_end/messages/zh.json index cd2c8290fe..38ed84a6ee 100644 --- a/front_end/messages/zh.json +++ b/front_end/messages/zh.json @@ -2073,5 +2073,22 @@ "openInAggregationExplorer": "在聚合探索器中打开", "switchBackToSlidersHint": "切回滑块以进行更精细的调整", "view": "查看", - "thousandsOfOpenQuestions": "20,000+ 开放问题" + "thousandsOfOpenQuestions": "20,000+ 开放问题", + "commentsFeed": "Comments", + "commentsFeedTitle": "Comments Feed", + "sortRecent": "Recent", + "sortMostUpvoted": "Most Upvoted", + "sortMostMindsChanged": "Most Minds Changed", + "sortRelevance": "Relevance", + "timeWindow": "时间范围", + "bots": "机器人", + "timeWindowAllTime": "All Time", + "timeWindowPastWeek": "Past Week", + "timeWindowPastMonth": "Past Month", + "timeWindowPastYear": "Past Year", + "excludeBots": "排除机器人", + "includeBots": "包含机器人", + "searchComments": "Search comments...", + "loadMoreComments": "Load More", + "noCommentsFound": "No comments found" } diff --git a/front_end/src/app/(main)/components/headers/hooks/useNavbarLinks.tsx b/front_end/src/app/(main)/components/headers/hooks/useNavbarLinks.tsx index 364dc61ce7..55780a249d 100644 --- a/front_end/src/app/(main)/components/headers/hooks/useNavbarLinks.tsx +++ b/front_end/src/app/(main)/components/headers/hooks/useNavbarLinks.tsx @@ -63,6 +63,10 @@ const useNavbarLinks = ({ label: t("communities"), href: "/questions/?communities=true", }, + commentsFeed: { + label: t("commentsFeedTitle"), + href: "/questions/?comments_feed=true", + }, about: { label: t("aboutMetaculus"), href: "/about/", @@ -173,7 +177,7 @@ const useNavbarLinks = ({ const menuLinks = useMemo(() => { // common links that are always shown const links: NavbarLinkDefinition[] = [ - ...(PUBLIC_MINIMAL_UI ? [] : [LINKS.communities]), + ...(PUBLIC_MINIMAL_UI ? [] : [LINKS.communities, LINKS.commentsFeed]), LINKS.leaderboards, LINKS.trackRecord, LINKS.aggregationExplorer, @@ -201,6 +205,7 @@ const useNavbarLinks = ({ LINKS.aggregationExplorer, LINKS.aiBenchmark, LINKS.communities, + LINKS.commentsFeed, LINKS.createQuestion, LINKS.faq, LINKS.journal, diff --git a/front_end/src/app/(main)/questions/hooks/use_feed.tsx b/front_end/src/app/(main)/questions/hooks/use_feed.tsx index 6a978567ee..7e6e5e454f 100644 --- a/front_end/src/app/(main)/questions/hooks/use_feed.tsx +++ b/front_end/src/app/(main)/questions/hooks/use_feed.tsx @@ -11,6 +11,7 @@ import { POST_TOPIC_FILTER, POST_USERNAMES_FILTER, POST_WEEKLY_TOP_COMMENTS_FILTER, + POST_COMMENTS_FEED_FILTER, } from "@/constants/posts_feed"; import { useAuth } from "@/contexts/auth_context"; import useSearchParams from "@/hooks/use_search_params"; @@ -28,6 +29,7 @@ const useFeed = () => { const orderBy = params.get(POST_ORDER_BY_FILTER); const communities = params.get(POST_COMMUNITIES_FILTER); const weeklyTopComments = params.get(POST_WEEKLY_TOP_COMMENTS_FILTER); + const commentsFeed = params.get(POST_COMMENTS_FEED_FILTER); const currentFeed = useMemo(() => { if (selectedTopic) return null; @@ -47,6 +49,7 @@ const useFeed = () => { if (weeklyTopComments) { return FeedType.WEEKLY_TOP_COMMENTS; } + if (commentsFeed) return FeedType.COMMENTS_FEED; return FeedType.HOME; }, [ selectedTopic, @@ -56,6 +59,7 @@ const useFeed = () => { communities, user, weeklyTopComments, + commentsFeed, ]); const clearInReview = useCallback(() => { @@ -84,6 +88,8 @@ const useFeed = () => { return { [POST_COMMUNITIES_FILTER]: "true" }; case FeedType.WEEKLY_TOP_COMMENTS: return { [POST_WEEKLY_TOP_COMMENTS_FILTER]: "true" }; + case FeedType.COMMENTS_FEED: + return { [POST_COMMENTS_FEED_FILTER]: "true" }; case FeedType.FOLLOWING: return { [POST_FOLLOWING_FILTER]: "true" }; case FeedType.HOME: diff --git a/front_end/src/app/(main)/questions/page.tsx b/front_end/src/app/(main)/questions/page.tsx index c45f708c12..b156f37b10 100644 --- a/front_end/src/app/(main)/questions/page.tsx +++ b/front_end/src/app/(main)/questions/page.tsx @@ -2,12 +2,14 @@ import { isNil } from "lodash"; import { Suspense } from "react"; import FeedSidebar from "@/app/(main)/questions/components/sidebar"; +import CommentFeedContent from "@/components/comment_feed/comment_feed_content"; import AwaitedCommunitiesFeed from "@/components/communities_feed"; import OnboardingCheck from "@/components/onboarding/onboarding_check"; import AwaitedPostsFeed from "@/components/posts_feed"; import LoadingIndicator from "@/components/ui/loading_indicator"; import AwaitedWeeklyTopCommentsFeed from "@/components/weekly_top_comments_feed"; import { + POST_COMMENTS_FEED_FILTER, POST_COMMUNITIES_FILTER, POST_PAGE_FILTER, POST_WEEKLY_TOP_COMMENTS_FILTER, @@ -35,6 +37,7 @@ export default async function Questions(props: { const searchParams = await props.searchParams; const isCommunityFeed = searchParams[POST_COMMUNITIES_FILTER]; const isWeeklyTopCommentsFeed = searchParams[POST_WEEKLY_TOP_COMMENTS_FILTER]; + const isCommentsFeed = searchParams[POST_COMMENTS_FEED_FILTER]; const filters = generateFiltersFromSearchParams(searchParams, { // Default Feed ordering should be hotness defaultOrderBy: QuestionOrder.HotDesc, @@ -51,7 +54,9 @@ export default async function Questions(props: {
+ {t("noCommentsFound")} +
+ )} + {hasMore && comments.length > 0 && ( +