diff --git a/app/user/page.tsx b/app/user/page.tsx index b86cde31..7351638b 100644 --- a/app/user/page.tsx +++ b/app/user/page.tsx @@ -802,6 +802,8 @@ function UserProfileContent() { author: reply.author, content: reply.content, createdAt: reply.createdAt, + updatedAt: reply.updatedAt, + isEdited: reply.isEdited, likes: reply.likes, reposts: reply.reposts, replies: reply.replies, diff --git a/components/post/edit-post-modal.tsx b/components/post/edit-post-modal.tsx new file mode 100644 index 00000000..90f01acd --- /dev/null +++ b/components/post/edit-post-modal.tsx @@ -0,0 +1,199 @@ +'use client' + +import { logger } from '@/lib/logger' +import { useEffect, useRef, useState } from 'react' +import * as Dialog from '@radix-ui/react-dialog' +import { XMarkIcon } from '@heroicons/react/24/outline' +import { motion, AnimatePresence } from 'framer-motion' +import toast from 'react-hot-toast' +import { Button } from '@/components/ui/button' +import { IconButton } from '@/components/ui/icon-button' +import { Spinner } from '@/components/ui/spinner' +import { UserAvatar } from '@/components/ui/avatar-image' +import { CharacterCounter } from '@/components/compose/compose-sub-components' +import { useEditPostModal } from '@/hooks/use-edit-post-modal' +import { useRequireAuth } from '@/hooks/use-require-auth' +import { useAuth } from '@/contexts/auth-context' +import { useSettingsStore } from '@/lib/store' +import { categorizeError } from '@/lib/error-utils' +import { CHARACTER_LIMIT } from '@/components/compose/thread-post-editor' + +/** + * Modal for editing the content of an existing post or reply. + * + * Uses document replacement on Dash Platform: only the content field changes, + * all other stored fields are preserved by the service layer. The platform + * bumps $revision on replacement, which drives the "(edited)" indicator. + */ +export function EditPostModal() { + const { isOpen, post, close } = useEditPostModal() + const { requireAuth } = useRequireAuth() + const { user } = useAuth() + const potatoMode = useSettingsStore((s) => s.potatoMode) + const [content, setContent] = useState('') + const [isSaving, setIsSaving] = useState(false) + const textareaRef = useRef(null) + + // Pre-populate content when the modal opens + useEffect(() => { + if (isOpen && post) { + setContent(post.content) + // Small delay so the modal animation has mounted the textarea + const timeoutId = setTimeout(() => { + const textarea = textareaRef.current + if (textarea) { + textarea.focus() + textarea.setSelectionRange(textarea.value.length, textarea.value.length) + } + }, 50) + return () => clearTimeout(timeoutId) + } + }, [isOpen, post]) + + // Auto-resize the textarea to fit its content, like the thread composer + useEffect(() => { + const textarea = textareaRef.current + if (!textarea) return + textarea.style.height = 'auto' + textarea.style.height = `${Math.max(120, textarea.scrollHeight)}px` + }, [content, isOpen]) + + 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 handleSave = async () => { + if (!post || !canSave) return + const authedUser = requireAuth('post') + if (!authedUser) return + + setIsSaving(true) + try { + if (isReply) { + const { replyService } = await import('@/lib/services/reply-service') + await replyService.updateReply(post.id, authedUser.identityId, trimmed) + } else { + const { postService } = await import('@/lib/services/post-service') + await postService.updatePost(post.id, authedUser.identityId, trimmed) + } + + toast.success(isReply ? 'Reply updated' : 'Post updated') + + // Let mounted cards/pages refresh their displayed content + window.dispatchEvent(new CustomEvent('post-updated', { + detail: { postId: post.id, content: trimmed, isReply } + })) + + close() + } catch (error) { + logger.error('Failed to update post:', error) + toast.error(categorizeError(error)) + } finally { + setIsSaving(false) + } + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { + e.preventDefault() + handleSave().catch(err => logger.error('Failed to save edit:', err)) + } + } + + return ( + !open && !isSaving && close()}> + + {isOpen && ( + + + + + e.stopPropagation()} + onKeyDown={handleKeyDown} + > + {/* Accessibility */} + + Edit {isReply ? 'reply' : 'post'} + + + Your changes replace the original content. The {isReply ? 'reply' : 'post'} will be marked as edited. + + + {/* Header */} +
+
+ !isSaving && close()} + className="hover:bg-gray-200 dark:hover:bg-gray-800" + > + + + {user && ( + + )} + + Edit {isReply ? 'reply' : 'post'} + +
+ + +
+ + {/* Main content area */} +
+