Skip to content

Conversation

@ECWireless
Copy link
Member

@ECWireless ECWireless commented Aug 28, 2025

This pull request introduces a new multi-step "Collection Wizard" feature for creating and publishing article collections, improves author name handling for posts, and makes several UI and UX enhancements. The most significant changes are the addition of the collection creation flow, improvements to author display, and better error feedback for image uploads.

New Collection Wizard Feature:

  • Added a new CollectionWizard component with a three-step process for creating, selecting articles for, and previewing a collection. This includes logic for editor permissions, cooldowns, and publishing via a system call.
  • Implemented CollectionWizardStep1 for entering collection details and uploading a cover image to IPFS, with image resizing and error feedback using toast notifications.

Post Author Handling Improvements:

  • Updated usePosts to resolve and decode the post author's name from the player name table, defaulting to "Anonymous" if not found.
  • Modified the NoteCard component to use the new author field from Post instead of resolving the player name separately, simplifying the component and ensuring consistent author display. [1] [2]

UI/UX Enhancements and Error Handling:

  • Improved error feedback for image uploads in both the article and collection wizards by displaying toast notifications on failure. [1] [2]
  • Cleaned up conditional rendering for cover images and improved code readability in the article wizard.

Dependency Updates:

  • Added fuse.js as a new dependency in package.json (though not yet used in this diff).

Summary by CodeRabbit

  • New Features

    • Create collections via a 3‑step wizard: details, select up to 5 articles (search, drag‑and‑drop ordering), and preview.
    • Publish collections with success/error toasts and editor cadence rules (7‑day restriction).
  • Improvements

    • Notes and articles now display author names (defaults to “Anonymous”).
    • Article editor shows clear image upload errors and avoids upscaling images.
    • Editor Room: “New Collection” added, separate dialogs for Article/Collection, default tab = Published.
  • Validation

    • Prevents adding non-article posts to collections with user-facing errors.
  • Chores

    • Added fuzzy-search dependency (fuse.js).

@ECWireless ECWireless linked an issue Aug 28, 2025 that may be closed by this pull request
@vercel
Copy link

vercel bot commented Aug 28, 2025

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

Project Deployment Preview Comments Updated (UTC)
thedailydust Ready Ready Preview Comment Aug 28, 2025 0:55am

@coderabbitai
Copy link

coderabbitai bot commented Aug 28, 2025

Walkthrough

Adds author resolution to posts from the on-chain PlayerName table and surfaces it in UI. Introduces a new 3-step CollectionWizard (details, articles, preview) with client-side fuzzy search, drag-and-drop ordering, and a publish flow that enforces on-chain article checks and editor publishing interval. Minor image-upload UX and rendering tweaks; adds Fuse.js dependency.

Changes

Cohort / File(s) Summary
Post author resolution & consumers
packages/app/src/common/usePosts.tsx, packages/app/src/utils/types.ts, packages/app/src/components/NoteCard.tsx, packages/app/src/pages/ArticlePage.tsx
Read owner PlayerName (hex), decode via hexToString, trim nulls, add author: string to Post and switch UI to display note.author / article.author instead of usePlayerName.
Collection wizard feature & editor wiring
packages/app/src/components/editor/CollectionWizard.tsx, packages/app/src/components/editor/CollectionWizardStep1.tsx, packages/app/src/components/editor/CollectionWizardStep2.tsx, packages/app/src/components/editor/CollectionWizardStep3.tsx, packages/app/src/pages/EditorRoomPage.tsx
New CollectionWizard (3 steps) with local state, validations (1–5 articles), drag-and-drop ordering, Fuse.js search, IPFS image upload helper in step1, preview step, and publish via systemCall (creates collection). EditorRoomPage now manages separate modals for Article vs Collection wizards and draft loading.
Fuse dependency
packages/app/package.json
Adds dependency fuse.js ^7.1.0 for fuzzy searching article titles/authors.
Article editor & UI tweaks
packages/app/src/components/editor/ArticleWizardStep1.tsx, packages/app/src/components/editor/ArticleWizardStep2.tsx
Step1: add toast.error on image upload failure and adjust resize to use targetW = Math.min(1000, image.bitmap.width) to avoid upscaling. Step2: simplify conditional coverImage rendering using short-circuit.
Contracts: collection validation
packages/contracts/src/systems/CollectionSystem.sol
Import IsArticle; require 1–5 postIds, ensure each postId is an article via IsArticle.get(postId), enforce editor 7-day publish interval and mark editor publications / update LatestEditorPublication. Added per-post checks in updateCollectionPosts. Public signatures unchanged.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as User
  participant ER as EditorRoomPage
  participant CW as CollectionWizard
  participant DC as DustClient
  participant CS as CollectionSystem (on-chain)
  participant IA as IsArticle table

  U->>ER: Click "New Collection"
  ER->>CW: Open wizard
  U->>CW: Fill title/desc/cover, select up to 5 articles
  U->>CW: Click Publish
  activate CW
  CW->>DC: systemCall(createCollection, [title,desc,cover,postIds])
  activate DC
  DC->>CS: createCollection(args)
  activate CS
  loop per postId
    CS->>IA: get(postId)
    IA-->>CS: bool isArticle
    CS-->>CS: require(isArticle)
  end
  CS-->>DC: tx result (success/revert)
  deactivate CS
  DC-->>CW: result
  deactivate DC
  alt success
    CW->>ER: onDone()
    ER->>U: show success, close wizard
  else error
    CW->>U: toast.error(message)
  end
  deactivate CW
Loading
sequenceDiagram
  autonumber
  participant UP as usePosts
  participant PN as PlayerName table
  participant VT as viem.hexToString
  participant UI as Consumers (ArticlePage, NoteCard)

  UP->>PN: get(owner) for each post
  PN-->>UP: ownerName (hex|null)
  alt ownerName present
    UP->>VT: hexToString(ownerName)
    VT-->>UP: "AuthorName"
    UP-->>UI: Post { author: "AuthorName", ... }
  else missing
    UP-->>UI: Post { author: "Anonymous", ... }
  end
  UI-->>UI: render @{post.author}
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

