Skip to content

Updated ActivityPub to use dynamic topics and recommendations from API#25646

Merged
minimaluminium merged 11 commits intomainfrom
ap-topics-recommendations-BER-3098-3041-3025
Dec 9, 2025
Merged

Updated ActivityPub to use dynamic topics and recommendations from API#25646
minimaluminium merged 11 commits intomainfrom
ap-topics-recommendations-BER-3098-3041-3025

Conversation

@minimaluminium
Copy link
Member

@minimaluminium minimaluminium commented Dec 8, 2025

ref https://linear.app/ghost/issue/BER-3098/hide-discovery-feeds-tabs-for-self-hosters
ref https://linear.app/ghost/issue/BER-3041/point-activitypub-explore-to-ghost-explore-axis-for-self-hosters
ref https://linear.app/ghost/issue/BER-3025/update-sidebar-in-feed-recommendations-to-use-new-explore-data

  • Added API methods for fetching topics and recommendations from new endpoints
  • Updated topic filter to use /topics endpoint instead of hardcoded list
  • Conditionally hide topics and recommendations UI when endpoints return empty
  • Link Explore to external discovery page when no topics available
  • Updated suggested profiles to use /recommendations endpoint with server-side randomization

Note

Use new API endpoints for topics and recommendations, update hooks and UI to load dynamically, and conditionally show/hide Explore/topic UI (with external link fallback).

  • API:
    • Add getTopics() and getRecommendations(limit?) to ActivityPubAPI with types (TopicData, GetTopicsResponse, GetRecommendationsResponse).
  • Data hooks:
    • Add QUERY_KEYS.topics and useTopicsForUser().
    • Update useSuggestedProfilesForUser() to fetch from getRecommendations() and cache accounts.
    • Remove legacy JSON-based explore/suggestions logic.
  • UI/UX:
    • Topic chips now dynamic: TopicFilter reads from /topics (type Topic = string; uses slug/name).
    • Conditionally render topic filter and suggested profiles only when data exists.
    • Sidebar "Explore": internal link when topics exist; otherwise external link to https://explore.ghost.org/social-web.
    • Inbox empty state CTA mirrors the above internal/external behavior.
    • Layout/Header border logic and Onboarding Step3 navigation depend on presence of topics.
  • Version:
    • Bump @tryghost/activitypub to 3.0.0.

Written by Cursor Bugbot for commit 086cb83. This will update automatically on new commits. Configure here.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 8, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Bumps apps/activitypub package version from 2.0.5 to 3.0.0. Adds TopicData, GetTopicsResponse, GetRecommendationsResponse interfaces and getTopics/getRecommendations methods to ActivityPubAPI. Introduces a new useTopicsForUser hook and updates useSuggestedProfilesForUser to use getRecommendations and cache accounts. TopicFilter now sources topics from the API and widens Topic to string. Multiple UI components (Layout, Sidebar, InboxList, Search, SuggestedProfiles, Feed SuggestedProfiles, Onboarding Step3) are updated to be topic-aware or conditionally render based on topics. Several components stop showing/updating followerCount and SuggestedProfiles consolidates scroll-button logic.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Focus areas:
    • apps/activitypub/package.json — version bump (2.0.5 → 3.0.0).
    • apps/activitypub/src/api/activitypub.ts — new interfaces and API methods.
    • apps/activitypub/src/hooks/use-activity-pub-queries.ts — new hook, QUERY_KEYS additions, cache/updater behavior, and removals of helpers.
    • apps/activitypub/src/components/TopicFilter.tsx — Topic type change and API-driven topics; check callers and types.
    • apps/activitypub/src/components/layout/Layout.tsx and Sidebar/Sidebar.tsx — hasTopics logic affecting header border and Explore link behavior.
    • apps/activitypub/src/components/modals/Search.tsx and views/Inbox/components/InboxList.tsx — conditional rendering based on topics/suggestions.
    • apps/activitypub/src/components/global/SuggestedProfiles.tsx and views/Feed/components/SuggestedProfiles.tsx — followerCount removal and scroll-button logic consolidation.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: updating ActivityPub to use dynamic topics and recommendations from API instead of hardcoded values.
Description check ✅ Passed The pull request description clearly relates to the changeset by outlining API additions, data hooks modifications, and UI/UX updates that align with the code changes.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ap-topics-recommendations-BER-3098-3041-3025

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
apps/activitypub/src/components/TopicFilter.tsx (1)

14-22: Consider exposing loading state for better UX.

When topicsQuery is loading, apiTopics will be an empty array, showing only the "Following" topic. This could cause a brief flash where users see fewer topics before the full list appears. Depending on the parent component's needs, consider:

  1. Exposing isLoading from the hook so parents can show a skeleton/placeholder
  2. Or returning early with a loading indicator if the full topic list is important

If the current behavior (gracefully degrading to just "Following" during load) is intentional, this is fine as-is.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 25af482 and d8237db.

