Skip to content

Added capability-gated comment dislike UI#28021

Open
jonatansberg wants to merge 1 commit into
mainfrom
adding-dislikes-to-comments-ui-and-admin-capability
Open

Added capability-gated comment dislike UI#28021
jonatansberg wants to merge 1 commit into
mainfrom
adding-dislikes-to-comments-ui-and-admin-capability

Conversation

@jonatansberg
Copy link
Copy Markdown
Member

@jonatansberg jonatansberg commented May 21, 2026

Stack

Summary

  • Adds capability-gated public comment dislike controls driven by meta.capabilities.dislikes.
  • Keeps old-server behavior like-only when the capability is absent and never renders a public dislike count.
  • Avoids labs/private-feature/settings wiring for dislikes.
  • Adds Admin comments UI support that enables dislike counts/member lists only after the API advertises dislike support.

Compatibility

  • New frontend against old server stays like-only and does not call dislike endpoints.
  • New frontend against dislike-capable server enables the dislike button/state and net-score sorting.
  • Public API dislike aggregate counts remain hidden.

Verification

  • pnpm --filter @tryghost/comments-ui test:unit -- actions.test.js
  • pnpm --filter @tryghost/comments-ui test:unit -- comment.test.jsx
  • pnpm --filter @tryghost/i18n translate

Known local verification blockers

  • pnpm --filter @tryghost/admin-x-framework test:types currently fails before these changes on missing bson-objectid/type-fest from automations.
  • pnpm --filter @tryghost/posts test:unit -- comments currently fails before the comments tests on missing dequal/bson-objectid from automations.
  • pnpm --filter @tryghost/i18n lint:translations currently fails on stale unused ignores in test/i18n-ignores.json for eu ghost translations.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 21, 2026

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds dislike reactions across the stack: extends comment types and counts, adds CommentDislike and updated CommentLike shapes, adds dislike API methods and a useBrowseCommentDislikes hook, makes read/thread hooks conditional on a dislikesEnabled include string, implements optimistic dislike actions with rollback and mutual-exclusion handling in state/actions, threads capabilities and ordering (likes vs net score) through app state and components, updates UI (buttons, counts, metrics, modal tabs), adjusts tests/mocks/fixtures, and adds i18n keys for dislike labels.

Suggested reviewers

  • weylandswart
  • sagzy
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ 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%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main change: adding capability-gated comment dislike UI, which is the core objective of this PR.
Description check ✅ Passed The description provides relevant context about the PR's purpose, compatibility, and verification steps, directly relating to the changeset of adding dislike support.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch adding-dislikes-to-comments-ui-and-admin-capability

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
Copy Markdown
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: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/admin-x-framework/src/api/comments.ts`:
- Around line 105-114: memberThreadCommentIncludes currently uses
commentDislikeIncludes which only adds count.dislikes, so nested/thread comments
lack the current member's disliked flag; update memberThreadCommentIncludes to
use commentDislikeMemberIncludes (the same used by memberCommentIncludes) when
dislikesEnabled is true so the returned includes include both count.dislikes and
disliked; locate memberThreadCommentIncludes and replace the conditional spread
from commentDislikeIncludes to commentDislikeMemberIncludes (or append the
'disliked' field) to ensure the UI can detect whether the current member has
disliked a reply.

In `@apps/comments-ui/src/components/content/buttons/like-button.tsx`:
- Around line 31-42: The click handler currently sets setDisabled(true) but only
resets it after awaited dispatchAction calls, so if
dispatchAction('likeComment', comment) or dispatchAction('unlikeComment',
comment) throws the button remains disabled; wrap the async/await blocks inside
a try/finally so setDisabled(false) is always executed (keep the like animation
logic inside the try so it only runs on success), and apply the same try/finally
pattern to the other identical handler around dispatchAction calls at the other
occurrence (the block that uses dispatchAction('likeComment'...) /
dispatchAction('unlikeComment'...) and setLikeAnimation).

In `@apps/comments-ui/test/utils/mocked-api.ts`:
- Around line 448-449: When decrementing dislike counts in the mock API, avoid
negative values by clamping after the decrement; replace the current decrement
logic that sets comment.count.dislikes = (comment.count.dislikes ?? 0) - 1 and
comment.disliked = false with a clamped assignment using Math.max(0,
(comment.count.dislikes ?? 0) - 1). Apply the same clamp to the other occurrence
referenced (lines around the second instance at 499-500) so both
reaction-removal/swap paths never produce negative comment.count.dislikes.

In `@apps/posts/src/views/comments/components/comment-likes-modal.tsx`:
- Around line 24-25: The "remaining" counters can go negative when aggregate
counts are stale; update the computations (e.g., likesRemaining and
dislikesRemaining and the equivalent calculations at the other spots) to clamp
at zero by replacing count - array.length with Math.max(0, count - array.length)
(or an equivalent clamp) so the UI never renders negative "and -N more" values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 360afee5-2b4f-4c65-aa58-44e150040794

📥 Commits

Reviewing files that changed from the base of the PR and between bd46a1c and 8a43a90.

⛔ Files ignored due to path filters (2)
  • apps/comments-ui/src/images/icons/thumbs-down.svg is excluded by !**/*.svg
  • apps/comments-ui/src/images/icons/thumbs-up.svg is excluded by !**/*.svg
📒 Files selected for processing (83)
  • apps/admin-x-framework/src/api/comments.ts
  • apps/admin-x-framework/src/utils/api/hooks.ts
  • apps/comments-ui/src/actions.ts
  • apps/comments-ui/src/app-context.ts
  • apps/comments-ui/src/app.tsx
  • apps/comments-ui/src/components/content/buttons/like-button.tsx
  • apps/comments-ui/src/components/content/buttons/like-count.tsx
  • apps/comments-ui/src/components/content/comment.tsx
  • apps/comments-ui/src/components/content/forms/sorting-form.tsx
  • apps/comments-ui/src/utils/api.ts
  • apps/comments-ui/test/unit/actions.test.js
  • apps/comments-ui/test/unit/components/content/comment.test.jsx
  • apps/comments-ui/test/utils/fixtures.ts
  • apps/comments-ui/test/utils/mocked-api.ts
  • apps/posts/src/views/comments/comments.tsx
  • apps/posts/src/views/comments/components/comment-likes-modal.tsx
  • apps/posts/src/views/comments/components/comment-metrics.tsx
  • apps/posts/src/views/comments/components/comment-thread-list.tsx
  • apps/posts/src/views/comments/components/comment-thread-sidebar.tsx
  • apps/posts/src/views/comments/components/comments-list.tsx
  • ghost/i18n/locales/af/comments.json
  • ghost/i18n/locales/ar/comments.json
  • ghost/i18n/locales/bg/comments.json
  • ghost/i18n/locales/bn/comments.json
  • ghost/i18n/locales/bs/comments.json
  • ghost/i18n/locales/ca/comments.json
  • ghost/i18n/locales/context.json
  • ghost/i18n/locales/cs/comments.json
  • ghost/i18n/locales/da/comments.json
  • ghost/i18n/locales/de-CH/comments.json
  • ghost/i18n/locales/de/comments.json
  • ghost/i18n/locales/el/comments.json
  • ghost/i18n/locales/en/comments.json
  • ghost/i18n/locales/eo/comments.json
  • ghost/i18n/locales/es/comments.json
  • ghost/i18n/locales/et/comments.json
  • ghost/i18n/locales/eu/comments.json
  • ghost/i18n/locales/fa/comments.json
  • ghost/i18n/locales/fi/comments.json
  • ghost/i18n/locales/fr/comments.json
  • ghost/i18n/locales/gd/comments.json
  • ghost/i18n/locales/he/comments.json
  • ghost/i18n/locales/hi/comments.json
  • ghost/i18n/locales/hr/comments.json
  • ghost/i18n/locales/hu/comments.json
  • ghost/i18n/locales/id/comments.json
  • ghost/i18n/locales/is/comments.json
  • ghost/i18n/locales/it/comments.json
  • ghost/i18n/locales/ja/comments.json
  • ghost/i18n/locales/ko/comments.json
  • ghost/i18n/locales/kz/comments.json
  • ghost/i18n/locales/lt/comments.json
  • ghost/i18n/locales/lv/comments.json
  • ghost/i18n/locales/mk/comments.json
  • ghost/i18n/locales/mn/comments.json
  • ghost/i18n/locales/ms/comments.json
  • ghost/i18n/locales/nb/comments.json
  • ghost/i18n/locales/ne/comments.json
  • ghost/i18n/locales/nl/comments.json
  • ghost/i18n/locales/nn/comments.json
  • ghost/i18n/locales/pa/comments.json
  • ghost/i18n/locales/pl/comments.json
  • ghost/i18n/locales/pt-BR/comments.json
  • ghost/i18n/locales/pt/comments.json
  • ghost/i18n/locales/ro/comments.json
  • ghost/i18n/locales/ru/comments.json
  • ghost/i18n/locales/si/comments.json
  • ghost/i18n/locales/sk/comments.json
  • ghost/i18n/locales/sl/comments.json
  • ghost/i18n/locales/sq/comments.json
  • ghost/i18n/locales/sr-Cyrl/comments.json
  • ghost/i18n/locales/sr/comments.json
  • ghost/i18n/locales/sv/comments.json
  • ghost/i18n/locales/sw/comments.json
  • ghost/i18n/locales/ta/comments.json
  • ghost/i18n/locales/th/comments.json
  • ghost/i18n/locales/tr/comments.json
  • ghost/i18n/locales/uk/comments.json
  • ghost/i18n/locales/ur/comments.json
  • ghost/i18n/locales/uz/comments.json
  • ghost/i18n/locales/vi/comments.json
  • ghost/i18n/locales/zh-Hant/comments.json
  • ghost/i18n/locales/zh/comments.json

Comment thread apps/admin-x-framework/src/api/comments.ts
Comment thread apps/comments-ui/src/components/content/buttons/like-button.tsx
Comment thread apps/comments-ui/test/utils/mocked-api.ts Outdated
Comment thread apps/comments-ui/test/utils/mocked-api.ts
Comment thread apps/posts/src/views/comments/components/comment-likes-modal.tsx Outdated
@codecov
Copy link
Copy Markdown

codecov Bot commented May 21, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 73.85%. Comparing base (6400262) to head (f90b584).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main   #28021   +/-   ##
=======================================
  Coverage   73.85%   73.85%           
=======================================
  Files        1523     1525    +2     
  Lines      128982   129052   +70     
  Branches    15488    15493    +5     
=======================================
+ Hits        95254    95312   +58     
- Misses      32766    32802   +36     
+ Partials      962      938   -24     
Flag Coverage Δ
admin-tests 53.57% <ø> (ø)
e2e-tests 73.85% <ø> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jonatansberg jonatansberg force-pushed the adding-dislikes-to-comments-ui-and-admin-capability branch from 8a43a90 to 598a1d0 Compare May 21, 2026 08:53
Copy link
Copy Markdown
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: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/comments-ui/src/components/content/forms/sorting-form.tsx`:
- Around line 9-14: selectedOption is only initialized from order once and can
become stale when order or bestOrder (which depends on capabilities) changes;
add a useEffect that watches order and bestOrder and calls setSelectedOption
with the new order (or computed bestOrder) so the UI label stays in sync with
capability-driven changes. Update the effect to reference selectedOption,
setSelectedOption, order, and bestOrder (and any i18n key if required) and
ensure options array continues to use bestOrder so the dropdown options and
selectedOption remain consistent.

In `@apps/comments-ui/test/unit/components/content/comment.test.jsx`:
- Around line 98-100: The current assertion for the icon-only dislike button
only excludes "1" and can miss other numeric text; update the assertion on
screen.getByTestId('dislike-button') to assert an empty text content (e.g.
expect(screen.getByTestId('dislike-button')).toHaveTextContent('')) so the
button truly contains no visible label or counts, keeping the accessible name
check intact; locate the test that queries 'dislike-button' and replace the
not.toHaveTextContent('1') line with a strict empty-text assertion.

In `@apps/posts/src/views/comments/components/comment-likes-modal.tsx`:
- Around line 167-205: The likes-only view currently shows a blank area when
dislikesEnabled is false and likes is empty; update the conditional rendering
inside the likes branch (the block using likesLoading, likes.map(...),
likesRemaining) to explicitly render an empty-state element when !likesLoading
and likes.length === 0 (e.g., a centered message/icon like "No likes yet" or
similar). Modify the component that contains Avatar, formatMemberName,
formatTimestamp and likesRemaining to check for the empty-case before mapping
likes so the LoadingIndicator, the list mapping, and the "and X more" footer
remain unchanged for non-empty states.

In `@apps/posts/src/views/comments/components/comment-metrics.tsx`:
- Around line 147-154: The click handler always sets
setLikesModalDefaultTab('likes') which opens an empty tab for dislike-only
comments; update the onClick in the button (the handler referencing
setLikesModalDefaultTab and setLikesModalOpen) to choose the initial tab based
on the comment's counts (e.g., if likes count is 0 and dislikes count > 0 then
setLikesModalDefaultTab('dislikes') else 'likes'), using the available
props/state for likes/dislikes (e.g., comment.scoreLikesCount or
scoreDislikesCount or equivalent) before calling setLikesModalOpen(true).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 19578c7a-2bf6-41b0-9eb8-c661059ead9d

📥 Commits

Reviewing files that changed from the base of the PR and between 8a43a90 and 598a1d0.

⛔ Files ignored due to path filters (2)
  • apps/comments-ui/src/images/icons/thumbs-down.svg is excluded by !**/*.svg
  • apps/comments-ui/src/images/icons/thumbs-up.svg is excluded by !**/*.svg
