diff --git a/server/migrations/versions/62dea3db63a5_add_room_options.py b/server/migrations/versions/62dea3db63a5_add_room_options.py new file mode 100644 index 00000000..6739100e --- /dev/null +++ b/server/migrations/versions/62dea3db63a5_add_room_options.py @@ -0,0 +1,86 @@ +"""Add room options + +Revision ID: 62dea3db63a5 +Revises: 1340c04426b8 +Create Date: 2024-09-03 16:19:26.861027 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "62dea3db63a5" +down_revision: Union[str, None] = "1340c04426b8" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "meeting", + sa.Column( + "is_locked", sa.Boolean(), server_default=sa.text("0"), nullable=False + ), + ) + op.add_column( + "meeting", + sa.Column("room_mode", sa.String(), server_default="normal", nullable=False), + ) + op.add_column( + "meeting", + sa.Column( + "recording_type", sa.String(), server_default="cloud", nullable=False + ), + ) + op.add_column( + "meeting", + sa.Column( + "recording_trigger", + sa.String(), + server_default="automatic-2nd-participant", + nullable=False, + ), + ) + op.add_column( + "room", + sa.Column( + "is_locked", sa.Boolean(), server_default=sa.text("0"), nullable=False + ), + ) + op.add_column( + "room", + sa.Column("room_mode", sa.String(), server_default="normal", nullable=False), + ) + op.add_column( + "room", + sa.Column( + "recording_type", sa.String(), server_default="cloud", nullable=False + ), + ) + op.add_column( + "room", + sa.Column( + "recording_trigger", + sa.String(), + server_default="automatic-2nd-participant", + nullable=False, + ), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("room", "recording_trigger") + op.drop_column("room", "recording_type") + op.drop_column("room", "room_mode") + op.drop_column("room", "is_locked") + op.drop_column("meeting", "recording_trigger") + op.drop_column("meeting", "recording_type") + op.drop_column("meeting", "room_mode") + op.drop_column("meeting", "is_locked") + # ### end Alembic commands ### diff --git a/server/reflector/db/meetings.py b/server/reflector/db/meetings.py index 1ddcc0ba..7a47f834 100644 --- a/server/reflector/db/meetings.py +++ b/server/reflector/db/meetings.py @@ -1,9 +1,12 @@ from datetime import datetime, timezone +from typing import Literal import sqlalchemy from fastapi import HTTPException from pydantic import BaseModel from reflector.db import database, metadata +from reflector.db.rooms import Room +from sqlalchemy.sql import false meetings = sqlalchemy.Table( "meeting", @@ -17,6 +20,21 @@ meetings = sqlalchemy.Table( sqlalchemy.Column("end_date", sqlalchemy.DateTime), sqlalchemy.Column("user_id", sqlalchemy.String), sqlalchemy.Column("room_id", sqlalchemy.String), + sqlalchemy.Column( + "is_locked", sqlalchemy.Boolean, nullable=False, server_default=false() + ), + sqlalchemy.Column( + "room_mode", sqlalchemy.String, nullable=False, server_default="normal" + ), + sqlalchemy.Column( + "recording_type", sqlalchemy.String, nullable=False, server_default="cloud" + ), + sqlalchemy.Column( + "recording_trigger", + sqlalchemy.String, + nullable=False, + server_default="automatic-2nd-participant", + ), ) @@ -30,6 +48,12 @@ class Meeting(BaseModel): end_date: datetime user_id: str | None = None room_id: str | None = None + is_locked: bool = False + room_mode: Literal["normal", "group"] = "normal" + recording_type: Literal["none", "local", "cloud"] = "cloud" + recording_trigger: Literal[ + "none", "prompt", "automatic", "automatic-2nd-participant" + ] = "automatic-2nd-participant" class MeetingController: @@ -43,7 +67,7 @@ class MeetingController: start_date: datetime, end_date: datetime, user_id: str, - room_id: str = None, + room: Room, ): """ Create a new meeting @@ -57,7 +81,11 @@ class MeetingController: start_date=start_date, end_date=end_date, user_id=user_id, - room_id=room_id, + room_id=room.id, + is_locked=room.is_locked, + room_mode=room.room_mode, + recording_type=room.recording_type, + recording_trigger=room.recording_trigger, ) query = meetings.insert().values(**meeting.model_dump()) await database.execute(query) @@ -77,15 +105,23 @@ class MeetingController: return Meeting(**result) - async def get_latest(self, room_id: str) -> Meeting: + async def get_latest(self, room: Room) -> Meeting: """ Get latest meeting for a room. """ end_date = getattr(meetings.c, "end_date") query = ( meetings.select() - .where(meetings.c.room_id == room_id) - .where(meetings.c.end_date > datetime.now(timezone.utc)) + .where( + sqlalchemy.and_( + meetings.c.room_id == room.id, + meetings.c.is_locked == room.is_locked, + meetings.c.room_mode == room.room_mode, + meetings.c.recording_type == room.recording_type, + meetings.c.recording_trigger == room.recording_trigger, + meetings.c.end_date > datetime.now(timezone.utc), + ) + ) .order_by(end_date.desc()) ) result = await database.fetch_one(query) diff --git a/server/reflector/db/rooms.py b/server/reflector/db/rooms.py index fa4eb2a7..5dc19cae 100644 --- a/server/reflector/db/rooms.py +++ b/server/reflector/db/rooms.py @@ -1,4 +1,5 @@ from datetime import datetime +from typing import Literal import sqlalchemy from fastapi import HTTPException @@ -19,6 +20,21 @@ rooms = sqlalchemy.Table( ), sqlalchemy.Column("zulip_stream", sqlalchemy.String), sqlalchemy.Column("zulip_topic", sqlalchemy.String), + sqlalchemy.Column( + "is_locked", sqlalchemy.Boolean, nullable=False, server_default=false() + ), + sqlalchemy.Column( + "room_mode", sqlalchemy.String, nullable=False, server_default="normal" + ), + sqlalchemy.Column( + "recording_type", sqlalchemy.String, nullable=False, server_default="cloud" + ), + sqlalchemy.Column( + "recording_trigger", + sqlalchemy.String, + nullable=False, + server_default="automatic-2nd-participant", + ), ) @@ -30,6 +46,12 @@ class Room(BaseModel): zulip_auto_post: bool = False zulip_stream: str = "" zulip_topic: str = "" + is_locked: bool = False + room_mode: Literal["normal", "group"] = "normal" + recording_type: Literal["none", "local", "cloud"] = "cloud" + recording_trigger: Literal[ + "none", "prompt", "automatic", "automatic-2nd-participant" + ] = "automatic-2nd-participant" class RoomController: diff --git a/server/reflector/views/rooms.py b/server/reflector/views/rooms.py index 7b94cd0b..db9b103b 100644 --- a/server/reflector/views/rooms.py +++ b/server/reflector/views/rooms.py @@ -24,6 +24,10 @@ class Room(BaseModel): zulip_auto_post: bool zulip_stream: str zulip_topic: str + is_locked: bool + room_mode: str + recording_type: str + recording_trigger: str class Meeting(BaseModel): @@ -41,6 +45,10 @@ class CreateRoom(BaseModel): zulip_auto_post: bool zulip_stream: str zulip_topic: str + is_locked: bool + room_mode: str + recording_type: str + recording_trigger: str class UpdateRoom(BaseModel): @@ -48,6 +56,10 @@ class UpdateRoom(BaseModel): zulip_auto_post: bool zulip_stream: str zulip_topic: str + is_locked: bool + room_mode: str + recording_type: str + recording_trigger: str class DeletionStatus(BaseModel): @@ -126,11 +138,13 @@ async def rooms_create_meeting( if not room: raise HTTPException(status_code=404, detail="Room not found") - meeting = await meetings_controller.get_latest(room_id=room.id) + meeting = await meetings_controller.get_latest(room=room) if meeting is None: start_date = datetime.now(timezone.utc) end_date = start_date + timedelta(hours=1) - meeting = await create_meeting("", start_date=start_date, end_date=end_date) + meeting = await create_meeting( + "", start_date=start_date, end_date=end_date, room=room + ) meeting = await meetings_controller.create( id=meeting["meetingId"], @@ -141,7 +155,10 @@ async def rooms_create_meeting( start_date=datetime.fromisoformat(meeting["startDate"]), end_date=datetime.fromisoformat(meeting["endDate"]), user_id=user_id, - room_id=room.id, + room=room, ) + if user_id is None: + meeting.host_room_url = "" + return meeting diff --git a/server/reflector/whereby.py b/server/reflector/whereby.py index 2f1efeb0..154e3771 100644 --- a/server/reflector/whereby.py +++ b/server/reflector/whereby.py @@ -1,11 +1,12 @@ from datetime import datetime import httpx +from reflector.db.rooms import Room from reflector.settings import settings async def create_meeting( - room_name_prefix: str, start_date: datetime, end_date: datetime + room_name_prefix: str, start_date: datetime, end_date: datetime, room: Room ): headers = { "Content-Type": "application/json; charset=utf-8", @@ -13,14 +14,14 @@ async def create_meeting( } data = { "templateType": "viewerMode", - "isLocked": False, + "isLocked": room.is_locked, "roomNamePrefix": room_name_prefix, "roomNamePattern": "uuid", - "roomMode": "normal", + "roomMode": room.room_mode, "startDate": start_date.isoformat(), "endDate": end_date.isoformat(), "recording": { - "type": "cloud", + "type": room.recording_type, "destination": { "provider": "s3", "bucket": settings.AWS_WHEREBY_S3_BUCKET, @@ -28,7 +29,7 @@ async def create_meeting( "accessKeySecret": settings.AWS_WHEREBY_ACCESS_KEY_SECRET, "fileFormat": "mp4", }, - "startTrigger": "automatic-2nd-participant", + "startTrigger": room.recording_trigger, }, } diff --git a/www/app/(app)/rooms/page.tsx b/www/app/(app)/rooms/page.tsx index 3477118d..c8256cba 100644 --- a/www/app/(app)/rooms/page.tsx +++ b/www/app/(app)/rooms/page.tsx @@ -32,7 +32,7 @@ import { } from "@chakra-ui/react"; import { useContext, useEffect, useState } from "react"; import { Container } from "@chakra-ui/react"; -import { FaEllipsisVertical, FaTrash, FaPencil } from "react-icons/fa6"; +import { FaEllipsisVertical, FaTrash, FaPencil, FaLink } from "react-icons/fa6"; import useApi from "../../lib/useApi"; import useRoomList from "./useRoomList"; import { DomainContext } from "../../domainContext"; @@ -51,14 +51,31 @@ interface SelectOption extends OptionBase { const RESERVED_PATHS = ["browse", "rooms", "transcripts"]; +const roomModeOptions: Options = [ + { label: "2-4 people", value: "normal" }, + { label: "2-200 people", value: "group" }, +]; + +const recordingTriggerOptions: Options = [ + { label: "None", value: "none" }, + { label: "Prompt", value: "prompt" }, + { label: "Automatic", value: "automatic-2nd-participant" }, +]; + +const roomInitialState = { + name: "", + zulipAutoPost: false, + zulipStream: "", + zulipTopic: "", + isLocked: false, + roomMode: "normal", + recordingType: "cloud", + recordingTrigger: "none", +}; + export default function RoomsList() { const { isOpen, onOpen, onClose } = useDisclosure(); - const [room, setRoom] = useState({ - name: "", - zulipAutoPost: false, - zulipStream: "", - zulipTopic: "", - }); + const [room, setRoom] = useState(roomInitialState); const [isEditing, setIsEditing] = useState(false); const [editRoomId, setEditRoomId] = useState(""); const api = useApi(); @@ -66,6 +83,7 @@ export default function RoomsList() { const { loading, response, refetch } = useRoomList(page); const [streams, setStreams] = useState([]); const [error, setError] = useState(""); + const [linkCopied, setLinkCopied] = useState(""); const { zulip_streams } = useContext(DomainContext); @@ -100,6 +118,16 @@ export default function RoomsList() { .find((stream) => stream.name === room.zulipStream) ?.topics.map((topic) => ({ label: topic, value: topic })) || []; + const handleCopyUrl = (roomName: string) => { + const roomUrl = `${window.location.origin}/${roomName}`; + navigator.clipboard.writeText(roomUrl); + setLinkCopied(roomName); + + setTimeout(() => { + setLinkCopied(""); + }, 2000); + }; + const handleSaveRoom = async () => { try { if (RESERVED_PATHS.includes(room.name)) { @@ -107,32 +135,29 @@ export default function RoomsList() { return; } + const roomData = { + name: room.name, + zulip_auto_post: room.zulipAutoPost, + zulip_stream: room.zulipStream, + zulip_topic: room.zulipTopic, + is_locked: room.isLocked, + room_mode: room.roomMode, + recording_type: room.recordingType, + recording_trigger: room.recordingTrigger, + }; + if (isEditing) { await api?.v1RoomsUpdate({ roomId: editRoomId, - requestBody: { - name: room.name, - zulip_auto_post: room.zulipAutoPost, - zulip_stream: room.zulipStream, - zulip_topic: room.zulipTopic, - }, + requestBody: roomData, }); } else { await api?.v1RoomsCreate({ - requestBody: { - name: room.name, - zulip_auto_post: room.zulipAutoPost, - zulip_stream: room.zulipStream, - zulip_topic: room.zulipTopic, - }, + requestBody: roomData, }); } - setRoom({ - name: "", - zulipAutoPost: false, - zulipStream: "", - zulipTopic: "", - }); + + setRoom(roomInitialState); setIsEditing(false); setEditRoomId(""); setError(""); @@ -149,6 +174,10 @@ export default function RoomsList() { zulipAutoPost: roomData.zulip_auto_post, zulipStream: roomData.zulip_stream, zulipTopic: roomData.zulip_topic, + isLocked: roomData.is_locked, + roomMode: roomData.room_mode, + recordingType: roomData.recording_type, + recordingTrigger: roomData.recording_trigger, }); setEditRoomId(roomId); setIsEditing(true); @@ -204,12 +233,7 @@ export default function RoomsList() { colorScheme="blue" onClick={() => { setIsEditing(false); - setRoom({ - name: "", - zulipAutoPost: false, - zulipStream: "", - zulipTopic: "", - }); + setRoom(roomInitialState); setError(""); onOpen(); }} @@ -236,6 +260,53 @@ export default function RoomsList() { {error && {error}} + + + Locked room + + + + Room size + rt.value === room.recordingTrigger, + )?.label, + value: room.recordingTrigger, + }} + onChange={(newValue) => + setRoom({ + ...room, + recordingTrigger: newValue!.value, + }) + } + /> + {roomData.name} + {linkCopied === roomData.name ? ( + + Link copied! + + ) : ( + } + onClick={() => handleCopyUrl(roomData.name)} + mr={2} + /> + )} +