diff --git a/src/addons/mod/forum/components/index/index.html b/src/addons/mod/forum/components/index/index.html index 9aaa67dea..cb0eaa788 100644 --- a/src/addons/mod/forum/components/index/index.html +++ b/src/addons/mod/forum/components/index/index.html @@ -3,7 +3,7 @@ - + @@ -103,6 +103,9 @@ + + + diff --git a/src/addons/mod/forum/components/index/index.ts b/src/addons/mod/forum/components/index/index.ts index 6445f5197..0e2d8eb69 100644 --- a/src/addons/mod/forum/components/index/index.ts +++ b/src/addons/mod/forum/components/index/index.ts @@ -29,7 +29,12 @@ import { CoreCourseContentsPage } from '@features/course/pages/contents/contents import { AddonModForumHelper } from '@addons/mod/forum/services/helper.service'; import { CoreGroups, CoreGroupsProvider } from '@services/groups'; import { CoreEvents, CoreEventObserver } from '@singletons/events'; -import { AddonModForumSyncProvider } from '@addons/mod/forum/services/sync.service'; +import { + AddonModForumAutoSyncData, + AddonModForumManualSyncData, + AddonModForumSyncProvider, + AddonModForumSyncResult, +} from '@addons/mod/forum/services/sync.service'; import { CoreSites } from '@services/sites'; import { CoreUser } from '@features/user/services/user'; import { CoreDomUtils } from '@services/utils/dom'; @@ -41,6 +46,7 @@ import { AddonModForumDiscussionOptionsMenuComponent } from '../discussion-optio import { AddonModForumSortOrderSelectorComponent } from '../sort-order-selector/sort-order-selector'; import { CoreScreen } from '@services/screen'; import { CoreArray } from '@singletons/array'; +import { AddonModForumPrefetchHandler } from '../../services/handlers/prefetch'; /** * Component that displays a forum entry page. @@ -58,8 +64,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom moduleName = 'forum'; descriptionNote?: string; forum?: AddonModForumData; - canLoadMore = false; - loadMoreError = false; + fetchMoreDiscussionsFailed = false; discussions: AddonModForumDiscussionsManager; canAddDiscussion = false; addDiscussionText!: string; @@ -201,12 +206,12 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom * @param sync If the refresh needs syncing. * @param showErrors Wether to show errors to the user or hide them. */ - protected async fetchContent(refresh: boolean = false, sync: boolean = false): Promise { - this.loadMoreError = false; + protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise { + this.fetchMoreDiscussionsFailed = false; const promises: Promise[] = []; - promises.push(this.fetchForum()); + promises.push(this.fetchForum(sync, showErrors)); promises.push(this.fetchSortOrderPreference()); try { @@ -219,7 +224,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom if (refresh) { CoreDomUtils.instance.showErrorModalDefault(error, 'addon.mod_forum.errorgetforum', true); - this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading. + this.fetchMoreDiscussionsFailed = true; // Set to prevent infinite calls with infinite-loading. } else { // Get forum failed, retry without using cache since it might be a new activity. await this.refreshContent(sync); @@ -229,136 +234,102 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom this.fillContextMenu(refresh); } - private async fetchForum(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise { + private async fetchForum(sync: boolean = false, showErrors: boolean = false): Promise { if (!this.courseId || !this.module) { return; } - this.loadMoreError = false; + const forum = await AddonModForum.instance.getForum(this.courseId, this.module.id); + + this.forum = forum; + this.description = forum.intro || this.description; + this.availabilityMessage = AddonModForumHelper.instance.getAvailabilityMessage(forum); + this.descriptionNote = Translate.instant('addon.mod_forum.numdiscussions', { + numdiscussions: forum.numdiscussions, + }); + + if (typeof forum.istracked != 'undefined') { + this.trackPosts = forum.istracked; + } + + this.dataRetrieved.emit(forum); + + switch (forum.type) { + case 'news': + case 'blog': + this.addDiscussionText = Translate.instant('addon.mod_forum.addanewtopic'); + break; + case 'qanda': + this.addDiscussionText = Translate.instant('addon.mod_forum.addanewquestion'); + break; + default: + this.addDiscussionText = Translate.instant('addon.mod_forum.addanewdiscussion'); + } + + if (sync) { + // Try to synchronize the forum. + const updated = await this.syncActivity(showErrors); + + if (updated) { + // Sync successful, send event. + CoreEvents.trigger(AddonModForumSyncProvider.MANUAL_SYNCED, { + forumId: forum.id, + userId: CoreSites.instance.getCurrentSiteUserId(), + source: 'index', + }, CoreSites.instance.getCurrentSiteId()); + } + } const promises: Promise[] = []; + // Check if the activity uses groups. promises.push( - AddonModForum.instance - .getForum(this.courseId, this.module.id) - .then(async (forum) => { - this.forum = forum; - this.description = forum.intro || this.description; - this.availabilityMessage = AddonModForumHelper.instance.getAvailabilityMessage(forum); - this.descriptionNote = Translate.instant('addon.mod_forum.numdiscussions', { - numdiscussions: forum.numdiscussions, - }); - - if (typeof forum.istracked != 'undefined') { - this.trackPosts = forum.istracked; - } - - this.dataRetrieved.emit(forum); - - switch (forum.type) { - case 'news': - case 'blog': - this.addDiscussionText = Translate.instant('addon.mod_forum.addanewtopic'); - break; - case 'qanda': - this.addDiscussionText = Translate.instant('addon.mod_forum.addanewquestion'); - break; - default: - this.addDiscussionText = Translate.instant('addon.mod_forum.addanewdiscussion'); - } - - if (sync) { - // Try to synchronize the forum. - const updated = await this.syncActivity(showErrors); - - if (updated) { - // Sync successful, send event. - CoreEvents.trigger(AddonModForumSyncProvider.MANUAL_SYNCED, { - forumId: forum.id, - userId: CoreSites.instance.getCurrentSiteUserId(), - source: 'index', - }, CoreSites.instance.getCurrentSiteId()); - } - } - - const promises: Promise[] = []; - - // Check if the activity uses groups. - promises.push( - // eslint-disable-next-line promise/no-nesting - CoreGroups.instance - .getActivityGroupMode(this.forum.cmid) - .then(async mode => { - this.usesGroups = mode === CoreGroupsProvider.SEPARATEGROUPS + CoreGroups.instance + .getActivityGroupMode(this.forum.cmid) + .then(async mode => { + this.usesGroups = mode === CoreGroupsProvider.SEPARATEGROUPS || mode === CoreGroupsProvider.VISIBLEGROUPS; - return; - }), - ); - - promises.push( - // eslint-disable-next-line promise/no-nesting - AddonModForum.instance - .getAccessInformation(this.forum.id, { cmId: this.module!.id }) - .then(async accessInfo => { - // Disallow adding discussions if cut-off date is reached and the user has not the - // capability to override it. - // Just in case the forum was fetched from WS when the cut-off date was not reached but it is now. - const cutoffDateReached = AddonModForumHelper.instance.isCutoffDateReached(this.forum!) - && !accessInfo.cancanoverridecutoff; - this.canAddDiscussion = !!this.forum?.cancreatediscussions && !cutoffDateReached; - - return; - }), - ); - - if (AddonModForum.instance.isSetPinStateAvailableForSite()) { - // Use the canAddDiscussion WS to check if the user can pin discussions. - promises.push( - // eslint-disable-next-line promise/no-nesting - AddonModForum.instance - .canAddDiscussionToAll(this.forum.id, { cmId: this.module!.id }) - .then(async response => { - this.canPin = !!response.canpindiscussions; - - return; - }) - .catch(async () => { - this.canPin = false; - - return; - }), - ); - } else { - this.canPin = false; - } - - await Promise.all(promises); - return; }), ); - promises.push(this.fetchSortOrderPreference()); + promises.push( + AddonModForum.instance + .getAccessInformation(this.forum.id, { cmId: this.module!.id }) + .then(async accessInfo => { + // Disallow adding discussions if cut-off date is reached and the user has not the + // capability to override it. + // Just in case the forum was fetched from WS when the cut-off date was not reached but it is now. + const cutoffDateReached = AddonModForumHelper.instance.isCutoffDateReached(this.forum!) + && !accessInfo.cancanoverridecutoff; + this.canAddDiscussion = !!this.forum?.cancreatediscussions && !cutoffDateReached; - try { - await Promise.all(promises); - await Promise.all([ - this.fetchOfflineDiscussions(), - this.fetchDiscussions(refresh), - ]); - } catch (message) { - if (!refresh) { - // Get forum failed, retry without using cache since it might be a new activity. - return this.refreshContent(sync); - } + return; + }), + ); - CoreDomUtils.instance.showErrorModalDefault(message, 'addon.mod_forum.errorgetforum', true); + if (AddonModForum.instance.isSetPinStateAvailableForSite()) { + // Use the canAddDiscussion WS to check if the user can pin discussions. + promises.push( + AddonModForum.instance + .canAddDiscussionToAll(this.forum.id, { cmId: this.module!.id }) + .then(async response => { + this.canPin = !!response.canpindiscussions; - this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading. + return; + }) + .catch(async () => { + this.canPin = false; + + return; + }), + ); + } else { + this.canPin = false; } - this.fillContextMenu(refresh); + await Promise.all(promises); } /** @@ -414,7 +385,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom */ protected async fetchDiscussions(refresh: boolean): Promise { const forum = this.forum!; - this.loadMoreError = false; + this.fetchMoreDiscussionsFailed = false; if (refresh) { this.page = 0; @@ -435,7 +406,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom if (forum.type === 'single') { for (const discussion of discussions) { if (discussion.userfullname && discussion.parent === 0) { - (discussion as any).userfullname = false; + discussion.userfullname = false; break; } } @@ -452,12 +423,11 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom } if (this.page === 0) { - this.discussions.setOnlineDiscussions(discussions); + this.discussions.setOnlineDiscussions(discussions, response.canLoadMore); } else { - this.discussions.setItems(this.discussions.items.concat(discussions)); + this.discussions.setItems(this.discussions.items.concat(discussions), response.canLoadMore); } - this.canLoadMore = response.canLoadMore; this.page++; // Check if there are replies for discussions stored in offline. @@ -467,7 +437,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom if (hasOffline) { // Only update new fetched discussions. - const promises = discussions.map(async (discussion: any) => { + const promises = discussions.map(async (discussion) => { // Get offline discussions. const replies = await AddonModForumOffline.instance.getDiscussionReplies(discussion.discussion); @@ -484,14 +454,16 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom * @param infiniteComplete Infinite scroll complete function. Only used from core-infinite-loading. * @return Promise resolved when done. */ - fetchMoreDiscussions(infiniteComplete?: any): Promise { - return this.fetchDiscussions(false).catch((message) => { - CoreDomUtils.instance.showErrorModalDefault(message, 'addon.mod_forum.errorgetforum', true); + async fetchMoreDiscussions(complete: () => void): Promise { + try { + await this.fetchDiscussions(false); + } catch (error) { + CoreDomUtils.instance.showErrorModalDefault(error, 'addon.mod_forum.errorgetforum', true); - this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading. - }).finally(() => { - infiniteComplete && infiniteComplete(); - }); + this.fetchMoreDiscussionsFailed = true; + } finally { + complete(); + } } /** @@ -522,7 +494,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom * * @return Resolved when done. */ - protected invalidateContent(): Promise { + protected async invalidateContent(): Promise { const promises: Promise[] = []; promises.push(AddonModForum.instance.invalidateForumData(this.courseId!)); @@ -537,7 +509,16 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom promises.push(CoreUser.instance.invalidateUserPreference(AddonModForumProvider.PREFERENCE_SORTORDER)); } - return Promise.all(promises); + await Promise.all(promises); + } + + /** + * Performs the sync of the activity. + * + * @return Promise resolved when done. + */ + protected sync(): Promise { + return AddonModForumPrefetchHandler.instance.sync(this.module!, this.courseId!); } /** @@ -546,10 +527,23 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom * @param result Data returned on the sync function. * @return Whether it succeed or not. */ - protected hasSyncSucceed(result: any): boolean { + protected hasSyncSucceed(result: AddonModForumSyncResult): boolean { return result.updated; } + /** + * Compares sync event data with current data to check if refresh content is needed. + * + * @param syncEventData Data receiven on sync observer. + * @return True if refresh is needed, false otherwise. + */ + protected isRefreshSyncNeeded(syncEventData: AddonModForumAutoSyncData | AddonModForumManualSyncData): boolean { + return !!this.forum + && (!('source' in syncEventData) || syncEventData.source != 'index') + && syncEventData.forumId == this.forum.id + && syncEventData.userId == CoreSites.instance.getCurrentSiteUserId(); + } + /** * Function called when we receive an event of new discussion or reply to discussion. * @@ -685,6 +679,8 @@ type DiscussionItem = AddonModForumDiscussion | AddonModForumOfflineDiscussion | */ class AddonModForumDiscussionsManager extends CorePageItemsListManager { + onlineLoaded = false; + private discussionsPathPrefix: string; private component: AddonModForumIndexComponent; @@ -747,10 +743,10 @@ class AddonModForumDiscussionsManager extends CorePageItemsListManager !this.isOnlineDiscussion(discussion)); - this.setItems(otherDiscussions.concat(onlineDiscussions)); + this.setItems(otherDiscussions.concat(onlineDiscussions), hasMoreItems); } /** @@ -761,7 +757,16 @@ class AddonModForumDiscussionsManager extends CorePageItemsListManager !this.isOfflineDiscussion(discussion)); - this.setItems((offlineDiscussions as DiscussionItem[]).concat(otherDiscussions)); + this.setItems((offlineDiscussions as DiscussionItem[]).concat(otherDiscussions), this.hasMoreItems); + } + + /** + * @inheritdoc + */ + setItems(discussions: DiscussionItem[], hasMoreItems: boolean = false): void { + super.setItems(discussions, hasMoreItems); + + this.onlineLoaded = this.onlineLoaded || discussions.some(discussion => this.isOnlineDiscussion(discussion)); } /** diff --git a/src/addons/mod/forum/forum.module.ts b/src/addons/mod/forum/forum.module.ts index 8f2ee34b6..4c7d58def 100644 --- a/src/addons/mod/forum/forum.module.ts +++ b/src/addons/mod/forum/forum.module.ts @@ -25,6 +25,10 @@ import { CoreScreen } from '@services/screen'; import { AddonModForumComponentsModule } from './components/components.module'; import { AddonModForumModuleHandler, AddonModForumModuleHandlerService } from './services/handlers/module'; import { SITE_SCHEMA } from './services/offline-db'; +import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate'; +import { AddonModForumPrefetchHandler } from './services/handlers/prefetch'; +import { CoreCronDelegate } from '@services/cron'; +import { AddonModForumSyncCronHandler } from './services/handlers/sync-cron'; const mainMenuRoutes: Routes = [ { @@ -77,7 +81,11 @@ const courseContentsRoutes: Routes = conditionalRoutes( { provide: APP_INITIALIZER, multi: true, - useValue: () => CoreCourseModuleDelegate.instance.registerHandler(AddonModForumModuleHandler.instance), + useValue: () => { + CoreCourseModuleDelegate.instance.registerHandler(AddonModForumModuleHandler.instance); + CoreCourseModulePrefetchDelegate.instance.registerHandler(AddonModForumPrefetchHandler.instance); + CoreCronDelegate.instance.register(AddonModForumSyncCronHandler.instance); + }, }, ], }) diff --git a/src/addons/mod/forum/pages/discussion/discussion.page.ts b/src/addons/mod/forum/pages/discussion/discussion.page.ts index c874b73ca..d6bf7a2d2 100644 --- a/src/addons/mod/forum/pages/discussion/discussion.page.ts +++ b/src/addons/mod/forum/pages/discussion/discussion.page.ts @@ -37,7 +37,7 @@ import { } from '../../services/forum.service'; import { AddonModForumHelper } from '../../services/helper.service'; import { AddonModForumOffline } from '../../services/offline.service'; -import { AddonModForumSync, AddonModForumSyncProvider } from '../../services/sync.service'; +import { AddonModForumManualSyncData, AddonModForumSync, AddonModForumSyncProvider } from '../../services/sync.service'; type SortType = 'flat-newest' | 'flat-oldest' | 'nested'; @@ -180,7 +180,7 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes }, CoreSites.instance.getCurrentSiteId()); // Refresh data if this forum discussion is synchronized from discussions list. - this.syncManualObserver = CoreEvents.on(AddonModForumSyncProvider.MANUAL_SYNCED, (data: any) => { + this.syncManualObserver = CoreEvents.on(AddonModForumSyncProvider.MANUAL_SYNCED, (data: AddonModForumManualSyncData) => { if (data.source != 'discussion' && data.forumId == this.forumId && data.userId == CoreSites.instance.getCurrentSiteUserId()) { // Refresh the data. @@ -541,7 +541,7 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes if (result && result.updated) { // Sync successful, send event. - CoreEvents.trigger(AddonModForumSyncProvider.MANUAL_SYNCED, { + CoreEvents.trigger(AddonModForumSyncProvider.MANUAL_SYNCED, { forumId: this.forumId, userId: CoreSites.instance.getCurrentSiteUserId(), source: 'discussion', diff --git a/src/addons/mod/forum/services/forum.service.ts b/src/addons/mod/forum/services/forum.service.ts index 6abf71c05..201f252ea 100644 --- a/src/addons/mod/forum/services/forum.service.ts +++ b/src/addons/mod/forum/services/forum.service.ts @@ -1401,7 +1401,7 @@ export type AddonModForumDiscussion = { attachments?: CoreWSExternalFile[]; totalscore: number; // The post message total score. mailnow: number; // Mail now?. - userfullname: string; // Post author full name. + userfullname: string | boolean; // Post author full name. usermodifiedfullname: string; // Post modifier full name. userpictureurl: string; // Post author picture. usermodifiedpictureurl: string; // Post modifier picture. @@ -1452,6 +1452,7 @@ export type AddonModForumPost = { }; attachment?: 0 | 1; attachments?: (CoreFileEntry | AddonModForumWSPostAttachment)[]; + messageinlinefiles?: CoreWSExternalFile[]; haswordcount?: boolean; // Haswordcount. wordcount?: number; // Wordcount. tags?: { // Tags. diff --git a/src/addons/mod/forum/services/handlers/prefetch.ts b/src/addons/mod/forum/services/handlers/prefetch.ts new file mode 100644 index 000000000..501d21090 --- /dev/null +++ b/src/addons/mod/forum/services/handlers/prefetch.ts @@ -0,0 +1,353 @@ +// (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 { CoreCourseActivityPrefetchHandlerBase } from '@features/course/classes/activity-prefetch-handler'; +import { AddonModForum, AddonModForumData, AddonModForumPost, AddonModForumProvider } from '../forum.service'; +import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; +import { CoreFilepool } from '@services/filepool'; +import { CoreWSExternalFile } from '@services/ws'; +import { CoreCourse, CoreCourseAnyModuleData, CoreCourseCommonModWSOptions } from '@features/course/services/course'; +import { CoreUser } from '@features/user/services/user'; +import { CoreGroups, CoreGroupsProvider } from '@services/groups'; +import { CoreUtils } from '@services/utils/utils'; +import { AddonModForumSync } from '../sync.service'; +import { makeSingleton } from '@singletons'; + +/** + * Handler to prefetch forums. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModForumPrefetchHandlerService extends CoreCourseActivityPrefetchHandlerBase { + + name = 'AddonModForum'; + modName = 'forum'; + component = AddonModForumProvider.COMPONENT; + updatesNames = /^configuration$|^.*files$|^discussions$/; + + /** + * Get list of files. If not defined, we'll assume they're in module.contents. + * + * @param module Module. + * @param courseId Course ID the module belongs to. + * @param single True if we're downloading a single module, false if we're downloading a whole section. + * @return Promise resolved with the list of files. + */ + async getFiles(module: CoreCourseAnyModuleData, courseId: number): Promise { + try { + const forum = await AddonModForum.instance.getForum(courseId, module.id); + + const files = this.getIntroFilesFromInstance(module, forum); + + // Get posts. + const posts = await this.getPostsForPrefetch(forum, { cmId: module.id }); + + // Add posts attachments and embedded files. + files.concat(this.getPostsFiles(posts)); + + return files; + } catch (error) { + // Forum not found, return empty list. + return []; + } + } + + /** + * Given a list of forum posts, return a list with all the files (attachments and embedded files). + * + * @param posts Forum posts. + * @return Files. + */ + protected getPostsFiles(posts: AddonModForumPost[]): CoreWSExternalFile[] { + let files: CoreWSExternalFile[] = []; + const getInlineFiles = CoreSites.instance.getCurrentSite()?.isVersionGreaterEqualThan('3.2'); + + posts.forEach((post) => { + if (post.attachments && post.attachments.length) { + files = files.concat(post.attachments as CoreWSExternalFile[]); + } + if (getInlineFiles && post.messageinlinefiles && post.messageinlinefiles.length) { + files = files.concat(post.messageinlinefiles); + } else if (post.message && !getInlineFiles) { + files = files.concat(CoreFilepool.instance.extractDownloadableFilesFromHtmlAsFakeFileObjects(post.message)); + } + }); + + return files; + } + + /** + * Get the posts to be prefetched. + * + * @param forum Forum instance. + * @param options Other options. + * @return Promise resolved with array of posts. + */ + protected getPostsForPrefetch( + forum: AddonModForumData, + options: CoreCourseCommonModWSOptions = {}, + ): Promise { + const promises = AddonModForum.instance.getAvailableSortOrders().map((sortOrder) => { + // Get discussions in first 2 pages. + const discussionsOptions = { + sortOrder: sortOrder.value, + numPages: 2, + ...options, // Include all options. + }; + + return AddonModForum.instance.getDiscussionsInPages(forum.id, discussionsOptions).then((response) => { + if (response.error) { + throw new Error('Failed getting discussions'); + } + + const promises: Promise<{ posts: AddonModForumPost[] }>[] = []; + + response.discussions.forEach((discussion) => { + promises.push(AddonModForum.instance.getDiscussionPosts(discussion.discussion, options)); + }); + + return Promise.all(promises); + }); + }); + + return Promise.all(promises).then((results) => { + // Each order has returned its own list of posts. Merge all the lists, preventing duplicates. + const posts: AddonModForumPost[] = []; + const postIds = {}; // To make the array unique. + + results.forEach((orderResults) => { + orderResults.forEach((orderResult) => { + orderResult.posts.forEach((post) => { + if (!postIds[post.id]) { + postIds[post.id] = true; + posts.push(post); + } + }); + }); + }); + + return posts; + }); + } + + /** + * Invalidate the prefetched content. + * + * @param moduleId The module ID. + * @param courseId The course ID the module belongs to. + * @return Promise resolved when the data is invalidated. + */ + invalidateContent(moduleId: number, courseId: number): Promise { + return AddonModForum.instance.invalidateContent(moduleId, courseId); + } + + /** + * Invalidate WS calls needed to determine module status (usually, to check if module is downloadable). + * It doesn't need to invalidate check updates. It should NOT invalidate files nor all the prefetched data. + * + * @param module Module. + * @param courseId Course ID the module belongs to. + * @return Promise resolved when invalidated. + */ + async invalidateModule(module: CoreCourseAnyModuleData, courseId: number): Promise { + // Invalidate forum data to recalculate unread message count badge. + const promises: Promise[] = []; + + promises.push(AddonModForum.instance.invalidateForumData(courseId)); + promises.push(CoreCourse.instance.invalidateModule(module.id)); + + await Promise.all(promises); + } + + /** + * Prefetch a module. + * + * @param module Module. + * @param courseId Course ID the module belongs to. + * @param single True if we're downloading a single module, false if we're downloading a whole section. + * @param dirPath Path of the directory where to store all the content files. + * @return Promise resolved when done. + */ + prefetch(module: CoreCourseAnyModuleData, courseId?: number, single?: boolean): Promise { + return this.prefetchPackage(module, courseId, this.prefetchForum.bind(this, module, courseId, single)); + } + + /** + * Prefetch a forum. + * + * @param module The module object returned by WS. + * @param courseId Course ID the module belongs to. + * @param single True if we're downloading a single module, false if we're downloading a whole section. + * @param siteId Site ID. + * @return Promise resolved when done. + */ + protected async prefetchForum( + module: CoreCourseAnyModuleData, + courseId: number, + single: boolean, + siteId: string, + ): Promise { + const commonOptions = { + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; + const modOptions = { + cmId: module.id, + ...commonOptions, // Include all common options. + }; + + // Get the forum data. + const forum = await AddonModForum.instance.getForum(courseId, module.id, commonOptions); + const promises: Promise[] = []; + + // Prefetch the posts. + promises.push(this.getPostsForPrefetch(forum, modOptions).then((posts) => { + const promises: Promise[] = []; + + const files = this.getIntroFilesFromInstance(module, forum).concat(this.getPostsFiles(posts)); + promises.push(CoreFilepool.instance.addFilesToQueue(siteId, files, this.component, module.id)); + + // Prefetch groups data. + promises.push(this.prefetchGroupsInfo(forum, courseId, !!forum.cancreatediscussions, siteId)); + + // Prefetch avatars. + promises.push(CoreUser.instance.prefetchUserAvatars(posts, 'userpictureurl', siteId)); + + return Promise.all(promises); + })); + + // Prefetch access information. + promises.push(AddonModForum.instance.getAccessInformation(forum.id, modOptions)); + + // Prefetch sort order preference. + if (AddonModForum.instance.isDiscussionListSortingAvailable()) { + promises.push(CoreUser.instance.getUserPreference(AddonModForumProvider.PREFERENCE_SORTORDER, siteId)); + } + + await Promise.all(promises); + } + + /** + * Prefetch groups info for a forum. + * + * @param module The module object returned by WS. + * @param courseI Course ID the module belongs to. + * @param canCreateDiscussions Whether the user can create discussions in the forum. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when group data has been prefetched. + */ + protected async prefetchGroupsInfo( + forum: AddonModForumData, + courseId: number, + canCreateDiscussions: boolean, + siteId?: string, + ): Promise { + const options = { + cmId: forum.cmid, + siteId, + }; + + // Check group mode. + try { + const mode = await CoreGroups.instance.getActivityGroupMode(forum.cmid, siteId); + + if (mode !== CoreGroupsProvider.SEPARATEGROUPS && mode !== CoreGroupsProvider.VISIBLEGROUPS) { + // Activity doesn't use groups. Prefetch canAddDiscussionToAll to determine if user can pin/attach. + await CoreUtils.instance.ignoreErrors(AddonModForum.instance.canAddDiscussionToAll(forum.id, options)); + + return; + } + + // Activity uses groups, prefetch allowed groups. + const result = await CoreGroups.instance.getActivityAllowedGroups(forum.cmid, undefined, siteId); + if (mode === CoreGroupsProvider.SEPARATEGROUPS) { + // Groups are already filtered by WS. Prefetch canAddDiscussionToAll to determine if user can pin/attach. + await CoreUtils.instance.ignoreErrors(AddonModForum.instance.canAddDiscussionToAll(forum.id, options)); + + return; + } + + if (canCreateDiscussions) { + // Prefetch data to check the visible groups when creating discussions. + const response = await CoreUtils.instance.ignoreErrors( + AddonModForum.instance.canAddDiscussionToAll(forum.id, options), + { status: false }, + ); + + if (response.status) { + // User can post to all groups, nothing else to prefetch. + return; + } + + // The user can't post to all groups, let's check which groups he can post to. + await Promise.all( + result.groups.map( + async (group) => CoreUtils.instance.ignoreErrors( + AddonModForum.instance.canAddDiscussion(forum.id, group.id, options), + ), + ), + ); + } + } catch (error) { + // Ignore errors if cannot create discussions. + if (canCreateDiscussions) { + throw error; + } + } + } + + /** + * Sync a module. + * + * @param module Module. + * @param courseId Course ID the module belongs to + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when done. + */ + async sync( + module: CoreCourseAnyModuleData, + courseId: number, + siteId?: string, + ): Promise { + const promises: Promise[] = []; + + promises.push(AddonModForumSync.instance.syncForumDiscussions(module.instance!, undefined, siteId)); + promises.push(AddonModForumSync.instance.syncForumReplies(module.instance!, undefined, siteId)); + promises.push(AddonModForumSync.instance.syncRatings(module.id, undefined, true, siteId)); + + const results = await Promise.all(promises); + + return results.reduce( + (a, b) => ({ + updated: a.updated || b.updated, + warnings: (a.warnings || []).concat(b.warnings || []), + }), + { + updated: false, + warnings: [], + }, + ); + } + +} + +export class AddonModForumPrefetchHandler extends makeSingleton(AddonModForumPrefetchHandlerService) {} + +/** + * Data returned by a forum sync. + */ +export type AddonModForumSyncResult = { + warnings: string[]; // List of warnings. + updated: boolean; // Whether some data was sent to the server or offline data was updated. +}; diff --git a/src/addons/mod/forum/services/handlers/sync-cron.ts b/src/addons/mod/forum/services/handlers/sync-cron.ts new file mode 100644 index 000000000..07cfdfd46 --- /dev/null +++ b/src/addons/mod/forum/services/handlers/sync-cron.ts @@ -0,0 +1,51 @@ +// (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 { CoreCronHandler } from '@services/cron'; +import { makeSingleton } from '@singletons'; +import { AddonModForumSync } from '../sync.service'; + +/** + * Synchronization cron handler. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModForumSyncCronHandlerService implements CoreCronHandler { + + name = 'AddonModForumSyncCronHandler'; + + /** + * Execute the process. + * Receives the ID of the site affected, undefined for all sites. + * + * @param siteId ID of the site affected, undefined for all sites. + * @param force Wether the execution is forced (manual sync). + * @return Promise resolved when done, rejected if failure. + */ + execute(siteId?: string, force?: boolean): Promise { + return AddonModForumSync.instance.syncAllForums(siteId, force); + } + + /** + * Get the time between consecutive executions. + * + * @return Time between consecutive executions (in ms). + */ + getInterval(): number { + return AddonModForumSync.instance.syncInterval; + } + +} + +export class AddonModForumSyncCronHandler extends makeSingleton(AddonModForumSyncCronHandlerService) {} diff --git a/src/addons/mod/forum/services/sync.service.ts b/src/addons/mod/forum/services/sync.service.ts index a7b2a564e..716e55b9b 100644 --- a/src/addons/mod/forum/services/sync.service.ts +++ b/src/addons/mod/forum/services/sync.service.ts @@ -25,7 +25,7 @@ import { CoreTextUtils } from '@services/utils/text'; import { CoreUtils } from '@services/utils/utils'; import { makeSingleton, Translate } from '@singletons'; import { CoreArray } from '@singletons/array'; -import { CoreEvents } from '@singletons/events'; +import { CoreEvents, CoreEventSiteData } from '@singletons/events'; import { AddonModForum, AddonModForumAddDiscussionPostWSOptionsObject, @@ -95,7 +95,7 @@ export class AddonModForumSyncProvider extends CoreSyncBaseProvider(AddonModForumSyncProvider.AUTO_SYNCED, { forumId: discussion.forumid, userId: discussion.userid, warnings: result.warnings, @@ -127,7 +127,7 @@ export class AddonModForumSyncProvider extends CoreSyncBaseProvider(AddonModForumSyncProvider.AUTO_SYNCED, { forumId: reply.forumid, discussionId: reply.discussionid, userId: reply.userid, @@ -619,3 +619,23 @@ export type AddonModForumSyncResult = { updated: boolean; warnings: string[]; }; + +/** + * Data passed to AUTO_SYNCED event. + */ +export type AddonModForumAutoSyncData = CoreEventSiteData & { + forumId: number; + userId: number; + warnings: string[]; + discussionId?: number; +}; + +/** + * Data passed to MANUAL_SYNCED event. + */ +export type AddonModForumManualSyncData = CoreEventSiteData & { + forumId: number; + userId: number; + source: string; + discussionId?: number; +}; diff --git a/src/core/classes/page-items-list-manager.ts b/src/core/classes/page-items-list-manager.ts index 5f5334b8b..c72a0257e 100644 --- a/src/core/classes/page-items-list-manager.ts +++ b/src/core/classes/page-items-list-manager.ts @@ -147,7 +147,10 @@ export abstract class CorePageItemsListManager { const params = this.getItemQueryParams(item); const pathPrefix = selectedItemPath ? selectedItemPath.split('/').fill('../').join('') : ''; - await CoreNavigator.instance.navigate(pathPrefix + itemPath, { params, reset: true }); + await CoreNavigator.instance.navigate(pathPrefix + itemPath, { + params, + reset: CoreScreen.instance.isTablet, + }); } /**