📒 Files selected for processing (10)
  • apps/activitypub/package.json (1 hunks)
  • apps/activitypub/src/api/activitypub.ts (2 hunks)
  • apps/activitypub/src/components/TopicFilter.tsx (3 hunks)
  • apps/activitypub/src/components/global/SuggestedProfiles.tsx (1 hunks)
  • apps/activitypub/src/components/layout/Layout.tsx (3 hunks)
  • apps/activitypub/src/components/layout/Sidebar/Sidebar.tsx (3 hunks)
  • apps/activitypub/src/components/modals/Search.tsx (3 hunks)
  • apps/activitypub/src/hooks/use-activity-pub-queries.ts (3 hunks)
  • apps/activitypub/src/views/Feed/components/SuggestedProfiles.tsx (1 hunks)
  • apps/activitypub/src/views/Inbox/components/InboxList.tsx (3 hunks)
🧰 Additional context used
🧠 Learnings (10)
📚 Learning: 2025-03-13T09:00:20.205Z
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 22471
File: apps/admin-x-activitypub/src/utils/pending-activity.ts:13-71
Timestamp: 2025-03-13T09:00:20.205Z
Learning: The pending activity utilities in the Ghost ActivityPub module are covered by tests in the file `apps/admin-x-activitypub/test/unit/utils/pending-activity.ts`.

Applied to files:

  • apps/activitypub/package.json
  • apps/activitypub/src/components/modals/Search.tsx
📚 Learning: 2025-03-13T09:00:20.205Z
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 22471
File: apps/admin-x-activitypub/src/utils/pending-activity.ts:13-71
Timestamp: 2025-03-13T09:00:20.205Z
Learning: The pending activity utilities in Ghost's ActivityPub module are thoroughly tested in `apps/admin-x-activitypub/test/unit/utils/pending-activity.ts`, which covers `generatePendingActivityId`, `isPendingActivity`, and `generatePendingActivity` functions.

Applied to files:

  • apps/activitypub/package.json
  • apps/activitypub/src/components/modals/Search.tsx
📚 Learning: 2025-03-13T09:02:50.102Z
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 22471
File: apps/admin-x-activitypub/src/views/Feed/components/NewPostModal.tsx:29-34
Timestamp: 2025-03-13T09:02:50.102Z
Learning: In the Ghost ActivityPub module, error handling for mutations is handled at the hook level (in use-activity-pub-queries.ts) rather than in individual components. This allows for centralized error handling across the application.

Applied to files:

  • apps/activitypub/package.json
  • apps/activitypub/src/components/modals/Search.tsx
📚 Learning: 2025-11-26T11:05:59.314Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: apps/shade/AGENTS.md:0-0
Timestamp: 2025-11-26T11:05:59.314Z
Learning: Applies to apps/shade/src/components/layout/**/*.{ts,tsx} : Reusable layout containers (Page, Heading, Header, ViewHeader, ErrorPage) should be placed in `src/components/layout/*`

Applied to files:

  • apps/activitypub/src/components/layout/Layout.tsx
📚 Learning: 2025-11-26T11:05:59.314Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: apps/shade/AGENTS.md:0-0
Timestamp: 2025-11-26T11:05:59.314Z
Learning: Applies to apps/shade/src/components/**/*.{ts,tsx} : Prefer compound subcomponents (e.g., `Header.Title`, `Header.Meta`, `Header.Actions`) for multi-region components instead of many props

Applied to files:

  • apps/activitypub/src/components/layout/Layout.tsx
📚 Learning: 2025-11-25T14:28:50.351Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T14:28:50.351Z
Learning: Use React with Vite for Admin app development (admin-x-settings, admin-x-activitypub, posts, stats)

Applied to files:

  • apps/activitypub/src/components/layout/Layout.tsx
  • apps/activitypub/src/components/modals/Search.tsx
📚 Learning: 2025-03-13T09:02:50.102Z
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 22471
File: apps/admin-x-activitypub/src/views/Feed/components/NewPostModal.tsx:29-34
Timestamp: 2025-03-13T09:02:50.102Z
Learning: In the ActivityPub module, error handling for mutations is handled at the hook level (in use-activity-pub-queries.ts) rather than in individual components. This allows for centralized error handling across the application.

Applied to files:

  • apps/activitypub/src/components/modals/Search.tsx
📚 Learning: 2025-11-26T11:05:59.314Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: apps/shade/AGENTS.md:0-0
Timestamp: 2025-11-26T11:05:59.314Z
Learning: Applies to apps/shade/src/hooks/**/*.{ts,tsx} : Custom React hooks should be placed in `src/hooks/*`

Applied to files:

  • apps/activitypub/src/components/TopicFilter.tsx
📚 Learning: 2025-10-27T11:59:33.968Z
Learnt from: peterzimon
Repo: TryGhost/Ghost PR: 25261
File: apps/admin/src/layout/app-sidebar/UserMenu.tsx:24-29
Timestamp: 2025-10-27T11:59:33.968Z
Learning: In apps/admin/src/layout/app-sidebar/UserMenu.tsx, the hardcoded placeholder data (avatar URL, initials "US", name "User Name", email "userexample.com") is a known limitation and should not be flagged for replacement with real user data.