📒 Files selected for processing (84)
  • apps/admin-x-framework/src/api/comments.ts
  • apps/admin-x-framework/src/utils/api/hooks.ts
  • apps/comments-ui/package.json
  • apps/comments-ui/src/actions.ts
  • apps/comments-ui/src/app-context.ts
  • apps/comments-ui/src/app.tsx
  • apps/comments-ui/src/components/content/buttons/like-button.tsx
  • apps/comments-ui/src/components/content/buttons/like-count.tsx
  • apps/comments-ui/src/components/content/comment.tsx
  • apps/comments-ui/src/components/content/forms/sorting-form.tsx
  • apps/comments-ui/src/utils/api.ts
  • apps/comments-ui/test/unit/actions.test.js
  • apps/comments-ui/test/unit/components/content/comment.test.jsx
  • apps/comments-ui/test/utils/fixtures.ts
  • apps/comments-ui/test/utils/mocked-api.ts
  • apps/posts/src/views/comments/comments.tsx
  • apps/posts/src/views/comments/components/comment-likes-modal.tsx
  • apps/posts/src/views/comments/components/comment-metrics.tsx
  • apps/posts/src/views/comments/components/comment-thread-list.tsx
  • apps/posts/src/views/comments/components/comment-thread-sidebar.tsx
  • apps/posts/src/views/comments/components/comments-list.tsx
  • ghost/i18n/locales/af/comments.json
  • ghost/i18n/locales/ar/comments.json
  • ghost/i18n/locales/bg/comments.json
  • ghost/i18n/locales/bn/comments.json
  • ghost/i18n/locales/bs/comments.json
  • ghost/i18n/locales/ca/comments.json
  • ghost/i18n/locales/context.json
  • ghost/i18n/locales/cs/comments.json
  • ghost/i18n/locales/da/comments.json
  • ghost/i18n/locales/de-CH/comments.json
  • ghost/i18n/locales/de/comments.json
  • ghost/i18n/locales/el/comments.json
  • ghost/i18n/locales/en/comments.json
  • ghost/i18n/locales/eo/comments.json
  • ghost/i18n/locales/es/comments.json
  • ghost/i18n/locales/et/comments.json
  • ghost/i18n/locales/eu/comments.json
  • ghost/i18n/locales/fa/comments.json
  • ghost/i18n/locales/fi/comments.json
  • ghost/i18n/locales/fr/comments.json
  • ghost/i18n/locales/gd/comments.json
  • ghost/i18n/locales/he/comments.json
  • ghost/i18n/locales/hi/comments.json
  • ghost/i18n/locales/hr/comments.json
  • ghost/i18n/locales/hu/comments.json
  • ghost/i18n/locales/id/comments.json
  • ghost/i18n/locales/is/comments.json
  • ghost/i18n/locales/it/comments.json
  • ghost/i18n/locales/ja/comments.json
  • ghost/i18n/locales/ko/comments.json
  • ghost/i18n/locales/kz/comments.json
  • ghost/i18n/locales/lt/comments.json
  • ghost/i18n/locales/lv/comments.json
  • ghost/i18n/locales/mk/comments.json
  • ghost/i18n/locales/mn/comments.json
  • ghost/i18n/locales/ms/comments.json
  • ghost/i18n/locales/nb/comments.json
  • ghost/i18n/locales/ne/comments.json
  • ghost/i18n/locales/nl/comments.json
  • ghost/i18n/locales/nn/comments.json
  • ghost/i18n/locales/pa/comments.json
  • ghost/i18n/locales/pl/comments.json
  • ghost/i18n/locales/pt-BR/comments.json
  • ghost/i18n/locales/pt/comments.json
  • ghost/i18n/locales/ro/comments.json
  • ghost/i18n/locales/ru/comments.json
  • ghost/i18n/locales/si/comments.json
  • ghost/i18n/locales/sk/comments.json
  • ghost/i18n/locales/sl/comments.json
  • ghost/i18n/locales/sq/comments.json
  • ghost/i18n/locales/sr-Cyrl/comments.json
  • ghost/i18n/locales/sr/comments.json
  • ghost/i18n/locales/sv/comments.json
  • ghost/i18n/locales/sw/comments.json
  • ghost/i18n/locales/ta/comments.json
  • ghost/i18n/locales/th/comments.json
  • ghost/i18n/locales/tr/comments.json
  • ghost/i18n/locales/uk/comments.json
  • ghost/i18n/locales/ur/comments.json
  • ghost/i18n/locales/uz/comments.json
  • ghost/i18n/locales/vi/comments.json
  • ghost/i18n/locales/zh-Hant/comments.json
  • ghost/i18n/locales/zh/comments.json
✅ Files skipped from review due to trivial changes (38)
  • apps/comments-ui/package.json
  • ghost/i18n/locales/si/comments.json
  • ghost/i18n/locales/bg/comments.json
  • ghost/i18n/locales/en/comments.json
  • ghost/i18n/locales/uk/comments.json
  • ghost/i18n/locales/hu/comments.json
  • ghost/i18n/locales/zh/comments.json
  • ghost/i18n/locales/ja/comments.json
  • ghost/i18n/locales/de/comments.json
  • ghost/i18n/locales/id/comments.json
  • ghost/i18n/locales/mn/comments.json
  • ghost/i18n/locales/eu/comments.json
  • ghost/i18n/locales/sq/comments.json
  • ghost/i18n/locales/sr-Cyrl/comments.json
  • ghost/i18n/locales/uz/comments.json
  • ghost/i18n/locales/bs/comments.json
  • ghost/i18n/locales/es/comments.json
  • ghost/i18n/locales/sr/comments.json
  • ghost/i18n/locales/sl/comments.json
  • ghost/i18n/locales/nl/comments.json
  • ghost/i18n/locales/et/comments.json
  • ghost/i18n/locales/cs/comments.json
  • ghost/i18n/locales/pl/comments.json
  • ghost/i18n/locales/lv/comments.json
  • ghost/i18n/locales/he/comments.json
  • ghost/i18n/locales/kz/comments.json
  • ghost/i18n/locales/gd/comments.json
  • ghost/i18n/locales/mk/comments.json
  • ghost/i18n/locales/pt-BR/comments.json
  • ghost/i18n/locales/pt/comments.json
  • ghost/i18n/locales/fr/comments.json
  • ghost/i18n/locales/ms/comments.json
  • ghost/i18n/locales/bn/comments.json
  • ghost/i18n/locales/ar/comments.json
  • ghost/i18n/locales/nb/comments.json
  • ghost/i18n/locales/sv/comments.json
  • ghost/i18n/locales/vi/comments.json
  • ghost/i18n/locales/is/comments.json

