fix: creation of meeting

This commit is contained in:
2025-09-12 16:40:24 -06:00
parent 0f59115a2a
commit 7e98c2eea7
6 changed files with 202 additions and 31 deletions

View File

@@ -113,6 +113,10 @@ class UpdateRoom(BaseModel):
ics_enabled: Optional[bool] = None ics_enabled: Optional[bool] = None
class CreateRoomMeeting(BaseModel):
allow_duplicated: Optional[bool] = False
class DeletionStatus(BaseModel): class DeletionStatus(BaseModel):
status: str status: str
@@ -235,6 +239,7 @@ async def rooms_delete(
@router.post("/rooms/{room_name}/meeting", response_model=Meeting) @router.post("/rooms/{room_name}/meeting", response_model=Meeting)
async def rooms_create_meeting( async def rooms_create_meeting(
room_name: str, room_name: str,
info: CreateRoomMeeting,
user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)], user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)],
): ):
user_id = user["sub"] if user else None user_id = user["sub"] if user else None
@@ -243,7 +248,12 @@ async def rooms_create_meeting(
raise HTTPException(status_code=404, detail="Room not found") raise HTTPException(status_code=404, detail="Room not found")
current_time = datetime.now(timezone.utc) current_time = datetime.now(timezone.utc)
meeting = await meetings_controller.get_active(room=room, current_time=current_time)
meeting = None
if not info.allow_duplicated:
meeting = await meetings_controller.get_active(
room=room, current_time=current_time
)
if meeting is None: if meeting is None:
end_date = current_time + timedelta(hours=8) end_date = current_time + timedelta(hours=8)

View File

@@ -191,6 +191,7 @@ async def process_meetings():
# This API call could be slow, extend lock if needed # This API call could be slow, extend lock if needed
response = await get_room_sessions(meeting.room_name) response = await get_room_sessions(meeting.room_name)
print(response)
try: try:
# Extend lock after slow operation to ensure we still hold it # Extend lock after slow operation to ensure we still hold it

View File

@@ -25,7 +25,6 @@ import {
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { formatDateTime, formatStartedAgo } from "../lib/timeUtils"; import { formatDateTime, formatStartedAgo } from "../lib/timeUtils";
import MeetingMinimalHeader from "../components/MeetingMinimalHeader"; import MeetingMinimalHeader from "../components/MeetingMinimalHeader";
import { MEETING_DEFAULT_TIME_MINUTES } from "./[meetingId]/constants";
type Meeting = components["schemas"]["Meeting"]; type Meeting = components["schemas"]["Meeting"];
@@ -36,6 +35,7 @@ interface MeetingSelectionProps {
authLoading: boolean; authLoading: boolean;
onMeetingSelect: (meeting: Meeting) => void; onMeetingSelect: (meeting: Meeting) => void;
onCreateUnscheduled: () => void; onCreateUnscheduled: () => void;
isCreatingMeeting?: boolean;
} }
export default function MeetingSelection({ export default function MeetingSelection({
@@ -44,6 +44,7 @@ export default function MeetingSelection({
isSharedRoom, isSharedRoom,
onMeetingSelect, onMeetingSelect,
onCreateUnscheduled, onCreateUnscheduled,
isCreatingMeeting = false,
}: MeetingSelectionProps) { }: MeetingSelectionProps) {
const router = useRouter(); const router = useRouter();
@@ -56,19 +57,22 @@ export default function MeetingSelection({
const allMeetings = activeMeetingsQuery.data || []; const allMeetings = activeMeetingsQuery.data || [];
const now = new Date(); const now = new Date();
const [currentMeetings, upcomingMeetings] = partition( const [currentMeetings, nonCurrentMeetings] = partition(
allMeetings, allMeetings,
(meeting) => { (meeting) => {
const startTime = new Date(meeting.start_date); const startTime = new Date(meeting.start_date);
// Meeting is ongoing if it started and participants have joined or it's been running for a while const endTime = new Date(meeting.end_date);
return ( // Meeting is ongoing if current time is between start and end
meeting.num_clients > 0 || return now >= startTime && now <= endTime;
now.getTime() - startTime.getTime() >
MEETING_DEFAULT_TIME_MINUTES * 1000
);
}, },
); );
const upcomingMeetings = nonCurrentMeetings.filter((meeting) => {
const startTime = new Date(meeting.start_date);
// Meeting is upcoming if it hasn't started yet
return now < startTime;
});
const loading = roomQuery.isLoading || activeMeetingsQuery.isLoading; const loading = roomQuery.isLoading || activeMeetingsQuery.isLoading;
const error = roomQuery.error || activeMeetingsQuery.error; const error = roomQuery.error || activeMeetingsQuery.error;
@@ -139,7 +143,30 @@ export default function MeetingSelection({
}; };
return ( return (
<Flex flexDir="column" minH="100vh"> <Flex flexDir="column" minH="100vh" position="relative">
{/* Loading overlay */}
{isCreatingMeeting && (
<Box
position="fixed"
top={0}
left={0}
right={0}
bottom={0}
bg="blackAlpha.600"
zIndex={9999}
display="flex"
alignItems="center"
justifyContent="center"
>
<VStack gap={4} p={8} bg="white" borderRadius="lg" boxShadow="xl">
<Spinner size="lg" color="blue.500" />
<Text fontSize="lg" fontWeight="medium">
Creating meeting...
</Text>
</VStack>
</Box>
)}
<MeetingMinimalHeader <MeetingMinimalHeader
roomName={roomName} roomName={roomName}
displayName={room?.name} displayName={room?.name}
@@ -147,6 +174,7 @@ export default function MeetingSelection({
onLeave={handleLeaveMeeting} onLeave={handleLeaveMeeting}
showCreateButton={isOwner || isSharedRoom} showCreateButton={isOwner || isSharedRoom}
onCreateMeeting={onCreateUnscheduled} onCreateMeeting={onCreateUnscheduled}
isCreatingMeeting={isCreatingMeeting}
/> />
<Flex <Flex
@@ -160,11 +188,8 @@ export default function MeetingSelection({
gap={6} gap={6}
> >
{/* Current Ongoing Meetings - BIG DISPLAY */} {/* Current Ongoing Meetings - BIG DISPLAY */}
{currentMeetings.length > 0 && ( {currentMeetings.length > 0 ? (
<VStack align="stretch" gap={6} mb={8}> <VStack align="stretch" gap={6} mb={8}>
<Text fontSize="xl" fontWeight="bold" color="gray.800">
Live Meeting{currentMeetings.length > 1 ? "s" : ""}
</Text>
{currentMeetings.map((meeting) => ( {currentMeetings.map((meeting) => (
<Box <Box
key={meeting.id} key={meeting.id}
@@ -273,10 +298,130 @@ export default function MeetingSelection({
</Box> </Box>
))} ))}
</VStack> </VStack>
) : upcomingMeetings.length > 0 ? (
/* Upcoming Meetings - BIG DISPLAY when no ongoing meetings */
<VStack align="stretch" gap={6} mb={8}>
<Text fontSize="xl" fontWeight="bold" color="gray.800">
Upcoming Meeting{upcomingMeetings.length > 1 ? "s" : ""}
</Text>
{upcomingMeetings.map((meeting) => {
const now = new Date();
const startTime = new Date(meeting.start_date);
const minutesUntilStart = Math.floor(
(startTime.getTime() - now.getTime()) / (1000 * 60),
);
return (
<Box
key={meeting.id}
width="100%"
bg="orange.50"
borderRadius="xl"
p={8}
border="2px solid"
borderColor="orange.200"
>
<HStack justify="space-between" align="start">
<VStack align="start" gap={4} flex={1}>
<HStack>
<Icon
as={FaCalendarAlt}
color="orange.600"
boxSize="24px"
/>
<Text
fontSize="2xl"
fontWeight="bold"
color="orange.800"
>
{(meeting.calendar_metadata as any)?.title ||
"Upcoming Meeting"}
</Text>
</HStack>
{isOwner &&
(meeting.calendar_metadata as any)?.description && (
<Text fontSize="lg" color="gray.700">
{(meeting.calendar_metadata as any).description}
</Text>
)} )}
{/* Upcoming Meetings - SMALLER ASIDE DISPLAY */} <HStack gap={8} fontSize="md" color="gray.600">
{upcomingMeetings.length > 0 && ( <Badge colorScheme="orange" fontSize="md" px={3} py={1}>
Starts in {minutesUntilStart} minute
{minutesUntilStart !== 1 ? "s" : ""}
</Badge>
<Text>{formatDateTime(meeting.start_date)}</Text>
</HStack>
{isOwner &&
(meeting.calendar_metadata as any)?.attendees && (
<HStack gap={3} flexWrap="wrap">
{(meeting.calendar_metadata as any).attendees
.slice(0, 4)
.map((attendee: any, idx: number) => (
<Badge
key={idx}
colorScheme="orange"
fontSize="sm"
px={3}
py={1}
>
{attendee.name || attendee.email}
</Badge>
))}
{(meeting.calendar_metadata as any).attendees
.length > 4 && (
<Badge
colorScheme="gray"
fontSize="sm"
px={3}
py={1}
>
+
{(meeting.calendar_metadata as any).attendees
.length - 4}{" "}
more
</Badge>
)}
</HStack>
)}
</VStack>
<VStack gap={3}>
<Button
colorScheme="orange"
size="xl"
fontSize="lg"
px={8}
py={6}
onClick={() => handleJoinUpcoming(meeting)}
>
<Icon as={FaClock} boxSize="20px" me={2} />
Join Early
</Button>
{isOwner && (
<Button
variant="outline"
colorScheme="red"
size="md"
onClick={() => handleEndMeeting(meeting.id)}
loading={deactivateMeetingMutation.isPending}
>
<Icon as={LuX} me={2} />
Cancel Meeting
</Button>
)}
</VStack>
</HStack>
</Box>
);
})}
</VStack>
) : null}
{/* Upcoming Meetings - SMALLER ASIDE DISPLAY when there are ongoing meetings */}
{currentMeetings.length > 0 && upcomingMeetings.length > 0 && (
<VStack align="stretch" gap={4} mb={6}> <VStack align="stretch" gap={4} mb={6}>
<Text fontSize="lg" fontWeight="semibold" color="gray.700"> <Text fontSize="lg" fontWeight="semibold" color="gray.700">
Starting Soon Starting Soon

View File

@@ -49,6 +49,9 @@ const useRoomMeeting = (
room_name: roomName, room_name: roomName,
}, },
}, },
body: {
allow_duplicated: false,
},
}); });
setResponse(result); setResponse(result);
} catch (error: any) { } catch (error: any) {

View File

@@ -12,6 +12,7 @@ interface MeetingMinimalHeaderProps {
onLeave?: () => void; onLeave?: () => void;
showCreateButton?: boolean; showCreateButton?: boolean;
onCreateMeeting?: () => void; onCreateMeeting?: () => void;
isCreatingMeeting?: boolean;
} }
export default function MeetingMinimalHeader({ export default function MeetingMinimalHeader({
@@ -21,6 +22,7 @@ export default function MeetingMinimalHeader({
onLeave, onLeave,
showCreateButton = false, showCreateButton = false,
onCreateMeeting, onCreateMeeting,
isCreatingMeeting = false,
}: MeetingMinimalHeaderProps) { }: MeetingMinimalHeaderProps) {
const router = useRouter(); const router = useRouter();
@@ -70,7 +72,13 @@ export default function MeetingMinimalHeader({
{/* Action Buttons */} {/* Action Buttons */}
<HStack gap={2}> <HStack gap={2}>
{showCreateButton && onCreateMeeting && ( {showCreateButton && onCreateMeeting && (
<Button colorScheme="green" size="sm" onClick={onCreateMeeting}> <Button
colorScheme="green"
size="sm"
onClick={onCreateMeeting}
loading={isCreatingMeeting}
disabled={isCreatingMeeting}
>
Create Meeting Create Meeting
</Button> </Button>
)} )}
@@ -80,6 +88,7 @@ export default function MeetingMinimalHeader({
colorScheme="gray" colorScheme="gray"
size="sm" size="sm"
onClick={handleLeaveMeeting} onClick={handleLeaveMeeting}
disabled={isCreatingMeeting}
> >
Leave Room Leave Room
</Button> </Button>

View File

@@ -54,10 +54,7 @@ export interface paths {
delete?: never; delete?: never;
options?: never; options?: never;
head?: never; head?: never;
/** /** Meeting Deactivate */
* Meeting Deactivate
* @description Deactivate a meeting (owner only)
*/
patch: operations["v1_meeting_deactivate"]; patch: operations["v1_meeting_deactivate"];
trace?: never; trace?: never;
}; };
@@ -227,10 +224,7 @@ export interface paths {
path?: never; path?: never;
cookie?: never; cookie?: never;
}; };
/** /** Rooms List Active Meetings */
* Rooms List Active Meetings
* @description List all active meetings for a room (supports multiple active meetings)
*/
get: operations["v1_rooms_list_active_meetings"]; get: operations["v1_rooms_list_active_meetings"];
put?: never; put?: never;
post?: never; post?: never;
@@ -249,10 +243,7 @@ export interface paths {
}; };
get?: never; get?: never;
put?: never; put?: never;
/** /** Rooms Join Meeting */
* Rooms Join Meeting
* @description Join a specific meeting by ID
*/
post: operations["v1_rooms_join_meeting"]; post: operations["v1_rooms_join_meeting"];
delete?: never; delete?: never;
options?: never; options?: never;
@@ -740,6 +731,14 @@ export interface components {
*/ */
ics_enabled: boolean; ics_enabled: boolean;
}; };
/** CreateRoomMeeting */
CreateRoomMeeting: {
/**
* Allow Duplicated
* @default false
*/
allow_duplicated: boolean | null;
};
/** CreateTranscript */ /** CreateTranscript */
CreateTranscript: { CreateTranscript: {
/** Name */ /** Name */
@@ -1780,7 +1779,11 @@ export interface operations {
}; };
cookie?: never; cookie?: never;
}; };
requestBody?: never; requestBody: {
content: {
"application/json": components["schemas"]["CreateRoomMeeting"];
};
};
responses: { responses: {
/** @description Successful Response */ /** @description Successful Response */
200: { 200: {