From 6d40f7f5c4fd4abfd39af8ac0c26d26902c59e9f Mon Sep 17 00:00:00 2001 From: Hanson Gu Date: Sat, 10 Feb 2024 05:33:48 -0500 Subject: [PATCH 01/19] structure, set up config, db --- migration/migrator/data/course_tables.sql | 95 +++++++++++++++++++ .../course/20240209021641_lecture_chat.py | 46 +++++++++ site/app/controllers/ChatroomController.php | 56 +++++++++++ site/app/controllers/GlobalController.php | 9 ++ .../admin/ConfigurationController.php | 6 +- site/app/models/Chatroom.php | 0 site/app/models/Config.php | 8 +- site/app/templates/admin/Configuration.twig | 11 +++ site/app/templates/chatroom/ChatroomIns.twig | 14 +++ site/app/templates/chatroom/ChatroomStu.twig | 19 ++++ site/app/templates/chatroom/NewRoomForm.twig | 9 ++ site/app/views/ChatroomView.php | 37 ++++++++ site/config/course_template.json | 3 +- site/public/css/chatroom.css | 3 + 14 files changed, 311 insertions(+), 5 deletions(-) create mode 100644 migration/migrator/migrations/course/20240209021641_lecture_chat.py create mode 100644 site/app/controllers/ChatroomController.php create mode 100644 site/app/models/Chatroom.php create mode 100644 site/app/templates/chatroom/ChatroomIns.twig create mode 100644 site/app/templates/chatroom/ChatroomStu.twig create mode 100644 site/app/templates/chatroom/NewRoomForm.twig create mode 100644 site/app/views/ChatroomView.php create mode 100644 site/public/css/chatroom.css diff --git a/migration/migrator/data/course_tables.sql b/migration/migrator/data/course_tables.sql index b40b718fc94..1dbf2616fd9 100644 --- a/migration/migrator/data/course_tables.sql +++ b/migration/migrator/data/course_tables.sql @@ -702,6 +702,71 @@ CREATE SEQUENCE public.categories_list_category_id_seq ALTER SEQUENCE public.categories_list_category_id_seq OWNED BY public.categories_list.category_id; +-- +-- Name: chatroom_messages; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.chatroom_messages ( + id integer NOT NULL, + user_id character varying NOT NULL, + content text NOT NULL, + "timestamp" timestamp(0) with time zone NOT NULL +); + + +-- +-- Name: chatroom_messages_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.chatroom_messages_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: chatroom_messages_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.chatroom_messages_id_seq OWNED BY public.chatroom_messages.id; + + +-- +-- Name: chatrooms; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.chatrooms ( + id integer NOT NULL, + host_id integer NOT NULL, + title text NOT NULL, + description text NOT NULL, + is_active boolean DEFAULT false NOT NULL +); + + +-- +-- Name: chatrooms_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.chatrooms_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: chatrooms_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.chatrooms_id_seq OWNED BY public.chatrooms.id; + + -- -- Name: course_materials; Type: TABLE; Schema: public; Owner: - -- @@ -1880,6 +1945,20 @@ ALTER TABLE ONLY public.calendar_messages ALTER COLUMN id SET DEFAULT nextval('p ALTER TABLE ONLY public.categories_list ALTER COLUMN category_id SET DEFAULT nextval('public.categories_list_category_id_seq'::regclass); +-- +-- Name: chatroom_messages id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.chatroom_messages ALTER COLUMN id SET DEFAULT nextval('public.chatroom_messages_id_seq'::regclass); + + +-- +-- Name: chatrooms id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.chatrooms ALTER COLUMN id SET DEFAULT nextval('public.chatrooms_id_seq'::regclass); + + -- -- Name: course_materials id; Type: DEFAULT; Schema: public; Owner: - -- @@ -2052,6 +2131,22 @@ ALTER TABLE ONLY public.categories_list ADD CONSTRAINT category_unique UNIQUE (category_desc); +-- +-- Name: chatroom_messages chatroom_messages_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.chatroom_messages + ADD CONSTRAINT chatroom_messages_pkey PRIMARY KEY (id); + + +-- +-- Name: chatrooms chatrooms_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.chatrooms + ADD CONSTRAINT chatrooms_pkey PRIMARY KEY (id); + + -- -- Name: course_materials course_materials_path_key; Type: CONSTRAINT; Schema: public; Owner: - -- diff --git a/migration/migrator/migrations/course/20240209021641_lecture_chat.py b/migration/migrator/migrations/course/20240209021641_lecture_chat.py new file mode 100644 index 00000000000..807de076f61 --- /dev/null +++ b/migration/migrator/migrations/course/20240209021641_lecture_chat.py @@ -0,0 +1,46 @@ +"""Migration for a given Submitty course database.""" + +import json +from pathlib import Path + +def up(config, database, semester, course): + """ + Run up migration. + :param config: Object holding configuration details about Submitty + :type config: migrator.config.Config + :param database: Object for interacting with given database for environment + :type database: migrator.db.Database + :param semester: Semester of the course being migrated + :type semester: str + :param course: Code of course being migrated + :type course: str + """ + database.execute("CREATE TABLE IF NOT EXISTS chatrooms(id SERIAL PRIMARY KEY, host_id integer NOT NULL, title text NOT NULL, description text NOT NULL, is_active BOOLEAN DEFAULT false NOT NULL)") + database.execute("CREATE TABLE IF NOT EXISTS chatroom_messages(id SERIAL PRIMARY KEY, user_id character varying NOT NULL, content text NOT NULL, timestamp timestamp(0) with time zone NOT NULL)") + + course_dir = Path(config.submitty['submitty_data_dir'], 'courses', semester, course) + # add boolean to course config + config_file = Path(course_dir, 'config', 'config.json') + if config_file.is_file(): + with open(config_file, 'r') as in_file: + j = json.load(in_file) + j['course_details']['chat_enabled'] = False + + with open(config_file, 'w') as out_file: + json.dump(j, out_file, indent=4) + + +def down(config, database, semester, course): + """ + Run down migration (rollback). + :param config: Object holding configuration details about Submitty + :type config: migrator.config.Config + :param database: Object for interacting with given database for environment + :type database: migrator.db.Database + :param semester: Semester of the course being migrated + :type semester: str + :param course: Code of course being migrated + :type course: str + """ + database.execute("DROP TABLE IF EXISTS chatrooms") + database.execute("DROP TABLE IF EXISTS chatroom_messages") \ No newline at end of file diff --git a/site/app/controllers/ChatroomController.php b/site/app/controllers/ChatroomController.php new file mode 100644 index 00000000000..d12728b906f --- /dev/null +++ b/site/app/controllers/ChatroomController.php @@ -0,0 +1,56 @@ +core->getUser()->accessAdmin()) { + + return new WebResponse( + 'Chatroom', + 'showChatroomPageInstructor' + ); + } + else { // Student view + return new WebResponse( + 'Chatroom', + 'showChatroomPageStudent' + ); + } + } + + // Activates the chatroom + public function activateChatroom() { + // Logic to mark the chatroom as active in the database + // Ensure any previously active chatroom is marked as inactive + } + + // Deactivates the chatroom + public function deactivateChatroom() { + // Logic to mark the chatroom as inactive + } + + // Handles message submission for the active chatroom + public function publishMessage() { + // Insert message into chatroom_messages table + } + + // Fetches messages for the active chatroom + public function getMessages() { + // Fetch messages from chatroom_messages table + } +} diff --git a/site/app/controllers/GlobalController.php b/site/app/controllers/GlobalController.php index 8752bb7517a..ec790e848cd 100644 --- a/site/app/controllers/GlobalController.php +++ b/site/app/controllers/GlobalController.php @@ -165,6 +165,15 @@ public function prep_course_sidebar(&$sidebar_buttons, $unread_notifications_cou ]); } + if ($this->core->getConfig()->isChatEnabled()) { + $sidebar_buttons[] = new NavButton($this->core, [ + "href" => $this->core->buildCourseUrl(['chat']), + "title" => "Live Lecture Chat", + "id" => "nav-sidebar-chat", + "icon" => "far fa-smiley" + ]); + } + $course_path = $this->core->getConfig()->getCoursePath(); $course_materials_path = $course_path . "/uploads/course_materials"; $empty = FileUtils::isEmptyDir($course_materials_path); diff --git a/site/app/controllers/admin/ConfigurationController.php b/site/app/controllers/admin/ConfigurationController.php index e5f0b424310..178fece319a 100644 --- a/site/app/controllers/admin/ConfigurationController.php +++ b/site/app/controllers/admin/ConfigurationController.php @@ -54,7 +54,8 @@ public function viewConfiguration(): MultiResponse { 'seek_message_enabled' => $this->core->getConfig()->isSeekMessageEnabled(), 'seek_message_instructions' => $this->core->getConfig()->getSeekMessageInstructions(), 'queue_announcement_message' => $this->core->getConfig()->getQueueAnnouncementMessage(), - 'polls_enabled' => $this->core->getConfig()->isPollsEnabled() + 'polls_enabled' => $this->core->getConfig()->isPollsEnabled(), + 'chat_enabled' => $this->core->getCOnfig()->isChatEnabled() ]; $seating_options = $this->getGradeableSeatingOptions(); $admin_in_course = false; @@ -149,7 +150,8 @@ public function updateConfiguration(): MultiResponse { 'seating_only_for_instructor', 'queue_enabled', 'seek_message_enabled', - 'polls_enabled' + 'polls_enabled', + 'chat_enabled' ] ) ) { diff --git a/site/app/models/Chatroom.php b/site/app/models/Chatroom.php new file mode 100644 index 00000000000..e69de29bb2d diff --git a/site/app/models/Config.php b/site/app/models/Config.php index 43bee9bf307..c5750baa072 100644 --- a/site/app/models/Config.php +++ b/site/app/models/Config.php @@ -68,6 +68,7 @@ * @method bool isQueueEnabled() * @method bool isSeekMessageEnabled() * @method bool isPollsEnabled() + * @method bool isChatEnabled() * @method void setTerm(string $term) * @method void setCourse(string $course) * @method void setCoursePath(string $course_path) @@ -328,7 +329,9 @@ class Config extends AbstractModel { /** @prop * @var bool */ protected $polls_enabled; - + /** @prop + * @var bool */ + protected $chat_enabled; /** @prop-read * @var array */ @@ -575,7 +578,7 @@ public function loadCourseJson($semester, $course, $course_json_path) { 'zero_rubric_grades', 'upload_message', 'display_rainbow_grades_summary', 'display_custom_message', 'room_seating_gradeable_id', 'course_email', 'vcs_base_url', 'vcs_type', 'private_repository', 'forum_enabled', 'forum_create_thread_message', 'seating_only_for_instructor', - 'grade_inquiry_message', 'auto_rainbow_grades', 'queue_enabled', 'queue_message', 'polls_enabled', 'queue_announcement_message', 'seek_message_enabled', 'seek_message_instructions' + 'grade_inquiry_message', 'auto_rainbow_grades', 'queue_enabled', 'queue_message', 'polls_enabled', 'chat_enabled', 'queue_announcement_message', 'seek_message_enabled', 'seek_message_instructions' ]; $this->setConfigValues($this->course_json, 'course_details', $array); @@ -604,6 +607,7 @@ public function loadCourseJson($semester, $course, $course_json_path) { 'seating_only_for_instructor', 'queue_enabled', 'polls_enabled', + 'chat_enabled', 'seek_message_enabled', ]; foreach ($array as $key) { diff --git a/site/app/templates/admin/Configuration.twig b/site/app/templates/admin/Configuration.twig index 42d06694903..ff430c68b42 100644 --- a/site/app/templates/admin/Configuration.twig +++ b/site/app/templates/admin/Configuration.twig @@ -90,6 +90,17 @@ Choose whether to enable online polling for this course. + +

Live Lecture Chat

+
+
+ + +

Submissions


diff --git a/site/app/templates/chatroom/ChatroomIns.twig b/site/app/templates/chatroom/ChatroomIns.twig new file mode 100644 index 00000000000..3a984c5e2af --- /dev/null +++ b/site/app/templates/chatroom/ChatroomIns.twig @@ -0,0 +1,14 @@ +
+

Lecture Chat Room

+ + New Room + + + +
diff --git a/site/app/templates/chatroom/ChatroomStu.twig b/site/app/templates/chatroom/ChatroomStu.twig new file mode 100644 index 00000000000..ee5db1733cd --- /dev/null +++ b/site/app/templates/chatroom/ChatroomStu.twig @@ -0,0 +1,19 @@ +
+
+ +
+
+ + +
+
+ + +
+
diff --git a/site/app/templates/chatroom/NewRoomForm.twig b/site/app/templates/chatroom/NewRoomForm.twig new file mode 100644 index 00000000000..cf3cf113034 --- /dev/null +++ b/site/app/templates/chatroom/NewRoomForm.twig @@ -0,0 +1,9 @@ +{% extends 'generic/Popup.twig' %} +{% block popup_id %}create-new-room{% endblock %} +{% block title %}Create New Room{% endblock %} +{% block body %} + + + +{% endblock %} + diff --git a/site/app/views/ChatroomView.php b/site/app/views/ChatroomView.php new file mode 100644 index 00000000000..79f89218a63 --- /dev/null +++ b/site/app/views/ChatroomView.php @@ -0,0 +1,37 @@ +core->getOutput()->addBreadcrumb("Live Lecture Chat", $this->core->buildCourseUrl(['chat'])); + $this->core->getOutput()->addInternalCss('chatroom.css'); + $this->core->getOutput()->enableMobileViewport(); + } + + public function showChatroomPageInstructor() + { + return $this->core->getOutput()->renderTwigTemplate("chatroom/ChatroomIns.twig", [ + 'base_url' => $this->core->buildCourseUrl() . '/chat', + 'semester' => $this->core->getConfig()->getTerm(), + 'course' => $this->core->getConfig()->getCourse() + ]); + } + + public function showChatroomPageStudent() + { + return $this->core->getOutput()->renderTwigTemplate("chatroom/ChatroomStu.twig", [ + 'base_url' => $this->core->buildCourseUrl() . '/chat', + 'semester' => $this->core->getConfig()->getTerm(), + 'course' => $this->core->getConfig()->getCourse() + ]); + } +} + + + diff --git a/site/config/course_template.json b/site/config/course_template.json index 2459e6cf0c5..ab9bd9e4a52 100644 --- a/site/config/course_template.json +++ b/site/config/course_template.json @@ -29,6 +29,7 @@ "polls_pts_for_incorrect": 0.0, "seek_message_enabled": false, "seek_message_instructions": "Optionally, provide your local timezone, desired project topic, or other information that would be relevant to forming your team.", - "git_autograding_branch": "main" + "git_autograding_branch": "main", + "chat_enabled": false } } diff --git a/site/public/css/chatroom.css b/site/public/css/chatroom.css new file mode 100644 index 00000000000..f73e81e984e --- /dev/null +++ b/site/public/css/chatroom.css @@ -0,0 +1,3 @@ +.welcome { + color: green; +} \ No newline at end of file From 4b12a158f4a0267e15bac9eccce0780e692c4c66 Mon Sep 17 00:00:00 2001 From: Hanson Gu Date: Sun, 11 Feb 2024 19:18:58 -0500 Subject: [PATCH 02/19] basic functionality and chatroom template --- migration/migrator/data/course_tables.sql | 4 +- .../course/20240209021641_lecture_chat.py | 2 +- site/app/controllers/ChatroomController.php | 126 +++++++++++++++--- site/app/entities/chat/Chatroom.php | 93 +++++++++++++ site/app/entities/chat/Message.php | 59 ++++++++ .../repositories/chat/ChatroomRepository.php | 47 +++++++ site/app/templates/chat/ChatPageIns.twig | 42 ++++++ site/app/templates/chat/ChatPageStu.twig | 39 ++++++ site/app/templates/chat/Chatroom.twig | 42 ++++++ .../templates/chat/CreateChatroomForm.twig | 21 +++ site/app/templates/chatroom/ChatroomIns.twig | 14 -- site/app/templates/chatroom/ChatroomStu.twig | 19 --- site/app/templates/chatroom/NewRoomForm.twig | 9 -- site/app/views/ChatroomView.php | 37 +++-- site/public/css/chat.css | 83 ++++++++++++ site/public/css/chatroom.css | 3 - site/public/css/server.css | 78 +++++++++++ site/public/js/server.js | 7 + 18 files changed, 646 insertions(+), 79 deletions(-) create mode 100644 site/app/entities/chat/Chatroom.php create mode 100644 site/app/entities/chat/Message.php create mode 100644 site/app/repositories/chat/ChatroomRepository.php create mode 100644 site/app/templates/chat/ChatPageIns.twig create mode 100644 site/app/templates/chat/ChatPageStu.twig create mode 100644 site/app/templates/chat/Chatroom.twig create mode 100644 site/app/templates/chat/CreateChatroomForm.twig delete mode 100644 site/app/templates/chatroom/ChatroomIns.twig delete mode 100644 site/app/templates/chatroom/ChatroomStu.twig delete mode 100644 site/app/templates/chatroom/NewRoomForm.twig create mode 100644 site/public/css/chat.css delete mode 100644 site/public/css/chatroom.css diff --git a/migration/migrator/data/course_tables.sql b/migration/migrator/data/course_tables.sql index 1dbf2616fd9..c7092c6845f 100644 --- a/migration/migrator/data/course_tables.sql +++ b/migration/migrator/data/course_tables.sql @@ -740,9 +740,9 @@ ALTER SEQUENCE public.chatroom_messages_id_seq OWNED BY public.chatroom_messages CREATE TABLE public.chatrooms ( id integer NOT NULL, - host_id integer NOT NULL, + host_id character varying NOT NULL, title text NOT NULL, - description text NOT NULL, + description text, is_active boolean DEFAULT false NOT NULL ); diff --git a/migration/migrator/migrations/course/20240209021641_lecture_chat.py b/migration/migrator/migrations/course/20240209021641_lecture_chat.py index 807de076f61..5a1675e6c8e 100644 --- a/migration/migrator/migrations/course/20240209021641_lecture_chat.py +++ b/migration/migrator/migrations/course/20240209021641_lecture_chat.py @@ -15,7 +15,7 @@ def up(config, database, semester, course): :param course: Code of course being migrated :type course: str """ - database.execute("CREATE TABLE IF NOT EXISTS chatrooms(id SERIAL PRIMARY KEY, host_id integer NOT NULL, title text NOT NULL, description text NOT NULL, is_active BOOLEAN DEFAULT false NOT NULL)") + database.execute("CREATE TABLE IF NOT EXISTS chatrooms(id SERIAL PRIMARY KEY, host_id character varying NOT NULL, title text NOT NULL, description text, is_active BOOLEAN DEFAULT false NOT NULL)") database.execute("CREATE TABLE IF NOT EXISTS chatroom_messages(id SERIAL PRIMARY KEY, user_id character varying NOT NULL, content text NOT NULL, timestamp timestamp(0) with time zone NOT NULL)") course_dir = Path(config.submitty['submitty_data_dir'], 'courses', semester, course) diff --git a/site/app/controllers/ChatroomController.php b/site/app/controllers/ChatroomController.php index d12728b906f..8ea6db9f04f 100644 --- a/site/app/controllers/ChatroomController.php +++ b/site/app/controllers/ChatroomController.php @@ -2,10 +2,18 @@ namespace app\controllers; - +use app\entities\poll\Poll; use app\libraries\Core; +use app\libraries\response\RedirectResponse; use app\libraries\response\WebResponse; +use app\entities\chat\Chatroom; +use app\entities\chat\Message; +use app\libraries\routers\AccessControl; +use app\libraries\routers\Enabled; use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Doctrine\ORM\EntityManagerInterface; class ChatroomController extends AbstractController { @@ -16,41 +24,119 @@ public function __construct(Core $core) { /** * @Route("/courses/{_semester}/{_course}/chat", methods={"GET"}) */ - public function showChatroomPage(): WebResponse { - - if ($this->core->getUser()->accessAdmin()) { + public function showChatroomsPage(): WebResponse { + /** @var \app\repositories\chat\ChatroomRepository */ + $repo = $this->core->getCourseEntityManager()->getRepository(Chatroom::class); + $user = $this->core->getUser(); + $chatrooms = $repo->findAll(); + $active_chatrooms = $repo->findAllActiveChatrooms(); + if ($user->accessAdmin()) { return new WebResponse( 'Chatroom', - 'showChatroomPageInstructor' + 'showChatPageInstructor', + $chatrooms ); } else { // Student view return new WebResponse( 'Chatroom', - 'showChatroomPageStudent' + 'showChatPageStudent', + $active_chatrooms ); } } - // Activates the chatroom - public function activateChatroom() { - // Logic to mark the chatroom as active in the database - // Ensure any previously active chatroom is marked as inactive - } + /** + * @Route("/courses/{_semester}/{_course}/chat/{chatroom_id}", methods={"GET"}, requirements={"chatroom_id": "\d*", }) + * @return RedirectResponse|WebResponse + */ + public function showChatroom(string $chatroom_id): WebResponse|RedirectResponse { + + /** @var \app\repositories\poll\PollRepository */ + $repo = $this->core->getCourseEntityManager()->getRepository(Chatroom::class); + $chatroom = $repo->findByChatroomId($chatroom_id); + + if (!is_numeric($chatroom_id)) { + $this->core->addErrorMessage("Invalid Chatroom ID"); + return new RedirectResponse($this->core->buildCourseUrl(['chat'])); + } + + return new WebResponse( + 'Chatroom', + 'showChatroom', + $chatroom + ); + } + + /** + * @Route("/courses/{_semester}/{_course}/chat/newChatroom", name="new_chatroom", methods={"POST"}) + * @AccessControl(role="INSTRUCTOR") + */ + public function addChatroom(): RedirectResponse { + $em = $this->core->getCourseEntityManager(); + + $fields = ['title', 'description']; + foreach ($fields as $field) { + if (empty($_POST[$field])) { + $this->core->addErrorMessage("Chatroom must fill out all fields"); + return new RedirectResponse($this->core->buildCourseUrl(['chat'])); + } + } + + $chatroom = new Chatroom(); + $chatroom->setTitle($_POST['title']); + $chatroom->setDescription($_POST['description']); + $chatroom->setHostId($this->core->getUser()->getId()); + $em->persist($chatroom); + $em->flush(); - // Deactivates the chatroom - public function deactivateChatroom() { - // Logic to mark the chatroom as inactive + $this->core->addSuccessMessage("Chatroom successfully added"); + return new RedirectResponse($this->core->buildCourseUrl(['chat'])); } - // Handles message submission for the active chatroom - public function publishMessage() { - // Insert message into chatroom_messages table + /** + * @Route("/chat/{chatroomId}/message", name="chatroom_post_message", methods={"POST"}) + */ + public function postMessage(int $chatroomId, Request $request, EntityManagerInterface $entityManager): JsonResponse { + $content = $request->request->get('content'); + + $chatroom = $entityManager->getRepository(Chatroom::class)->find($chatroomId); + if (!$chatroom || !$chatroom->getIsActive()) { + return new JsonResponse(['error' => 'Chatroom not found or not active.'], Response::HTTP_NOT_FOUND); + } + + $message = new Message(); + $message->setContent($content); + $message->setChatroom($chatroom); + // Set other necessary properties, such as timestamp + + $entityManager->persist($message); + $entityManager->flush(); + + return new JsonResponse(['success' => true, 'messageId' => $message->getId()]); } - // Fetches messages for the active chatroom - public function getMessages() { - // Fetch messages from chatroom_messages table + /** + * @Route("/chat/{chatroomId}/messages", name="chatroom_messages", methods={"GET"}) + */ + public function getMessages(int $chatroomId, EntityManagerInterface $entityManager): JsonResponse { + $chatroom = $entityManager->getRepository(Chatroom::class)->find($chatroomId); + if (!$chatroom) { + return new JsonResponse(['error' => 'Chatroom not found.'], Response::HTTP_NOT_FOUND); + } + + $messages = $chatroom->getMessages(); + $response = []; + + foreach ($messages as $message) { + $response[] = [ + 'id' => $message->getId(), + 'content' => $message->getContent(), + 'timestamp' => $message->getTimestamp()->format('Y-m-d H:i:s') + ]; + } + + return new JsonResponse(['messages' => $response]); } } diff --git a/site/app/entities/chat/Chatroom.php b/site/app/entities/chat/Chatroom.php new file mode 100644 index 00000000000..128f9bd96b2 --- /dev/null +++ b/site/app/entities/chat/Chatroom.php @@ -0,0 +1,93 @@ + + */ + #[ORM\OneToMany(mappedBy: "chat", targetEntity: Message::class)] + protected Collection $messages; + + public function __construct() { + + $this->messages = new ArrayCollection(); + $this->isActive = true; + } + + public function getId(): int { + return $this->id; + } + + public function setHostId($hostId): void { + $this->host_id = $hostId; + } + + public function getHostId(): string { + return $this->host_id; + } + + public function getTitle(): string { + return $this->title; + } + + public function setTitle(string $title): void { + $this->title = $title; + } + + public function getDescription(): string { + return $this->description; + } + + public function setDescription($description): void { + $this->description = $description; + } + + public function getStatus(): bool { + return $this->isActive; + } + + public function activate(): void { + $this->isActive = True; + } + + public function deactivate(): void { + $this->isActive = False; + } + + public function getMessages(): Collection { + return $this->messages; + } + + public function addMessage(Message $message): void { + $this->messages->add($message); + } +} \ No newline at end of file diff --git a/site/app/entities/chat/Message.php b/site/app/entities/chat/Message.php new file mode 100644 index 00000000000..b2c54a51d43 --- /dev/null +++ b/site/app/entities/chat/Message.php @@ -0,0 +1,59 @@ +timestamp = new \DateTime(); + } + + public function getId(): int { + return $this->id; + } + + public function getContent(): string { + return $this->content; + } + + public function setContent(string $text): void { + $this->content = $text; + } + + public function getTimestamp(): \DateTime { + return $this->timestamp; + } + + public function getChatroom(): Chatroom { + return $this->chatroom; + } + + public function setChatroom(Chatroom $chatroom): self { + $this->chatroom = $chatroom; + return $this; + } +} diff --git a/site/app/repositories/chat/ChatroomRepository.php b/site/app/repositories/chat/ChatroomRepository.php new file mode 100644 index 00000000000..13d4331b0cf --- /dev/null +++ b/site/app/repositories/chat/ChatroomRepository.php @@ -0,0 +1,47 @@ +createQueryBuilder('c') + ->where('c.host_id = :hostId') + ->setParameter('hostId', $hostId) + ->getQuery() + ->getResult(); + } + + public function findAllActiveChatrooms() { + return $this->createQueryBuilder('c') + ->where('c.isActive = :isActive') + ->setParameter('isActive', true) + ->getQuery() + ->getResult(); + } + + public function findAllInactiveChatrooms() { + return $this->createQueryBuilder('c') + ->where('c.isActive = :isActive') + ->setParameter('isActive', false) + ->getQuery() + ->getResult(); + } + + public function findByChatroomId(string $chatroomId): ?Chatroom { + $result = $this->createQueryBuilder('c') + ->where('c.id = :id') + ->setParameter('id', $chatroomId) + ->getQuery() + ->getResult(); + if (count($result) === 0) { + return null; + } + return $result[0]; + } +} diff --git a/site/app/templates/chat/ChatPageIns.twig b/site/app/templates/chat/ChatPageIns.twig new file mode 100644 index 00000000000..dad623080c9 --- /dev/null +++ b/site/app/templates/chat/ChatPageIns.twig @@ -0,0 +1,42 @@ +
+

Live Lecture Chat

+ +
+
+

Chatrooms

+ + + + + + + + + + + + + + + {% for chatroom in chatrooms %} + + + + + + + {% endfor %} + +
NameDescriptionHost
+ {{ chatroom.getTitle() }} + + {{ chatroom.getDescription() }} + + {{ chatroom.getHostId() }} + + Join +
+
+
+ +{% include('chat/CreateChatroomForm.twig') %} \ No newline at end of file diff --git a/site/app/templates/chat/ChatPageStu.twig b/site/app/templates/chat/ChatPageStu.twig new file mode 100644 index 00000000000..ec8832c2a0b --- /dev/null +++ b/site/app/templates/chat/ChatPageStu.twig @@ -0,0 +1,39 @@ +
+

Live Lecture Chat

+
+
+

Chatrooms

+ + + + + + + + + + + + + + + {% for chatroom in chatrooms %} + + + + + + + {% endfor %} + +
NameDescriptionHost
+ {{ chatroom.getTitle() }} + + {{ chatroom.getDescription() }} + + {{ chatroom.getHostId() }} + + Join +
+
+
\ No newline at end of file diff --git a/site/app/templates/chat/Chatroom.twig b/site/app/templates/chat/Chatroom.twig new file mode 100644 index 00000000000..81d7a834aae --- /dev/null +++ b/site/app/templates/chat/Chatroom.twig @@ -0,0 +1,42 @@ +{% block body %} +
+
+
+

{{ chatroom.getTitle() }}

+ +
+ +
+
+ +
+ Alice: +

Hey, how's everyone doing today?

+ 09:45 +
+
+ You: +

Doing great, thanks! How about you?

+ 09:46 +
+ +
+ +
+ + +
+
+ + +
+
+{% endblock %} + diff --git a/site/app/templates/chat/CreateChatroomForm.twig b/site/app/templates/chat/CreateChatroomForm.twig new file mode 100644 index 00000000000..422ef195800 --- /dev/null +++ b/site/app/templates/chat/CreateChatroomForm.twig @@ -0,0 +1,21 @@ +{% set url_path = "/newChatroom" %} +{% extends 'generic/Popup.twig' %} +{% block popup_id %}create-chatroom-form{% endblock %} +{% block title %}Create Chatroom{% endblock %} +{% block body %} +
+
+ + + + + + + +
+
+{% endblock %} diff --git a/site/app/templates/chatroom/ChatroomIns.twig b/site/app/templates/chatroom/ChatroomIns.twig deleted file mode 100644 index 3a984c5e2af..00000000000 --- a/site/app/templates/chatroom/ChatroomIns.twig +++ /dev/null @@ -1,14 +0,0 @@ -
-

Lecture Chat Room

- - New Room - - - -
diff --git a/site/app/templates/chatroom/ChatroomStu.twig b/site/app/templates/chatroom/ChatroomStu.twig deleted file mode 100644 index ee5db1733cd..00000000000 --- a/site/app/templates/chatroom/ChatroomStu.twig +++ /dev/null @@ -1,19 +0,0 @@ -
-
- -
-
- - -
-
- - -
-
diff --git a/site/app/templates/chatroom/NewRoomForm.twig b/site/app/templates/chatroom/NewRoomForm.twig deleted file mode 100644 index cf3cf113034..00000000000 --- a/site/app/templates/chatroom/NewRoomForm.twig +++ /dev/null @@ -1,9 +0,0 @@ -{% extends 'generic/Popup.twig' %} -{% block popup_id %}create-new-room{% endblock %} -{% block title %}Create New Room{% endblock %} -{% block body %} - - - -{% endblock %} - diff --git a/site/app/views/ChatroomView.php b/site/app/views/ChatroomView.php index 79f89218a63..872c9337727 100644 --- a/site/app/views/ChatroomView.php +++ b/site/app/views/ChatroomView.php @@ -3,32 +3,47 @@ namespace app\views; use app\libraries\Core; +use app\libraries\FileUtils; use app\libraries\Output; +use app\libraries\PollUtils; class ChatroomView extends AbstractView { public function __construct(Core $core, Output $output) { parent::__construct($core, $output); - $this->core->getOutput()->addBreadcrumb("Live Lecture Chat", $this->core->buildCourseUrl(['chat'])); - $this->core->getOutput()->addInternalCss('chatroom.css'); - $this->core->getOutput()->enableMobileViewport(); } - public function showChatroomPageInstructor() - { - return $this->core->getOutput()->renderTwigTemplate("chatroom/ChatroomIns.twig", [ + public function showChatPageInstructor(array $chatrooms) { + $this->core->getOutput()->addInternalCss('chat.css'); + return $this->core->getOutput()->renderTwigTemplate("chat/ChatPageIns.twig", [ + 'csrf_token' => $this->core->getCsrfToken(), 'base_url' => $this->core->buildCourseUrl() . '/chat', 'semester' => $this->core->getConfig()->getTerm(), - 'course' => $this->core->getConfig()->getCourse() + 'course' => $this->core->getConfig()->getCourse(), + 'chatrooms' => $chatrooms ]); } - public function showChatroomPageStudent() - { - return $this->core->getOutput()->renderTwigTemplate("chatroom/ChatroomStu.twig", [ + public function showChatPageStudent(array $chatrooms) { + $this->core->getOutput()->addInternalCss('chat.css'); + return $this->core->getOutput()->renderTwigTemplate("chat/ChatPageStu.twig", [ + 'csrf_token' => $this->core->getCsrfToken(), 'base_url' => $this->core->buildCourseUrl() . '/chat', 'semester' => $this->core->getConfig()->getTerm(), - 'course' => $this->core->getConfig()->getCourse() + 'course' => $this->core->getConfig()->getCourse(), + 'chatrooms' => $chatrooms + ]); + } + + public function showChatroom($chatroom) { + $this->core->getOutput()->addInternalCss('chat.css'); + $this->core->getOutput()->addBreadcrumb("Chatroom"); + + return $this->core->getOutput()->renderTwigTemplate("chat/Chatroom.twig", [ + 'csrf_token' => $this->core->getCsrfToken(), + 'base_url' => $this->core->buildCourseUrl() . '/chat', + 'chatroom' => $chatroom, + 'user_admin' => $this->core->getUser()->accessAdmin() ]); } } diff --git a/site/public/css/chat.css b/site/public/css/chat.css new file mode 100644 index 00000000000..bfa757d2b64 --- /dev/null +++ b/site/public/css/chat.css @@ -0,0 +1,83 @@ +body, html { + margin: 0; + padding: 0; + font-family: Arial, sans-serif; +} + +.chatroom-container { + display: flex; + flex-direction: column; + height: 100vh; +} + +.chatroom-header { + background-color: #007bff; + color: #ffffff; + padding: 10px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.chatroom-main { + flex: 1; + display: flex; + flex-direction: column; +} + +.message-area { + flex: 1; + overflow-y: auto; + padding: 10px; + background-color: #f9f9f9; +} + +.message { + margin-bottom: 10px; + padding: 5px; + border-radius: 5px; +} + +.message.you { + background-color: #e0f7fa; + align-self: flex-end; +} + +.message.other { + background-color: #e0e0e0; +} + +.user { + font-weight: bold; +} + +.time { + font-size: 0.8em; +} + +.input-area { + display: flex; + padding: 10px; + background-color: #fff; +} + +#messageInput { + flex: 1; + margin-right: 10px; +} + +.chatroom-sidebar { + background-color: #f2f2f2; + padding: 10px; + width: 200px; +} + +.chatroom-sidebar ul { + list-style: none; + padding: 0; +} + +.chatroom-sidebar ul li { + padding: 5px; + border-bottom: 1px solid #ddd; +} diff --git a/site/public/css/chatroom.css b/site/public/css/chatroom.css deleted file mode 100644 index f73e81e984e..00000000000 --- a/site/public/css/chatroom.css +++ /dev/null @@ -1,3 +0,0 @@ -.welcome { - color: green; -} \ No newline at end of file diff --git a/site/public/css/server.css b/site/public/css/server.css index 889529bdd95..09e6bd9a910 100644 --- a/site/public/css/server.css +++ b/site/public/css/server.css @@ -2328,3 +2328,81 @@ header > * { float: none; } /* stylelint-enable no-descending-specificity */ + +.chatroom-container { + display: flex; + flex-direction: column; + height: 100vh; +} + +.chatroom-header { + background-color: #007bff; + color: #ffffff; + padding: 10px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.chatroom-main { + flex: 1; + display: flex; + flex-direction: column; +} + +.message-area { + flex: 1; + overflow-y: auto; + padding: 10px; + background-color: #f9f9f9; +} + +.message { + margin-bottom: 10px; + padding: 5px; + border-radius: 5px; +} + +.message.you { + background-color: #e0f7fa; + align-self: flex-end; +} + +.message.other { + background-color: #e0e0e0; +} + +.user { + font-weight: bold; +} + +.time { + font-size: 0.8em; +} + +.input-area { + display: flex; + padding: 10px; + background-color: #fff; +} + +#messageInput { + flex: 1; + margin-right: 10px; +} + +.chatroom-sidebar { + background-color: #f2f2f2; + padding: 10px; + width: 200px; +} + +.chatroom-sidebar ul { + list-style: none; + padding: 0; +} + +.chatroom-sidebar ul li { + padding: 5px; + border-bottom: 1px solid #ddd; +} diff --git a/site/public/js/server.js b/site/public/js/server.js index 2c89ec6ce08..13e14bdcdef 100644 --- a/site/public/js/server.js +++ b/site/public/js/server.js @@ -1892,4 +1892,11 @@ function tzWarn() { Cookies.set("last_tz_warn_time", Date.now()); } } + document.addEventListener('DOMContentLoaded', tzWarn); + +function newChatroomForm() { + $('.popup-form').css('display', 'none'); + var form = $("#create-chatroom-form"); + form.css("display", "block"); +} \ No newline at end of file From a24e7101da46ec499c0ab7015f590b23b70dd8b5 Mon Sep 17 00:00:00 2001 From: Hanson Gu Date: Tue, 13 Feb 2024 16:42:41 -0500 Subject: [PATCH 03/19] added buttons on instructor's page --- migration/migrator/data/course_tables.sql | 1 + .../course/20240209021641_lecture_chat.py | 2 +- site/app/controllers/ChatroomController.php | 85 +++++++----- site/app/entities/chat/Chatroom.php | 10 +- site/app/entities/chat/Message.php | 34 ++++- site/app/templates/chat/ChatPageIns.twig | 52 +++++++- site/app/templates/chat/Chatroom.twig | 124 ++++++++++++------ site/app/views/ChatroomView.php | 14 +- site/public/css/chat.css | 83 ------------ site/public/css/chatroom.css | 64 +++++++++ site/public/css/server.css | 78 ----------- site/public/js/chatroom.js | 0 12 files changed, 298 insertions(+), 249 deletions(-) delete mode 100644 site/public/css/chat.css create mode 100644 site/public/css/chatroom.css create mode 100644 site/public/js/chatroom.js diff --git a/migration/migrator/data/course_tables.sql b/migration/migrator/data/course_tables.sql index c7092c6845f..410e7102934 100644 --- a/migration/migrator/data/course_tables.sql +++ b/migration/migrator/data/course_tables.sql @@ -708,6 +708,7 @@ ALTER SEQUENCE public.categories_list_category_id_seq OWNED BY public.categories CREATE TABLE public.chatroom_messages ( id integer NOT NULL, + chatroom_id integer NOT NULL, user_id character varying NOT NULL, content text NOT NULL, "timestamp" timestamp(0) with time zone NOT NULL diff --git a/migration/migrator/migrations/course/20240209021641_lecture_chat.py b/migration/migrator/migrations/course/20240209021641_lecture_chat.py index 5a1675e6c8e..ac388f99f63 100644 --- a/migration/migrator/migrations/course/20240209021641_lecture_chat.py +++ b/migration/migrator/migrations/course/20240209021641_lecture_chat.py @@ -16,7 +16,7 @@ def up(config, database, semester, course): :type course: str """ database.execute("CREATE TABLE IF NOT EXISTS chatrooms(id SERIAL PRIMARY KEY, host_id character varying NOT NULL, title text NOT NULL, description text, is_active BOOLEAN DEFAULT false NOT NULL)") - database.execute("CREATE TABLE IF NOT EXISTS chatroom_messages(id SERIAL PRIMARY KEY, user_id character varying NOT NULL, content text NOT NULL, timestamp timestamp(0) with time zone NOT NULL)") + database.execute("CREATE TABLE IF NOT EXISTS chatroom_messages(id SERIAL PRIMARY KEY, chatroom_id integer NOT NULL, user_id character varying NOT NULL, content text NOT NULL, timestamp timestamp(0) with time zone NOT NULL)") course_dir = Path(config.submitty['submitty_data_dir'], 'courses', semester, course) # add boolean to course config diff --git a/site/app/controllers/ChatroomController.php b/site/app/controllers/ChatroomController.php index 8ea6db9f04f..735aadb09ea 100644 --- a/site/app/controllers/ChatroomController.php +++ b/site/app/controllers/ChatroomController.php @@ -4,8 +4,9 @@ use app\entities\poll\Poll; use app\libraries\Core; -use app\libraries\response\RedirectResponse; use app\libraries\response\WebResponse; +use app\libraries\response\JsonResponse; +use app\libraries\response\RedirectResponse; use app\entities\chat\Chatroom; use app\entities\chat\Message; use app\libraries\routers\AccessControl; @@ -53,15 +54,15 @@ public function showChatroomsPage(): WebResponse { */ public function showChatroom(string $chatroom_id): WebResponse|RedirectResponse { - /** @var \app\repositories\poll\PollRepository */ - $repo = $this->core->getCourseEntityManager()->getRepository(Chatroom::class); - $chatroom = $repo->findByChatroomId($chatroom_id); - if (!is_numeric($chatroom_id)) { $this->core->addErrorMessage("Invalid Chatroom ID"); return new RedirectResponse($this->core->buildCourseUrl(['chat'])); } + /** @var \app\repositories\poll\PollRepository */ + $repo = $this->core->getCourseEntityManager()->getRepository(Chatroom::class); + $chatroom = $repo->find($chatroom_id); + return new WebResponse( 'Chatroom', 'showChatroom', @@ -96,47 +97,67 @@ public function addChatroom(): RedirectResponse { } /** - * @Route("/chat/{chatroomId}/message", name="chatroom_post_message", methods={"POST"}) + * @Route("/courses/{_semester}/{_course}/chat/deleteChatroom", methods={"POST"}) + * @AccessControl(role="INSTRUCTOR") */ - public function postMessage(int $chatroomId, Request $request, EntityManagerInterface $entityManager): JsonResponse { - $content = $request->request->get('content'); + public function deleteChatroom(): JsonResponse { + $chatroom_id = intval($_POST['chatroom_id'] ?? -1); + $em = $this->core->getCourseEntityManager(); - $chatroom = $entityManager->getRepository(Chatroom::class)->find($chatroomId); - if (!$chatroom || !$chatroom->getIsActive()) { - return new JsonResponse(['error' => 'Chatroom not found or not active.'], Response::HTTP_NOT_FOUND); + /** @var \app\repositories\chat\ChatroomRepository */ + $repo = $em->getRepository(Chatroom::class); + + /** @var Chatroom|null */ + $chatroom = $repo->find(Chatroom::class, $chatroom_id); + if ($chatroom === null) { + return JsonResponse::getFailResponse('Invalid Chatroom ID'); } - $message = new Message(); - $message->setContent($content); - $message->setChatroom($chatroom); - // Set other necessary properties, such as timestamp + foreach ($chatroom->getMessages() as $message) { + $em->remove($message); + } - $entityManager->persist($message); - $entityManager->flush(); + $em->remove($chatroom); + $em->flush(); - return new JsonResponse(['success' => true, 'messageId' => $message->getId()]); + return JsonResponse::getSuccessResponse(); } /** - * @Route("/chat/{chatroomId}/messages", name="chatroom_messages", methods={"GET"}) + * @Route("/courses/{_semester}/{_course}/chat/{chatroom_id}/messages", name="fetch_chatroom_messages", methods={"GET"}) */ - public function getMessages(int $chatroomId, EntityManagerInterface $entityManager): JsonResponse { - $chatroom = $entityManager->getRepository(Chatroom::class)->find($chatroomId); - if (!$chatroom) { - return new JsonResponse(['error' => 'Chatroom not found.'], Response::HTTP_NOT_FOUND); - } - - $messages = $chatroom->getMessages(); - $response = []; + public function fetchChatroomMessages(int $chatroom_id): JsonResponse { + $em = $this->core->getCourseEntityManager(); + $messages = $em->getRepository(Message::class)->findBy(['chatroom' => $chatroom_id], ['timestamp' => 'ASC']); - foreach ($messages as $message) { - $response[] = [ + $formattedMessages = array_map(function ($message) { + return [ 'id' => $message->getId(), 'content' => $message->getContent(), - 'timestamp' => $message->getTimestamp()->format('Y-m-d H:i:s') + 'timestamp' => $message->getTimestamp()->format('Y-m-d H:i:s'), + 'user_id' => $message->getUserId() ]; - } + }, $messages); + + return JsonResponse::getSuccessResponse($formattedMessages); + } + + /** + * @Route("/courses/{_semester}/{_course}/chat/{chatroom_id}/send", name="send_chatroom_messages", methods={"POST"}) + */ + public function sendMessage(int $chatroom_id): JsonResponse { + $em = $this->core->getCourseEntityManager(); + $content = $_POST['content'] ?? ''; + $userId = $this->core->getUser()->getId(); + $message = new Message(); + $message->setChatroomId($chatroom_id); + $message->setUserId($userId); + $message->setContent($content); + $message->setTimestamp(new \DateTimeImmutable("now")); + + $em->persist($message); + $em->flush(); - return new JsonResponse(['messages' => $response]); + return JsonResponse::getSuccessResponse("Message sent successfully"); } } diff --git a/site/app/entities/chat/Chatroom.php b/site/app/entities/chat/Chatroom.php index 128f9bd96b2..7b5f3d39131 100644 --- a/site/app/entities/chat/Chatroom.php +++ b/site/app/entities/chat/Chatroom.php @@ -17,24 +17,24 @@ class Chatroom { #[ORM\Id] #[ORM\Column(name: "id", type: Types::INTEGER)] #[ORM\GeneratedValue] - protected $id; + protected int $id; #[ORM\Column(type: Types::STRING)] protected string $host_id; #[ORM\Column(name: "title", type: Types::STRING)] - protected $title; + protected string $title; #[ORM\Column(name: "description", type: Types::STRING)] - protected $description; + protected string $description; #[ORM\Column(name: "is_active", type: Types::BOOLEAN)] - protected $isActive; + protected bool $isActive; /** * @var Collection */ - #[ORM\OneToMany(mappedBy: "chat", targetEntity: Message::class)] + #[ORM\OneToMany(mappedBy: "chat", targetEntity: Message::class, cascade: ["remove"])] protected Collection $messages; public function __construct() { diff --git a/site/app/entities/chat/Message.php b/site/app/entities/chat/Message.php index b2c54a51d43..7fe0f6b103d 100644 --- a/site/app/entities/chat/Message.php +++ b/site/app/entities/chat/Message.php @@ -18,14 +18,19 @@ class Message { #[ORM\GeneratedValue] private $id; - #[ORM\ManyToOne(targetEntity: Chatroom::class, inversedBy: "messages")] + #[ORM\ManyToOne(targetEntity: "Chatroom")] #[ORM\JoinColumn(name: "chatroom_id", referencedColumnName: "id")] private $chatroom; + #[ORM\Column(name: "chatroom_id", type: Types::INTEGER)] + private $chatroom_id; + + #[ORM\Column(type: Types::STRING)] + private string $user_id; #[ORM\Column(type: Types::TEXT)] private $content; - #[ORM\Column(type: Types::DATE_IMMUTABLE)] + #[ORM\Column(type: Types::DATETIME_IMMUTABLE)] private $timestamp; public function __construct() { @@ -36,6 +41,14 @@ public function getId(): int { return $this->id; } + public function getUserId(): string { + return $this->user_id; + } + + public function setUserId($userId): void { + $this->user_id = $userId; + } + public function getContent(): string { return $this->content; } @@ -44,16 +57,27 @@ public function setContent(string $text): void { $this->content = $text; } - public function getTimestamp(): \DateTime { + public function getTimestamp(): \DateTimeImmutable { return $this->timestamp; } + public function setTimestamp(\DateTimeImmutable $timestamp): void { + $this->timestamp = $timestamp; + } + + public function getChatroomId(): int { + return $this->chatroom_id; + } + + public function setChatroomId(int $chatroom_id): void { + $this->chatroom_id = $chatroom_id; + } + public function getChatroom(): Chatroom { return $this->chatroom; } - public function setChatroom(Chatroom $chatroom): self { + public function setChatroom(Chatroom $chatroom): void { $this->chatroom = $chatroom; - return $this; } } diff --git a/site/app/templates/chat/ChatPageIns.twig b/site/app/templates/chat/ChatPageIns.twig index dad623080c9..40b531ccb19 100644 --- a/site/app/templates/chat/ChatPageIns.twig +++ b/site/app/templates/chat/ChatPageIns.twig @@ -8,13 +8,15 @@ - + + Name Description Host + @@ -31,6 +33,10 @@ Join + Delete + + + Show History {% endfor %} @@ -39,4 +45,48 @@
+ + {% include('chat/CreateChatroomForm.twig') %} \ No newline at end of file diff --git a/site/app/templates/chat/Chatroom.twig b/site/app/templates/chat/Chatroom.twig index 81d7a834aae..04b37cf0624 100644 --- a/site/app/templates/chat/Chatroom.twig +++ b/site/app/templates/chat/Chatroom.twig @@ -1,42 +1,90 @@ -{% block body %} -
-
-
-

{{ chatroom.getTitle() }}

- -
- -
-
- -
- Alice: -

Hey, how's everyone doing today?

- 09:45 -
-
- You: -

Doing great, thanks! How about you?

- 09:46 -
- -
- -
- - -
+
+
+ {{ chatroom.getTitle() }} + +
+
+
+
+
+
+ +
- -
-{% endblock %} + \ No newline at end of file diff --git a/site/app/views/ChatroomView.php b/site/app/views/ChatroomView.php index 872c9337727..035c66a23e5 100644 --- a/site/app/views/ChatroomView.php +++ b/site/app/views/ChatroomView.php @@ -2,19 +2,22 @@ namespace app\views; +use app\entities\chat\Chatroom; +use app\entities\chat\Message; use app\libraries\Core; -use app\libraries\FileUtils; use app\libraries\Output; -use app\libraries\PollUtils; +use app\libraries\FileUtils; +use app\libraries\Utils; class ChatroomView extends AbstractView { public function __construct(Core $core, Output $output) { parent::__construct($core, $output); $this->core->getOutput()->addBreadcrumb("Live Lecture Chat", $this->core->buildCourseUrl(['chat'])); + $this->core->getOutput()->addInternalCss('chatroom.css'); + $this->core->getOutput()->addInternalJs('chatroom.js'); } public function showChatPageInstructor(array $chatrooms) { - $this->core->getOutput()->addInternalCss('chat.css'); return $this->core->getOutput()->renderTwigTemplate("chat/ChatPageIns.twig", [ 'csrf_token' => $this->core->getCsrfToken(), 'base_url' => $this->core->buildCourseUrl() . '/chat', @@ -25,7 +28,6 @@ public function showChatPageInstructor(array $chatrooms) { } public function showChatPageStudent(array $chatrooms) { - $this->core->getOutput()->addInternalCss('chat.css'); return $this->core->getOutput()->renderTwigTemplate("chat/ChatPageStu.twig", [ 'csrf_token' => $this->core->getCsrfToken(), 'base_url' => $this->core->buildCourseUrl() . '/chat', @@ -36,12 +38,12 @@ public function showChatPageStudent(array $chatrooms) { } public function showChatroom($chatroom) { - $this->core->getOutput()->addInternalCss('chat.css'); $this->core->getOutput()->addBreadcrumb("Chatroom"); - return $this->core->getOutput()->renderTwigTemplate("chat/Chatroom.twig", [ 'csrf_token' => $this->core->getCsrfToken(), 'base_url' => $this->core->buildCourseUrl() . '/chat', + 'semester' => $this->core->getConfig()->getTerm(), + 'course' => $this->core->getConfig()->getCourse(), 'chatroom' => $chatroom, 'user_admin' => $this->core->getUser()->accessAdmin() ]); diff --git a/site/public/css/chat.css b/site/public/css/chat.css deleted file mode 100644 index bfa757d2b64..00000000000 --- a/site/public/css/chat.css +++ /dev/null @@ -1,83 +0,0 @@ -body, html { - margin: 0; - padding: 0; - font-family: Arial, sans-serif; -} - -.chatroom-container { - display: flex; - flex-direction: column; - height: 100vh; -} - -.chatroom-header { - background-color: #007bff; - color: #ffffff; - padding: 10px; - display: flex; - justify-content: space-between; - align-items: center; -} - -.chatroom-main { - flex: 1; - display: flex; - flex-direction: column; -} - -.message-area { - flex: 1; - overflow-y: auto; - padding: 10px; - background-color: #f9f9f9; -} - -.message { - margin-bottom: 10px; - padding: 5px; - border-radius: 5px; -} - -.message.you { - background-color: #e0f7fa; - align-self: flex-end; -} - -.message.other { - background-color: #e0e0e0; -} - -.user { - font-weight: bold; -} - -.time { - font-size: 0.8em; -} - -.input-area { - display: flex; - padding: 10px; - background-color: #fff; -} - -#messageInput { - flex: 1; - margin-right: 10px; -} - -.chatroom-sidebar { - background-color: #f2f2f2; - padding: 10px; - width: 200px; -} - -.chatroom-sidebar ul { - list-style: none; - padding: 0; -} - -.chatroom-sidebar ul li { - padding: 5px; - border-bottom: 1px solid #ddd; -} diff --git a/site/public/css/chatroom.css b/site/public/css/chatroom.css new file mode 100644 index 00000000000..a73fe0eb8a2 --- /dev/null +++ b/site/public/css/chatroom.css @@ -0,0 +1,64 @@ +.chatroom-container { + width:75%; + margin-left:10%; +} + +.chatroom_title { + font-size: 1.3rem; +} + +.chatroom_description { + font-size: 1rem; +} + +.chatroom-main { + +} + +.leave_room { + float:right; +} + +.message-area { + overflow-y: auto; + height: 33rem; +} + +.message { + margin: 1rem; +} + +.message_header { + margin-bottom: 3px; +} + +.fa-circle-user { + margin-right: 0.5rem; +} + +.username { + font-size: 1.1rem; +} + +.timestamp { + color:grey; +} + +.message_content { + margin-left: 1.7rem; +} + +.input-area { +} + +.message_input { + min-height: 30px; + min-width: 50rem; + resize: none; + border-radius: 15px; + margin-left:1rem; +} + +.send_message { + margin-left: 1rem; +} \ No newline at end of file diff --git a/site/public/css/server.css b/site/public/css/server.css index 09e6bd9a910..889529bdd95 100644 --- a/site/public/css/server.css +++ b/site/public/css/server.css @@ -2328,81 +2328,3 @@ header > * { float: none; } /* stylelint-enable no-descending-specificity */ - -.chatroom-container { - display: flex; - flex-direction: column; - height: 100vh; -} - -.chatroom-header { - background-color: #007bff; - color: #ffffff; - padding: 10px; - display: flex; - justify-content: space-between; - align-items: center; -} - -.chatroom-main { - flex: 1; - display: flex; - flex-direction: column; -} - -.message-area { - flex: 1; - overflow-y: auto; - padding: 10px; - background-color: #f9f9f9; -} - -.message { - margin-bottom: 10px; - padding: 5px; - border-radius: 5px; -} - -.message.you { - background-color: #e0f7fa; - align-self: flex-end; -} - -.message.other { - background-color: #e0e0e0; -} - -.user { - font-weight: bold; -} - -.time { - font-size: 0.8em; -} - -.input-area { - display: flex; - padding: 10px; - background-color: #fff; -} - -#messageInput { - flex: 1; - margin-right: 10px; -} - -.chatroom-sidebar { - background-color: #f2f2f2; - padding: 10px; - width: 200px; -} - -.chatroom-sidebar ul { - list-style: none; - padding: 0; -} - -.chatroom-sidebar ul li { - padding: 5px; - border-bottom: 1px solid #ddd; -} diff --git a/site/public/js/chatroom.js b/site/public/js/chatroom.js new file mode 100644 index 00000000000..e69de29bb2d From 3109145efd3eadb79d7eb8c6b93e503699cd8cd1 Mon Sep 17 00:00:00 2001 From: Hanson Gu Date: Tue, 13 Feb 2024 19:57:10 -0500 Subject: [PATCH 04/19] change ChatPageIns.twig --- site/app/templates/chat/ChatPageIns.twig | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/site/app/templates/chat/ChatPageIns.twig b/site/app/templates/chat/ChatPageIns.twig index 40b531ccb19..6260c8e6ca3 100644 --- a/site/app/templates/chat/ChatPageIns.twig +++ b/site/app/templates/chat/ChatPageIns.twig @@ -5,6 +5,7 @@

Chatrooms

+ @@ -12,6 +13,7 @@ + @@ -22,6 +24,10 @@ {% for chatroom in chatrooms %} + From 1639183d3c2396c6c181f8c8932cced3ad3676b6 Mon Sep 17 00:00:00 2001 From: Hanson Gu Date: Fri, 23 Feb 2024 00:33:29 -0500 Subject: [PATCH 05/19] populated edit chatroom --- .../course/20240209021641_lecture_chat.py | 4 +- site/app/controllers/ChatroomController.php | 85 ++++++++++----- site/app/controllers/GlobalController.php | 2 +- site/app/entities/chat/Message.php | 19 +--- site/app/libraries/socket/Server.php | 5 +- site/app/templates/chat/ChatPageIns.twig | 52 +-------- site/app/templates/chat/Chatroom.twig | 103 +++--------------- .../templates/chat/CreateChatroomForm.twig | 7 +- site/app/templates/chat/EditChatroomForm.twig | 19 ++++ site/app/views/ChatroomView.php | 7 +- site/public/css/chatroom.css | 65 +++++++---- site/public/js/chatroom.js | 83 ++++++++++++++ site/public/js/server.js | 8 +- 13 files changed, 247 insertions(+), 212 deletions(-) create mode 100644 site/app/templates/chat/EditChatroomForm.twig diff --git a/migration/migrator/migrations/course/20240209021641_lecture_chat.py b/migration/migrator/migrations/course/20240209021641_lecture_chat.py index ac388f99f63..858a1c69651 100644 --- a/migration/migrator/migrations/course/20240209021641_lecture_chat.py +++ b/migration/migrator/migrations/course/20240209021641_lecture_chat.py @@ -42,5 +42,5 @@ def down(config, database, semester, course): :param course: Code of course being migrated :type course: str """ - database.execute("DROP TABLE IF EXISTS chatrooms") - database.execute("DROP TABLE IF EXISTS chatroom_messages") \ No newline at end of file + + pass \ No newline at end of file diff --git a/site/app/controllers/ChatroomController.php b/site/app/controllers/ChatroomController.php index 735aadb09ea..a32ae70930c 100644 --- a/site/app/controllers/ChatroomController.php +++ b/site/app/controllers/ChatroomController.php @@ -2,7 +2,6 @@ namespace app\controllers; -use app\entities\poll\Poll; use app\libraries\Core; use app\libraries\response\WebResponse; use app\libraries\response\JsonResponse; @@ -11,9 +10,8 @@ use app\entities\chat\Message; use app\libraries\routers\AccessControl; use app\libraries\routers\Enabled; +use app\views\ChatroomView; use Symfony\Component\Routing\Annotation\Route; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; use Doctrine\ORM\EntityManagerInterface; class ChatroomController extends AbstractController { @@ -26,7 +24,6 @@ public function __construct(Core $core) { * @Route("/courses/{_semester}/{_course}/chat", methods={"GET"}) */ public function showChatroomsPage(): WebResponse { - /** @var \app\repositories\chat\ChatroomRepository */ $repo = $this->core->getCourseEntityManager()->getRepository(Chatroom::class); $user = $this->core->getUser(); $chatrooms = $repo->findAll(); @@ -49,17 +46,16 @@ public function showChatroomsPage(): WebResponse { } /** - * @Route("/courses/{_semester}/{_course}/chat/{chatroom_id}", methods={"GET"}, requirements={"chatroom_id": "\d*", }) + * @Route("/courses/{_semester}/{_course}/chat/{chatroom_id}", methods={"GET"}) + * @param string $chatroom_id * @return RedirectResponse|WebResponse */ public function showChatroom(string $chatroom_id): WebResponse|RedirectResponse { - if (!is_numeric($chatroom_id)) { $this->core->addErrorMessage("Invalid Chatroom ID"); return new RedirectResponse($this->core->buildCourseUrl(['chat'])); } - /** @var \app\repositories\poll\PollRepository */ $repo = $this->core->getCourseEntityManager()->getRepository(Chatroom::class); $chatroom = $repo->find($chatroom_id); @@ -77,18 +73,11 @@ public function showChatroom(string $chatroom_id): WebResponse|RedirectResponse public function addChatroom(): RedirectResponse { $em = $this->core->getCourseEntityManager(); - $fields = ['title', 'description']; - foreach ($fields as $field) { - if (empty($_POST[$field])) { - $this->core->addErrorMessage("Chatroom must fill out all fields"); - return new RedirectResponse($this->core->buildCourseUrl(['chat'])); - } - } - $chatroom = new Chatroom(); $chatroom->setTitle($_POST['title']); $chatroom->setDescription($_POST['description']); $chatroom->setHostId($this->core->getUser()->getId()); + $em->persist($chatroom); $em->flush(); @@ -97,17 +86,14 @@ public function addChatroom(): RedirectResponse { } /** - * @Route("/courses/{_semester}/{_course}/chat/deleteChatroom", methods={"POST"}) + * @Route("/courses/{_semester}/{_course}/chat/{chatroom_id}/delete", methods={"POST"}) * @AccessControl(role="INSTRUCTOR") */ - public function deleteChatroom(): JsonResponse { + public function deleteChatroom(int $chatroom_id): JsonResponse { $chatroom_id = intval($_POST['chatroom_id'] ?? -1); $em = $this->core->getCourseEntityManager(); - - /** @var \app\repositories\chat\ChatroomRepository */ $repo = $em->getRepository(Chatroom::class); - /** @var Chatroom|null */ $chatroom = $repo->find(Chatroom::class, $chatroom_id); if ($chatroom === null) { return JsonResponse::getFailResponse('Invalid Chatroom ID'); @@ -116,17 +102,43 @@ public function deleteChatroom(): JsonResponse { foreach ($chatroom->getMessages() as $message) { $em->remove($message); } - $em->remove($chatroom); $em->flush(); - return JsonResponse::getSuccessResponse(); + return JsonResponse::getSuccessResponse("Chatroom deleted successfully"); + } + +/** + * @Route("/courses/{_semester}/{_course}/chat/{chatroom_id}/edit", name="edit_chatroom", methods={"POST"}) + * @AccessControl(role="INSTRUCTOR") + */ +public function editChatroom(int $chatroom_id): RedirectResponse { + $em = $this->core->getCourseEntityManager(); + $chatroom = $em->getRepository(Chatroom::class)->find($chatroom_id); + + if (!$chatroom) { + $this->core->addErrorMessage("Chatroom not found"); + return new RedirectResponse($this->core->buildCourseUrl(['chat'])); } + if (isset($_POST['title'])) { + $chatroom->setTitle($_POST['title']); + } + if (isset($_POST['description'])) { + $chatroom->setDescription($_POST['description']); + } + + $em->persist($chatroom); + $em->flush(); + + $this->core->addSuccessMessage("Chatroom successfully updated"); + return new RedirectResponse($this->core->buildCourseUrl(['chat'])); +} + /** * @Route("/courses/{_semester}/{_course}/chat/{chatroom_id}/messages", name="fetch_chatroom_messages", methods={"GET"}) */ - public function fetchChatroomMessages(int $chatroom_id): JsonResponse { + public function fetchMessages(int $chatroom_id): JsonResponse { $em = $this->core->getCourseEntityManager(); $messages = $em->getRepository(Message::class)->findBy(['chatroom' => $chatroom_id], ['timestamp' => 'ASC']); @@ -142,6 +154,26 @@ public function fetchChatroomMessages(int $chatroom_id): JsonResponse { return JsonResponse::getSuccessResponse($formattedMessages); } + /** + * @Route("/courses/{_semester}/{_course}/chat/{chatroom_id}/test", name="test_chatroom_messages", methods={"POST"}) + */ + public function testMessage(int $chatroom_id): JsonResponse + { + $em = $this->core->getCourseEntityManager(); + $chatroom = $em->getRepository(Chatroom::class)->find($chatroom_id); + + $msg = new Message(); + $msg->setTimestamp(new \DateTime("now")); + $msg->setContent("test message"); + $msg->setUserId('test user'); + $msg->setChatroom($chatroom); + + $em->persist($msg); + $em->flush(); + + return JsonResponse::getSuccessResponse("test message sent"); + } + /** * @Route("/courses/{_semester}/{_course}/chat/{chatroom_id}/send", name="send_chatroom_messages", methods={"POST"}) */ @@ -149,15 +181,16 @@ public function sendMessage(int $chatroom_id): JsonResponse { $em = $this->core->getCourseEntityManager(); $content = $_POST['content'] ?? ''; $userId = $this->core->getUser()->getId(); + $chatroom = $em->getRepository(Chatroom::class)->find($chatroom_id); $message = new Message(); - $message->setChatroomId($chatroom_id); + $message->setChatroom($chatroom); $message->setUserId($userId); $message->setContent($content); - $message->setTimestamp(new \DateTimeImmutable("now")); + $message->setTimestamp(new \DateTime("now")); $em->persist($message); $em->flush(); - return JsonResponse::getSuccessResponse("Message sent successfully"); + return JsonResponse::getSuccessResponse($message); } } diff --git a/site/app/controllers/GlobalController.php b/site/app/controllers/GlobalController.php index ec790e848cd..769d0267cc9 100644 --- a/site/app/controllers/GlobalController.php +++ b/site/app/controllers/GlobalController.php @@ -170,7 +170,7 @@ public function prep_course_sidebar(&$sidebar_buttons, $unread_notifications_cou "href" => $this->core->buildCourseUrl(['chat']), "title" => "Live Lecture Chat", "id" => "nav-sidebar-chat", - "icon" => "far fa-smiley" + "icon" => "fa-regular fa-keyboard" ]); } diff --git a/site/app/entities/chat/Message.php b/site/app/entities/chat/Message.php index 7fe0f6b103d..e71b43305d6 100644 --- a/site/app/entities/chat/Message.php +++ b/site/app/entities/chat/Message.php @@ -21,8 +21,6 @@ class Message { #[ORM\ManyToOne(targetEntity: "Chatroom")] #[ORM\JoinColumn(name: "chatroom_id", referencedColumnName: "id")] private $chatroom; - #[ORM\Column(name: "chatroom_id", type: Types::INTEGER)] - private $chatroom_id; #[ORM\Column(type: Types::STRING)] private string $user_id; @@ -30,11 +28,10 @@ class Message { #[ORM\Column(type: Types::TEXT)] private $content; - #[ORM\Column(type: Types::DATETIME_IMMUTABLE)] - private $timestamp; + #[ORM\Column(type: Types::DATETIMETZ_MUTABLE)] + protected DateTime $timestamp; public function __construct() { - $this->timestamp = new \DateTime(); } public function getId(): int { @@ -57,22 +54,14 @@ public function setContent(string $text): void { $this->content = $text; } - public function getTimestamp(): \DateTimeImmutable { + public function getTimestamp(): DateTime { return $this->timestamp; } - public function setTimestamp(\DateTimeImmutable $timestamp): void { + public function setTimestamp(datetime $timestamp): void { $this->timestamp = $timestamp; } - public function getChatroomId(): int { - return $this->chatroom_id; - } - - public function setChatroomId(int $chatroom_id): void { - $this->chatroom_id = $chatroom_id; - } - public function getChatroom(): Chatroom { return $this->chatroom; } diff --git a/site/app/libraries/socket/Server.php b/site/app/libraries/socket/Server.php index 9766d51ca89..122c509a2a7 100644 --- a/site/app/libraries/socket/Server.php +++ b/site/app/libraries/socket/Server.php @@ -180,15 +180,15 @@ public function onOpen(ConnectionInterface $conn): void { * @param string $msgString */ public function onMessage(ConnectionInterface $from, $msgString): void { + try { if ($msgString === 'ping') { $from->send('pong'); return; } - $msg = json_decode($msgString, true); - if (isset($msg["type"]) && $msg["type"] === "new_connection") { + $msg = json_decode($msgString, true); if (isset($msg['page']) && is_string($msg['page'])) { if (!array_key_exists($msg['page'], $this->clients)) { $this->clients[$msg['page']] = new \SplObjectStorage(); @@ -213,6 +213,7 @@ public function onMessage(ConnectionInterface $from, $msgString): void { else { $this->broadcast($from, $msgString, $this->getSocketClientPage($from)); } + } catch (\Throwable $t) { $this->logError($t, $from); diff --git a/site/app/templates/chat/ChatPageIns.twig b/site/app/templates/chat/ChatPageIns.twig index 6260c8e6ca3..b5e0a86e427 100644 --- a/site/app/templates/chat/ChatPageIns.twig +++ b/site/app/templates/chat/ChatPageIns.twig @@ -25,8 +25,7 @@ {% for chatroom in chatrooms %} {% endfor %} @@ -52,47 +51,8 @@ -{% include('chat/CreateChatroomForm.twig') %} \ No newline at end of file +{% include('chat/CreateChatroomForm.twig') %} +{% include('chat/EditChatroomForm.twig') %} \ No newline at end of file diff --git a/site/app/templates/chat/Chatroom.twig b/site/app/templates/chat/Chatroom.twig index 04b37cf0624..8d7cfe767ec 100644 --- a/site/app/templates/chat/Chatroom.twig +++ b/site/app/templates/chat/Chatroom.twig @@ -1,90 +1,23 @@ -
-
- {{ chatroom.getTitle() }} - -
-
-
-
-
-
- - +
+
+
+ {{ chatroom.getTitle() }} + leave room +
+
+
+
+ + +
- - \ No newline at end of file diff --git a/site/app/templates/chat/CreateChatroomForm.twig b/site/app/templates/chat/CreateChatroomForm.twig index 422ef195800..09ef5feb3f2 100644 --- a/site/app/templates/chat/CreateChatroomForm.twig +++ b/site/app/templates/chat/CreateChatroomForm.twig @@ -9,13 +9,12 @@ - + - - - + +
{% endblock %} diff --git a/site/app/templates/chat/EditChatroomForm.twig b/site/app/templates/chat/EditChatroomForm.twig new file mode 100644 index 00000000000..bb06ca82a27 --- /dev/null +++ b/site/app/templates/chat/EditChatroomForm.twig @@ -0,0 +1,19 @@ +{% extends 'generic/Popup.twig' %} +{% block popup_id %}edit-chatroom-form{% endblock %} +{% block title %}Edit Chatroom{% endblock %} +{% block body %} +
+
+ + + + + + +
+ +{% endblock %} \ No newline at end of file diff --git a/site/app/views/ChatroomView.php b/site/app/views/ChatroomView.php index 035c66a23e5..c63e47cb840 100644 --- a/site/app/views/ChatroomView.php +++ b/site/app/views/ChatroomView.php @@ -4,6 +4,7 @@ use app\entities\chat\Chatroom; use app\entities\chat\Message; +use app\entities\poll\Poll; use app\libraries\Core; use app\libraries\Output; use app\libraries\FileUtils; @@ -15,6 +16,7 @@ public function __construct(Core $core, Output $output) { $this->core->getOutput()->addBreadcrumb("Live Lecture Chat", $this->core->buildCourseUrl(['chat'])); $this->core->getOutput()->addInternalCss('chatroom.css'); $this->core->getOutput()->addInternalJs('chatroom.js'); + $this->core->getOutput()->addInternalJs('websocket.js'); } public function showChatPageInstructor(array $chatrooms) { @@ -45,7 +47,10 @@ public function showChatroom($chatroom) { 'semester' => $this->core->getConfig()->getTerm(), 'course' => $this->core->getConfig()->getCourse(), 'chatroom' => $chatroom, - 'user_admin' => $this->core->getUser()->accessAdmin() + 'user_admin' => $this->core->getUser()->accessAdmin(), + 'user_id' => $this->core->getUser()->getId(), + 'user_first_name' => $this->core->getUser()->getDisplayedGivenName(), + 'user_last_name' => $this->core->getUser()->getDisplayedFamilyName() ]); } } diff --git a/site/public/css/chatroom.css b/site/public/css/chatroom.css index a73fe0eb8a2..814d1bef319 100644 --- a/site/public/css/chatroom.css +++ b/site/public/css/chatroom.css @@ -1,34 +1,39 @@ .chatroom-container { - width:75%; - margin-left:10%; + width: 65%; + margin-left: 16%; + box-shadow: 0 0 5px 0 rgba(0, 0, 0, .5); } -.chatroom_title { - font-size: 1.3rem; +.chatroom-header{ + padding-top: 1rem; + padding-bottom: 1rem; + box-shadow: 0 4px 2px -2px rgba(0, 0, 0, .2); } -.chatroom_description { - font-size: 1rem; +.chatroom-title { + font-size: 1.3rem; + padding-left: 1rem; } -.chatroom-main { - +.chatroom-content { } -.leave_room { +.leave-room { float:right; + margin-right: 1rem; } -.message-area { +.messages-area { overflow-y: auto; - height: 33rem; + height: 35rem; } -.message { +.message-container { margin: 1rem; + max-width: 50%; } -.message_header { +.message-header { margin-bottom: 3px; } @@ -44,21 +49,35 @@ color:grey; } -.message_content { +.message-content { margin-left: 1.7rem; } -.input-area { +.input-container { + display: flex; + align-items: center; + background-color: #fff; + border-top: 1px solid #e0e0e0; + box-shadow: 0 -2px 10px 0 rgba(0,0,0,0.05); + padding: 0.5rem; } -.message_input { - min-height: 30px; - min-width: 50rem; - resize: none; - border-radius: 15px; - margin-left:1rem; +.message-input { + border: 1px solid #e0e0e0; + border-radius: 5px; + outline: none; + resize: none; + overflow: hidden; + font-size: 15px; + min-width: 85%; + margin-left: 1rem; } -.send_message { - margin-left: 1rem; +.send-message-btn { + margin-left: 1rem; + border: none; + border-radius: 25px; + cursor: pointer; + outline: none; + transition: background-color 0.3s ease; } \ No newline at end of file diff --git a/site/public/js/chatroom.js b/site/public/js/chatroom.js index e69de29bb2d..5b88ac16375 100644 --- a/site/public/js/chatroom.js +++ b/site/public/js/chatroom.js @@ -0,0 +1,83 @@ +function fetchMessages(baseURL, chatroomId) { + $.ajax({ + url: `${baseURL}/${chatroomId}/messages`, + type: 'GET', + dataType: 'json', + success: function(responseData) { + if (responseData.status === "success" && Array.isArray(responseData.data)) { + responseData.data.forEach(msg => { + appendMessage(msg.user_id, msg.timestamp, msg.content); + }); + } + }, + error: function() { + window.alert('Something went wrong with fetching messages'); + }, + }); +} + +function appendMessage(senderName, timestamp, content) { + const messages_area = document.querySelector('.messages-area'); + const message = document.createElement('div'); + message.classList.add('message-container'); + message.innerHTML = ` +
+ + ${senderName} + - ${timestamp} +
+
+ ${content} +
+ `; + messages_area.appendChild(message); + messages_area.scrollTop = messages_area.scrollHeight; +} + +function initChatroomSocketClient(chatroomId) { + +} + +function newChatroomForm() { + let form = $("#create-chatroom-form"); + form.css("display", "block"); +} + +function editChatroomForm(chatroom_id, baseUrl) { + let form= $("#edit-chatroom-form"); + form.css('display', 'block'); + document.getElementById('chatroom-edit-form').action = `${baseUrl}/${chatroom_id}/edit`; +} + +document.addEventListener('DOMContentLoaded', () => { + $('.popup-form').css('display', 'none'); + const pageDataElement = document.getElementById('page-data'); + if (pageDataElement) { + const pageData = JSON.parse(pageDataElement.textContent); + const { chatroomId, baseURL, userId } = pageData; + + const client = new WebSocketClient(); + client.onmessage = (msg) => { + console.log(`Data received from server:`, msg); + }; + client.onopen = () => { + client.send({ type: "ping" }); + } + client.open('some URL'); + + fetchMessages(baseURL, chatroomId); + + const sendMsgButton = document.querySelector('.send-message-btn'); + const messageInput = document.querySelector('.message-input'); + + sendMsgButton.addEventListener('click', function (e) { + e.preventDefault(); + const messageContent = messageInput.value.trim(); + if (messageContent === '') { + alert('Please enter a message.'); + return; + } + messageInput.value = ''; + }); + } +}); \ No newline at end of file diff --git a/site/public/js/server.js b/site/public/js/server.js index 13e14bdcdef..bcf4518283a 100644 --- a/site/public/js/server.js +++ b/site/public/js/server.js @@ -1893,10 +1893,4 @@ function tzWarn() { } } -document.addEventListener('DOMContentLoaded', tzWarn); - -function newChatroomForm() { - $('.popup-form').css('display', 'none'); - var form = $("#create-chatroom-form"); - form.css("display", "block"); -} \ No newline at end of file +document.addEventListener('DOMContentLoaded', tzWarn); \ No newline at end of file From 2018ab70b9482c0659e528ecab213c031ec35fa2 Mon Sep 17 00:00:00 2001 From: Hanson Gu Date: Sat, 24 Feb 2024 01:59:15 -0500 Subject: [PATCH 06/19] fixed websockets --- migration/migrator/data/course_tables.sql | 3 +- .../course/20240209021641_lecture_chat.py | 4 +- site/app/controllers/ChatroomController.php | 28 ++---- site/app/entities/chat/Message.php | 23 +++-- site/app/libraries/socket/Server.php | 5 +- site/app/templates/chat/Chatroom.twig | 10 ++- site/app/views/ChatroomView.php | 8 +- site/public/css/chatroom.css | 78 +++++++++-------- site/public/js/chatroom.js | 86 +++++++++++++------ 9 files changed, 146 insertions(+), 99 deletions(-) diff --git a/migration/migrator/data/course_tables.sql b/migration/migrator/data/course_tables.sql index 410e7102934..902187bd476 100644 --- a/migration/migrator/data/course_tables.sql +++ b/migration/migrator/data/course_tables.sql @@ -709,7 +709,8 @@ ALTER SEQUENCE public.categories_list_category_id_seq OWNED BY public.categories CREATE TABLE public.chatroom_messages ( id integer NOT NULL, chatroom_id integer NOT NULL, - user_id character varying NOT NULL, + userid character varying NOT NULL, + display_name character varying, content text NOT NULL, "timestamp" timestamp(0) with time zone NOT NULL ); diff --git a/migration/migrator/migrations/course/20240209021641_lecture_chat.py b/migration/migrator/migrations/course/20240209021641_lecture_chat.py index 858a1c69651..0a9fa900fac 100644 --- a/migration/migrator/migrations/course/20240209021641_lecture_chat.py +++ b/migration/migrator/migrations/course/20240209021641_lecture_chat.py @@ -16,7 +16,7 @@ def up(config, database, semester, course): :type course: str """ database.execute("CREATE TABLE IF NOT EXISTS chatrooms(id SERIAL PRIMARY KEY, host_id character varying NOT NULL, title text NOT NULL, description text, is_active BOOLEAN DEFAULT false NOT NULL)") - database.execute("CREATE TABLE IF NOT EXISTS chatroom_messages(id SERIAL PRIMARY KEY, chatroom_id integer NOT NULL, user_id character varying NOT NULL, content text NOT NULL, timestamp timestamp(0) with time zone NOT NULL)") + database.execute("CREATE TABLE IF NOT EXISTS chatroom_messages(id SERIAL PRIMARY KEY, chatroom_id integer NOT NULL, userId character varying NOT NULL, display_name character varying, content text NOT NULL, timestamp timestamp(0) with time zone NOT NULL)") course_dir = Path(config.submitty['submitty_data_dir'], 'courses', semester, course) # add boolean to course config @@ -42,5 +42,7 @@ def down(config, database, semester, course): :param course: Code of course being migrated :type course: str """ + database.execute("DROP TABLE IF EXISTS chatrooms") + database.execute("DROP TABLE IF EXISTS chatroom_messages") pass \ No newline at end of file diff --git a/site/app/controllers/ChatroomController.php b/site/app/controllers/ChatroomController.php index a32ae70930c..fa7f04e326b 100644 --- a/site/app/controllers/ChatroomController.php +++ b/site/app/controllers/ChatroomController.php @@ -147,44 +147,28 @@ public function fetchMessages(int $chatroom_id): JsonResponse { 'id' => $message->getId(), 'content' => $message->getContent(), 'timestamp' => $message->getTimestamp()->format('Y-m-d H:i:s'), - 'user_id' => $message->getUserId() + 'user_id' => $message->getUserId(), + 'display_name' => $message->getDisplayName() ]; }, $messages); return JsonResponse::getSuccessResponse($formattedMessages); } - /** - * @Route("/courses/{_semester}/{_course}/chat/{chatroom_id}/test", name="test_chatroom_messages", methods={"POST"}) - */ - public function testMessage(int $chatroom_id): JsonResponse - { - $em = $this->core->getCourseEntityManager(); - $chatroom = $em->getRepository(Chatroom::class)->find($chatroom_id); - - $msg = new Message(); - $msg->setTimestamp(new \DateTime("now")); - $msg->setContent("test message"); - $msg->setUserId('test user'); - $msg->setChatroom($chatroom); - - $em->persist($msg); - $em->flush(); - - return JsonResponse::getSuccessResponse("test message sent"); - } - /** * @Route("/courses/{_semester}/{_course}/chat/{chatroom_id}/send", name="send_chatroom_messages", methods={"POST"}) */ public function sendMessage(int $chatroom_id): JsonResponse { $em = $this->core->getCourseEntityManager(); $content = $_POST['content'] ?? ''; - $userId = $this->core->getUser()->getId(); + $userId = $_POST['user_id'] ?? null; + $displayName = $_POST['display_name'] ?? ''; + $chatroom = $em->getRepository(Chatroom::class)->find($chatroom_id); $message = new Message(); $message->setChatroom($chatroom); $message->setUserId($userId); + $message->setDisplayName($displayName); $message->setContent($content); $message->setTimestamp(new \DateTime("now")); diff --git a/site/app/entities/chat/Message.php b/site/app/entities/chat/Message.php index e71b43305d6..cab31c46c01 100644 --- a/site/app/entities/chat/Message.php +++ b/site/app/entities/chat/Message.php @@ -16,17 +16,20 @@ class Message { #[ORM\Id] #[ORM\Column(name: "id", type: Types::INTEGER)] #[ORM\GeneratedValue] - private $id; + private int $id; #[ORM\ManyToOne(targetEntity: "Chatroom")] #[ORM\JoinColumn(name: "chatroom_id", referencedColumnName: "id")] - private $chatroom; + private Chatroom $chatroom; #[ORM\Column(type: Types::STRING)] - private string $user_id; + private string $userId; + + #[ORM\Column(type: Types::STRING)] + private string $display_name; #[ORM\Column(type: Types::TEXT)] - private $content; + private string $content; #[ORM\Column(type: Types::DATETIMETZ_MUTABLE)] protected DateTime $timestamp; @@ -39,11 +42,19 @@ public function getId(): int { } public function getUserId(): string { - return $this->user_id; + return $this->userId; } public function setUserId($userId): void { - $this->user_id = $userId; + $this->userId = $userId; + } + + public function getDisplayName(): string { + return $this->display_name; + } + + public function setDisplayName($displayName): void { + $this->display_name = $displayName; } public function getContent(): string { diff --git a/site/app/libraries/socket/Server.php b/site/app/libraries/socket/Server.php index 122c509a2a7..9766d51ca89 100644 --- a/site/app/libraries/socket/Server.php +++ b/site/app/libraries/socket/Server.php @@ -180,15 +180,15 @@ public function onOpen(ConnectionInterface $conn): void { * @param string $msgString */ public function onMessage(ConnectionInterface $from, $msgString): void { - try { if ($msgString === 'ping') { $from->send('pong'); return; } + $msg = json_decode($msgString, true); + if (isset($msg["type"]) && $msg["type"] === "new_connection") { - $msg = json_decode($msgString, true); if (isset($msg['page']) && is_string($msg['page'])) { if (!array_key_exists($msg['page'], $this->clients)) { $this->clients[$msg['page']] = new \SplObjectStorage(); @@ -213,7 +213,6 @@ public function onMessage(ConnectionInterface $from, $msgString): void { else { $this->broadcast($from, $msgString, $this->getSocketClientPage($from)); } - } catch (\Throwable $t) { $this->logError($t, $from); diff --git a/site/app/templates/chat/Chatroom.twig b/site/app/templates/chat/Chatroom.twig index 8d7cfe767ec..32811f54529 100644 --- a/site/app/templates/chat/Chatroom.twig +++ b/site/app/templates/chat/Chatroom.twig @@ -1,13 +1,13 @@ -
+
{{ chatroom.getTitle() }} - leave room + leave
- +
@@ -15,9 +15,11 @@
\ No newline at end of file diff --git a/site/app/views/ChatroomView.php b/site/app/views/ChatroomView.php index c63e47cb840..80be17dd834 100644 --- a/site/app/views/ChatroomView.php +++ b/site/app/views/ChatroomView.php @@ -41,6 +41,11 @@ public function showChatPageStudent(array $chatrooms) { public function showChatroom($chatroom) { $this->core->getOutput()->addBreadcrumb("Chatroom"); + $user = $this->core->getUser(); + $display_name = $user->getDisplayFullName(); + if (!$user->accessAdmin()) { + $display_name = $user->getDisplayedGivenName() . " " . substr($user->getDisplayedFamilyName(), 0, 1) . "."; + } return $this->core->getOutput()->renderTwigTemplate("chat/Chatroom.twig", [ 'csrf_token' => $this->core->getCsrfToken(), 'base_url' => $this->core->buildCourseUrl() . '/chat', @@ -49,8 +54,7 @@ public function showChatroom($chatroom) { 'chatroom' => $chatroom, 'user_admin' => $this->core->getUser()->accessAdmin(), 'user_id' => $this->core->getUser()->getId(), - 'user_first_name' => $this->core->getUser()->getDisplayedGivenName(), - 'user_last_name' => $this->core->getUser()->getDisplayedFamilyName() + 'user_display_name' => $display_name, ]); } } diff --git a/site/public/css/chatroom.css b/site/public/css/chatroom.css index 814d1bef319..e1c0589f8a0 100644 --- a/site/public/css/chatroom.css +++ b/site/public/css/chatroom.css @@ -1,13 +1,18 @@ +.chatroom-page { + +} + .chatroom-container { - width: 65%; - margin-left: 16%; - box-shadow: 0 0 5px 0 rgba(0, 0, 0, .5); + width: 60%; + box-shadow: 0 0 2px 0 var(--default-black); + left: 30%; + transform: translate(30%, 0); } -.chatroom-header{ - padding-top: 1rem; +.chatroom-header { + padding-top: 0.5rem; padding-bottom: 1rem; - box-shadow: 0 4px 2px -2px rgba(0, 0, 0, .2); + box-shadow: 0 4px 2px -2px rgb(0 0 0 / 20%); } .chatroom-title { @@ -19,65 +24,70 @@ } .leave-room { - float:right; + float: right; margin-right: 1rem; } .messages-area { overflow-y: auto; - height: 35rem; + height: 38rem; } .message-container { - margin: 1rem; - max-width: 50%; + padding-bottom: 0.5rem; + padding-top: 0.5rem; + margin-left: 2rem; } .message-header { margin-bottom: 3px; } -.fa-circle-user { +.user-icon { margin-right: 0.5rem; } -.username { - font-size: 1.1rem; +.sender-name { + font-weight: bold; + font-size: 1.05rem; } .timestamp { - color:grey; + color: var(--subheading-underscore-grey); + font-size: 0.8rem; + margin-left: 3px; } .message-content { margin-left: 1.7rem; + max-width: 90%; + word-break: break-word; } .input-container { - display: flex; - align-items: center; - background-color: #fff; - border-top: 1px solid #e0e0e0; - box-shadow: 0 -2px 10px 0 rgba(0,0,0,0.05); - padding: 0.5rem; + display: flex; + align-items: center; + border-top: 1px solid #e0e0e0; + box-shadow: 0 -2px 10px 0 rgb(0 0 0 / 5%); + padding: 0.5rem; } .message-input { - border: 1px solid #e0e0e0; - border-radius: 5px; - outline: none; - resize: none; - overflow: hidden; - font-size: 15px; - min-width: 85%; - margin-left: 1rem; + border-radius: 5px; + outline: none; + resize: none; + overflow: hidden; + font-size: 15px; + min-width: 85%; + margin-left: 1rem; + background-color: var(--standard-hover-light-gray); } .send-message-btn { - margin-left: 1rem; - border: none; - border-radius: 25px; - cursor: pointer; - outline: none; - transition: background-color 0.3s ease; + margin-left: 1rem; + border: none; + border-radius: 25px; + cursor: pointer; + outline: none; + transition: background-color 0.3s ease; } \ No newline at end of file diff --git a/site/public/js/chatroom.js b/site/public/js/chatroom.js index 5b88ac16375..f3309f1e380 100644 --- a/site/public/js/chatroom.js +++ b/site/public/js/chatroom.js @@ -1,12 +1,12 @@ -function fetchMessages(baseURL, chatroomId) { +function fetchMessages(chatroomId) { $.ajax({ - url: `${baseURL}/${chatroomId}/messages`, + url: buildCourseUrl(['chat', chatroomId, 'messages']), type: 'GET', dataType: 'json', success: function(responseData) { - if (responseData.status === "success" && Array.isArray(responseData.data)) { + if (responseData.status === 'success' && Array.isArray(responseData.data)) { responseData.data.forEach(msg => { - appendMessage(msg.user_id, msg.timestamp, msg.content); + appendMessage(msg.display_name, msg.timestamp, msg.content); }); } }, @@ -16,15 +16,42 @@ function fetchMessages(baseURL, chatroomId) { }); } -function appendMessage(senderName, timestamp, content) { +function sendMessage(csrfToken, chatroomId, userId, displayName, content) { + console.log("csrf_token:", csrfToken, "chatroom_id:", chatroomId, "user_id:", userId); + $.ajax({ + url: buildCourseUrl(['chat', chatroomId, 'send']), + type: 'POST', + data: { + 'csrf_token': csrfToken, + 'user_id': userId, + 'content': content, + 'display_name': displayName + }, + error: function() { + window.alert('Something went wrong with storing message'); + }, + }) + window.socketClient.send({'type': "chat_message", 'content': content, 'user_id': userId, 'display_name': displayName, 'timestamp': new Date(Date.now()).toLocaleString()}) + appendMessage("me", null, content); +} + +function appendMessage(displayName, ts, content) { + let timestamp = ts; + if (timestamp === null) { + timestamp = new Date(Date.now()).toLocaleString('en-us', { year:"numeric", month:"short", day:"numeric", hour:"numeric", minute:"numeric"}); + } + else { + timestamp = new Date(ts).toLocaleString('en-us', { year:"numeric", month:"short", day:"numeric", hour:"numeric", minute:"numeric"}); + } + const messages_area = document.querySelector('.messages-area'); const message = document.createElement('div'); message.classList.add('message-container'); message.innerHTML = `
- ${senderName} - - ${timestamp} + ${displayName} + ${timestamp}
${content} @@ -35,16 +62,23 @@ function appendMessage(senderName, timestamp, content) { } function initChatroomSocketClient(chatroomId) { - + window.socketClient = new WebSocketClient(); + window.socketClient.onmessage = (msg) => { + if (msg.type === "chat_message") { + let sender = msg.display_name; + appendMessage(sender, msg.timestamp, msg.content); + } + }; + window.socketClient.open(`chatroom_${chatroomId}`); } function newChatroomForm() { - let form = $("#create-chatroom-form"); - form.css("display", "block"); + const form = $('#create-chatroom-form'); + form.css('display', 'block'); } function editChatroomForm(chatroom_id, baseUrl) { - let form= $("#edit-chatroom-form"); + const form= $('#edit-chatroom-form'); form.css('display', 'block'); document.getElementById('chatroom-edit-form').action = `${baseUrl}/${chatroom_id}/edit`; } @@ -54,30 +88,30 @@ document.addEventListener('DOMContentLoaded', () => { const pageDataElement = document.getElementById('page-data'); if (pageDataElement) { const pageData = JSON.parse(pageDataElement.textContent); - const { chatroomId, baseURL, userId } = pageData; - - const client = new WebSocketClient(); - client.onmessage = (msg) => { - console.log(`Data received from server:`, msg); - }; - client.onopen = () => { - client.send({ type: "ping" }); - } - client.open('some URL'); - - fetchMessages(baseURL, chatroomId); + const { csrfToken, chatroomId, userId, displayName } = pageData; + console.log(displayName); + initChatroomSocketClient(chatroomId) + fetchMessages(chatroomId); const sendMsgButton = document.querySelector('.send-message-btn'); const messageInput = document.querySelector('.message-input'); - sendMsgButton.addEventListener('click', function (e) { - e.preventDefault(); + messageInput.addEventListener("keypress", function(event) { + if (event.keyCode === 13 && !event.shiftKey) { + event.preventDefault(); + sendMsgButton.click(); + } + }); + + sendMsgButton.addEventListener('click', (event) => { + event.preventDefault(); const messageContent = messageInput.value.trim(); if (messageContent === '') { alert('Please enter a message.'); return; } + sendMessage(csrfToken, chatroomId, userId, displayName, messageContent); messageInput.value = ''; }); } -}); \ No newline at end of file +}); From 61b65f78426aa8773d7ab4335e040c25980fe394 Mon Sep 17 00:00:00 2001 From: Hanson Gu Date: Fri, 1 Mar 2024 06:57:28 -0500 Subject: [PATCH 07/19] fixed delete, more frontend adjustments --- site/app/controllers/ChatroomController.php | 13 ++-- site/app/entities/chat/Chatroom.php | 3 +- site/app/templates/chat/ChatPageIns.twig | 40 +++++------- site/app/templates/chat/ChatPageStu.twig | 24 ++++--- site/app/templates/chat/Chatroom.twig | 1 - .../templates/chat/CreateChatroomForm.twig | 11 +++- site/app/templates/chat/EditChatroomForm.twig | 11 +++- site/public/css/chatroom.css | 12 ++-- site/public/js/chatroom.js | 63 +++++++++++++++---- site/public/js/polls.js | 1 + 10 files changed, 120 insertions(+), 59 deletions(-) diff --git a/site/app/controllers/ChatroomController.php b/site/app/controllers/ChatroomController.php index fa7f04e326b..59025d7bf46 100644 --- a/site/app/controllers/ChatroomController.php +++ b/site/app/controllers/ChatroomController.php @@ -2,6 +2,7 @@ namespace app\controllers; +use app\entities\poll\Poll; use app\libraries\Core; use app\libraries\response\WebResponse; use app\libraries\response\JsonResponse; @@ -86,15 +87,19 @@ public function addChatroom(): RedirectResponse { } /** - * @Route("/courses/{_semester}/{_course}/chat/{chatroom_id}/delete", methods={"POST"}) + * @Route("/courses/{_semester}/{_course}/chat/deleteChatroom", methods={"POST"}) * @AccessControl(role="INSTRUCTOR") */ - public function deleteChatroom(int $chatroom_id): JsonResponse { + public function deleteChatroom(): JsonResponse { $chatroom_id = intval($_POST['chatroom_id'] ?? -1); $em = $this->core->getCourseEntityManager(); + + /** @var \app\repositories\chat\ChatroomRepository $repo */ $repo = $em->getRepository(Chatroom::class); - $chatroom = $repo->find(Chatroom::class, $chatroom_id); + /** @var Chatroom|null */ + $chatroom = $repo->findByChatroomId($chatroom_id); + if ($chatroom === null) { return JsonResponse::getFailResponse('Invalid Chatroom ID'); } @@ -105,7 +110,7 @@ public function deleteChatroom(int $chatroom_id): JsonResponse { $em->remove($chatroom); $em->flush(); - return JsonResponse::getSuccessResponse("Chatroom deleted successfully"); + return JsonResponse::getSuccessResponse(); } /** diff --git a/site/app/entities/chat/Chatroom.php b/site/app/entities/chat/Chatroom.php index 7b5f3d39131..b5cb8326836 100644 --- a/site/app/entities/chat/Chatroom.php +++ b/site/app/entities/chat/Chatroom.php @@ -34,11 +34,10 @@ class Chatroom { /** * @var Collection */ - #[ORM\OneToMany(mappedBy: "chat", targetEntity: Message::class, cascade: ["remove"])] + #[ORM\OneToMany(mappedBy: "chatroom", targetEntity: Message::class, cascade: ["remove"])] protected Collection $messages; public function __construct() { - $this->messages = new ArrayCollection(); $this->isActive = true; } diff --git a/site/app/templates/chat/ChatPageIns.twig b/site/app/templates/chat/ChatPageIns.twig index b5e0a86e427..dc12bcc40ec 100644 --- a/site/app/templates/chat/ChatPageIns.twig +++ b/site/app/templates/chat/ChatPageIns.twig @@ -2,23 +2,21 @@

Live Lecture Chat


-
+

Chatrooms

Name Description Host
+ + + {{ chatroom.getTitle() }}
- - + {{ chatroom.getTitle() }} @@ -39,10 +38,10 @@ Join - Delete + Delete - Show History + Show History
+ - - - - + + - - - - + + + @@ -27,21 +25,21 @@ - - - {% endfor %} @@ -50,9 +48,5 @@ - - {% include('chat/CreateChatroomForm.twig') %} {% include('chat/EditChatroomForm.twig') %} \ No newline at end of file diff --git a/site/app/templates/chat/ChatPageStu.twig b/site/app/templates/chat/ChatPageStu.twig index ec8832c2a0b..6657a1a795e 100644 --- a/site/app/templates/chat/ChatPageStu.twig +++ b/site/app/templates/chat/ChatPageStu.twig @@ -1,7 +1,7 @@

Live Lecture Chat


-
+

Chatrooms

NameDescriptionHost NameDescription
- {{ chatroom.getTitle() }} - - {{ chatroom.getDescription() }} + + - {{ chatroom.getHostId() }} + + + {{ chatroom.getTitle() | length > 15 ? chatroom.getTitle() | slice(0, 15) ~ '...' : chatroom.getTitle() }} + - Join - Delete + + {{ chatroom.getDescription() | length > 25 ? chatroom.getDescription() | slice(0, 25) ~ '...' : chatroom.getDescription() }} + - Show History + Join
@@ -10,24 +10,28 @@ - - - + + + {% for chatroom in chatrooms %} - - - + diff --git a/site/app/templates/chat/Chatroom.twig b/site/app/templates/chat/Chatroom.twig index 32811f54529..3d04f7023c5 100644 --- a/site/app/templates/chat/Chatroom.twig +++ b/site/app/templates/chat/Chatroom.twig @@ -15,7 +15,6 @@ +{% endblock %} +{% block buttons %} + {{ block('close_button') }} + {% endblock %} diff --git a/site/app/templates/chat/EditChatroomForm.twig b/site/app/templates/chat/EditChatroomForm.twig index bb06ca82a27..a99915f7106 100644 --- a/site/app/templates/chat/EditChatroomForm.twig +++ b/site/app/templates/chat/EditChatroomForm.twig @@ -13,7 +13,16 @@ Description: - + +{% endblock %} +{% block buttons %} + {{ block('close_button') }} + {% endblock %} \ No newline at end of file diff --git a/site/public/css/chatroom.css b/site/public/css/chatroom.css index e1c0589f8a0..134b3ccc1e7 100644 --- a/site/public/css/chatroom.css +++ b/site/public/css/chatroom.css @@ -1,7 +1,3 @@ -.chatroom-page { - -} - .chatroom-container { width: 60%; box-shadow: 0 0 2px 0 var(--default-black); @@ -36,11 +32,12 @@ .message-container { padding-bottom: 0.5rem; padding-top: 0.5rem; - margin-left: 2rem; + margin-left: 1rem; } .message-header { margin-bottom: 3px; + margin-left: 1.2rem; } .user-icon { @@ -59,7 +56,7 @@ } .message-content { - margin-left: 1.7rem; + margin-left: 1.2rem; max-width: 90%; word-break: break-word; } @@ -80,6 +77,9 @@ font-size: 15px; min-width: 85%; margin-left: 1rem; +} + +.message-input:focus { background-color: var(--standard-hover-light-gray); } diff --git a/site/public/js/chatroom.js b/site/public/js/chatroom.js index f3309f1e380..8f428b0cd7b 100644 --- a/site/public/js/chatroom.js +++ b/site/public/js/chatroom.js @@ -1,4 +1,5 @@ -function fetchMessages(chatroomId) { +/* global csrfToken userId */ +function fetchMessages(chatroomId, my_id) { $.ajax({ url: buildCourseUrl(['chat', chatroomId, 'messages']), type: 'GET', @@ -6,7 +7,12 @@ function fetchMessages(chatroomId) { success: function(responseData) { if (responseData.status === 'success' && Array.isArray(responseData.data)) { responseData.data.forEach(msg => { - appendMessage(msg.display_name, msg.timestamp, msg.content); + if (msg.user_id === my_id) { + appendMessage("me", msg.timestamp, msg.content); + } + else { + appendMessage(msg.display_name, msg.timestamp, msg.content); + } }); } }, @@ -16,8 +22,7 @@ function fetchMessages(chatroomId) { }); } -function sendMessage(csrfToken, chatroomId, userId, displayName, content) { - console.log("csrf_token:", csrfToken, "chatroom_id:", chatroomId, "user_id:", userId); +function sendMessage(chatroomId, userId, displayName, content) { $.ajax({ url: buildCourseUrl(['chat', chatroomId, 'send']), type: 'POST', @@ -37,7 +42,7 @@ function sendMessage(csrfToken, chatroomId, userId, displayName, content) { function appendMessage(displayName, ts, content) { let timestamp = ts; - if (timestamp === null) { + if (!timestamp) { timestamp = new Date(Date.now()).toLocaleString('en-us', { year:"numeric", month:"short", day:"numeric", hour:"numeric", minute:"numeric"}); } else { @@ -49,7 +54,6 @@ function appendMessage(displayName, ts, content) { message.classList.add('message-container'); message.innerHTML = `
- ${displayName} ${timestamp}
@@ -78,20 +82,57 @@ function newChatroomForm() { } function editChatroomForm(chatroom_id, baseUrl) { - const form= $('#edit-chatroom-form'); + const form = $('#edit-chatroom-form'); form.css('display', 'block'); document.getElementById('chatroom-edit-form').action = `${baseUrl}/${chatroom_id}/edit`; } +function deleteChatroomForm(chatroom_id, chatroom_name, base_url) { + if (confirm(`This will delete chatroom '${chatroom_name}'. Are you sure?`)) { + const url = `${base_url}/deleteChatroom`; + const fd = new FormData(); + fd.append('csrf_token', csrfToken); + fd.append('chatroom_id', chatroom_id); + $.ajax({ + url: url, + type: 'POST', + data: fd, + processData: false, + cache: false, + contentType: false, + success: function(data) { + try { + const msg = JSON.parse(data); + if (msg.status !== 'success') { + console.error(msg); + window.alert('Something went wrong. Please try again.'); + } + else { + window.location.reload(); + } + } + catch (err) { + console.error(err); + window.alert('Something went wrong. Please try again.'); + } + }, + error: function(err) { + console.error(err); + window.alert('Something went wrong. Please try again.'); + }, + }); + } +} + document.addEventListener('DOMContentLoaded', () => { $('.popup-form').css('display', 'none'); const pageDataElement = document.getElementById('page-data'); if (pageDataElement) { const pageData = JSON.parse(pageDataElement.textContent); - const { csrfToken, chatroomId, userId, displayName } = pageData; - console.log(displayName); + const { chatroomId, userId, displayName } = pageData; + initChatroomSocketClient(chatroomId) - fetchMessages(chatroomId); + fetchMessages(chatroomId, userId); const sendMsgButton = document.querySelector('.send-message-btn'); const messageInput = document.querySelector('.message-input'); @@ -110,7 +151,7 @@ document.addEventListener('DOMContentLoaded', () => { alert('Please enter a message.'); return; } - sendMessage(csrfToken, chatroomId, userId, displayName, messageContent); + sendMessage(chatroomId, userId, displayName, messageContent); messageInput.value = ''; }); } diff --git a/site/public/js/polls.js b/site/public/js/polls.js index 26cd51cab12..ffbe8a11e4e 100644 --- a/site/public/js/polls.js +++ b/site/public/js/polls.js @@ -24,6 +24,7 @@ function newDeletePollForm(pollid, pollname, base_url) { success: function(data) { try { const msg = JSON.parse(data); + console.log(data); if (msg.status !== 'success') { console.error(msg); window.alert('Something went wrong. Please try again.'); From 3586e8dd5f1938c857c1a59d009c9c698bae47a3 Mon Sep 17 00:00:00 2001 From: Hanson Gu Date: Fri, 1 Mar 2024 07:24:12 -0500 Subject: [PATCH 08/19] remove irrelevant changes --- site/public/js/polls.js | 1 - site/public/js/server.js | 1 - 2 files changed, 2 deletions(-) diff --git a/site/public/js/polls.js b/site/public/js/polls.js index ffbe8a11e4e..26cd51cab12 100644 --- a/site/public/js/polls.js +++ b/site/public/js/polls.js @@ -24,7 +24,6 @@ function newDeletePollForm(pollid, pollname, base_url) { success: function(data) { try { const msg = JSON.parse(data); - console.log(data); if (msg.status !== 'success') { console.error(msg); window.alert('Something went wrong. Please try again.'); diff --git a/site/public/js/server.js b/site/public/js/server.js index bcf4518283a..eacbb9cbe96 100644 --- a/site/public/js/server.js +++ b/site/public/js/server.js @@ -1892,5 +1892,4 @@ function tzWarn() { Cookies.set("last_tz_warn_time", Date.now()); } } - document.addEventListener('DOMContentLoaded', tzWarn); \ No newline at end of file From bfaf6e7cc7e3663bdeb4ff2dbe9391bb5bd4d7df Mon Sep 17 00:00:00 2001 From: Hanson Gu Date: Fri, 1 Mar 2024 13:35:15 -0500 Subject: [PATCH 09/19] add role UI --- migration/migrator/data/course_tables.sql | 1 + .../course/20240209021641_lecture_chat.py | 2 +- site/app/controllers/ChatroomController.php | 6 ++- site/app/entities/chat/Message.php | 12 +++++ site/public/css/chatroom.css | 15 +++--- site/public/js/chatroom.js | 50 +++++++++++++------ site/public/js/server.js | 2 +- 7 files changed, 61 insertions(+), 27 deletions(-) diff --git a/migration/migrator/data/course_tables.sql b/migration/migrator/data/course_tables.sql index 902187bd476..c5cd5bb411e 100644 --- a/migration/migrator/data/course_tables.sql +++ b/migration/migrator/data/course_tables.sql @@ -711,6 +711,7 @@ CREATE TABLE public.chatroom_messages ( chatroom_id integer NOT NULL, userid character varying NOT NULL, display_name character varying, + role character varying, content text NOT NULL, "timestamp" timestamp(0) with time zone NOT NULL ); diff --git a/migration/migrator/migrations/course/20240209021641_lecture_chat.py b/migration/migrator/migrations/course/20240209021641_lecture_chat.py index 0a9fa900fac..4ab210576c2 100644 --- a/migration/migrator/migrations/course/20240209021641_lecture_chat.py +++ b/migration/migrator/migrations/course/20240209021641_lecture_chat.py @@ -16,7 +16,7 @@ def up(config, database, semester, course): :type course: str """ database.execute("CREATE TABLE IF NOT EXISTS chatrooms(id SERIAL PRIMARY KEY, host_id character varying NOT NULL, title text NOT NULL, description text, is_active BOOLEAN DEFAULT false NOT NULL)") - database.execute("CREATE TABLE IF NOT EXISTS chatroom_messages(id SERIAL PRIMARY KEY, chatroom_id integer NOT NULL, userId character varying NOT NULL, display_name character varying, content text NOT NULL, timestamp timestamp(0) with time zone NOT NULL)") + database.execute("CREATE TABLE IF NOT EXISTS chatroom_messages(id SERIAL PRIMARY KEY, chatroom_id integer NOT NULL, userId character varying NOT NULL, display_name character varying, role character varying, content text NOT NULL, timestamp timestamp(0) with time zone NOT NULL)") course_dir = Path(config.submitty['submitty_data_dir'], 'courses', semester, course) # add boolean to course config diff --git a/site/app/controllers/ChatroomController.php b/site/app/controllers/ChatroomController.php index 59025d7bf46..88db69fd8c1 100644 --- a/site/app/controllers/ChatroomController.php +++ b/site/app/controllers/ChatroomController.php @@ -153,7 +153,8 @@ public function fetchMessages(int $chatroom_id): JsonResponse { 'content' => $message->getContent(), 'timestamp' => $message->getTimestamp()->format('Y-m-d H:i:s'), 'user_id' => $message->getUserId(), - 'display_name' => $message->getDisplayName() + 'display_name' => $message->getDisplayName(), + 'role' => $message->getRole() ]; }, $messages); @@ -168,14 +169,15 @@ public function sendMessage(int $chatroom_id): JsonResponse { $content = $_POST['content'] ?? ''; $userId = $_POST['user_id'] ?? null; $displayName = $_POST['display_name'] ?? ''; + $role = $_POST['role'] ?? 'student'; $chatroom = $em->getRepository(Chatroom::class)->find($chatroom_id); $message = new Message(); $message->setChatroom($chatroom); $message->setUserId($userId); $message->setDisplayName($displayName); + $message->setRole($role); $message->setContent($content); - $message->setTimestamp(new \DateTime("now")); $em->persist($message); $em->flush(); diff --git a/site/app/entities/chat/Message.php b/site/app/entities/chat/Message.php index cab31c46c01..b8e72040ad1 100644 --- a/site/app/entities/chat/Message.php +++ b/site/app/entities/chat/Message.php @@ -28,6 +28,9 @@ class Message { #[ORM\Column(type: Types::STRING)] private string $display_name; + #[ORM\Column(type: Types::STRING)] + private string $role; + #[ORM\Column(type: Types::TEXT)] private string $content; @@ -35,6 +38,7 @@ class Message { protected DateTime $timestamp; public function __construct() { + $this->setTimestamp(new \DateTime("now")); } public function getId(): int { @@ -57,6 +61,14 @@ public function setDisplayName($displayName): void { $this->display_name = $displayName; } + public function getRole(): string { + return $this->role; + } + + public function setRole($role): string { + return $this->role = $role; + } + public function getContent(): string { return $this->content; } diff --git a/site/public/css/chatroom.css b/site/public/css/chatroom.css index 134b3ccc1e7..c98f7ab9fd9 100644 --- a/site/public/css/chatroom.css +++ b/site/public/css/chatroom.css @@ -16,9 +16,6 @@ padding-left: 1rem; } -.chatroom-content { -} - .leave-room { float: right; margin-right: 1rem; @@ -27,17 +24,22 @@ .messages-area { overflow-y: auto; height: 38rem; + margin-left: 1rem; } .message-container { padding-bottom: 0.5rem; padding-top: 0.5rem; - margin-left: 1rem; + margin-right: 1rem; +} + +.message-container:hover { + background-color: var(--standard-hover-light-gray); } .message-header { margin-bottom: 3px; - margin-left: 1.2rem; + margin-left: 0.5rem; } .user-icon { @@ -46,7 +48,6 @@ .sender-name { font-weight: bold; - font-size: 1.05rem; } .timestamp { @@ -56,9 +57,9 @@ } .message-content { - margin-left: 1.2rem; max-width: 90%; word-break: break-word; + margin-left: 0.5rem; } .input-container { diff --git a/site/public/js/chatroom.js b/site/public/js/chatroom.js index 8f428b0cd7b..13dacdacdbd 100644 --- a/site/public/js/chatroom.js +++ b/site/public/js/chatroom.js @@ -7,13 +7,14 @@ function fetchMessages(chatroomId, my_id) { success: function(responseData) { if (responseData.status === 'success' && Array.isArray(responseData.data)) { responseData.data.forEach(msg => { + let display_name = msg.display_name; if (msg.user_id === my_id) { - appendMessage("me", msg.timestamp, msg.content); - } - else { - appendMessage(msg.display_name, msg.timestamp, msg.content); + display_name = "me"; } + appendMessage(display_name, msg.role, msg.timestamp, msg.content); }); + const messages_area = document.querySelector(".messages-area"); + messages_area.scrollTop = messages_area.scrollHeight; } }, error: function() { @@ -22,25 +23,26 @@ function fetchMessages(chatroomId, my_id) { }); } -function sendMessage(chatroomId, userId, displayName, content) { +function sendMessage(chatroomId, userId, displayName, role, content) { $.ajax({ url: buildCourseUrl(['chat', chatroomId, 'send']), type: 'POST', data: { 'csrf_token': csrfToken, 'user_id': userId, - 'content': content, - 'display_name': displayName + 'display_name': displayName, + 'role': role, + 'content': content }, error: function() { window.alert('Something went wrong with storing message'); }, }) - window.socketClient.send({'type': "chat_message", 'content': content, 'user_id': userId, 'display_name': displayName, 'timestamp': new Date(Date.now()).toLocaleString()}) - appendMessage("me", null, content); + window.socketClient.send({'type': "chat_message", 'content': content, 'user_id': userId, 'display_name': displayName, 'role': role, 'timestamp': new Date(Date.now()).toLocaleString()}) + appendMessage("me", role, null, content); } -function appendMessage(displayName, ts, content) { +function appendMessage(displayName, role, ts, content) { let timestamp = ts; if (!timestamp) { timestamp = new Date(Date.now()).toLocaleString('en-us', { year:"numeric", month:"short", day:"numeric", hour:"numeric", minute:"numeric"}); @@ -49,28 +51,41 @@ function appendMessage(displayName, ts, content) { timestamp = new Date(ts).toLocaleString('en-us', { year:"numeric", month:"short", day:"numeric", hour:"numeric", minute:"numeric"}); } + let display_name = displayName; + if (role && role !== 'student' && display_name !== 'me') { + display_name = `${displayName}[instructor]`; + } + const messages_area = document.querySelector('.messages-area'); const message = document.createElement('div'); message.classList.add('message-container'); + if (role === "instructor") { + message.classList.add('admin-message') + } message.innerHTML = `
- ${displayName} + ${display_name} ${timestamp}
${content}
`; + messages_area.appendChild(message); - messages_area.scrollTop = messages_area.scrollHeight; + const distanceFromBottom = messages_area.scrollHeight - messages_area.scrollTop - messages_area.clientHeight; + if ( distanceFromBottom < 110) { + messages_area.scrollTop = messages_area.scrollHeight; + } } function initChatroomSocketClient(chatroomId) { window.socketClient = new WebSocketClient(); window.socketClient.onmessage = (msg) => { if (msg.type === "chat_message") { - let sender = msg.display_name; - appendMessage(sender, msg.timestamp, msg.content); + let sender_name = msg.display_name; + let role = msg.role; + appendMessage(sender_name, role, msg.timestamp, msg.content); } }; window.socketClient.open(`chatroom_${chatroomId}`); @@ -129,7 +144,7 @@ document.addEventListener('DOMContentLoaded', () => { const pageDataElement = document.getElementById('page-data'); if (pageDataElement) { const pageData = JSON.parse(pageDataElement.textContent); - const { chatroomId, userId, displayName } = pageData; + const { chatroomId, userId, displayName, user_admin } = pageData; initChatroomSocketClient(chatroomId) fetchMessages(chatroomId, userId); @@ -151,7 +166,10 @@ document.addEventListener('DOMContentLoaded', () => { alert('Please enter a message.'); return; } - sendMessage(chatroomId, userId, displayName, messageContent); + + let role = user_admin ? 'instructor' : 'student'; + sendMessage(chatroomId, userId, displayName, role, messageContent); + messageInput.value = ''; }); } diff --git a/site/public/js/server.js b/site/public/js/server.js index eacbb9cbe96..2c89ec6ce08 100644 --- a/site/public/js/server.js +++ b/site/public/js/server.js @@ -1892,4 +1892,4 @@ function tzWarn() { Cookies.set("last_tz_warn_time", Date.now()); } } -document.addEventListener('DOMContentLoaded', tzWarn); \ No newline at end of file +document.addEventListener('DOMContentLoaded', tzWarn); From 0d7574f6290ec0ca181c52dba0eed33a8c5b4393 Mon Sep 17 00:00:00 2001 From: Hanson Gu Date: Sun, 10 Mar 2024 00:39:26 -0500 Subject: [PATCH 10/19] anonymous, toast, enable/disable anon. --- migration/migrator/data/course_tables.sql | 4 +- .../course/20240209021641_lecture_chat.py | 2 +- site/app/controllers/ChatroomController.php | 135 ++++++++++++------ site/app/entities/chat/Chatroom.php | 53 ++++--- site/app/entities/chat/Message.php | 4 +- .../repositories/chat/ChatroomRepository.php | 12 -- site/app/templates/chat/ChatPageIns.twig | 41 ++++-- site/app/templates/chat/ChatPageStu.twig | 10 +- site/app/templates/chat/Chatroom.twig | 10 +- .../templates/chat/CreateChatroomForm.twig | 4 +- site/app/templates/chat/EditChatroomForm.twig | 12 +- site/app/views/ChatroomView.php | 15 +- site/public/css/chatroom.css | 22 ++- site/public/js/chatroom.js | 108 +++++++++++--- 14 files changed, 304 insertions(+), 128 deletions(-) diff --git a/migration/migrator/data/course_tables.sql b/migration/migrator/data/course_tables.sql index c5cd5bb411e..c866da788c8 100644 --- a/migration/migrator/data/course_tables.sql +++ b/migration/migrator/data/course_tables.sql @@ -744,9 +744,11 @@ ALTER SEQUENCE public.chatroom_messages_id_seq OWNED BY public.chatroom_messages CREATE TABLE public.chatrooms ( id integer NOT NULL, host_id character varying NOT NULL, + host_name character varying, title text NOT NULL, description text, - is_active boolean DEFAULT false NOT NULL + is_active boolean DEFAULT false NOT NULL, + allow_anon boolean DEFAULT true NOT NULL ); diff --git a/migration/migrator/migrations/course/20240209021641_lecture_chat.py b/migration/migrator/migrations/course/20240209021641_lecture_chat.py index 4ab210576c2..583837556a3 100644 --- a/migration/migrator/migrations/course/20240209021641_lecture_chat.py +++ b/migration/migrator/migrations/course/20240209021641_lecture_chat.py @@ -15,7 +15,7 @@ def up(config, database, semester, course): :param course: Code of course being migrated :type course: str """ - database.execute("CREATE TABLE IF NOT EXISTS chatrooms(id SERIAL PRIMARY KEY, host_id character varying NOT NULL, title text NOT NULL, description text, is_active BOOLEAN DEFAULT false NOT NULL)") + database.execute("CREATE TABLE IF NOT EXISTS chatrooms(id SERIAL PRIMARY KEY, host_id character varying NOT NULL, host_name character varying, title text NOT NULL, description text, is_active BOOLEAN DEFAULT false NOT NULL, allow_anon BOOLEAN DEFAULT true NOT NULL)") database.execute("CREATE TABLE IF NOT EXISTS chatroom_messages(id SERIAL PRIMARY KEY, chatroom_id integer NOT NULL, userId character varying NOT NULL, display_name character varying, role character varying, content text NOT NULL, timestamp timestamp(0) with time zone NOT NULL)") course_dir = Path(config.submitty['submitty_data_dir'], 'courses', semester, course) diff --git a/site/app/controllers/ChatroomController.php b/site/app/controllers/ChatroomController.php index 88db69fd8c1..c8137c114b4 100644 --- a/site/app/controllers/ChatroomController.php +++ b/site/app/controllers/ChatroomController.php @@ -2,7 +2,6 @@ namespace app\controllers; -use app\entities\poll\Poll; use app\libraries\Core; use app\libraries\response\WebResponse; use app\libraries\response\JsonResponse; @@ -27,8 +26,8 @@ public function __construct(Core $core) { public function showChatroomsPage(): WebResponse { $repo = $this->core->getCourseEntityManager()->getRepository(Chatroom::class); $user = $this->core->getUser(); - $chatrooms = $repo->findAll(); - $active_chatrooms = $repo->findAllActiveChatrooms(); + $chatrooms = $repo->findBy([], ['id' => 'ASC']); + $active_chatrooms = $repo->findBy(['is_active' => true], ['id' => 'ASC']); if ($user->accessAdmin()) { return new WebResponse( @@ -46,12 +45,32 @@ public function showChatroomsPage(): WebResponse { } } + /** + * @Route("/courses/{_semester}/{_course}/chat/newChatroom", methods={"POST"}) + * @AccessControl(role="INSTRUCTOR") + */ + public function addChatroom(): RedirectResponse { + $em = $this->core->getCourseEntityManager(); + + $chatroom = new Chatroom(); + $chatroom->setTitle($_POST['title']); + $chatroom->setDescription($_POST['description']); + $chatroom->setHostId($this->core->getUser()->getId()); + $chatroom->setHostName($this->core->getUser()->getDisplayFullName()); + + $em->persist($chatroom); + $em->flush(); + + $this->core->addSuccessMessage("Chatroom successfully added"); + return new RedirectResponse($this->core->buildCourseUrl(['chat'])); + } + /** * @Route("/courses/{_semester}/{_course}/chat/{chatroom_id}", methods={"GET"}) * @param string $chatroom_id * @return RedirectResponse|WebResponse */ - public function showChatroom(string $chatroom_id): WebResponse|RedirectResponse { + public function getChatroom(string $chatroom_id): WebResponse|RedirectResponse { if (!is_numeric($chatroom_id)) { $this->core->addErrorMessage("Invalid Chatroom ID"); return new RedirectResponse($this->core->buildCourseUrl(['chat'])); @@ -63,28 +82,30 @@ public function showChatroom(string $chatroom_id): WebResponse|RedirectResponse return new WebResponse( 'Chatroom', 'showChatroom', - $chatroom + $chatroom, ); } - /** - * @Route("/courses/{_semester}/{_course}/chat/newChatroom", name="new_chatroom", methods={"POST"}) - * @AccessControl(role="INSTRUCTOR") + /** + * @Route("/courses/{_semester}/{_course}/chat/{chatroom_id}/anonymous", methods={"GET"}) + * @param string $chatroom_id + * @return RedirectResponse|WebResponse */ - public function addChatroom(): RedirectResponse { - $em = $this->core->getCourseEntityManager(); - - $chatroom = new Chatroom(); - $chatroom->setTitle($_POST['title']); - $chatroom->setDescription($_POST['description']); - $chatroom->setHostId($this->core->getUser()->getId()); + public function getChatroomAnon(string $chatroom_id): WebResponse|RedirectResponse { + if (!is_numeric($chatroom_id)) { + $this->core->addErrorMessage("Invalid Chatroom ID"); + return new RedirectResponse($this->core->buildCourseUrl(['chat'])); + } - $em->persist($chatroom); - $em->flush(); + $repo = $this->core->getCourseEntityManager()->getRepository(Chatroom::class); + $chatroom = $repo->find($chatroom_id); - $this->core->addSuccessMessage("Chatroom successfully added"); - return new RedirectResponse($this->core->buildCourseUrl(['chat'])); - } + return new WebResponse( + 'Chatroom', + 'showChatroom', + $chatroom, true, + ); + } /** * @Route("/courses/{_semester}/{_course}/chat/deleteChatroom", methods={"POST"}) @@ -98,7 +119,7 @@ public function deleteChatroom(): JsonResponse { $repo = $em->getRepository(Chatroom::class); /** @var Chatroom|null */ - $chatroom = $repo->findByChatroomId($chatroom_id); + $chatroom = $repo->find($chatroom_id); if ($chatroom === null) { return JsonResponse::getFailResponse('Invalid Chatroom ID'); @@ -113,35 +134,61 @@ public function deleteChatroom(): JsonResponse { return JsonResponse::getSuccessResponse(); } -/** - * @Route("/courses/{_semester}/{_course}/chat/{chatroom_id}/edit", name="edit_chatroom", methods={"POST"}) - * @AccessControl(role="INSTRUCTOR") - */ -public function editChatroom(int $chatroom_id): RedirectResponse { - $em = $this->core->getCourseEntityManager(); - $chatroom = $em->getRepository(Chatroom::class)->find($chatroom_id); + /** + * @Route("/courses/{_semester}/{_course}/chat/{chatroom_id}/edit", methods={"POST"}) + * @AccessControl(role="INSTRUCTOR") + */ + public function editChatroom(int $chatroom_id): RedirectResponse { + $em = $this->core->getCourseEntityManager(); + $chatroom = $em->getRepository(Chatroom::class)->find($chatroom_id); + + if (!$chatroom) { + $this->core->addErrorMessage("Chatroom not found"); + return new RedirectResponse($this->core->buildCourseUrl(['chat'])); + } + + if (isset($_POST['title'])) { + $chatroom->setTitle($_POST['title']); + } + if (isset($_POST['description'])) { + $chatroom->setDescription($_POST['description']); + } + if (isset($_POST['allow-anon'])) { + $chatroom->setAllowAnon(true); + } + else { + $chatroom->setAllowAnon(false); + } + + $em->flush(); - if (!$chatroom) { - $this->core->addErrorMessage("Chatroom not found"); + $this->core->addSuccessMessage("Chatroom successfully updated"); return new RedirectResponse($this->core->buildCourseUrl(['chat'])); } - if (isset($_POST['title'])) { - $chatroom->setTitle($_POST['title']); - } - if (isset($_POST['description'])) { - $chatroom->setDescription($_POST['description']); - } + /** + * @Route("/courses/{_semester}/{_course}/chat/{chatroom_id}/toggleOnOff", methods={"POST"}) + * @AccessControl(role="INSTRUCTOR") + */ + public function toggleChatroomOnOf(int $chatroom_id): RedirectResponse { + $em = $this->core->getCourseEntityManager(); + $chatroom = $em->getRepository(Chatroom::class)->find($chatroom_id); - $em->persist($chatroom); - $em->flush(); + if ($chatroom === null) { + $this->core->addErrorMessage("Chatroom not found"); + return new RedirectResponse($this->core->buildCourseUrl(['chat'])); + } + + $chatroom->toggle_on_off(); + + $em->flush(); + + return new RedirectResponse($this->core->buildCourseUrl(['chat'])); + } - $this->core->addSuccessMessage("Chatroom successfully updated"); - return new RedirectResponse($this->core->buildCourseUrl(['chat'])); -} /** - * @Route("/courses/{_semester}/{_course}/chat/{chatroom_id}/messages", name="fetch_chatroom_messages", methods={"GET"}) + * @Route("/courses/{_semester}/{_course}/chat/{chatroom_id}/messages", methods={"GET"}) */ public function fetchMessages(int $chatroom_id): JsonResponse { $em = $this->core->getCourseEntityManager(); @@ -162,9 +209,9 @@ public function fetchMessages(int $chatroom_id): JsonResponse { } /** - * @Route("/courses/{_semester}/{_course}/chat/{chatroom_id}/send", name="send_chatroom_messages", methods={"POST"}) + * @Route("/courses/{_semester}/{_course}/chat/{chatroom_id}/send", methods={"POST"}) */ - public function sendMessage(int $chatroom_id): JsonResponse { + public function addMessage(int $chatroom_id): JsonResponse { $em = $this->core->getCourseEntityManager(); $content = $_POST['content'] ?? ''; $userId = $_POST['user_id'] ?? null; diff --git a/site/app/entities/chat/Chatroom.php b/site/app/entities/chat/Chatroom.php index b5cb8326836..87c0aed1064 100644 --- a/site/app/entities/chat/Chatroom.php +++ b/site/app/entities/chat/Chatroom.php @@ -17,29 +17,36 @@ class Chatroom { #[ORM\Id] #[ORM\Column(name: "id", type: Types::INTEGER)] #[ORM\GeneratedValue] - protected int $id; + private int $id; #[ORM\Column(type: Types::STRING)] - protected string $host_id; + private string $host_id; - #[ORM\Column(name: "title", type: Types::STRING)] - protected string $title; + #[ORM\Column(type: Types::STRING)] + private string $host_name; + + #[ORM\Column(type: Types::STRING)] + private string $title; - #[ORM\Column(name: "description", type: Types::STRING)] - protected string $description; + #[ORM\Column(type: Types::STRING)] + private string $description; + + #[ORM\Column(type: Types::BOOLEAN)] + private bool $is_active; - #[ORM\Column(name: "is_active", type: Types::BOOLEAN)] - protected bool $isActive; + #[ORM\Column(type: Types::BOOLEAN)] + private bool $allow_anon; /** * @var Collection */ - #[ORM\OneToMany(mappedBy: "chatroom", targetEntity: Message::class, cascade: ["remove"])] - protected Collection $messages; + #[ORM\OneToMany(mappedBy: "chatroom", targetEntity: Message::class)] + private Collection $messages; public function __construct() { $this->messages = new ArrayCollection(); - $this->isActive = true; + $this->is_active = false; + $this->allow_anon = true; } public function getId(): int { @@ -54,6 +61,14 @@ public function getHostId(): string { return $this->host_id; } + public function setHostName($hostName): void { + $this->host_name = $hostName; + } + + public function getHostName(): string { + return $this->host_name; + } + public function getTitle(): string { return $this->title; } @@ -70,16 +85,20 @@ public function setDescription($description): void { $this->description = $description; } - public function getStatus(): bool { - return $this->isActive; + public function toggle_on_off(): void { + $this->is_active = !$this->is_active; + } + + public function isActive(): bool { + return $this->is_active; } - public function activate(): void { - $this->isActive = True; + public function isAllowAnon(): bool { + return $this->allow_anon; } - public function deactivate(): void { - $this->isActive = False; + public function setAllowAnon($allow_anon): void { + $this->allow_anon = $allow_anon; } public function getMessages(): Collection { diff --git a/site/app/entities/chat/Message.php b/site/app/entities/chat/Message.php index b8e72040ad1..fb51c335afa 100644 --- a/site/app/entities/chat/Message.php +++ b/site/app/entities/chat/Message.php @@ -18,7 +18,7 @@ class Message { #[ORM\GeneratedValue] private int $id; - #[ORM\ManyToOne(targetEntity: "Chatroom")] + #[ORM\ManyToOne(targetEntity: Chatroom::class)] #[ORM\JoinColumn(name: "chatroom_id", referencedColumnName: "id")] private Chatroom $chatroom; @@ -35,7 +35,7 @@ class Message { private string $content; #[ORM\Column(type: Types::DATETIMETZ_MUTABLE)] - protected DateTime $timestamp; + private DateTime $timestamp; public function __construct() { $this->setTimestamp(new \DateTime("now")); diff --git a/site/app/repositories/chat/ChatroomRepository.php b/site/app/repositories/chat/ChatroomRepository.php index 13d4331b0cf..19f20901802 100644 --- a/site/app/repositories/chat/ChatroomRepository.php +++ b/site/app/repositories/chat/ChatroomRepository.php @@ -32,16 +32,4 @@ public function findAllInactiveChatrooms() { ->getQuery() ->getResult(); } - - public function findByChatroomId(string $chatroomId): ?Chatroom { - $result = $this->createQueryBuilder('c') - ->where('c.id = :id') - ->setParameter('id', $chatroomId) - ->getQuery() - ->getResult(); - if (count($result) === 0) { - return null; - } - return $result[0]; - } } diff --git a/site/app/templates/chat/ChatPageIns.twig b/site/app/templates/chat/ChatPageIns.twig index dc12bcc40ec..d130a4da19b 100644 --- a/site/app/templates/chat/ChatPageIns.twig +++ b/site/app/templates/chat/ChatPageIns.twig @@ -7,9 +7,10 @@
NameDescriptionHostNameHostDescription
- {{ chatroom.getTitle() }} - - {{ chatroom.getDescription() }} + + + {{ chatroom.getTitle() | length > 15 ? chatroom.getTitle() | slice(0, 15) ~ '...' : chatroom.getTitle() }} + + {{ chatroom.getHostId() }} + + {{ chatroom.getDescription() | length > 25 ? chatroom.getDescription() | slice(0, 25) ~ '...' : chatroom.getDescription() }} + + Join
- - + + + @@ -17,29 +18,51 @@ + {% for chatroom in chatrooms %} + {% endfor %} diff --git a/site/app/templates/chat/ChatPageStu.twig b/site/app/templates/chat/ChatPageStu.twig index 6657a1a795e..6f523e75737 100644 --- a/site/app/templates/chat/ChatPageStu.twig +++ b/site/app/templates/chat/ChatPageStu.twig @@ -21,19 +21,23 @@ {% endfor %} diff --git a/site/app/templates/chat/Chatroom.twig b/site/app/templates/chat/Chatroom.twig index 3d04f7023c5..79fe5711c63 100644 --- a/site/app/templates/chat/Chatroom.twig +++ b/site/app/templates/chat/Chatroom.twig @@ -1,7 +1,8 @@ -
+
+
- {{ chatroom.getTitle() }} + {{ chatroom.getTitle() | length > 40 ? chatroom.getTitle() | slice(0, 40) ~ '...' : chatroom.getTitle() }} leave
@@ -19,6 +20,7 @@ "baseURL": "{{ base_url }}", "userId": "{{ user_id }}", "displayName": "{{ user_display_name }}", - "user_admin": "{{ user_admin }}" + "user_admin": "{{ user_admin }}", + "isAnonymous": "{{ anonymous }}" } - \ No newline at end of file + diff --git a/site/app/templates/chat/CreateChatroomForm.twig b/site/app/templates/chat/CreateChatroomForm.twig index f2d24165ae9..65747e443f4 100644 --- a/site/app/templates/chat/CreateChatroomForm.twig +++ b/site/app/templates/chat/CreateChatroomForm.twig @@ -10,10 +10,10 @@ Chatroom Title: -
Name Description
- + - + {% if not chatroom.isActive() %} + + {% endif %} - - {{ chatroom.getTitle() | length > 15 ? chatroom.getTitle() | slice(0, 15) ~ '...' : chatroom.getTitle() }} + + {{ chatroom.getTitle() | length > 30 ? chatroom.getTitle() | slice(0, 30) ~ '...' : chatroom.getTitle() }} - - {{ chatroom.getDescription() | length > 25 ? chatroom.getDescription() | slice(0, 25) ~ '...' : chatroom.getDescription() }} + + {{ chatroom.getDescription() | length > 45 ? chatroom.getDescription() | slice(0, 45) ~ '...' : chatroom.getDescription() }} - Join +
+ +
+ {% if not chatroom.isActive() %} + + {% else %} + + {% endif %} +
+ join + {% if chatroom.isAllowAnon() %} + or + join as anon. + {% endif %}
- {{ chatroom.getTitle() | length > 15 ? chatroom.getTitle() | slice(0, 15) ~ '...' : chatroom.getTitle() }} + {{ chatroom.getTitle() | length > 30 ? chatroom.getTitle() | slice(0, 30) ~ '...' : chatroom.getTitle() }} - {{ chatroom.getHostId() }} + {{ chatroom.getHostName() }} - {{ chatroom.getDescription() | length > 25 ? chatroom.getDescription() | slice(0, 25) ~ '...' : chatroom.getDescription() }} + {{ chatroom.getDescription() | length > 45 ? chatroom.getDescription() | slice(0, 45) ~ '...' : chatroom.getDescription() }} Join + {% if chatroom.isAllowAnon() %} + or + join as anon. + {% endif %}