Skip to content

feat(mod queue): transfer posts across boards#1182

Merged
tomcasaburi merged 11 commits into
masterfrom
codex/feature/mod-queue-transfer
Jul 2, 2026
Merged

feat(mod queue): transfer posts across boards#1182
tomcasaburi merged 11 commits into
masterfrom
codex/feature/mod-queue-transfer

Conversation

@tomcasaburi

@tomcasaburi tomcasaburi commented Jul 1, 2026

Copy link
Copy Markdown
Member

Summary

  • Add a moderator transfer action for queued and published top-level posts.
  • Recreate the selected post fields on the target board with a one-use temporary account, then mark the source post rejected/removed with a markdown rules reason.
  • Add the 5chan:transferred moderation marker, render [Transferred], and suppress author flags on transferred posts.
  • Make the transfer modal reply-style: draggable, non-blocking, centered on open, yotsuba-b light native controls, and reply-modal-matched close icon sizing.

Verification

  • corepack yarn test
  • corepack yarn lint
  • corepack yarn type-check
  • corepack yarn build
  • corepack yarn doctor --verbose --scope changed (1 non-blocking maintainability warning: large PostTransferModal)
  • playwright-cli computed-style probe in Chrome, Firefox, and WebKit at desktop and mobile viewports: centered modal, light yotsuba-b select/buttons, reply-style header, no footer divider, and transfer close icon matching reply close icon at 18px x 18px.

Note

Medium Risk
Moderation paths that remove/reject posts and publish cross-board content; failures are partially handled (finalization errors, challenge abandon cleanup) but a mistaken transfer still deletes replies with the original.

Overview
Adds a moderator transfer flow for eligible top-level posts (mod queue and edit menu): pick a target board, choose which fields to copy, then recreate the post on the destination—not a protocol-level move.

The client publishes via a one-use temporary account, applies a 5chan:transferred moderation flair on the new post, and rejects or removes the source with a markdown reason (e.g. moved to >>>/g/). UI copy states that replies are not moved and are lost with the original.

PostTransferModal is draggable and non-blocking; mod queue enforces a single open transfer and locks actions while publishing. [Transferred] renders from moderation flairs only; author flags are hidden on transferred posts. New modQueue.* / transfer strings are added across locale files.

Reviewed by Cursor Bugbot for commit d6265f9. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • New Features
    • Added a moderator “transfer post” workflow, including a transfer action, modal with destination selection, optional field copying, and completion feedback.
    • Posts can now show a “Transferred” tag when the transfer marker is present.
  • Bug Fixes
    • Improved transfer availability/visibility so the action only appears for eligible posts and accurately reflects publishing/finalization states.
    • Refined transfer outcome messaging, including how replies and temporary recreated accounts are handled.
  • Documentation
    • Added/updated localized UI strings and notices for the transfer flow across many languages.

@vercel

vercel Bot commented Jul 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
5chan Ready Ready Preview, Comment Jul 2, 2026 7:40am

Request Review

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds moderator transfer-post utilities, a draggable transfer modal, transferred-tag rendering, edit-menu and mod-queue wiring, related tests, and new translation keys across locale files.

Changes

Post Transfer Feature

Layer / File(s) Summary
Transfer helpers and marker logic
src/lib/comment-transfer.ts, src/lib/comment-flags.ts, src/lib/__tests__/comment-transfer.test.ts
Adds transfer field types, marker constants, eligibility checks, payload builders, moderation reference helpers, transferred-marker detection, isCommentFlagFlair, and unit tests.
Transferred tag and post-level wiring
src/components/post-transferred-tag.tsx, src/components/post-transferred-tag.module.css, src/components/__tests__/post-transferred-tag.test.tsx, src/components/post-author-flags.tsx, src/components/post-desktop/post-desktop.tsx, src/components/post-mobile/post-mobile.tsx, src/views/post/post.tsx, src/views/post/__tests__/post.test.tsx, src/components/comment-content/__tests__/comment-content.test.tsx
Adds PostTransferredTag, renders the transferred marker in post views, hides author flags for transferred comments, threads onTransfer through post components, updates memoization, and adjusts related tests.
Transfer modal implementation
src/components/post-transfer-modal/post-transfer-modal.tsx, src/components/post-transfer-modal/post-transfer-modal.module.css
Adds a draggable transfer modal handling field and target selection, temporary account creation, publish/finalize flow, and terminal error states.
Edit menu transfer wiring
src/components/edit-menu/edit-menu.tsx, src/components/edit-menu/edit-menu.module.css, src/components/edit-menu/__tests__/edit-menu.test.tsx
Reworks edit-menu state, adds the Transfer action and modal trigger, styles the menu button, and updates transfer-related tests.
Mod queue transfer actions
src/views/mod-queue/mod-queue.tsx, src/views/mod-queue/mod-queue.module.css, src/views/mod-queue/__tests__/mod-queue.test.tsx
Adds transfer controls to mod-queue rows, cards, and feed posts, wires transfer lifecycle state through the view, adjusts action layout, and expands transfer-focused test coverage.

