From 651461b01e7640ab0dd6fbe3a7a4a1c8fedceda9 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Wed, 1 Dec 2021 13:59:19 +0100 Subject: [PATCH] MOBILE-3926 forum: Discussions swipe navigation --- .../forum/classes/forum-discussions-source.ts | 265 +++++++++++ .../forum-discussions-swipe-manager.ts | 52 ++ .../mod/forum/components/index/index.html | 24 +- .../mod/forum/components/index/index.ts | 445 +++++++----------- src/addons/mod/forum/forum.module.ts | 5 + .../forum/pages/discussion/discussion.html | 132 +++--- .../forum/pages/discussion/discussion.page.ts | 34 +- .../pages/new-discussion/new-discussion.html | 137 +++--- .../new-discussion/new-discussion.page.ts | 36 +- .../mod/glossary/components/index/index.ts | 4 +- .../items-management/items-manager-source.ts | 27 +- .../items-management/list-items-manager.ts | 6 +- .../items-management/swipe-items-manager.ts | 2 +- .../pages/participants/participants.page.ts | 2 +- 14 files changed, 729 insertions(+), 442 deletions(-) create mode 100644 src/addons/mod/forum/classes/forum-discussions-source.ts create mode 100644 src/addons/mod/forum/classes/forum-discussions-swipe-manager.ts diff --git a/src/addons/mod/forum/classes/forum-discussions-source.ts b/src/addons/mod/forum/classes/forum-discussions-source.ts new file mode 100644 index 000000000..7a08b4e8d --- /dev/null +++ b/src/addons/mod/forum/classes/forum-discussions-source.ts @@ -0,0 +1,265 @@ +// (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 { Params } from '@angular/router'; +import { CoreItemsManagerSource } from '@classes/items-management/items-manager-source'; +import { CoreUser } from '@features/user/services/user'; +import { + AddonModForum, + AddonModForumData, + AddonModForumDiscussion, + AddonModForumProvider, + AddonModForumSortOrder, +} from '../services/forum'; +import { AddonModForumOffline, AddonModForumOfflineDiscussion } from '../services/forum-offline'; + +export class AddonModForumDiscussionsSource extends CoreItemsManagerSource { + + static readonly NEW_DISCUSSION: AddonModForumNewDiscussionForm = { newDiscussion: true }; + + readonly DISCUSSIONS_PATH_PREFIX: string; + readonly COURSE_ID: number; + readonly CM_ID: number; + + forum?: AddonModForumData; + trackPosts = false; + usesGroups = false; + selectedSortOrder: AddonModForumSortOrder | null = null; + + constructor(courseId: number, cmId: number, discussionsPathPrefix: string) { + super(); + + this.DISCUSSIONS_PATH_PREFIX = discussionsPathPrefix; + this.COURSE_ID = courseId; + this.CM_ID = cmId; + } + + /** + * Type guard to infer NewDiscussionForm objects. + * + * @param discussion Item to check. + * @return Whether the item is a new discussion form. + */ + isNewDiscussionForm(discussion: AddonModForumDiscussionItem): discussion is AddonModForumNewDiscussionForm { + return 'newDiscussion' in discussion; + } + + /** + * Type guard to infer AddonModForumDiscussion objects. + * + * @param discussion Item to check. + * @return Whether the item is an online discussion. + */ + isOfflineDiscussion(discussion: AddonModForumDiscussionItem): discussion is AddonModForumOfflineDiscussion { + return !this.isNewDiscussionForm(discussion) && !this.isOnlineDiscussion(discussion); + } + + /** + * Type guard to infer AddonModForumDiscussion objects. + * + * @param discussion Item to check. + * @return Whether the item is an online discussion. + */ + isOnlineDiscussion(discussion: AddonModForumDiscussionItem): discussion is AddonModForumDiscussion { + return 'id' in discussion; + } + + /** + * @inheritdoc + */ + getItemPath(discussion: AddonModForumDiscussionItem): string { + if (this.isOnlineDiscussion(discussion)) { + return this.DISCUSSIONS_PATH_PREFIX + discussion.discussion; + } + + if (this.isOfflineDiscussion(discussion)) { + return `${this.DISCUSSIONS_PATH_PREFIX}new/${discussion.timecreated}`; + } + + return `${this.DISCUSSIONS_PATH_PREFIX}new/0`; + } + + /** + * @inheritdoc + */ + getItemQueryParams(discussion: AddonModForumDiscussionItem): Params { + return { + courseId: this.COURSE_ID, + cmId: this.CM_ID, + forumId: this.forum?.id, + ...(this.isOnlineDiscussion(discussion) ? { discussion, trackPosts: this.trackPosts } : {}), + }; + } + + /** + * @inheritdoc + */ + getPagesLoaded(): number { + if (this.items === null) { + return 0; + } + + const onlineEntries = this.items.filter(item => this.isOnlineDiscussion(item)); + + return Math.ceil(onlineEntries.length / this.getPageLength()); + } + + /** + * @inheritdoc + */ + getPageLength(): number { + return AddonModForumProvider.DISCUSSIONS_PER_PAGE; + } + + /** + * Load forum. + */ + async loadForum(): Promise { + this.forum = await AddonModForum.getForum(this.COURSE_ID, this.CM_ID); + + if (typeof this.forum.istracked != 'undefined') { + this.trackPosts = this.forum.istracked; + } + } + + /** + * @inheritdoc + */ + protected async loadPageItems(page: number): Promise<{ items: AddonModForumDiscussionItem[]; hasMoreItems: boolean }> { + const discussions: AddonModForumDiscussionItem[] = []; + + if (page === 0) { + const offlineDiscussions = await this.loadOfflineDiscussions(); + + discussions.push(AddonModForumDiscussionsSource.NEW_DISCUSSION); + discussions.push(...offlineDiscussions); + } + + const { discussions: onlineDiscussions, canLoadMore } = await this.loadOnlineDiscussions(page); + + discussions.push(...onlineDiscussions); + + return { + items: discussions, + hasMoreItems: canLoadMore, + }; + } + + /** + * Load online discussions for the given page. + * + * @param page Page. + * @returns Online discussions info. + */ + private async loadOnlineDiscussions(page: number): Promise<{ + discussions: AddonModForumDiscussionItem[]; + canLoadMore: boolean; + }> { + if (!this.forum || !this.selectedSortOrder) { + throw new Error('Can\'t load discussions without a forum or selected sort order'); + } + + const response = await AddonModForum.getDiscussions(this.forum.id, { + cmId: this.forum.cmid, + sortOrder: this.selectedSortOrder.value, + page, + }); + let discussions = response.discussions; + + if (this.usesGroups) { + discussions = await AddonModForum.formatDiscussionsGroups(this.forum.cmid, discussions); + } + + // Hide author for first post and type single. + if (this.forum.type === 'single') { + for (const discussion of discussions) { + if (discussion.userfullname && discussion.parent === 0) { + discussion.userfullname = false; + break; + } + } + } + + // If any discussion has unread posts, the whole forum is being tracked. + if (typeof this.forum.istracked === 'undefined' && !this.trackPosts) { + for (const discussion of discussions) { + if (discussion.numunread > 0) { + this.trackPosts = true; + break; + } + } + } + + return { discussions, canLoadMore: response.canLoadMore }; + } + + /** + * Load offline discussions. + * + * @returns Offline discussions. + */ + private async loadOfflineDiscussions(): Promise { + if (!this.forum) { + throw new Error('Can\'t load discussions without a forum'); + } + + const forum = this.forum; + let offlineDiscussions = await AddonModForumOffline.getNewDiscussions(forum.id); + + if (offlineDiscussions.length === 0) { + return []; + } + + if (this.usesGroups) { + offlineDiscussions = await AddonModForum.formatDiscussionsGroups(forum.cmid, offlineDiscussions); + } + + // Fill user data for Offline discussions (should be already cached). + const promises = offlineDiscussions.map(async (offlineDiscussion) => { + const discussion = offlineDiscussion as unknown as AddonModForumDiscussion; + + if (discussion.parent === 0 || forum.type === 'single') { + // Do not show author for first post and type single. + return; + } + + try { + const user = await CoreUser.getProfile(discussion.userid, this.COURSE_ID, true); + + discussion.userfullname = user.fullname; + discussion.userpictureurl = user.profileimageurl; + } catch (error) { + // Ignore errors. + } + }); + + await Promise.all(promises); + + // Sort discussion by time (newer first). + offlineDiscussions.sort((a, b) => b.timecreated - a.timecreated); + + return offlineDiscussions; + } + +} + +/** + * Type to select the new discussion form. + */ +export type AddonModForumNewDiscussionForm = { newDiscussion: true }; + +/** + * Type of items that can be held by the discussions manager. + */ +export type AddonModForumDiscussionItem = AddonModForumDiscussion | AddonModForumOfflineDiscussion | AddonModForumNewDiscussionForm; diff --git a/src/addons/mod/forum/classes/forum-discussions-swipe-manager.ts b/src/addons/mod/forum/classes/forum-discussions-swipe-manager.ts new file mode 100644 index 000000000..d409a6f58 --- /dev/null +++ b/src/addons/mod/forum/classes/forum-discussions-swipe-manager.ts @@ -0,0 +1,52 @@ +// (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 { CoreSwipeItemsManager } from '@classes/items-management/swipe-items-manager'; +import { AddonModForumDiscussionItem, AddonModForumDiscussionsSource } from './forum-discussions-source'; + +/** + * Helper to manage swiping within a collection of discussions. + */ +export class AddonModForumDiscussionsSwipeManager + extends CoreSwipeItemsManager { + + /** + * @inheritdoc + */ + async navigateToNextItem(): Promise { + let delta = -1; + const item = await this.getItemBy(-1); + + if (item && this.getSource().isNewDiscussionForm(item)) { + delta--; + } + + await this.navigateToItemBy(delta, 'back'); + } + + /** + * @inheritdoc + */ + async navigateToPreviousItem(): Promise { + let delta = 1; + const item = await this.getItemBy(1); + + if (item && this.getSource().isNewDiscussionForm(item)) { + delta++; + } + + await this.navigateToItemBy(delta, 'forward'); + } + +} diff --git a/src/addons/mod/forum/components/index/index.html b/src/addons/mod/forum/components/index/index.html index e61fce1ef..d282b828e 100644 --- a/src/addons/mod/forum/components/index/index.html +++ b/src/addons/mod/forum/components/index/index.html @@ -10,11 +10,11 @@ - - @@ -32,11 +32,11 @@ - + - + - + -
+
- @@ -96,17 +97,16 @@ {{ discussion.groupname }}

-

+

{{discussion.created * 1000 | coreFormatDate: "strftimerecentfull"}}

-

+

{{ 'core.notsent' | translate }}

- + {{ 'addon.mod_forum.lastpost' | translate }} @@ -134,7 +134,7 @@ -
diff --git a/src/addons/mod/forum/components/index/index.ts b/src/addons/mod/forum/components/index/index.ts index 2312f7ff4..e7a778d3b 100644 --- a/src/addons/mod/forum/components/index/index.ts +++ b/src/addons/mod/forum/components/index/index.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Component, Optional, OnInit, OnDestroy, ViewChild, AfterViewInit } from '@angular/core'; -import { ActivatedRoute, Params } from '@angular/router'; +import { ActivatedRoute } from '@angular/router'; import { IonContent } from '@ionic/angular'; import { ModalOptions } from '@ionic/core'; @@ -27,7 +27,7 @@ import { AddonModForumNewDiscussionData, AddonModForumReplyDiscussionData, } from '@addons/mod/forum/services/forum'; -import { AddonModForumOffline, AddonModForumOfflineDiscussion } from '@addons/mod/forum/services/forum-offline'; +import { AddonModForumOffline } from '@addons/mod/forum/services/forum-offline'; import { Translate } from '@singletons'; import { CoreCourseContentsPage } from '@features/course/pages/contents/contents'; import { AddonModForumHelper } from '@addons/mod/forum/services/forum-helper'; @@ -44,7 +44,6 @@ import { CoreUser } from '@features/user/services/user'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; import { CoreCourse } from '@features/course/services/course'; -import { CorePageItemsListManager } from '@classes/page-items-list-manager'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { AddonModForumDiscussionOptionsMenuComponent } from '../discussion-options-menu/discussion-options-menu'; import { AddonModForumSortOrderSelectorComponent } from '../sort-order-selector/sort-order-selector'; @@ -56,6 +55,9 @@ 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'; +import { AddonModForumDiscussionItem, AddonModForumDiscussionsSource } from '../../classes/forum-discussions-source'; +import { CoreListItemsManager } from '@classes/items-management/list-items-manager'; +import { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker'; /** * Component that displays a forum entry page. @@ -72,24 +74,21 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom component = AddonModForumProvider.COMPONENT; moduleName = 'forum'; descriptionNote?: string; - forum?: AddonModForumData; - discussions: AddonModForumDiscussionsManager; + discussions!: AddonModForumDiscussionsManager; + discussionsItems: AddonModForumDiscussionItem[] = []; + fetchFailed = false; canAddDiscussion = false; addDiscussionText!: string; availabilityMessage: string | null = null; sortingAvailable!: boolean; sortOrders: AddonModForumSortOrder[] = []; - selectedSortOrder: AddonModForumSortOrder | null = null; canPin = false; - trackPosts = false; hasOfflineRatings = false; sortOrderSelectorModalOptions: ModalOptions = { component: AddonModForumSortOrderSelectorComponent, }; protected syncEventName = AddonModForumSyncProvider.AUTO_SYNCED; - protected page = 0; - protected usesGroups = false; protected syncManualObserver?: CoreEventObserver; // It will observe the sync manual event. protected replyObserver?: CoreEventObserver; protected newDiscObserver?: CoreEventObserver; @@ -97,19 +96,42 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom protected changeDiscObserver?: CoreEventObserver; protected ratingOfflineObserver?: CoreEventObserver; protected ratingSyncObserver?: CoreEventObserver; + protected sourceUnsubscribe?: () => void; constructor( - route: ActivatedRoute, + public route: ActivatedRoute, @Optional() protected content?: IonContent, @Optional() courseContentsPage?: CoreCourseContentsPage, ) { super('AddonModForumIndexComponent', content, courseContentsPage); + } - this.discussions = new AddonModForumDiscussionsManager( - route.component, - this, - courseContentsPage ? `${AddonModForumModuleHandlerService.PAGE_NAME}/` : '', - ); + get forum(): AddonModForumData | undefined { + return this.discussions?.getSource().forum; + } + + get selectedSortOrder(): AddonModForumSortOrder | undefined { + return this.discussions?.getSource().selectedSortOrder ?? undefined; + } + + /** + * Check whether a discussion is online. + * + * @param discussion Discussion + * @return Whether the discussion is online. + */ + isOnlineDiscussion(discussion: AddonModForumDiscussionItem): boolean { + return this.discussions && this.discussions.getSource().isOnlineDiscussion(discussion); + } + + /** + * Check whether a discussion is offline. + * + * @param discussion Discussion + * @return Whether the discussion is offline. + */ + isOfflineDiscussion(discussion: AddonModForumDiscussionItem): boolean { + return this.discussions && this.discussions.getSource().isOfflineDiscussion(discussion); } /** @@ -126,6 +148,48 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom await super.ngOnInit(); + // Initialize discussions manager. + const source = CoreItemsManagerSourcesTracker.getOrCreateSource( + AddonModForumDiscussionsSource, + [this.courseId, this.module.id, this.courseContentsPage ? `${AddonModForumModuleHandlerService.PAGE_NAME}/` : ''], + ); + + this.sourceUnsubscribe = source.addListener({ + onItemsUpdated: async discussions => { + this.discussionsItems = discussions.filter(discussion => !source.isNewDiscussionForm(discussion)); + + if (!this.forum) { + return; + } + + // Check if there are replies for discussions stored in offline. + const hasOffline = await AddonModForumOffline.hasForumReplies(this.forum.id); + + this.hasOffline = this.hasOffline || hasOffline; + + if (hasOffline) { + // Only update new fetched discussions. + const promises = discussions.map(async (discussion) => { + if (!this.discussions.getSource().isOnlineDiscussion(discussion)) { + return; + } + + // Get offline discussions. + const replies = await AddonModForumOffline.getDiscussionReplies(discussion.discussion); + + discussion.numreplies = Number(discussion.numreplies) + replies.length; + }); + + await Promise.all(promises); + } + }, + onReset: () => { + this.discussionsItems = []; + }, + }); + + this.discussions = new AddonModForumDiscussionsManager(source, this); + // Refresh data if this forum discussion is synchronized from discussions list. this.syncManualObserver = CoreEvents.on(AddonModForumSyncProvider.MANUAL_SYNCED, (data) => { this.autoSyncEventReceived(data); @@ -141,12 +205,16 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom this.eventReceived.bind(this, false), ); this.changeDiscObserver = CoreEvents.on(AddonModForumProvider.CHANGE_DISCUSSION_EVENT, data => { - if ((this.forum && this.forum.id === data.forumId) || data.cmId === this.module.id) { - AddonModForum.invalidateDiscussionsList(this.forum!.id).finally(() => { + if (!this.forum) { + return; + } + + if (this.forum.id === data.forumId || data.cmId === this.module.id) { + AddonModForum.invalidateDiscussionsList(this.forum.id).finally(() => { if (data.discussionId) { // Discussion changed, search it in the list of discussions. const discussion = this.discussions.items.find( - (disc) => this.discussions.isOnlineDiscussion(disc) && data.discussionId == disc.discussion, + (disc) => this.discussions.getSource().isOnlineDiscussion(disc) && data.discussionId == disc.discussion, ) as AddonModForumDiscussion; if (discussion) { @@ -196,20 +264,6 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom async ngAfterViewInit(): Promise { await this.loadContent(false, true); - if (!this.forum) { - return; - } - - CoreUtils.ignoreErrors( - AddonModForum.instance - .logView(this.forum.id, this.forum.name) - .then(async () => { - CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata); - - return; - }), - ); - this.discussions.start(this.splitView); } @@ -226,6 +280,8 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom this.changeDiscObserver && this.changeDiscObserver.off(); this.ratingOfflineObserver && this.ratingOfflineObserver.off(); this.ratingSyncObserver && this.ratingSyncObserver.off(); + this.sourceUnsubscribe && this.sourceUnsubscribe(); + this.discussions.destroy(); } /** @@ -236,19 +292,21 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom * @param showErrors Wether to show errors to the user or hide them. */ protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise { - this.discussions.fetchFailed = false; - - const promises: Promise[] = []; - - promises.push(this.fetchForum(sync, showErrors)); - promises.push(this.fetchSortOrderPreference()); + this.fetchFailed = false; try { - await Promise.all(promises); await Promise.all([ - this.fetchOfflineDiscussions(), - this.fetchDiscussions(refresh), - CoreRatingOffline.hasRatings('mod_forum', 'post', ContextLevel.MODULE, this.forum!.cmid).then((hasRatings) => { + this.fetchForum(sync, showErrors), + this.fetchSortOrderPreference(), + ]); + + if (!this.forum) { + return; + } + + await Promise.all([ + refresh ? this.discussions.reload() : this.discussions.load(), + CoreRatingOffline.hasRatings('mod_forum', 'post', ContextLevel.MODULE, this.forum.cmid).then((hasRatings) => { this.hasOfflineRatings = hasRatings; return; @@ -258,7 +316,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom if (refresh) { CoreDomUtils.showErrorModalDefault(error, 'addon.mod_forum.errorgetforum', true); - this.discussions.fetchFailed = true; // Set to prevent infinite calls with infinite-loading. + this.fetchFailed = 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); @@ -273,19 +331,19 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom return; } - const forum = await AddonModForum.getForum(this.courseId, this.module.id); + await this.discussions.getSource().loadForum(); - this.forum = forum; + if (!this.forum) { + return; + } + + const forum = this.forum; this.description = forum.intro || this.description; this.availabilityMessage = AddonModForumHelper.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) { @@ -319,10 +377,10 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom // Check if the activity uses groups. promises.push( CoreGroups.instance - .getActivityGroupMode(this.forum.cmid) + .getActivityGroupMode(forum.cmid) .then(async mode => { - this.usesGroups = mode === CoreGroupsProvider.SEPARATEGROUPS - || mode === CoreGroupsProvider.VISIBLEGROUPS; + this.discussions.getSource().usesGroups = + mode === CoreGroupsProvider.SEPARATEGROUPS || mode === CoreGroupsProvider.VISIBLEGROUPS; return; }), @@ -330,14 +388,14 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom promises.push( AddonModForum.instance - .getAccessInformation(this.forum.id, { cmId: this.module.id }) + .getAccessInformation(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.isCutoffDateReached(this.forum!) + const cutoffDateReached = AddonModForumHelper.isCutoffDateReached(forum) && !accessInfo.cancanoverridecutoff; - this.canAddDiscussion = !!this.forum?.cancreatediscussions && !cutoffDateReached; + this.canAddDiscussion = !!forum.cancreatediscussions && !cutoffDateReached; return; }), @@ -347,7 +405,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom // Use the canAddDiscussion WS to check if the user can pin discussions. promises.push( AddonModForum.instance - .canAddDiscussionToAll(this.forum.id, { cmId: this.module.id }) + .canAddDiscussionToAll(forum.id, { cmId: this.module.id }) .then(async response => { this.canPin = !!response.canpindiscussions; @@ -366,124 +424,6 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom await Promise.all(promises); } - /** - * Convenience function to fetch offline discussions. - * - * @return Promise resolved when done. - */ - protected async fetchOfflineDiscussions(): Promise { - const forum = this.forum!; - let offlineDiscussions = await AddonModForumOffline.getNewDiscussions(forum.id); - this.hasOffline = !!offlineDiscussions.length; - - if (!this.hasOffline) { - this.discussions.setOfflineDiscussions([]); - - return; - } - - if (this.usesGroups) { - offlineDiscussions = await AddonModForum.formatDiscussionsGroups(forum.cmid, offlineDiscussions); - } - - // Fill user data for Offline discussions (should be already cached). - const promises = offlineDiscussions.map(async (offlineDiscussion) => { - const discussion = offlineDiscussion as unknown as AddonModForumDiscussion; - - if (discussion.parent === 0 || forum.type === 'single') { - // Do not show author for first post and type single. - return; - } - - try { - const user = await CoreUser.getProfile(discussion.userid, this.courseId, true); - - discussion.userfullname = user.fullname; - discussion.userpictureurl = user.profileimageurl; - } catch (error) { - // Ignore errors. - } - }); - - await Promise.all(promises); - - // Sort discussion by time (newer first). - offlineDiscussions.sort((a, b) => b.timecreated - a.timecreated); - - this.discussions.setOfflineDiscussions(offlineDiscussions); - } - - /** - * Convenience function to get forum discussions. - * - * @param refresh Whether we're refreshing data. - * @return Promise resolved when done. - */ - protected async fetchDiscussions(refresh: boolean): Promise { - const forum = this.forum!; - this.discussions.fetchFailed = false; - - if (refresh) { - this.page = 0; - } - - const response = await AddonModForum.getDiscussions(forum.id, { - cmId: forum.cmid, - sortOrder: this.selectedSortOrder!.value, - page: this.page, - }); - let discussions = response.discussions; - - if (this.usesGroups) { - discussions = await AddonModForum.formatDiscussionsGroups(forum.cmid, discussions); - } - - // Hide author for first post and type single. - if (forum.type === 'single') { - for (const discussion of discussions) { - if (discussion.userfullname && discussion.parent === 0) { - discussion.userfullname = false; - break; - } - } - } - - // If any discussion has unread posts, the whole forum is being tracked. - if (typeof forum.istracked === 'undefined' && !this.trackPosts) { - for (const discussion of discussions) { - if (discussion.numunread > 0) { - this.trackPosts = true; - break; - } - } - } - - if (this.page === 0) { - this.discussions.setOnlineDiscussions(discussions, response.canLoadMore); - } else { - this.discussions.setItems(this.discussions.items.concat(discussions), response.canLoadMore); - } - - this.page++; - - // Check if there are replies for discussions stored in offline. - const hasOffline = await AddonModForumOffline.hasForumReplies(forum.id); - - this.hasOffline = this.hasOffline || hasOffline; - - if (hasOffline) { - // Only update new fetched discussions. - const promises = discussions.map(async (discussion) => { - // Get offline discussions. - const replies = await AddonModForumOffline.getDiscussionReplies(discussion.discussion); - - discussion.numreplies = Number(discussion.numreplies) + replies.length; - }); - - await Promise.all(promises); - } - } - /** * Convenience function to load more forum discussions. * @@ -492,11 +432,13 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom */ async fetchMoreDiscussions(complete: () => void): Promise { try { - await this.fetchDiscussions(false); + this.fetchFailed = false; + + await this.discussions.load(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.mod_forum.errorgetforum', true); - this.discussions.fetchFailed = true; + this.fetchFailed = true; } finally { complete(); } @@ -521,9 +463,13 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom }; const value = await getSortOrder(); + const selectedOrder = this.sortOrders.find(sortOrder => sortOrder.value === value) || this.sortOrders[0]; - this.selectedSortOrder = this.sortOrders.find(sortOrder => sortOrder.value === value) || this.sortOrders[0]; - this.sortOrderSelectorModalOptions.componentProps!.selected = this.selectedSortOrder.value; + this.discussions.getSource().selectedSortOrder = selectedOrder; + + if (this.sortOrderSelectorModalOptions.componentProps) { + this.sortOrderSelectorModalOptions.componentProps.selected = selectedOrder.value; + } } /** @@ -597,11 +543,11 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom if (isNewDiscussion && CoreScreen.isTablet) { const newDiscussionData = data as AddonModForumNewDiscussionData; const discussion = this.discussions.items.find(disc => { - if (this.discussions.isOfflineDiscussion(disc)) { + if (this.discussions.getSource().isOfflineDiscussion(disc)) { return disc.timecreated === newDiscussionData.discTimecreated; } - if (this.discussions.isOnlineDiscussion(disc)) { + if (this.discussions.getSource().isOnlineDiscussion(disc)) { return CoreArray.contains(newDiscussionData.discussionIds ?? [], disc.discussion); } @@ -625,7 +571,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom * @param timeCreated Creation time of the offline discussion. */ openNewDiscussion(): void { - this.discussions.select({ newDiscussion: true }); + this.discussions.select(AddonModForumDiscussionsSource.NEW_DISCUSSION); } /** @@ -634,10 +580,13 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom * @param sortOrder Sort order new data. */ async setSortOrder(sortOrder: AddonModForumSortOrder): Promise { - if (sortOrder.value != this.selectedSortOrder?.value) { - this.selectedSortOrder = sortOrder; - this.sortOrderSelectorModalOptions.componentProps!.selected = this.selectedSortOrder.value; - this.page = 0; + if (sortOrder.value != this.discussions.getSource().selectedSortOrder?.value) { + this.discussions.getSource().selectedSortOrder = sortOrder; + this.discussions.getSource().setDirty(true); + + if (this.sortOrderSelectorModalOptions.componentProps) { + this.sortOrderSelectorModalOptions.componentProps.selected = sortOrder.value; + } try { await CoreUser.setUserPreference(AddonModForumProvider.PREFERENCE_SORTORDER, sortOrder.value.toFixed(0)); @@ -666,6 +615,10 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom * @param discussion Discussion. */ async showOptionsMenu(event: Event, discussion: AddonModForumDiscussion): Promise { + if (!this.forum) { + return; + } + event.preventDefault(); event.stopPropagation(); @@ -673,7 +626,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom component: AddonModForumDiscussionOptionsMenuComponent, componentProps: { discussion, - forumId: this.forum!.id, + forumId: this.forum.id, cmId: this.module.id, }, event, @@ -698,125 +651,47 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom } -/** - * Type to select the new discussion form. - */ -type NewDiscussionForm = { newDiscussion: true }; - -/** - * Type of items that can be held by the discussions manager. - */ -type DiscussionItem = AddonModForumDiscussion | AddonModForumOfflineDiscussion | NewDiscussionForm; - /** * Discussions manager. */ -class AddonModForumDiscussionsManager extends CorePageItemsListManager { +class AddonModForumDiscussionsManager extends CoreListItemsManager { - onlineLoaded = false; - fetchFailed = false; + page: AddonModForumIndexComponent; - private discussionsPathPrefix: string; - private component: AddonModForumIndexComponent; + constructor(source: AddonModForumDiscussionsSource, page: AddonModForumIndexComponent) { + super(source, page.route.component); - constructor(pageComponent: unknown, component: AddonModForumIndexComponent, discussionsPathPrefix: string) { - super(pageComponent); - - this.component = component; - this.discussionsPathPrefix = discussionsPathPrefix; - } - - get loaded(): boolean { - return super.loaded && (this.onlineLoaded || this.fetchFailed); - } - - get onlineDiscussions(): AddonModForumDiscussion[] { - return this.items.filter(discussion => this.isOnlineDiscussion(discussion)) as AddonModForumDiscussion[]; + this.page = page; } /** * @inheritdoc */ - getItemQueryParams(discussion: DiscussionItem): Params { - return { - courseId: this.component.courseId, - cmId: this.component.module.id, - forumId: this.component.forum!.id, - ...(this.isOnlineDiscussion(discussion) ? { discussion, trackPosts: this.component.trackPosts } : {}), - }; - } + protected getDefaultItem(): AddonModForumDiscussionItem | null { + const source = this.getSource(); - /** - * Type guard to infer NewDiscussionForm objects. - * - * @param discussion Item to check. - * @return Whether the item is a new discussion form. - */ - isNewDiscussionForm(discussion: DiscussionItem): discussion is NewDiscussionForm { - return 'newDiscussion' in discussion; - } - - /** - * Type guard to infer AddonModForumDiscussion objects. - * - * @param discussion Item to check. - * @return Whether the item is an online discussion. - */ - isOfflineDiscussion(discussion: DiscussionItem): discussion is AddonModForumOfflineDiscussion { - return !this.isNewDiscussionForm(discussion) - && !this.isOnlineDiscussion(discussion); - } - - /** - * Type guard to infer AddonModForumDiscussion objects. - * - * @param discussion Item to check. - * @return Whether the item is an online discussion. - */ - isOnlineDiscussion(discussion: DiscussionItem): discussion is AddonModForumDiscussion { - return 'id' in discussion; - } - - /** - * Update online discussion items. - * - * @param onlineDiscussions Online discussions - */ - setOnlineDiscussions(onlineDiscussions: AddonModForumDiscussion[], hasMoreItems: boolean = false): void { - const otherDiscussions = this.items.filter(discussion => !this.isOnlineDiscussion(discussion)); - - this.setItems(otherDiscussions.concat(onlineDiscussions), hasMoreItems); - this.onlineLoaded = true; - } - - /** - * Update offline discussion items. - * - * @param offlineDiscussions Offline discussions - */ - setOfflineDiscussions(offlineDiscussions: AddonModForumOfflineDiscussion[]): void { - const otherDiscussions = this.items.filter(discussion => !this.isOfflineDiscussion(discussion)); - - this.setItems((offlineDiscussions as DiscussionItem[]).concat(otherDiscussions), this.hasMoreItems); + return this.items.find(discussion => !source.isNewDiscussionForm(discussion)) || null; } /** * @inheritdoc */ - protected getItemPath(discussion: DiscussionItem): string { - const getRelativePath = () => { - if (this.isOnlineDiscussion(discussion)) { - return discussion.discussion; - } + protected async logActivity(): Promise { + const forum = this.getSource().forum; - if (this.isOfflineDiscussion(discussion)) { - return `new/${discussion.timecreated}`; - } + if (!forum) { + return; + } - return 'new/0'; - }; + CoreUtils.ignoreErrors( + AddonModForum.instance + .logView(forum.id, forum.name) + .then(async () => { + CoreCourse.checkModuleCompletion(this.page.courseId, this.page.module.completiondata); - return this.discussionsPathPrefix + getRelativePath(); + return; + }), + ); } } diff --git a/src/addons/mod/forum/forum.module.ts b/src/addons/mod/forum/forum.module.ts index ec0c89af1..aa1fd3a18 100644 --- a/src/addons/mod/forum/forum.module.ts +++ b/src/addons/mod/forum/forum.module.ts @@ -55,6 +55,7 @@ const mainMenuRoutes: Routes = [ { path: `${AddonModForumModuleHandlerService.PAGE_NAME}/discussion/:discussionId`, loadChildren: () => import('./pages/discussion/discussion.module').then(m => m.AddonForumDiscussionPageModule), + data: { swipeEnabled: false }, }, { path: AddonModForumModuleHandlerService.PAGE_NAME, @@ -66,10 +67,12 @@ const mainMenuRoutes: Routes = [ path: `${COURSE_CONTENTS_PATH}/${AddonModForumModuleHandlerService.PAGE_NAME}/new/:timeCreated`, loadChildren: () => import('./pages/new-discussion/new-discussion.module') .then(m => m.AddonForumNewDiscussionPageModule), + data: { discussionsPathPrefix: `${AddonModForumModuleHandlerService.PAGE_NAME}/` }, }, { path: `${COURSE_CONTENTS_PATH}/${AddonModForumModuleHandlerService.PAGE_NAME}/:discussionId`, loadChildren: () => import('./pages/discussion/discussion.module').then(m => m.AddonForumDiscussionPageModule), + data: { discussionsPathPrefix: `${AddonModForumModuleHandlerService.PAGE_NAME}/` }, }, ], () => CoreScreen.isMobile, @@ -82,10 +85,12 @@ const courseContentsRoutes: Routes = conditionalRoutes( path: `${AddonModForumModuleHandlerService.PAGE_NAME}/new/:timeCreated`, loadChildren: () => import('./pages/new-discussion/new-discussion.module') .then(m => m.AddonForumNewDiscussionPageModule), + data: { discussionsPathPrefix: `${AddonModForumModuleHandlerService.PAGE_NAME}/` }, }, { path: `${AddonModForumModuleHandlerService.PAGE_NAME}/:discussionId`, loadChildren: () => import('./pages/discussion/discussion.module').then(m => m.AddonForumDiscussionPageModule), + data: { discussionsPathPrefix: `${AddonModForumModuleHandlerService.PAGE_NAME}/` }, }, ], () => CoreScreen.isTablet, diff --git a/src/addons/mod/forum/pages/discussion/discussion.html b/src/addons/mod/forum/pages/discussion/discussion.html index 8753e0b5d..e022d7c11 100644 --- a/src/addons/mod/forum/pages/discussion/discussion.html +++ b/src/addons/mod/forum/pages/discussion/discussion.html @@ -56,72 +56,74 @@ - - - + + + + - - - - - - {{ 'core.hasdatatosync' | translate:{$a: discussionStr} }} - - - - - - - - {{ availabilityMessage }} - - - - - - - {{ 'addon.mod_forum.discussionlocked' | translate }} - - - -
- - -
- - - - - - - - - - - - - - - - - - - + + + + + + {{ 'core.hasdatatosync' | translate:{$a: discussionStr} }} + -
- - - + + + + + + {{ availabilityMessage }} + + + + + + + {{ 'addon.mod_forum.discussionlocked' | translate }} + + + +
+ +
- - + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+ + diff --git a/src/addons/mod/forum/pages/discussion/discussion.page.ts b/src/addons/mod/forum/pages/discussion/discussion.page.ts index e1ef3b5a8..103b9355b 100644 --- a/src/addons/mod/forum/pages/discussion/discussion.page.ts +++ b/src/addons/mod/forum/pages/discussion/discussion.page.ts @@ -14,6 +14,8 @@ import { ContextLevel, CoreConstants } from '@/core/constants'; import { Component, OnDestroy, ViewChild, OnInit, AfterViewInit, ElementRef, Optional } from '@angular/core'; +import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router'; +import { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { CoreFileUploader } from '@features/fileuploader/services/fileuploader'; import { CoreRatingInfo, CoreRatingProvider } from '@features/rating/services/rating'; @@ -32,6 +34,8 @@ import { Network, NgZone, Translate } from '@singletons'; import { CoreArray } from '@singletons/array'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { Subscription } from 'rxjs'; +import { AddonModForumDiscussionsSource } from '../../classes/forum-discussions-source'; +import { AddonModForumDiscussionsSwipeManager } from '../../classes/forum-discussions-swipe-manager'; import { AddonModForum, AddonModForumAccessInformation, @@ -68,6 +72,7 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes forum: Partial = {}; accessInfo: AddonModForumAccessInformation = {}; discussion?: AddonModForumDiscussion; + discussions?: AddonModForumDiscussionDiscussionsSwipeManager; startingPost?: Post; posts!: Post[]; discussionLoaded = false; @@ -117,14 +122,16 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes constructor( @Optional() protected splitView: CoreSplitViewComponent, protected elementRef: ElementRef, + protected route: ActivatedRoute, ) {} get isMobile(): boolean { return CoreScreen.isMobile; } - ngOnInit(): void { + async ngOnInit(): Promise { try { + const routeData = this.route.snapshot.data; this.courseId = CoreNavigator.getRouteNumberParam('courseId'); this.cmId = CoreNavigator.getRouteNumberParam('cmId'); this.forumId = CoreNavigator.getRouteNumberParam('forumId'); @@ -136,6 +143,16 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes this.postId = CoreNavigator.getRouteNumberParam('postId'); this.parent = CoreNavigator.getRouteNumberParam('parent'); + if (this.courseId && this.cmId && (routeData.swipeEnabled ?? true)) { + this.discussions = new AddonModForumDiscussionDiscussionsSwipeManager( + CoreItemsManagerSourcesTracker.getOrCreateSource( + AddonModForumDiscussionsSource, + [this.courseId, this.cmId, routeData.discussionsPathPrefix ?? ''], + ), + ); + + await this.discussions.start(); + } } catch (error) { CoreDomUtils.showErrorModal(error); @@ -311,6 +328,7 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes */ ngOnDestroy(): void { this.onlineObserver && this.onlineObserver.unsubscribe(); + this.discussions && this.discussions.destroy(); } /** @@ -839,3 +857,17 @@ export type AddonModForumSharedPostFormData = Omit - - - - - -
- - {{ 'addon.mod_forum.subject' | translate }} - - - - - {{ 'addon.mod_forum.message' | translate }} - - - - - - - -

{{ 'addon.mod_forum.advanced' | translate }}

-
-
-
- - {{ 'addon.mod_forum.posttomygroups' | translate }} - - - - {{ 'addon.mod_forum.group' | translate }} - - {{ group.name }} - + + + + + + + + {{ 'addon.mod_forum.subject' | translate }} + + - {{ 'addon.mod_forum.discussionsubscription' | translate }} - + {{ 'addon.mod_forum.message' | translate }} + + - - {{ 'addon.mod_forum.discussionpinned' | translate }} - + + + + +

{{ 'addon.mod_forum.advanced' | translate }}

+
- - -
- - - - - - {{ 'addon.mod_forum.posttoforum' | translate }} - - - - {{ 'core.discard' | translate }} - - - - -
-
+
+ + {{ 'addon.mod_forum.posttomygroups' | translate }} + + + + {{ 'addon.mod_forum.group' | translate }} + + {{ group.name }} + + + + {{ 'addon.mod_forum.discussionsubscription' | translate }} + + + + {{ 'addon.mod_forum.discussionpinned' | translate }} + + + + +
+ + + + + + {{ 'addon.mod_forum.posttoforum' | translate }} + + + + {{ 'core.discard' | translate }} + + + + + + +
diff --git a/src/addons/mod/forum/pages/new-discussion/new-discussion.page.ts b/src/addons/mod/forum/pages/new-discussion/new-discussion.page.ts index f5495fb71..7e9efa79a 100644 --- a/src/addons/mod/forum/pages/new-discussion/new-discussion.page.ts +++ b/src/addons/mod/forum/pages/new-discussion/new-discussion.page.ts @@ -40,6 +40,10 @@ import { CoreTextUtils } from '@services/utils/text'; import { CanLeave } from '@guards/can-leave'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { CoreForms } from '@singletons/form'; +import { AddonModForumDiscussionsSwipeManager } from '../../classes/forum-discussions-swipe-manager'; +import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router'; +import { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker'; +import { AddonModForumDiscussionsSource } from '../../classes/forum-discussions-source'; type NewDiscussionData = { subject: string; @@ -88,6 +92,8 @@ export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLea accessInfo: AddonModForumAccessInformation = {}; courseId!: number; + discussions?: AddonModForumNewDiscussionDiscussionsSwipeManager; + protected cmId!: number; protected forumId!: number; protected timeCreated!: number; @@ -97,17 +103,29 @@ export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLea protected originalData?: Partial; protected forceLeave = false; - constructor(@Optional() protected splitView: CoreSplitViewComponent) {} + constructor(protected route: ActivatedRoute, @Optional() protected splitView: CoreSplitViewComponent) {} /** * Component being initialized. */ - ngOnInit(): void { + async ngOnInit(): Promise { try { + const routeData = this.route.snapshot.data; this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId'); this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId'); this.forumId = CoreNavigator.getRequiredRouteNumberParam('forumId'); this.timeCreated = CoreNavigator.getRequiredRouteNumberParam('timeCreated'); + + if (this.timeCreated !== 0 && (routeData.swipeEnabled ?? true)) { + const source = CoreItemsManagerSourcesTracker.getOrCreateSource( + AddonModForumDiscussionsSource, + [this.courseId, this.cmId, routeData.discussionsPathPrefix ?? ''], + ); + + this.discussions = new AddonModForumNewDiscussionDiscussionsSwipeManager(source); + + await this.discussions.start(); + } } catch (error) { CoreDomUtils.showErrorModal(error); @@ -625,3 +643,17 @@ export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLea } } + +/** + * Helper to manage swiping within a collection of discussions. + */ +class AddonModForumNewDiscussionDiscussionsSwipeManager extends AddonModForumDiscussionsSwipeManager { + + /** + * @inheritdoc + */ + protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null { + return `${this.getSource().DISCUSSIONS_PATH_PREFIX}new/${route.params.timeCreated}`; + } + +} diff --git a/src/addons/mod/glossary/components/index/index.ts b/src/addons/mod/glossary/components/index/index.ts index 1e5c4d361..6f250813b 100644 --- a/src/addons/mod/glossary/components/index/index.ts +++ b/src/addons/mod/glossary/components/index/index.ts @@ -198,7 +198,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity const [hasOfflineRatings] = await Promise.all([ CoreRatingOffline.hasRatings('mod_glossary', 'entry', ContextLevel.MODULE, this.glossary.coursemodule), - refresh ? this.entries.reload() : this.entries.loadNextPage(), + refresh ? this.entries.reload() : this.entries.load(), ]); this.hasOfflineRatings = hasOfflineRatings; @@ -307,7 +307,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity try { this.loadMoreError = false; - await this.entries.loadNextPage(); + await this.entries.load(); } catch (error) { this.loadMoreError = true; CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentries', true); diff --git a/src/core/classes/items-management/items-manager-source.ts b/src/core/classes/items-management/items-manager-source.ts index e512747ec..29acf445f 100644 --- a/src/core/classes/items-management/items-manager-source.ts +++ b/src/core/classes/items-management/items-manager-source.ts @@ -40,6 +40,7 @@ export abstract class CoreItemsManagerSource { protected items: Item[] | null = null; protected hasMoreItems = true; protected listeners: CoreItemsListSourceListener[] = []; + protected dirty = false; /** * Check whether any page has been loaded. @@ -59,6 +60,17 @@ export abstract class CoreItemsManagerSource { return !this.hasMoreItems; } + /** + * Set whether the source as dirty. + * + * When a source is dirty, the next load request will reload items from the beginning. + * + * @param dirty Whether source should be marked as dirty or not. + */ + setDirty(dirty: boolean): void { + this.dirty = dirty; + } + /** * Get collection items. * @@ -92,6 +104,7 @@ export abstract class CoreItemsManagerSource { reset(): void { this.items = null; this.hasMoreItems = true; + this.dirty = false; this.listeners.forEach(listener => listener.onReset?.call(listener)); } @@ -129,13 +142,23 @@ export abstract class CoreItemsManagerSource { async reload(): Promise { const { items, hasMoreItems } = await this.loadPageItems(0); + this.dirty = false; this.setItems(items, hasMoreItems ?? false); } /** - * Load items for the next page, if any. + * Load more items, if any. */ - async loadNextPage(): Promise { + async load(): Promise { + if (this.dirty) { + const { items, hasMoreItems } = await this.loadPageItems(0); + + this.dirty = false; + this.setItems(items, hasMoreItems ?? false); + + return; + } + if (!this.hasMoreItems) { return; } diff --git a/src/core/classes/items-management/list-items-manager.ts b/src/core/classes/items-management/list-items-manager.ts index a9e22b9a7..baacfdefc 100644 --- a/src/core/classes/items-management/list-items-manager.ts +++ b/src/core/classes/items-management/list-items-manager.ts @@ -140,10 +140,10 @@ export class CoreListItemsManager< } /** - * Load items for the next page, if any. + * Load more items, if any. */ - async loadNextPage(): Promise { - await this.getSource().loadNextPage(); + async load(): Promise { + await this.getSource().load(); } /** diff --git a/src/core/classes/items-management/swipe-items-manager.ts b/src/core/classes/items-management/swipe-items-manager.ts index 3dd41cd4f..918cab741 100644 --- a/src/core/classes/items-management/swipe-items-manager.ts +++ b/src/core/classes/items-management/swipe-items-manager.ts @@ -110,7 +110,7 @@ export class CoreSwipeItemsManager< const item = items?.[index + delta] ?? null; if (!item && !this.getSource().isCompleted()) { - await this.getSource().loadNextPage(); + await this.getSource().load(); return this.getItemBy(delta); } diff --git a/src/core/features/user/pages/participants/participants.page.ts b/src/core/features/user/pages/participants/participants.page.ts index 73f6410f8..a15ed9a11 100644 --- a/src/core/features/user/pages/participants/participants.page.ts +++ b/src/core/features/user/pages/participants/participants.page.ts @@ -185,7 +185,7 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro private async fetchParticipants(reload: boolean): Promise { reload ? await this.participants.reload() - : await this.participants.loadNextPage(); + : await this.participants.load(); this.fetchMoreParticipantsFailed = false; }