diff --git a/.env.example b/.env.example index c57a09b2c29..3dd78566a23 100644 --- a/.env.example +++ b/.env.example @@ -40,6 +40,9 @@ DATABASE_URL='mysql://root:@localhost:3306/planetscale' # Generate a value by running `openssl rand -hex 32` DATABASE_ENCRYPTION_KEY= +# Video privacy settings +# Set to false to make new uploaded videos private by default. If not set or set to true, videos are public by default. +# CAP_VIDEOS_DEFAULT_PUBLIC=true ## AWS/S3 diff --git a/apps/web/actions/caps/share.ts b/apps/web/actions/caps/share.ts index 86853cb5582..762f0c1a15a 100644 --- a/apps/web/actions/caps/share.ts +++ b/apps/web/actions/caps/share.ts @@ -4,17 +4,17 @@ import { revalidatePath } from 'next/cache' import { db } from "@cap/database" import { getCurrentUser } from "@cap/database/auth/session" import { sharedVideos, videos, spaces, organizationMembers, organizations, spaceVideos } from "@cap/database/schema" -import { eq, and, inArray, or } from "drizzle-orm" +import { eq, and, inArray } from "drizzle-orm" import { nanoId } from "@cap/database/helpers" interface ShareCapParams { capId: string spaceIds: string[] + public?: boolean } -export async function shareCap({ capId, spaceIds }: ShareCapParams) { +export async function shareCap({ capId, spaceIds, public: isPublic }: ShareCapParams) { try { - const user = await getCurrentUser() if (!user) { return { success: false, error: "Unauthorized" } @@ -122,6 +122,15 @@ export async function shareCap({ capId, spaceIds }: ShareCapParams) { }) } } + + // Update public status if provided + if (typeof isPublic === 'boolean') { + await db() + .update(videos) + .set({ public: isPublic }) + .where(eq(videos.id, capId)) + } + revalidatePath('/dashboard/caps') revalidatePath(`/dashboard/caps/${capId}`) return { success: true } diff --git a/apps/web/actions/video/upload.ts b/apps/web/actions/video/upload.ts index c572be02ba6..82aabdd46b7 100644 --- a/apps/web/actions/video/upload.ts +++ b/apps/web/actions/video/upload.ts @@ -224,6 +224,7 @@ export async function createVideoAndGetUploadUrl({ source: { type: "desktopMP4" as const }, isScreenshot, bucket: customBucket?.id, + public: serverEnv().CAP_VIDEOS_DEFAULT_PUBLIC, ...(folderId ? { folderId } : {}), }; diff --git a/apps/web/app/(org)/dashboard/caps/Caps.tsx b/apps/web/app/(org)/dashboard/caps/Caps.tsx index 317c5731fd9..ccd338a3111 100644 --- a/apps/web/app/(org)/dashboard/caps/Caps.tsx +++ b/apps/web/app/(org)/dashboard/caps/Caps.tsx @@ -26,6 +26,7 @@ export type VideoData = { ownerId: string; name: string; createdAt: Date; + public: boolean; totalComments: number; totalReactions: number; foldersData: FolderDataType[]; diff --git a/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx b/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx index e8185cdab88..d7a146ed39f 100644 --- a/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx +++ b/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx @@ -34,6 +34,7 @@ export interface CapCardProps extends PropsWithChildren { ownerId: string; name: string; createdAt: Date; + public?: boolean; totalComments: number; totalReactions: number; sharedOrganizations?: { @@ -256,6 +257,7 @@ export const CapCard = ({ capName={cap.name} sharedSpaces={cap.sharedSpaces || []} onSharingUpdated={handleSharingUpdated} + isPublic={cap.public} /> = ({ hideSharedStatus ? "pointer-events-none" : "cursor-pointer" ); if (isOwner && !hideSharedStatus) { - if ( - (cap.sharedOrganizations?.length === 0 || !cap.sharedOrganizations) && - (cap.sharedSpaces?.length === 0 || !cap.sharedSpaces) - ) { + const hasSpaceSharing = (cap.sharedOrganizations?.length ?? 0) > 0 || (cap.sharedSpaces?.length ?? 0) > 0; + const isPublic = cap.public; + + if (!hasSpaceSharing && !isPublic) { return (

