From dd194b1a855d6e8fc2de8aea781dcedf9d9ea251 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Thu, 7 Mar 2024 13:12:17 +0100 Subject: [PATCH] MOBILE-4540 url: Reduce WS calls on module handler --- src/addons/mod/url/components/index/index.ts | 5 +- src/addons/mod/url/constants.ts | 22 +++ .../mod/url/services/handlers/index-link.ts | 3 +- .../mod/url/services/handlers/list-link.ts | 3 +- .../mod/url/services/handlers/module.ts | 128 ++++++++++-------- .../mod/url/services/handlers/prefetch.ts | 26 ++-- src/addons/mod/url/services/url.ts | 12 +- src/addons/mod/url/url.module.ts | 5 +- src/core/components/mod-icon/mod-icon.ts | 15 +- src/core/services/utils/url.ts | 60 ++++++++ 10 files changed, 179 insertions(+), 100 deletions(-) create mode 100644 src/addons/mod/url/constants.ts diff --git a/src/addons/mod/url/components/index/index.ts b/src/addons/mod/url/components/index/index.ts index 0d2cea0c12e..edfb1194ce9 100644 --- a/src/addons/mod/url/components/index/index.ts +++ b/src/addons/mod/url/components/index/index.ts @@ -20,8 +20,9 @@ import { CoreCourseContentsPage } from '@features/course/pages/contents/contents import { CoreCourse } from '@features/course/services/course'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreTextUtils } from '@services/utils/text'; -import { AddonModUrl, AddonModUrlDisplayOptions, AddonModUrlProvider, AddonModUrlUrl } from '../../services/url'; +import { AddonModUrl, AddonModUrlDisplayOptions, AddonModUrlUrl } from '../../services/url'; import { AddonModUrlHelper } from '../../services/url-helper'; +import { ADDON_MOD_URL_COMPONENT } from '../../constants'; /** * Component that displays a url. @@ -33,7 +34,7 @@ import { AddonModUrlHelper } from '../../services/url-helper'; }) export class AddonModUrlIndexComponent extends CoreCourseModuleMainResourceComponent implements OnInit { - component = AddonModUrlProvider.COMPONENT; + component = ADDON_MOD_URL_COMPONENT; pluginName = 'url'; url?: string; diff --git a/src/addons/mod/url/constants.ts b/src/addons/mod/url/constants.ts new file mode 100644 index 00000000000..692791ee903 --- /dev/null +++ b/src/addons/mod/url/constants.ts @@ -0,0 +1,22 @@ +// (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. + +export const ADDON_MOD_URL_COMPONENT = 'mmaModUrl'; + +// Routing. +export const ADDON_MOD_URL_PAGE_NAME = 'mod_url'; + +// Handlers. +export const ADDON_MOD_URL_ADDON_NAME = 'AddonModUrl'; +export const ADDON_MOD_URL_MODNAME = 'url'; diff --git a/src/addons/mod/url/services/handlers/index-link.ts b/src/addons/mod/url/services/handlers/index-link.ts index 2f3ced07670..71453ac8d60 100644 --- a/src/addons/mod/url/services/handlers/index-link.ts +++ b/src/addons/mod/url/services/handlers/index-link.ts @@ -15,6 +15,7 @@ import { Injectable } from '@angular/core'; import { CoreContentLinksModuleIndexHandler } from '@features/contentlinks/classes/module-index-handler'; import { makeSingleton } from '@singletons'; +import { ADDON_MOD_URL_ADDON_NAME, ADDON_MOD_URL_MODNAME } from '../../constants'; /** * Handler to treat links to url. @@ -26,7 +27,7 @@ export class AddonModUrlIndexLinkHandlerService extends CoreContentLinksModuleIn useModNameToGetModule = true; constructor() { - super('AddonModUrl', 'url', 'u'); + super(ADDON_MOD_URL_ADDON_NAME, ADDON_MOD_URL_MODNAME, 'u'); } } diff --git a/src/addons/mod/url/services/handlers/list-link.ts b/src/addons/mod/url/services/handlers/list-link.ts index 37a88442e92..1956dc05d2a 100644 --- a/src/addons/mod/url/services/handlers/list-link.ts +++ b/src/addons/mod/url/services/handlers/list-link.ts @@ -15,6 +15,7 @@ import { Injectable } from '@angular/core'; import { CoreContentLinksModuleListHandler } from '@features/contentlinks/classes/module-list-handler'; import { makeSingleton } from '@singletons'; +import { ADDON_MOD_URL_ADDON_NAME, ADDON_MOD_URL_MODNAME } from '../../constants'; /** * Handler to treat links to URL list page. @@ -25,7 +26,7 @@ export class AddonModUrlListLinkHandlerService extends CoreContentLinksModuleLis name = 'AddonModUrlListLinkHandler'; constructor() { - super('AddonModUrl', 'url'); + super(ADDON_MOD_URL_ADDON_NAME, ADDON_MOD_URL_MODNAME); } } diff --git a/src/addons/mod/url/services/handlers/module.ts b/src/addons/mod/url/services/handlers/module.ts index a67325fdffa..6f4ec41b1a1 100644 --- a/src/addons/mod/url/services/handlers/module.ts +++ b/src/addons/mod/url/services/handlers/module.ts @@ -16,7 +16,7 @@ import { CoreConstants, ModPurpose } from '@/core/constants'; import { Injectable, Type } from '@angular/core'; import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper'; import { CoreModuleHandlerBase } from '@features/course/classes/module-base-handler'; -import { CoreCourse } from '@features/course/services/course'; +import { CoreCourse, CoreCourseModuleContentFile } from '@features/course/services/course'; import { CoreCourseModuleData } from '@features/course/services/course-helper'; import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate'; import { CoreNavigationOptions } from '@services/navigator'; @@ -27,6 +27,9 @@ import { AddonModUrlIndexComponent } from '../../components/index/index'; import { AddonModUrl } from '../url'; import { AddonModUrlHelper } from '../url-helper'; import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { CoreUrlUtils } from '@services/utils/url'; +import { CoreMimetypeUtils } from '@services/utils/mimetype'; +import { ADDON_MOD_URL_ADDON_NAME, ADDON_MOD_URL_MODNAME, ADDON_MOD_URL_PAGE_NAME } from '../../constants'; /** * Handler to support url modules. @@ -34,11 +37,9 @@ import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; @Injectable({ providedIn: 'root' }) export class AddonModUrlModuleHandlerService extends CoreModuleHandlerBase implements CoreCourseModuleHandler { - static readonly PAGE_NAME = 'mod_url'; - - name = 'AddonModUrl'; - modName = 'url'; - protected pageName = AddonModUrlModuleHandlerService.PAGE_NAME; + name = ADDON_MOD_URL_ADDON_NAME; + modName = ADDON_MOD_URL_MODNAME; + protected pageName = ADDON_MOD_URL_PAGE_NAME; supportedFeatures = { [CoreConstants.FEATURE_MOD_ARCHETYPE]: CoreConstants.MOD_ARCHETYPE_RESOURCE, @@ -57,7 +58,6 @@ export class AddonModUrlModuleHandlerService extends CoreModuleHandlerBase imple * @inheritdoc */ async getData(module: CoreCourseModuleData): Promise { - /** * Open the URL. * @@ -69,8 +69,12 @@ export class AddonModUrlModuleHandlerService extends CoreModuleHandlerBase imple CoreCourse.storeModuleViewed(courseId, module.id); - const contents = await CoreCourse.getModuleContents(module); - AddonModUrlHelper.open(contents[0].fileurl); + const mainFile = await this.getModuleMainFile(module); + if (!mainFile) { + return; + } + + AddonModUrlHelper.open(mainFile.fileurl); }; const handlerData: CoreCourseModuleHandlerData = { @@ -94,7 +98,6 @@ export class AddonModUrlModuleHandlerService extends CoreModuleHandlerBase imple } }, button: { - hidden: true, // Hide it until we calculate if it should be displayed or not. icon: 'fas-link', label: 'core.openmodinbrowser', action: (event: Event, module: CoreCourseModuleData, courseId: number): void => { @@ -103,14 +106,8 @@ export class AddonModUrlModuleHandlerService extends CoreModuleHandlerBase imple }, }; - const hideButton = await CoreUtils.ignoreErrors(this.hideLinkButton(module)); - - if (handlerData.button && hideButton !== undefined) { - handlerData.button.hidden = hideButton; - } - try { - handlerData.icon = await this.getIconSrc(module); + handlerData.icon = await this.getIconSrc(module, handlerData.icon as string); } catch { // Ignore errors. } @@ -121,57 +118,72 @@ export class AddonModUrlModuleHandlerService extends CoreModuleHandlerBase imple /** * @inheritdoc */ - async getIconSrc(module?: CoreCourseModuleData): Promise { + async getIconSrc(module?: CoreCourseModuleData, modIcon?: string): Promise { if (!module) { - return; + return modIcon; } - let mainFile = module.contents?.[0]; - - if (!mainFile) { - try { - // Try to get module contents, it's needed to get the URL with parameters. - const contents = await CoreCourse.getModuleContents( - module, - undefined, - undefined, - true, - false, - undefined, - 'url', - ); - - mainFile = contents[0]; - } catch { - // Fallback in case is not prefetched. - const mod = await CoreCourse.getModule(module.id, module.course, undefined, true, false, undefined, 'url'); - - mainFile = mod.contents?.[0]; - } + const component = CoreUrlUtils.getThemeImageUrlParam(module.modicon, 'component'); + if (component === this.modName) { + return modIcon; } - const icon = mainFile? AddonModUrl.guessIcon(mainFile.fileurl) : undefined; + let icon: string | undefined; + + let image = CoreUrlUtils.getThemeImageUrlParam(module.modicon, 'image'); + if (image.startsWith('f/')) { + // Remove prefix, and hyphen + numbered suffix. + image = image.substring(2).replace(/-[0-9]+$/, ''); + + // In case we get an extension, try to get the type. + image = CoreMimetypeUtils.getExtensionType(image) ?? image; + + icon = CoreMimetypeUtils.getFileIconForType(image); + } else { + const mainFile = await this.getModuleMainFile(module); + + icon = mainFile? AddonModUrl.guessIcon(mainFile.fileurl) : undefined; + } // Calculate the icon to use. return CoreCourse.getModuleIconSrc(module.modname, module.modicon, icon); } /** - * Returns if contents are loaded to show link button. + * Get the module main file if not set. * - * @param module The module object. - * @returns Resolved when done. + * @param module Module. + * @returns Module contents. */ - protected async hideLinkButton(module: CoreCourseModuleData): Promise { - try { - const contents = - await CoreCourse.getModuleContents(module, undefined, undefined, false, false, undefined, this.modName); + protected async getModuleMainFile(module?: CoreCourseModuleData): Promise { + if (!module) { + return; + } + + if (module.contents?.[0]) { + return module.contents[0]; + } - return !(contents[0] && contents[0].fileurl); + try { + // Try to get module contents, it's needed to get the URL with parameters. + const contents = await CoreCourse.getModuleContents( + module, + undefined, + undefined, + true, + false, + undefined, + 'url', + ); + + module.contents = contents; } catch { - // Module contents could not be loaded, most probably device is offline. - return true; + // Fallback in case is not prefetched. + const mod = await CoreCourse.getModule(module.id, module.course, undefined, true, false, undefined, 'url'); + module.contents = mod.contents; } + + return module.contents?.[0]; } /** @@ -189,11 +201,13 @@ export class AddonModUrlModuleHandlerService extends CoreModuleHandlerBase imple */ protected async shouldOpenLink(module: CoreCourseModuleData): Promise { try { - const contents = - await CoreCourse.getModuleContents(module, undefined, undefined, false, false, undefined, this.modName); + const mainFile = await this.getModuleMainFile(module); + if (!mainFile) { + return false; + } // Check if the URL can be handled by the app. If so, always open it directly. - const canHandle = await CoreContentLinksHelper.canHandleLink(contents[0].fileurl, module.course, undefined, true); + const canHandle = await CoreContentLinksHelper.canHandleLink(mainFile.fileurl, module.course, undefined, true); if (canHandle) { // URL handled by the app, open it directly. @@ -203,8 +217,8 @@ export class AddonModUrlModuleHandlerService extends CoreModuleHandlerBase imple const url = await CoreUtils.ignoreErrors(AddonModUrl.getUrl(module.course, module.id)); const displayType = AddonModUrl.getFinalDisplayType(url); - return displayType == CoreConstants.RESOURCELIB_DISPLAY_OPEN || - displayType == CoreConstants.RESOURCELIB_DISPLAY_POPUP; + return displayType === CoreConstants.RESOURCELIB_DISPLAY_OPEN || + displayType === CoreConstants.RESOURCELIB_DISPLAY_POPUP; } } catch { return false; diff --git a/src/addons/mod/url/services/handlers/prefetch.ts b/src/addons/mod/url/services/handlers/prefetch.ts index d702def86cd..f143042c8c9 100644 --- a/src/addons/mod/url/services/handlers/prefetch.ts +++ b/src/addons/mod/url/services/handlers/prefetch.ts @@ -16,7 +16,11 @@ import { Injectable } from '@angular/core'; import { CoreCourseResourcePrefetchHandlerBase } from '@features/course/classes/resource-prefetch-handler'; import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/course'; import { makeSingleton } from '@singletons'; -import { AddonModUrlProvider } from '../url'; +import { + ADDON_MOD_URL_COMPONENT, + ADDON_MOD_URL_MODNAME, + ADDON_MOD_URL_ADDON_NAME, +} from '../../constants'; /** * Handler to prefetch URLs. URLs cannot be prefetched, but the handler will be used to invalidate some data on course PTR. @@ -24,16 +28,9 @@ import { AddonModUrlProvider } from '../url'; @Injectable({ providedIn: 'root' }) export class AddonModUrlPrefetchHandlerService extends CoreCourseResourcePrefetchHandlerBase { - name = 'AddonModUrl'; - modName = 'url'; - component = AddonModUrlProvider.COMPONENT; - - /** - * @inheritdoc - */ - async download(): Promise { - return; - } + name = ADDON_MOD_URL_ADDON_NAME; + modName = ADDON_MOD_URL_MODNAME; + component = ADDON_MOD_URL_COMPONENT; /** * @inheritdoc @@ -49,12 +46,5 @@ export class AddonModUrlPrefetchHandlerService extends CoreCourseResourcePrefetc return false; // URLs aren't downloadable. } - /** - * @inheritdoc - */ - async prefetch(): Promise { - return; - } - } export const AddonModUrlPrefetchHandler = makeSingleton(AddonModUrlPrefetchHandlerService); diff --git a/src/addons/mod/url/services/url.ts b/src/addons/mod/url/services/url.ts index 04722cb3652..80825678b54 100644 --- a/src/addons/mod/url/services/url.ts +++ b/src/addons/mod/url/services/url.ts @@ -24,8 +24,7 @@ import { CoreUtils } from '@services/utils/utils'; import { CoreCourseLogHelper } from '@features/course/services/log-helper'; import { CoreError } from '@classes/errors/error'; import { CoreSiteWSPreSets } from '@classes/sites/authenticated-site'; - -const ROOT_CACHE_KEY = 'mmaModUrl:'; +import { ADDON_MOD_URL_COMPONENT } from '../constants'; /** * Service that provides some features for urls. @@ -33,7 +32,8 @@ const ROOT_CACHE_KEY = 'mmaModUrl:'; @Injectable({ providedIn: 'root' }) export class AddonModUrlProvider { - static readonly COMPONENT = 'mmaModUrl'; + protected static readonly ROOT_CACHE_KEY = 'mmaModUrl:'; + static readonly COMPONENT = ADDON_MOD_URL_COMPONENT; /** * Get the final display type for a certain URL. Based on Moodle's url_get_final_display_type. @@ -94,7 +94,7 @@ export class AddonModUrlProvider { * @returns Cache key. */ protected getUrlCacheKey(courseId: number): string { - return ROOT_CACHE_KEY + 'url:' + courseId; + return AddonModUrlProvider.ROOT_CACHE_KEY + 'url:' + courseId; } /** @@ -121,7 +121,7 @@ export class AddonModUrlProvider { const preSets: CoreSiteWSPreSets = { cacheKey: this.getUrlCacheKey(courseId), updateFrequency: CoreSite.FREQUENCY_RARELY, - component: AddonModUrlProvider.COMPONENT, + component: ADDON_MOD_URL_COMPONENT, ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), }; @@ -222,7 +222,7 @@ export class AddonModUrlProvider { return CoreCourseLogHelper.log( 'mod_url_view_url', params, - AddonModUrlProvider.COMPONENT, + ADDON_MOD_URL_COMPONENT, id, siteId, ); diff --git a/src/addons/mod/url/url.module.ts b/src/addons/mod/url/url.module.ts index 0ff0b1720c4..215faea1d0c 100644 --- a/src/addons/mod/url/url.module.ts +++ b/src/addons/mod/url/url.module.ts @@ -21,8 +21,9 @@ import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-ro import { AddonModUrlComponentsModule } from './components/components.module'; import { AddonModUrlIndexLinkHandler } from './services/handlers/index-link'; import { AddonModUrlListLinkHandler } from './services/handlers/list-link'; -import { AddonModUrlModuleHandler, AddonModUrlModuleHandlerService } from './services/handlers/module'; +import { AddonModUrlModuleHandler } from './services/handlers/module'; import { AddonModUrlPrefetchHandler } from './services/handlers/prefetch'; +import { ADDON_MOD_URL_PAGE_NAME } from './constants'; /** * Get mod Url services. @@ -41,7 +42,7 @@ export async function getModUrlServices(): Promise[]> { const routes: Routes = [ { - path: AddonModUrlModuleHandlerService.PAGE_NAME, + path: ADDON_MOD_URL_PAGE_NAME, loadChildren: () => import('./url-lazy.module').then(m => m.AddonModUrlLazyModule), }, ]; diff --git a/src/core/components/mod-icon/mod-icon.ts b/src/core/components/mod-icon/mod-icon.ts index 8ce84db68e5..5b217762e70 100644 --- a/src/core/components/mod-icon/mod-icon.ts +++ b/src/core/components/mod-icon/mod-icon.ts @@ -213,18 +213,7 @@ export class CoreModIconComponent implements OnInit, OnChanges { * @returns Guessed modname. */ protected getComponentNameFromIconUrl(iconUrl: string): string { - if (!CoreUrlUtils.isThemeImageUrl(this.iconUrl)) { - // Cannot be guessed. - return ''; - } - - const iconParams = CoreUrlUtils.extractUrlParams(iconUrl); - let component = iconParams['component']; - - if (!component) { - const matches = iconUrl.match('/theme/image.php/[^/]+/([^/]+)/[-0-9]*/'); - component = (matches && matches[1]) || ''; - } + const component = CoreUrlUtils.getThemeImageUrlParam(iconUrl, 'component'); // Some invalid components (others may be added later on). if (component === 'core' || component === 'theme') { @@ -232,7 +221,7 @@ export class CoreModIconComponent implements OnInit, OnChanges { } if (component.startsWith('mod_')) { - component = component.substring(4); + return component.substring(4); } return component; diff --git a/src/core/services/utils/url.ts b/src/core/services/utils/url.ts index 149630ec620..a89f37267ad 100644 --- a/src/core/services/utils/url.ts +++ b/src/core/services/utils/url.ts @@ -505,6 +505,66 @@ export class CoreUrlUtilsProvider { return imageUrl?.indexOf('/theme/image.php') !== -1; } + /** + * Returns an specific param from an image URL. + * + * @param imageUrl Image Url + * @param param Param to get from the URL. + * @param siteUrl Site URL. + * @returns Param from the URL. + */ + getThemeImageUrlParam(imageUrl: string, param: string, siteUrl?: string): string { + if (!this.isThemeImageUrl(imageUrl, siteUrl)) { + // Cannot be guessed. + return ''; + } + + const matches = imageUrl.match('/theme/image.php/(.*)'); + if (matches?.[1]) { + // Slash arguments found. + const slasharguments = matches[1].split('/'); + + if (slasharguments.length < 4) { + // Image not found, malformed URL. + return ''; + } + + // Join from the third element to the end. + const image = slasharguments.slice(3).join('/'); + switch (param) { + case 'theme': + return slasharguments[0]; + case 'component': + return slasharguments[1]; + case 'rev': + return slasharguments[2]; + case 'image': + // Remove possible url params. + return CoreUrlUtils.removeUrlParams(image); + default: + return CoreUrlUtils.extractUrlParams(image)[param] || ''; + } + + } + + // URL arguments found. + const iconParams = CoreUrlUtils.extractUrlParams(imageUrl); + + switch (param) { + case 'theme': + return iconParams[param] || 'standard'; + case 'component': + return iconParams[param] || 'core'; + case 'rev': + return iconParams[param] || '-1'; + case 'svg': + return iconParams[param] || '1'; + case 'image': + default: + return iconParams[param] || ''; + } + } + /** * Remove protocol and www from a URL. *