Applied to files:

  • apps/activitypub/src/components/layout/Sidebar/Sidebar.tsx
📚 Learning: 2025-11-05T16:49:21.124Z
Learnt from: rob-ghost
Repo: TryGhost/Ghost PR: 25356
File: apps/admin/src/hooks/user-preferences.ts:20-20
Timestamp: 2025-11-05T16:49:21.124Z
Learning: In apps/admin/src/hooks/user-preferences.ts, the query key for user preferences intentionally includes `user?.accessibility` (not just `user?.id`) so the cache automatically reacts to changes from any source (useEditUserPreferences, other editUser calls, external updates). Combined with `cacheTime: 0`, this design garbage-collects orphaned entries while keeping the active entry cached. This is a documented, intentional architectural decision.

Applied to files:

  • apps/activitypub/src/hooks/use-activity-pub-queries.ts
🧬 Code graph analysis (6)
apps/activitypub/src/views/Inbox/components/InboxList.tsx (1)
apps/activitypub/src/hooks/use-activity-pub-queries.ts (1)
  • useTopicsForUser (2722-2735)
apps/activitypub/src/components/layout/Layout.tsx (1)
apps/activitypub/src/hooks/use-activity-pub-queries.ts (1)
  • useTopicsForUser (2722-2735)
apps/activitypub/src/components/modals/Search.tsx (1)
apps/activitypub/src/hooks/use-activity-pub-queries.ts (1)
  • useSuggestedProfilesForUser (2677-2720)
apps/activitypub/src/components/TopicFilter.tsx (1)
apps/activitypub/src/hooks/use-activity-pub-queries.ts (1)
  • useTopicsForUser (2722-2735)
apps/activitypub/src/components/layout/Sidebar/Sidebar.tsx (2)
apps/activitypub/src/hooks/use-activity-pub-queries.ts (1)
  • useTopicsForUser (2722-2735)
ghost/core/core/server/services/koenig/node-renderers/image-renderer.js (1)
  • a (114-114)
apps/activitypub/src/hooks/use-activity-pub-queries.ts (1)
apps/activitypub/src/api/activitypub.ts (1)
  • Account (9-31)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Cursor Bugbot
  • GitHub Check: Setup
  • GitHub Check: Build & Push
  • GitHub Check: Setup
🔇 Additional comments (16)
apps/activitypub/package.json (1)

3-3: LGTM: Version bump is appropriate.

The patch version increment aligns with the new features and API additions introduced in this PR.

apps/activitypub/src/api/activitypub.ts (2)

43-54: LGTM: Well-defined interfaces.

The new TypeScript interfaces for topics and recommendations are clean and follow the existing patterns in the codebase.


492-509: Backend endpoints are not implemented.

The getTopics() and getRecommendations() methods call .ghost/activitypub/v1/topics and .ghost/activitypub/v1/recommendations endpoints, but these endpoints do not exist in the backend. Without corresponding server-side route handlers, these methods will fail at runtime. Either implement the missing endpoints or remove these unused methods.

⛔ Skipped due to learnings
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T14:28:50.351Z
Learning: API routes should be defined in `ghost/core/core/server/api/`
apps/activitypub/src/components/global/SuggestedProfiles.tsx (1)

84-86: LGTM: Good defensive rendering.

The early return prevents rendering empty UI sections when there are no suggested profiles to display.

apps/activitypub/src/views/Inbox/components/InboxList.tsx (2)

11-11: LGTM: Proper integration of topics data.

The hook usage follows the established pattern and provides a safe fallback when the topics query fails (hasTopics will be false if topicsData is undefined).

Also applies to: 36-38


79-79: LGTM: Conditional topic filter rendering.

The TopicFilter is appropriately hidden when no topics are available, preventing an empty or broken UI state.

apps/activitypub/src/views/Feed/components/SuggestedProfiles.tsx (1)

19-30: LGTM: Deduplication of scroll button logic.

Moving updateScrollButtons to the top eliminates the duplicate definition and makes it accessible to all consumers within the component.

apps/activitypub/src/components/layout/Layout.tsx (1)

11-11: LGTM: Topic-aware border rendering.

The layout now conditionally adjusts the header border based on topic availability for the reader page, maintaining visual consistency with the topic filter's presence.

Also applies to: 20-22, 51-51

apps/activitypub/src/components/modals/Search.tsx (2)

9-9: LGTM: Integration of suggested profiles hook.

The hook usage is clean and the derived hasSuggestedProfiles state properly accounts for both loading and data presence.

Also applies to: 130-132


190-190: LGTM: Conditional rendering prevents empty sections.

The combined condition ensures the "More people to follow" section only appears when appropriate and when suggestions are actually available.

apps/activitypub/src/components/layout/Sidebar/Sidebar.tsx (2)

13-13: LGTM: Topic-aware sidebar rendering.

The hook integration follows the established pattern and provides the foundation for conditional Explore link behavior.

Also applies to: 29-31


79-96: LGTM: Conditional Explore link with external fallback.

