From 6e1677a757844e30b80e443922a099b7049f8095 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 2 Nov 2022 15:55:56 +0100 Subject: [PATCH] MOBILE-4100 bbb: Display recordings --- scripts/langindex.json | 4 + .../components/index/index.html | 40 +++++ .../components/index/index.scss | 27 +++- .../bigbluebuttonbn/components/index/index.ts | 121 +++++++++++++++ src/addons/mod/bigbluebuttonbn/lang.json | 6 +- .../services/bigbluebuttonbn.ts | 138 ++++++++++++++++++ 6 files changed, 333 insertions(+), 3 deletions(-) diff --git a/scripts/langindex.json b/scripts/langindex.json index 1ef97ed16..90313fa3d 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -458,10 +458,14 @@ "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_norecordings": "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_bigbluebuttonbn.view_nojoin": "bigbluebuttonbn", + "addon.mod_bigbluebuttonbn.view_recording_format_presentation": "bigbluebuttonbn", + "addon.mod_bigbluebuttonbn.view_recording_list_action_play": "bigbluebuttonbn", + "addon.mod_bigbluebuttonbn.view_section_title_recordings": "bigbluebuttonbn", "addon.mod_book.errorchapter": "book", "addon.mod_book.modulenameplural": "book", "addon.mod_book.navnexttitle": "book", diff --git a/src/addons/mod/bigbluebuttonbn/components/index/index.html b/src/addons/mod/bigbluebuttonbn/components/index/index.html index f51197f46..899bee333 100644 --- a/src/addons/mod/bigbluebuttonbn/components/index/index.html +++ b/src/addons/mod/bigbluebuttonbn/components/index/index.html @@ -83,6 +83,46 @@ + + + +

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

+
+
+ + + + +

{{ recording.type }}

+

{{ recording.name }}

+
+ + + +
+
+ + +

{{ data.label }}

+

+ +

+

{{ data.value }}

+
+
+
+
+ + + +
+
diff --git a/src/addons/mod/bigbluebuttonbn/components/index/index.scss b/src/addons/mod/bigbluebuttonbn/components/index/index.scss index c6f581f99..dd64226f6 100644 --- a/src/addons/mod/bigbluebuttonbn/components/index/index.scss +++ b/src/addons/mod/bigbluebuttonbn/components/index/index.scss @@ -1,3 +1,26 @@ -ion-item > p[slot="end"] { - font-size: var(--text-size); +:host { + --recording-details-background: var(--gray-100); + --recording-details-border: var(--gray-500); + + ion-item > p[slot="end"] { + font-size: var(--text-size); + } + + core-empty-box ::ng-deep p { + font-size: 100%; + } + + .addon-mod_bbb-recording-details { + border-top: 2px solid var(--recording-details-border); + border-bottom: 2px solid var(--recording-details-border); + + ion-item { + --background: var(--recording-details-background); + } + } +} + +:host-context(html.dark) { + --recording-details-background: var(--gray-800); + --recording-details-border: var(--gray-500); } diff --git a/src/addons/mod/bigbluebuttonbn/components/index/index.ts b/src/addons/mod/bigbluebuttonbn/components/index/index.ts index 95ad3c6c4..3b7d126cf 100644 --- a/src/addons/mod/bigbluebuttonbn/components/index/index.ts +++ b/src/addons/mod/bigbluebuttonbn/components/index/index.ts @@ -19,7 +19,10 @@ import { CoreCourseContentsPage } from '@features/course/pages/contents/contents import { IonContent } from '@ionic/angular'; import { CoreApp } from '@services/app'; import { CoreGroupInfo, CoreGroups } from '@services/groups'; +import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; +import { CoreTextUtils } from '@services/utils/text'; +import { CoreTimeUtils } from '@services/utils/time'; import { CoreUtils } from '@services/utils/utils'; import { Translate } from '@singletons'; import { AddonModBBB, AddonModBBBData, AddonModBBBMeetingInfo, AddonModBBBService } from '../../services/bigbluebuttonbn'; @@ -40,6 +43,7 @@ export class AddonModBBBIndexComponent extends CoreCourseModuleMainActivityCompo groupInfo?: CoreGroupInfo; groupId = 0; meetingInfo?: AddonModBBBMeetingInfo; + recordings?: RecordingData[]; constructor( protected content?: IonContent, @@ -61,6 +65,10 @@ export class AddonModBBBIndexComponent extends CoreCourseModuleMainActivityCompo return !!this.meetingInfo && (!this.meetingInfo.features || this.meetingInfo.features.showroom); } + get showRecordings(): boolean { + return !!this.meetingInfo && (!this.meetingInfo.features || this.meetingInfo.features.showrecordings); + } + /** * @inheritdoc */ @@ -79,6 +87,8 @@ export class AddonModBBBIndexComponent extends CoreCourseModuleMainActivityCompo } await this.fetchMeetingInfo(); + + await this.fetchRecordings(); } /** @@ -113,6 +123,72 @@ export class AddonModBBBIndexComponent extends CoreCourseModuleMainActivityCompo } } + /** + * Get recordings. + * + * @return Promise resolved when done. + */ + async fetchRecordings(): Promise { + if (!this.bbb || !this.showRecordings) { + return; + } + + const recordingsTable = await AddonModBBB.getRecordings(this.bbb.id, this.groupId, { + cmId: this.module.id, + }); + const columns = CoreUtils.arrayToObject(recordingsTable.columns, 'key'); + + this.recordings = recordingsTable.parsedData.map(recordingData => { + const playbackEl = CoreDomUtils.convertToElement(String(recordingData.playback)); + const playbackAnchor = playbackEl.querySelector('a'); + const details: RecordingDetailData[] = []; + + Object.entries(recordingData).forEach(([key, value]) => { + const columnData = columns[key]; + if (!columnData || value === '' || key === 'actionbar') { + return; + } + + if (columnData.formatter === 'customDate' && !isNaN(Number(value))) { + value = CoreTimeUtils.userDate(Number(value), 'core.strftimedaydate'); + } else if (columnData.allowHTML && typeof value === 'string') { + // If the HTML is empty, don't display it. + const valueElement = CoreDomUtils.convertToElement(value); + if (!valueElement.querySelector('img') && (valueElement.textContent ?? '').trim() === '') { + return; + } + + if (key === 'playback') { + // Remove HTML, we're only interested in the text. + value = (valueElement.textContent ?? '').trim(); + } else { + // Treat "quick edit" buttons, they aren't supported in the app. + const quickEditLink = valueElement.querySelector('.quickeditlink'); + if (quickEditLink) { + // The first span in quick edit link contains the actual HTML, use it. + value = (quickEditLink.querySelector('span')?.innerHTML ?? '').trim(); + } + } + } + + details.push({ + label: columnData.label, + value: String(value), + allowHTML: !!columnData.allowHTML, + }); + }); + + return { + type: playbackAnchor?.innerText ?? + Translate.instant('addon.mod_bigbluebuttonbn.view_recording_format_presentation'), + name: CoreTextUtils.cleanTags(String(recordingData.recording), true), + url: playbackAnchor?.href ?? '', + details, + expanded: false, + }; + }); + } + /** * @inheritdoc */ @@ -157,6 +233,7 @@ export class AddonModBBBIndexComponent extends CoreCourseModuleMainActivityCompo if (this.bbb) { promises.push(AddonModBBB.invalidateAllGroupsMeetingInfo(this.bbb.id)); + promises.push(AddonModBBB.invalidateAllGroupsRecordings(this.bbb.id)); } await Promise.all(promises); @@ -172,6 +249,8 @@ export class AddonModBBBIndexComponent extends CoreCourseModuleMainActivityCompo try { await this.fetchMeetingInfo(); + + await this.fetchRecordings(); } catch (error) { CoreDomUtils.showErrorModal(error); } finally { @@ -239,4 +318,46 @@ export class AddonModBBBIndexComponent extends CoreCourseModuleMainActivityCompo } } + /** + * Toogle the visibility of a recording (expand/collapse). + * + * @param recording Recording. + */ + toggle(recording: RecordingData): void { + recording.expanded = !recording.expanded; + } + + /** + * Play a recording. + * + * @param event Click event. + * @param recording Recording. + */ + playRecording(event: MouseEvent, recording: RecordingData): void { + event.preventDefault(); + event.stopPropagation(); + + CoreSites.getCurrentSite()?.openInBrowserWithAutoLogin(recording.url); + } + } + +/** + * Recording data. + */ +type RecordingData = { + type: string; + name: string; + url: string; + expanded: boolean; + details: RecordingDetailData[]; +}; + +/** + * Recording detail data. + */ +type RecordingDetailData = { + label: string; + value: string; + allowHTML: boolean; +}; diff --git a/src/addons/mod/bigbluebuttonbn/lang.json b/src/addons/mod/bigbluebuttonbn/lang.json index 380c2a357..5a12c19d1 100644 --- a/src/addons/mod/bigbluebuttonbn/lang.json +++ b/src/addons/mod/bigbluebuttonbn/lang.json @@ -12,8 +12,12 @@ "view_message_conference_room_ready": "This room is ready. You can join the session now.", "view_message_moderator": "moderator", "view_message_moderators": "moderators", + "view_message_norecordings": "There are no recordings available.", "view_message_session_started_at": "This session started at", "view_message_viewer": "viewer", "view_message_viewers": "viewers", - "view_nojoin": "You do not have a role that is allowed to join this session." + "view_nojoin": "You do not have a role that is allowed to join this session.", + "view_recording_format_presentation": "Presentation", + "view_recording_list_action_play": "Play", + "view_section_title_recordings": "Recordings" } diff --git a/src/addons/mod/bigbluebuttonbn/services/bigbluebuttonbn.ts b/src/addons/mod/bigbluebuttonbn/services/bigbluebuttonbn.ts index 655454fc5..03b962739 100644 --- a/src/addons/mod/bigbluebuttonbn/services/bigbluebuttonbn.ts +++ b/src/addons/mod/bigbluebuttonbn/services/bigbluebuttonbn.ts @@ -19,6 +19,7 @@ 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 { CoreTextUtils } from '@services/utils/text'; import { CoreUtils } from '@services/utils/utils'; import { CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws'; import { makeSingleton, Translate } from '@singletons'; @@ -206,6 +207,71 @@ export class AddonModBBBService { return ROOT_CACHE_KEY + 'meetingInfo:' + id + ':'; } + /** + * 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 getRecordings( + id: number, + groupId: number = 0, + options: AddonModBBBGetMeetingInfoOptions = {}, + ): Promise { + const site = await CoreSites.getSite(options.siteId); + + const params: AddonModBBBGetRecordingsWSParams = { + bigbluebuttonbnid: id, + groupid: groupId, + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getRecordingsCacheKey(id, groupId), + component: AddonModBBBService.COMPONENT, + componentId: options.cmId, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + const result = await site.read( + 'mod_bigbluebuttonbn_get_recordings', + params, + preSets, + ); + + if (result.warnings?.length) { + throw new CoreWSError(result.warnings[0]); + } else if (!result.tabledata) { + throw new CoreError('Cannot retrieve recordings.'); + } + + return { + ...result.tabledata, + parsedData: CoreTextUtils.parseJSON(result.tabledata?.data, []), + }; + } + + /** + * Get cache key for get recordings WS call. + * + * @param id BBB ID. + * @param groupId Group ID, 0 means that the function will determine the user group. + * @return Cache key. + */ + protected getRecordingsCacheKey(id: number, groupId: number = 0): string { + return this.getRecordingsCacheKeyPrefix(id) + groupId; + } + + /** + * Get cache key prefix for get recordings WS call. + * + * @param id BBB ID. + * @return Cache key prefix. + */ + protected getRecordingsCacheKeyPrefix(id: number): string { + return ROOT_CACHE_KEY + 'recordings:' + id + ':'; + } + /** * Report a BBB as being viewed. * @@ -271,6 +337,33 @@ export class AddonModBBBService { await site.invalidateWsCacheForKeyStartingWith(this.getMeetingInfoCacheKeyPrefix(id)); } + /** + * Invalidate recordings 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 invalidateRecordings(id: number, groupId: number = 0, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getRecordingsCacheKey(id, groupId)); + } + + /** + * Invalidate recordings 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 invalidateAllGroupsRecordings(id: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + await site.invalidateWsCacheForKeyStartingWith(this.getRecordingsCacheKeyPrefix(id)); + } + /** * Returns whether or not the BBB plugin is enabled for a certain site. * @@ -410,3 +503,48 @@ export type AddonModBBBEndMeetingWSParams = { export type AddonModBBBGetMeetingInfoOptions = CoreCourseCommonModWSOptions & { updateCache?: boolean; }; + +/** + * Params of mod_bigbluebuttonbn_get_recordings WS. + */ +export type AddonModBBBGetRecordingsWSParams = { + bigbluebuttonbnid: number; // Bigbluebuttonbn instance id. + tools?: string; // A set of enabled tools. + groupid?: number; // Group ID. +}; + +/** + * Data returned by mod_bigbluebuttonbn_get_recordings WS. + */ +export type AddonModBBBGetRecordingsWSResponse = { + status: boolean; // Whether the fetch was successful. + tabledata?: AddonModBBBRecordingsWSTableData; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Table data returned by mod_bigbluebuttonbn_get_recordings WS. + */ +export type AddonModBBBRecordingsWSTableData = { + activity: string; + ping_interval: number; // eslint-disable-line @typescript-eslint/naming-convention + locale: string; + profile_features: string[]; // eslint-disable-line @typescript-eslint/naming-convention + columns: { + key: string; + label: string; + width: string; + type?: string; // Column type. + sortable?: boolean; // Whether this column is sortable. + allowHTML?: boolean; // Whether this column contains HTML. + formatter?: string; // Formatter name. + }[]; + data: string; +}; + +/** + * Recordings table data with some calculated data. + */ +export type AddonModBBBRecordingsTableData = AddonModBBBRecordingsWSTableData & { + parsedData: Record[]; +};