A rabbit hops with code and glee,
I fetch names from chain for all to see.
Five picks, a fuzzy search, and drag-and-drop play—
Publish checks shout "halt" if posts stray.
Carrots for success, and toast on the way 🥕✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 31-add-collections-system-and-flows

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbit in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbit in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbit gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbit read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbit help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbit ignore or @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbit summary or @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbit or @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@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: 9

🧹 Nitpick comments (15)
packages/app/src/components/editor/ArticleWizardStep1.tsx (2)

2-2: UX: Toasts are good; guard against non-Error throws and empty messages.

Make the error description resilient and avoid leaking raw server text verbatim.

Apply:

-import { toast } from "sonner";
+import { toast } from "sonner";

(see catch block change below)


113-115: Harden error handling in upload catch block.

Ensure we always show a sane message even if err is not an Error or has empty message.

-                    toast.error("Failed to Upload", {
-                      description: (err as Error).message,
-                    });
+                    {
+                      const msg =
+                        err instanceof Error && err.message
+                          ? err.message
+                          : "Unexpected error during image upload. Please try again.";
+                      toast.error("Failed to upload image", { description: msg });
+                    }
packages/app/src/components/NoteCard.tsx (1)

46-47: Avoid UX confusion: clicking author copies the address.

Label shows @{author} but clicking copies owner address. Either copy the author or clarify intent with a tooltip/aria-label.