When topics are unavailable, the external link to Ghost's discovery page provides users with an alternative way to find content. The link properly uses rel="noopener noreferrer" and includes an external link icon for clarity.

apps/activitypub/src/components/TopicFilter.tsx (1)

49-58: LGTM!

The rendering logic correctly uses slug for keys and comparisons, and displays the human-readable name. This aligns well with the new dynamic topics data structure.

apps/activitypub/src/hooks/use-activity-pub-queries.ts (3)

95-97: LGTM!

The topics query key follows the established pattern in QUERY_KEYS and the trailing comma improves consistency.


2684-2697: LGTM on the API migration.

The refactor from local filtering to using api.getRecommendations(limit) simplifies the data flow. The caching of individual accounts for follow mutations is preserved, and the null return for empty results is a reasonable design choice.


2722-2735: Verify retry: false is intentional for topics.

With retry: false, if the topics endpoint fails (e.g., temporary network issue), the query won't automatically retry, potentially leaving users with only the "Following" topic until a manual refresh or cache expiration.

If topics are critical to the explore experience, consider allowing at least one retry, or ensure the UI gracefully degrades (which it appears to do by showing "Following" only).

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the final PR Bugbot will review for you during this billing cycle

Your free Bugbot reviews will reset on January 3

Details

Your team is on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle for each member of your team.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
apps/activitypub/src/hooks/use-activity-pub-queries.ts (1)

96-96: Consider following the established pattern for parameterless query keys.

The topics key is defined as a function () => ['topics'], but other parameterless keys in this object (feed, inbox, discoveryFeed, postsByAccount, postsLikedByAccount) are defined as plain arrays. For consistency, consider changing this to:

-    topics: () => ['topics']
+    topics: ['topics']

Then update the usage on line 2512 from QUERY_KEYS.topics() to QUERY_KEYS.topics.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d8237db and c827657.

📒 Files selected for processing (1)
  • apps/activitypub/src/hooks/use-activity-pub-queries.ts (4 hunks)
🧰 Additional context used
🧠 Learnings (6)
📚 Learning: 2025-11-05T16:49:21.124Z
Learnt from: rob-ghost
Repo: TryGhost/Ghost PR: 25356
File: apps/admin/src/hooks/user-preferences.ts:20-20
Timestamp: 2025-11-05T16:49:21.124Z
Learning: In apps/admin/src/hooks/user-preferences.ts, the query key for user preferences intentionally includes `user?.accessibility` (not just `user?.id`) so the cache automatically reacts to changes from any source (useEditUserPreferences, other editUser calls, external updates). Combined with `cacheTime: 0`, this design garbage-collects orphaned entries while keeping the active entry cached. This is a documented, intentional architectural decision.

Applied to files:

  • apps/activitypub/src/hooks/use-activity-pub-queries.ts
📚 Learning: 2025-03-13T09:02:50.102Z
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 22471
File: apps/admin-x-activitypub/src/views/Feed/components/NewPostModal.tsx:29-34
Timestamp: 2025-03-13T09:02:50.102Z
Learning: In the ActivityPub module, error handling for mutations is handled at the hook level (in use-activity-pub-queries.ts) rather than in individual components. This allows for centralized error handling across the application.

Applied to files:

  • apps/activitypub/src/hooks/use-activity-pub-queries.ts
📚 Learning: 2025-03-04T17:19:50.908Z
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 22330
File: apps/admin-x-activitypub/src/hooks/use-activity-pub-queries.ts:911-912
Timestamp: 2025-03-04T17:19:50.908Z
Learning: In the ActivityPub app, posts can appear in multiple threads, so when updating thread caches (e.g., when deleting a post), using QUERY_KEYS.thread(null) is the appropriate approach to update all thread caches rather than targeting a specific thread.

Applied to files:

  • apps/activitypub/src/hooks/use-activity-pub-queries.ts
📚 Learning: 2025-03-13T09:02:50.102Z
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 22471
File: apps/admin-x-activitypub/src/views/Feed/components/NewPostModal.tsx:29-34
Timestamp: 2025-03-13T09:02:50.102Z
Learning: In the Ghost ActivityPub module, error handling for mutations is handled at the hook level (in use-activity-pub-queries.ts) rather than in individual components. This allows for centralized error handling across the application.

Applied to files:

  • apps/activitypub/src/hooks/use-activity-pub-queries.ts
📚 Learning: 2025-03-13T09:00:20.205Z
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 22471
File: apps/admin-x-activitypub/src/utils/pending-activity.ts:13-71
Timestamp: 2025-03-13T09:00:20.205Z
Learning: The pending activity utilities in Ghost's ActivityPub module are thoroughly tested in `apps/admin-x-activitypub/test/unit/utils/pending-activity.ts`, which covers `generatePendingActivityId`, `isPendingActivity`, and `generatePendingActivity` functions.

Applied to files:

  • apps/activitypub/src/hooks/use-activity-pub-queries.ts
📚 Learning: 2025-03-13T09:00:20.205Z
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 22471
File: apps/admin-x-activitypub/src/utils/pending-activity.ts:13-71
Timestamp: 2025-03-13T09:00:20.205Z
Learning: The pending activity utilities in the Ghost ActivityPub module are covered by tests in the file `apps/admin-x-activitypub/test/unit/utils/pending-activity.ts`.

Applied to files:

  • apps/activitypub/src/hooks/use-activity-pub-queries.ts
🧬 Code graph analysis (1)
apps/activitypub/src/hooks/use-activity-pub-queries.ts (1)
apps/activitypub/src/api/activitypub.ts (1)
  • Account (9-31)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Unit tests (Node 22.18.0)
  • GitHub Check: ActivityPub tests
  • GitHub Check: Lint
  • GitHub Check: Cursor Bugbot
  • GitHub Check: Build & Push
🔇 Additional comments (3)
apps/activitypub/src/hooks/use-activity-pub-queries.ts (3)

27-27: LGTM: Unused import removed.

Good cleanup removing the unused useMemo import.


510-523: Correct the line numbers: this review applies to lines 2510-2523, not 510-523.

The implementation follows the established patterns with appropriate caching (24-hour stale time for infrequently changing topics) and error handling (retry: false for graceful degradation). The getTopics API method is correctly called and the hook properly returns the query object.

Likely an incorrect or invalid review comment.


472-489: The review comment is incorrectly applied to the wrong line numbers.

Lines 472-489 contain notification post replyCount update logic, not the useSuggestedProfilesForUser hook. The hook is defined at line 2465. Please verify you've targeted the correct lines for this review.

Likely an incorrect or invalid review comment.

@minimaluminium minimaluminium requested a review from sagzy December 8, 2025 07:35
@minimaluminium minimaluminium removed the request for review from sagzy December 8, 2025 07:43
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
apps/activitypub/src/views/Feed/components/SuggestedProfiles.tsx (2)

19-30: LGTM! Consider wrapping in useCallback for optimization.

The scroll button visibility logic is correct and properly guards against null references.

To avoid recreating this function on every render, consider wrapping it in useCallback:

-    const updateScrollButtons = () => {
+    const updateScrollButtons = useCallback(() => {
         const container = scrollContainerRef.current;
         if (!container) {
             return;
         }
 
         const canScrollL = container.scrollLeft > 0;
         const canScrollR = container.scrollLeft < container.scrollWidth - container.clientWidth;
 
         setCanScrollLeft(canScrollL);
         setCanScrollRight(canScrollR);
-    };
+    }, []);

115-115: Consider debouncing the scroll handler.

The scroll handler triggers on every scroll event, which could cause multiple state updates during rapid scrolling. While React batches updates, debouncing would further improve performance.

You can use a debounce utility or implement a simple version:

import {useCallback} from 'react';

// Create a debounced version
const debouncedUpdateScrollButtons = useCallback(
    debounce(updateScrollButtons, 50),
    [updateScrollButtons]
);

// Then use it:
onScroll={debouncedUpdateScrollButtons}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 15b5222 and 21c2706.

📒 Files selected for processing (1)
  • apps/activitypub/src/views/Feed/components/SuggestedProfiles.tsx (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/activitypub/src/views/Feed/components/SuggestedProfiles.tsx (1)
apps/activitypub/src/api/activitypub.ts (1)
  • Account (9-31)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Merge React Reports
  • GitHub Check: Merge Ember Reports
  • GitHub Check: ActivityPub tests
  • GitHub Check: Unit tests (Node 22.18.0)
  • GitHub Check: Cursor Bugbot
🔇 Additional comments (2)
apps/activitypub/src/views/Feed/components/SuggestedProfiles.tsx (2)

45-55: LGTM! Correctly removes local followerCount mutation.

The follow/unfollow handlers now only toggle followedByMe status, aligning with the PR objective to stop locally mutating follower counts. The backend will handle the actual follower count updates.


154-154: LGTM! Spacing adjustment for removed follower count.

The mb-6 class adds appropriate spacing now that the follower count display has been removed, maintaining the visual consistency of the profile cards.

@minimaluminium minimaluminium requested a review from sagzy December 8, 2025 08:09
Copy link
Contributor

@sagzy sagzy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good!

@minimaluminium minimaluminium force-pushed the ap-topics-recommendations-BER-3098-3041-3025 branch from 03c482b to 1e95da5 Compare December 8, 2025 08:43
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 03c482b and e307ec1.

📒 Files selected for processing (11)
  • apps/activitypub/package.json (1 hunks)
  • apps/activitypub/src/api/activitypub.ts (2 hunks)
  • apps/activitypub/src/components/TopicFilter.tsx (3 hunks)
  • apps/activitypub/src/components/global/SuggestedProfiles.tsx (2 hunks)
  • apps/activitypub/src/components/layout/Layout.tsx (3 hunks)
  • apps/activitypub/src/components/layout/Onboarding/Step3.tsx (3 hunks)
  • apps/activitypub/src/components/layout/Sidebar/Sidebar.tsx (3 hunks)
  • apps/activitypub/src/components/modals/Search.tsx (3 hunks)
  • apps/activitypub/src/hooks/use-activity-pub-queries.ts (4 hunks)
  • apps/activitypub/src/views/Feed/components/SuggestedProfiles.tsx (4 hunks)
  • apps/activitypub/src/views/Inbox/components/InboxList.tsx (4 hunks)
✅ Files skipped from review due to trivial changes (1)
  • apps/activitypub/package.json
🚧 Files skipped from review as they are similar to previous changes (5)
  • apps/activitypub/src/views/Inbox/components/InboxList.tsx
  • apps/activitypub/src/components/layout/Layout.tsx
  • apps/activitypub/src/api/activitypub.ts
  • apps/activitypub/src/views/Feed/components/SuggestedProfiles.tsx
  • apps/activitypub/src/components/modals/Search.tsx
🧰 Additional context used
🧠 Learnings (11)
📚 Learning: 2025-11-06T05:35:41.162Z
Learnt from: danielraffel
Repo: TryGhost/Ghost PR: 25366
File: apps/admin/src/layout/app-sidebar/NavHeader.tsx:13-23
Timestamp: 2025-11-06T05:35:41.162Z
Learning: In apps/admin/src/layout/app-sidebar/NavHeader.tsx, the React component dispatches a synthetic KeyboardEvent to trigger the Ember keymaster.js search modal shortcut. This approach is known to have cross-browser reliability issues but was deferred for architectural refactoring in a separate PR. The recommended fix is to expose a global function or custom DOM event from the Ember app instead of relying on synthetic keyboard events with keymaster.js.

Applied to files:

  • apps/activitypub/src/components/layout/Sidebar/Sidebar.tsx
📚 Learning: 2025-08-11T14:18:31.752Z
Learnt from: niranjan-uma-shankar
Repo: TryGhost/Ghost PR: 24626
File: apps/portal/src/components/pages/AccountHomePage/components/AccountFooter.js:15-15
Timestamp: 2025-08-11T14:18:31.752Z
Learning: In Ghost, Portal components render within iframes while admin components (like gh-member-details.hbs, dashboard/charts/recents.hbs, and posts/post-activity-feed.hbs) do not render in iframes. Therefore, iframe-related navigation issues only affect Portal components.

Applied to files:

  • apps/activitypub/src/components/layout/Sidebar/Sidebar.tsx
📚 Learning: 2025-09-02T07:55:08.601Z
Learnt from: minimaluminium
Repo: TryGhost/Ghost PR: 24798
File: apps/admin-x-activitypub/src/components/feed/layouts/FeedLayout.tsx:133-138
Timestamp: 2025-09-02T07:55:08.601Z
Learning: In apps/admin-x-activitypub FeedLayout component, renderFeedAttachment for Article-type objects intentionally receives onClick (card click handler) instead of onImageClick to ensure clicking article images opens the article rather than triggering the image lightbox.

Applied to files:

  • apps/activitypub/src/components/layout/Sidebar/Sidebar.tsx
📚 Learning: 2025-10-27T11:59:33.968Z
Learnt from: peterzimon
Repo: TryGhost/Ghost PR: 25261
File: apps/admin/src/layout/app-sidebar/UserMenu.tsx:24-29
Timestamp: 2025-10-27T11:59:33.968Z
Learning: In apps/admin/src/layout/app-sidebar/UserMenu.tsx, the hardcoded placeholder data (avatar URL, initials "US", name "User Name", email "userexample.com") is a known limitation and should not be flagged for replacement with real user data.

Applied to files:

  • apps/activitypub/src/components/layout/Sidebar/Sidebar.tsx
  • apps/activitypub/src/components/layout/Onboarding/Step3.tsx
📚 Learning: 2025-11-26T11:05:59.314Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: apps/shade/AGENTS.md:0-0
Timestamp: 2025-11-26T11:05:59.314Z
Learning: Applies to apps/shade/src/hooks/**/*.{ts,tsx} : Custom React hooks should be placed in `src/hooks/*`

Applied to files:

  • apps/activitypub/src/components/TopicFilter.tsx
📚 Learning: 2025-11-05T16:49:21.124Z
Learnt from: rob-ghost
Repo: TryGhost/Ghost PR: 25356
File: apps/admin/src/hooks/user-preferences.ts:20-20
Timestamp: 2025-11-05T16:49:21.124Z
Learning: In apps/admin/src/hooks/user-preferences.ts, the query key for user preferences intentionally includes `user?.accessibility` (not just `user?.id`) so the cache automatically reacts to changes from any source (useEditUserPreferences, other editUser calls, external updates). Combined with `cacheTime: 0`, this design garbage-collects orphaned entries while keeping the active entry cached. This is a documented, intentional architectural decision.

Applied to files:

  • apps/activitypub/src/hooks/use-activity-pub-queries.ts
📚 Learning: 2025-03-04T17:19:50.908Z
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 22330
File: apps/admin-x-activitypub/src/hooks/use-activity-pub-queries.ts:911-912
Timestamp: 2025-03-04T17:19:50.908Z
Learning: In the ActivityPub app, posts can appear in multiple threads, so when updating thread caches (e.g., when deleting a post), using QUERY_KEYS.thread(null) is the appropriate approach to update all thread caches rather than targeting a specific thread.

Applied to files:

  • apps/activitypub/src/hooks/use-activity-pub-queries.ts
📚 Learning: 2025-03-13T09:02:50.102Z
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 22471
File: apps/admin-x-activitypub/src/views/Feed/components/NewPostModal.tsx:29-34
Timestamp: 2025-03-13T09:02:50.102Z
Learning: In the ActivityPub module, error handling for mutations is handled at the hook level (in use-activity-pub-queries.ts) rather than in individual components. This allows for centralized error handling across the application.

Applied to files:

  • apps/activitypub/src/hooks/use-activity-pub-queries.ts
📚 Learning: 2025-03-13T09:02:50.102Z
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 22471
File: apps/admin-x-activitypub/src/views/Feed/components/NewPostModal.tsx:29-34
Timestamp: 2025-03-13T09:02:50.102Z
Learning: In the Ghost ActivityPub module, error handling for mutations is handled at the hook level (in use-activity-pub-queries.ts) rather than in individual components. This allows for centralized error handling across the application.

Applied to files:

  • apps/activitypub/src/hooks/use-activity-pub-queries.ts
📚 Learning: 2025-03-13T09:00:20.205Z
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 22471
File: apps/admin-x-activitypub/src/utils/pending-activity.ts:13-71
Timestamp: 2025-03-13T09:00:20.205Z
Learning: The pending activity utilities in Ghost's ActivityPub module are thoroughly tested in `apps/admin-x-activitypub/test/unit/utils/pending-activity.ts`, which covers `generatePendingActivityId`, `isPendingActivity`, and `generatePendingActivity` functions.

Applied to files:

  • apps/activitypub/src/hooks/use-activity-pub-queries.ts
📚 Learning: 2025-03-13T09:00:20.205Z
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 22471
File: apps/admin-x-activitypub/src/utils/pending-activity.ts:13-71
Timestamp: 2025-03-13T09:00:20.205Z
Learning: The pending activity utilities in the Ghost ActivityPub module are covered by tests in the file `apps/admin-x-activitypub/test/unit/utils/pending-activity.ts`.

Applied to files:

  • apps/activitypub/src/hooks/use-activity-pub-queries.ts
🧬 Code graph analysis (4)
apps/activitypub/src/components/layout/Sidebar/Sidebar.tsx (1)
apps/activitypub/src/hooks/use-activity-pub-queries.ts (1)
  • useTopicsForUser (2510-2523)
apps/activitypub/src/components/layout/Onboarding/Step3.tsx (1)
apps/activitypub/src/hooks/use-activity-pub-queries.ts (1)
  • useTopicsForUser (2510-2523)
apps/activitypub/src/components/TopicFilter.tsx (1)
apps/activitypub/src/hooks/use-activity-pub-queries.ts (1)
  • useTopicsForUser (2510-2523)
apps/activitypub/src/hooks/use-activity-pub-queries.ts (1)
apps/activitypub/src/api/activitypub.ts (1)
  • Account (9-31)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Unit tests (Node 22.18.0)
  • GitHub Check: ActivityPub tests
  • GitHub Check: Lint
  • GitHub Check: Cursor Bugbot
  • GitHub Check: Build & Push
🔇 Additional comments (11)
apps/activitypub/src/components/global/SuggestedProfiles.tsx (2)

20-30: LGTM - Follower count mutation removed as intended.

The removal of optimistic followerCount updates aligns with the PR objective to stop locally mutating follower counts. The followedByMe toggle is sufficient for immediate UI feedback.


82-84: LGTM - Clean conditional rendering.

The early return properly waits for loading to complete before checking data emptiness, avoiding premature hiding of the component during initial fetch.

apps/activitypub/src/components/layout/Onboarding/Step3.tsx (1)

321-324: Navigation logic is sound.

The conditional navigation approach is appropriate for directing users to the most relevant page based on topic availability.

apps/activitypub/src/hooks/use-activity-pub-queries.ts (3)

95-96: LGTM - Query key follows existing patterns.


2465-2508: LGTM - Clean refactor to API-driven recommendations.

Good approach caching individual accounts for follow mutation consistency. The null return for empty results enables proper conditional rendering in consumers.


2510-2523: LGTM - Well-designed topics hook.

The 24-hour staleTime is appropriate for infrequently-changing topic data, and retry: false prevents unnecessary API calls on failure.

apps/activitypub/src/components/layout/Sidebar/Sidebar.tsx (2)

29-31: LGTM - Proper loading state handling.

Including !isLoading in the hasTopics check prevents the flicker issue where the Explore link would briefly show as external before topics load.


79-96: LGTM - Well-structured conditional rendering.

Good UX decisions: the external link icon clearly indicates off-site navigation, and proper rel="noopener noreferrer" ensures security for the new tab.

apps/activitypub/src/components/TopicFilter.tsx (3)

3-5: LGTM - Type widening for dynamic topics.

Changing Topic to string is necessary to support API-driven topics. The previous union type would require constant updates as topics change.


14-22: LGTM - Robust topic construction.

The hardcoded "Following" topic ensures there's always at least one option even if the API returns empty, providing a fallback for edge cases.


49-58: LGTM - Clean topic rendering.

Proper separation of concerns: slug for identification and event handling, name for user-facing display.

Comment on lines +305 to +307
const {topicsQuery} = useTopicsForUser();
const {data: topicsData} = topicsQuery;
const hasTopics = topicsData && topicsData.topics.length > 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistent loading state handling compared to Sidebar.

The hasTopics computation doesn't check isLoading, unlike Sidebar.tsx (line 31) which uses !isLoading && topicsData && topicsData.topics.length > 0. If the user clicks "Next" before topics finish loading, hasTopics will be false and they'll navigate to / instead of /explore.

 const {topicsQuery} = useTopicsForUser();
-const {data: topicsData} = topicsQuery;
-const hasTopics = topicsData && topicsData.topics.length > 0;
+const {data: topicsData, isLoading} = topicsQuery;
+const hasTopics = !isLoading && topicsData && topicsData.topics.length > 0;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const {topicsQuery} = useTopicsForUser();
const {data: topicsData} = topicsQuery;
const hasTopics = topicsData && topicsData.topics.length > 0;
const {topicsQuery} = useTopicsForUser();
const {data: topicsData, isLoading} = topicsQuery;
const hasTopics = !isLoading && topicsData && topicsData.topics.length > 0;
🤖 Prompt for AI Agents
In apps/activitypub/src/components/layout/Onboarding/Step3.tsx around lines 305
to 307, hasTopics is computed without checking the topicsQuery loading state
which can treat pending data as "no topics" and misroute users; update the logic
to destructure isLoading (or the equivalent loading flag) from topicsQuery and
compute hasTopics as !isLoading && topicsData && topicsData.topics.length > 0 so
it matches Sidebar.tsx behavior and prevents navigating to the wrong route if
the user clicks Next before topics finish loading.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Recommendations "Find more" link ignores missing topics check

The "Find more" button in Recommendations.tsx and SuggestedProfiles.tsx always navigates to the internal /explore route, but the Sidebar menu correctly checks hasTopics to decide between internal navigation and an external link. When the topics API returns empty but recommendations exist, users clicking "Find more" will land on an /explore page with an empty TopicFilter (since excludeTopics={['following']} filters out the only hardcoded topic). This creates an inconsistent UX where the main Explore menu link behaves differently from these secondary navigation links.

apps/activitypub/src/views/Feed/components/SuggestedProfiles.tsx#L81-L84

<H4 className='text-lg font-semibold text-black dark:text-white'>More people to follow</H4>
<Button className='px-0 font-medium text-gray-700 hover:text-black dark:text-gray-600 dark:hover:text-white' variant='link' onClick={() => navigate('/explore')}>
Find more &rarr;
</Button>

Fix in Cursor Fix in Web


const handleComplete = async () => {
await setOnboarded(true);
navigate('/explore');
navigate(hasTopics ? '/explore' : '/');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Race condition in onboarding navigation when topics query is loading

The handleComplete function navigates based on hasTopics, but unlike Sidebar.tsx which checks isLoading state, Step3.tsx derives hasTopics directly from topicsData without considering the query's loading state. When topicsQuery is still loading, topicsData is undefined, causing hasTopics to be false regardless of whether topics actually exist. This means users completing onboarding before the query finishes will be incorrectly routed to / instead of /explore, even when topics are available.

Fix in Cursor Fix in Web

{
"name": "@tryghost/activitypub",
"version": "2.0.5",
"version": "2.0.6",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should ship that change as new major, as this relies on new backend API endpoints

@github-actions
Copy link
Contributor

github-actions bot commented Dec 9, 2025

React E2E Tests Failed

To view the Playwright test report locally, run:

REPORT_DIR=$(mktemp -d) && gh run download 20049874684 -n playwright-report-react -D "$REPORT_DIR" && npx playwright show-report "$REPORT_DIR"

@minimaluminium minimaluminium merged commit 2c9fc66 into main Dec 9, 2025
36 of 37 checks passed
@minimaluminium minimaluminium deleted the ap-topics-recommendations-BER-3098-3041-3025 branch December 9, 2025 08:54
sagzy added a commit to TryGhost/ActivityPub that referenced this pull request Dec 9, 2025
ref TryGhost/Ghost#25646

- Activitypub frontend change in TryGhost/Ghost#25646 relies on two new API endpoints (`/topics` and `/recommendations`), and therefore has been shipped with a new major (3.x)
- With this change, we're saying: this version of the ActivityPub API is compatible with the frontend version 3.x
- More details on versioning in ADR: https://github.com/TryGhost/ActivityPub/blob/main/adr/0002-frontend-backend-versioning.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants