Skip to content

Conversation

@avocadoheather
Copy link

Description

This change introduces a lightweight post archiving feature so posts can be archived and unarchived without being deleted or purged.

What I changed

Added a persistent archived integer field to post objects so archives are stored on posts:

Why

  • Archiving is a common moderation/maintenance action that preserves content while removing it from active browsing or marking it for later review. This implementation is minimally invasive: it stores an archived flag on posts, provides server-side helpers and API endpoints, and hooks into the existing post tools/menu flow.

API

  • PUT /posts/:pid/archive — archive the post (requires privileges; currently uses posts:delete privilege as a proxy)
  • DELETE /posts/:pid/archive — unarchive the post

Websocket events

Notes, assumptions, and rationale

  • Permission model: I used the existing posts:delete/delete privilege as a proxy for archiving permission (admins/mods who can delete can archive). This is intentional to avoid adding new privilege wiring inside category/admin convenience code in this change. I recommend adding an explicit posts:archive privilege in categories.js (and updating posts.js checks) if you want separate archive permissions.
  • I added client-side handlers so the archive menu item will work if the template or a plugin includes post/archive or post/unarchive in the posts.tools menu. I also made the socket tool loader automatically add those menu entries when the caller has moderation/delete privileges.
  • No database migration file needed: archived is stored as a post object field (integer). Existing posts will simply have archived unset / falsy until changed.
  • I did not add translations for [[topic:archive]] / [[topic:unarchive]] in every language; two language keys used in the menu rely on existing translation pipeline — these should be added to language JSONs if you want human-readable strings for all locales.
  • Tests: I did not add unit/integration tests in this PR. I can add tests for:
  • Posts.tools.archive/unarchive behavior and permission checks
  • postsAPI.archive/unarchive endpoints and emitted websocket events
  • client postAction integration
  • Template changes: The socket tool loader inserts post/archive/post/unarchive items into the tools data structure; ensure your forum template/partials (partials/topic/post-menu-list) renders those items (standard NodeBB templates do).

Files changed (high-level)

How to test (manual)

  • Start the app and log in as a moderator (or a user with delete permissions for a category).
  • In a topic, open the post tools/menu for a post. You should see an "Archive" action (or "Unarchive" if the post is already archived). If your theme/template does not show the menu item, the socket loader still exposes the tool data and a theme change may be needed to show it.
  • Click "Archive" and confirm. The post should be archived (server-side post:.archived becomes 1) and the websocket event event:post_archived is emitted to the topic room. UI elements can then react to archived state (this PR does not hide posts by default — that is left to the theme/UX or follow-up).
  • Click "Unarchive" to restore archived to 0; event:post_unarchived should be emitted.

Quick curl example (RPC/HTTP mapping may vary depending on server config but the API layer supports these calls):

  • Archive: PUT /posts//archive
  • Unarchive: DELETE /posts//archive

Follow-ups / TODO

  • Add a distinct posts:archive privilege and expose it in admin category privilege settings.
  • Add language keys for [[topic:archive]] and [[topic:unarchive]] across locales.
  • Add unit/integration tests for backend and API.
  • Optionally: update topic/post rendering to hide archived posts or display an "archived" badge in templates, per project UX needs.

Changelog entry (one-liner) Add post archive/unarchive support (archived flag, posts.tools, API + client)

@katherinee-li katherinee-li self-assigned this Oct 2, 2025
Copy link

@katherinee-li katherinee-li left a comment

Choose a reason for hiding this comment

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

Not passing npm, not enough data

@ChrisTimperley ChrisTimperley requested a review from Copilot October 2, 2025 13:58
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR adds post archiving functionality to NodeBB, allowing moderators to archive and unarchive posts without deletion. The implementation includes backend storage, API endpoints, and client-side integration.

  • Adds an archived integer field to post objects for persistent storage
  • Implements backend tools and API endpoints for archive/unarchive operations
  • Integrates archive/unarchive actions into the post tools menu and client handlers

Reviewed Changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/posts/data.js Adds archived to integer fields for post data parsing
src/posts/tools.js Implements archive/unarchive methods with permission checks
src/api/posts.js Adds API endpoints and shared helper for archive operations
src/socket.io/posts/tools.js Includes archive field in post data and adds menu tools
public/src/client/topic/postTools.js Adds client-side handlers for archive/unarchive actions
src/utils.js Refactors profile/timing utilities from process to utils module
test/utils.js Updates test to use new utils.profile method
src/user/search.js Updates to use utils.elapsedTimeSince
src/search.js Updates to use utils.elapsedTimeSince
src/categories/search.js Updates to use utils.elapsedTimeSince

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment on lines +197 to 207

postContainer.on('click', '[component="post/archive"]', function () {
const pid = getData($(this), 'data-pid');
postAction('archive', pid);
});

postContainer.on('click', '[component="post/unarchive"]', function () {
const pid = getData($(this), 'data-pid');
postAction('unarchive', pid);
});
}
Copy link

Copilot AI Oct 2, 2025

Choose a reason for hiding this comment

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

Event handlers are being attached inside an existing event handler callback, which can lead to duplicate event handlers being registered. These handlers should be moved outside the existing click handler for edit functionality.

Suggested change
postContainer.on('click', '[component="post/archive"]', function () {
const pid = getData($(this), 'data-pid');
postAction('archive', pid);
});
postContainer.on('click', '[component="post/unarchive"]', function () {
const pid = getData($(this), 'data-pid');
postAction('unarchive', pid);
});
}
}
});
postContainer.on('click', '[component="post/archive"]', function () {
const pid = getData($(this), 'data-pid');
postAction('archive', pid);
});
postContainer.on('click', '[component="post/unarchive"]', function () {
const pid = getData($(this), 'data-pid');
postAction('unarchive', pid);

Copilot uses AI. Check for mistakes.
return c1.order - c2.order;
});
searchResult.timing = (process.elapsedTimeSince(startTime) / 1000).toFixed(2);
searchResult.timing = (utils.elapsedTimeSince(startTime) / 1000).toFixed(2);
Copy link

Copilot AI Oct 2, 2025

Choose a reason for hiding this comment

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

Inconsistent indentation - this line should maintain the same indentation as the surrounding code (appears to be missing a tab).

Suggested change
searchResult.timing = (utils.elapsedTimeSince(startTime) / 1000).toFixed(2);
searchResult.timing = (utils.elapsedTimeSince(startTime) / 1000).toFixed(2);

Copilot uses AI. Check for mistakes.
Comment on lines +33 to +34


Copy link

Copilot AI Oct 2, 2025

Choose a reason for hiding this comment

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

Unnecessary blank lines should be removed to maintain clean code formatting.

Suggested change

Copilot uses AI. Check for mistakes.
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.

3 participants