Add post and reply editing - #90
Conversation
- Add updatedAt and isEdited fields to Post type - Implement updatePost() method in PostService - Add editingPost state to Zustand store with mutual exclusivity - Enhance ComposeModal to support edit mode with content pre-population - Add Edit button to PostCard dropdown menu (for own posts only) - Show "(edited)" indicator with tooltip showing edit timestamp
|
Warning Review limit reachedNext included review available in 30 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (10)
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. Comment |
Dash Platform documents start with revision 1, so posts are only considered edited when revision > 1.
Deploying yappr with
|
| Latest commit: |
e1b4052
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://5f747bd2.yappr.pages.dev |
| Branch Preview URL: | https://claude-add-post-edit-feature.yappr.pages.dev |
Includes fix for document update revision handling from: dashpay/platform#2960
v3.0.0-dev.11 has breaking API changes that require significant migration work. Reverting to stable version for now.
Resolve conflicts by taking master's current architecture as baseline; the edit feature is re-applied on top in follow-up commits. Takes master's package.json/package-lock.json wholesale (evo-sdk 3.1.0-dev.8), master's split types/ modules, and master's restructured store, compose modal, post card, and post service.
Adds updatePost() to PostService and updateReply() to ReplyService. Both fetch the raw document to preserve all stored fields (language, mediaUrl, quoted/parent references) through the Dash Platform document replacement, use the current $revision, and refuse to edit encrypted documents. Post and Reply types gain updatedAt/isEdited, derived from $revision > 1 in the document transforms. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a dedicated EditPostModal (mirroring the delete-confirmation-modal pattern) mounted in Providers, an Edit menu item on own non-encrypted posts/replies in PostCard, and an (edited) indicator with tooltip next to the timestamp. Cards refresh their displayed content via a post-updated window event so no refetch is needed. The reply-to-post adapter carries updatedAt/isEdited through. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Carry updatedAt/isEdited through the remaining Reply-to-Post adapters (profile replies tab, post reference resolution, fetchPostsOrReplies fallback) so the edited indicator shows on fetched replies. Abort updatePost instead of silently dropping an unreadable stored quote reference, fail loudly when the current document revision is unavailable in updatePost/updateReply, and reuse the ActionTooltip helper for the edited indicator. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deploying yappr-v2 with
|
| Latest commit: |
e1b4052
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://74f8ce80.yappr-v2.pages.dev |
| Branch Preview URL: | https://claude-add-post-edit-feature.yappr-v2.pages.dev |
|
⛔ Blockers found — Opus deferred (commit e1b4052) |
Restyle the edit dialog to match the compose modal: same overlay (dim + potato-aware blur, top-anchored panel), same rounded-2xl panel with bordered header, X icon button + avatar on the left and the prominent pill Save action on the right, a transparent auto-growing textarea instead of a boxed input, and the composer's circular CharacterCounter in a bordered footer alongside the marked-as-edited note. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
thepastaclaw
left a comment
There was a problem hiding this comment.
Preliminary review — Codex only
The document-replacement logic preserves the fields declared by the active post and reply contracts, but the edit result is retained only by currently mounted cards. This makes successfully edited content appear to revert after common remounts and leaves internal post-reference embeds stale, so the PR needs changes before approval.
Source: reviewer backend codex-general (the supplied evidence does not expose its exact model ID); final verifier backend Claude Agent SDK (the runtime does not expose its exact model ID); openclaw-agent/cliproxy/gpt-5.6-sol is orchestration-only and not reviewer evidence.
Validated blockers were found in the Codex precheck. Opus is deferred until a fresh Codex revalidation clears the blocker gate.
Review provenance
- Codex reviewers:
gpt-5.6-sol— general (completed) - Verifier:
gpt-5.6-sol— verifier - Sonnet: not run (deferred by blocker gate)
🔴 1 blocking | 🟡 1 suggestion(s) | 💬 1 nitpick(s)
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `components/post/post-card.tsx`:
- [BLOCKING] components/post/post-card.tsx:176-180: Persist edited content beyond the mounted card
A successful edit updates only the local state of `PostCard` instances that are mounted when the event fires. Parent collections such as the profile page's `posts` and `userReplies` remain unchanged, so switching between Posts and Replies unmounts the card and recreates it from the old `post.content`, making the successful edit appear to revert. The separate module-level `referenceCache` in `use-yappr-post-reference.ts` is also neither updated nor invalidated, so internal embeds can continue showing the old content for the rest of the SPA session. Store edit overrides in shared ID-keyed state or update the owning collections and invalidate all content caches when the edit succeeds.
In `components/post/edit-post-modal.tsx`:
- [NITPICK] components/post/edit-post-modal.tsx:61-64: Validate the trimmed content that will be submitted
The services receive and validate `trimmed`, but `canSave` applies the 500-character limit to the untrimmed textarea value. An edit containing 500 meaningful characters plus a trailing space or newline is therefore disabled even though the submitted payload would be valid. Apply the limit to `trimmed.length` so modal validation matches the payload.
In `lib/services/post-service.ts`:
- [SUGGESTION] lib/services/post-service.ts:294-338: Add focused coverage for full-document replacements
The new update methods rely on invariants that are easy to regress: preserving optional fields, converting identifier references back to typed bytes, using the fetched revision, rejecting encrypted documents, and aborting rather than silently dropping unreadable references. Add focused service tests for both `updatePost` and `updateReply` using representative SDK document shapes. The repository has no existing test target, so this may require establishing a small test harness, but these write paths warrant regression coverage.
| // Local override applied when this post is edited while mounted | ||
| // (set from the global 'post-updated' event dispatched by the edit modal) | ||
| const [editedContent, setEditedContent] = useState<string | null>(null) | ||
| const displayContent = editedContent ?? post.content | ||
| const isEdited = Boolean(post.isEdited) || editedContent !== null |
There was a problem hiding this comment.
🔴 Blocking: Persist edited content beyond the mounted card
A successful edit updates only the local state of PostCard instances that are mounted when the event fires. Parent collections such as the profile page's posts and userReplies remain unchanged, so switching between Posts and Replies unmounts the card and recreates it from the old post.content, making the successful edit appear to revert. The separate module-level referenceCache in use-yappr-post-reference.ts is also neither updated nor invalidated, so internal embeds can continue showing the old content for the rest of the SPA session. Store edit overrides in shared ID-keyed state or update the owning collections and invalidate all content caches when the edit succeeds.
source: ['codex']
| const trimmed = content.trim() | ||
| const isUnchanged = !!post && trimmed === post.content.trim() | ||
| const canSave = !!post && trimmed.length > 0 && content.length <= CHARACTER_LIMIT && !isUnchanged && !isSaving | ||
| const isReply = Boolean(post?.parentId) |
There was a problem hiding this comment.
💬 Nitpick: Validate the trimmed content that will be submitted
The services receive and validate trimmed, but canSave applies the 500-character limit to the untrimmed textarea value. An edit containing 500 meaningful characters plus a trailing space or newline is therefore disabled even though the submitted payload would be valid. Apply the limit to trimmed.length so modal validation matches the payload.
| const trimmed = content.trim() | |
| const isUnchanged = !!post && trimmed === post.content.trim() | |
| const canSave = !!post && trimmed.length > 0 && content.length <= CHARACTER_LIMIT && !isUnchanged && !isSaving | |
| const isReply = Boolean(post?.parentId) | |
| const canSave = !!post && trimmed.length > 0 && trimmed.length <= CHARACTER_LIMIT && !isUnchanged && !isSaving |
source: ['codex']
| // Fetch the raw document (bypassing the lossy Post transform) so the | ||
| // replacement payload preserves every stored field and the current revision. | ||
| const { getEvoSdk } = await import('./evo-sdk-service'); | ||
| const sdk = await getEvoSdk(); | ||
| const response = await sdk.documents.get(this.contractId, this.documentType, postId); | ||
| if (!response) { | ||
| throw new Error('Post not found'); | ||
| } | ||
| const raw = documentToPlainObject(response); | ||
|
|
||
| if (raw.encryptedContent) { | ||
| throw new Error('Encrypted posts cannot be edited'); | ||
| } | ||
|
|
||
| const data: Record<string, unknown> = { | ||
| content: trimmed, | ||
| // language is required by the contract — preserve the stored value | ||
| language: (raw.language as string) || 'en', | ||
| }; | ||
| if (raw.mediaUrl != null) data.mediaUrl = raw.mediaUrl; | ||
| if (raw.sensitive != null) data.sensitive = raw.sensitive; | ||
| // Abort rather than silently drop a stored quote reference the replacement must preserve | ||
| for (const field of ['quotedPostId', 'quotedPostOwnerId'] as const) { | ||
| if (raw[field] == null) continue; | ||
| const base58 = identifierToBase58(raw[field]); | ||
| if (!base58) { | ||
| logger.error(`updatePost: could not convert stored ${field} for post ${postId}`); | ||
| throw new Error('Post could not be edited: stored quote reference is unreadable'); | ||
| } | ||
| data[field] = identifierStringToDocumentBytes(base58); | ||
| } | ||
|
|
||
| const revision = Number(raw.$revision ?? raw.revision); | ||
| if (!Number.isFinite(revision) || revision < 1) { | ||
| throw new Error('Post could not be edited: current document revision is unavailable'); | ||
| } | ||
|
|
||
| const { stateTransitionService } = await import('./state-transition-service'); | ||
| const result = await stateTransitionService.updateDocument( | ||
| this.contractId, | ||
| this.documentType, | ||
| postId, | ||
| ownerId, | ||
| data, | ||
| revision |
There was a problem hiding this comment.
🟡 Suggestion: Add focused coverage for full-document replacements
The new update methods rely on invariants that are easy to regress: preserving optional fields, converting identifier references back to typed bytes, using the fetched revision, rejecting encrypted documents, and aborting rather than silently dropping unreadable references. Add focused service tests for both updatePost and updateReply using representative SDK document shapes. The repository has no existing test target, so this may require establishing a small test harness, but these write paths warrant regression coverage.
source: ['codex']
Lets users edit their own posts and replies.
What's included
updatePost()/updateReply(): content-only document replacement via the typed state-transition path (signer-service), fetching the raw document first so every stored field (language, mediaUrl, sensitive, quoted refs, parentId/parentOwnerId) is preserved at the current$revision.components/post/edit-post-modal.tsx+use-edit-post-modalstore, mirroring the delete-confirmation/tip modal pattern) — kept out of the thread composer, whose visibility/encryption/image state doesn't apply to edits.$revision > 1, carried through all Reply→Post adapter sites; cards refresh via apost-updatedwindow event.Deliberate exclusions
Contract basis
The deployed contract sets no
documentsMutable: falseonpost/reply(platform default is mutable), so document replacement is permitted.$updatedAtis not stored for these types, so the indicator relies on$revisiononly.Follow-ups
updateDocumentlacks the 504-timeout "assume success" handling thatcreateDocumenthas (pre-existing; see CLAUDE.md DAPI gateway notes).