Skip to content

refactor(channel): share attachment infra (DM-files PR 1/2) - #1175

Merged
2witstudios merged 8 commits into
masterfrom
pu/dm-files
May 1, 2026
Merged

2witstudios merged 8 commits into
masterfrom
pu/dm-files

Conversation

@2witstudios

@2witstudios 2witstudios commented Apr 30, 2026 •

Copy link
Copy Markdown
Owner

Summary

Pure-refactor groundwork for DM file attachments — Tasks 1–3 of the DM File Attachments epic. Channels adopt three shared modules so the second caller (DM upload route in PR 2) drops in as a thin wrapper.

  • One canonical AttachmentMeta type (was 3 copies — db schema, attachment-utils, channel messages route). Lives in packages/db/src/schema/storage.ts next to files; re-exported from @pagespace/lib/types for non-db consumers.
  • One MessageAttachment component (was 2 copies of a 50-line block — ChannelView.tsx + inbox/channel/[pageId]/page.tsx). Pixel-identical output by construction.
  • One useAttachmentUpload hook with useEditingStore registration so SWR cannot clobber an in-flight upload — closes a latent gap that affected channels too. ChannelInput.tsx is ~75 lines lighter.
  • 5 colocated unit tests for the new hook covering happy path, null-URL no-op, 413 toast, thrown-error session release, and clearAttachment. The original inline upload logic was untested; this is the right moment to lock the contract once for both channels and DMs.

Zero behavior change: channel rendering, channel upload, and channel send all work identically.

Out of scope (deliberate, lands in PR 2)

  • AttachmentTarget discriminated union — the abstraction is only meaningful with a second caller, which only appears in PR 2 (DM upload route). Keeping it out of this PR avoids forcing reviewers to imagine a future branch.
  • fileConversations schema migration, files.driveId nullability, DM upload endpoint, DM POST acceptance of fileId, DM composer wiring. All planned in tasks/dm-file-attachments.md.

Test plan

  • pnpm turbo typecheck lint — 4/4 tasks pass, no new warnings
  • vitest run src/hooks/__tests__/useAttachmentUpload.test.ts — 5/5 pass (new)
  • vitest run src/lib/ai/tools/__tests__/channel-tools.test.ts — 11/11 pass
  • Manual: open a channel, pick a file via the attachment button → image preview renders inline; non-image renders as download card
  • Manual: open /dashboard/inbox/channel/[id], scroll history with mixed image/file attachments → identical to today
  • Manual: start a slow upload, confirm SWR refresh doesn't clear the pending preview mid-upload (new behavior — uses useEditingStore)

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • Refactor
    • Consolidated attachment handling infrastructure by centralizing attachment metadata type definitions across the application.
    • Extracted shared attachment upload logic into a reusable hook with improved error handling and user feedback for file upload operations.
    • Created a unified attachment rendering component to ensure consistent behavior for file preview and download functionality across the platform.

2witstudios and others added 4 commits April 30, 2026 16:50
Plan and epic for bringing DM file attachments to channel parity via a fileConversations join table, nullable files.driveId, and four DRY refactors that share an attachment-upload core, service-token issuer, processor binding, composer hook, and renderer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Defines AttachmentMeta once in packages/db/src/schema/storage.ts (next to files) and re-exports from @pagespace/lib/types. Removes three duplicate inline shapes (channelMessages $type, attachment-utils.ts, channel messages route handler). Pure type refactor with no runtime change; sets up DM directMessages.attachmentMeta to import from the same canonical source without drifting.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extracts the attachment-rendering JSX (image preview / file download card) from ChannelView and the inbox channel page into a single MessageAttachment component under components/shared. Both call sites now render <MessageAttachment message={m} /> and drop their FileIcon/FileText/Download imports plus the seven attachment-utils helpers they only used for that block. Pure code-motion; output is pixel-identical.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pulls drag/paste/picker upload state and error-handling logic out of ChannelInput into a reusable useAttachmentUpload hook parameterized by upload URL. The hook registers each in-flight upload with useEditingStore (type 'form') so SWR cannot clobber a pending upload, fixing a latent gap that affected channels and would have affected DMs. ChannelInput re-exports FileAttachment from the hook to keep existing consumers working.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Apr 30, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@2witstudios has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 51 minutes and 30 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1d049ec7-ab22-43a2-826e-40c08a84762f

📥 Commits

Reviewing files that changed from the base of the PR and between fb922ad and 1f42a3f.

📒 Files selected for processing (4)
  • apps/web/src/components/layout/middle-content/page-views/channel/ChannelInput.tsx
  • apps/web/src/hooks/__tests__/useAttachmentUpload.test.ts
  • apps/web/src/hooks/useAttachmentUpload.ts
  • tasks/dm-file-attachments.md
📝 Walkthrough

Walkthrough

The PR refactors attachment handling by extracting the shared AttachmentMeta type to a central location, creating reusable MessageAttachment component and useAttachmentUpload hook, and consolidating attachment logic from multiple components into these shared modules.

Changes

