diff --git a/course/edit_form.php b/course/edit_form.php index fdcdad7f299..897ee07b2c7 100644 --- a/course/edit_form.php +++ b/course/edit_form.php @@ -403,6 +403,16 @@ class course_edit_form extends moodleform { $handler->set_parent_context($categorycontext); // For course handler only. $handler->instance_form_definition($mform, empty($course->id) ? 0 : $course->id); + // Add communication plugins to the form. + if (core_communication\api::is_available()) { + $communication = \core_communication\api::load_by_instance( + 'core_course', + 'coursecommunication', + empty($course->id) ? 0 : $course->id); + $communication->form_definition($mform); + $communication->set_data($course); + } + // When two elements we need a group. $buttonarray = array(); $classarray = array('class' => 'form-submit'); diff --git a/course/lib.php b/course/lib.php index 7b9f0f9ec92..3748203e437 100644 --- a/course/lib.php +++ b/course/lib.php @@ -24,6 +24,7 @@ defined('MOODLE_INTERNAL') || die; +use core_course\external\course_summary_exporter; use core_courseformat\base as course_format; require_once($CFG->libdir.'/completionlib.php'); @@ -2313,6 +2314,18 @@ function create_course($data, $editoroptions = NULL) { core_tag_tag::set_item_tags('core', 'course', $course->id, context_course::instance($course->id), $data->tags); } + // Communication api implementation in course. + if (isset($data->selectedcommunication) && core_communication\api::is_available()) { + // Prepare the communication api date. + $courseimage = course_summary_exporter::get_course_image($course); + $communicationroomname = !empty($data->communicationroomname) ? $data->communicationroomname : $data->fullname; + $selectedcommunication = $data->selectedcommunication; + + // Communication api call. + $communication = \core_communication\api::load_by_instance('core_course', 'coursecommunication', $course->id); + $communication->create_and_configure_room($selectedcommunication, $communicationroomname, $courseimage); + } + // Save custom fields if there are any of them in the form. $handler = core_course\customfield\course_handler::create(); // Make sure to set the handler's parent context first. @@ -2434,6 +2447,71 @@ function update_course($data, $editoroptions = NULL) { $data->showcompletionconditions = null; } + // Check if provider is selected. + $provider = $data->selectedcommunication ?? null; + // If the course moved to hidden category, set provider to none. + if ($changesincoursecat && empty($data->visible)) { + $provider = 'none'; + } + + // Communication api call. + if (!empty($provider) && core_communication\api::is_available()) { + // Prepare the communication api data. + $courseimage = course_summary_exporter::get_course_image($data); + if (!$courseimage) { + $courseimage = null; + } + + // This nasty logic is here because of hide course doesn't pass anything in the data object. + if (!empty($data->communicationroomname)) { + $communicationroomname = $data->communicationroomname; + } else { + $communicationroomname = $data->fullname ?? $oldcourse->fullname; + } + + // Update communication room membership of enrolled users. + require_once($CFG->libdir . '/enrollib.php'); + $courseusers = enrol_get_course_users($data->id); + $enrolledusers = []; + + foreach ($courseusers as $user) { + $enrolledusers[] = $user->id; + } + + $communication = \core_communication\api::load_by_instance( + 'core_course', + 'coursecommunication', + $data->id + ); + + $addafterupdate = false; + if ($provider !== $communication->get_provider()) { + // If provider set to none, remove all the members. + if ($provider === 'none') { + $communication->remove_members_from_room($enrolledusers); + } else if ( + // If previous provider was not none and current provider is not none, but a different provider, remove members. + $communication->get_provider() !== '' && + $communication->get_provider() !== 'none' && + $provider !== $communication->get_provider() + ) { + $communication->remove_members_from_room($enrolledusers); + $addafterupdate = true; + } else if ( + // If previous provider was none and current provider is not none, but a different provider, remove members. + ($communication->get_provider() === '' || $communication->get_provider() === 'none') && + $provider !== $communication->get_provider() + ) { + $addafterupdate = true; + } + } + + $communication->update_room($provider, $communicationroomname, $courseimage); + if ($addafterupdate) { + $communication->add_members_to_room($enrolledusers, false); + } + } + // Update custom fields if there are any of them in the form. $handler = core_course\customfield\course_handler::create(); $handler->instance_form_save($data); diff --git a/lang/en/course.php b/lang/en/course.php index d4471fe6b29..e01ce31d124 100644 --- a/lang/en/course.php +++ b/lang/en/course.php @@ -51,6 +51,7 @@ $string['aria:recommendedtab'] = 'Recommended activities'; $string['aria:modulefavourite'] = 'Star {$a} activity'; $string['browsecourseadminindex'] = 'Browse the course administration with this index.'; $string['browsesettingindex'] = 'Browse settings with this index.'; +$string['communicationroomlink'] = 'Chat to course participants'; $string['completion_automatic:done'] = 'Done:'; $string['completion_automatic:failed'] = 'Failed:'; $string['completion_automatic:todo'] = 'To do:'; diff --git a/lib/classes/output/icon_system_fontawesome.php b/lib/classes/output/icon_system_fontawesome.php index 5fc08899f8d..314f16c8189 100644 --- a/lib/classes/output/icon_system_fontawesome.php +++ b/lib/classes/output/icon_system_fontawesome.php @@ -414,6 +414,7 @@ class icon_system_fontawesome extends icon_system_font { 'core:t/locktime' => 'fa-lock', 'core:t/markasread' => 'fa-check', 'core:t/messages' => 'fa-comments', + 'core:t/messages-o' => 'fa-comments-o', 'core:t/message' => 'fa-comment-o', 'core:t/more' => 'fa-caret-down', 'core:t/move' => 'fa-arrows-v', diff --git a/lib/moodlelib.php b/lib/moodlelib.php index a20699e381e..d7da9862deb 100644 --- a/lib/moodlelib.php +++ b/lib/moodlelib.php @@ -4145,6 +4145,19 @@ function delete_user(stdClass $user) { // Keep a copy of user context, we need it for event. $usercontext = context_user::instance($user->id); + // Remove user from communication rooms immediately. + if (core_communication\api::is_enabled()) { + foreach (enrol_get_users_courses($user->id) as $course) { + $communication = \core_communication\communication_processor::load_by_instance( + 'core_course', + 'coursecommunication', + $course->id + ); + $communication->get_room_user_provider()->remove_members_from_room([$user->id]); + $communication->delete_instance_user_mapping([$user->id]); + } + } + // Delete all grades - backup is kept in grade_grades_history table. grade_user_delete($user->id); @@ -5031,7 +5044,7 @@ function set_login_session_preferences() { * failed, but you have no way of knowing which. */ function delete_course($courseorid, $showfeedback = true) { - global $DB; + global $DB, $CFG; if (is_object($courseorid)) { $courseid = $courseorid->id; @@ -5071,6 +5084,26 @@ function delete_course($courseorid, $showfeedback = true) { // Delete the course and related context instance. context_helper::delete_instance(CONTEXT_COURSE, $courseid); + // Communication provider delete associated information. + $communication = \core_communication\api::load_by_instance( + 'core_course', + 'coursecommunication', + $course->id + ); + + // Update communication room membership of enrolled users. + require_once($CFG->libdir . '/enrollib.php'); + $courseusers = enrol_get_course_users($courseid); + $enrolledusers = []; + + foreach ($courseusers as $user) { + $enrolledusers[] = $user->id; + } + + $communication->remove_members_from_room($enrolledusers); + + $communication->delete_room(); + $DB->delete_records("course", array("id" => $courseid)); $DB->delete_records("course_format_options", array("courseid" => $courseid)); diff --git a/lib/outputrenderers.php b/lib/outputrenderers.php index 2bcc1b27619..b98830b5d3e 100644 --- a/lib/outputrenderers.php +++ b/lib/outputrenderers.php @@ -4285,6 +4285,54 @@ EOD; return !empty($this->services_support_link()) || !empty($this->page_doc_link()) || !empty($this->supportemail()); } + /** + * Helper function to decide whether to show the communication link or not. + * + * @return bool + */ + public function has_communication_links(): bool { + if (!core_communication\api::is_available()) { + return false; + } + return !empty($this->communication_link()); + } + + /** + * Returns the communication link, complete with html. + * + * @return string + */ + public function communication_link(): string { + + if (during_initial_install()) { + return ''; + } + + $link = $this->communication_url() ?? ''; + $commicon = $this->pix_icon('t/messages-o', '', 'moodle', ['class' => 'fa fa-comments']); + $newwindowicon = $this->pix_icon('i/externallink', get_string('opensinnewwindow'), 'moodle', ['class' => 'ml-1']); + $content = $commicon . get_string('communicationroomlink', 'course') . $newwindowicon; + $html = html_writer::tag('a', $content, ['target' => '_blank', 'href' => $link]); + + return !empty($link) ? $html : ''; + } + + /** + * Returns the communication url for a given instance if it exists. + * + * @return string + */ + public function communication_url(): string { + global $COURSE; + $url = ''; + if ($COURSE->id !== SITEID) { + $comm = \core_communication\api::load_by_instance('core_course', 'coursecommunication', $COURSE->id); + $url = $comm->get_communication_room_url(); + } + + return !empty($url) ? $url : ''; + } + /** * Returns the page heading menu. * diff --git a/theme/boost/scss/moodle/core.scss b/theme/boost/scss/moodle/core.scss index 11aa1366e75..581903a3a93 100644 --- a/theme/boost/scss/moodle/core.scss +++ b/theme/boost/scss/moodle/core.scss @@ -2309,10 +2309,22 @@ $footer-link-color: $bg-inverse-link-color !default; @include box-shadow($popover-box-shadow); } +.btn-footer-communication { + display: none; + position: fixed; + bottom: 5rem; + right: 2rem; + @include box-shadow($popover-box-shadow); +} + .hasstickyfooter .btn-footer-popover { bottom: calc(1rem + #{$stickyfooter-height}); } +.hasstickyfooter .btn-footer-communication { + bottom: calc(4rem + #{$stickyfooter-height}); +} + .popover.footer { .popover-body { padding: 0; @@ -2339,7 +2351,8 @@ $footer-link-color: $bg-inverse-link-color !default; .jsenabled #page-footer .footer-content-popover { display: none; } - .jsenabled .btn-footer-popover { + .jsenabled .btn-footer-popover, + .jsenabled .btn-footer-communication { display: block; z-index: $zindex-dropdown; } diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css index f37ffbaca16..190c1a2e7ec 100644 --- a/theme/boost/style/moodle.css +++ b/theme/boost/style/moodle.css @@ -25138,10 +25138,21 @@ h3.sectionname .inplaceeditable.inplaceeditingon .editinstructions { right: 2rem; } +.btn-footer-communication { + display: none; + position: fixed; + bottom: 5rem; + right: 2rem; +} + .hasstickyfooter .btn-footer-popover { bottom: calc(1rem + max(96px, 0.9375rem * 3)); } +.hasstickyfooter .btn-footer-communication { + bottom: calc(4rem + max(96px, 0.9375rem * 3)); +} + .popover.footer .popover-body { padding: 0; } @@ -25164,7 +25175,8 @@ h3.sectionname .inplaceeditable.inplaceeditingon .editinstructions { .jsenabled #page-footer .footer-content-popover { display: none; } - .jsenabled .btn-footer-popover { + .jsenabled .btn-footer-popover, + .jsenabled .btn-footer-communication { display: block; z-index: 1000; } diff --git a/theme/boost/templates/footer.mustache b/theme/boost/templates/footer.mustache index 5afe5891f24..1bd8d6af3d4 100644 --- a/theme/boost/templates/footer.mustache +++ b/theme/boost/templates/footer.mustache @@ -27,18 +27,31 @@ "has_popover_links": true, "services_support": "Services and support", "login_info": "You are logged in as cute kitten", - "moodle_release": "90210" + "moodle_release": "90210", + "has_communication_links": true, + "communication_url": "https://element:8081/#/room/#yourroom:synapse", + "communication_link": "Communication room" } } }}