Estimated code review effort: 5 (Critical) | ~120 minutes

Changes

Transfer Feature Translations

Layer / File(s) Summary
Locale transfer strings
public/translations/*/default.json
Each locale file adds transfer, transferred, and modQueue.transfer* keys covering titles, source/target labels, field-copy options, success messaging, and behavior notices.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • bitsocialnet/5chan#1173: Shares mod-queue rendering and ModQueueContent wiring changes, which overlaps with the transfer controls threaded through the same view.
🚥 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding cross-board post transfer for mod queue moderation.
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.
✨ 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 codex/feature/mod-queue-transfer

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.

Comment thread src/components/post-transfer-modal/post-transfer-modal.tsx
Comment thread src/components/post-transfer-modal/post-transfer-modal.tsx Outdated
@tomcasaburi

Copy link
Copy Markdown
Member Author

Addressed the valid Cursor Bugbot findings in the latest commit.

  • Abandoned transfer challenges now delete the pending temp-account post, delete the temp account, and return the modal to an actionable failed state instead of leaving it stuck as publishing.
  • Transfer finalization now queues the target transferred marker before source removal/rejection, so a target-marker failure does not remove the original post.
  • Remaining partial-transfer risk after a target post is already accepted is protocol-level/non-transactional and cannot be fully rolled back by the UI, so it is documented in code and not blocking this PR.

Local verification after the review fix: yarn test, yarn lint, yarn type-check, yarn build, and yarn doctor --verbose --scope changed.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🧹 Nitpick comments (2)
src/components/post-transferred-tag.tsx (1)

2-3: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Keep this shared component's styles out of src/views/.

Importing ../views/post/post.module.css couples reusable UI to a route stylesheet. Move .transferredTag into a component-local/shared CSS module so this stays reusable outside the post view. As per coding guidelines, "Keep route composition in src/views/, reusable UI in src/components/."

🤖 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 `@src/components/post-transferred-tag.tsx` around lines 2 - 3, The shared
`PostTransferredTag` component is importing styling from the post route
stylesheet, which ties reusable UI to `src/views/`. Move the `.transferredTag`
rules into a component-local or shared CSS module for
`post-transferred-tag.tsx`, then update the `styles` import in that component to
use the new module so it stays reusable outside the post view.

Source: Coding guidelines

src/views/post/__tests__/post.test.tsx (1)

200-215: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicated data-transferred derivation across desktop/mobile mocks.

The flair.text === '5chan:transferred' lookup logic is repeated verbatim in both mock presenters. Extracting a tiny shared helper would avoid drift if the marker key or logic changes.

Also applies to: 230-245

🤖 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 `@src/views/post/__tests__/post.test.tsx` around lines 200 - 215, The
`data-transferred` value is derived by repeating the same
`post.commentModeration.flairs.some(flair => flair.text ===
'5chan:transferred')` logic in both mock presenters. Extract this check into a
small shared helper used by the desktop and mobile presenter code in
`post.test.tsx`, so both render paths stay consistent if the marker text or
matching logic changes. Keep the helper close to the existing presenter setup
and update the `data-transferred` attribute to call it from both places.
🤖 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 `@public/translations/hu/default.json`:
- Line 456: The translation for modQueue.transferTarget is inconsistent with the
surrounding transfer strings that use the board term. Update the Hungarian copy
in the default translations so it uses the same “tábla” wording as the related
transfer labels, keeping the terminology aligned across the modal.

In `@public/translations/id/default.json`:
- Around line 454-461: The transfer modal strings in the Indonesian translations
are only partially localized, leaving English terms in the moderator flow.
Update the affected keys in default.json—especially modQueue.transferTitle,
modQueue.transferTitleWithNumber, modQueue.transferTarget, and
modQueue.transferSuccess—to use consistent Indonesian wording, and align the
target term with the locale’s existing “papan” usage instead of “forum” or
“board.”

In `@public/translations/it/default.json`:
- Around line 456-459: The transfer strings in the Italian translation file are
only partially localized, with “board” still left in English in the modQueue
transfer messages. Update the affected translation entries in the modQueue block
so the board term is fully and consistently translated in Italian, keeping the
wording aligned with the surrounding strings and the same terminology used
across the file.

In `@src/components/post-transfer-modal/post-transfer-modal.tsx`:
- Around line 111-333: Add a test file for PostTransferModal covering the
transfer state machine and lifecycle behavior, since this logic is reasonably
testable and currently unverified. Focus tests on PostTransferModal’s
handleSubmit flow, challenge abandonment cleanup, success path finalization, and
failure recovery by mocking the store actions and transfer helpers. Include
cases for selecting fields/target gating canSubmit, temporary account
creation/deletion, pending comment cleanup, and dispatching
publishSucceeded/publishFailed on the reducer.
- Around line 134-155: The target-board selection logic in the post transfer
modal is defaulting to boardOptions[0], which makes a board appear chosen before
the moderator explicitly picks one. Update the resolved target board handling in
post-transfer-modal.tsx so it only uses targetBoardAddress and does not
auto-select the first eligible board, and keep canSubmit disabled until an
explicit target board address is present. Use the existing symbols
resolvedTargetBoardAddress, resolvedTargetBoard, and canSubmit to locate and
adjust the selection flow consistently for the transfer action.

In `@src/lib/comment-transfer.ts`:
- Around line 53-66: `getTransferPublishPayload()` is trimming transferred
comment bodies by routing `comment.content` through `getTextField()`. Update the
payload-building logic in `getTransferPublishPayload` so body text is copied
verbatim after separately checking it isn’t blank, and keep the existing
handling for `displayName`, `title`, `link`, and `getTransferableCommentFlairs`.

In `@src/views/mod-queue/__tests__/mod-queue.test.tsx`:
- Around line 27-55: The mod-queue tests only cover the compact path, so the new
transfer wiring in ModQueueFeedPost is still unverified. Update
src/views/mod-queue/__tests__/mod-queue.test.tsx to add a feed-mode case by
setting testState.viewMode to 'feed' and exercising the transfer flow through
ModQueueFeedPost. Assert that the transfer action still opens and closes
correctly in feed mode, using the existing mod-queue helpers and mocks to locate
the behavior.

In `@src/views/post/post.tsx`:
- Line 209: The post transfer handler is being forwarded to reply posts, but
transfer should only be available for top-level posts. Update the Post rendering
flow in post.tsx so the handler is derived once for top-level usage and not
passed into child/reply Post instances; adjust the relevant Post props and the
rendering paths that currently forward onTransfer so replies never receive it.

---

Nitpick comments:
In `@src/components/post-transferred-tag.tsx`:
- Around line 2-3: The shared `PostTransferredTag` component is importing
styling from the post route stylesheet, which ties reusable UI to `src/views/`.
Move the `.transferredTag` rules into a component-local or shared CSS module for
`post-transferred-tag.tsx`, then update the `styles` import in that component to
use the new module so it stays reusable outside the post view.

In `@src/views/post/__tests__/post.test.tsx`:
- Around line 200-215: The `data-transferred` value is derived by repeating the
same `post.commentModeration.flairs.some(flair => flair.text ===
'5chan:transferred')` logic in both mock presenters. Extract this check into a
small shared helper used by the desktop and mobile presenter code in
`post.test.tsx`, so both render paths stay consistent if the marker text or
matching logic changes. Keep the helper close to the existing presenter setup
and update the `data-transferred` attribute to call it from both places.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: b6addd2b-84bb-4288-8001-9dcb2317b0ca

📥 Commits

Reviewing files that changed from the base of the PR and between 546ee99 and f35c01a.

📒 Files selected for processing (55)
  • public/translations/ar/default.json
  • public/translations/bn/default.json
  • public/translations/cs/default.json
  • public/translations/da/default.json
  • public/translations/de/default.json
  • public/translations/el/default.json
  • public/translations/en/default.json
  • public/translations/es/default.json
  • public/translations/fa/default.json
  • public/translations/fi/default.json
  • public/translations/fil/default.json
  • public/translations/fr/default.json
  • public/translations/he/default.json
  • public/translations/hi/default.json
  • public/translations/hu/default.json
  • public/translations/id/default.json
  • public/translations/it/default.json
  • public/translations/ja/default.json
  • public/translations/ko/default.json
  • public/translations/mr/default.json
  • public/translations/nl/default.json
  • public/translations/no/default.json
  • public/translations/pl/default.json
  • public/translations/pt/default.json
  • public/translations/ro/default.json
  • public/translations/ru/default.json
  • public/translations/sq/default.json
  • public/translations/sv/default.json
  • public/translations/te/default.json
  • public/translations/th/default.json
  • public/translations/tr/default.json
  • public/translations/uk/default.json
  • public/translations/ur/default.json
  • public/translations/vi/default.json
  • public/translations/zh/default.json
  • src/components/__tests__/post-transferred-tag.test.tsx
  • src/components/comment-content/__tests__/comment-content.test.tsx
  • src/components/edit-menu/__tests__/edit-menu.test.tsx
  • src/components/edit-menu/edit-menu.module.css
  • src/components/edit-menu/edit-menu.tsx
  • src/components/post-author-flags.tsx
  • src/components/post-desktop/post-desktop.tsx
  • src/components/post-mobile/post-mobile.tsx
  • src/components/post-transfer-modal/post-transfer-modal.module.css
  • src/components/post-transfer-modal/post-transfer-modal.tsx
  • src/components/post-transferred-tag.tsx
  • src/lib/__tests__/comment-transfer.test.ts
  • src/lib/comment-flags.ts
  • src/lib/comment-transfer.ts
  • src/views/mod-queue/__tests__/mod-queue.test.tsx
  • src/views/mod-queue/mod-queue.module.css
  • src/views/mod-queue/mod-queue.tsx
  • src/views/post/__tests__/post.test.tsx
  • src/views/post/post.module.css
  • src/views/post/post.tsx

"transfer": "Átvitel",
"modQueue.transferTitle": "Bejegyzés áthelyezése",
"modQueue.transferSource": "Honnan",
"modQueue.transferTarget": "Fórumra",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use the board term consistently.

Fórumra shifts the meaning from the rest of the locale copy, which refers to the target as a tábla in the surrounding transfer strings. That makes the modal inconsistent and a bit misleading.

♻️ Suggested fix
-  "modQueue.transferTarget": "Fórumra",
+  "modQueue.transferTarget": "Táblára",
📝 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
"modQueue.transferTarget": "Fórumra",
"modQueue.transferTarget": "Táblára",
🤖 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 `@public/translations/hu/default.json` at line 456, The translation for
modQueue.transferTarget is inconsistent with the surrounding transfer strings
that use the board term. Update the Hungarian copy in the default translations
so it uses the same “tábla” wording as the related transfer labels, keeping the
terminology aligned across the modal.

Comment on lines +454 to +461
"modQueue.transferTitle": "Transfer posting",
"modQueue.transferSource": "Dari",
"modQueue.transferTarget": "Ke forum",
"modQueue.transferFields": "Salin bidang",
"modQueue.transferNoFields": "Pilih setidaknya satu bidang untuk disalin.",
"modQueue.transferSuccess": "Ditransfer. Postingan asli sekarang mengarah ke board baru.",
"transferred": "Ditransfer",
"modQueue.transferTitleWithNumber": "Transfer postingan No.{{number}}",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fully localize the transfer modal strings.

Transfer posting, Transfer postingan, and board leak English into the Indonesian UI, and Ke forum disagrees with the papan wording used elsewhere in the locale. That leaves the moderator flow half-translated.

♻️ Suggested fix
-  "modQueue.transferTitle": "Transfer posting",
+  "modQueue.transferTitle": "Pindahkan postingan",
...
-  "modQueue.transferTarget": "Ke forum",
+  "modQueue.transferTarget": "Ke papan",
...
-  "modQueue.transferSuccess": "Ditransfer. Postingan asli sekarang mengarah ke board baru.",
+  "modQueue.transferSuccess": "Dipindahkan. Posting asli sekarang mengarah ke papan baru.",
...
-  "modQueue.transferTitleWithNumber": "Transfer postingan No.{{number}}",
+  "modQueue.transferTitleWithNumber": "Pindahkan postingan No.{{number}}",
📝 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
"modQueue.transferTitle": "Transfer posting",
"modQueue.transferSource": "Dari",
"modQueue.transferTarget": "Ke forum",
"modQueue.transferFields": "Salin bidang",
"modQueue.transferNoFields": "Pilih setidaknya satu bidang untuk disalin.",
"modQueue.transferSuccess": "Ditransfer. Postingan asli sekarang mengarah ke board baru.",
"transferred": "Ditransfer",
"modQueue.transferTitleWithNumber": "Transfer postingan No.{{number}}",
"modQueue.transferTitle": "Pindahkan postingan",
"modQueue.transferSource": "Dari",
"modQueue.transferTarget": "Ke papan",
"modQueue.transferFields": "Salin bidang",
"modQueue.transferNoFields": "Pilih setidaknya satu bidang untuk disalin.",
"modQueue.transferSuccess": "Dipindahkan. Posting asli sekarang mengarah ke papan baru.",
"transferred": "Ditransfer",
"modQueue.transferTitleWithNumber": "Pindahkan postingan No.{{number}}",
🤖 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 `@public/translations/id/default.json` around lines 454 - 461, The transfer
modal strings in the Indonesian translations are only partially localized,
leaving English terms in the moderator flow. Update the affected keys in
default.json—especially modQueue.transferTitle,
modQueue.transferTitleWithNumber, modQueue.transferTarget, and
modQueue.transferSuccess—to use consistent Indonesian wording, and align the
target term with the locale’s existing “papan” usage instead of “forum” or
“board.”

Comment on lines +456 to +459
"modQueue.transferTarget": "Al board",
"modQueue.transferFields": "Copia campi",
"modQueue.transferNoFields": "Scegli almeno un campo da copiare.",
"modQueue.transferSuccess": "Trasferito. Il post originale ora punta alla nuova board.",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Translate the board labels fully.

Al board / nuova board leave English in the Italian UI, so the transfer flow reads half-localized next to the surrounding Italian strings. Use the same Italian board term consistently.

♻️ Suggested fix
-  "modQueue.transferTarget": "Al board",
+  "modQueue.transferTarget": "Alla bacheca",
...
-  "modQueue.transferSuccess": "Trasferito. Il post originale ora punta alla nuova board.",
+  "modQueue.transferSuccess": "Trasferito. Il post originale ora punta alla nuova bacheca.",
📝 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
"modQueue.transferTarget": "Al board",
"modQueue.transferFields": "Copia campi",
"modQueue.transferNoFields": "Scegli almeno un campo da copiare.",
"modQueue.transferSuccess": "Trasferito. Il post originale ora punta alla nuova board.",
"modQueue.transferTarget": "Alla bacheca",
"modQueue.transferFields": "Copia campi",
"modQueue.transferNoFields": "Scegli almeno un campo da copiare.",
"modQueue.transferSuccess": "Trasferito. Il post originale ora punta alla nuova bacheca.",
🤖 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 `@public/translations/it/default.json` around lines 456 - 459, The transfer
strings in the Italian translation file are only partially localized, with
“board” still left in English in the modQueue transfer messages. Update the
affected translation entries in the modQueue block so the board term is fully
and consistently translated in Italian, keeping the wording aligned with the
surrounding strings and the same terminology used across the file.

Comment on lines +111 to +333
const PostTransferModal = ({ comment, onClose }: PostTransferModalProps) => {
const { t } = useTranslation();
const nodeRef = useRef<HTMLDivElement>(null);
const finalizedTransferRef = useRef(false);
const bodySelectionStyleBeforeDragRef = useRef<{ userSelect: string; webkitUserSelect: string } | null>(null);
const directories = useDirectories();
const createAccount = useAccountsStore((state) => state.accountsActions.createAccount) as CreateAccountAction;
const publishComment = useAccountsStore((state) => state.accountsActions.publishComment) as PublishCommentAction;
const publishCommentModeration = useAccountsStore((state) => state.accountsActions.publishCommentModeration) as PublishCommentModerationAction;
const deleteComment = useAccountsStore((state) => state.accountsActions.deleteComment) as DeleteCommentAction;
const deleteAccount = useAccountsStore((state) => state.accountsActions.deleteAccount) as DeleteAccountAction;
const sourceCommunityAddress = getCommentCommunityAddress(comment);
const sourceCommentCid = comment.cid;
const boardOptions = useMemo(
() =>
directories
.filter((community) => community.address && (!sourceCommunityAddress || !areSameBoardAddress(community.address, sourceCommunityAddress)))
.sort((left, right) => getTransferBoardLabel(left).localeCompare(getTransferBoardLabel(right), undefined, { sensitivity: 'base' })),
[directories, sourceCommunityAddress],
);
const [modalState, dispatchModalState] = useReducer(transferModalReducer, comment, getInitialTransferModalState);
const { targetBoardAddress, selectedFields, transferState, transferError, transferredIndex } = modalState;

const resolvedTargetBoardAddress = targetBoardAddress || boardOptions[0]?.address || '';
const resolvedTargetBoard = boardOptions.find((community) => community.address === resolvedTargetBoardAddress);
const availableFields = useMemo(() => getAvailableTransferFields(comment), [comment]);
const sourceBoard = useMemo(
() => (sourceCommunityAddress ? directories.find((community) => areSameBoardAddress(community.address, sourceCommunityAddress)) : undefined),
[directories, sourceCommunityAddress],
);
const sourceBoardLabel = useMemo(() => {
return sourceBoard ? getTransferBoardLabel(sourceBoard) : sourceCommunityAddress || 'N/A';
}, [sourceBoard, sourceCommunityAddress]);
const transferTitle = comment.number !== undefined ? t('modQueue.transferTitleWithNumber', { number: comment.number }) : t('modQueue.transferTitle');
const isPublishingTransfer = transferState === 'publishing';
const canSubmit =
!isPublishingTransfer &&
Boolean(resolvedTargetBoardAddress) &&
Boolean(sourceCommunityAddress) &&
Boolean(sourceCommentCid) &&
typeof createAccount === 'function' &&
typeof publishComment === 'function' &&
typeof publishCommentModeration === 'function' &&
typeof deleteAccount === 'function' &&
hasSelectedTransferFields(selectedFields, availableFields);

const [{ left, top }, api] = useSpring(
() => ({
from: getInitialTransferModalPosition(),
}),
[],
);

const disableBodyTextSelection = () => {
if (!bodySelectionStyleBeforeDragRef.current) {
bodySelectionStyleBeforeDragRef.current = {
userSelect: document.body.style.userSelect,
webkitUserSelect: document.body.style.webkitUserSelect,
};
}
Object.assign(document.body.style, { userSelect: 'none', webkitUserSelect: 'none' });
};

const restoreBodyTextSelection = () => {
const previousStyle = bodySelectionStyleBeforeDragRef.current;
Object.assign(document.body.style, {
userSelect: previousStyle?.userSelect ?? '',
webkitUserSelect: previousStyle?.webkitUserSelect ?? '',
});
bodySelectionStyleBeforeDragRef.current = null;
};

const bind = useDrag(
({ active, event, offset: [ox, oy] }) => {
const nextLeft = Math.round(ox);
const nextTop = Math.round(oy);

if (active) {
event.preventDefault();
disableBodyTextSelection();
} else {
restoreBodyTextSelection();
}
api.start({ left: nextLeft, top: nextTop, immediate: true });
},
{
from: () => [left.get(), top.get()],
filterTaps: true,
bounds: undefined,
},
);

const closeTransferModalOnEscape = useEffectEvent(() => {
if (!isPublishingTransfer) {
onClose();
}
});

useEffect(() => {
const handleTransferModalKeydown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
closeTransferModalOnEscape();
}
};

document.addEventListener('keydown', handleTransferModalKeydown);
return () => {
document.removeEventListener('keydown', handleTransferModalKeydown);
restoreBodyTextSelection();
};
}, []);

const getTransferModerationCallbacks = (message: string) => ({
onChallenge: async (...args: any[]) => {
useChallengesStore.getState().addChallenge([...args, comment]);
},
onChallengeVerification: async (challengeVerification: ChallengeVerification, moderation: Comment) => {
alertChallengeVerificationFailed(challengeVerification, moderation);
},
onError: (error: Error & { details?: unknown }) => {
console.error(message, error, error.details);
},
});

const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!canSubmit) return;

let pendingIndex: number | undefined;
let temporaryAccountCreated = false;
const temporaryAccountName = getTemporaryTransferAccountName(sourceCommentCid);
const cleanupTemporaryAccount = async ({ deletePendingComment = false } = {}) => {
if (!temporaryAccountCreated) return;
if (deletePendingComment && pendingIndex !== undefined && typeof deleteComment === 'function') {
try {
await deleteComment(pendingIndex, temporaryAccountName);
} catch (error) {
console.error('Transfer pending comment cleanup failed:', error);
}
}
try {
await deleteAccount(temporaryAccountName);
temporaryAccountCreated = false;
} catch (error) {
console.error('Transfer temporary account cleanup failed:', error);
}
};
finalizedTransferRef.current = false;
dispatchModalState({ type: 'publishStarted' });

try {
await createAccount(temporaryAccountName);
temporaryAccountCreated = true;
const payload = getTransferPublishPayload(comment, selectedFields, resolvedTargetBoardAddress);
await publishComment(
{
...payload,
_onPendingCommentIndex: (index: number) => {
pendingIndex = index;
},
onChallenge: async (...args: any[]) => {
useChallengesStore.getState().addChallenge([...args, comment], async () => {
await cleanupTemporaryAccount({ deletePendingComment: true });
dispatchModalState({ type: 'publishFailed', error: new Error('Transfer challenge was abandoned.') });
});
},
onChallengeVerification: async (challengeVerification: ChallengeVerification, challengeComment: Comment) => {
try {
alertChallengeVerificationFailed(challengeVerification, challengeComment);
if (challengeVerification?.challengeSuccess !== true) {
return;
}
if (finalizedTransferRef.current) return;
finalizedTransferRef.current = true;

const targetCommentCid = getTransferredCommentCid(challengeVerification, challengeComment);
if (!targetCommentCid) {
throw new Error('Transferred post was accepted, but no target CID was returned.');
}

// Queue the target marker first so a target moderation failure does not remove the original post.
await publishCommentModeration({
commentCid: targetCommentCid,
communityAddress: resolvedTargetBoardAddress,
commentModeration: {
flairs: getTargetTransferModerationFlairs(comment, selectedFields),
},
...getTransferModerationCallbacks('Transfer target moderation failed:'),
});
await publishCommentModeration({
commentCid: sourceCommentCid,
communityAddress: sourceCommunityAddress,
commentModeration: getTransferSourceModeration(
comment,
getTransferBoardReference(resolvedTargetBoard, resolvedTargetBoardAddress),
getTransferSourceBoardReference(sourceBoard, sourceCommunityAddress),
getTransferSourceBoardRulesLink(sourceBoard),
),
...getTransferModerationCallbacks('Transfer source moderation failed:'),
});

await cleanupTemporaryAccount();
dispatchModalState({ type: 'publishSucceeded', transferredIndex: pendingIndex });
} catch (error) {
console.error('Transfer finalization failed:', error);
await cleanupTemporaryAccount();
dispatchModalState({ type: 'publishFailed', error });
}
},
onError: (error: Error & { details?: unknown }) => {
console.error('Transfer failed:', error, error.details);
void cleanupTemporaryAccount({ deletePendingComment: true });
dispatchModalState({ type: 'publishFailed', error });
},
},
temporaryAccountName,
);
} catch (error) {
console.error('Transfer failed:', error);
await cleanupTemporaryAccount({ deletePendingComment: true });
dispatchModalState({ type: 'publishFailed', error });
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Add tests for this transfer flow before merge.

This modal now owns a fairly intricate state machine: field selection, explicit target selection, temp-account lifecycle, challenge abandonment cleanup, and success/failure recovery. I don't see a companion test file in this layer, and this is well within the repo's "reasonably testable" bar. As per coding guidelines, "**/*.test.{ts,tsx}: Add or update tests for bug fixes and non-trivial logic changes when the code is reasonably testable."