Cohort / File(s) Summary
Type Centralization
packages/db/src/schema/storage.ts, packages/lib/src/types.ts, packages/db/src/schema/chat.ts, apps/web/src/app/api/channels/.../route.ts, apps/web/src/lib/attachment-utils.ts
New AttachmentMeta interface defined in schema/storage and re-exported through lib/types; all local definitions replaced with imports from shared type source. MessageWithAttachment now exported from attachment-utils.
Shared Component & Hook
apps/web/src/components/shared/MessageAttachment.tsx, apps/web/src/hooks/useAttachmentUpload.ts
New MessageAttachment component for rendering image previews and file downloads with icon/size metadata. New useAttachmentUpload hook handles file upload with authenticated requests, error-specific toast messages, and session management. FileAttachment type moved to hook module.
Component Integration
apps/web/src/components/layout/.../ChannelInput.tsx, apps/web/src/components/layout/.../ChannelView.tsx, apps/web/src/app/dashboard/inbox/channel/.../page.tsx
Components refactored to use shared MessageAttachment component for rendering and useAttachmentUpload hook for uploads; removed local attachment logic, utilities, and icon imports.
Documentation
plan.md, tasks/dm-file-attachments.md
Added active epic tracking DM file attachments feature; detailed planning document outlining polymorphic upload service, DB schema updates for fileConversations join table, permission checks, and test coverage requirements.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 Files unite, attachments now share,
A hop through hooks and components with care,
No more repeats in the code we create,
Just shared little types, oh how they're great!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'refactor(channel): share attachment infra (DM-files PR 1/2)' accurately describes the main objective: consolidating and sharing attachment infrastructure across the channel module as preparation for DM file attachments.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pu/dm-files

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
Review rate limit: 0/1 reviews remaining, refill in 51 minutes and 30 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

Replaces module-level sessionCounter+timestamp with createId() (cuid2) for upload-session IDs — matches existing hook conventions in useVoiceMode and useDocument and removes shared mutable module state. Also drops the unused setAttachment from the hook's return; the only consumer (ChannelInput) uses uploadFile/clearAttachment exclusively.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@2witstudios

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

2witstudios and others added 2 commits April 30, 2026 18:10
Adds 5 colocated tests covering: success path stores the attachment and brackets the upload in startEditing/endEditing; null uploadUrl is a no-op; 413 surfaces the file-too-large toast; thrown fetch errors still release the editing session; clearAttachment resets state. The original inline ChannelInput upload logic was untested; this PR's extraction is the right moment to lock the contract once for both channels and DMs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ChannelInput.tsx had a private formatFileSize identical to the one already exported from attachment-utils.ts. Imports the shared one and removes the local copy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/src/hooks/useAttachmentUpload.ts`:
- Around line 38-47: The uploadFile concurrency bug: multiple invocations race
on isUploading/setIsUploading; add a persistent ref-based in-flight counter
(e.g., uploadCountRef = useRef(0)) and in uploadFile (and the other upload
handler at lines ~90-93) increment uploadCountRef.current before starting, call
setIsUploading(true) if count becomes >0, then in finally decrement the counter
and call setIsUploading(uploadCountRef.current > 0); use the same
sessionId/startEditing/endEditing logic but rely on the ref counter to ensure
the loading flag is only cleared when all uploads complete.
- Around line 57-60: The response.json() results in useAttachmentUpload
currently assume a specific shape and directly access properties like
errorData.error and result.file.id/originalName/size/mimeType which can throw;
update the fetch error and success handling in the useAttachmentUpload hook to
perform runtime shape validation (e.g., using a small runtime guard or a schema
validator) before accessing fields: verify that errorData is an object with a
string "error" property before reading it, and verify that result is an object
with a "file" object containing string id, originalName, mimeType and numeric
size before constructing the Attachment payload; if validation fails, return or
throw a clear, handled error so callers don't encounter undefined-property
runtime exceptions (apply the checks around the response.json() usages in the
error branch and the success branch that reference result.file.*).

In `@tasks/dm-file-attachments.md`:
- Line 14: Update the doc to point to the canonical definition of AttachmentMeta
in the DB schema module (storage.ts) rather than the lib re-export; change the
line that currently references packages/lib/src/types.ts to reference the
schema's AttachmentMeta and note that channelMessages.attachmentMeta and
directMessages.attachmentMeta JSONB columns import that canonical type (i.e.,
reference AttachmentMeta in the DB schema module and mention both
channelMessages.attachmentMeta and directMessages.attachmentMeta).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2a87bc01-6d93-47b4-9505-396fdf7a56da

📥 Commits

Reviewing files that changed from the base of the PR and between c961de7 and fb922ad.

