Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **First-time sign-in no longer breaks at the set-password step** — after verifying a magic code, a brand-new user (one who hasn't set a password yet) was advanced to the "set password" screen but the tokens issued by `verify-magic-code` were discarded, so the follow-up `POST /auth/set-password` (which requires an authenticated user) returned 401 and bounced the user back to the login screen — never able to finish onboarding. The tokens are now persisted before the set-password step. The password-login form also validates the email format client-side, so a malformed address shows a friendly "Enter a valid email address" instead of surfacing the raw backend validation message.
- **"Copy invite link" works over plain HTTP / LAN** — the admin users page called the browser Clipboard API directly, which only exists in a secure context (HTTPS or `localhost`); on a plain-HTTP LAN address the button threw. It now uses the shared clipboard helper (with an `execCommand` fallback) and only shows "Copied" on success.
- **Presigned URLs are correct in AWS S3 mode** — with `S3_STORAGE=s3`, a configured `S3_PUBLIC_ENDPOINT` (a MinIO/dev-only concept) would override the AWS host and point presigned upload/download URLs at the wrong endpoint. AWS mode now always uses native presigned URLs and ignores `S3_PUBLIC_ENDPOINT`.
- **Public share links open on the media, not the comment panel, on phones** — the comment panel in both share viewers (single asset and folder) defaulted to open at every screen size, so opening a share link on a phone showed the comment pane first with the media squeezed to a sliver off-screen. The panel now starts closed below the `md` breakpoint (768px) and open above it; it can still be toggled either way.
- **Public share links are usable on phones** — the comment panel defaulted to open at every screen size and was a fixed 360px column that refused to shrink, so opening a share link on a phone showed the comment pane first with the media squeezed to a sliver — and tapping the toggle to leave a comment did it again. Below the `md` breakpoint (768px) the panel now starts closed and opens as a full-width sheet over the media instead of competing for width; at `md` and above the side-by-side layout is unchanged. Applies to both the single-asset and folder share viewers.

## [1.6.0] - 2026-07-14

Expand Down
109 changes: 2 additions & 107 deletions apps/web/app/share/[token]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -693,7 +693,7 @@ function ShareRightPanel({
const [activeTab, setActiveTab] = React.useState<'comments' | 'fields'>('comments')

return (
<div className="w-[360px] flex flex-col border-l border-white/[0.06] bg-[#141416] shrink-0 animate-in slide-in-from-right-2 duration-150">
<div className="w-full md:w-[360px] absolute inset-y-0 right-0 z-20 md:static md:inset-auto flex flex-col border-l-0 md:border-l border-white/[0.06] bg-[#141416] shrink-0 animate-in slide-in-from-right-2 duration-150">
{/* Tabs */}
<div className="px-4 pt-3 pb-2 shrink-0">
<div className="flex items-center bg-white/5 rounded-lg p-0.5">
Expand Down Expand Up @@ -874,7 +874,7 @@ function ShareViewer({
/>

{/* Main content: viewer + sidebar */}
<div className="flex flex-1 overflow-hidden min-h-0">
<div className="relative flex flex-1 overflow-hidden min-h-0">
{/* Left: full-screen media viewer */}
<ShareMediaViewer
asset={asset}
Expand Down Expand Up @@ -905,112 +905,7 @@ function ShareViewer({
)
}

// ─── Folder Asset Viewer (single asset within folder share) ──────────────────

interface FolderAssetViewerProps {
token: string
assetId: string
permission: SharePermission
allowDownload: boolean
branding: any
folderName: string
onBack: () => void
}

function FolderAssetViewer({
token,
assetId,
permission,
allowDownload,
branding,
folderName,
onBack,
}: FolderAssetViewerProps) {
const [streamUrl, setStreamUrl] = React.useState<string | null>(null)
const [thumbnailUrl, setThumbnailUrl] = React.useState<string | null>(null)
const [assetInfo, setAssetInfo] = React.useState<{
name: string
asset_type: string
description?: string
status?: string
keywords?: string[]
} | null>(null)
const [loading, setLoading] = React.useState(true)

React.useEffect(() => {
let cancelled = false
setLoading(true)

const streamPromise = fetch(`${API_URL}/share/${token}/stream/${assetId}`)
.then((r) => (r.ok ? r.json() : null))
.catch(() => null)

const thumbPromise = fetch(`${API_URL}/share/${token}/thumbnail/${assetId}`)
.then((r) => {
if (!r.ok) return null
const contentType = r.headers.get('content-type')
if (contentType?.includes('application/json')) {
return r.json()
}
return { url: `${API_URL}/share/${token}/thumbnail/${assetId}` }
})
.catch(() => null)

Promise.all([streamPromise, thumbPromise]).then(([streamData, thumbData]) => {
if (cancelled) return
if (streamData?.url) setStreamUrl(streamData.url)
if (streamData?.name)
setAssetInfo({
name: streamData.name,
asset_type: streamData.asset_type ?? 'image',
description: streamData.description,
status: streamData.status,
keywords: streamData.keywords,
})
else setAssetInfo({ name: 'Asset', asset_type: 'image' })
if (thumbData?.url) setThumbnailUrl(thumbData.url)
setLoading(false)
})

return () => {
cancelled = true
}
}, [token, assetId])

if (loading) {
return (
<div className="flex min-h-screen items-center justify-center bg-zinc-950">
<Loader2 className="h-8 w-8 animate-spin text-zinc-500" />
</div>
)
}

const assetType = assetInfo?.asset_type ?? 'image'

// Build a pseudo-asset object for the viewer
const pseudoAsset = {
id: assetId,
name: assetInfo?.name ?? 'Asset',
asset_type: assetType,
description: assetInfo?.description,
status: assetInfo?.status ?? 'draft',
keywords: assetInfo?.keywords ?? [],
thumbnail_url: thumbnailUrl,
stream_url: streamUrl,
} as Asset & { thumbnail_url?: string; stream_url?: string }

return (
<ShareViewer
token={token}
asset={pseudoAsset}
permission={permission}
allowDownload={allowDownload}
branding={branding}
shareName={folderName}
onBack={onBack}
/>
)
}

// ─── Page ─────────────────────────────────────────────────────────────────────

Expand Down
4 changes: 2 additions & 2 deletions apps/web/components/share/folder-share-viewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -882,7 +882,7 @@ function ShareReviewInner({
</div>

{/* Main: viewer + sidebar */}
<div className="flex flex-1 overflow-hidden min-h-0">
<div className="relative flex flex-1 overflow-hidden min-h-0">
{/* Media viewer — reuses project components */}
<div className="flex-1 flex flex-col bg-bg-primary overflow-hidden min-w-0">
{asset.asset_type === 'video' && versionReady && VideoPlayer ? (
Expand Down Expand Up @@ -922,7 +922,7 @@ function ShareReviewInner({

{/* Right sidebar — reuses project comment panel */}
{sidebarOpen && (
<div className="w-[360px] flex flex-col border-l border-border bg-bg-secondary shrink-0">
<div className="w-full md:w-[360px] absolute inset-y-0 right-0 z-20 md:static md:inset-auto flex flex-col border-l-0 md:border-l border-border bg-bg-secondary shrink-0">
<div className="px-4 pt-3 pb-2 shrink-0">
<div className="flex items-center bg-bg-tertiary rounded-lg p-0.5">
<button onClick={() => setActiveTab('comments')} className={`flex-1 py-1.5 text-[13px] font-medium rounded-md transition-all ${activeTab === 'comments' ? 'bg-bg-hover text-text-primary shadow-sm' : 'text-text-tertiary'}`}>
Expand Down