🤖 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 `@src/components/post-transfer-modal/post-transfer-modal.tsx` around lines 111
- 333, Add a test file for PostTransferModal covering the transfer state machine
and lifecycle behavior, since this logic is reasonably testable and currently
unverified. Focus tests on PostTransferModal’s handleSubmit flow, challenge
abandonment cleanup, success path finalization, and failure recovery by mocking
the store actions and transfer helpers. Include cases for selecting
fields/target gating canSubmit, temporary account creation/deletion, pending
comment cleanup, and dispatching publishSucceeded/publishFailed on the reducer.

Source: Coding guidelines

Comment thread src/components/post-transfer-modal/post-transfer-modal.tsx Outdated
Comment thread src/lib/comment-transfer.ts
Comment thread src/views/mod-queue/__tests__/mod-queue.test.tsx
Comment thread src/views/post/post.tsx
@tomcasaburi

Copy link
Copy Markdown
Member Author

Addressed the valid CodeRabbit correctness findings in the latest commit.

Fixed:

  • Target board now must be chosen explicitly before Transfer is enabled.
  • Transferred comment body text is copied verbatim while still ignoring blank bodies.
  • Transfer handlers are not forwarded to replies, and the Post memo comparator now tracks parentCid for that guard.
  • Feed-mode mod queue transfer wiring is covered by a test.
  • PostTransferredTag now owns its component-local CSS, and duplicate transferred-marker test logic was removed.

