From d41523d4bb51c00eb7abdcf3ace397c50a0aa16d Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Wed, 24 Feb 2021 14:26:38 +0100 Subject: [PATCH] MOBILE-3643 forum: Migrate offline discussions --- .../mod/forum/components/index/index.html | 54 ++--- .../mod/forum/components/index/index.ts | 227 +++++++++++++----- src/addons/mod/forum/forum-lazy.module.ts | 4 +- src/addons/mod/forum/forum.module.ts | 4 +- src/core/classes/page-items-list-manager.ts | 26 +- 5 files changed, 217 insertions(+), 98 deletions(-) diff --git a/src/addons/mod/forum/components/index/index.html b/src/addons/mod/forum/components/index/index.html index 78a8a86df..9aaa67dea 100644 --- a/src/addons/mod/forum/components/index/index.html +++ b/src/addons/mod/forum/components/index/index.html @@ -1,3 +1,17 @@ + + + + + + + + + + + + + + @@ -27,7 +41,7 @@ - +
@@ -41,39 +55,15 @@
- - -
-

- -

-
-
- - -
-

{{discussion.userfullname}}

-

{{ discussion.groupname }}

-

{{ 'core.notsent' | translate }}

-
-
-
-
-

- - - - + +

- - +

{{discussion.userfullname}}

{{ discussion.groupname }}

-

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

+

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

+

{{ 'core.notsent' | translate }}

- + {{ 'addon.mod_forum.lastpost' | translate }} diff --git a/src/addons/mod/forum/components/index/index.ts b/src/addons/mod/forum/components/index/index.ts index d28d61443..6445f5197 100644 --- a/src/addons/mod/forum/components/index/index.ts +++ b/src/addons/mod/forum/components/index/index.ts @@ -28,7 +28,7 @@ import { ModalController, PopoverController, Translate } from '@singletons'; 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 } from '@singletons/events'; +import { CoreEvents, CoreEventObserver } from '@singletons/events'; import { AddonModForumSyncProvider } from '@addons/mod/forum/services/sync.service'; import { CoreSites } from '@services/sites'; import { CoreUser } from '@features/user/services/user'; @@ -40,24 +40,7 @@ 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'; import { CoreScreen } from '@services/screen'; - -/** - * Type to use for selecting new discussion form in the discussions manager. - */ -type NewDiscussionForm = { - newDiscussion: true; - timeCreated: number; -}; - -/** - * Type guard to infer NewDiscussionForm objects. - * - * @param discussion Object to check. - * @return Whether the object is a new discussion form. - */ -function isNewDiscussionForm(discussion: Record): discussion is NewDiscussionForm { - return 'newDiscussion' in discussion; -} +import { CoreArray } from '@singletons/array'; /** * Component that displays a forum entry page. @@ -78,8 +61,6 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom canLoadMore = false; loadMoreError = false; discussions: AddonModForumDiscussionsManager; - offlineDiscussions: AddonModForumOfflineDiscussion[] = []; - selectedDiscussion = 0; // Disucssion ID or negative timecreated if it's an offline discussion. canAddDiscussion = false; addDiscussionText!: string; availabilityMessage: string | null = null; @@ -93,11 +74,11 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom protected page = 0; trackPosts = false; protected usesGroups = false; - protected syncManualObserver: any; // It will observe the sync manual event. - protected replyObserver: any; - protected newDiscObserver: any; - protected viewDiscObserver: any; - protected changeDiscObserver: any; + protected syncManualObserver?: CoreEventObserver; // It will observe the sync manual event. + protected replyObserver?: CoreEventObserver; + protected newDiscObserver?: CoreEventObserver; + protected viewDiscObserver?: CoreEventObserver; + protected changeDiscObserver?: CoreEventObserver; hasOfflineRatings?: boolean; protected ratingOfflineObserver: any; @@ -141,6 +122,41 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom AddonModForumProvider.REPLY_DISCUSSION_EVENT, this.eventReceived.bind(this, false), ); + this.changeDiscObserver = CoreEvents.on(AddonModForumProvider.CHANGE_DISCUSSION_EVENT, (data: any) => { + if ((this.forum && this.forum.id === data.forumId) || data.cmId === this.module!.id) { + AddonModForum.instance.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, + ) as AddonModForumDiscussion; + + if (discussion) { + if (typeof data.locked != 'undefined') { + discussion.locked = data.locked; + } + if (typeof data.pinned != 'undefined') { + discussion.pinned = data.pinned; + } + if (typeof data.starred != 'undefined') { + discussion.starred = data.starred; + } + + this.showLoadingAndRefresh(false); + } + } + + if (typeof data.deleted != 'undefined' && data.deleted) { + if (data.post.parentid == 0 && CoreScreen.instance.isTablet && !this.discussions.empty) { + // Discussion deleted, clear details page. + this.discussions.select(this.discussions[0]); + } + + this.showLoadingAndRefresh(false); + } + }); + } + }); } async ngAfterViewInit(): Promise { @@ -356,7 +372,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom this.hasOffline = !!offlineDiscussions.length; if (!this.hasOffline) { - this.offlineDiscussions = []; + this.discussions.setOfflineDiscussions([]); return; } @@ -387,7 +403,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom // Sort discussion by time (newer first). offlineDiscussions.sort((a, b) => b.timecreated - a.timecreated); - this.offlineDiscussions = offlineDiscussions; + this.discussions.setOfflineDiscussions(offlineDiscussions); } /** @@ -435,7 +451,12 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom } } - this.discussions.setItems(this.page === 0 ? discussions : this.discussions.items.concat(discussions)); + if (this.page === 0) { + this.discussions.setOnlineDiscussions(discussions); + } else { + this.discussions.setItems(this.discussions.items.concat(discussions)); + } + this.canLoadMore = response.canLoadMore; this.page++; @@ -540,18 +561,20 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom this.showLoadingAndRefresh(false).finally(() => { // If it's a new discussion in tablet mode, try to open it. if (isNewDiscussion && CoreScreen.instance.isTablet) { - if (data.discussionIds) { - // Discussion sent to server, search it in the list of discussions. - const discussion = this.discussions.items.find( - (disc) => - !isNewDiscussionForm(disc) && - data.discussionIds.indexOf(disc.discussion) >= 0, - ); + const discussion = this.discussions.items.find(disc => { + if (this.discussions.isOfflineDiscussion(disc)) { + return disc.timecreated === data.discTimecreated; + } + if (this.discussions.isOnlineDiscussion(disc)) { + return CoreArray.contains(data.discussionIds, disc.discussion); + } + + return false; + }); + + if (discussion || !this.discussions.empty) { this.discussions.select(discussion ?? this.discussions.items[0]); - } else if (data.discTimecreated) { - // It's an offline discussion, open it. - this.openNewDiscussion(data.discTimecreated); } } }); @@ -566,11 +589,8 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom * * @param timeCreated Creation time of the offline discussion. */ - openNewDiscussion(timeCreated: number = 0): void { - this.discussions.select({ - newDiscussion: true, - timeCreated, - }); + openNewDiscussion(): void { + this.discussions.select({ newDiscussion: true }); } /** @@ -650,7 +670,20 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom } -class AddonModForumDiscussionsManager extends CorePageItemsListManager { +/** + * 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 { private discussionsPathPrefix: string; private component: AddonModForumIndexComponent; @@ -662,29 +695,107 @@ class AddonModForumDiscussionsManager extends CorePageItemsListManager this.isOnlineDiscussion(discussion)) as AddonModForumDiscussion[]; + } + + /** + * @inheritdoc + */ + getItemQueryParams(discussion: DiscussionItem): Params { return { courseId: this.component.courseId, cmId: this.component.module!.id, forumId: this.component.forum!.id, - ...( - isNewDiscussionForm(discussion) - ? { timeCreated: discussion.timeCreated } - : { discussion, trackPosts: this.component.trackPosts } - ), + ...(this.isOnlineDiscussion(discussion) ? { discussion, trackPosts: this.component.trackPosts } : {}), }; } - protected getItemPath(discussion: AddonModForumDiscussion | NewDiscussionForm): string { - const discussionId = isNewDiscussionForm(discussion) ? 'new' : discussion.id; - - return this.discussionsPathPrefix + discussionId; + /** + * 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; } - protected getSelectedItemPath(route: ActivatedRouteSnapshot): string | null { - const discussionId = route.params.discussionId; + /** + * 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); + } - return discussionId ? this.discussionsPathPrefix + discussionId : null; + /** + * 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[]): void { + const otherDiscussions = this.items.filter(discussion => !this.isOnlineDiscussion(discussion)); + + this.setItems(otherDiscussions.concat(onlineDiscussions)); + } + + /** + * 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)); + } + + /** + * @inheritdoc + */ + protected getItemPath(discussion: DiscussionItem): string { + const getRelativePath = () => { + if (this.isOnlineDiscussion(discussion)) { + return discussion.id; + } + + if (this.isOfflineDiscussion(discussion)) { + return `new/${discussion.timecreated}`; + } + + return 'new/0'; + }; + + return this.discussionsPathPrefix + getRelativePath(); + } + + /** + * @inheritdoc + */ + protected getSelectedItemPath(route: ActivatedRouteSnapshot): string | null { + if (route.params.discussionId) { + return this.discussionsPathPrefix + route.params.discussionId; + } + + if (route.params.timeCreated) { + return this.discussionsPathPrefix + `new/${route.params.timeCreated}`; + } + + return null; } } diff --git a/src/addons/mod/forum/forum-lazy.module.ts b/src/addons/mod/forum/forum-lazy.module.ts index b1566c9ec..a4b888420 100644 --- a/src/addons/mod/forum/forum-lazy.module.ts +++ b/src/addons/mod/forum/forum-lazy.module.ts @@ -28,7 +28,7 @@ const mobileRoutes: Routes = [ component: AddonModForumIndexPage, }, { - path: ':courseId/:cmId/new', + path: ':courseId/:cmId/new/:timeCreated', loadChildren: () => import('./pages/new-discussion/new-discussion.module').then(m => m.AddonForumNewDiscussionPageModule), }, { @@ -43,7 +43,7 @@ const tabletRoutes: Routes = [ component: AddonModForumIndexPage, children: [ { - path: 'new', + path: 'new/:timeCreated', loadChildren: () => import('./pages/new-discussion/new-discussion.module') .then(m => m.AddonForumNewDiscussionPageModule), }, diff --git a/src/addons/mod/forum/forum.module.ts b/src/addons/mod/forum/forum.module.ts index ca6a7128b..8f2ee34b6 100644 --- a/src/addons/mod/forum/forum.module.ts +++ b/src/addons/mod/forum/forum.module.ts @@ -34,7 +34,7 @@ const mainMenuRoutes: Routes = [ ...conditionalRoutes( [ { - path: 'course/index/contents/mod_forum/new', + path: 'course/index/contents/mod_forum/new/:timeCreated', loadChildren: () => import('./pages/new-discussion/new-discussion.module') .then(m => m.AddonForumNewDiscussionPageModule), }, @@ -50,7 +50,7 @@ const mainMenuRoutes: Routes = [ const courseContentsRoutes: Routes = conditionalRoutes( [ { - path: 'mod_forum/new', + path: 'mod_forum/new/:timeCreated', loadChildren: () => import('./pages/new-discussion/new-discussion.module') .then(m => m.AddonForumNewDiscussionPageModule), }, diff --git a/src/core/classes/page-items-list-manager.ts b/src/core/classes/page-items-list-manager.ts index 38c6d6c79..5f5334b8b 100644 --- a/src/core/classes/page-items-list-manager.ts +++ b/src/core/classes/page-items-list-manager.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { ActivatedRouteSnapshot, Params } from '@angular/router'; +import { ActivatedRoute, ActivatedRouteSnapshot, Params } from '@angular/router'; import { Subscription } from 'rxjs'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; @@ -135,17 +135,19 @@ export abstract class CorePageItemsListManager { } // If this item is already selected, do nothing. + const itemRoute = this.getItemRoute(route); const itemPath = this.getItemPath(item); + const selectedItemPath = itemRoute ? this.getSelectedItemPath(itemRoute.snapshot) : null; - if (route.firstChild?.routeConfig?.path === itemPath) { + if (selectedItemPath === itemPath) { return; } // Navigate to item. const params = this.getItemQueryParams(item); - const pathPrefix = route.firstChild ? itemPath.split('/').fill('../').join('') : ''; + const pathPrefix = selectedItemPath ? selectedItemPath.split('/').fill('../').join('') : ''; - await CoreNavigator.instance.navigate(pathPrefix + itemPath, { params }); + await CoreNavigator.instance.navigate(pathPrefix + itemPath, { params, reset: true }); } /** @@ -220,4 +222,20 @@ export abstract class CorePageItemsListManager { */ protected abstract getSelectedItemPath(route: ActivatedRouteSnapshot): string | null; + /** + * Get the active item route, if any. + * + * @param pageRoute Page route. + * @return Item route. + */ + private getItemRoute(pageRoute: ActivatedRoute): ActivatedRoute | null { + let itemRoute = pageRoute.firstChild; + + while (itemRoute && !itemRoute.component) { + itemRoute = itemRoute.firstChild; + } + + return itemRoute; + } + }