Skip to content
Merged
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 46 additions & 14 deletions app/routes/_app+/recipients+/$recipientId.index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,8 @@ function MessageForms({
const deleteSafeDelayMs = 150
const [confirmDelete, setConfirmDelete] = useState(false)
const [canDelete, setCanDelete] = useState(false)
const [savedContent, setSavedContent] = useState(message.content)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@cursor, message.content will always be the saved content (react router revalidates on changes), so we don't need state for this.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Taking a look!

Open in Cursor Open in Web

const [currentContent, setCurrentContent] = useState(message.content)
const formRef = useRef<HTMLFormElement | null>(null)
const textareaRef = useRef<HTMLTextAreaElement | null>(null)
const [updateContentForm, updateContentFields] = useForm({
Expand All @@ -408,6 +410,8 @@ function MessageForms({
const sendIsPending = sendNowFetcher.state !== 'idle'
const deleteIsPending = deleteFetcher.state !== 'idle'
const textareaProps = getTextareaProps(updateContentFields.content)
const hasEdits = currentContent !== savedContent
const showSaveButton = hasEdits || updateIsPending

useEffect(() => {
if (confirmDelete) {
Expand All @@ -419,6 +423,19 @@ function MessageForms({
setCanDelete(false)
}, [confirmDelete, deleteSafeDelayMs])

useEffect(() => {
setSavedContent(message.content)
setCurrentContent(message.content)
}, [message.content])
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@cursor, ditch this useEffect in favor of just setting the current content in the event handler

Copy link
Copy Markdown

@cursor cursor Bot Feb 2, 2026

Choose a reason for hiding this comment

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

Walkthrough


Save checkmark appears only after edits, hides after save.

message-edit-save-derived-4

Summary

  • Review response: removed the sync useEffect and rely on the input handler to update currentContent against message.content for save visibility in app/routes/_app+/recipients+/$recipientId.index.tsx.【F:app/routes/_app+/recipients+/$recipientId.index.tsx†L380-L458】

Testing

  • npm run format — full-gate formatting check.【F:/tmp/format-3.log†L1-L4】
  • npm run typecheck — full-gate type safety check.【F:/tmp/typecheck-3.log†L1-L3】
  • ⚠️ npm run lint — full-gate linting; existing warnings in scripts directory.【F:/tmp/lint-3.log†L1-L14】
  • npm run test -- --run --project node --pool=threads --maxWorkers=1 --no-file-parallelism — full-gate node tests, serialized to avoid SQLite locking.【F:/tmp/test-node-3.log†L1-L77】
  • npm run test -- --run --project browser --pool=threads --maxWorkers=1 --no-file-parallelism — full-gate browser tests.【F:/tmp/test-browser-3.log†L1-L15】
  • DATABASE_URL="file:/workspace/tests/prisma/base.db" CACHE_DATABASE_PATH="/workspace/tests/prisma/cache.db" PORT=3001 npm run test:e2e:run — 7 e2e failures in onboarding/send/settings profile flows.【F:/home/ubuntu/.cursor/projects/workspace/terminals/19483.txt†L2671-L2714】
  • ✅ Manual UI test (no command) — required for UI change; verified checkmark appears after edits and hides after save (see walkthrough).

Open in Cursor Open in Web


useEffect(() => {
if (updateContentFetcher.data?.result.status === 'success') {
const nextContent = textareaRef.current?.value ?? currentContent
setSavedContent(nextContent)
setCurrentContent(nextContent)
}
}, [currentContent, updateContentFetcher.data?.result.status])

const handleSendNow = () => {
setConfirmDelete(false)
const formData = new FormData()
Expand All @@ -429,7 +446,19 @@ function MessageForms({

const handleEditMessage = () => {
setConfirmDelete(false)
setTimeout(() => textareaRef.current?.focus(), 0)
requestAnimationFrame(() => {
const textarea = textareaRef.current
if (!textarea) return
textarea.focus()
const caretPosition = textarea.value.length
textarea.setSelectionRange(caretPosition, caretPosition)
})
Comment thread
kentcdodds marked this conversation as resolved.
Outdated
}

const handleContentInput: React.FormEventHandler<HTMLTextAreaElement> = (
event,
) => {
setCurrentContent(event.currentTarget.value)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@cursoragent just make this inline

Copy link
Copy Markdown

@cursor cursor Bot Feb 2, 2026

Choose a reason for hiding this comment

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

Summary

  • Inlined the message edit onInput handler in app/routes/_app+/recipients+/$recipientId.index.tsx as requested.【F:app/routes/_app+/recipients+/$recipientId.index.tsx†L540-L551】

Testing

  • Manual UI testing not rerun because this is a handler refactor with no behavior change.
  • npm run format — full-gate formatting check.【F:/tmp/format-4.log†L1-L4】
  • npm run typecheck — full-gate type safety check.【F:/tmp/typecheck-4.log†L1-L3】
  • npm run lint — full-gate linting; existing warnings in scripts directory.【F:/tmp/lint-4.log†L1-L14】
  • npm run test -- --run --project node --pool=threads --maxWorkers=1 --no-file-parallelism — full-gate node tests, serialized to avoid SQLite locking.【F:/tmp/test-node-4.log†L1-L77】
  • npm run test -- --run --project browser --pool=threads --maxWorkers=1 --no-file-parallelism — full-gate browser tests.【F:/tmp/test-browser-4.log†L1-L15】
  • ⚠️ E2E not rerun for this refactor; last run failed in onboarding/send/settings flows.【F:/home/ubuntu/.cursor/projects/workspace/terminals/19483.txt†L2671-L2714】

Open in Cursor Open in Web

}

const handleDeleteSelect = (event: Event) => {
Expand Down Expand Up @@ -461,19 +490,21 @@ function MessageForms({
<span>{headerText}</span>
</div>
<div className="flex items-center gap-2">
<StatusButton
form={updateContentForm.id}
status={updateIsPending ? 'pending' : 'idle'}
className="h-11 w-11 gap-0 text-[hsl(var(--palette-cream))] hover:bg-[hsl(var(--palette-cream))/0.15] sm:h-10 sm:w-10"
size="icon"
variant="ghost"
type="submit"
name="intent"
value={updateMessageContentActionIntent}
>
<Icon name="check" size="sm" />
<span className="sr-only">Save</span>
</StatusButton>
{showSaveButton ? (
<StatusButton
form={updateContentForm.id}
status={updateIsPending ? 'pending' : 'idle'}
className="h-11 w-11 gap-0 text-[hsl(var(--palette-cream))] hover:bg-[hsl(var(--palette-cream))/0.15] sm:h-10 sm:w-10"
size="icon"
variant="ghost"
type="submit"
name="intent"
value={updateMessageContentActionIntent}
>
<Icon name="check" size="sm" />
<span className="sr-only">Save</span>
</StatusButton>
) : null}
<DropdownMenu
onOpenChange={(open) => {
if (!open) setConfirmDelete(false)
Expand Down Expand Up @@ -532,6 +563,7 @@ function MessageForms({
</label>
<textarea
{...textareaProps}
onInput={handleContentInput}
ref={textareaRef}
className="mt-4 w-full resize-none bg-transparent text-sm leading-relaxed text-[hsl(var(--palette-cream))] placeholder:text-[hsl(var(--palette-cream))]/80 focus-visible:outline-none"
rows={4}
Expand Down
Loading