Deferred/declined:

  • A separate PostTransferModal-only test file is not needed for this PR because the modal lifecycle is covered through the mod-queue user workflow tests, including success, failed challenge retry, abandoned challenge cleanup, close behavior, centering, and feed-mode entry.
  • The remaining locale wording suggestions are minor copy polish and not merge-blocking for this feature.

Local verification after this batch: yarn test, yarn lint, yarn type-check, yarn build, yarn doctor --verbose --scope changed, and yarn knip.

Comment thread src/components/edit-menu/edit-menu.tsx Outdated
@tomcasaburi

Copy link
Copy Markdown
Member Author

Addressed the latest Cursor Bugbot finding in ff6841e: transfer eligibility is now centralized in canTransferComment and unavailable source posts (deleted, removed, moderation-removed, purged, archived) no longer expose transfer from either the edit menu or mod queue. Added helper, edit-menu, and mod-queue coverage.

Comment thread src/components/post-transfer-modal/post-transfer-modal.tsx
@tomcasaburi

Copy link
Copy Markdown
Member Author

Addressed the latest Cursor Bugbot finding in b1c59ac: after a transfer succeeds, the modal now treats success as a terminal state. Target/field controls and the submit button stay disabled, so the moderator can close the modal but cannot publish a second copied post from the same transfer dialog. Added workflow coverage for the repeat-submit guard.