void; + isPublic?: boolean; + spacesData?: Spaces[] | null; } export const SharingDialog: React.FC = ({ @@ -41,17 +45,22 @@ export const SharingDialog: React.FC = ({ capName, sharedSpaces, onSharingUpdated, + isPublic = false, + spacesData: propSpacesData = null, }) => { - const { spacesData } = useDashboardContext(); + const { spacesData: contextSpacesData } = useDashboardContext(); + const spacesData = propSpacesData || contextSpacesData; const [selectedSpaces, setSelectedSpaces] = useState>(new Set()); const [searchTerm, setSearchTerm] = useState(""); const [initialSelectedSpaces, setInitialSelectedSpaces] = useState< Set >(new Set()); const [loading, setLoading] = useState(false); - const tabs = ["Share to space", "Embed"] as const; + const [publicToggle, setPublicToggle] = useState(isPublic); + const [initialPublicState, setInitialPublicState] = useState(isPublic); + const tabs = ["Share", "Embed"] as const; const [activeTab, setActiveTab] = - useState<(typeof tabs)[number]>("Share to space"); + useState<(typeof tabs)[number]>("Share"); const sharedSpaceIds = new Set(sharedSpaces?.map((space) => space.id) || []); @@ -60,10 +69,12 @@ export const SharingDialog: React.FC = ({ const spaceIds = new Set(sharedSpaces.map((space) => space.id)); setSelectedSpaces(spaceIds); setInitialSelectedSpaces(spaceIds); + setPublicToggle(isPublic); + setInitialPublicState(isPublic); setSearchTerm(""); setActiveTab(tabs[0]); } - }, [isOpen, sharedSpaces]); + }, [isOpen, sharedSpaces, isPublic]); const isSpaceSharedViaOrganization = useCallback( (spaceId: string) => { @@ -92,6 +103,7 @@ export const SharingDialog: React.FC = ({ const result = await shareCap({ capId, spaceIds: Array.from(selectedSpaces), + public: publicToggle, }); if (!result.success) { @@ -108,22 +120,26 @@ export const SharingDialog: React.FC = ({ (id) => !newSelectedSpaces.includes(id) ); + const publicChanged = publicToggle !== initialPublicState; + const getSpaceName = (id: string) => { const space = spacesData?.find((space) => space.id === id); return space?.name || `Space ${id}`; }; - if (addedSpaceIds.length === 1 && removedSpaceIds.length === 0) { + if (publicChanged && addedSpaceIds.length === 0 && removedSpaceIds.length === 0) { + toast.success(publicToggle ? "Video is now public" : "Video is now private"); + } else if (addedSpaceIds.length === 1 && removedSpaceIds.length === 0 && !publicChanged) { toast.success(`Shared to ${getSpaceName(addedSpaceIds[0] as string)}`); - } else if (removedSpaceIds.length === 1 && addedSpaceIds.length === 0) { + } else if (removedSpaceIds.length === 1 && addedSpaceIds.length === 0 && !publicChanged) { toast.success( `Unshared from ${getSpaceName(removedSpaceIds[0] as string)}` ); - } else if (addedSpaceIds.length > 0 && removedSpaceIds.length === 0) { + } else if (addedSpaceIds.length > 0 && removedSpaceIds.length === 0 && !publicChanged) { toast.success(`Shared to ${addedSpaceIds.length} spaces`); - } else if (removedSpaceIds.length > 0 && addedSpaceIds.length === 0) { + } else if (removedSpaceIds.length > 0 && addedSpaceIds.length === 0 && !publicChanged) { toast.success(`Unshared from ${removedSpaceIds.length} spaces`); - } else if (addedSpaceIds.length > 0 && removedSpaceIds.length > 0) { + } else if (addedSpaceIds.length > 0 || removedSpaceIds.length > 0 || publicChanged) { toast.success(`Sharing settings updated`); } else { toast.info("No changes to sharing settings"); @@ -139,7 +155,7 @@ export const SharingDialog: React.FC = ({ const handleCopyEmbedCode = async () => { const embedCode = `

`; @@ -151,11 +167,24 @@ export const SharingDialog: React.FC = ({ } }; + // Separate organization entries from real spaces + const organizationEntries = spacesData?.filter((space) => + space.id === space.organizationId && space.primary === true + ) || []; + + const realSpaces = spacesData?.filter((space) => + !(space.id === space.organizationId && space.primary === true) + ) || []; + + const allShareableItems = [...organizationEntries, ...realSpaces]; + const filteredSpaces = searchTerm - ? spacesData?.filter((space) => + ? allShareableItems.filter((space) => space.name.toLowerCase().includes(searchTerm.toLowerCase()) ) - : spacesData; + : allShareableItems; + + return ( @@ -163,13 +192,13 @@ export const SharingDialog: React.FC = ({ } description={ - activeTab === "Share to space" - ? "Select the spaces you would like to share with" + activeTab === "Share" + ? "Select how you would like to share the cap" : "Copy the embed code to share your cap" } > - {activeTab === "Share to space" + {activeTab === "Share" ? `Share ${capName}` : `Embed ${capName}`} @@ -202,12 +231,29 @@ export const SharingDialog: React.FC = ({
- {activeTab === "Share to space" ? ( + {activeTab === "Share" ? ( <> + {/* Public sharing toggle */} +
+
+
+ +
+
+

Anyone with the link

+

{publicToggle ? 'Anyone on the internet with the link can view': 'Only people with access can view'}

+
+
+ +
+
setSearchTerm(e.target.value)} @@ -233,7 +279,7 @@ export const SharingDialog: React.FC = ({ ) : (

- {spacesData && spacesData.length > 0 + {allShareableItems && allShareableItems.length > 0 ? "No spaces match your search" : "No spaces available"}

@@ -246,7 +292,7 @@ export const SharingDialog: React.FC = ({
{`
`}
@@ -264,7 +310,7 @@ export const SharingDialog: React.FC = ({
- {activeTab === "Share to space" ? ( + {activeTab === "Share" ? ( <>