fix: update web app for presigned R2 URL compatibility (#916)

This commit is contained in:
Dhravya Shah 2026-05-09 10:25:33 -07:00 committed by GitHub
parent 5771618dbb
commit 03e37df455
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 78 additions and 15 deletions

View file

@ -1,6 +1,6 @@
"use client"
import { memo, useState } from "react"
import { memo, useCallback, useState } from "react"
import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
import type { z } from "zod"
import { dmSansClassName } from "@/lib/fonts"
@ -49,6 +49,7 @@ export const FilePreview = memo(function FilePreview({
document: DocumentWithMemories
}) {
const [imageError, setImageError] = useState(false)
const [retryKey, setRetryKey] = useState(0)
const { extension, color } = getFileTypeInfo(document)
const type = document.type?.toLowerCase()
@ -58,6 +59,17 @@ export const FilePreview = memo(function FilePreview({
document.url &&
!imageError
// On first failure, wait briefly then force a re-render with a new key to
// retry the fetch (covers transient R2 timing issues).
// On second failure, give up and show the fallback file icon view.
const handleImageError = useCallback(() => {
if (retryKey === 0) {
setTimeout(() => setRetryKey(1), 500)
return
}
setImageError(true)
}, [retryKey])
return (
<div className="bg-[#0B1017] rounded-[18px] gap-3 relative overflow-hidden">
{color && (
@ -80,10 +92,11 @@ export const FilePreview = memo(function FilePreview({
/>
<div className="absolute inset-0 bg-black/20" />
<img
key={retryKey}
src={document.url}
alt={document.title || "Image preview"}
className="relative max-w-full max-h-full w-auto h-auto object-contain z-10"
onError={() => setImageError(true)}
onError={handleImageError}
loading="lazy"
/>
</div>

View file

@ -1,6 +1,6 @@
"use client"
import { useState } from "react"
import { useCallback, useState } from "react"
import { cn } from "@lib/utils"
interface ImagePreviewProps {
@ -11,6 +11,19 @@ interface ImagePreviewProps {
export function ImagePreview({ url, title }: ImagePreviewProps) {
const [imageError, setImageError] = useState(false)
const [isLoading, setIsLoading] = useState(true)
const [retryKey, setRetryKey] = useState(0)
// On first failure, wait briefly then force a re-render with a new key to
// retry the fetch (covers transient R2 timing issues).
// On second failure, give up and show the error state.
const handleImageError = useCallback(() => {
if (retryKey === 0) {
setTimeout(() => setRetryKey(1), 500)
return
}
setImageError(true)
setIsLoading(false)
}, [retryKey])
if (imageError || !url) {
return (
@ -38,16 +51,14 @@ export function ImagePreview({ url, title }: ImagePreviewProps) {
/>
<div className="absolute inset-0 bg-black/30" />
<img
key={retryKey}
src={url}
alt={title || "Image preview"}
className={cn(
"relative max-w-full max-h-full w-auto h-auto object-contain z-10",
isLoading && "opacity-0",
)}
onError={() => {
setImageError(true)
setIsLoading(false)
}}
onError={handleImageError}
onLoad={() => setIsLoading(false)}
loading="lazy"
/>

View file

@ -1,7 +1,7 @@
"use client"
import { Document, Page, pdfjs } from "react-pdf"
import { useState } from "react"
import { useCallback, useState } from "react"
import "react-pdf/dist/Page/AnnotationLayer.css"
import "react-pdf/dist/Page/TextLayer.css"
@ -19,6 +19,7 @@ export function PdfViewer({ url }: PdfViewerProps) {
const [numPages, setNumPages] = useState<number | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [retryKey, setRetryKey] = useState(0)
if (!url) {
return (
@ -34,10 +35,24 @@ export function PdfViewer({ url }: PdfViewerProps) {
setError(null)
}
function onDocumentLoadError(error: Error) {
setError(error.message || "Failed to load PDF")
setLoading(false)
}
// On first failure, wait briefly then force a re-mount of the Document
// component to retry (covers transient R2 timing issues).
// On second failure, give up and show the error state.
const onDocumentLoadError = useCallback(
(err: Error) => {
if (retryKey === 0) {
setTimeout(() => {
setRetryKey(1)
setLoading(true)
setError(null)
}, 500)
return
}
setError(err.message || "Failed to load PDF")
setLoading(false)
},
[retryKey],
)
return (
<div className="flex flex-col h-full w-full overflow-hidden scrollbar-thin">
@ -53,6 +68,7 @@ export function PdfViewer({ url }: PdfViewerProps) {
)}
<div className="flex-1 overflow-auto w-full">
<Document
key={retryKey}
file={
url ||
"https://corsproxy.io/?" +

View file

@ -24,7 +24,7 @@ import { getAbsoluteUrl, isYouTubeUrl, useYouTubeChannelName } from "./utils"
import { SyncLogoIcon } from "@ui/assets/icons"
import { McpPreview } from "./document-cards/mcp-preview"
import { NotionPreview } from "./document-cards/notion-preview"
import { getFaviconUrl } from "@/lib/url-helpers"
import { getFaviconUrl, isSupermemoryFileUrl } from "@/lib/url-helpers"
import { QuickNoteCard } from "./quick-note-card"
import type { HighlightItem } from "./highlights-card"
import { Button } from "@ui/components/button"
@ -947,7 +947,7 @@ const DocumentCard = memo(
document.type !== "notion_doc" &&
!document.url.includes("x.com") &&
!document.url.includes("twitter.com") &&
!document.url.includes("files.supermemory.ai") &&
!isSupermemoryFileUrl(document.url) &&
!document.url.includes("docs.googleapis.com") &&
!document.url.includes("notion.so") &&
(!document.title || !ogImage)
@ -1062,7 +1062,7 @@ const DocumentCard = memo(
) && (
<div className="pb-[10px] space-y-1">
{document.url &&
!document.url.includes("files.supermemory.ai") &&
!isSupermemoryFileUrl(document.url) &&
(document.title ||
(!document.url.includes("x.com") &&
!document.url.includes("twitter.com"))) && (

View file

@ -189,6 +189,29 @@ export function toLinkedInProfileUrl(handle: string): string {
return `https://linkedin.com/in/${handle.trim()}`
}
/**
* Checks if a URL points to a supermemory-hosted file.
* Matches the public bucket domain (files.supermemory.ai) and
* presigned R2 URLs whose hostname ends with `.r2.cloudflarestorage.com`.
*
* Note: The R2 check is intentionally broad it matches any Cloudflare R2
* presigned URL, not only supermemory's account. This is acceptable because
* the function is only called on `document.url` values returned by our own
* backend, where all R2 URLs originate from the supermemory bucket.
* If user-supplied external R2 URLs ever appear in this field, tighten the
* check by also validating the account-id subdomain or the bucket path prefix.
*/
export const isSupermemoryFileUrl = (url: string): boolean => {
try {
const parsed = new URL(url)
if (parsed.hostname === "files.supermemory.ai") return true
if (parsed.hostname.endsWith(".r2.cloudflarestorage.com")) return true
return false
} catch {
return false
}
}
/**
* Gets the favicon URL for a given URL.
*/