Comment thread src/views/mod-queue/mod-queue.tsx
Comment thread src/views/mod-queue/mod-queue.tsx Outdated
@tomcasaburi

Copy link
Copy Markdown
Member Author

Addressed the latest Cursor Bugbot queue-state findings in a18e121. Mod-queue transfer state now flows back to the queue item: approve/reject/transfer actions are locked while transfer is publishing, success locally marks the item as rejected and remembers the rejected queue snapshot, and abandoned/failed transfer releases the publishing lock. Added compact and feed-mode coverage for those states.

Comment thread src/components/post-transfer-modal/post-transfer-modal.tsx Outdated
@tomcasaburi

Copy link
Copy Markdown
Member Author

Addressed the latest Cursor Bugbot finding in b04694f: the transfer success message no longer appends the temporary account pending-comment index, since that is not the destination post number. The modal state no longer stores that internal index, and the mod-queue workflow test now guards against showing it.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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 `@src/components/post-transfer-modal/post-transfer-modal.tsx`:
- Around line 328-332: The catch block in post-transfer finalization is mapping
every error to the generic failed state, which re-enables submit after the
target post has already been accepted. Update the post-transfer-modal flow in
the finalize/acceptance path to use a distinct post-acceptance partial-failure
state or otherwise keep resubmission disabled, and make sure
`onTransferStateChange` and `dispatchModalState` in the transfer finalization
logic do not allow retry from the same dialog after target acceptance.
- Around line 335-338: The transfer failure handler in post-transfer-modal.tsx
reports the state as failed before the rollback cleanup finishes, which can let
retries race with temporary account deletion. Update the onError path in the
transfer flow to await cleanupTemporaryAccount({ deletePendingComment: true })
before calling onTransferStateChange?.('failed'), and keep the sequencing inside
the same onError callback so retries are only re-enabled after rollback
completes.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 100b1cbf-b5c6-45bf-bd0f-6373890b6be2