Comment thread apps/comments-ui/src/components/content/forms/sorting-form.tsx
Comment thread apps/comments-ui/test/unit/components/content/comment.test.jsx Outdated
Comment thread apps/posts/src/views/comments/components/comment-likes-modal.tsx
Comment thread apps/posts/src/views/comments/components/comment-metrics.tsx
@jonatansberg jonatansberg force-pushed the adding-dislikes-to-comments-ui-and-admin-capability branch from 598a1d0 to 42493f3 Compare May 21, 2026 09:14
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 42493f3921

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/comments-ui/src/actions.ts Outdated
return {};
} catch {
dispatchAction('updateCommentLikeState', {id: comment.id, liked: false});
dispatchAction('updateCommentLikeState', {id: comment.id, liked: false, restoreDisliked: wasDisliked});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Guard rollback against overlapping reaction toggles

This rollback uses wasDisliked captured before the request, so if a user clicks Like and then quickly clicks Dislike (dislikes-enabled UI shows two independent buttons), a later failure of the first request will force disliked back to that stale snapshot and can undo the newer successful action. The same stale-snapshot pattern exists in the inverse dislike rollback path, so concurrent/out-of-order toggles can leave client state inconsistent with the server until a full refetch.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
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.

♻️ Duplicate comments (2)
apps/comments-ui/test/unit/components/content/comment.test.jsx (1)

100-100: ⚠️ Potential issue | 🟡 Minor

Weak assertion for icon-only button.

This assertion only excludes the string '1' and will pass if other numeric counts (e.g., '2', '3') are rendered. A past review comment already suggested strengthening this to either assert empty text content or exclude all digits using a regex pattern.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/comments-ui/test/unit/components/content/comment.test.jsx` at line 100,
The assertion on getByTestId('dislike-button') is too weak (only excludes the
string '1'); update the test to assert that the icon-only button contains no
digits by either asserting an empty text content or asserting no numeric
characters (for example replace the current
expect(screen.getByTestId('dislike-button')).not.toHaveTextContent('1') with a
stricter assertion such as expecting an empty string or using a regex to ensure
no digits), keeping the check on getByTestId('dislike-button') and the same test
setup.
apps/admin-x-framework/src/api/comments.ts (1)

105-114: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

memberThreadCommentIncludes should use commentDislikeMemberIncludes to include the disliked flag.

Thread/nested comments fetched via this include string will have count.dislikes but not the current member's disliked status, so the dislike button UI won't know if the user has already disliked a reply.

Proposed fix
 export const memberThreadCommentIncludes = (dislikesEnabled: boolean) => [
     'member',
     'post',
     'count.direct_replies',
     'count.likes',
-    ...(dislikesEnabled ? [commentDislikeIncludes] : []),
+    ...(dislikesEnabled ? [commentDislikeMemberIncludes] : []),
     'count.reports',
     'parent',
     'in_reply_to'
 ].join(',');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/admin-x-framework/src/api/comments.ts` around lines 105 - 114,
memberThreadCommentIncludes currently adds commentDislikeIncludes when
dislikesEnabled, which only brings count.dislikes; change it to include
commentDislikeMemberIncludes so the fetched thread comments also contain the
current member's disliked flag. Update the spread in memberThreadCommentIncludes
(the expression that currently uses commentDislikeIncludes) to use
commentDislikeMemberIncludes while keeping the dislikesEnabled conditional and
the rest of the include list unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@apps/admin-x-framework/src/api/comments.ts`:
- Around line 105-114: memberThreadCommentIncludes currently adds
commentDislikeIncludes when dislikesEnabled, which only brings count.dislikes;
change it to include commentDislikeMemberIncludes so the fetched thread comments
also contain the current member's disliked flag. Update the spread in
memberThreadCommentIncludes (the expression that currently uses
commentDislikeIncludes) to use commentDislikeMemberIncludes while keeping the
dislikesEnabled conditional and the rest of the include list unchanged.

In `@apps/comments-ui/test/unit/components/content/comment.test.jsx`:
- Line 100: The assertion on getByTestId('dislike-button') is too weak (only
excludes the string '1'); update the test to assert that the icon-only button
contains no digits by either asserting an empty text content or asserting no
numeric characters (for example replace the current
expect(screen.getByTestId('dislike-button')).not.toHaveTextContent('1') with a
stricter assertion such as expecting an empty string or using a regex to ensure
no digits), keeping the check on getByTestId('dislike-button') and the same test
setup.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2446e70a-1fc1-4bb8-9174-99aab9222ed2

📥 Commits

Reviewing files that changed from the base of the PR and between 598a1d0 and 42493f3.

⛔ Files ignored due to path filters (2)
  • apps/comments-ui/src/images/icons/thumbs-down.svg is excluded by !**/*.svg
  • apps/comments-ui/src/images/icons/thumbs-up.svg is excluded by !**/*.svg
📒 Files selected for processing (84)
  • apps/admin-x-framework/src/api/comments.ts
  • apps/admin-x-framework/src/utils/api/hooks.ts
  • apps/comments-ui/package.json
  • apps/comments-ui/src/actions.ts
  • apps/comments-ui/src/app-context.ts
  • apps/comments-ui/src/app.tsx
  • apps/comments-ui/src/components/content/buttons/like-button.tsx
  • apps/comments-ui/src/components/content/buttons/like-count.tsx
  • apps/comments-ui/src/components/content/comment.tsx
  • apps/comments-ui/src/components/content/forms/sorting-form.tsx
  • apps/comments-ui/src/utils/api.ts
  • apps/comments-ui/test/unit/actions.test.js
  • apps/comments-ui/test/unit/components/content/comment.test.jsx
  • apps/comments-ui/test/utils/fixtures.ts
  • apps/comments-ui/test/utils/mocked-api.ts
  • apps/posts/src/views/comments/comments.tsx
  • apps/posts/src/views/comments/components/comment-likes-modal.tsx
  • apps/posts/src/views/comments/components/comment-metrics.tsx
  • apps/posts/src/views/comments/components/comment-thread-list.tsx
  • apps/posts/src/views/comments/components/comment-thread-sidebar.tsx
  • apps/posts/src/views/comments/components/comments-list.tsx
  • ghost/i18n/locales/af/comments.json
  • ghost/i18n/locales/ar/comments.json
  • ghost/i18n/locales/bg/comments.json
  • ghost/i18n/locales/bn/comments.json
  • ghost/i18n/locales/bs/comments.json
  • ghost/i18n/locales/ca/comments.json
  • ghost/i18n/locales/context.json
  • ghost/i18n/locales/cs/comments.json
  • ghost/i18n/locales/da/comments.json
  • ghost/i18n/locales/de-CH/comments.json
  • ghost/i18n/locales/de/comments.json
  • ghost/i18n/locales/el/comments.json
  • ghost/i18n/locales/en/comments.json
  • ghost/i18n/locales/eo/comments.json
  • ghost/i18n/locales/es/comments.json
  • ghost/i18n/locales/et/comments.json
  • ghost/i18n/locales/eu/comments.json
  • ghost/i18n/locales/fa/comments.json
  • ghost/i18n/locales/fi/comments.json
  • ghost/i18n/locales/fr/comments.json
  • ghost/i18n/locales/gd/comments.json
  • ghost/i18n/locales/he/comments.json
  • ghost/i18n/locales/hi/comments.json
  • ghost/i18n/locales/hr/comments.json
  • ghost/i18n/locales/hu/comments.json
  • ghost/i18n/locales/id/comments.json
  • ghost/i18n/locales/is/comments.json
  • ghost/i18n/locales/it/comments.json
  • ghost/i18n/locales/ja/comments.json
  • ghost/i18n/locales/ko/comments.json
  • ghost/i18n/locales/kz/comments.json
  • ghost/i18n/locales/lt/comments.json
  • ghost/i18n/locales/lv/comments.json
  • ghost/i18n/locales/mk/comments.json
  • ghost/i18n/locales/mn/comments.json
  • ghost/i18n/locales/ms/comments.json
  • ghost/i18n/locales/nb/comments.json
  • ghost/i18n/locales/ne/comments.json
  • ghost/i18n/locales/nl/comments.json
  • ghost/i18n/locales/nn/comments.json
  • ghost/i18n/locales/pa/comments.json
  • ghost/i18n/locales/pl/comments.json
  • ghost/i18n/locales/pt-BR/comments.json
  • ghost/i18n/locales/pt/comments.json
  • ghost/i18n/locales/ro/comments.json
  • ghost/i18n/locales/ru/comments.json
  • ghost/i18n/locales/si/comments.json
  • ghost/i18n/locales/sk/comments.json
  • ghost/i18n/locales/sl/comments.json
  • ghost/i18n/locales/sq/comments.json
  • ghost/i18n/locales/sr-Cyrl/comments.json
  • ghost/i18n/locales/sr/comments.json
  • ghost/i18n/locales/sv/comments.json
  • ghost/i18n/locales/sw/comments.json
  • ghost/i18n/locales/ta/comments.json
  • ghost/i18n/locales/th/comments.json
  • ghost/i18n/locales/tr/comments.json
  • ghost/i18n/locales/uk/comments.json
  • ghost/i18n/locales/ur/comments.json
  • ghost/i18n/locales/uz/comments.json
  • ghost/i18n/locales/vi/comments.json
  • ghost/i18n/locales/zh-Hant/comments.json
  • ghost/i18n/locales/zh/comments.json
✅ Files skipped from review due to trivial changes (39)
  • ghost/i18n/locales/hi/comments.json
  • ghost/i18n/locales/sq/comments.json
  • ghost/i18n/locales/nn/comments.json
  • ghost/i18n/locales/gd/comments.json
  • ghost/i18n/locales/ms/comments.json
  • ghost/i18n/locales/zh/comments.json
  • ghost/i18n/locales/ko/comments.json
  • ghost/i18n/locales/nl/comments.json
  • ghost/i18n/locales/pa/comments.json
  • apps/comments-ui/package.json
  • ghost/i18n/locales/sr-Cyrl/comments.json
  • ghost/i18n/locales/vi/comments.json
  • ghost/i18n/locales/uk/comments.json
  • ghost/i18n/locales/si/comments.json
  • ghost/i18n/locales/it/comments.json
  • ghost/i18n/locales/uz/comments.json
  • ghost/i18n/locales/sv/comments.json
  • ghost/i18n/locales/eu/comments.json
  • ghost/i18n/locales/et/comments.json
  • ghost/i18n/locales/lv/comments.json
  • ghost/i18n/locales/el/comments.json
  • ghost/i18n/locales/es/comments.json
  • ghost/i18n/locales/sl/comments.json
  • ghost/i18n/locales/fr/comments.json
  • ghost/i18n/locales/ca/comments.json
  • ghost/i18n/locales/lt/comments.json
  • apps/admin-x-framework/src/utils/api/hooks.ts
  • ghost/i18n/locales/ne/comments.json
  • ghost/i18n/locales/en/comments.json
  • ghost/i18n/locales/pt/comments.json
  • ghost/i18n/locales/ar/comments.json
  • ghost/i18n/locales/he/comments.json
  • ghost/i18n/locales/fi/comments.json
  • ghost/i18n/locales/de/comments.json
  • ghost/i18n/locales/eo/comments.json
  • ghost/i18n/locales/ro/comments.json
  • ghost/i18n/locales/ja/comments.json
  • ghost/i18n/locales/mk/comments.json
  • apps/comments-ui/src/components/content/buttons/like-button.tsx

@jonatansberg jonatansberg force-pushed the adding-dislikes-to-comments-ui-and-admin-capability branch from 42493f3 to c8f3cbd Compare May 21, 2026 09:27
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c8f3cbdeef

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/comments-ui/src/actions.ts Outdated
return {};
} catch {
dispatchAction('updateCommentLikeState', {id: comment.id, liked: true});
dispatchAction('updateCommentLikeState', {id: comment.id, liked: true, expectedLiked: false});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Skip unlike rollback after a newer dislike toggle

When unlikeComment fails, this rollback always dispatches liked: true as long as the current liked state is false. If the user clicks Unlike and then quickly clicks Dislike, a later unlike failure will reapply liked: true and (via updateCommentLikeState) clear disliked, overwriting the newer successful dislike action and leaving the client out of sync until a refetch.

Useful? React with 👍 / 👎.

Comment thread apps/comments-ui/src/actions.ts Outdated
await api.comments.undislike({comment});
return {};
} catch {
dispatchAction('updateCommentDislikeState', {id: comment.id, disliked: true, expectedDisliked: false});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Skip undislike rollback after a newer like toggle

When undislikeComment fails, this rollback unconditionally dispatches disliked: true if the current disliked value is false. In the sequence Undislike → Like, a late undislike failure can overwrite a newer successful like by restoring disliked: true and forcing liked: false, producing incorrect reaction state/counts on the client until the next reload/refetch.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
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.

♻️ Duplicate comments (2)
apps/comments-ui/test/unit/components/content/comment.test.jsx (1)

99-101: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Strengthen the icon-only assertion for dislike-button.

On Line 101, not.toHaveTextContent('1') can miss other numeric text; assert no numeric content (or empty visible text) instead.

Suggested fix
-        expect(screen.getByTestId('dislike-button')).not.toHaveTextContent('1');
+        expect(screen.getByTestId('dislike-button')).not.toHaveTextContent(/\d/);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/comments-ui/test/unit/components/content/comment.test.jsx` around lines
99 - 101, The current assertion only checks that the dislike button does not
contain the specific character '1'; update the assertion for
screen.getByTestId('dislike-button') so it verifies the visible text is empty or
contains no digits (e.g., assert empty visible text or use a regex to assert no
numeric characters), ensuring the icon-only button has no numeric label.
apps/admin-x-framework/src/api/comments.ts (1)

105-114: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

memberThreadCommentIncludes still omits per-member disliked state.

Line 110 uses commentDislikeIncludes (count.dislikes) instead of the member-aware include, so thread replies may miss disliked and render incorrect dislike state.

Proposed fix
 export const memberThreadCommentIncludes = (dislikesEnabled: boolean) => [
     'member',
     'post',
     'count.direct_replies',
     'count.likes',
-    ...(dislikesEnabled ? [commentDislikeIncludes] : []),
+    ...(dislikesEnabled ? [commentDislikeMemberIncludes] : []),
     'count.reports',
     'parent',
     'in_reply_to'
 ].join(',');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/admin-x-framework/src/api/comments.ts` around lines 105 - 114,
memberThreadCommentIncludes currently spreads commentDislikeIncludes which only
adds count.dislikes and therefore omits the per-member disliked state; update
memberThreadCommentIncludes to include the member-aware dislike include (replace
commentDislikeIncludes with the include that adds the per-member "disliked"
field, e.g. commentDislikedIncludes or the existing member-aware dislike include
in the codebase) so thread replies receive both count.dislikes and the
member-specific disliked flag; keep the conditional (dislikesEnabled) behavior
and ensure you reference the member-aware include symbol instead of
commentDislikeIncludes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@apps/admin-x-framework/src/api/comments.ts`:
- Around line 105-114: memberThreadCommentIncludes currently spreads
commentDislikeIncludes which only adds count.dislikes and therefore omits the
per-member disliked state; update memberThreadCommentIncludes to include the
member-aware dislike include (replace commentDislikeIncludes with the include
that adds the per-member "disliked" field, e.g. commentDislikedIncludes or the
existing member-aware dislike include in the codebase) so thread replies receive
both count.dislikes and the member-specific disliked flag; keep the conditional
(dislikesEnabled) behavior and ensure you reference the member-aware include
symbol instead of commentDislikeIncludes.

In `@apps/comments-ui/test/unit/components/content/comment.test.jsx`:
- Around line 99-101: The current assertion only checks that the dislike button
does not contain the specific character '1'; update the assertion for
screen.getByTestId('dislike-button') so it verifies the visible text is empty or
contains no digits (e.g., assert empty visible text or use a regex to assert no
numeric characters), ensuring the icon-only button has no numeric label.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 50159703-c3c5-44c9-bfc9-70cd86ffb0d6

📥 Commits

Reviewing files that changed from the base of the PR and between 42493f3 and c8f3cbd.

⛔ Files ignored due to path filters (2)
  • apps/comments-ui/src/images/icons/thumbs-down.svg is excluded by !**/*.svg
  • apps/comments-ui/src/images/icons/thumbs-up.svg is excluded by !**/*.svg
📒 Files selected for processing (84)
  • apps/admin-x-framework/src/api/comments.ts
  • apps/admin-x-framework/src/utils/api/hooks.ts
  • apps/comments-ui/package.json
  • apps/comments-ui/src/actions.ts
  • apps/comments-ui/src/app-context.ts
  • apps/comments-ui/src/app.tsx
  • apps/comments-ui/src/components/content/buttons/like-button.tsx
  • apps/comments-ui/src/components/content/buttons/like-count.tsx
  • apps/comments-ui/src/components/content/comment.tsx
  • apps/comments-ui/src/components/content/forms/sorting-form.tsx
  • apps/comments-ui/src/utils/api.ts
  • apps/comments-ui/test/unit/actions.test.js
  • apps/comments-ui/test/unit/components/content/comment.test.jsx
  • apps/comments-ui/test/utils/fixtures.ts
  • apps/comments-ui/test/utils/mocked-api.ts
  • apps/posts/src/views/comments/comments.tsx
  • apps/posts/src/views/comments/components/comment-likes-modal.tsx
  • apps/posts/src/views/comments/components/comment-metrics.tsx
  • apps/posts/src/views/comments/components/comment-thread-list.tsx
  • apps/posts/src/views/comments/components/comment-thread-sidebar.tsx
  • apps/posts/src/views/comments/components/comments-list.tsx
  • ghost/i18n/locales/af/comments.json
  • ghost/i18n/locales/ar/comments.json
  • ghost/i18n/locales/bg/comments.json
  • ghost/i18n/locales/bn/comments.json
  • ghost/i18n/locales/bs/comments.json
  • ghost/i18n/locales/ca/comments.json
  • ghost/i18n/locales/context.json
  • ghost/i18n/locales/cs/comments.json
  • ghost/i18n/locales/da/comments.json
  • ghost/i18n/locales/de-CH/comments.json
  • ghost/i18n/locales/de/comments.json
  • ghost/i18n/locales/el/comments.json
  • ghost/i18n/locales/en/comments.json
  • ghost/i18n/locales/eo/comments.json
  • ghost/i18n/locales/es/comments.json
  • ghost/i18n/locales/et/comments.json
  • ghost/i18n/locales/eu/comments.json
  • ghost/i18n/locales/fa/comments.json
  • ghost/i18n/locales/fi/comments.json
  • ghost/i18n/locales/fr/comments.json
  • ghost/i18n/locales/gd/comments.json
  • ghost/i18n/locales/he/comments.json
  • ghost/i18n/locales/hi/comments.json
  • ghost/i18n/locales/hr/comments.json
  • ghost/i18n/locales/hu/comments.json
  • ghost/i18n/locales/id/comments.json
  • ghost/i18n/locales/is/comments.json
  • ghost/i18n/locales/it/comments.json
  • ghost/i18n/locales/ja/comments.json
  • ghost/i18n/locales/ko/comments.json
  • ghost/i18n/locales/kz/comments.json
  • ghost/i18n/locales/lt/comments.json
  • ghost/i18n/locales/lv/comments.json
  • ghost/i18n/locales/mk/comments.json
  • ghost/i18n/locales/mn/comments.json
  • ghost/i18n/locales/ms/comments.json
  • ghost/i18n/locales/nb/comments.json
  • ghost/i18n/locales/ne/comments.json
  • ghost/i18n/locales/nl/comments.json
  • ghost/i18n/locales/nn/comments.json
  • ghost/i18n/locales/pa/comments.json
  • ghost/i18n/locales/pl/comments.json
  • ghost/i18n/locales/pt-BR/comments.json
  • ghost/i18n/locales/pt/comments.json
  • ghost/i18n/locales/ro/comments.json
  • ghost/i18n/locales/ru/comments.json
  • ghost/i18n/locales/si/comments.json
  • ghost/i18n/locales/sk/comments.json
  • ghost/i18n/locales/sl/comments.json
  • ghost/i18n/locales/sq/comments.json
  • ghost/i18n/locales/sr-Cyrl/comments.json
  • ghost/i18n/locales/sr/comments.json
  • ghost/i18n/locales/sv/comments.json
  • ghost/i18n/locales/sw/comments.json
  • ghost/i18n/locales/ta/comments.json
  • ghost/i18n/locales/th/comments.json
  • ghost/i18n/locales/tr/comments.json
  • ghost/i18n/locales/uk/comments.json
  • ghost/i18n/locales/ur/comments.json
  • ghost/i18n/locales/uz/comments.json
  • ghost/i18n/locales/vi/comments.json
  • ghost/i18n/locales/zh-Hant/comments.json
  • ghost/i18n/locales/zh/comments.json
✅ Files skipped from review due to trivial changes (33)
  • ghost/i18n/locales/zh/comments.json
  • apps/comments-ui/package.json
  • ghost/i18n/locales/fi/comments.json
  • ghost/i18n/locales/ja/comments.json
  • ghost/i18n/locales/ne/comments.json
  • ghost/i18n/locales/de/comments.json
  • ghost/i18n/locales/hr/comments.json
  • ghost/i18n/locales/eo/comments.json
  • ghost/i18n/locales/si/comments.json
  • ghost/i18n/locales/vi/comments.json
  • ghost/i18n/locales/sv/comments.json
  • ghost/i18n/locales/ru/comments.json
  • ghost/i18n/locales/et/comments.json
  • ghost/i18n/locales/eu/comments.json
  • ghost/i18n/locales/lv/comments.json
  • ghost/i18n/locales/gd/comments.json
  • ghost/i18n/locales/hi/comments.json
  • ghost/i18n/locales/fr/comments.json
  • ghost/i18n/locales/nl/comments.json
  • ghost/i18n/locales/en/comments.json
  • ghost/i18n/locales/ms/comments.json
  • ghost/i18n/locales/ro/comments.json
  • ghost/i18n/locales/kz/comments.json
  • ghost/i18n/locales/th/comments.json
  • ghost/i18n/locales/ur/comments.json
  • ghost/i18n/locales/mn/comments.json
  • ghost/i18n/locales/sr/comments.json
  • ghost/i18n/locales/lt/comments.json
  • ghost/i18n/locales/sr-Cyrl/comments.json
  • ghost/i18n/locales/sw/comments.json
  • ghost/i18n/locales/id/comments.json
  • ghost/i18n/locales/ar/comments.json
  • ghost/i18n/locales/context.json

ref https://linear.app/ghost/issue/BER-3550/adjust-ui-for-liking-and-disliking-comments-in-comments-ui

Enables the public dislike controls only when comments responses advertise support, while keeping old servers like-only and avoiding public dislike counts.

Also wires the Admin comments UI to the same capability cue for authenticated moderation views.
@jonatansberg jonatansberg force-pushed the adding-dislikes-to-comments-ui-and-admin-capability branch from c8f3cbd to f90b584 Compare May 21, 2026 09:47
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f90b584cec

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

await api.comments.dislike({comment});
return {};
} catch {
dispatchAction('updateCommentDislikeState', {id: comment.id, disliked: false, restoreLiked: wasLiked, expectedDisliked: true});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Prevent stale rollback after dual like/dislike failures

This rollback restores liked from wasLiked captured before the dislike request, but that snapshot may itself be an optimistic state from an earlier in-flight like/unlike request. If a user quickly toggles Like→Dislike (or Unlike→Dislike) and both requests fail out of order, the earlier rollback can be skipped by expected* guards and this later rollback can still reapply the stale snapshot, leaving client reactions/counts inconsistent with the server until a refetch.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
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

🧹 Nitpick comments (1)
apps/comments-ui/test/utils/mocked-api.ts (1)

145-156: ⚡ Quick win

Align tie-break ordering with created_at desc in net-score sorting.

The comparator currently sorts ties by oldest first, which conflicts with the order key (count__net_score desc, created_at desc) and can make mock behavior diverge from real API ordering.

Proposed fix
-                const aDate = new Date(a.created_at).getTime();
-                const bDate = new Date(b.created_at).getTime();
-                return aDate - bDate; // For the rest, sort by date asc
+                const aDate = new Date(a.created_at).getTime();
+                const bDate = new Date(b.created_at).getTime();
+                return bDate - aDate; // Tie-break by created_at desc
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/comments-ui/test/utils/mocked-api.ts` around lines 145 - 156,
Tie-breaker is currently ascending but should be descending for order key
"count__net_score desc, created_at desc": update the comparator inside the if
(setOrder === 'count__net_score desc, created_at desc') block (the
this.comments.sort callback) to return bDate - aDate instead of aDate - bDate so
ties on net score are ordered by created_at descending; ensure the comparator
still computes likesDiff the same way and only flips the final date comparison.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/comments-ui/test/utils/mocked-api.ts`:
- Around line 497-500: The decrement of comment.count.likes when swapping from
liked to disliked can produce negatives if count.likes is 0/undefined; change
the logic around comment.liked to ensure a numeric baseline and clamp to zero,
e.g. compute likes = Math.max(0, (comment.count?.likes ?? 0) - 1) and assign
back to comment.count.likes, then set comment.liked = false; also ensure
comment.count exists before reading/writing to avoid undefined errors.

---

Nitpick comments:
In `@apps/comments-ui/test/utils/mocked-api.ts`:
- Around line 145-156: Tie-breaker is currently ascending but should be
descending for order key "count__net_score desc, created_at desc": update the
comparator inside the if (setOrder === 'count__net_score desc, created_at desc')
block (the this.comments.sort callback) to return bDate - aDate instead of aDate
- bDate so ties on net score are ordered by created_at descending; ensure the
comparator still computes likesDiff the same way and only flips the final date
comparison.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 144b24cd-2aec-4a80-abc3-6133e739fb9b

📥 Commits

Reviewing files that changed from the base of the PR and between c8f3cbd and f90b584.

⛔ Files ignored due to path filters (2)
  • apps/comments-ui/src/images/icons/thumbs-down.svg is excluded by !**/*.svg
  • apps/comments-ui/src/images/icons/thumbs-up.svg is excluded by !**/*.svg
📒 Files selected for processing (84)
  • apps/admin-x-framework/src/api/comments.ts
  • apps/admin-x-framework/src/utils/api/hooks.ts
  • apps/comments-ui/package.json
  • apps/comments-ui/src/actions.ts
  • apps/comments-ui/src/app-context.ts
  • apps/comments-ui/src/app.tsx
  • apps/comments-ui/src/components/content/buttons/like-button.tsx
  • apps/comments-ui/src/components/content/buttons/like-count.tsx
  • apps/comments-ui/src/components/content/comment.tsx
  • apps/comments-ui/src/components/content/forms/sorting-form.tsx
  • apps/comments-ui/src/utils/api.ts
  • apps/comments-ui/test/unit/actions.test.js
  • apps/comments-ui/test/unit/components/content/comment.test.jsx
  • apps/comments-ui/test/utils/fixtures.ts
  • apps/comments-ui/test/utils/mocked-api.ts
  • apps/posts/src/views/comments/comments.tsx
  • apps/posts/src/views/comments/components/comment-likes-modal.tsx
  • apps/posts/src/views/comments/components/comment-metrics.tsx
  • apps/posts/src/views/comments/components/comment-thread-list.tsx
  • apps/posts/src/views/comments/components/comment-thread-sidebar.tsx
  • apps/posts/src/views/comments/components/comments-list.tsx
  • ghost/i18n/locales/af/comments.json
  • ghost/i18n/locales/ar/comments.json
  • ghost/i18n/locales/bg/comments.json
  • ghost/i18n/locales/bn/comments.json
  • ghost/i18n/locales/bs/comments.json
  • ghost/i18n/locales/ca/comments.json
  • ghost/i18n/locales/context.json
  • ghost/i18n/locales/cs/comments.json
  • ghost/i18n/locales/da/comments.json
  • ghost/i18n/locales/de-CH/comments.json
  • ghost/i18n/locales/de/comments.json
  • ghost/i18n/locales/el/comments.json
  • ghost/i18n/locales/en/comments.json
  • ghost/i18n/locales/eo/comments.json
  • ghost/i18n/locales/es/comments.json
  • ghost/i18n/locales/et/comments.json
  • ghost/i18n/locales/eu/comments.json
  • ghost/i18n/locales/fa/comments.json
  • ghost/i18n/locales/fi/comments.json
  • ghost/i18n/locales/fr/comments.json
  • ghost/i18n/locales/gd/comments.json
  • ghost/i18n/locales/he/comments.json
  • ghost/i18n/locales/hi/comments.json
  • ghost/i18n/locales/hr/comments.json
  • ghost/i18n/locales/hu/comments.json
  • ghost/i18n/locales/id/comments.json
  • ghost/i18n/locales/is/comments.json
  • ghost/i18n/locales/it/comments.json
  • ghost/i18n/locales/ja/comments.json
  • ghost/i18n/locales/ko/comments.json
  • ghost/i18n/locales/kz/comments.json
  • ghost/i18n/locales/lt/comments.json
  • ghost/i18n/locales/lv/comments.json
  • ghost/i18n/locales/mk/comments.json
  • ghost/i18n/locales/mn/comments.json
  • ghost/i18n/locales/ms/comments.json
  • ghost/i18n/locales/nb/comments.json
  • ghost/i18n/locales/ne/comments.json
  • ghost/i18n/locales/nl/comments.json
  • ghost/i18n/locales/nn/comments.json
  • ghost/i18n/locales/pa/comments.json
  • ghost/i18n/locales/pl/comments.json
  • ghost/i18n/locales/pt-BR/comments.json
  • ghost/i18n/locales/pt/comments.json
  • ghost/i18n/locales/ro/comments.json
  • ghost/i18n/locales/ru/comments.json
  • ghost/i18n/locales/si/comments.json
  • ghost/i18n/locales/sk/comments.json
  • ghost/i18n/locales/sl/comments.json
  • ghost/i18n/locales/sq/comments.json
  • ghost/i18n/locales/sr-Cyrl/comments.json
  • ghost/i18n/locales/sr/comments.json
  • ghost/i18n/locales/sv/comments.json
  • ghost/i18n/locales/sw/comments.json
  • ghost/i18n/locales/ta/comments.json
  • ghost/i18n/locales/th/comments.json
  • ghost/i18n/locales/tr/comments.json
  • ghost/i18n/locales/uk/comments.json
  • ghost/i18n/locales/ur/comments.json
  • ghost/i18n/locales/uz/comments.json
  • ghost/i18n/locales/vi/comments.json
  • ghost/i18n/locales/zh-Hant/comments.json
  • ghost/i18n/locales/zh/comments.json
✅ Files skipped from review due to trivial changes (40)
  • apps/comments-ui/package.json
  • ghost/i18n/locales/lt/comments.json
  • apps/comments-ui/test/utils/fixtures.ts
  • apps/admin-x-framework/src/utils/api/hooks.ts
  • ghost/i18n/locales/tr/comments.json
  • ghost/i18n/locales/sw/comments.json
  • ghost/i18n/locales/nb/comments.json
  • ghost/i18n/locales/vi/comments.json
  • ghost/i18n/locales/sl/comments.json
  • ghost/i18n/locales/id/comments.json
  • ghost/i18n/locales/pt/comments.json
  • ghost/i18n/locales/sv/comments.json
  • ghost/i18n/locales/de/comments.json
  • ghost/i18n/locales/fi/comments.json
  • ghost/i18n/locales/mn/comments.json
  • ghost/i18n/locales/ru/comments.json
  • ghost/i18n/locales/en/comments.json
  • ghost/i18n/locales/th/comments.json
  • ghost/i18n/locales/zh/comments.json
  • ghost/i18n/locales/nn/comments.json
  • ghost/i18n/locales/bs/comments.json
  • ghost/i18n/locales/cs/comments.json
  • ghost/i18n/locales/ca/comments.json
  • ghost/i18n/locales/nl/comments.json
  • ghost/i18n/locales/de-CH/comments.json
  • ghost/i18n/locales/sr-Cyrl/comments.json
  • ghost/i18n/locales/gd/comments.json
  • ghost/i18n/locales/bg/comments.json
  • ghost/i18n/locales/sq/comments.json
  • ghost/i18n/locales/ta/comments.json
  • ghost/i18n/locales/uk/comments.json
  • ghost/i18n/locales/lv/comments.json
  • ghost/i18n/locales/ur/comments.json
  • ghost/i18n/locales/ar/comments.json
  • ghost/i18n/locales/da/comments.json
  • ghost/i18n/locales/eu/comments.json
  • ghost/i18n/locales/is/comments.json
  • ghost/i18n/locales/context.json
  • ghost/i18n/locales/ko/comments.json
  • ghost/i18n/locales/pa/comments.json

Comment on lines +497 to +500
if (comment.liked) {
comment.count.likes -= 1;
comment.liked = false;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clamp like decrement during dislike swap to avoid negative likes in mocks.

If fixture state is inconsistent (liked: true with count.likes at 0/undefined), this path can return negative likes and cause flaky assertions.

Proposed fix
                 if (comment.liked) {
-                    comment.count.likes -= 1;
+                    comment.count.likes = Math.max(0, (comment.count.likes ?? 0) - 1);
                     comment.liked = false;
                 }
📝 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
if (comment.liked) {
comment.count.likes -= 1;
comment.liked = false;
}
if (comment.liked) {
comment.count.likes = Math.max(0, (comment.count.likes ?? 0) - 1);
comment.liked = false;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/comments-ui/test/utils/mocked-api.ts` around lines 497 - 500, The
decrement of comment.count.likes when swapping from liked to disliked can
produce negatives if count.likes is 0/undefined; change the logic around
comment.liked to ensure a numeric baseline and clamp to zero, e.g. compute likes
= Math.max(0, (comment.count?.likes ?? 0) - 1) and assign back to
comment.count.likes, then set comment.liked = false; also ensure comment.count
exists before reading/writing to avoid undefined errors.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants