diff --git a/src/addons/mod/forum/components/components.module.ts b/src/addons/mod/forum/components/components.module.ts index 6e3dcd101..55da612ac 100644 --- a/src/addons/mod/forum/components/components.module.ts +++ b/src/addons/mod/forum/components/components.module.ts @@ -18,6 +18,7 @@ import { CoreCourseComponentsModule } from '@features/course/components/componen import { CoreEditorComponentsModule } from '@features/editor/components/components.module'; import { CoreSharedModule } from '@/core/shared.module'; import { CoreTagComponentsModule } from '@features/tag/components/components.module'; +import { CoreRatingComponentsModule } from '@features/rating/components/components.module'; import { AddonModForumDiscussionOptionsMenuComponent } from './discussion-options-menu/discussion-options-menu'; import { AddonModForumEditPostComponent } from './edit-post/edit-post'; @@ -40,6 +41,7 @@ import { AddonModForumSortOrderSelectorComponent } from './sort-order-selector/s CoreCourseComponentsModule, CoreTagComponentsModule, CoreEditorComponentsModule, + CoreRatingComponentsModule, ], exports: [ AddonModForumIndexComponent, diff --git a/src/addons/mod/forum/components/index/index.ts b/src/addons/mod/forum/components/index/index.ts index 3fca535d3..322df8f67 100644 --- a/src/addons/mod/forum/components/index/index.ts +++ b/src/addons/mod/forum/components/index/index.ts @@ -50,6 +50,10 @@ import { CoreScreen } from '@services/screen'; import { CoreArray } from '@singletons/array'; import { AddonModForumPrefetchHandler } from '../../services/handlers/prefetch'; import { AddonModForumModuleHandlerService } from '../../services/handlers/module'; +import { CoreRatingProvider } from '@features/rating/services/rating'; +import { CoreRatingSyncProvider } from '@features/rating/services/rating-sync'; +import { CoreRatingOffline } from '@features/rating/services/rating-offline'; +import { ContextLevel } from '@/core/constants'; /** * Component that displays a forum entry page. @@ -88,9 +92,9 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom protected viewDiscObserver?: CoreEventObserver; protected changeDiscObserver?: CoreEventObserver; - hasOfflineRatings?: boolean; - protected ratingOfflineObserver: any; - protected ratingSyncObserver: any; + hasOfflineRatings = false; + protected ratingOfflineObserver?: CoreEventObserver; + protected ratingSyncObserver?: CoreEventObserver; constructor( route: ActivatedRoute, @@ -166,7 +170,21 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom } }); - // @todo Listen for offline ratings saved and synced. + // Listen for offline ratings saved and synced. + this.ratingOfflineObserver = CoreEvents.on(CoreRatingProvider.RATING_SAVED_EVENT, (data) => { + if (this.forum && data.component == 'mod_forum' && data.ratingArea == 'post' && + data.contextLevel == ContextLevel.MODULE && data.instanceId == this.forum.cmid) { + this.hasOfflineRatings = true; + } + }); + + this.ratingSyncObserver = CoreEvents.on(CoreRatingSyncProvider.SYNCED_EVENT, async (data) => { + if (this.forum && data.component == 'mod_forum' && data.ratingArea == 'post' && + data.contextLevel == ContextLevel.MODULE && data.instanceId == this.forum.cmid) { + this.hasOfflineRatings = + await CoreRatingOffline.hasRatings('mod_forum', 'post', ContextLevel.MODULE, this.forum.cmid); + } + }); } async ngAfterViewInit(): Promise { @@ -224,7 +242,11 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom await Promise.all([ this.fetchOfflineDiscussions(), this.fetchDiscussions(refresh), - // @todo fetch hasOfflineRatings. + CoreRatingOffline.hasRatings('mod_forum', 'post', ContextLevel.MODULE, this.forum!.cmid).then((hasRatings) => { + this.hasOfflineRatings = hasRatings; + + return; + }), ]); } catch (error) { if (refresh) { diff --git a/src/addons/mod/forum/components/post/post.html b/src/addons/mod/forum/components/post/post.html index 6f48c3db6..b5a6a8b76 100644 --- a/src/addons/mod/forum/components/post/post.html +++ b/src/addons/mod/forum/components/post/post.html @@ -75,8 +75,7 @@ - - + diff --git a/src/addons/mod/forum/components/post/post.ts b/src/addons/mod/forum/components/post/post.ts index fa853d497..b87811f9c 100644 --- a/src/addons/mod/forum/components/post/post.ts +++ b/src/addons/mod/forum/components/post/post.ts @@ -52,6 +52,7 @@ import { AddonModForumOffline, AddonModForumReplyOptions } from '../../services/ import { CoreUtils } from '@services/utils/utils'; import { AddonModForumPostOptionsMenuComponent } from '../post-options-menu/post-options-menu'; import { AddonModForumEditPostComponent } from '../edit-post/edit-post'; +import { CoreRatingInfo } from '@features/rating/services/rating'; /** * Components that shows a discussion post, its attachments and the action buttons allowed (reply, etc.). @@ -75,7 +76,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges @Input() forum!: AddonModForumData; // The forum the post belongs to. Required for attachments and offline posts. @Input() accessInfo!: AddonModForumAccessInformation; // Forum access information. @Input() parentSubject?: string; // Subject of parent post. - @Input() ratingInfo?: any; // TODO CoreRatingInfo; // Rating info item. + @Input() ratingInfo?: CoreRatingInfo; // Rating info item. @Input() leavingPage?: boolean; // Whether the page that contains this post is being left and will be destroyed. @Input() highlight = false; @Output() onPostChange: EventEmitter = new EventEmitter(); // Event emitted when a reply is posted or modified. diff --git a/src/addons/mod/forum/pages/discussion/discussion.page.ts b/src/addons/mod/forum/pages/discussion/discussion.page.ts index 6b10eb4df..9cfa80c3a 100644 --- a/src/addons/mod/forum/pages/discussion/discussion.page.ts +++ b/src/addons/mod/forum/pages/discussion/discussion.page.ts @@ -12,9 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { ContextLevel } from '@/core/constants'; import { Component, OnDestroy, ViewChild, OnInit, AfterViewInit, ElementRef, Optional } from '@angular/core'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { CoreFileUploader } from '@features/fileuploader/services/fileuploader'; +import { CoreRatingInfo, CoreRatingProvider } from '@features/rating/services/rating'; +import { CoreRatingOffline } from '@features/rating/services/rating-offline'; +import { CoreRatingSyncProvider } from '@features/rating/services/rating-sync'; import { CoreUser } from '@features/user/services/user'; import { CanLeave } from '@guards/can-leave'; import { IonContent } from '@ionic/angular'; @@ -34,7 +38,6 @@ import { AddonModForumDiscussion, AddonModForumPost, AddonModForumProvider, - AddonModForumRatingInfo, } from '../../services/forum'; import { AddonModForumHelper } from '../../services/helper'; import { AddonModForumOffline } from '../../services/offline'; @@ -101,8 +104,8 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes protected syncObserver?: CoreEventObserver; protected syncManualObserver?: CoreEventObserver; - ratingInfo?: AddonModForumRatingInfo; - hasOfflineRatings!: boolean; + ratingInfo?: CoreRatingInfo; + hasOfflineRatings = false; protected ratingOfflineObserver?: CoreEventObserver; protected ratingSyncObserver?: CoreEventObserver; protected changeDiscObserver?: CoreEventObserver; @@ -203,7 +206,20 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes AddonModForum.invalidateDiscussionsList(this.forumId); } - // @todo Listen for offline ratings saved and synced. + // Listen for offline ratings saved and synced. + this.ratingOfflineObserver = CoreEvents.on(CoreRatingProvider.RATING_SAVED_EVENT, (data) => { + if (data.component == 'mod_forum' && data.ratingArea == 'post' && data.contextLevel == ContextLevel.MODULE && + data.instanceId == this.cmId && data.itemSetId == this.discussionId) { + this.hasOfflineRatings = true; + } + }); + + this.ratingSyncObserver = CoreEvents.on(CoreRatingSyncProvider.SYNCED_EVENT, async (data) => { + if (data.component == 'mod_forum' && data.ratingArea == 'post' && data.contextLevel == ContextLevel.MODULE && + data.instanceId == this.cmId && data.itemSetId == this.discussionId) { + this.hasOfflineRatings = false; + } + }); this.changeDiscObserver = CoreEvents.on(AddonModForumProvider.CHANGE_DISCUSSION_EVENT, data => { if ((this.forumId && this.forumId === data.forumId) || data.cmId === this.cmId) { @@ -345,7 +361,8 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes const response = await AddonModForum.getDiscussionPosts(this.discussionId, { cmId: this.cmId }); const replies = await AddonModForumOffline.getDiscussionReplies(this.discussionId); - const ratingInfo = response.ratinginfo; + this.ratingInfo = response.ratinginfo; + onlinePosts = response.posts; this.courseId = response.courseid || this.courseId; this.forumId = response.forumid || this.forumId; @@ -462,7 +479,6 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes } this.posts = posts; - this.ratingInfo = ratingInfo; this.postSubjects = this.getAllPosts().reduce( (postSubjects, post) => { postSubjects[post.id] = post.subject; @@ -487,7 +503,8 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes this.canPin = false; } - // @todo fetch hasOfflineRatings. + this.hasOfflineRatings = + await CoreRatingOffline.hasRatings('mod_forum', 'post', ContextLevel.MODULE, this.cmId, this.discussionId); } catch (error) { CoreDomUtils.showErrorModal(error); } finally { diff --git a/src/addons/mod/forum/services/forum.ts b/src/addons/mod/forum/services/forum.ts index 87dd1bd02..9e46db2e9 100644 --- a/src/addons/mod/forum/services/forum.ts +++ b/src/addons/mod/forum/services/forum.ts @@ -17,6 +17,7 @@ import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; import { CoreCourseCommonModWSOptions } from '@features/course/services/course'; import { CoreCourseLogHelper } from '@features/course/services/log-helper'; import { CoreFileEntry } from '@features/fileuploader/services/fileuploader'; +import { CoreRatingInfo } from '@features/rating/services/rating'; import { CoreUser } from '@features/user/services/user'; import { CoreApp } from '@services/app'; import { CoreFilepool } from '@services/filepool'; @@ -553,7 +554,7 @@ export class AddonModForumProvider { posts: AddonModForumPost[]; courseid?: number; forumid?: number; - ratinginfo?: AddonModForumRatingInfo; + ratinginfo?: CoreRatingInfo; }> { // Convenience function to translate legacy data to new format. const translateLegacyPostsFormat = (posts: AddonModForumLegacyPost[]): AddonModForumPost[] => posts.map((post) => { @@ -1526,40 +1527,6 @@ export type AddonModForumLegacyPost = { }[]; }; -/** - * Forum rating info. - */ -export type AddonModForumRatingInfo = { - contextid: number; // Context id. - component: string; // Context name. - ratingarea: string; // Rating area name. - canviewall?: boolean; // Whether the user can view all the individual ratings. - canviewany?: boolean; // Whether the user can view aggregate of ratings of others. - scales?: { // Different scales used information. - id: number; // Scale id. - courseid?: number; // Course id. - name?: string; // Scale name (when a real scale is used). - max: number; // Max value for the scale. - isnumeric: boolean; // Whether is a numeric scale. - items?: { // Scale items. Only returned for not numerical scales. - value: number; // Scale value/option id. - name: string; // Scale name. - }[]; - }[]; - ratings?: { // The ratings. - itemid: number; // Item id. - scaleid?: number; // Scale id. - userid?: number; // User who rated id. - aggregate?: number; // Aggregated ratings grade. - aggregatestr?: string; // Aggregated ratings as string. - aggregatelabel?: string; // The aggregation label. - count?: number; // Ratings count (used when aggregating). - rating?: number; // The rating the user gave. - canrate?: boolean; // Whether the user can rate the item. - canviewaggregate?: boolean; // Whether the user can view the aggregated grade. - }[]; -}; - /** * Options to pass to get discussions. */ @@ -1994,7 +1961,7 @@ export type AddonModForumGetDiscussionPostsWSResponse = { posts: AddonModForumWSPost[]; forumid: number; // The forum id. courseid: number; // The forum course id. - ratinginfo?: AddonModForumRatingInfo; // Rating information. + ratinginfo?: CoreRatingInfo; // Rating information. warnings?: CoreWSExternalWarning[]; }; @@ -2012,7 +1979,7 @@ export type AddonModForumGetForumDiscussionPostsWSParams = { */ export type AddonModForumGetForumDiscussionPostsWSResponse = { posts: AddonModForumLegacyPost[]; - ratinginfo?: AddonModForumRatingInfo; // Rating information. + ratinginfo?: CoreRatingInfo; // Rating information. warnings?: CoreWSExternalWarning[]; }; diff --git a/src/addons/mod/forum/services/sync.ts b/src/addons/mod/forum/services/sync.ts index 863360c3f..17d26ce26 100644 --- a/src/addons/mod/forum/services/sync.ts +++ b/src/addons/mod/forum/services/sync.ts @@ -12,11 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { ContextLevel } from '@/core/constants'; import { Injectable } from '@angular/core'; import { CoreSyncBaseProvider } from '@classes/base-sync'; import { CoreCourse } from '@features/course/services/course'; import { CoreCourseLogHelper } from '@features/course/services/log-helper'; import { CoreFileUploader } from '@features/fileuploader/services/fileuploader'; +import { CoreRatingSync } from '@features/rating/services/rating-sync'; import { CoreApp } from '@services/app'; import { CoreGroups } from '@services/groups'; import { CoreSites } from '@services/sites'; @@ -327,14 +329,44 @@ export class AddonModForumSyncProvider extends CoreSyncBaseProvider { - // @todo + async syncRatings(cmId?: number, discussionId?: number, force?: boolean, siteId?: string): Promise { + siteId = siteId || CoreSites.getCurrentSiteId(); - return { updated: true, warnings: [] }; + const results = + await CoreRatingSync.syncRatings('mod_forum', 'post', ContextLevel.MODULE, cmId, discussionId, force, siteId); + + let updated = false; + const warnings: string[] = []; + const promises: Promise[] = []; + + results.forEach((result) => { + if (result.updated.length) { + updated = true; + + // Invalidate discussions of updated ratings. + promises.push(AddonModForum.invalidateDiscussionPosts(result.itemSet!.itemSetId, undefined, siteId)); + } + + if (result.warnings.length) { + // Fetch forum to construct the warning message. + promises.push(AddonModForum.getForum(result.itemSet!.courseId!, result.itemSet!.instanceId, { siteId }) + .then((forum) => { + result.warnings.forEach((warning) => { + warnings.push(Translate.instant('core.warningofflinedatadeleted', { + component: this.componentTranslate, + name: forum.name, + error: warning, + })); + }); + + return; + })); + } + }); + + await CoreUtils.allPromises(promises); + + return { updated, warnings }; } /**