📥 Commits

Reviewing files that changed from the base of the PR and between b1c59ac and a18e121.

📒 Files selected for processing (3)
  • src/components/post-transfer-modal/post-transfer-modal.tsx
  • src/views/mod-queue/__tests__/mod-queue.test.tsx
  • src/views/mod-queue/mod-queue.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/views/mod-queue/tests/mod-queue.test.tsx
  • src/views/mod-queue/mod-queue.tsx

Comment thread src/components/post-transfer-modal/post-transfer-modal.tsx Outdated
Comment thread src/components/post-transfer-modal/post-transfer-modal.tsx Outdated
Comment thread src/components/post-transfer-modal/post-transfer-modal.tsx
Comment thread src/views/mod-queue/mod-queue.tsx
@tomcasaburi

Copy link
Copy Markdown
Member Author

Addressed the latest Cursor Bugbot/CodeRabbit transfer-failure findings in 359ee04. Failures before target acceptance still clean up and allow retry, but failures after the target post may exist now enter a terminal finalization-failed state so the same dialog cannot publish another copy. The publish error path now awaits pending-post/account cleanup before re-enabling retry, and the mod queue now allows only one transfer modal to be open at a time. Added regression coverage for all three cases.

Comment thread src/views/mod-queue/mod-queue.tsx
@tomcasaburi