📒 Files selected for processing (12)
  • apps/web/src/app/api/channels/[pageId]/messages/route.ts
  • apps/web/src/app/dashboard/inbox/channel/[pageId]/page.tsx
  • apps/web/src/components/layout/middle-content/page-views/channel/ChannelInput.tsx
  • apps/web/src/components/layout/middle-content/page-views/channel/ChannelView.tsx
  • apps/web/src/components/shared/MessageAttachment.tsx
  • apps/web/src/hooks/useAttachmentUpload.ts
  • apps/web/src/lib/attachment-utils.ts
  • packages/db/src/schema/chat.ts
  • packages/db/src/schema/storage.ts
  • packages/lib/src/types.ts
  • plan.md
  • tasks/dm-file-attachments.md

Comment thread apps/web/src/hooks/useAttachmentUpload.ts
Comment thread apps/web/src/hooks/useAttachmentUpload.ts Outdated
Comment thread tasks/dm-file-attachments.md Outdated
Addresses three CodeRabbit findings on PR #1175:

1. Race in uploadFile — multiple invocations would each setIsUploading(false) in finally, clearing the flag while later uploads were still in flight. Adds an isUploadingRef sibling to the React state so concurrent calls early-return without waiting for a state flush. New test covers the gate.

2. Untyped response.json() — adds explicit UploadErrorBody / UploadSuccessBody casts at the trust boundary. Skips a full runtime validator: the upload endpoint is internal and we control both sides; per CLAUDE.md, validation belongs at system boundaries (user input / external APIs), not between our own server and our own client. The cast satisfies the no-any rule without runtime overhead.

3. Epic doc misdirect — fixes the line that said AttachmentMeta lives in packages/lib/src/types.ts; the canonical home is packages/db/src/schema/storage.ts (lib re-exports it; db can't depend on lib without a cycle).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@2witstudios
2witstudios merged commit b96674c into master May 1, 2026
10 checks passed
@2witstudios
2witstudios deleted the pu/dm-files branch May 2, 2026 03:22
2witstudios added a commit that referenced this pull request May 15, 2026
* chore(tasks): add DM file attachments epic

Plan and epic for bringing DM file attachments to channel parity via a fileConversations join table, nullable files.driveId, and four DRY refactors that share an attachment-upload core, service-token issuer, processor binding, composer hook, and renderer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(types): centralize AttachmentMeta

Defines AttachmentMeta once in packages/db/src/schema/storage.ts (next to files) and re-exports from @pagespace/lib/types. Removes three duplicate inline shapes (channelMessages $type, attachment-utils.ts, channel messages route handler). Pure type refactor with no runtime change; sets up DM directMessages.attachmentMeta to import from the same canonical source without drifting.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(channel): share MessageAttachment renderer

Extracts the attachment-rendering JSX (image preview / file download card) from ChannelView and the inbox channel page into a single MessageAttachment component under components/shared. Both call sites now render <MessageAttachment message={m} /> and drop their FileIcon/FileText/Download imports plus the seven attachment-utils helpers they only used for that block. Pure code-motion; output is pixel-identical.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(channel): extract useAttachmentUpload hook

Pulls drag/paste/picker upload state and error-handling logic out of ChannelInput into a reusable useAttachmentUpload hook parameterized by upload URL. The hook registers each in-flight upload with useEditingStore (type 'form') so SWR cannot clobber a pending upload, fixing a latent gap that affected channels and would have affected DMs. ChannelInput re-exports FileAttachment from the hook to keep existing consumers working.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(useAttachmentUpload): tighten API surface

Replaces module-level sessionCounter+timestamp with createId() (cuid2) for upload-session IDs — matches existing hook conventions in useVoiceMode and useDocument and removes shared mutable module state. Also drops the unused setAttachment from the hook's return; the only consumer (ChannelInput) uses uploadFile/clearAttachment exclusively.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(useAttachmentUpload): cover happy path, errors, edit lock

Adds 5 colocated tests covering: success path stores the attachment and brackets the upload in startEditing/endEditing; null uploadUrl is a no-op; 413 surfaces the file-too-large toast; thrown fetch errors still release the editing session; clearAttachment resets state. The original inline ChannelInput upload logic was untested; this PR's extraction is the right moment to lock the contract once for both channels and DMs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(channel): drop duplicated formatFileSize

ChannelInput.tsx had a private formatFileSize identical to the one already exported from attachment-utils.ts. Imports the shared one and removes the local copy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(useAttachmentUpload): gate concurrent uploads + type response

Addresses three CodeRabbit findings on PR #1175:

1. Race in uploadFile — multiple invocations would each setIsUploading(false) in finally, clearing the flag while later uploads were still in flight. Adds an isUploadingRef sibling to the React state so concurrent calls early-return without waiting for a state flush. New test covers the gate.

2. Untyped response.json() — adds explicit UploadErrorBody / UploadSuccessBody casts at the trust boundary. Skips a full runtime validator: the upload endpoint is internal and we control both sides; per CLAUDE.md, validation belongs at system boundaries (user input / external APIs), not between our own server and our own client. The cast satisfies the no-any rule without runtime overhead.

3. Epic doc misdirect — fixes the line that said AttachmentMeta lives in packages/lib/src/types.ts; the canonical home is packages/db/src/schema/storage.ts (lib re-exports it; db can't depend on lib without a cycle).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

1 participant