From c6f52c10d335f63f9253025c221aa8dccd2be3cb Mon Sep 17 00:00:00 2001 From: Alan Daniel Date: Thu, 16 Apr 2026 21:18:25 -0400 Subject: [PATCH 1/2] feat(comments): R2 media uploads and composer improvements Add Cloudflare R2-backed image/video upload for issue/PR comments (multipart route, finalize HTML embeds). Integrate react-dropzone with inline uploading placeholders, scroll-to-actions when needed, and remote R2 binding for local dev. Extend MarkdownEditor with field-sizing, non-blocking uploads, replaceUploadPlaceholder, and conditional scroll behavior. Document optional R2_PUBLIC_BASE_URL in .dev.vars.example. --- apps/dashboard/.dev.vars.example | 15 + apps/dashboard/package.json | 1 + .../components/details/comment-reply-form.tsx | 21 +- .../components/details/detail-activity.tsx | 17 +- apps/dashboard/src/entry-worker.ts | 10 + apps/dashboard/src/env.d.ts | 2 + .../src/hooks/use-comment-media-upload.ts | 206 + .../src/lib/comment-media-upload.handler.ts | 76 + .../dashboard/src/lib/comment-media.server.ts | 134 + apps/dashboard/src/lib/media.functions.ts | 84 + apps/dashboard/worker-configuration.d.ts | 23415 ++++++++-------- apps/dashboard/wrangler.jsonc | 11 + .../ui/src/components/markdown-editor.tsx | 222 +- packages/ui/src/components/markdown.tsx | 9 + .../use-textarea-content-field-sizing.ts | 128 + pnpm-lock.yaml | 58 + 16 files changed, 11939 insertions(+), 12470 deletions(-) create mode 100644 apps/dashboard/src/hooks/use-comment-media-upload.ts create mode 100644 apps/dashboard/src/lib/comment-media-upload.handler.ts create mode 100644 apps/dashboard/src/lib/comment-media.server.ts create mode 100644 apps/dashboard/src/lib/media.functions.ts create mode 100644 packages/ui/src/hooks/use-textarea-content-field-sizing.ts diff --git a/apps/dashboard/.dev.vars.example b/apps/dashboard/.dev.vars.example index b1d4674..22ccee5 100644 --- a/apps/dashboard/.dev.vars.example +++ b/apps/dashboard/.dev.vars.example @@ -99,3 +99,18 @@ BETTER_AUTH_URL=http://localhost:3000 # 3. Set the same URL as the webhook URL in your GitHub App settings, # appending /api/webhooks/github DEV_TUNNEL_URL= + +# ----------------------------------------------------------------------------- +# 6. R2 comment media (optional) +# ----------------------------------------------------------------------------- +# Not required for the app to run. Without it, comment image/video uploads stay disabled +# (upload and finalize endpoints return a configuration error). +# +# Set to the public base URL for the SAME bucket Wrangler writes to (custom domain or r2.dev), +# no trailing slash. Example: https://pub-xxxxxxxxxxxxxxxx.r2.dev +# +# Local dev uses Miniflare by default: objects stay under .wrangler/state and do not show in the +# Cloudflare dashboard. wrangler.jsonc enables "remote": true on COMMENT_MEDIA so puts go to the +# real bucket named there (see bucket_name). Your pub URL must be for that bucket — not only +# preview_bucket_name (that name is for local simulation naming when not remote). +R2_PUBLIC_BASE_URL= diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index 0c2c8f2..f643b15 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -43,6 +43,7 @@ "octokit": "^5.0.5", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-dropzone": "^15.0.0", "tailwindcss": "^4.1.18" }, "devDependencies": { diff --git a/apps/dashboard/src/components/details/comment-reply-form.tsx b/apps/dashboard/src/components/details/comment-reply-form.tsx index 0281525..5ec2269 100644 --- a/apps/dashboard/src/components/details/comment-reply-form.tsx +++ b/apps/dashboard/src/components/details/comment-reply-form.tsx @@ -1,9 +1,13 @@ import { CommentIcon } from "@diffkit/icons"; -import { MarkdownEditor } from "@diffkit/ui/components/markdown-editor"; +import { + MarkdownEditor, + type MarkdownEditorHandle, +} from "@diffkit/ui/components/markdown-editor"; import { toast } from "@diffkit/ui/components/sonner"; import { Spinner } from "@diffkit/ui/components/spinner"; import { useQueryClient } from "@tanstack/react-query"; -import { useCallback, useState } from "react"; +import { useCallback, useRef, useState } from "react"; +import { useCommentMediaUpload } from "#/hooks/use-comment-media-upload"; import { createComment } from "#/lib/github.functions"; import { githubQueryKeys } from "#/lib/github.query"; import { checkPermissionWarning } from "#/lib/warning-store"; @@ -28,6 +32,10 @@ export function CommentReplyForm({ const [value, setValue] = useState(""); const [isSending, setIsSending] = useState(false); const queryClient = useQueryClient(); + const editorRef = useRef(null); + const commentActionsRef = useRef(null); + const { media: mediaUpload, onPaste: onMediaPaste } = + useCommentMediaUpload(editorRef); const handleSend = useCallback(async () => { if (!value.trim()) return; @@ -79,12 +87,19 @@ export function CommentReplyForm({ return (
-
+