feat: add setting to automatic start recording (#21853)
* feat: disable transcription setting in cal video * feat: add enable automatic recording feature * chore: only when organizer joins * chore: update name of variable * fix: improvements * chore: improvements * chore: add badge * refactor: cal video premium * tests: add tests for cal video * fix: use isOrganizer * Simplify type to remove 'as SessionUser' from new code --------- Co-authored-by: Benny Joo <sldisek783@gmail.com> Co-authored-by: Alex van Andel <me@alexvanandel.com>
This commit is contained in:
co-authored by
Benny Joo
Alex van Andel
parent
8ee7b12c2b
commit
10bf982028
@@ -23,6 +23,7 @@ type CalVideoSettings = {
|
||||
disableRecordingForGuests: boolean;
|
||||
disableRecordingForOrganizer: boolean;
|
||||
enableAutomaticTranscription: boolean;
|
||||
enableAutomaticRecordingForOrganizer: boolean;
|
||||
disableTranscriptionForGuests: boolean;
|
||||
disableTranscriptionForOrganizer: boolean;
|
||||
};
|
||||
@@ -59,6 +60,21 @@ const shouldEnableAutomaticTranscription = ({
|
||||
return !!calVideoSettings.enableAutomaticTranscription;
|
||||
};
|
||||
|
||||
const shouldEnableAutomaticRecording = ({
|
||||
hasTeamPlan,
|
||||
calVideoSettings,
|
||||
isOrganizer,
|
||||
}: {
|
||||
hasTeamPlan: boolean;
|
||||
calVideoSettings?: CalVideoSettings | null;
|
||||
isOrganizer: boolean;
|
||||
}) => {
|
||||
if (!hasTeamPlan || !isOrganizer) return false;
|
||||
if (!calVideoSettings) return false;
|
||||
|
||||
return !!calVideoSettings.enableAutomaticRecordingForOrganizer;
|
||||
};
|
||||
|
||||
const shouldEnableTranscriptionButton = ({
|
||||
hasTeamPlan,
|
||||
calVideoSettings,
|
||||
@@ -239,17 +255,24 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const showRecordingButton = shouldEnableRecordButton({
|
||||
hasTeamPlan: !!hasTeamPlan,
|
||||
calVideoSettings: bookingObj.eventType?.calVideoSettings,
|
||||
isOrganizer: sessionUserId === bookingObj.user?.id,
|
||||
isOrganizer,
|
||||
});
|
||||
|
||||
const enableAutomaticTranscription = shouldEnableAutomaticTranscription({
|
||||
hasTeamPlan: !!hasTeamPlan,
|
||||
calVideoSettings: bookingObj.eventType?.calVideoSettings,
|
||||
});
|
||||
|
||||
const enableAutomaticRecordingForOrganizer = shouldEnableAutomaticRecording({
|
||||
hasTeamPlan: !!hasTeamPlan,
|
||||
calVideoSettings: bookingObj.eventType?.calVideoSettings,
|
||||
isOrganizer,
|
||||
});
|
||||
|
||||
const showTranscriptionButton = shouldEnableTranscriptionButton({
|
||||
hasTeamPlan: !!hasTeamPlan,
|
||||
calVideoSettings: bookingObj.eventType?.calVideoSettings,
|
||||
isOrganizer: sessionUserId === bookingObj.user?.id,
|
||||
isOrganizer,
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -274,6 +297,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
loggedInUserName: sessionUserId ? session?.user?.name : undefined,
|
||||
showRecordingButton,
|
||||
enableAutomaticTranscription,
|
||||
enableAutomaticRecordingForOrganizer,
|
||||
showTranscriptionButton,
|
||||
rediectAttendeeToOnExit: isOrganizer
|
||||
? undefined
|
||||
|
||||
@@ -0,0 +1,492 @@
|
||||
import type { DailyCall } from "@daily-co/daily-js";
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
import { BUTTONS } from "../button-states";
|
||||
import { createCalVideoCallbacks } from "../cal-video-premium-features";
|
||||
|
||||
vi.mock("@calcom/lib/constants", () => ({
|
||||
TRANSCRIPTION_STARTED_ICON: "/transcription-started-icon.svg",
|
||||
RECORDING_IN_PROGRESS_ICON: "/recording-in-progress-icon.svg",
|
||||
TRANSCRIPTION_STOPPED_ICON: "/transcription-stopped-icon.svg",
|
||||
RECORDING_DEFAULT_ICON: "/recording-default-icon.svg",
|
||||
}));
|
||||
|
||||
const mockDaily: DailyCall = {
|
||||
startRecording: vi.fn(),
|
||||
stopRecording: vi.fn(),
|
||||
startTranscription: vi.fn(),
|
||||
stopTranscription: vi.fn(),
|
||||
updateCustomTrayButtons: vi.fn(),
|
||||
} as unknown as DailyCall;
|
||||
|
||||
const createMockRecording = (isRecording = false) => ({
|
||||
isRecording,
|
||||
});
|
||||
|
||||
const createMockTranscription = (isTranscribing = false) => ({
|
||||
isTranscribing,
|
||||
});
|
||||
|
||||
describe("CalVideoPremiumFeatures - End-to-End Callback Tests", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("onMeetingJoined", () => {
|
||||
it("should start transcription automatically when enabled and not already transcribing", () => {
|
||||
const callbacks = createCalVideoCallbacks({
|
||||
daily: mockDaily,
|
||||
recording: createMockRecording(),
|
||||
transcription: createMockTranscription(false), // not transcribing
|
||||
showRecordingButton: true,
|
||||
showTranscriptionButton: true,
|
||||
enableAutomaticTranscription: true,
|
||||
enableAutomaticRecordingForOrganizer: false,
|
||||
});
|
||||
|
||||
callbacks.onMeetingJoined();
|
||||
|
||||
expect(mockDaily.startTranscription).toHaveBeenCalledTimes(1);
|
||||
expect(mockDaily.startRecording).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should start recording automatically when enabled and not already recording", () => {
|
||||
const callbacks = createCalVideoCallbacks({
|
||||
daily: mockDaily,
|
||||
recording: createMockRecording(false), // not recording
|
||||
transcription: createMockTranscription(),
|
||||
showRecordingButton: true,
|
||||
showTranscriptionButton: true,
|
||||
enableAutomaticTranscription: false,
|
||||
enableAutomaticRecordingForOrganizer: true,
|
||||
});
|
||||
|
||||
callbacks.onMeetingJoined();
|
||||
|
||||
expect(mockDaily.startRecording).toHaveBeenCalledTimes(1);
|
||||
expect(mockDaily.startRecording).toHaveBeenCalledWith({
|
||||
videoBitrate: 2000,
|
||||
});
|
||||
expect(mockDaily.startTranscription).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should start both transcription and recording when both are enabled", () => {
|
||||
const callbacks = createCalVideoCallbacks({
|
||||
daily: mockDaily,
|
||||
recording: createMockRecording(false),
|
||||
transcription: createMockTranscription(false),
|
||||
showRecordingButton: true,
|
||||
showTranscriptionButton: true,
|
||||
enableAutomaticTranscription: true,
|
||||
enableAutomaticRecordingForOrganizer: true,
|
||||
});
|
||||
|
||||
callbacks.onMeetingJoined();
|
||||
|
||||
expect(mockDaily.startTranscription).toHaveBeenCalledTimes(1);
|
||||
expect(mockDaily.startRecording).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should not start transcription when already transcribing", () => {
|
||||
const callbacks = createCalVideoCallbacks({
|
||||
daily: mockDaily,
|
||||
recording: createMockRecording(),
|
||||
transcription: createMockTranscription(true), // already transcribing
|
||||
showRecordingButton: true,
|
||||
showTranscriptionButton: true,
|
||||
enableAutomaticTranscription: true,
|
||||
enableAutomaticRecordingForOrganizer: false,
|
||||
});
|
||||
|
||||
callbacks.onMeetingJoined();
|
||||
|
||||
expect(mockDaily.startTranscription).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not start recording when already recording", () => {
|
||||
const callbacks = createCalVideoCallbacks({
|
||||
daily: mockDaily,
|
||||
recording: createMockRecording(true), // already recording
|
||||
transcription: createMockTranscription(),
|
||||
showRecordingButton: true,
|
||||
showTranscriptionButton: true,
|
||||
enableAutomaticTranscription: false,
|
||||
enableAutomaticRecordingForOrganizer: true,
|
||||
});
|
||||
|
||||
callbacks.onMeetingJoined();
|
||||
|
||||
expect(mockDaily.startRecording).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("onCustomButtonClick - Recording Button", () => {
|
||||
it("should start recording when click occurs on Recording button and recording is stopped", async () => {
|
||||
const callbacks = createCalVideoCallbacks({
|
||||
daily: mockDaily,
|
||||
recording: createMockRecording(false), // not recording
|
||||
transcription: createMockTranscription(),
|
||||
showRecordingButton: true,
|
||||
showTranscriptionButton: true,
|
||||
enableAutomaticTranscription: false,
|
||||
enableAutomaticRecordingForOrganizer: false,
|
||||
});
|
||||
|
||||
await callbacks.onCustomButtonClick({ button_id: "recording" });
|
||||
|
||||
expect(mockDaily.startRecording).toHaveBeenCalledTimes(1);
|
||||
expect(mockDaily.startRecording).toHaveBeenCalledWith({
|
||||
videoBitrate: 2000,
|
||||
});
|
||||
expect(mockDaily.stopRecording).not.toHaveBeenCalled();
|
||||
expect(mockDaily.updateCustomTrayButtons).toHaveBeenCalledWith({
|
||||
recording: BUTTONS.WAIT_FOR_RECORDING_TO_START,
|
||||
transcription: BUTTONS.START_TRANSCRIPTION, // current state included
|
||||
});
|
||||
});
|
||||
|
||||
it("should stop recording when click occurs on Recording button and recording is started", async () => {
|
||||
const callbacks = createCalVideoCallbacks({
|
||||
daily: mockDaily,
|
||||
recording: createMockRecording(true), // currently recording
|
||||
transcription: createMockTranscription(),
|
||||
showRecordingButton: true,
|
||||
showTranscriptionButton: true,
|
||||
enableAutomaticTranscription: false,
|
||||
enableAutomaticRecordingForOrganizer: false,
|
||||
});
|
||||
|
||||
await callbacks.onCustomButtonClick({ button_id: "recording" });
|
||||
|
||||
expect(mockDaily.stopRecording).toHaveBeenCalledTimes(1);
|
||||
expect(mockDaily.startRecording).not.toHaveBeenCalled();
|
||||
expect(mockDaily.updateCustomTrayButtons).toHaveBeenCalledWith({
|
||||
recording: BUTTONS.WAIT_FOR_RECORDING_TO_STOP,
|
||||
transcription: BUTTONS.START_TRANSCRIPTION, // current state included
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("onCustomButtonClick - Transcription Button", () => {
|
||||
it("should start transcription when click occurs on Transcription button and transcription is stopped", async () => {
|
||||
const callbacks = createCalVideoCallbacks({
|
||||
daily: mockDaily,
|
||||
recording: createMockRecording(),
|
||||
transcription: createMockTranscription(false), // not transcribing
|
||||
showRecordingButton: true,
|
||||
showTranscriptionButton: true,
|
||||
enableAutomaticTranscription: false,
|
||||
enableAutomaticRecordingForOrganizer: false,
|
||||
});
|
||||
|
||||
await callbacks.onCustomButtonClick({ button_id: "transcription" });
|
||||
|
||||
expect(mockDaily.startTranscription).toHaveBeenCalledTimes(1);
|
||||
expect(mockDaily.stopTranscription).not.toHaveBeenCalled();
|
||||
expect(mockDaily.updateCustomTrayButtons).toHaveBeenCalledWith({
|
||||
recording: BUTTONS.START_RECORDING, // current state included
|
||||
transcription: BUTTONS.WAIT_FOR_TRANSCRIPTION_TO_START,
|
||||
});
|
||||
});
|
||||
|
||||
it("should stop transcription when click occurs on Transcription button and transcription is started", async () => {
|
||||
const callbacks = createCalVideoCallbacks({
|
||||
daily: mockDaily,
|
||||
recording: createMockRecording(),
|
||||
transcription: createMockTranscription(true), // currently transcribing
|
||||
showRecordingButton: true,
|
||||
showTranscriptionButton: true,
|
||||
enableAutomaticTranscription: false,
|
||||
enableAutomaticRecordingForOrganizer: false,
|
||||
});
|
||||
|
||||
await callbacks.onCustomButtonClick({ button_id: "transcription" });
|
||||
|
||||
expect(mockDaily.stopTranscription).toHaveBeenCalledTimes(1);
|
||||
expect(mockDaily.startTranscription).not.toHaveBeenCalled();
|
||||
expect(mockDaily.updateCustomTrayButtons).toHaveBeenCalledWith({
|
||||
recording: BUTTONS.START_RECORDING, // current state included
|
||||
transcription: BUTTONS.WAIT_FOR_TRANSCRIPTION_TO_STOP,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("onRecordingStarted", () => {
|
||||
it("should update custom tray buttons to show stop recording button", () => {
|
||||
const callbacks = createCalVideoCallbacks({
|
||||
daily: mockDaily,
|
||||
recording: createMockRecording(),
|
||||
transcription: createMockTranscription(),
|
||||
showRecordingButton: true,
|
||||
showTranscriptionButton: true,
|
||||
enableAutomaticTranscription: false,
|
||||
enableAutomaticRecordingForOrganizer: false,
|
||||
});
|
||||
|
||||
callbacks.onRecordingStarted();
|
||||
|
||||
expect(mockDaily.updateCustomTrayButtons).toHaveBeenCalledWith({
|
||||
recording: BUTTONS.STOP_RECORDING,
|
||||
transcription: BUTTONS.START_TRANSCRIPTION, // current state included
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("onRecordingStopped", () => {
|
||||
it("should update custom tray buttons to show start recording button", () => {
|
||||
const callbacks = createCalVideoCallbacks({
|
||||
daily: mockDaily,
|
||||
recording: createMockRecording(),
|
||||
transcription: createMockTranscription(),
|
||||
showRecordingButton: true,
|
||||
showTranscriptionButton: true,
|
||||
enableAutomaticTranscription: false,
|
||||
enableAutomaticRecordingForOrganizer: false,
|
||||
});
|
||||
|
||||
callbacks.onRecordingStopped();
|
||||
|
||||
expect(mockDaily.updateCustomTrayButtons).toHaveBeenCalledWith({
|
||||
recording: BUTTONS.START_RECORDING,
|
||||
transcription: BUTTONS.START_TRANSCRIPTION, // current state included
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("onTranscriptionStarted", () => {
|
||||
it("should update custom tray buttons to show stop transcription button", () => {
|
||||
const callbacks = createCalVideoCallbacks({
|
||||
daily: mockDaily,
|
||||
recording: createMockRecording(),
|
||||
transcription: createMockTranscription(),
|
||||
showRecordingButton: true,
|
||||
showTranscriptionButton: true,
|
||||
enableAutomaticTranscription: false,
|
||||
enableAutomaticRecordingForOrganizer: false,
|
||||
});
|
||||
|
||||
callbacks.onTranscriptionStarted();
|
||||
|
||||
expect(mockDaily.updateCustomTrayButtons).toHaveBeenCalledWith({
|
||||
recording: BUTTONS.START_RECORDING, // current state included
|
||||
transcription: BUTTONS.STOP_TRANSCRIPTION,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("onTranscriptionStopped", () => {
|
||||
it("should update custom tray buttons to show start transcription button", () => {
|
||||
const callbacks = createCalVideoCallbacks({
|
||||
daily: mockDaily,
|
||||
recording: createMockRecording(),
|
||||
transcription: createMockTranscription(),
|
||||
showRecordingButton: true,
|
||||
showTranscriptionButton: true,
|
||||
enableAutomaticTranscription: false,
|
||||
enableAutomaticRecordingForOrganizer: false,
|
||||
});
|
||||
|
||||
callbacks.onTranscriptionStopped();
|
||||
|
||||
expect(mockDaily.updateCustomTrayButtons).toHaveBeenCalledWith({
|
||||
recording: BUTTONS.START_RECORDING, // current state included
|
||||
transcription: BUTTONS.START_TRANSCRIPTION,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("toggleRecording", () => {
|
||||
it("should start recording when not currently recording", async () => {
|
||||
const callbacks = createCalVideoCallbacks({
|
||||
daily: mockDaily,
|
||||
recording: createMockRecording(false), // not recording
|
||||
transcription: createMockTranscription(),
|
||||
showRecordingButton: true,
|
||||
showTranscriptionButton: true,
|
||||
enableAutomaticTranscription: false,
|
||||
enableAutomaticRecordingForOrganizer: false,
|
||||
});
|
||||
|
||||
await callbacks.toggleRecording();
|
||||
|
||||
expect(mockDaily.startRecording).toHaveBeenCalledTimes(1);
|
||||
expect(mockDaily.startRecording).toHaveBeenCalledWith({
|
||||
videoBitrate: 2000,
|
||||
});
|
||||
expect(mockDaily.stopRecording).not.toHaveBeenCalled();
|
||||
expect(mockDaily.updateCustomTrayButtons).toHaveBeenCalledWith({
|
||||
recording: BUTTONS.WAIT_FOR_RECORDING_TO_START,
|
||||
transcription: BUTTONS.START_TRANSCRIPTION, // current state included
|
||||
});
|
||||
});
|
||||
|
||||
it("should stop recording when currently recording", async () => {
|
||||
const callbacks = createCalVideoCallbacks({
|
||||
daily: mockDaily,
|
||||
recording: createMockRecording(true), // currently recording
|
||||
transcription: createMockTranscription(),
|
||||
showRecordingButton: true,
|
||||
showTranscriptionButton: true,
|
||||
enableAutomaticTranscription: false,
|
||||
enableAutomaticRecordingForOrganizer: false,
|
||||
});
|
||||
|
||||
await callbacks.toggleRecording();
|
||||
|
||||
expect(mockDaily.stopRecording).toHaveBeenCalledTimes(1);
|
||||
expect(mockDaily.startRecording).not.toHaveBeenCalled();
|
||||
expect(mockDaily.updateCustomTrayButtons).toHaveBeenCalledWith({
|
||||
recording: BUTTONS.WAIT_FOR_RECORDING_TO_STOP,
|
||||
transcription: BUTTONS.START_TRANSCRIPTION, // current state included
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("toggleTranscription", () => {
|
||||
it("should start transcription when not currently transcribing", async () => {
|
||||
const callbacks = createCalVideoCallbacks({
|
||||
daily: mockDaily,
|
||||
recording: createMockRecording(),
|
||||
transcription: createMockTranscription(false), // not transcribing
|
||||
showRecordingButton: true,
|
||||
showTranscriptionButton: true,
|
||||
enableAutomaticTranscription: false,
|
||||
enableAutomaticRecordingForOrganizer: false,
|
||||
});
|
||||
|
||||
await callbacks.toggleTranscription();
|
||||
|
||||
expect(mockDaily.startTranscription).toHaveBeenCalledTimes(1);
|
||||
expect(mockDaily.stopTranscription).not.toHaveBeenCalled();
|
||||
expect(mockDaily.updateCustomTrayButtons).toHaveBeenCalledWith({
|
||||
recording: BUTTONS.START_RECORDING, // current state included
|
||||
transcription: BUTTONS.WAIT_FOR_TRANSCRIPTION_TO_START,
|
||||
});
|
||||
});
|
||||
|
||||
it("should stop transcription when currently transcribing", async () => {
|
||||
const callbacks = createCalVideoCallbacks({
|
||||
daily: mockDaily,
|
||||
recording: createMockRecording(),
|
||||
transcription: createMockTranscription(true), // currently transcribing
|
||||
showRecordingButton: true,
|
||||
showTranscriptionButton: true,
|
||||
enableAutomaticTranscription: false,
|
||||
enableAutomaticRecordingForOrganizer: false,
|
||||
});
|
||||
|
||||
await callbacks.toggleTranscription();
|
||||
|
||||
expect(mockDaily.stopTranscription).toHaveBeenCalledTimes(1);
|
||||
expect(mockDaily.startTranscription).not.toHaveBeenCalled();
|
||||
expect(mockDaily.updateCustomTrayButtons).toHaveBeenCalledWith({
|
||||
recording: BUTTONS.START_RECORDING, // current state included
|
||||
transcription: BUTTONS.WAIT_FOR_TRANSCRIPTION_TO_STOP,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateCustomTrayButtons", () => {
|
||||
it("should update both recording and transcription buttons when both are shown", () => {
|
||||
const callbacks = createCalVideoCallbacks({
|
||||
daily: mockDaily,
|
||||
recording: createMockRecording(false),
|
||||
transcription: createMockTranscription(false),
|
||||
showRecordingButton: true,
|
||||
showTranscriptionButton: true,
|
||||
enableAutomaticTranscription: false,
|
||||
enableAutomaticRecordingForOrganizer: false,
|
||||
});
|
||||
|
||||
callbacks.updateCustomTrayButtons({
|
||||
recording: BUTTONS.STOP_RECORDING,
|
||||
transcription: BUTTONS.STOP_TRANSCRIPTION,
|
||||
});
|
||||
|
||||
expect(mockDaily.updateCustomTrayButtons).toHaveBeenCalledWith({
|
||||
recording: BUTTONS.STOP_RECORDING,
|
||||
transcription: BUTTONS.STOP_TRANSCRIPTION,
|
||||
});
|
||||
});
|
||||
|
||||
it("should update only recording button when transcription button is not shown", () => {
|
||||
const callbacks = createCalVideoCallbacks({
|
||||
daily: mockDaily,
|
||||
recording: createMockRecording(false),
|
||||
transcription: createMockTranscription(false),
|
||||
showRecordingButton: true,
|
||||
showTranscriptionButton: false,
|
||||
enableAutomaticTranscription: false,
|
||||
enableAutomaticRecordingForOrganizer: false,
|
||||
});
|
||||
|
||||
callbacks.updateCustomTrayButtons({
|
||||
recording: BUTTONS.STOP_RECORDING,
|
||||
});
|
||||
|
||||
expect(mockDaily.updateCustomTrayButtons).toHaveBeenCalledWith({
|
||||
recording: BUTTONS.STOP_RECORDING,
|
||||
});
|
||||
});
|
||||
|
||||
it("should update only transcription button when recording button is not shown", () => {
|
||||
const callbacks = createCalVideoCallbacks({
|
||||
daily: mockDaily,
|
||||
recording: createMockRecording(false),
|
||||
transcription: createMockTranscription(false),
|
||||
showRecordingButton: false,
|
||||
showTranscriptionButton: true,
|
||||
enableAutomaticTranscription: false,
|
||||
enableAutomaticRecordingForOrganizer: false,
|
||||
});
|
||||
|
||||
callbacks.updateCustomTrayButtons({
|
||||
transcription: BUTTONS.STOP_TRANSCRIPTION,
|
||||
});
|
||||
|
||||
expect(mockDaily.updateCustomTrayButtons).toHaveBeenCalledWith({
|
||||
transcription: BUTTONS.STOP_TRANSCRIPTION,
|
||||
});
|
||||
});
|
||||
|
||||
it("should use current state when no override is provided", () => {
|
||||
const callbacks = createCalVideoCallbacks({
|
||||
daily: mockDaily,
|
||||
recording: createMockRecording(true), // currently recording
|
||||
transcription: createMockTranscription(false), // not transcribing
|
||||
showRecordingButton: true,
|
||||
showTranscriptionButton: true,
|
||||
enableAutomaticTranscription: false,
|
||||
enableAutomaticRecordingForOrganizer: false,
|
||||
});
|
||||
|
||||
callbacks.updateCustomTrayButtons({});
|
||||
|
||||
expect(mockDaily.updateCustomTrayButtons).toHaveBeenCalledWith({
|
||||
recording: BUTTONS.STOP_RECORDING, // because isRecording is true
|
||||
transcription: BUTTONS.START_TRANSCRIPTION, // because isTranscribing is false
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("startRecording", () => {
|
||||
it("should call daily.startRecording with correct parameters", () => {
|
||||
const callbacks = createCalVideoCallbacks({
|
||||
daily: mockDaily,
|
||||
recording: createMockRecording(),
|
||||
transcription: createMockTranscription(),
|
||||
showRecordingButton: true,
|
||||
showTranscriptionButton: true,
|
||||
enableAutomaticTranscription: false,
|
||||
enableAutomaticRecordingForOrganizer: false,
|
||||
});
|
||||
|
||||
callbacks.startRecording();
|
||||
|
||||
expect(mockDaily.startRecording).toHaveBeenCalledTimes(1);
|
||||
expect(mockDaily.startRecording).toHaveBeenCalledWith({
|
||||
videoBitrate: 2000,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
TRANSCRIPTION_STARTED_ICON,
|
||||
RECORDING_IN_PROGRESS_ICON,
|
||||
TRANSCRIPTION_STOPPED_ICON,
|
||||
RECORDING_DEFAULT_ICON,
|
||||
} from "@calcom/lib/constants";
|
||||
|
||||
export const BUTTONS = {
|
||||
STOP_TRANSCRIPTION: {
|
||||
label: "Stop",
|
||||
tooltip: "Stop transcription",
|
||||
iconPath: TRANSCRIPTION_STARTED_ICON,
|
||||
iconPathDarkMode: TRANSCRIPTION_STARTED_ICON,
|
||||
},
|
||||
START_TRANSCRIPTION: {
|
||||
label: "Cal.ai",
|
||||
tooltip: "Transcription powered by AI",
|
||||
iconPath: TRANSCRIPTION_STOPPED_ICON,
|
||||
iconPathDarkMode: TRANSCRIPTION_STOPPED_ICON,
|
||||
},
|
||||
WAIT_FOR_TRANSCRIPTION_TO_START: {
|
||||
label: "Starting..",
|
||||
tooltip: "Please wait while we start transcription",
|
||||
iconPath: TRANSCRIPTION_STOPPED_ICON,
|
||||
iconPathDarkMode: TRANSCRIPTION_STOPPED_ICON,
|
||||
},
|
||||
WAIT_FOR_TRANSCRIPTION_TO_STOP: {
|
||||
label: "Stopping..",
|
||||
tooltip: "Please wait while we stop transcription",
|
||||
iconPath: TRANSCRIPTION_STOPPED_ICON,
|
||||
iconPathDarkMode: TRANSCRIPTION_STOPPED_ICON,
|
||||
},
|
||||
START_RECORDING: {
|
||||
label: "Record",
|
||||
tooltip: "Start recording",
|
||||
iconPath: RECORDING_DEFAULT_ICON,
|
||||
iconPathDarkMode: RECORDING_DEFAULT_ICON,
|
||||
},
|
||||
WAIT_FOR_RECORDING_TO_START: {
|
||||
label: "Starting..",
|
||||
tooltip: "Please wait while we start recording",
|
||||
iconPath: RECORDING_DEFAULT_ICON,
|
||||
iconPathDarkMode: RECORDING_DEFAULT_ICON,
|
||||
},
|
||||
WAIT_FOR_RECORDING_TO_STOP: {
|
||||
label: "Stopping..",
|
||||
tooltip: "Please wait while we stop recording",
|
||||
iconPath: RECORDING_DEFAULT_ICON,
|
||||
iconPathDarkMode: RECORDING_DEFAULT_ICON,
|
||||
},
|
||||
STOP_RECORDING: {
|
||||
label: "Stop",
|
||||
tooltip: "Stop recording",
|
||||
iconPath: RECORDING_IN_PROGRESS_ICON,
|
||||
iconPathDarkMode: RECORDING_IN_PROGRESS_ICON,
|
||||
},
|
||||
};
|
||||
+164
-168
@@ -1,65 +1,9 @@
|
||||
import type { DailyCall } from "@daily-co/daily-js";
|
||||
import { useTranscription, useRecording } from "@daily-co/daily-react";
|
||||
import { useDaily, useDailyEvent } from "@daily-co/daily-react";
|
||||
import React, { Fragment, useCallback, useRef, useState, useLayoutEffect, useEffect } from "react";
|
||||
|
||||
import {
|
||||
TRANSCRIPTION_STARTED_ICON,
|
||||
RECORDING_IN_PROGRESS_ICON,
|
||||
TRANSCRIPTION_STOPPED_ICON,
|
||||
RECORDING_DEFAULT_ICON,
|
||||
} from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
||||
const BUTTONS = {
|
||||
STOP_TRANSCRIPTION: {
|
||||
label: "Stop",
|
||||
tooltip: "Stop transcription",
|
||||
iconPath: TRANSCRIPTION_STARTED_ICON,
|
||||
iconPathDarkMode: TRANSCRIPTION_STARTED_ICON,
|
||||
},
|
||||
START_TRANSCRIPTION: {
|
||||
label: "Cal.ai",
|
||||
tooltip: "Transcription powered by AI",
|
||||
iconPath: TRANSCRIPTION_STOPPED_ICON,
|
||||
iconPathDarkMode: TRANSCRIPTION_STOPPED_ICON,
|
||||
},
|
||||
WAIT_FOR_TRANSCRIPTION_TO_START: {
|
||||
label: "Starting..",
|
||||
tooltip: "Please wait while we start transcription",
|
||||
iconPath: TRANSCRIPTION_STOPPED_ICON,
|
||||
iconPathDarkMode: TRANSCRIPTION_STOPPED_ICON,
|
||||
},
|
||||
WAIT_FOR_TRANSCRIPTION_TO_STOP: {
|
||||
label: "Stopping..",
|
||||
tooltip: "Please wait while we stop transcription",
|
||||
iconPath: TRANSCRIPTION_STOPPED_ICON,
|
||||
iconPathDarkMode: TRANSCRIPTION_STOPPED_ICON,
|
||||
},
|
||||
START_RECORDING: {
|
||||
label: "Record",
|
||||
tooltip: "Start recording",
|
||||
iconPath: RECORDING_DEFAULT_ICON,
|
||||
iconPathDarkMode: RECORDING_DEFAULT_ICON,
|
||||
},
|
||||
WAIT_FOR_RECORDING_TO_START: {
|
||||
label: "Starting..",
|
||||
tooltip: "Please wait while we start recording",
|
||||
iconPath: RECORDING_DEFAULT_ICON,
|
||||
iconPathDarkMode: RECORDING_DEFAULT_ICON,
|
||||
},
|
||||
WAIT_FOR_RECORDING_TO_STOP: {
|
||||
label: "Stopping..",
|
||||
tooltip: "Please wait while we stop recording",
|
||||
iconPath: RECORDING_DEFAULT_ICON,
|
||||
iconPathDarkMode: RECORDING_DEFAULT_ICON,
|
||||
},
|
||||
STOP_RECORDING: {
|
||||
label: "Stop",
|
||||
tooltip: "Stop recording",
|
||||
iconPath: RECORDING_IN_PROGRESS_ICON,
|
||||
iconPathDarkMode: RECORDING_IN_PROGRESS_ICON,
|
||||
},
|
||||
};
|
||||
import { BUTTONS } from "./button-states";
|
||||
|
||||
export type DailyCustomTrayButtonVisualState = "default" | "sidebar-open" | "active";
|
||||
|
||||
@@ -70,46 +14,180 @@ export interface DailyCustomTrayButton {
|
||||
tooltip: string;
|
||||
visualState?: DailyCustomTrayButtonVisualState;
|
||||
}
|
||||
export const CalAiTranscribe = ({
|
||||
|
||||
type RecordingState = {
|
||||
isRecording: boolean;
|
||||
};
|
||||
|
||||
type TranscriptionState = {
|
||||
isTranscribing: boolean;
|
||||
};
|
||||
|
||||
type CalVideoCallbacksParams = {
|
||||
daily: DailyCall | null;
|
||||
recording: RecordingState | null;
|
||||
transcription: TranscriptionState | null;
|
||||
showRecordingButton: boolean;
|
||||
showTranscriptionButton: boolean;
|
||||
enableAutomaticTranscription: boolean;
|
||||
enableAutomaticRecordingForOrganizer: boolean;
|
||||
};
|
||||
|
||||
export const createCalVideoCallbacks = (params: CalVideoCallbacksParams) => {
|
||||
const {
|
||||
daily,
|
||||
recording,
|
||||
transcription,
|
||||
showRecordingButton,
|
||||
showTranscriptionButton,
|
||||
enableAutomaticTranscription,
|
||||
enableAutomaticRecordingForOrganizer,
|
||||
} = params;
|
||||
|
||||
const startRecording = () => {
|
||||
daily?.startRecording({
|
||||
// 480p
|
||||
videoBitrate: 2000,
|
||||
});
|
||||
};
|
||||
|
||||
const updateCustomTrayButtons = ({
|
||||
recording: overrideRecording,
|
||||
transcription: overrideTranscription,
|
||||
}: {
|
||||
recording?: DailyCustomTrayButton;
|
||||
transcription?: DailyCustomTrayButton;
|
||||
}) => {
|
||||
const currentRecordingState = recording?.isRecording ? BUTTONS.STOP_RECORDING : BUTTONS.START_RECORDING;
|
||||
const currentTranscriptionState = transcription?.isTranscribing
|
||||
? BUTTONS.STOP_TRANSCRIPTION
|
||||
: BUTTONS.START_TRANSCRIPTION;
|
||||
|
||||
daily?.updateCustomTrayButtons({
|
||||
...(showRecordingButton
|
||||
? {
|
||||
recording: overrideRecording ?? currentRecordingState,
|
||||
}
|
||||
: {}),
|
||||
...(showTranscriptionButton
|
||||
? {
|
||||
transcription: overrideTranscription ?? currentTranscriptionState,
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
};
|
||||
|
||||
const onMeetingJoined = () => {
|
||||
if (enableAutomaticTranscription && !transcription?.isTranscribing) {
|
||||
daily?.startTranscription();
|
||||
}
|
||||
if (enableAutomaticRecordingForOrganizer && !recording?.isRecording) {
|
||||
startRecording();
|
||||
}
|
||||
};
|
||||
|
||||
const onRecordingStarted = () => {
|
||||
updateCustomTrayButtons({
|
||||
recording: BUTTONS.STOP_RECORDING,
|
||||
});
|
||||
};
|
||||
|
||||
const onRecordingStopped = () => {
|
||||
updateCustomTrayButtons({
|
||||
recording: BUTTONS.START_RECORDING,
|
||||
});
|
||||
};
|
||||
|
||||
const onTranscriptionStarted = () => {
|
||||
updateCustomTrayButtons({
|
||||
transcription: BUTTONS.STOP_TRANSCRIPTION,
|
||||
});
|
||||
};
|
||||
|
||||
const onTranscriptionStopped = () => {
|
||||
updateCustomTrayButtons({
|
||||
transcription: BUTTONS.START_TRANSCRIPTION,
|
||||
});
|
||||
};
|
||||
|
||||
const toggleRecording = async () => {
|
||||
if (recording?.isRecording) {
|
||||
updateCustomTrayButtons({
|
||||
recording: BUTTONS.WAIT_FOR_RECORDING_TO_STOP,
|
||||
});
|
||||
daily?.stopRecording();
|
||||
} else {
|
||||
updateCustomTrayButtons({
|
||||
recording: BUTTONS.WAIT_FOR_RECORDING_TO_START,
|
||||
});
|
||||
startRecording();
|
||||
}
|
||||
};
|
||||
|
||||
const toggleTranscription = async () => {
|
||||
if (transcription?.isTranscribing) {
|
||||
updateCustomTrayButtons({
|
||||
transcription: BUTTONS.WAIT_FOR_TRANSCRIPTION_TO_STOP,
|
||||
});
|
||||
daily?.stopTranscription();
|
||||
} else {
|
||||
updateCustomTrayButtons({
|
||||
transcription: BUTTONS.WAIT_FOR_TRANSCRIPTION_TO_START,
|
||||
});
|
||||
daily?.startTranscription();
|
||||
}
|
||||
};
|
||||
|
||||
const onCustomButtonClick = async (ev: { button_id: string }) => {
|
||||
if (ev?.button_id === "recording") {
|
||||
toggleRecording();
|
||||
} else if (ev?.button_id === "transcription") {
|
||||
toggleTranscription();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
onMeetingJoined,
|
||||
onRecordingStarted,
|
||||
onRecordingStopped,
|
||||
onTranscriptionStarted,
|
||||
onTranscriptionStopped,
|
||||
onCustomButtonClick,
|
||||
toggleRecording,
|
||||
toggleTranscription,
|
||||
updateCustomTrayButtons,
|
||||
startRecording,
|
||||
};
|
||||
};
|
||||
|
||||
export const CalVideoPremiumFeatures = ({
|
||||
showRecordingButton,
|
||||
enableAutomaticTranscription,
|
||||
enableAutomaticRecordingForOrganizer,
|
||||
showTranscriptionButton,
|
||||
}: {
|
||||
showRecordingButton: boolean;
|
||||
enableAutomaticTranscription: boolean;
|
||||
enableAutomaticRecordingForOrganizer: boolean;
|
||||
showTranscriptionButton: boolean;
|
||||
}) => {
|
||||
const daily = useDaily();
|
||||
const { t } = useLocale();
|
||||
|
||||
const [transcript, setTranscript] = useState("");
|
||||
|
||||
const [transcriptHeight, setTranscriptHeight] = useState(0);
|
||||
const transcriptRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const transcription = useTranscription();
|
||||
const recording = useRecording();
|
||||
|
||||
const updateCustomTrayButtons = ({
|
||||
const callbacks = createCalVideoCallbacks({
|
||||
daily,
|
||||
recording,
|
||||
transcription,
|
||||
}: {
|
||||
recording: DailyCustomTrayButton;
|
||||
transcription: DailyCustomTrayButton;
|
||||
}) => {
|
||||
daily?.updateCustomTrayButtons({
|
||||
...(showRecordingButton
|
||||
? {
|
||||
recording,
|
||||
}
|
||||
: {}),
|
||||
...(showTranscriptionButton
|
||||
? {
|
||||
transcription,
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
};
|
||||
showRecordingButton,
|
||||
showTranscriptionButton,
|
||||
enableAutomaticTranscription,
|
||||
enableAutomaticRecordingForOrganizer,
|
||||
});
|
||||
|
||||
useDailyEvent(
|
||||
"app-message",
|
||||
@@ -119,94 +197,12 @@ export const CalAiTranscribe = ({
|
||||
}, [])
|
||||
);
|
||||
|
||||
useDailyEvent("joined-meeting", (ev) => {
|
||||
if (enableAutomaticTranscription) {
|
||||
daily?.startTranscription();
|
||||
updateCustomTrayButtons({
|
||||
recording: BUTTONS.START_RECORDING,
|
||||
transcription: transcription?.isTranscribing
|
||||
? BUTTONS.STOP_TRANSCRIPTION
|
||||
: BUTTONS.START_TRANSCRIPTION,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
useDailyEvent("transcription-started", (ev) => {
|
||||
updateCustomTrayButtons({
|
||||
recording: recording?.isRecording ? BUTTONS.STOP_RECORDING : BUTTONS.START_RECORDING,
|
||||
transcription: BUTTONS.STOP_TRANSCRIPTION,
|
||||
});
|
||||
});
|
||||
|
||||
useDailyEvent("recording-started", (ev) => {
|
||||
updateCustomTrayButtons({
|
||||
recording: BUTTONS.STOP_RECORDING,
|
||||
transcription: transcription?.isTranscribing ? BUTTONS.STOP_TRANSCRIPTION : BUTTONS.START_TRANSCRIPTION,
|
||||
});
|
||||
});
|
||||
|
||||
useDailyEvent("transcription-stopped", (ev) => {
|
||||
updateCustomTrayButtons({
|
||||
recording: recording?.isRecording ? BUTTONS.STOP_RECORDING : BUTTONS.START_RECORDING,
|
||||
transcription: BUTTONS.START_TRANSCRIPTION,
|
||||
});
|
||||
});
|
||||
|
||||
useDailyEvent("recording-stopped", (ev) => {
|
||||
updateCustomTrayButtons({
|
||||
recording: BUTTONS.START_RECORDING,
|
||||
transcription: transcription?.isTranscribing ? BUTTONS.STOP_TRANSCRIPTION : BUTTONS.START_TRANSCRIPTION,
|
||||
});
|
||||
});
|
||||
|
||||
const toggleRecording = async () => {
|
||||
if (recording?.isRecording) {
|
||||
updateCustomTrayButtons({
|
||||
recording: BUTTONS.WAIT_FOR_RECORDING_TO_STOP,
|
||||
transcription: transcription?.isTranscribing
|
||||
? BUTTONS.STOP_TRANSCRIPTION
|
||||
: BUTTONS.START_TRANSCRIPTION,
|
||||
});
|
||||
await daily?.stopRecording();
|
||||
} else {
|
||||
updateCustomTrayButtons({
|
||||
recording: BUTTONS.WAIT_FOR_RECORDING_TO_START,
|
||||
transcription: transcription?.isTranscribing
|
||||
? BUTTONS.STOP_TRANSCRIPTION
|
||||
: BUTTONS.START_TRANSCRIPTION,
|
||||
});
|
||||
|
||||
await daily?.startRecording({
|
||||
// 480p
|
||||
videoBitrate: 2000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const toggleTranscription = async () => {
|
||||
if (transcription?.isTranscribing) {
|
||||
updateCustomTrayButtons({
|
||||
recording: recording?.isRecording ? BUTTONS.STOP_RECORDING : BUTTONS.START_RECORDING,
|
||||
transcription: BUTTONS.WAIT_FOR_TRANSCRIPTION_TO_STOP,
|
||||
});
|
||||
daily?.stopTranscription();
|
||||
} else {
|
||||
updateCustomTrayButtons({
|
||||
recording: recording?.isRecording ? BUTTONS.STOP_RECORDING : BUTTONS.START_RECORDING,
|
||||
transcription: BUTTONS.WAIT_FOR_TRANSCRIPTION_TO_START,
|
||||
});
|
||||
|
||||
daily?.startTranscription();
|
||||
}
|
||||
};
|
||||
|
||||
useDailyEvent("custom-button-click", async (ev) => {
|
||||
if (ev?.button_id === "recording") {
|
||||
toggleRecording();
|
||||
} else if (ev?.button_id === "transcription") {
|
||||
toggleTranscription();
|
||||
}
|
||||
});
|
||||
useDailyEvent("joined-meeting", callbacks.onMeetingJoined);
|
||||
useDailyEvent("transcription-started", callbacks.onTranscriptionStarted);
|
||||
useDailyEvent("recording-started", callbacks.onRecordingStarted);
|
||||
useDailyEvent("transcription-stopped", callbacks.onTranscriptionStopped);
|
||||
useDailyEvent("recording-stopped", callbacks.onRecordingStopped);
|
||||
useDailyEvent("custom-button-click", callbacks.onCustomButtonClick);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
@@ -21,7 +21,7 @@ import { Icon } from "@calcom/ui/components/icon";
|
||||
|
||||
import type { getServerSideProps } from "@lib/video/[uid]/getServerSideProps";
|
||||
|
||||
import { CalAiTranscribe } from "~/videos/ai/ai-transcribe";
|
||||
import { CalVideoPremiumFeatures } from "../cal-video-premium-features";
|
||||
|
||||
export type PageProps = inferSSRProps<typeof getServerSideProps>;
|
||||
|
||||
@@ -37,6 +37,7 @@ export default function JoinCall(props: PageProps) {
|
||||
overrideName,
|
||||
showRecordingButton,
|
||||
enableAutomaticTranscription,
|
||||
enableAutomaticRecordingForOrganizer,
|
||||
showTranscriptionButton,
|
||||
rediectAttendeeToOnExit,
|
||||
} = props;
|
||||
@@ -116,8 +117,9 @@ export default function JoinCall(props: PageProps) {
|
||||
<div
|
||||
className="mx-auto hidden sm:block"
|
||||
style={{ zIndex: 2, left: "30%", position: "absolute", bottom: 100, width: "auto" }}>
|
||||
<CalAiTranscribe
|
||||
<CalVideoPremiumFeatures
|
||||
showRecordingButton={showRecordingButton}
|
||||
enableAutomaticRecordingForOrganizer={enableAutomaticRecordingForOrganizer}
|
||||
enableAutomaticTranscription={enableAutomaticTranscription}
|
||||
showTranscriptionButton={showTranscriptionButton}
|
||||
/>
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
"calvideo_settings_description": "Configure video meeting preferences including recording permissions for hosts and attendees.",
|
||||
"disable_recording_for_guests": "Disable recording for guests",
|
||||
"enable_automatic_transcription": "Enable automatic transcription after joining the meeting",
|
||||
"enable_automatic_recording": "Enable automatic recording after organizer joins the meeting",
|
||||
"video_options": "Video Options",
|
||||
"get_meeting_session_details": "Get Meeting Session Details",
|
||||
"meeting_session_details": "Meeting Session Details",
|
||||
|
||||
@@ -25,6 +25,7 @@ import ServerTrans from "@calcom/lib/components/ServerTrans";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
import { UpgradeTeamsBadge } from "@calcom/ui/components/badge";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { TextField } from "@calcom/ui/components/form";
|
||||
import { SettingsToggle } from "@calcom/ui/components/form";
|
||||
@@ -355,6 +356,7 @@ const Locations: React.FC<LocationsProps> = ({
|
||||
labelClassName="text-sm leading-6 whitespace-normal break-words"
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
Badge={<UpgradeTeamsBadge checkForActiveStatus />}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
@@ -370,11 +372,30 @@ const Locations: React.FC<LocationsProps> = ({
|
||||
labelClassName="text-sm leading-6 whitespace-normal break-words"
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
Badge={<UpgradeTeamsBadge checkForActiveStatus />}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
{!isPlatform && (
|
||||
<Controller
|
||||
name="calVideoSettings.enableAutomaticRecordingForOrganizer"
|
||||
defaultValue={!!eventType.calVideoSettings?.enableAutomaticRecordingForOrganizer}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<SettingsToggle
|
||||
title={t("enable_automatic_recording")}
|
||||
labelClassName="text-sm"
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
Badge={<UpgradeTeamsBadge checkForActiveStatus />}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Controller
|
||||
name="calVideoSettings.enableAutomaticTranscription"
|
||||
defaultValue={!!eventType.calVideoSettings?.enableAutomaticTranscription}
|
||||
@@ -385,6 +406,7 @@ const Locations: React.FC<LocationsProps> = ({
|
||||
labelClassName="text-sm leading-6 whitespace-normal break-words"
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
Badge={<UpgradeTeamsBadge checkForActiveStatus />}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
@@ -401,6 +423,7 @@ const Locations: React.FC<LocationsProps> = ({
|
||||
labelClassName="text-sm leading-6 whitespace-normal break-words"
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
Badge={<UpgradeTeamsBadge checkForActiveStatus />}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
@@ -417,6 +440,7 @@ const Locations: React.FC<LocationsProps> = ({
|
||||
labelClassName="text-sm leading-6 whitespace-normal break-words"
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
Badge={<UpgradeTeamsBadge checkForActiveStatus />}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -159,6 +159,7 @@ export type FormValues = {
|
||||
disableRecordingForOrganizer?: boolean;
|
||||
disableRecordingForGuests?: boolean;
|
||||
enableAutomaticTranscription?: boolean;
|
||||
enableAutomaticRecordingForOrganizer?: boolean;
|
||||
disableTranscriptionForGuests?: boolean;
|
||||
disableTranscriptionForOrganizer?: boolean;
|
||||
redirectUrlOnExit?: string;
|
||||
|
||||
@@ -398,6 +398,7 @@ export class BookingRepository {
|
||||
disableRecordingForGuests: true,
|
||||
disableRecordingForOrganizer: true,
|
||||
enableAutomaticTranscription: true,
|
||||
enableAutomaticRecordingForOrganizer: true,
|
||||
disableTranscriptionForGuests: true,
|
||||
disableTranscriptionForOrganizer: true,
|
||||
redirectUrlOnExit: true,
|
||||
|
||||
@@ -7,6 +7,35 @@ export class CalVideoSettingsRepository {
|
||||
});
|
||||
}
|
||||
|
||||
static async createCalVideoSettings({
|
||||
eventTypeId,
|
||||
calVideoSettings,
|
||||
}: {
|
||||
eventTypeId: number;
|
||||
calVideoSettings: {
|
||||
disableRecordingForGuests?: boolean | null;
|
||||
disableRecordingForOrganizer?: boolean | null;
|
||||
enableAutomaticTranscription?: boolean | null;
|
||||
enableAutomaticRecordingForOrganizer?: boolean | null;
|
||||
disableTranscriptionForGuests?: boolean | null;
|
||||
disableTranscriptionForOrganizer?: boolean | null;
|
||||
redirectUrlOnExit?: string | null;
|
||||
};
|
||||
}) {
|
||||
return await prisma.calVideoSettings.create({
|
||||
data: {
|
||||
disableRecordingForGuests: calVideoSettings.disableRecordingForGuests ?? false,
|
||||
disableRecordingForOrganizer: calVideoSettings.disableRecordingForOrganizer ?? false,
|
||||
enableAutomaticTranscription: calVideoSettings.enableAutomaticTranscription ?? false,
|
||||
enableAutomaticRecordingForOrganizer: calVideoSettings.enableAutomaticRecordingForOrganizer ?? false,
|
||||
disableTranscriptionForGuests: calVideoSettings.disableTranscriptionForGuests ?? false,
|
||||
disableTranscriptionForOrganizer: calVideoSettings.disableTranscriptionForOrganizer ?? false,
|
||||
redirectUrlOnExit: calVideoSettings.redirectUrlOnExit ?? null,
|
||||
eventTypeId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static async createOrUpdateCalVideoSettings({
|
||||
eventTypeId,
|
||||
calVideoSettings,
|
||||
@@ -18,6 +47,7 @@ export class CalVideoSettingsRepository {
|
||||
disableTranscriptionForGuests?: boolean | null;
|
||||
disableTranscriptionForOrganizer?: boolean | null;
|
||||
enableAutomaticTranscription?: boolean | null;
|
||||
enableAutomaticRecordingForOrganizer?: boolean | null;
|
||||
redirectUrlOnExit?: string | null;
|
||||
};
|
||||
}) {
|
||||
@@ -27,6 +57,7 @@ export class CalVideoSettingsRepository {
|
||||
disableRecordingForGuests: calVideoSettings.disableRecordingForGuests ?? false,
|
||||
disableRecordingForOrganizer: calVideoSettings.disableRecordingForOrganizer ?? false,
|
||||
enableAutomaticTranscription: calVideoSettings.enableAutomaticTranscription ?? false,
|
||||
enableAutomaticRecordingForOrganizer: calVideoSettings.enableAutomaticRecordingForOrganizer ?? false,
|
||||
disableTranscriptionForGuests: calVideoSettings.disableTranscriptionForGuests ?? false,
|
||||
disableTranscriptionForOrganizer: calVideoSettings.disableTranscriptionForOrganizer ?? false,
|
||||
redirectUrlOnExit: calVideoSettings.redirectUrlOnExit ?? null,
|
||||
@@ -36,6 +67,7 @@ export class CalVideoSettingsRepository {
|
||||
disableRecordingForGuests: calVideoSettings.disableRecordingForGuests ?? false,
|
||||
disableRecordingForOrganizer: calVideoSettings.disableRecordingForOrganizer ?? false,
|
||||
enableAutomaticTranscription: calVideoSettings.enableAutomaticTranscription ?? false,
|
||||
enableAutomaticRecordingForOrganizer: calVideoSettings.enableAutomaticRecordingForOrganizer ?? false,
|
||||
disableTranscriptionForGuests: calVideoSettings.disableTranscriptionForGuests ?? false,
|
||||
disableTranscriptionForOrganizer: calVideoSettings.disableTranscriptionForOrganizer ?? false,
|
||||
redirectUrlOnExit: calVideoSettings.redirectUrlOnExit ?? null,
|
||||
|
||||
@@ -709,6 +709,7 @@ export class EventTypeRepository {
|
||||
disableRecordingForGuests: true,
|
||||
disableRecordingForOrganizer: true,
|
||||
enableAutomaticTranscription: true,
|
||||
enableAutomaticRecordingForOrganizer: true,
|
||||
disableTranscriptionForGuests: true,
|
||||
disableTranscriptionForOrganizer: true,
|
||||
redirectUrlOnExit: true,
|
||||
|
||||
@@ -177,6 +177,7 @@ export const useEventTypeForm = ({
|
||||
disableRecordingForOrganizer: z.boolean().nullable(),
|
||||
disableRecordingForGuests: z.boolean().nullable(),
|
||||
enableAutomaticTranscription: z.boolean().nullable(),
|
||||
enableAutomaticRecordingForOrganizer: z.boolean().nullable(),
|
||||
disableTranscriptionForGuests: z.boolean().nullable(),
|
||||
disableTranscriptionForOrganizer: z.boolean().nullable(),
|
||||
})
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "CalVideoSettings" ADD COLUMN "enableAutomaticRecordingForOrganizer" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -72,15 +72,15 @@ model CalVideoSettings {
|
||||
eventTypeId Int @id
|
||||
eventType EventType @relation(fields: [eventTypeId], references: [id], onDelete: Cascade)
|
||||
|
||||
disableRecordingForOrganizer Boolean @default(false)
|
||||
disableRecordingForGuests Boolean @default(false)
|
||||
enableAutomaticTranscription Boolean @default(false)
|
||||
redirectUrlOnExit String?
|
||||
disableTranscriptionForGuests Boolean @default(false)
|
||||
disableTranscriptionForOrganizer Boolean @default(false)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
disableRecordingForOrganizer Boolean @default(false)
|
||||
disableRecordingForGuests Boolean @default(false)
|
||||
enableAutomaticTranscription Boolean @default(false)
|
||||
enableAutomaticRecordingForOrganizer Boolean @default(false)
|
||||
redirectUrlOnExit String?
|
||||
disableTranscriptionForGuests Boolean @default(false)
|
||||
disableTranscriptionForOrganizer Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model EventType {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
import { generateHashedLink } from "@calcom/lib/generateHashedLink";
|
||||
import { CalVideoSettingsRepository } from "@calcom/lib/server/repository/calVideoSettings";
|
||||
import { EventTypeRepository } from "@calcom/lib/server/repository/eventType";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
@@ -44,6 +45,17 @@ export const duplicateHandler = async ({ ctx, input }: DuplicateOptions) => {
|
||||
webhooks: true,
|
||||
hashedLink: true,
|
||||
destinationCalendar: true,
|
||||
calVideoSettings: {
|
||||
select: {
|
||||
disableRecordingForOrganizer: true,
|
||||
disableRecordingForGuests: true,
|
||||
enableAutomaticTranscription: true,
|
||||
enableAutomaticRecordingForOrganizer: true,
|
||||
redirectUrlOnExit: true,
|
||||
disableTranscriptionForGuests: true,
|
||||
disableTranscriptionForOrganizer: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -95,6 +107,7 @@ export const duplicateHandler = async ({ ctx, input }: DuplicateOptions) => {
|
||||
secondaryEmailId,
|
||||
instantMeetingScheduleId: _instantMeetingScheduleId,
|
||||
restrictionScheduleId: _restrictionScheduleId,
|
||||
calVideoSettings,
|
||||
...rest
|
||||
} = eventType;
|
||||
|
||||
@@ -179,6 +192,13 @@ export const duplicateHandler = async ({ ctx, input }: DuplicateOptions) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (calVideoSettings) {
|
||||
await CalVideoSettingsRepository.createCalVideoSettings({
|
||||
eventTypeId: newEventType.id,
|
||||
calVideoSettings,
|
||||
});
|
||||
}
|
||||
|
||||
if (workflows.length > 0) {
|
||||
const relationCreateData = workflows.map((workflow) => {
|
||||
return { eventTypeId: newEventType.id, workflowId: workflow.workflowId };
|
||||
|
||||
@@ -27,12 +27,13 @@ const aiPhoneCallConfig = z
|
||||
|
||||
const calVideoSettingsSchema = z
|
||||
.object({
|
||||
disableRecordingForGuests: z.boolean().optional().nullable(),
|
||||
disableRecordingForOrganizer: z.boolean().optional().nullable(),
|
||||
enableAutomaticTranscription: z.boolean().optional().nullable(),
|
||||
disableTranscriptionForGuests: z.boolean().optional().nullable(),
|
||||
disableTranscriptionForOrganizer: z.boolean().optional().nullable(),
|
||||
redirectUrlOnExit: z.string().url().optional().nullable(),
|
||||
disableRecordingForGuests: z.boolean().nullish(),
|
||||
disableRecordingForOrganizer: z.boolean().nullish(),
|
||||
enableAutomaticTranscription: z.boolean().nullish(),
|
||||
enableAutomaticRecordingForOrganizer: z.boolean().nullish(),
|
||||
disableTranscriptionForGuests: z.boolean().nullish(),
|
||||
disableTranscriptionForOrganizer: z.boolean().nullish(),
|
||||
redirectUrlOnExit: z.string().url().nullish(),
|
||||
})
|
||||
.optional()
|
||||
.nullable();
|
||||
|
||||
@@ -26,6 +26,7 @@ import { TRPCError } from "@trpc/server";
|
||||
|
||||
import type { TrpcSessionUser } from "../../../types";
|
||||
import { setDestinationCalendarHandler } from "../../viewer/calendars/setDestinationCalendar.handler";
|
||||
import { hasTeamPlanHandler } from "../teams/hasTeamPlan.handler";
|
||||
import type { TUpdateInputSchema } from "./update.schema";
|
||||
import {
|
||||
ensureUniqueBookingFields,
|
||||
@@ -130,6 +131,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||
disableRecordingForOrganizer: true,
|
||||
disableRecordingForGuests: true,
|
||||
enableAutomaticTranscription: true,
|
||||
enableAutomaticRecordingForOrganizer: true,
|
||||
disableTranscriptionForGuests: true,
|
||||
disableTranscriptionForOrganizer: true,
|
||||
redirectUrlOnExit: true,
|
||||
@@ -598,10 +600,16 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||
}
|
||||
|
||||
if (calVideoSettings) {
|
||||
await CalVideoSettingsRepository.createOrUpdateCalVideoSettings({
|
||||
eventTypeId: id,
|
||||
calVideoSettings,
|
||||
const { hasTeamPlan } = await hasTeamPlanHandler({
|
||||
ctx: { user: ctx.user },
|
||||
});
|
||||
|
||||
if (hasTeamPlan) {
|
||||
await CalVideoSettingsRepository.createOrUpdateCalVideoSettings({
|
||||
eventTypeId: id,
|
||||
calVideoSettings,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const parsedEventTypeLocations = eventTypeLocations.safeParse(eventType.locations ?? []);
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
|
||||
|
||||
type HasTeamPlanOptions = {
|
||||
ctx: {
|
||||
user: NonNullable<TrpcSessionUser>;
|
||||
user: { id: number };
|
||||
};
|
||||
};
|
||||
|
||||
export const hasTeamPlanHandler = async ({ ctx }: HasTeamPlanOptions) => {
|
||||
const userId = ctx.user.id;
|
||||
|
||||
export const hasTeamPlanHandler = async ({ ctx: { user } }: HasTeamPlanOptions) => {
|
||||
const hasTeamPlan = await prisma.membership.findFirst({
|
||||
where: {
|
||||
accepted: true,
|
||||
userId,
|
||||
userId: user.id,
|
||||
team: {
|
||||
slug: {
|
||||
not: null,
|
||||
|
||||
@@ -106,7 +106,7 @@ export function SettingsToggle({
|
||||
<div>
|
||||
<Label
|
||||
className={classNames("text-emphasis text-sm font-semibold leading-none", labelClassName)}>
|
||||
{title}
|
||||
{title} {Badge ? Badge : null}
|
||||
{LockedIcon}
|
||||
</Label>
|
||||
{description && <p className="text-default -mt-1.5 text-sm leading-normal">{description}</p>}
|
||||
|
||||
Reference in New Issue
Block a user