Copy link
Copy Markdown
Member Author

Addressed the latest Cursor Bugbot hidden-lock finding in bb80e71. The mod queue now keeps the raw active transfer id in the shared transfer controls, so an in-flight transfer continues to block other transfer modals even if that active row drops out of the visible feed. Added a regression test for the active item leaving the visible queue while the lock remains held.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit bb80e71. Configure here.

Comment thread src/views/mod-queue/mod-queue.tsx
@tomcasaburi

Copy link
Copy Markdown
Member Author

Addressed the latest transfer-lock review note. The active transfer lock now stays while a hidden queue item is still publishing, then clears from the async terminal state even after the modal unmounts. I also removed the transfer modal centering transform so the close sprite renders on whole-pixel coordinates while keeping the button styling identical to the reply modal.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/post-transfer-modal/post-transfer-modal.tsx (1)

165-175: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Gate transfer on deleteComment availability too.

cleanupTemporaryAccount({ deletePendingComment: true }) relies on deleteComment, but canSubmit can still enable transfer when that action is unavailable. That would leave the pending temp-account post behind on challenge abandonment or publish failure.

Proposed fix
     typeof publishComment === 'function' &&
     typeof publishCommentModeration === 'function' &&
     typeof deleteAccount === 'function' &&
+    typeof deleteComment === 'function' &&
     hasSelectedTransferFields(selectedFields, availableFields);

