mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-05-19 07:42:43 +00:00
fix: update web app for presigned R2 URL compatibility (#916)
This commit is contained in:
parent
5771618dbb
commit
03e37df455
5 changed files with 78 additions and 15 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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/?" +
|
||||
|
|
|
|||
|
|
@ -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"))) && (
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue