Skip to content

Commit 6da2563

Browse files
feature: org settings (#1147)
* wip * wip * wip * wip * more settings * setup org-wide settings and more * cleanup * edge cases, handle backend and stop transcript generation, ui enable/disable and more * text * show download button for non-owners * skipped transcription status and conditionally render chapters and summary * Update _journal.json * Update pnpm-lock.yaml * switches toggled by default * review points * cleanup * better messaging * description abit too long * fix label * make sure dialog resets, fix pro text color * Update SharedCaps.tsx --------- Co-authored-by: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com>
1 parent e78028b commit 6da2563

43 files changed

Lines changed: 1331 additions & 618 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"use server";
2+
3+
import { db } from "@cap/database";
4+
import { getCurrentUser } from "@cap/database/auth/session";
5+
import { organizations } from "@cap/database/schema";
6+
import { eq } from "drizzle-orm";
7+
import { revalidatePath } from "next/cache";
8+
9+
export async function updateOrganizationSettings(settings: {
10+
disableSummary?: boolean;
11+
disableCaptions?: boolean;
12+
disableChapters?: boolean;
13+
disableReactions?: boolean;
14+
disableTranscript?: boolean;
15+
disableComments?: boolean;
16+
}) {
17+
const user = await getCurrentUser();
18+
19+
if (!user) {
20+
throw new Error("Unauthorized");
21+
}
22+
23+
if (!settings) {
24+
throw new Error("Settings are required");
25+
}
26+
27+
const [organization] = await db()
28+
.select()
29+
.from(organizations)
30+
.where(eq(organizations.id, user.activeOrganizationId));
31+
32+
if (!organization) {
33+
throw new Error("Organization not found");
34+
}
35+
36+
await db()
37+
.update(organizations)
38+
.set({ settings })
39+
.where(eq(organizations.id, user.activeOrganizationId));
40+
41+
revalidatePath("/dashboard/caps");
42+
43+
return { success: true };
44+
}

apps/web/actions/videos/generate-ai-metadata.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ export async function generateAiMetadata(
4141
const updatedAtTime = new Date(videoData.updatedAt).getTime();
4242
const currentTime = new Date().getTime();
4343
const tenMinutesInMs = 10 * 60 * 1000;
44-
const minutesElapsed = Math.round((currentTime - updatedAtTime) / 60000);
4544

4645
if (currentTime - updatedAtTime > tenMinutesInMs) {
4746
await db()

apps/web/actions/videos/get-status.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ import { generateAiMetadata } from "./generate-ai-metadata";
1414

1515
const MAX_AI_PROCESSING_TIME = 10 * 60 * 1000;
1616

17+
type TranscriptionStatus = "PROCESSING" | "COMPLETE" | "ERROR" | "SKIPPED";
18+
1719
export interface VideoStatusResult {
18-
transcriptionStatus: "PROCESSING" | "COMPLETE" | "ERROR" | null;
20+
transcriptionStatus: TranscriptionStatus | null;
1921
aiTitle: string | null;
2022
aiProcessing: boolean;
2123
summary: string | null;
@@ -124,10 +126,7 @@ export async function getVideoStatus(
124126

125127
return {
126128
transcriptionStatus:
127-
(updatedVideo.transcriptionStatus as
128-
| "PROCESSING"
129-
| "COMPLETE"
130-
| "ERROR") || null,
129+
(updatedVideo.transcriptionStatus as TranscriptionStatus) || null,
131130
aiProcessing: false,
132131
aiTitle: updatedMetadata.aiTitle || null,
133132
summary: updatedMetadata.summary || null,
@@ -214,8 +213,7 @@ export async function getVideoStatus(
214213

215214
return {
216215
transcriptionStatus:
217-
(video.transcriptionStatus as "PROCESSING" | "COMPLETE" | "ERROR") ||
218-
null,
216+
(video.transcriptionStatus as TranscriptionStatus) || null,
219217
aiProcessing: true,
220218
aiTitle: metadata.aiTitle || null,
221219
summary: metadata.summary || null,
@@ -232,8 +230,7 @@ export async function getVideoStatus(
232230

233231
return {
234232
transcriptionStatus:
235-
(video.transcriptionStatus as "PROCESSING" | "COMPLETE" | "ERROR") ||
236-
null,
233+
(video.transcriptionStatus as TranscriptionStatus) || null,
237234
aiProcessing: metadata.aiProcessing || false,
238235
aiTitle: metadata.aiTitle || null,
239236
summary: metadata.summary || null,
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"use server";
2+
3+
import { db } from "@cap/database";
4+
import { getCurrentUser } from "@cap/database/auth/session";
5+
import { videos } from "@cap/database/schema";
6+
import type { Video } from "@cap/web-domain";
7+
import { eq } from "drizzle-orm";
8+
9+
export async function updateVideoSettings(
10+
videoId: Video.VideoId,
11+
videoSettings: {
12+
disableSummary?: boolean;
13+
disableCaptions?: boolean;
14+
disableChapters?: boolean;
15+
disableReactions?: boolean;
16+
disableTranscript?: boolean;
17+
disableComments?: boolean;
18+
},
19+
) {
20+
const user = await getCurrentUser();
21+
22+
if (!user || !videoId || !videoSettings) {
23+
throw new Error("Missing required data for updating video settings");
24+
}
25+
26+
const [video] = await db()
27+
.select()
28+
.from(videos)
29+
.where(eq(videos.id, videoId));
30+
31+
if (!video) {
32+
throw new Error("Video not found for updating video settings");
33+
}
34+
35+
if (video.ownerId !== user.id) {
36+
throw new Error("You don't have permission to update this video settings");
37+
}
38+
39+
await db()
40+
.update(videos)
41+
.set({ settings: videoSettings })
42+
.where(eq(videos.id, videoId));
43+
44+
return { success: true };
45+
}

apps/web/app/(org)/dashboard/Contexts.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,17 @@ import Cookies from "js-cookie";
66
import { usePathname } from "next/navigation";
77
import { createContext, useContext, useEffect, useState } from "react";
88
import { UpgradeModal } from "@/components/UpgradeModal";
9-
import type { Organization, Spaces, UserPreferences } from "./dashboard-data";
9+
import type {
10+
Organization,
11+
OrganizationSettings,
12+
Spaces,
13+
UserPreferences,
14+
} from "./dashboard-data";
1015

1116
type SharedContext = {
1217
organizationData: Organization[] | null;
1318
activeOrganization: Organization | null;
19+
organizationSettings: OrganizationSettings | null;
1420
spacesData: Spaces[] | null;
1521
userSpaces: Spaces[] | null;
1622
sharedSpaces: Spaces[] | null;
@@ -50,6 +56,7 @@ export function DashboardContexts({
5056
spacesData,
5157
user,
5258
isSubscribed,
59+
organizationSettings,
5360
userPreferences,
5461
anyNewNotifications,
5562
initialTheme,
@@ -62,6 +69,7 @@ export function DashboardContexts({
6269
spacesData: SharedContext["spacesData"];
6370
user: SharedContext["user"];
6471
isSubscribed: SharedContext["isSubscribed"];
72+
organizationSettings: SharedContext["organizationSettings"];
6573
userPreferences: SharedContext["userPreferences"];
6674
anyNewNotifications: boolean;
6775
initialTheme: ITheme;
@@ -154,6 +162,7 @@ export function DashboardContexts({
154162
spacesData,
155163
anyNewNotifications,
156164
userPreferences,
165+
organizationSettings,
157166
userSpaces,
158167
sharedSpaces,
159168
activeSpace,

apps/web/app/(org)/dashboard/_components/MobileTab.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ const MobileTab = () => {
4444
}
4545
});
4646
return (
47-
<div className="flex sticky bottom-0 z-50 flex-1 justify-between items-center px-5 w-screen h-16 border-t lg:hidden border-gray-5 bg-gray-1">
47+
<div className="flex sticky bottom-0 z-50 flex-1 gap-5 justify-between items-center px-5 w-screen h-16 border-t lg:hidden border-gray-5 bg-gray-1">
4848
<AnimatePresence>
4949
{open && <OrgsMenu setOpen={setOpen} menuRef={menuRef} />}
5050
</AnimatePresence>

apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ const AdminNavItems = ({ toggleMobileNav }: Props) => {
9494
position="right"
9595
content={activeOrg?.organization.name ?? "No organization found"}
9696
>
97-
<PopoverTrigger asChild>
97+
<PopoverTrigger suppressHydrationWarning asChild>
9898
<motion.div
9999
transition={{
100100
type: "easeInOut",

apps/web/app/(org)/dashboard/caps/Caps.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { CapPagination } from "./components/CapPagination";
2424
import { EmptyCapState } from "./components/EmptyCapState";
2525
import type { FolderDataType } from "./components/Folder";
2626
import Folder from "./components/Folder";
27-
import { useUploadingContext, useUploadingStatus } from "./UploadingContext";
27+
import { useUploadingStatus } from "./UploadingContext";
2828

2929
export type VideoData = {
3030
id: Video.VideoId;

apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx

Lines changed: 62 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"use client";
2+
13
import type { VideoMetadata } from "@cap/database/types";
24
import { buildEnv, NODE_ENV } from "@cap/env";
35
import {
@@ -13,6 +15,7 @@ import {
1315
faCopy,
1416
faDownload,
1517
faEllipsis,
18+
faGear,
1619
faLink,
1720
faLock,
1821
faTrash,
@@ -40,6 +43,7 @@ import {
4043
import { useEffectMutation } from "@/lib/EffectRuntime";
4144
import { withRpc } from "@/lib/Rpcs";
4245
import { PasswordDialog } from "../PasswordDialog";
46+
import { SettingsDialog } from "../SettingsDialog";
4347
import { SharingDialog } from "../SharingDialog";
4448
import { CapCardAnalytics } from "./CapCardAnalytics";
4549
import { CapCardButton } from "./CapCardButton";
@@ -70,6 +74,14 @@ export interface CapCardProps extends PropsWithChildren {
7074
hasPassword?: boolean;
7175
hasActiveUpload: boolean | undefined;
7276
duration?: number;
77+
settings?: {
78+
disableComments?: boolean;
79+
disableSummary?: boolean;
80+
disableCaptions?: boolean;
81+
disableChapters?: boolean;
82+
disableReactions?: boolean;
83+
disableTranscript?: boolean;
84+
};
7385
};
7486
analytics: number;
7587
isLoadingAnalytics: boolean;
@@ -111,6 +123,7 @@ export const CapCard = ({
111123
);
112124
const [copyPressed, setCopyPressed] = useState(false);
113125
const [isDragging, setIsDragging] = useState(false);
126+
const [isSettingsDialogOpen, setIsSettingsDialogOpen] = useState(false);
114127
const { isSubscribed, setUpgradeModalOpen } = useDashboardContext();
115128

116129
const [confirmOpen, setConfirmOpen] = useState(false);
@@ -285,6 +298,12 @@ export const CapCard = ({
285298
onSharingUpdated={handleSharingUpdated}
286299
isPublic={cap.public}
287300
/>
301+
<SettingsDialog
302+
isOpen={isSettingsDialogOpen}
303+
settingsData={cap.settings}
304+
capId={cap.id}
305+
onClose={() => setIsSettingsDialogOpen(false)}
306+
/>
288307
<PasswordDialog
289308
isOpen={isPasswordDialogOpen}
290309
onClose={() => setIsPasswordDialogOpen(false)}
@@ -323,6 +342,31 @@ export const CapCard = ({
323342
"top-2 right-2 flex-col gap-2 z-[51]",
324343
)}
325344
>
345+
{isOwner ? (
346+
<CapCardButton
347+
tooltipContent="Settings"
348+
onClick={(e) => {
349+
e.stopPropagation();
350+
setIsSettingsDialogOpen(true);
351+
}}
352+
className="delay-0"
353+
icon={() => {
354+
return <FontAwesomeIcon className="size-4" icon={faGear} />;
355+
}}
356+
/>
357+
) : (
358+
<CapCardButton
359+
tooltipContent="Download Cap"
360+
onClick={(e) => {
361+
e.stopPropagation();
362+
handleDownload();
363+
}}
364+
className="delay-0"
365+
icon={() => (
366+
<FontAwesomeIcon className="size-4" icon={faDownload} />
367+
)}
368+
/>
369+
)}
326370
<CapCardButton
327371
tooltipContent="Copy link"
328372
onClick={(e) => {
@@ -363,54 +407,10 @@ export const CapCard = ({
363407
);
364408
}}
365409
/>
366-
<CapCardButton
367-
tooltipContent="Download Cap"
368-
onClick={(e) => {
369-
e.stopPropagation();
370-
handleDownload();
371-
}}
372-
disabled={
373-
downloadMutation.isPending ||
374-
(enableBetaUploadProgress && cap.hasActiveUpload)
375-
}
376-
className="delay-25"
377-
icon={() => {
378-
return downloadMutation.isPending ? (
379-
<div className="animate-spin size-3">
380-
<svg
381-
className="size-3"
382-
xmlns="http://www.w3.org/2000/svg"
383-
fill="none"
384-
viewBox="0 0 24 24"
385-
aria-hidden="true"
386-
>
387-
<circle
388-
className="opacity-25"
389-
cx="12"
390-
cy="12"
391-
r="10"
392-
stroke="currentColor"
393-
strokeWidth="4"
394-
></circle>
395-
<path
396-
className="opacity-75"
397-
fill="currentColor"
398-
d="m2 12c0-5.523 4.477-10 10-10v3c-3.866 0-7 3.134-7 7s3.134 7 7 7 7-3.134 7-7c0-1.457-.447-2.808-1.208-3.926l2.4-1.6c1.131 1.671 1.808 3.677 1.808 5.526 0 5.523-4.477 10-10 10s-10-4.477-10-10z"
399-
></path>
400-
</svg>
401-
</div>
402-
) : (
403-
<FontAwesomeIcon
404-
className="text-gray-12 size-3"
405-
icon={faDownload}
406-
/>
407-
);
408-
}}
409-
/>
410410

411411
{isOwner && (
412412
<DropdownMenu modal={false} onOpenChange={setIsDropdownOpen}>
413-
<DropdownMenuTrigger asChild>
413+
<DropdownMenuTrigger asChild suppressHydrationWarning>
414414
<div>
415415
<CapCardButton
416416
tooltipContent="More options"
@@ -421,7 +421,21 @@ export const CapCard = ({
421421
/>
422422
</div>
423423
</DropdownMenuTrigger>
424-
<DropdownMenuContent align="end" sideOffset={5}>
424+
<DropdownMenuContent
425+
align="end"
426+
sideOffset={5}
427+
suppressHydrationWarning
428+
>
429+
<DropdownMenuItem
430+
onClick={(e) => {
431+
e.stopPropagation();
432+
handleDownload();
433+
}}
434+
className="flex gap-2 items-center rounded-lg"
435+
>
436+
<FontAwesomeIcon className="size-3" icon={faDownload} />
437+
<p className="text-sm text-gray-12">Download</p>
438+
</DropdownMenuItem>
425439
<DropdownMenuItem
426440
onClick={() => {
427441
toast.promise(duplicateMutation.mutateAsync(), {
@@ -522,8 +536,8 @@ export const CapCard = ({
522536
href={`/s/${cap.id}`}
523537
>
524538
{imageStatus !== "success" && uploadProgress ? (
525-
<div className="relative inset-0 w-full h-full z-20">
526-
<div className="overflow-hidden relative mx-auto w-full h-full rounded-t-xl border-b border-gray-3 aspect-video bg-black z-5">
539+
<div className="relative inset-0 z-20 w-full h-full">
540+
<div className="overflow-hidden relative mx-auto w-full h-full bg-black rounded-t-xl border-b border-gray-3 aspect-video z-5">
527541
<div className="flex absolute inset-0 justify-center items-center rounded-t-xl">
528542
{uploadProgress.status === "failed" ? (
529543
<div className="flex flex-col items-center">

0 commit comments

Comments
 (0)