diff --git a/templates/clips/app/components/player/share-dialog.tsx b/templates/clips/app/components/player/share-dialog.tsx index 6831d0a49a..c9393767de 100644 --- a/templates/clips/app/components/player/share-dialog.tsx +++ b/templates/clips/app/components/player/share-dialog.tsx @@ -72,6 +72,13 @@ export interface ShareRecordingPopoverProps { animatedThumbnailUrl?: string | null; isLoomRecording?: boolean; hasPassword?: boolean; + /** + * Restricts the dialog to a bare copy-link control for viewers who can + * reshare a public/org clip's link but have no edit access: it skips + * `list-resource-shares` (which returns every individually-shared + * principal's email to any reader) and hides the Invite tab entirely. + */ + viewerReshareOnly?: boolean; /** Trigger element rendered as the popover anchor (usually the Share button). */ children: ReactNode; open?: boolean; @@ -103,6 +110,7 @@ export function ShareRecordingPopover({ animatedThumbnailUrl, isLoomRecording = false, hasPassword, + viewerReshareOnly = false, children, open, onOpenChange, @@ -125,6 +133,7 @@ export function ShareRecordingPopover({ animatedThumbnailUrl={animatedThumbnailUrl} isLoomRecording={isLoomRecording} hasPassword={hasPassword} + viewerReshareOnly={viewerReshareOnly} /> @@ -148,6 +157,7 @@ export function ShareRecordingDialog({ open, onOpenChange, hasPassword, + viewerReshareOnly = false, }: ShareRecordingDialogProps) { const t = useT(); return ( @@ -168,6 +178,7 @@ export function ShareRecordingDialog({ animatedThumbnailUrl={animatedThumbnailUrl} isLoomRecording={isLoomRecording} hasPassword={hasPassword} + viewerReshareOnly={viewerReshareOnly} reserveCloseButton /> @@ -186,6 +197,7 @@ function ShareRecordingContent({ isLoomRecording = false, reserveCloseButton = false, hasPassword, + viewerReshareOnly = false, }: { recordingId: string; recordingTitle?: string; @@ -197,20 +209,34 @@ function ShareRecordingContent({ isLoomRecording?: boolean; reserveCloseButton?: boolean; hasPassword?: boolean; + viewerReshareOnly?: boolean; }) { const t = useT(); - const sharesQuery = useActionQuery("list-resource-shares", { - resourceType: "recording", - resourceId: recordingId, - }); + const sharesQuery = useActionQuery( + "list-resource-shares", + { resourceType: "recording", resourceId: recordingId }, + { enabled: !viewerReshareOnly }, + ); - const data = sharesQuery.data; + const data = viewerReshareOnly ? undefined : sharesQuery.data; const role = data?.role ?? initialRole; const canManage = role === "owner" || role === "admin"; + // Editors could always see (read-only) who a clip is shared with; only + // gate the Invite tab's mutation controls behind canManage. Commenters are + // grouped with plain viewers here -- neither can manage shares. + const canViewShares = + role === "owner" || role === "admin" || role === "editor"; const visibility = (data?.visibility as Visibility | null | undefined) ?? initialVisibility ?? null; + // A plain viewer/commenter can't produce a working embed for a non-public + // clip (they have no way to make it public), so don't dangle the tab in + // front of them only to show an "ask the owner" dead end. Owner/admin/ + // editor keep it regardless of visibility since they can flip to public + // from inside it. + const canEmbed = canViewShares || visibility === "public"; + const tabCount = 1 + (canViewShares ? 1 : 0) + (canEmbed ? 1 : 0); // Attribution `via` must be a stable non-PII id, never an email. The only // owner id available client-side is the *current* session's userId, which is @@ -234,20 +260,28 @@ function ShareRecordingContent({ defaultValue="link" className={cn("min-w-0 px-4 py-3", reserveCloseButton && "pe-12")} > - - - - {t("shareDialog.link")} - - - - {t("shareDialog.invite")} - - - - {t("shareDialog.embed")} - - + {tabCount > 1 ? ( + + + + {t("shareDialog.link")} + + {canViewShares ? ( + + + {t("shareDialog.invite")} + + ) : null} + {canEmbed ? ( + + + {t("shareDialog.embed")} + + ) : null} + + ) : null} - - - - - - - + {canViewShares ? ( + + + + ) : null} + + {canEmbed ? ( + + + + ) : null} ); @@ -311,6 +350,7 @@ function LinkTab({ animatedThumbnailUrl, isLoomRecording: isLoomRecordingProp, hasPassword, + canViewShares, }: { recordingId: string; recordingTitle?: string; @@ -323,6 +363,7 @@ function LinkTab({ animatedThumbnailUrl?: string | null; isLoomRecording?: boolean; hasPassword?: boolean; + canViewShares: boolean; }) { const t = useT(); const { setResourceVisibility, isPending } = useResourceVisibilityMutation( @@ -460,21 +501,23 @@ function LinkTab({ return (
- {visibility ? ( - setResourceVisibility(next)} - publicDescription={t("shareDialog.publicDescription")} - showDescription={false} - /> - ) : ( -
-
-
-
- )} + {canViewShares ? ( + visibility ? ( + setResourceVisibility(next)} + publicDescription={t("shareDialog.publicDescription")} + showDescription={false} + /> + ) : ( +
+
+
+
+ ) + ) : null} { if (!recording?.videoUrl) return; setDownloading(true); @@ -983,6 +995,7 @@ export default function RecordingPage() { initialVisibility={recording.visibility} initialRole={role} hasPassword={Boolean(recording.hasPassword)} + viewerReshareOnly={viewerReshareOnly} >