-          <button
-            onClick={() => {
+          <button
+            title="Copy author address"
+            aria-label="Copy author address"
+            onClick={() => {
               copyToClipboard(note.owner);
               toast.success(`Copied ${shortenAddress(note.owner)}`);
             }}
           >
             @{note.author}
           </button>

Alternative: copy the author instead of the address:

- copyToClipboard(note.owner);
- toast.success(`Copied ${shortenAddress(note.owner)}`);
+ copyToClipboard(note.author);
+ toast.success(`Copied @${note.author}`);
packages/contracts/src/systems/CollectionSystem.sol (3)

59-66: Consistency: use custom error instead of string revert.

Other checks use require; prefer a custom error to match the style above and save gas.

-      if (timeSinceLastPublication < 7 days) {
-        revert("Editor publication interval not met");
-      }
+      if (timeSinceLastPublication < 7 days) revert EditorCooldown();

48-50: Optional: enforce uniqueness of postIds.

Given the max of 5, a simple O(n^2) or a small in-memory mapping would prevent duplicates and accidental repeats.

Example inside _requireAllArticles:

function _requireAllArticles(bytes32[] memory postIds) internal view {
  mapping(bytes32 => bool) memory seen; // pseudo-code; Solidity doesn't support memory mappings.
}

Practical approach for n<=5:

for (uint256 i = 0; i < postIds.length; i++) {
  for (uint256 j = i + 1; j < postIds.length; j++) {
    if (postIds[i] == postIds[j]) revert DuplicatePost();
  }
  if (!IsArticle.get(postIds[i])) revert NonArticlePost();
}

Add:

error DuplicatePost();

68-69: Emit events for indexing.

Consider emitting CollectionCreated/Updated/PostsUpdated to simplify client indexing and analytics.

packages/app/src/components/editor/CollectionWizardStep1.tsx (2)

78-111: UX: add upload-in-progress state to prevent duplicate uploads and indicate progress.

Disable the input while uploading and re-enable on completion/failure. Also consider rejecting files > a reasonable size (e.g., 5–10MB).

-          <input
+          <input
             id="collection-cover-image-input"
             accept="image/png, image/jpeg, image/jpg"
             className="text-sm w-full"
-            onChange={(e) => {
+            onChange={async (e) => {
               const file = e.target.files?.[0];
               if (file) {
-                uploadImageToIpfs(file, file.name)
+                setIsUploading(true);
+                uploadImageToIpfs(file, file.name)
                   .then((res) => {
                     setCoverImage(`ipfs://${res.cid}`);
                   })
                   .catch((err) => {
                     // eslint-disable-next-line no-console
                     console.error("Failed to upload image:", err);
 
                     toast.error("Failed to Upload", {
                       description: (err as Error).message,
                     });
-                  });
+                  })
+                  .finally(() => setIsUploading(false));
               }
             }}
             type="file"
+            disabled={isUploading}
+            aria-busy={isUploading}
           />

Add state at component top:

const [isUploading, setIsUploading] = useState(false);

Want me to add a size/type guard too?


1-1: Avoid bundling Jimp on the client

  • Jimp v1 no longer has a default export—using import { Jimp } from "jimp" is correct; import Jimp from "jimp" will fail (jimp-dev.github.io).
  • Pulling in the full Jimp package (~4 MB) will bloat the client bundle. For in-browser resizing, either use a lightweight library (Pica, Squoosh), build a custom Jimp with @jimp/core+plugins, or move image processing to your server.
    Locations:
  • packages/app/src/components/editor/ArticleWizardStep1.tsx
  • packages/app/src/components/editor/CollectionWizardStep1.tsx
packages/app/src/pages/EditorRoomPage.tsx (2)

81-107: Prevent simultaneous modals.

If a user triggers both buttons quickly, both overlays could be open. Close the other when opening one.

-const onOpenNewArticle = () => {
+const onOpenNewArticle = () => {
+  setOpenCollectionWizard(false);
   setWizardDraftId(undefined);
   setWizardArticleId(undefined);
   setOpenArticleWizard(true);
 };
 
-const onOpenDraft = (id: string) => {
+const onOpenDraft = (id: string) => {
+  setOpenCollectionWizard(false);
   setWizardDraftId(id);
   setWizardArticleId(undefined);
   setOpenArticleWizard(true);
 };
 
-const onOpenArticle = (id: string) => {
+const onOpenArticle = (id: string) => {
+  setOpenCollectionWizard(false);
   setWizardArticleId(id);
   setWizardDraftId(undefined);
   setOpenArticleWizard(true);
 };

207-241: Add Escape-to-close and focus trap for accessibility.

Improve modal UX and a11y; close on Esc and trap focus within the dialog.

Minimal Esc-close example:

-      {openCollectionWizard && (
-        <div className="fixed flex inset-0 items-start justify-center p-4 sm:items-center z-50">
+      {openCollectionWizard && (
+        <div
+          className="fixed flex inset-0 items-start justify-center p-4 sm:items-center z-50"
+          onKeyDown={(e) => e.key === "Escape" && setOpenCollectionWizard(false)}
+          role="dialog"
+          aria-modal="true"
+        >

Apply similarly to the ArticleWizard block. I can add a simple focus trap hook if desired.

packages/app/src/components/editor/CollectionWizardStep3.tsx (2)

69-75: Provide safe fallback for cover image src

uriToHttp(coverImage)[0] can be an empty string on parse errors. Add a fallback to avoid a broken image request.

-                  src={uriToHttp(coverImage)[0]}
+                  src={uriToHttp(coverImage)[0] || "/assets/placeholder-notext.png"}

86-88: Confirm intended numbering for “Pick N” labels

Current label uses zero-based index (second article shows “Pick 1”). If you intended human-friendly numbering, use index + 1.

-                    {index === 0 ? "Main Story: " : `Pick ${index}: `}
+                    {index === 0 ? "Main Story: " : `Pick ${index + 1}: `}
packages/app/src/components/editor/CollectionWizardStep2.tsx (2)

12-17: Rename misleading prop canContinueFrom1 to canContinue

This prop controls Step 2’s continue button, but the name implies Step 1. Rename for clarity; update parent call accordingly.

-type CollectionWizardStep2Props = {
+type CollectionWizardStep2Props = {
   articleIds: string[];
-  canContinueFrom1: boolean;
+  canContinue: boolean;
   onContinue: () => void;
   setArticleIds: React.Dispatch<React.SetStateAction<string[]>>;
 };
-        <button
-          disabled={!canContinueFrom1}
+        <button
+          disabled={!canContinue}
           onClick={() => onContinue?.()}
           className="bg-white border border-neutral-900 disabled:opacity-50 hover:bg-neutral-100 px-3 py-1.5 rounded text-sm text-text-primary"
         >
           Continue
         </button>

Also applies to: 206-212


30-35: Use source of truth for count check

Rely on articleIds.length instead of selectedArticles.length inside addArticle to avoid subtle closure ordering/derivation issues.

-  const addArticle = (article: Post) => {
-    if (selectedArticles.length < 5) {
+  const addArticle = (article: Post) => {
+    if (articleIds.length < 5) {
       setArticleIds((prev) => [...prev, article.id]);
       setSearchQuery("");
     }
   };
packages/app/src/components/editor/CollectionWizard.tsx (1)

230-237: Align prop rename for Step 2 continue gating

Update the parent to use the renamed canContinue prop.

         {step === 2 && (
           <CollectionWizardStep2
             articleIds={articleIds}
-            canContinueFrom1={articleIds.length > 0 && articleIds.length <= 5}
+            canContinue={articleIds.length > 0 && articleIds.length <= 5}
             onContinue={() => setStep(3)}
             setArticleIds={setArticleIds}
           />
         )}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f4cb4b7 and 04a3490.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (13)
  • packages/app/package.json (1 hunks)
  • packages/app/src/common/usePosts.tsx (3 hunks)
  • packages/app/src/components/NoteCard.tsx (2 hunks)
  • packages/app/src/components/editor/ArticleWizardStep1.tsx (2 hunks)
  • packages/app/src/components/editor/ArticleWizardStep2.tsx (1 hunks)
  • packages/app/src/components/editor/CollectionWizard.tsx (1 hunks)
  • packages/app/src/components/editor/CollectionWizardStep1.tsx (1 hunks)
  • packages/app/src/components/editor/CollectionWizardStep2.tsx (1 hunks)
  • packages/app/src/components/editor/CollectionWizardStep3.tsx (1 hunks)
  • packages/app/src/pages/ArticlePage.tsx (2 hunks)
  • packages/app/src/pages/EditorRoomPage.tsx (5 hunks)
  • packages/app/src/utils/types.ts (1 hunks)
  • packages/contracts/src/systems/CollectionSystem.sol (3 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-11T21:57:41.186Z
Learnt from: CR
PR: raid-guild/dust-dailydust#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-11T21:57:41.186Z
Learning: Applies to src/**/*.sol : Import generated tables and systems from ./codegen paths (e.g., ./codegen/tables/Owner.sol, ./codegen/systems/*Lib.sol)

Applied to files:

  • packages/contracts/src/systems/CollectionSystem.sol
🧬 Code graph analysis (9)
packages/app/src/components/editor/CollectionWizardStep3.tsx (4)
packages/app/src/common/usePosts.tsx (1)
  • usePosts (12-104)
packages/app/src/components/editor/CollectionWizard.tsx (1)
  • SEVEN_DAYS_SECONDS (25-25)
packages/app/src/lib/utils.ts (1)
  • cn (4-6)
packages/app/src/utils/helpers.ts (1)
  • uriToHttp (58-90)
packages/app/src/components/editor/CollectionWizardStep2.tsx (2)
packages/app/src/common/usePosts.tsx (1)
  • usePosts (12-104)
packages/app/src/utils/types.ts (1)
  • Post (1-16)
packages/app/src/components/editor/CollectionWizardStep1.tsx (2)
packages/app/src/lib/utils.ts (1)
  • cn (4-6)
packages/app/src/utils/helpers.ts (1)
  • uriToHttp (58-90)
packages/app/src/pages/EditorRoomPage.tsx (3)
packages/app/src/utils/markdown.ts (1)
  • renderMarkdownToHtml (15-138)
packages/app/src/components/editor/CollectionWizard.tsx (1)
  • CollectionWizard (27-251)
packages/app/src/components/editor/ArticleWizard.tsx (1)
  • ArticleWizard (89-663)
packages/app/src/common/usePosts.tsx (1)
packages/app/src/mud/stash.ts (1)
  • tables (16-21)
packages/app/src/components/editor/CollectionWizard.tsx (6)
packages/app/src/common/useDustClient.ts (1)
  • useDustClient (4-18)
packages/app/src/common/usePlayerEntityId.ts (1)
  • usePlayerEntityId (5-19)
packages/app/src/mud/stash.ts (1)
  • tables (16-21)
packages/app/src/components/editor/CollectionWizardStep1.tsx (1)
  • CollectionWizardStep1 (21-155)
packages/app/src/components/editor/CollectionWizardStep2.tsx (1)
  • CollectionWizardStep2 (19-215)
packages/app/src/components/editor/CollectionWizardStep3.tsx (1)
  • CollectionWizardStep3 (19-97)
packages/app/src/components/editor/ArticleWizardStep2.tsx (1)
packages/app/src/utils/helpers.ts (1)
  • uriToHttp (58-90)
packages/app/src/components/NoteCard.tsx (1)
packages/app/src/utils/markdown.ts (1)
  • renderMarkdownToHtml (15-138)
packages/app/src/pages/ArticlePage.tsx (1)
packages/app/src/utils/markdown.ts (1)
  • renderMarkdownToHtml (15-138)
🔇 Additional comments (9)
packages/app/package.json (1)

30-30: Confirm fuse.js usage and bundle impact

  • CollectionWizardStep2.tsx imports Fuse but no new Fuse(…) instantiation was found; ensure the library is actually used for fuzzy searching or remove the unused import.
  • Replace the quick grep-based count with a proper bundle analysis (e.g. webpack-bundle-analyzer) to assess production-size impact.
packages/app/src/components/NoteCard.tsx (1)

59-61: Markdown render path looks safe.

renderMarkdownToHtml escapes HTML and supports minimal formatting; good balance of UX and XSS defense.

packages/contracts/src/systems/CollectionSystem.sol (1)

7-7: Correct import location for generated table.

Importing IsArticle from codegen aligns with the project’s convention.

packages/app/src/components/editor/ArticleWizardStep2.tsx (1)

48-56: LGTM: simpler conditional for cover image rendering.

The short-circuit read improves readability without changing behavior.

packages/app/src/common/usePosts.tsx (1)

62-82: LGTM: Post shape enrichment looks correct.

Author is injected, cover image normalized via gateway, sorting by createdAt maintained.

packages/app/src/pages/ArticlePage.tsx (2)

81-85: LGTM: use resolved author field.

Button now shows @{article.author}, aligning with usePosts.


109-110: LGTM: markdown rendering path.

Pass-through to renderMarkdownToHtml is correct; function already escapes HTML.

packages/app/src/pages/EditorRoomPage.tsx (1)

10-27: Draft storage key and event name are consistent. Both EditorRoomPage.tsx and ArticleWizard.tsx use "editor-article-drafts" for DRAFT_KEY and "editor-article-drafts-updated" for the custom event.

packages/app/src/components/editor/CollectionWizard.tsx (1)

122-128: Sanity check: editor cooldown logic looks correct

Good: non-editors are unrestricted; editors require now - latest >= 7d or no prior publication. Matches the product requirement.

Comment on lines 40 to 46
const isEditor =
useRecord({
stash,
table: tables.IsEditor,
key: { id: encodePlayer(playerEntityId ?? "0x") },
})?.value ?? false;

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Avoid encoding an invalid address; use zero address fallback

Passing "0x" to encodePlayer risks runtime errors. Use the zero address when playerEntityId is unavailable.

-  const isEditor =
+  const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" as const;
+  const isEditor =
     useRecord({
       stash,
       table: tables.IsEditor,
-      key: { id: encodePlayer(playerEntityId ?? "0x") },
+      key: { id: encodePlayer((playerEntityId ?? ZERO_ADDRESS) as `0x${string}`) },
     })?.value ?? 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
const isEditor =
useRecord({
stash,
table: tables.IsEditor,
key: { id: encodePlayer(playerEntityId ?? "0x") },
})?.value ?? false;
const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" as const;
const isEditor =
useRecord({
stash,
table: tables.IsEditor,
key: { id: encodePlayer((playerEntityId ?? ZERO_ADDRESS) as `0x${string}`) },
})?.value ?? false;
🤖 Prompt for AI Agents
In packages/app/src/components/editor/CollectionWizard.tsx around lines 40 to
46, the code calls encodePlayer(playerEntityId ?? "0x") which can pass an
invalid address ("0x") into encodePlayer; replace the fallback with a valid zero
address (e.g. "0x0000000000000000000000000000000000000000") or a shared
ZERO_ADDRESS constant (import or define it) and use encodePlayer(playerEntityId
?? ZERO_ADDRESS) so encodePlayer always receives a well-formed address; add the
constant import/definition if missing.

Comment on lines 85 to 115
const onPublishCollection = useCallback(
async (e: React.FormEvent) => {
e.preventDefault();
if (!title || !description) return;
if (articleIds.length === 0) return;

try {
await createCollection.mutateAsync({
title,
description,
coverImage,
articleIds,
});

setTitle("");
setDescription("");
setCoverImage("");
setArticleIds([]);
onDone();
toast.success("Collection Published!");
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error publishing collection:", error);

toast.error("Error Publishing Collection", {
description: (error as Error).message,
});
}
},
[articleIds, coverImage, createCollection, description, onDone, title]
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Correct event typing and add defensive guards for publish (cooldown and max items)

The handler is attached to a button (not a form), so FormEvent/preventDefault isn’t needed. Also guard against bypassing UI disable (cooldown, max 5) before invoking the mutation.

-  const onPublishCollection = useCallback(
-    async (e: React.FormEvent) => {
-      e.preventDefault();
-      if (!title || !description) return;
-      if (articleIds.length === 0) return;
+  const onPublishCollection = useCallback(
+    async () => {
+      if (!title.trim() || !description.trim()) return;
+      if (articleIds.length === 0 || articleIds.length > 5) {
+        toast.error("Select between 1 and 5 articles.");
+        return;
+      }
+      if (!canPublish) {
+        toast.error("Cooldown active", {
+          description: "Editors can publish once every 7 days.",
+        });
+        return;
+      }
 
       try {
         await createCollection.mutateAsync({
           title,
           description,
           coverImage,
           articleIds,
         });
@@
-    [articleIds, coverImage, createCollection, description, onDone, title]
+    [articleIds, canPublish, coverImage, createCollection, description, onDone, title]
   );
📝 Committable suggestion

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

Suggested change
const onPublishCollection = useCallback(
async (e: React.FormEvent) => {
e.preventDefault();
if (!title || !description) return;
if (articleIds.length === 0) return;
try {
await createCollection.mutateAsync({
title,
description,
coverImage,
articleIds,
});
setTitle("");
setDescription("");
setCoverImage("");
setArticleIds([]);
onDone();
toast.success("Collection Published!");
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error publishing collection:", error);
toast.error("Error Publishing Collection", {
description: (error as Error).message,
});
}
},
[articleIds, coverImage, createCollection, description, onDone, title]
);
const onPublishCollection = useCallback(
async () => {
if (!title.trim() || !description.trim()) return;
if (articleIds.length === 0 || articleIds.length > 5) {
toast.error("Select between 1 and 5 articles.");
return;
}
if (!canPublish) {
toast.error("Cooldown active", {
description: "Editors can publish once every 7 days.",
});
return;
}
try {
await createCollection.mutateAsync({
title,
description,
coverImage,
articleIds,
});
setTitle("");
setDescription("");
setCoverImage("");
setArticleIds([]);
onDone();
toast.success("Collection Published!");
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error publishing collection:", error);
toast.error("Error Publishing Collection", {
description: (error as Error).message,
});
}
},
[articleIds, canPublish, coverImage, createCollection, description, onDone, title]
);

Comment on lines 34 to 43
// Using Jimp to resize the image and convert to a PNG
const buffer = await file.arrayBuffer();
const image = await Jimp.read(buffer);
const pngBuffer = (await image
.resize({ w: 1000 })
.getBuffer("image/png")) as BlobPart;

const form = new FormData();
form.append("file", new File([pngBuffer], name, { type: "image/png" }));
if (name) form.append("name", name);
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Harden image processing: prevent upscaling and use the async buffer API.

Prevent enlarging smaller images and use getBufferAsync (widely supported) to avoid API mismatches.

-    const buffer = await file.arrayBuffer();
-    const image = await Jimp.read(buffer);
-    const pngBuffer = (await image
-      .resize({ w: 1000 })
-      .getBuffer("image/png")) as BlobPart;
+    const buffer = await file.arrayBuffer();
+    const image = await Jimp.read(buffer);
+    const targetW = Math.min(1000, (image as any)?.bitmap?.width ?? 1000);
+    const pngBuffer = await image
+      .resize({ w: targetW }) // don't upscale
+      .getBufferAsync("image/png");

🏁 Script executed:

#!/bin/bash
set -e

echo "1. Confirm Jimp import in CollectionWizardStep1.tsx"
rg -n "import\s+Jimp" packages/app/src/components/editor/CollectionWizardStep1.tsx || true

echo -e "\n2. Locate any getBuffer calls in that file"
rg -n "getBuffer" packages/app/src/components/editor/CollectionWizardStep1.tsx || true

echo -e "\n3. Check Jimp version in root package.json"
rg -n '"jimp"' package.json || true

Length of output: 578


🏁 Script executed:

#!/bin/bash
set -e

echo "Showing the first 50 lines of CollectionWizardStep1.tsx"
sed -n '1,50p' packages/app/src/components/editor/CollectionWizardStep1.tsx

Length of output: 1651


Harden image processing: prevent upscaling and switch to promise-based buffer API

  • Compute targetW = Math.min(1000, image.bitmap.width) to avoid enlarging smaller images.
  • Replace .getBuffer("image/png") with await image.getBufferAsync("image/png"); requires Jimp ≥ 0.16.0—upgrade if needed.
-    const pngBuffer = (await image
-      .resize({ w: 1000 })
-      .getBuffer("image/png")) as BlobPart;
+    const targetW = Math.min(1000, image.bitmap.width);
+    const pngBuffer = await image
+      .resize({ w: targetW }) // don't upscale
+      .getBufferAsync("image/png");

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/app/src/components/editor/CollectionWizardStep1.tsx around lines 34
to 43, the image processing currently always resizes to width 1000 and uses the
synchronous getBuffer API; change it to compute targetW = Math.min(1000,
image.bitmap.width) so smaller images are not upscaled, call await
image.getBufferAsync("image/png") instead of getBuffer to use the promise-based
API (ensure Jimp >= 0.16.0 in package.json and run yarn/npm install if needed),
then use the returned buffer to create the File and append it to the FormData as
before.

Comment on lines +45 to +60
const resp = await fetch(`${API_BASE}/ipfs/file`, {
method: "POST",
body: form,
});

if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err?.error || `HTTP ${resp.status}`);
}

return resp.json() as Promise<{
cid: string;
size: number;
created_at: string;
}>;
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Validate upload response shape and surface non-JSON errors gracefully.

Ensure cid exists; otherwise show a clearer error.

-    return resp.json() as Promise<{
-      cid: string;
-      size: number;
-      created_at: string;
-    }>;
+    const data = (await resp.json().catch(() => null)) as
+      | { cid?: string; size?: number; created_at?: string }
+      | null;
+    if (!data?.cid) {
+      throw new Error("Upload completed but no CID returned from server");
+    }
+    return data as { cid: string; size: number; created_at: string };
📝 Committable suggestion

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

Suggested change
const resp = await fetch(`${API_BASE}/ipfs/file`, {
method: "POST",
body: form,
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err?.error || `HTTP ${resp.status}`);
}
return resp.json() as Promise<{
cid: string;
size: number;
created_at: string;
}>;
};
const resp = await fetch(`${API_BASE}/ipfs/file`, {
method: "POST",
body: form,
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err?.error || `HTTP ${resp.status}`);
}
const data = (await resp.json().catch(() => null)) as
| { cid?: string; size?: number; created_at?: string }
| null;
if (!data?.cid) {
throw new Error("Upload completed but no CID returned from server");
}
return data as { cid: string; size: number; created_at: string };
};
🤖 Prompt for AI Agents
In packages/app/src/components/editor/CollectionWizardStep1.tsx around lines 45
to 60, the upload response is assumed to be JSON and to contain a cid; update
the handling to (1) attempt to parse JSON but fall back to reading resp.text()
when parsing fails so non-JSON errors are surfaced, (2) validate that the parsed
result has a non-empty cid and throw a descriptive Error including HTTP status
and returned body when cid is missing, and (3) only return the parsed object
after the cid check (preserving the expected shape { cid, size, created_at }) so
callers never receive an invalid value.

Comment on lines +49 to +60
const handleDrop = (e: React.DragEvent, dropIndex: number) => {
e.preventDefault();
if (draggedIndex === null) return;

const newArticles = [...selectedArticles];
const draggedArticle = newArticles[draggedIndex];
newArticles.splice(draggedIndex, 1);
newArticles.splice(dropIndex, 0, draggedArticle);

setArticleIds(newArticles.map((a) => a.id));
setDraggedIndex(null);
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix reordering bug when dragging downward (index shift on splice)

Dropping an item below its original position inserts it one slot too far due to index shift after removal. Adjust the target index when draggedIndex < dropIndex.

-  const handleDrop = (e: React.DragEvent, dropIndex: number) => {
+  const handleDrop = (e: React.DragEvent, dropIndex: number) => {
     e.preventDefault();
     if (draggedIndex === null) return;
 
     const newArticles = [...selectedArticles];
-    const draggedArticle = newArticles[draggedIndex];
-    newArticles.splice(draggedIndex, 1);
-    newArticles.splice(dropIndex, 0, draggedArticle);
+    const [draggedArticle] = newArticles.splice(draggedIndex, 1);
+    const targetIndex = draggedIndex < dropIndex ? dropIndex - 1 : dropIndex;
+    newArticles.splice(targetIndex, 0, draggedArticle);
 
     setArticleIds(newArticles.map((a) => a.id));
     setDraggedIndex(null);
   };
📝 Committable suggestion

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

Suggested change
const handleDrop = (e: React.DragEvent, dropIndex: number) => {
e.preventDefault();
if (draggedIndex === null) return;
const newArticles = [...selectedArticles];
const draggedArticle = newArticles[draggedIndex];
newArticles.splice(draggedIndex, 1);
newArticles.splice(dropIndex, 0, draggedArticle);
setArticleIds(newArticles.map((a) => a.id));
setDraggedIndex(null);
};
const handleDrop = (e: React.DragEvent, dropIndex: number) => {
e.preventDefault();
if (draggedIndex === null) return;
const newArticles = [...selectedArticles];
const [draggedArticle] = newArticles.splice(draggedIndex, 1);
const targetIndex = draggedIndex < dropIndex ? dropIndex - 1 : dropIndex;
newArticles.splice(targetIndex, 0, draggedArticle);
setArticleIds(newArticles.map((a) => a.id));
setDraggedIndex(null);
};
🤖 Prompt for AI Agents
In packages/app/src/components/editor/CollectionWizardStep2.tsx around lines 49
to 60, the drop handler shifts target positions when dragging an item downward
because removing the dragged item reduces indices; adjust the drop index before
insertion: if draggedIndex < dropIndex then decrement dropIndex by 1 (or compute
an adjustedIndex = draggedIndex < dropIndex ? dropIndex - 1 : dropIndex) and use
that adjusted index in the splice that inserts the draggedArticle, then update
state and clear draggedIndex as before.

Comment on lines 48 to 53
require(postIds.length > 0, "Collection must have at least one post");
require(postIds.length <= 5, "Collection cannot have more than 5 posts");

for (uint256 i = 0; i < postIds.length; i++) {
require(IsArticle.get(postIds[i]), "All postIds must be an article");
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Validate before writing; consider DRY and custom errors.

  • Do all validation, including IsArticle checks, before any storage writes to avoid wasted gas on reverted stores.
  • Extract article validation into an internal helper to avoid duplication.
  • Prefer custom errors for gas and consistency.
   function createCollection(
@@
-    Collection.set(
-      collectionId,
-      CollectionData({
-        createdAt: currentTime,
-        owner: _msgSender(),
-        updatedAt: currentTime,
-        coverImage: coverImage,
-        description: description,
-        title: title
-      })
-    );
-
     require(postIds.length > 0, "Collection must have at least one post");
     require(postIds.length <= 5, "Collection cannot have more than 5 posts");
 
-    for (uint256 i = 0; i < postIds.length; i++) {
-      require(IsArticle.get(postIds[i]), "All postIds must be an article");
-    }
+    _requireAllArticles(postIds);
+
+    Collection.set(
+      collectionId,
+      CollectionData({
+        createdAt: currentTime,
+        owner: _msgSender(),
+        updatedAt: currentTime,
+        coverImage: coverImage,
+        description: description,
+        title: title
+      })
+    );

Add helper and custom errors (outside function bodies):

error EditorCooldown();
error EmptyCollection();
error TooManyPosts();
error NonArticlePost();

function _requireAllArticles(bytes32[] memory postIds) internal view {
  for (uint256 i = 0; i < postIds.length; i++) {
    if (!IsArticle.get(postIds[i])) revert NonArticlePost();
  }
}

And update requires:

- require(postIds.length > 0, "Collection must have at least one post");
- require(postIds.length <= 5, "Collection cannot have more than 5 posts");
+ if (postIds.length == 0) revert EmptyCollection();
+ if (postIds.length > 5) revert TooManyPosts();

Comment on lines +124 to +126
for (uint256 i = 0; i < postIds.length; i++) {
require(IsArticle.get(postIds[i]), "All postIds must be an article");
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Duplicate validation logic — reuse helper.

Use the same _requireAllArticles and length checks here to keep logic consistent.

-    require(postIds.length > 0, "Collection must have at least one post");
-    require(postIds.length <= 5, "Collection cannot have more than 5 posts");
-
-    for (uint256 i = 0; i < postIds.length; i++) {
-      require(IsArticle.get(postIds[i]), "All postIds must be an article");
-    }
+    if (postIds.length == 0) revert EmptyCollection();
+    if (postIds.length > 5) revert TooManyPosts();
+    _requireAllArticles(postIds);
📝 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
for (uint256 i = 0; i < postIds.length; i++) {
require(IsArticle.get(postIds[i]), "All postIds must be an article");
}
if (postIds.length == 0) revert EmptyCollection();
if (postIds.length > 5) revert TooManyPosts();
_requireAllArticles(postIds);
🤖 Prompt for AI Agents
In packages/contracts/src/systems/CollectionSystem.sol around lines 124 to 126,
the loop manually validates each postId with require(IsArticle.get(postIds[i]),
...) duplicating existing validation; replace this loop with a call to the
existing helper _requireAllArticles(postIds) and ensure you preserve any length
checks (e.g., require(postIds.length > 0) or max length constraints) that other
callers use so validation behavior remains consistent across the contract.

Copy link

@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

Caution

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

⚠️ Outside diff range comments (1)
packages/contracts/src/systems/CollectionSystem.sol (1)

100-107: Clean up IsEditorPublication on delete to prevent stale flags.

Deleting the collection should also remove its IsEditorPublication flag so indexers/UI don’t read phantom editor marks.

Apply this diff:

   Collection.deleteRecord(collectionId);
   CollectionPosts.deleteRecord(collectionId);
+  IsEditorPublication.deleteRecord(collectionId);
♻️ Duplicate comments (2)
packages/contracts/src/systems/CollectionSystem.sol (2)

116-126: Reuse the shared validation helper(s) here as well.

Mirror the same length checks and article validation helper to keep logic consistent across create/update.

Apply this diff in-place:

-    require(postIds.length > 0, "Collection must have at least one post");
-    require(postIds.length <= 5, "Collection cannot have more than 5 posts");
-
-    for (uint256 i = 0; i < postIds.length; i++) {
-      require(IsArticle.get(postIds[i]), "All postIds must be an article");
-    }
+    if (postIds.length == 0) revert EmptyCollection();
+    if (postIds.length > MAX_POSTS) revert TooManyPosts();
+    _requireAllArticles(postIds);
+    // Optional: also enforce uniqueness
+    // _requireUnique(postIds);

34-39: DRY validations, switch to custom errors, and add helper(s) (plus optional uniqueness check).

Replace repeated length checks and per-item loop with a reusable helper and custom errors (cheaper gas, clearer intent). Also introduce a MAX_POSTS constant. Optionally, enforce unique postIds to prevent duplicates.

Apply this diff in-place:

-    require(postIds.length > 0, "Collection must have at least one post");
-    require(postIds.length <= 5, "Collection cannot have more than 5 posts");
-
-    for (uint256 i = 0; i < postIds.length; i++) {
-      require(IsArticle.get(postIds[i]), "All postIds must be an article");
-    }
+    if (postIds.length == 0) revert EmptyCollection();
+    if (postIds.length > MAX_POSTS) revert TooManyPosts();
+    _requireAllArticles(postIds);
+    // Optional: enforce uniqueness to avoid duplicate posts in a collection
+    // _requireUnique(postIds);

Add these once at contract scope (outside any function):

uint256 internal constant MAX_POSTS = 5;

error EmptyCollection();
error TooManyPosts();
error NonArticlePost(bytes32 postId);
error DuplicatePostId(bytes32 postId);

function _requireAllArticles(bytes32[] memory postIds) internal view {
  for (uint256 i = 0; i < postIds.length; i++) {
    if (!IsArticle.get(postIds[i])) revert NonArticlePost(postIds[i]);
  }
}

function _requireUnique(bytes32[] memory postIds) internal pure {
  for (uint256 i = 0; i < postIds.length; i++) {
    for (uint256 j = i + 1; j < postIds.length; j++) {
      if (postIds[i] == postIds[j]) revert DuplicatePostId(postIds[i]);
    }
  }
}
🧹 Nitpick comments (1)
packages/contracts/src/systems/CollectionSystem.sol (1)

21-26: Minor gas/style: prefer external + calldata (strings), keep arrays as memory if codegen setters require memory.

Switching to external and using calldata for strings avoids an extra mem copy on entry; arrays can stay memory if codegen setters take memory.

Apply these diffs:

-  function createCollection(
-    string memory title,
-    string memory description,
-    string memory coverImage,
-    bytes32[] memory postIds
-  ) public returns (bytes32) {
+  function createCollection(
+    string calldata title,
+    string calldata description,
+    string calldata coverImage,
+    bytes32[] memory postIds
+  ) external returns (bytes32) {
-  function updateCollection(
+  function updateCollection(
     bytes32 collectionId,
-    string memory title,
-    string memory description,
-    string memory coverImage
-  ) public returns (bytes32) {
+    string calldata title,
+    string calldata description,
+    string calldata coverImage
+  ) external returns (bytes32) {
-  function updateCollectionPosts(bytes32 collectionId, bytes32[] memory postIds) public returns (bytes32) {
+  function updateCollectionPosts(bytes32 collectionId, bytes32[] memory postIds) external returns (bytes32) {

Please confirm the generated table setters’ parameter types to ensure compatibility.

Also applies to: 76-81, 116-116

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 04a3490 and 9938cce.

📒 Files selected for processing (6)
  • packages/app/src/common/usePosts.tsx (3 hunks)
  • packages/app/src/components/editor/ArticleWizardStep1.tsx (3 hunks)
  • packages/app/src/components/editor/CollectionWizard.tsx (1 hunks)
  • packages/app/src/components/editor/CollectionWizardStep1.tsx (1 hunks)
  • packages/app/src/components/editor/CollectionWizardStep3.tsx (1 hunks)
  • packages/contracts/src/systems/CollectionSystem.sol (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/app/src/components/editor/ArticleWizardStep1.tsx
  • packages/app/src/common/usePosts.tsx
  • packages/app/src/components/editor/CollectionWizardStep3.tsx
  • packages/app/src/components/editor/CollectionWizard.tsx
  • packages/app/src/components/editor/CollectionWizardStep1.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-11T21:57:41.186Z
Learnt from: CR
PR: raid-guild/dust-dailydust#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-11T21:57:41.186Z
Learning: Applies to src/**/*.sol : Import generated tables and systems from ./codegen paths (e.g., ./codegen/tables/Owner.sol, ./codegen/systems/*Lib.sol)

Applied to files:

  • packages/contracts/src/systems/CollectionSystem.sol
🔇 Additional comments (1)
packages/contracts/src/systems/CollectionSystem.sol (1)

7-7: Good: on-chain article validation imported from codegen.

Import path aligns with our codegen convention and retrieved learnings. No issues.

Comment on lines 41 to +51
uint64 currentTime = uint64(block.timestamp);
bytes32 playerId = encodePlayerEntityId(_msgSender());
if (IsEditor.get(playerId)) {
uint64 timeSinceLastPublication = currentTime - LatestEditorPublication.get();
if (timeSinceLastPublication < 7 days) {
revert("Editor publication interval not met");
}

IsEditorPublication.set(collectionId, true);
LatestEditorPublication.set(currentTime);
}
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Harden editor cooldown logic; avoid underflow, use custom errors, and extract a constant. Also confirm global vs per-editor cooldown requirement.

  • Underflow on subtraction reverts if last > current; add a guard for skew.
  • Replace string revert with typed errors and expose remaining seconds.
  • Extract EDITOR_COOLDOWN = 7 days for consistency with the client.
  • Confirm whether cooldown is global (singleton LatestEditorPublication) or per-editor (keyed by playerId). Current code enforces a global cooldown.

Apply this diff in-place:

-    if (IsEditor.get(playerId)) {
-      uint64 timeSinceLastPublication = currentTime - LatestEditorPublication.get();
-      if (timeSinceLastPublication < 7 days) {
-        revert("Editor publication interval not met");
-      }
-
-      IsEditorPublication.set(collectionId, true);
-      LatestEditorPublication.set(currentTime);
-    }
+    if (IsEditor.get(playerId)) {
+      uint64 last = LatestEditorPublication.get();
+      if (currentTime <= last) revert ClockSkew();
+      uint64 elapsed = currentTime - last;
+      if (elapsed < EDITOR_COOLDOWN) revert EditorCooldown(uint64(EDITOR_COOLDOWN) - elapsed);
+
+      IsEditorPublication.set(collectionId, true);
+      LatestEditorPublication.set(currentTime);
+    }

Add these once at contract scope:

uint256 internal constant EDITOR_COOLDOWN = 7 days;
error EditorCooldown(uint64 remainingSeconds);
error ClockSkew();

If cooldown must be per-editor, switch LatestEditorPublication to be keyed by playerId (schema + calls), e.g., LatestEditorPublication.get(playerId) / set(playerId, currentTime).


🏁 Script executed:

#!/usr/bin/env bash
set -e
# Locate CollectionSystem.sol
fd packages/contracts/src/systems/CollectionSystem.sol -t f
# Print first 100 lines of CollectionSystem.sol
sed -n '1,100p' packages/contracts/src/systems/CollectionSystem.sol
# Search occurrences of LatestEditorPublication in solidity sources
rg -n "LatestEditorPublication" -g "packages/contracts/src/**/*.sol"
# Search for schema or table definitions of LatestEditorPublication
grep -R "LatestEditorPublication" -n packages/contracts/src

Length of output: 5191


Harden editor cooldown logic

  • Add a skew check before subtraction to prevent underflow:
    uint64 last = LatestEditorPublication.get();
    if (currentTime <= last) revert ClockSkew();
  • Replace string revert with custom errors and include remaining seconds:
    if (elapsed < EDITOR_COOLDOWN) revert EditorCooldown(uint64(EDITOR_COOLDOWN) - elapsed);
  • Extract EDITOR_COOLDOWN = 7 days into an internal constant.
  • Confirm that LatestEditorPublication is intended as a global cooldown; if per-editor cooldown is required, key it by playerId (e.g. get(playerId) / set(playerId, currentTime)).

Apply this diff in-place:

-    if (IsEditor.get(playerId)) {
-      uint64 timeSinceLastPublication = currentTime - LatestEditorPublication.get();
-      if (timeSinceLastPublication < 7 days) {
-        revert("Editor publication interval not met");
-      }
-
-      IsEditorPublication.set(collectionId, true);
-      LatestEditorPublication.set(currentTime);
-    }
+    if (IsEditor.get(playerId)) {
+      uint64 last = LatestEditorPublication.get();
+      if (currentTime <= last) revert ClockSkew();
+      uint64 elapsed = currentTime - last;
+      if (elapsed < EDITOR_COOLDOWN) revert EditorCooldown(uint64(EDITOR_COOLDOWN) - elapsed);
+
+      IsEditorPublication.set(collectionId, true);
+      LatestEditorPublication.set(currentTime);
+    }

And add at contract scope:

uint256 internal constant EDITOR_COOLDOWN = 7 days;
error EditorCooldown(uint64 remainingSeconds);
error ClockSkew();
🤖 Prompt for AI Agents
In packages/contracts/src/systems/CollectionSystem.sol around lines 41 to 51,
harden the editor cooldown: introduce an internal constant EDITOR_COOLDOWN = 7
days and custom errors EditorCooldown(uint64 remainingSeconds) and ClockSkew(),
then before subtracting do a skew check (if currentTime <= last revert
ClockSkew()), compute elapsed = currentTime - last, if elapsed < EDITOR_COOLDOWN
revert EditorCooldown(uint64(EDITOR_COOLDOWN) - uint64(elapsed)); replace the
string revert and the literal 7 days with the constant; finally confirm intent
of LatestEditorPublication scope and if cooldown should be per-editor change
LatestEditorPublication.get()/set() calls to use playerId as the key
(get(playerId)/set(playerId, currentTime)).

@ECWireless ECWireless merged commit f5065dd into main Aug 28, 2025
5 checks passed
@ECWireless ECWireless deleted the 31-add-collections-system-and-flows branch August 28, 2025 13:04
@coderabbitai coderabbitai bot mentioned this pull request Aug 29, 2025
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.

Add Collections system and flows

2 participants