diff --git a/scripts/langindex.json b/scripts/langindex.json index cc6776547..5a800908f 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -433,6 +433,22 @@ "addon.mod_assign_submission_file.pluginname": "assignsubmission_file", "addon.mod_assign_submission_onlinetext.pluginname": "assignsubmission_onlinetext", "addon.mod_assign_submission_onlinetext.wordlimitexceeded": "assignsubmission_onlinetext", + "addon.mod_bigbluebuttonbn.end_session_confirm": "bigbluebuttonbn", + "addon.mod_bigbluebuttonbn.end_session_confirm_title": "bigbluebuttonbn", + "addon.mod_bigbluebuttonbn.mod_form_field_closingtime": "bigbluebuttonbn", + "addon.mod_bigbluebuttonbn.mod_form_field_openingtime": "bigbluebuttonbn", + "addon.mod_bigbluebuttonbn.userlimitreached": "bigbluebuttonbn", + "addon.mod_bigbluebuttonbn.view_conference_action_end": "bigbluebuttonbn", + "addon.mod_bigbluebuttonbn.view_conference_action_join": "bigbluebuttonbn", + "addon.mod_bigbluebuttonbn.view_error_unable_join_student": "bigbluebuttonbn", + "addon.mod_bigbluebuttonbn.view_groups_selection_warning": "bigbluebuttonbn", + "addon.mod_bigbluebuttonbn.view_message_conference_in_progress": "bigbluebuttonbn", + "addon.mod_bigbluebuttonbn.view_message_conference_room_ready": "bigbluebuttonbn", + "addon.mod_bigbluebuttonbn.view_message_moderator": "bigbluebuttonbn", + "addon.mod_bigbluebuttonbn.view_message_moderators": "bigbluebuttonbn", + "addon.mod_bigbluebuttonbn.view_message_session_started_at": "bigbluebuttonbn", + "addon.mod_bigbluebuttonbn.view_message_viewer": "bigbluebuttonbn", + "addon.mod_bigbluebuttonbn.view_message_viewers": "bigbluebuttonbn", "addon.mod_book.errorchapter": "book", "addon.mod_book.modulenameplural": "book", "addon.mod_book.navnexttitle": "book", diff --git a/src/addons/mod/assign/classes/submissions-source.ts b/src/addons/mod/assign/classes/submissions-source.ts index fce121386..b0905d5fe 100644 --- a/src/addons/mod/assign/classes/submissions-source.ts +++ b/src/addons/mod/assign/classes/submissions-source.ts @@ -55,6 +55,7 @@ export class AddonModAssignSubmissionsSource extends CoreItemsManagerSource[] = [ + AddonModBBBService, +]; + +const routes: Routes = [ + { + path: ADDON_MOD_BBB_MAIN_MENU_PAGE_NAME, + loadChildren: () => import('./bigbluebuttonbn-lazy.module').then(m => m.AddonModBBBLazyModule), + }, +]; + +@NgModule({ + imports: [ + CoreMainMenuTabRoutingModule.forChild(routes), + AddonModBBBComponentsModule, + ], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + useValue: () => { + CoreCourseModuleDelegate.registerHandler(AddonModBBBModuleHandler.instance); + CoreContentLinksDelegate.registerHandler(AddonModBBBIndexLinkHandler.instance); + CoreContentLinksDelegate.registerHandler(AddonModBBBListLinkHandler.instance); + }, + }, + ], +}) +export class AddonModBBBModule {} diff --git a/src/addons/mod/bigbluebuttonbn/components/components.module.ts b/src/addons/mod/bigbluebuttonbn/components/components.module.ts new file mode 100644 index 000000000..d786b230c --- /dev/null +++ b/src/addons/mod/bigbluebuttonbn/components/components.module.ts @@ -0,0 +1,34 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { AddonModBBBIndexComponent } from './index/index'; +import { CoreSharedModule } from '@/core/shared.module'; +import { CoreCourseComponentsModule } from '@features/course/components/components.module'; + +@NgModule({ + declarations: [ + AddonModBBBIndexComponent, + ], + imports: [ + CoreSharedModule, + CoreCourseComponentsModule, + ], + providers: [ + ], + exports: [ + AddonModBBBIndexComponent, + ], +}) +export class AddonModBBBComponentsModule {} diff --git a/src/addons/mod/bigbluebuttonbn/components/index/index.html b/src/addons/mod/bigbluebuttonbn/components/index/index.html new file mode 100644 index 000000000..a5a2baeb8 --- /dev/null +++ b/src/addons/mod/bigbluebuttonbn/components/index/index.html @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ 'addon.mod_bigbluebuttonbn.view_groups_selection_warning' | translate }} + + + + + + {{'core.groupsseparate' | translate }} + {{'core.groupsvisible' | translate }} + + + + {{groupOpt.name}} + + + + + + + + +

{{ 'addon.mod_bigbluebuttonbn.mod_form_field_openingtime' | translate }}

+
+

{{ meetingInfo.openingtime * 1000 | coreFormatDate }}

+
+ + +

{{ 'addon.mod_bigbluebuttonbn.mod_form_field_closingtime' | translate }}

+
+

{{ meetingInfo.closingtime * 1000 | coreFormatDate }}

+
+ + + +

{{ 'addon.mod_bigbluebuttonbn.view_message_conference_room_ready' | translate }}

+
+
+ + + + +

{{ 'addon.mod_bigbluebuttonbn.view_message_conference_in_progress' | translate }}

+
+
+ + + +

{{ 'addon.mod_bigbluebuttonbn.view_message_session_started_at' | translate }}

+
+

{{ meetingInfo.startedat * 1000 | coreFormatDate: "strftimetime" }}

+
+ + + +

+ {{ 'addon.mod_bigbluebuttonbn.view_message_moderators' | translate }} +

+

+ {{ 'addon.mod_bigbluebuttonbn.view_message_moderator' | translate }} +

+
+

{{ meetingInfo.moderatorcount }}

+
+ + + +

+ {{ 'addon.mod_bigbluebuttonbn.view_message_viewers' | translate }} +

+

+ {{ 'addon.mod_bigbluebuttonbn.view_message_viewer' | translate }} +

+
+

{{ meetingInfo.participantcount }}

+
+
+ + + {{ 'addon.mod_bigbluebuttonbn.view_conference_action_join' | translate }} + + + + {{ 'addon.mod_bigbluebuttonbn.view_conference_action_end' | translate }} + + + + + + {{ meetingInfo.statusmessage }} + + +
+
+ + + diff --git a/src/addons/mod/bigbluebuttonbn/components/index/index.scss b/src/addons/mod/bigbluebuttonbn/components/index/index.scss new file mode 100644 index 000000000..66566a376 --- /dev/null +++ b/src/addons/mod/bigbluebuttonbn/components/index/index.scss @@ -0,0 +1,7 @@ +@import "~theme/globals"; + +:host { + ion-item > p[slot="end"] { + font-size: 14px; + } +} diff --git a/src/addons/mod/bigbluebuttonbn/components/index/index.ts b/src/addons/mod/bigbluebuttonbn/components/index/index.ts new file mode 100644 index 000000000..16ff70143 --- /dev/null +++ b/src/addons/mod/bigbluebuttonbn/components/index/index.ts @@ -0,0 +1,220 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit, Optional } from '@angular/core'; +import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component'; +import { CoreCourseContentsPage } from '@features/course/pages/contents/contents'; +import { CoreCourse } from '@features/course/services/course'; +import { IonContent } from '@ionic/angular'; +import { CoreGroupInfo, CoreGroups } from '@services/groups'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreUtils } from '@services/utils/utils'; +import { Translate } from '@singletons'; +import { AddonModBBB, AddonModBBBData, AddonModBBBMeetingInfoWSResponse, AddonModBBBService } from '../../services/bigbluebuttonbn'; + +/** + * Component that displays a Big Blue Button activity. + */ +@Component({ + selector: 'addon-mod-bbb-index', + templateUrl: 'index.html', + styleUrls: ['index.scss'], +}) +export class AddonModBBBIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit { + + component = AddonModBBBService.COMPONENT; + moduleName = 'bigbluebuttonbn'; + bbb?: AddonModBBBData; + groupInfo?: CoreGroupInfo; + groupId = 0; + meetingInfo?: AddonModBBBMeetingInfoWSResponse; + + constructor( + protected content?: IonContent, + @Optional() courseContentsPage?: CoreCourseContentsPage, + ) { + super('AddonModBBBIndexComponent', content, courseContentsPage); + } + + /** + * @inheritdoc + */ + async ngOnInit(): Promise { + super.ngOnInit(); + + await this.loadContent(); + + if (!this.bbb) { + return; + } + + try { + await AddonModBBB.logView(this.bbb.id, this.bbb.name); + + CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata); + } catch { + // Ignore errors. + } + } + + /** + * @inheritdoc + */ + protected async fetchContent(refresh: boolean = false): Promise { + try { + this.bbb = await AddonModBBB.getBBB(this.courseId, this.module.id); + + this.description = this.bbb.intro; + this.dataRetrieved.emit(this.bbb); + + this.groupInfo = await CoreGroups.getActivityGroupInfo(this.module.id, false); + + this.groupId = CoreGroups.validateGroupId(this.groupId, this.groupInfo); + + await this.fetchMeetingInfo(); + } finally { + this.fillContextMenu(refresh); + } + } + + /** + * Get meeting info. + * + * @return Promise resolved when done. + */ + async fetchMeetingInfo(): Promise { + if (!this.bbb) { + return; + } + + this.meetingInfo = await AddonModBBB.getMeetingInfo(this.bbb.id, this.groupId); + + if (this.meetingInfo.statusrunning && this.meetingInfo.userlimit > 0) { + const count = (this.meetingInfo.participantcount || 0) + (this.meetingInfo.moderatorcount || 0); + if (count === this.meetingInfo.userlimit) { + this.meetingInfo.statusmessage = Translate.instant('addon.mod_bigbluebuttonbn.userlimitreached'); + } + } + } + + /** + * Update meeting info. + * + * @return Promise resolved when done. + */ + async updateMeetingInfo(): Promise { + if (!this.bbb) { + return; + } + + this.loaded = false; + + try { + await AddonModBBB.invalidateAllGroupsMeetingInfo(this.bbb.id); + + await this.fetchMeetingInfo(); + } finally { + this.loaded = true; + } + } + + /** + * @inheritdoc + */ + protected async invalidateContent(): Promise { + const promises: Promise[] = []; + + promises.push(AddonModBBB.invalidateBBBs(this.courseId)); + promises.push(CoreGroups.invalidateActivityGroupInfo(this.module.id)); + + if (this.bbb) { + promises.push(AddonModBBB.invalidateAllGroupsMeetingInfo(this.bbb.id)); + } + + await Promise.all(promises); + } + + /** + * Group changed, reload some data. + * + * @return Promise resolved when done. + */ + async groupChanged(): Promise { + this.loaded = false; + + try { + await this.fetchMeetingInfo(); + } catch (error) { + CoreDomUtils.showErrorModal(error); + } finally { + this.loaded = true; + } + } + + /** + * Join the room. + * + * @return Promise resolved when done. + */ + async joinRoom(): Promise { + const modal = await CoreDomUtils.showModalLoading(); + + try { + const joinUrl = await AddonModBBB.getJoinUrl(this.module.id, this.groupId); + + CoreUtils.openInBrowser(joinUrl); + + this.updateMeetingInfo(); + } catch (error) { + CoreDomUtils.showErrorModal(error); + } finally { + modal.dismiss(); + } + } + + /** + * End the meeting. + * + * @return Promise resolved when done. + */ + async endMeeting(): Promise { + if (!this.bbb) { + return; + } + + try { + await CoreDomUtils.showConfirm( + Translate.instant('addon.mod_bigbluebuttonbn.end_session_confirm'), + Translate.instant('addon.mod_bigbluebuttonbn.end_session_confirm_title'), + Translate.instant('core.yes'), + ); + } catch { + // User canceled. + return; + } + + const modal = await CoreDomUtils.showModalLoading(); + + try { + await AddonModBBB.endMeeting(this.bbb.id, this.groupId); + + this.updateMeetingInfo(); + } catch (error) { + CoreDomUtils.showErrorModal(error); + } finally { + modal.dismiss(); + } + } + +} diff --git a/src/addons/mod/bigbluebuttonbn/lang.json b/src/addons/mod/bigbluebuttonbn/lang.json new file mode 100644 index 000000000..dba17cf44 --- /dev/null +++ b/src/addons/mod/bigbluebuttonbn/lang.json @@ -0,0 +1,18 @@ +{ + "end_session_confirm": "Are you sure you want to end the virtual classroom session?", + "end_session_confirm_title": "Really end session?", + "mod_form_field_closingtime": "Join closed", + "mod_form_field_openingtime": "Join open", + "userlimitreached": "The number of users allowed in a meeting has been reached.", + "view_conference_action_end": "End session", + "view_conference_action_join": "Join session", + "view_error_unable_join_student": "Unable to connect to the BigBlueButton server. Please contact your Teacher or the Administrator.", + "view_groups_selection_warning": "There is a conference room for each group and you have access to more than one. Be sure to select the correct one.", + "view_message_conference_in_progress": "This conference is in progress.", + "view_message_conference_room_ready": "This conference room is ready. You can join the session now.", + "view_message_moderator": "moderator", + "view_message_moderators": "moderators", + "view_message_session_started_at": "This session started at", + "view_message_viewer": "viewer", + "view_message_viewers": "viewers" +} diff --git a/src/addons/mod/bigbluebuttonbn/pages/index/index.html b/src/addons/mod/bigbluebuttonbn/pages/index/index.html new file mode 100644 index 000000000..5d8e5197e --- /dev/null +++ b/src/addons/mod/bigbluebuttonbn/pages/index/index.html @@ -0,0 +1,23 @@ + + + + + + +

+ + +

+
+ + + +
+
+ + + + + + + diff --git a/src/addons/mod/bigbluebuttonbn/pages/index/index.ts b/src/addons/mod/bigbluebuttonbn/pages/index/index.ts new file mode 100644 index 000000000..3b5c6ddf7 --- /dev/null +++ b/src/addons/mod/bigbluebuttonbn/pages/index/index.ts @@ -0,0 +1,30 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, ViewChild } from '@angular/core'; +import { CoreCourseModuleMainActivityPage } from '@features/course/classes/main-activity-page'; +import { AddonModBBBIndexComponent } from '../../components/index/index'; + +/** + * Page that displays a Big Blue Button activity. + */ +@Component({ + selector: 'page-addon-mod-bbb-index', + templateUrl: 'index.html', +}) +export class AddonModBBBIndexPage extends CoreCourseModuleMainActivityPage { + + @ViewChild(AddonModBBBIndexComponent) activityComponent?: AddonModBBBIndexComponent; + +} diff --git a/src/addons/mod/bigbluebuttonbn/services/bigbluebuttonbn.ts b/src/addons/mod/bigbluebuttonbn/services/bigbluebuttonbn.ts new file mode 100644 index 000000000..4889b310d --- /dev/null +++ b/src/addons/mod/bigbluebuttonbn/services/bigbluebuttonbn.ts @@ -0,0 +1,382 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { CoreError } from '@classes/errors/error'; +import { CoreWSError } from '@classes/errors/wserror'; +import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreCourseCommonModWSOptions } from '@features/course/services/course'; +import { CoreCourseLogHelper } from '@features/course/services/log-helper'; +import { CoreSites, CoreSitesCommonWSOptions } from '@services/sites'; +import { CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws'; +import { makeSingleton, Translate } from '@singletons'; + +const ROOT_CACHE_KEY = 'AddonModBBB:'; + +/** + * Service that provides some features for Big Blue Button activity. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModBBBService { + + static readonly COMPONENT = 'mmaModBigBlueButtonBN'; + + /** + * End a meeting. + * + * @param id BBB ID. + * @param groupId Group ID, 0 means that the function will determine the user group. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when done. + */ + async endMeeting( + id: number, + groupId: number = 0, + siteId?: string, + ): Promise { + const site = await CoreSites.getSite(siteId); + + const params: AddonModBBBEndMeetingWSParams = { + bigbluebuttonbnid: id, + groupid: groupId, + }; + + await site.write('mod_bigbluebuttonbn_end_meeting', params); + } + + /** + * Get a BBB activity. + * + * @param courseId Course ID. + * @param cmId Course module ID. + * @param options Other options. + * @return Promise resolved when the activity is retrieved. + */ + async getBBB(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + const site = await CoreSites.getSite(options.siteId); + + const params: AddonModBBBGetBigBlueButtonBNsByCoursesWSParams = { + courseids: [courseId], + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getBBBsCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModBBBService.COMPONENT, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + const response = await site.read( + 'mod_bigbluebuttonbn_get_bigbluebuttonbns_by_courses', + params, + preSets, + ); + + const bbb = response.bigbluebuttonbns.find((bbb) => bbb.coursemodule == cmId); + if (bbb) { + return bbb; + } + + throw new CoreError(Translate.instant('core.course.modulenotfound')); + } + + /** + * Get cache key for get BBB WS call. + * + * @param courseId Course ID. + * @return Cache key. + */ + protected getBBBsCacheKey(courseId: number): string { + return ROOT_CACHE_KEY + 'bbb:' + courseId; + } + + /** + * Get join URL for a BBB activity. + * + * @param cmId Course module ID. + * @param groupId Group ID, 0 means that the function will determine the user group. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with the list of messages. + */ + async getJoinUrl( + cmId: number, + groupId: number = 0, + siteId?: string, + ): Promise { + const site = await CoreSites.getSite(siteId); + + const params: AddonModBBBGetJoinUrlWSParams = { + cmid: cmId, + groupid: groupId, + }; + + // Don't use cache. + const response = await site.write( + 'mod_bigbluebuttonbn_get_join_url', + params, + ); + + if (response.warnings?.length) { + throw new CoreWSError(response.warnings[0]); + } + + if (!response.join_url) { + // Shouldn't happen, if there are no warning the app should always receive the URL. + throw new CoreError( + Translate.instant('addon.mod_bigbluebuttonbn.view_error_unable_join_studentview_error_unable_join_student'), + ); + } + + return response.join_url; + } + + /** + * Get meeting info for a BBB activity. + * + * @param id BBB ID. + * @param groupId Group ID, 0 means that the function will determine the user group. + * @param options Other options. + * @return Promise resolved with the list of messages. + */ + async getMeetingInfo( + id: number, + groupId: number = 0, + options: CoreCourseCommonModWSOptions = {}, + ): Promise { + const site = await CoreSites.getSite(options.siteId); + + const params: AddonModBBBMeetingInfoWSParams = { + bigbluebuttonbnid: id, + groupid: groupId, + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getMeetingInfoCacheKey(id, groupId), + component: AddonModBBBService.COMPONENT, + componentId: options.cmId, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + return await site.read( + 'mod_bigbluebuttonbn_meeting_info', + params, + preSets, + ); + } + + /** + * Get cache key for meeting info WS call. + * + * @param id BBB ID. + * @param groupId Group ID, 0 means that the function will determine the user group. + * @return Cache key. + */ + protected getMeetingInfoCacheKey(id: number, groupId: number = 0): string { + return this.getMeetingInfoCacheKeyPrefix(id) + groupId; + } + + /** + * Get cache key prefix for meeting info WS call. + * + * @param id BBB ID. + * @return Cache key prefix. + */ + protected getMeetingInfoCacheKeyPrefix(id: number): string { + return ROOT_CACHE_KEY + 'meetingInfo:' + id + ':'; + } + + /** + * Report a BBB as being viewed. + * + * @param id BBB instance ID. + * @param name Name of the BBB. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the WS call is successful. + */ + async logView(id: number, name?: string, siteId?: string): Promise { + const params: AddonModBBBViewBigBlueButtonBNWSParams = { + bigbluebuttonbnid: id, + }; + + await CoreCourseLogHelper.logSingle( + 'mod_bigbluebuttonbn_view_bigbluebuttonbn', + params, + AddonModBBBService.COMPONENT, + id, + name, + 'bigbluebuttonbn', + {}, + siteId, + ); + } + + /** + * Invalidate BBBs. + * + * @param courseId Course ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateBBBs(courseId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getBBBsCacheKey(courseId)); + } + + /** + * Invalidate meeting info for a certain group. + * + * @param id BBB ID. + * @param groupId Group ID, 0 means that the function will determine the user group. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateMeetingInfo(id: number, groupId: number = 0, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getMeetingInfoCacheKey(id, groupId)); + } + + /** + * Invalidate meeting info for all groups. + * + * @param id BBB ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateAllGroupsMeetingInfo(id: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + await site.invalidateWsCacheForKeyStartingWith(this.getMeetingInfoCacheKeyPrefix(id)); + } + + /** + * Returns whether or not the BBB plugin is enabled for a certain site. + * + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with true if enabled, resolved with false or rejected otherwise. + * @since 4.0, but the WebServices were backported to the plugin so it can be supported in older versions. + */ + async isPluginEnabled(siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + return site.wsAvailable('mod_bigbluebuttonbn_meeting_info'); + } + +} + +export const AddonModBBB = makeSingleton(AddonModBBBService); + +/** + * Params of mod_bigbluebuttonbn_get_bigbluebuttonbns_by_courses WS. + */ +export type AddonModBBBGetBigBlueButtonBNsByCoursesWSParams = { + courseids?: number[]; // Array of course ids. +}; + +/** + * Data returned by mod_bigbluebuttonbn_get_bigbluebuttonbns_by_courses WS. + */ +export type AddonModBBBGetBigBlueButtonBNsByCoursesWSResponse = { + bigbluebuttonbns: AddonModBBBData[]; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * BBB data returned by mod_bigbluebuttonbn_get_bigbluebuttonbns_by_courses. + */ +export type AddonModBBBData = { + id: number; // Module id. + coursemodule: number; // Course module id. + course: number; // Course id. + name: string; // Name. + intro: string; // Description. + meetingid: string; // Meeting id. + introformat?: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + introfiles: CoreWSExternalFile[]; + timemodified: number; // Last time the instance was modified. + section: number; // Course section id. + visible: number; // Module visibility. + groupmode: number; // Group mode. + groupingid: number; // Grouping id. +}; + +/** + * Params of mod_bigbluebuttonbn_meeting_info WS. + */ +export type AddonModBBBMeetingInfoWSParams = { + bigbluebuttonbnid: number; // Bigbluebuttonbn instance id. + groupid?: number; // Bigbluebuttonbn group id. + updatecache?: boolean; // Update cache ?. +}; + +/** + * Data returned by mod_bigbluebuttonbn_meeting_info WS. + */ +export type AddonModBBBMeetingInfoWSResponse = { + cmid: number; // CM id. + userlimit: number; // User limit. + bigbluebuttonbnid: string; // Bigbluebuttonbn instance id. + meetingid: string; // Meeting id. + openingtime?: number; // Opening time. + closingtime?: number; // Closing time. + statusrunning?: boolean; // Status running. + statusclosed?: boolean; // Status closed. + statusopen?: boolean; // Status open. + statusmessage?: string; // Status message. + startedat?: number; // Started at. + moderatorcount?: number; // Moderator count. + participantcount?: number; // Participant count. + moderatorplural?: boolean; // Several moderators ?. + participantplural?: boolean; // Several participants ?. + canjoin: boolean; // Can join. + ismoderator: boolean; // Is moderator. + presentations: { + url: string; // Presentation URL. + iconname: string; // Icon name. + icondesc: string; // Icon text. + name: string; // Presentation name. + }[]; + joinurl: string; // Join URL. +}; + +/** + * Params of mod_bigbluebuttonbn_get_join_url WS. + */ +export type AddonModBBBGetJoinUrlWSParams = { + cmid: number; // Course module id. + groupid?: number; // Bigbluebuttonbn group id. +}; + +/** + * Data returned by mod_bigbluebuttonbn_get_join_url WS. + */ +export type AddonModBBBGetJoinUrlWSResponse = { + // eslint-disable-next-line @typescript-eslint/naming-convention + join_url?: string; // Can join session. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Params of mod_bigbluebuttonbn_view_bigbluebuttonbn WS. + */ +export type AddonModBBBViewBigBlueButtonBNWSParams = { + bigbluebuttonbnid: number; // Bigbluebuttonbn instance id. +}; + +/** + * Params of mod_bigbluebuttonbn_end_meeting WS. + */ +export type AddonModBBBEndMeetingWSParams = { + bigbluebuttonbnid: number; // Bigbluebuttonbn instance id. + groupid?: number; // Bigbluebuttonbn group id. +}; diff --git a/src/addons/mod/bigbluebuttonbn/services/handlers/index-link.ts b/src/addons/mod/bigbluebuttonbn/services/handlers/index-link.ts new file mode 100644 index 000000000..2c89a1398 --- /dev/null +++ b/src/addons/mod/bigbluebuttonbn/services/handlers/index-link.ts @@ -0,0 +1,33 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { CoreContentLinksModuleIndexHandler } from '@features/contentlinks/classes/module-index-handler'; +import { makeSingleton } from '@singletons'; + +/** + * Handler to treat links to Big Blue Button activity. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModBBBIndexLinkHandlerService extends CoreContentLinksModuleIndexHandler { + + name = 'AddonModBBBIndexLinkHandlerService'; + + constructor() { + super('AddonModBBB', 'bigbluebuttonbn', 'bn'); + } + +} + +export const AddonModBBBIndexLinkHandler = makeSingleton(AddonModBBBIndexLinkHandlerService); diff --git a/src/addons/mod/bigbluebuttonbn/services/handlers/list-link.ts b/src/addons/mod/bigbluebuttonbn/services/handlers/list-link.ts new file mode 100644 index 000000000..3933fc7ea --- /dev/null +++ b/src/addons/mod/bigbluebuttonbn/services/handlers/list-link.ts @@ -0,0 +1,33 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { CoreContentLinksModuleListHandler } from '@features/contentlinks/classes/module-list-handler'; +import { makeSingleton } from '@singletons'; + +/** + * Handler to treat links to Big Blue Button list page. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModBBBListLinkHandlerService extends CoreContentLinksModuleListHandler { + + name = 'AddonModBBBListLinkHandler'; + + constructor() { + super('AddonModBBB', 'bigbluebuttonbn'); + } + +} + +export const AddonModBBBListLinkHandler = makeSingleton(AddonModBBBListLinkHandlerService); diff --git a/src/addons/mod/bigbluebuttonbn/services/handlers/module.ts b/src/addons/mod/bigbluebuttonbn/services/handlers/module.ts new file mode 100644 index 000000000..85956b4ed --- /dev/null +++ b/src/addons/mod/bigbluebuttonbn/services/handlers/module.ts @@ -0,0 +1,104 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { CoreConstants } from '@/core/constants'; +import { Injectable, Type } from '@angular/core'; +import { CoreModuleHandlerBase } from '@features/course/classes/module-base-handler'; +import { CoreCourseModule } from '@features/course/services/course-helper'; +import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate'; +import { CoreSitePluginsModuleHandler } from '@features/siteplugins/classes/handlers/module-handler'; +import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins'; +import { makeSingleton } from '@singletons'; +import { AddonModBBBIndexComponent } from '../../components/index'; +import { AddonModBBB } from '../bigbluebuttonbn'; + +export const ADDON_MOD_BBB_MAIN_MENU_PAGE_NAME = 'mod_bigbluebuttonbn'; + +/** + * Handler to support Big Blue Button activities. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModBBBModuleHandlerService extends CoreModuleHandlerBase implements CoreCourseModuleHandler { + + name = 'AddonModBBB'; + modName = 'bigbluebuttonbn'; + protected pageName = ADDON_MOD_BBB_MAIN_MENU_PAGE_NAME; + protected sitePluginHandler?: CoreSitePluginsModuleHandler; + + supportedFeatures = { + [CoreConstants.FEATURE_GROUPS]: true, + [CoreConstants.FEATURE_GROUPINGS]: true, + [CoreConstants.FEATURE_MOD_INTRO]: true, + [CoreConstants.FEATURE_COMPLETION_TRACKS_VIEWS]: true, + [CoreConstants.FEATURE_GRADE_HAS_GRADE]: false, + [CoreConstants.FEATURE_GRADE_OUTCOMES]: true, + [CoreConstants.FEATURE_BACKUP_MOODLE2]: true, + [CoreConstants.FEATURE_SHOW_DESCRIPTION]: true, + }; + + /** + * @inheritdoc + */ + async isEnabled(): Promise { + const enabled = await AddonModBBB.isPluginEnabled(); + + if (enabled) { + delete this.sitePluginHandler; + this.name = 'AddonModBBB'; + + return true; + } + + // Native support not available in this site. Check if it's supported by site plugin. + this.sitePluginHandler = CoreSitePlugins.getModuleHandlerInstance(this.modName); + // Change the handler name to be able to retrieve the plugin data in component. + this.name = this.sitePluginHandler?.name || this.name; + + return !!this.sitePluginHandler; + } + + /** + * @inheritdoc + */ + async getData( + module: CoreCourseModule, + courseId: number, + sectionId?: number, + forCoursePage?: boolean, + ): Promise { + if (this.sitePluginHandler) { + return this.sitePluginHandler.getData(module, courseId, sectionId, forCoursePage); + } + + const data = await super.getData(module, courseId, sectionId, forCoursePage); + + data.showDownloadButton = false; + + return data; + } + + /** + * @inheritdoc + */ + async getMainComponent(): Promise> { + if (this.sitePluginHandler) { + return this.sitePluginHandler.getMainComponent(); + } + + return AddonModBBBIndexComponent; + } + +} + +export const AddonModBBBModuleHandler = makeSingleton(AddonModBBBModuleHandlerService); diff --git a/src/addons/mod/mod.module.ts b/src/addons/mod/mod.module.ts index 1c3e3fe0e..f5f039c55 100644 --- a/src/addons/mod/mod.module.ts +++ b/src/addons/mod/mod.module.ts @@ -15,6 +15,7 @@ import { NgModule } from '@angular/core'; import { AddonModAssignModule } from './assign/assign.module'; +import { AddonModBBBModule } from './bigbluebuttonbn/bigbluebuttonbn.module'; import { AddonModBookModule } from './book/book.module'; import { AddonModChatModule } from './chat/chat.module'; import { AddonModChoiceModule } from './choice/choice.module'; @@ -40,6 +41,7 @@ import { AddonModWorkshopModule } from './workshop/workshop.module'; @NgModule({ imports: [ AddonModAssignModule, + AddonModBBBModule, AddonModBookModule, AddonModChatModule, AddonModChoiceModule, diff --git a/src/addons/mod/workshop/components/index/index.ts b/src/addons/mod/workshop/components/index/index.ts index ef11aedf7..91c8bba44 100644 --- a/src/addons/mod/workshop/components/index/index.ts +++ b/src/addons/mod/workshop/components/index/index.ts @@ -82,6 +82,7 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity separateGroups: false, visibleGroups: false, defaultGroupId: 0, + canAccessAllGroups: false, }; canSubmit = false; diff --git a/src/assets/img/mod/bigbluebuttonbn.svg b/src/assets/img/mod/bigbluebuttonbn.svg new file mode 100644 index 000000000..7b92ab21b --- /dev/null +++ b/src/assets/img/mod/bigbluebuttonbn.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/core/features/course/services/course.ts b/src/core/features/course/services/course.ts index acea7dd52..6d5b8ea86 100644 --- a/src/core/features/course/services/course.ts +++ b/src/core/features/course/services/course.ts @@ -87,7 +87,7 @@ export class CoreCourseProvider { readonly CORE_MODULES = [ 'assign', 'assignment', 'book', 'chat', 'choice', 'data', 'database', 'date', 'external-tool', 'feedback', 'file', 'folder', 'forum', 'glossary', 'ims', 'imscp', 'label', 'lesson', 'lti', 'page', 'quiz', - 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop', 'h5pactivity', + 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop', 'h5pactivity', 'bigbluebuttonbn', ]; protected logger: CoreLogger; diff --git a/src/core/features/siteplugins/components/module-index/module-index.ts b/src/core/features/siteplugins/components/module-index/module-index.ts index 5e83cbc9d..499458d36 100644 --- a/src/core/features/siteplugins/components/module-index/module-index.ts +++ b/src/core/features/siteplugins/components/module-index/module-index.ts @@ -161,7 +161,11 @@ export class CoreSitePluginsModuleIndexComponent implements OnInit, OnDestroy, C * Expand the description. */ expandDescription(): void { - CoreTextUtils.viewText(Translate.instant('core.description'), this.description!, { + if (!this.description) { + return; + } + + CoreTextUtils.viewText(Translate.instant('core.description'), this.description, { component: this.component, componentId: this.module.id, filter: true, diff --git a/src/core/features/siteplugins/services/siteplugins-helper.ts b/src/core/features/siteplugins/services/siteplugins-helper.ts index 3ce97bba9..2d674dac2 100644 --- a/src/core/features/siteplugins/services/siteplugins-helper.ts +++ b/src/core/features/siteplugins/services/siteplugins-helper.ts @@ -205,7 +205,7 @@ export class CoreSitePluginsHelperProvider { undefined, undefined, undefined, - handlerSchema.styles!.version, + handlerSchema.styles?.version, ); // File is downloaded, get the contents. @@ -378,8 +378,9 @@ export class CoreSitePluginsHelperProvider { if (plugin.parsedHandlers) { // Register all the handlers. - await CoreUtils.allPromises(Object.keys(plugin.parsedHandlers).map(async (name) => { - await this.registerHandler(plugin, name, plugin.parsedHandlers![name]); + const parsedHandlers = plugin.parsedHandlers; + await CoreUtils.allPromises(Object.keys(parsedHandlers).map(async (name) => { + await this.registerHandler(plugin, name, parsedHandlers[name]); })); } } @@ -891,6 +892,7 @@ export class CoreSitePluginsHelperProvider { const moduleHandler = new CoreSitePluginsModuleHandler(uniqueName, modName, plugin, handlerSchema, initResult); CoreCourseModuleDelegate.registerHandler(moduleHandler); + CoreSitePlugins.setModuleHandlerInstance(modName, moduleHandler); if (handlerSchema.offlinefunctions && Object.keys(handlerSchema.offlinefunctions).length) { // Register the prefetch handler. diff --git a/src/core/features/siteplugins/services/siteplugins.ts b/src/core/features/siteplugins/services/siteplugins.ts index 6d7fdb702..c03ffbe91 100644 --- a/src/core/features/siteplugins/services/siteplugins.ts +++ b/src/core/features/siteplugins/services/siteplugins.ts @@ -28,6 +28,7 @@ import { CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws'; import { makeSingleton } from '@singletons'; import { CoreEvents } from '@singletons/events'; import { CoreLogger } from '@singletons/logger'; +import { CoreSitePluginsModuleHandler } from '../classes/handlers/module-handler'; const ROOT_CACHE_KEY = 'CoreSitePlugins:'; @@ -44,6 +45,8 @@ export class CoreSitePluginsProvider { protected sitePlugins: {[name: string]: CoreSitePluginsHandler} = {}; // Site plugins registered. protected sitePluginPromises: {[name: string]: Promise} = {}; // Promises of loading plugins. protected fetchPluginsDeferred: PromiseDefer; + protected moduleHandlerInstances: Record = {}; + hasSitePluginsLoaded = false; sitePluginsFinishedLoading = false; @@ -262,7 +265,7 @@ export class CoreSitePluginsProvider { switch (paramName) { case 'courseids': // The WS needs the list of course IDs. Create the list. - return [courseId!]; + return [courseId || 0]; case component + 'id': // The WS needs the instance id. @@ -540,10 +543,13 @@ export class CoreSitePluginsProvider { return; } + const siteInstance = site; + const offlineFunctions = handlerSchema.offlinefunctions; + await Promise.all(Object.keys(handlerSchema.offlinefunctions).map(async(method) => { - if (site!.wsAvailable(method)) { + if (siteInstance.wsAvailable(method)) { // The method is a WS. - const paramsList = handlerSchema.offlinefunctions![method]; + const paramsList = offlineFunctions[method]; const cacheKey = this.getCallWSCacheKey(method, args); let params: Record = {}; @@ -584,7 +590,7 @@ export class CoreSitePluginsProvider { // Prefetch the files in the content. if (result.files.length) { await CoreFilepool.downloadOrPrefetchFiles( - site!.getId(), + siteInstance.getId(), result.files, !!prefetch, false, @@ -657,6 +663,26 @@ export class CoreSitePluginsProvider { return this.fetchPluginsDeferred.promise; } + /** + * Get a module hander instance, if present. + * + * @param modName Mod name without "mod_". + * @return Handler instance, undefined if not found. + */ + getModuleHandlerInstance(modName: string): CoreSitePluginsModuleHandler | undefined { + return this.moduleHandlerInstances[modName]; + } + + /** + * Set a module hander instance. + * + * @param modName Mod name. + * @param handler Handler instance. + */ + setModuleHandlerInstance(modName: string, handler: CoreSitePluginsModuleHandler): void { + this.moduleHandlerInstances[modName] = handler; + } + } export const CoreSitePlugins = makeSingleton(CoreSitePluginsProvider); diff --git a/src/core/services/groups.ts b/src/core/services/groups.ts index 7aad3f30c..b0d63b803 100644 --- a/src/core/services/groups.ts +++ b/src/core/services/groups.ts @@ -151,6 +151,7 @@ export class CoreGroupsProvider { const groupInfo: CoreGroupInfo = { groups: [], defaultGroupId: 0, + canAccessAllGroups: false, }; const groupMode = await this.getActivityGroupMode(cmId, siteId, ignoreCache); @@ -161,6 +162,8 @@ export class CoreGroupsProvider { let result: CoreGroupGetActivityAllowedGroupsWSResponse; if (groupInfo.separateGroups || groupInfo.visibleGroups) { result = await this.getActivityAllowedGroups(cmId, userId, siteId, ignoreCache); + + groupInfo.canAccessAllGroups = !!result.canaccessallgroups; } else { result = { groups: [], @@ -471,6 +474,11 @@ export type CoreGroupInfo = { * The group ID to use by default. If all participants is visible, 0 will be used. First group ID otherwise. */ defaultGroupId: number; + + /** + * Whether the user has the capability to access all groups in the context. + */ + canAccessAllGroups: boolean; }; /**