Skip to content

Add post and reply editing - #90

Open
PastaPastaPasta wants to merge 9 commits into
masterfrom
claude/add-post-edit-feature-TSFTa
Open

Add post and reply editing#90
PastaPastaPasta wants to merge 9 commits into
masterfrom
claude/add-post-edit-feature-TSFTa

Conversation

@PastaPastaPasta

@PastaPastaPasta PastaPastaPasta commented Jan 13, 2026

Copy link
Copy Markdown
Owner

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.
  • Dedicated edit modal (components/post/edit-post-modal.tsx + use-edit-post-modal store, mirroring the delete-confirmation/tip modal pattern) — kept out of the thread composer, whose visibility/encryption/image state doesn't apply to edits.
  • "(edited)" indicator derived from $revision > 1, carried through all Reply→Post adapter sites; cards refresh via a post-updated window event.
  • Edit menu item on own posts and replies.

Deliberate exclusions

  • Encrypted (private) posts cannot be edited — a content-only replacement would desync ciphertext from the public teaser.
  • Edits do not re-index hashtags/mentions (follow-up).

Contract basis

The deployed contract sets no documentsMutable: false on post/reply (platform default is mutable), so document replacement is permitted. $updatedAt is not stored for these types, so the indicator relies on $revision only.

Follow-ups

  • updateDocument lacks the 504-timeout "assume success" handling that createDocument has (pre-existing; see CLAUDE.md DAPI gateway notes).
  • Hashtag/mention re-indexing on edit.

- 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
@coderabbitai

coderabbitai Bot commented Jan 13, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 30 minutes.

View limit details

Limit 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.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 04a46602-8d5d-4afd-abae-83facf454015

📥 Commits

Reviewing files that changed from the base of the PR and between 1ba3af7 and e1b4052.

📒 Files selected for processing (10)
  • app/user/page.tsx
  • components/post/edit-post-modal.tsx
  • components/post/post-card.tsx
  • components/post/reply-thread.tsx
  • components/providers.tsx
  • hooks/use-edit-post-modal.ts
  • hooks/use-yappr-post-reference.ts
  • lib/services/post-service.ts
  • lib/services/reply-service.ts
  • types/post.ts

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.

Dash Platform documents start with revision 1, so posts are
only considered edited when revision > 1.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jan 13, 2026

Copy link
Copy Markdown

Deploying yappr with  Cloudflare Pages  Cloudflare Pages

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

View logs

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.
@PastaPastaPasta
PastaPastaPasta marked this pull request as draft January 13, 2026 22:36
PastaPastaPasta and others added 4 commits August 25, 2026 12:20
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>
@PastaPastaPasta PastaPastaPasta changed the title Add post and reply editing feature Add post and reply editing Aug 25, 2026
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 25, 2026

Copy link
Copy Markdown

Deploying yappr-v2 with  Cloudflare Pages  Cloudflare Pages

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

View logs

@PastaPastaPasta
PastaPastaPasta marked this pull request as ready for review August 25, 2026 12:15
@thepastaclaw

thepastaclaw commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

⛔ Blockers found — Opus deferred (commit e1b4052)
Canonical validated blockers: 1

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 thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

Comment on lines +176 to +180
// 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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔴 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']

Comment on lines +61 to +64
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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

💬 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.

Suggested change
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']

Comment on lines +294 to +338
// 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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 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']

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