Also applies to: 266-274

🤖 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 `@src/components/post-transfer-modal/post-transfer-modal.tsx` around lines 165
- 175, `canSubmit` in `post-transfer-modal.tsx` does not gate submission on
`deleteComment`, even though `cleanupTemporaryAccount({ deletePendingComment:
true })` depends on it. Update the `canSubmit` check to require `typeof
deleteComment === 'function'` alongside the existing action availability checks,
and make sure any related submit/cleanup path that relies on
`cleanupTemporaryAccount` uses the same guard so transfers cannot start when
pending comment deletion is unavailable.
🤖 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.

Outside diff comments:
In `@src/components/post-transfer-modal/post-transfer-modal.tsx`:
- Around line 165-175: `canSubmit` in `post-transfer-modal.tsx` does not gate
submission on `deleteComment`, even though `cleanupTemporaryAccount({
deletePendingComment: true })` depends on it. Update the `canSubmit` check to
require `typeof deleteComment === 'function'` alongside the existing action
availability checks, and make sure any related submit/cleanup path that relies
on `cleanupTemporaryAccount` uses the same guard so transfers cannot start when
pending comment deletion is unavailable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b59e6390-6fa1-45e0-9b81-4f6f60bfbece

📥 Commits

Reviewing files that changed from the base of the PR and between a18e121 and d6265f9.

📒 Files selected for processing (4)
  • src/components/post-transfer-modal/post-transfer-modal.module.css
  • src/components/post-transfer-modal/post-transfer-modal.tsx
  • src/views/mod-queue/__tests__/mod-queue.test.tsx
  • src/views/mod-queue/mod-queue.tsx
💤 Files with no reviewable changes (1)
  • src/components/post-transfer-modal/post-transfer-modal.module.css
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/views/mod-queue/mod-queue.tsx

@tomcasaburi tomcasaburi merged commit b4a6f0a into master Jul 2, 2026
12 checks passed
@tomcasaburi tomcasaburi deleted the codex/feature/mod-queue-transfer branch July 2, 2026 07:52
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.

1 participant