MOBILE-3643 forum: Migrate index page
This commit is contained in:
		
							parent
							
								
									549735bf98
								
							
						
					
					
						commit
						b318b0e4a5
					
				| @ -29,7 +29,7 @@ | ||||
|                             <ion-item class="ion-text-wrap addon-messages-conversation-item" | ||||
|                                 *ngFor="let contact of confirmedContacts" [title]="contact.fullname" detail | ||||
|                                 (click)="selectUser(contact.id)" [class.core-selected-item]="contact.id == selectedUserId"> | ||||
|                                 <core-user-avatar slot="start" core-user-avatar [user]="contact" | ||||
|                                 <core-user-avatar slot="start" [user]="contact" | ||||
|                                     [checkOnline]="contact.showonlinestatus" [linkProfile]="false"> | ||||
|                                 </core-user-avatar> | ||||
|                                 <ion-label> | ||||
|  | ||||
							
								
								
									
										36
									
								
								src/addons/mod/forum/components/components.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/addons/mod/forum/components/components.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,36 @@ | ||||
| // (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 { NgModule } from '@angular/core'; | ||||
| 
 | ||||
| import { CoreSharedModule } from '@/core/shared.module'; | ||||
| import { CoreCourseComponentsModule } from '@features/course/components/components.module'; | ||||
| import { CoreTagComponentsModule } from '@features/tag/components/components.module'; | ||||
| 
 | ||||
| import { AddonModForumIndexComponent } from './index/index'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
|         AddonModForumIndexComponent, | ||||
|     ], | ||||
|     imports: [ | ||||
|         CoreSharedModule, | ||||
|         CoreCourseComponentsModule, | ||||
|         CoreTagComponentsModule, | ||||
|     ], | ||||
|     exports: [ | ||||
|         AddonModForumIndexComponent, | ||||
|     ], | ||||
| }) | ||||
| export class AddonModForumComponentsModule {} | ||||
							
								
								
									
										127
									
								
								src/addons/mod/forum/components/index/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								src/addons/mod/forum/components/index/index.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,127 @@ | ||||
| <!-- Content. --> | ||||
| <ion-content> | ||||
|     <core-split-view> | ||||
|         <ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($event)"> | ||||
|             <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> | ||||
|         </ion-refresher> | ||||
| 
 | ||||
|         <core-loading [hideUntil]="loaded" class="core-loading-center"> | ||||
|             <core-course-module-description *ngIf="forum && forum.type != 'single'" | ||||
|                 [description]="description" [component]="component" [componentId]="componentId" [note]="descriptionNote" | ||||
|                 contextLevel="module" [contextInstanceId]="module && module.id" [courseId]="courseId"> | ||||
|             </core-course-module-description> | ||||
| 
 | ||||
|             <!-- Forum discussions found to be synchronized --> | ||||
|             <ion-card class="core-warning-card" *ngIf="hasOffline || hasOfflineRatings"> | ||||
|                 <ion-item> | ||||
|                     <ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon> | ||||
|                     <ion-label>{{ 'core.hasdatatosync' | translate:{$a: moduleName} }}</ion-label> | ||||
|                 </ion-item> | ||||
|             </ion-card> | ||||
| 
 | ||||
|             <!-- Cut-off date or due date message --> | ||||
|             <ion-card class="core-info-card" *ngIf="availabilityMessage"> | ||||
|                 <ion-item> | ||||
|                     <ion-icon name="fas-info-circle" slot="start"></ion-icon> | ||||
|                     <ion-label>{{ availabilityMessage }}</ion-label> | ||||
|                 </ion-item> | ||||
|             </ion-card> | ||||
| 
 | ||||
|             <ng-container *ngIf="forum"> | ||||
|                 <core-empty-box *ngIf="discussions.length == 0 && offlineDiscussions.length == 0" icon="chatbubbles" [message]="'addon.mod_forum.forumnodiscussionsyet' | translate"> | ||||
|                 </core-empty-box> | ||||
| 
 | ||||
|                 <div *ngIf="discussions.length > 0 && sortingAvailable && selectedSortOrder" class="ion-text-wrap addon-forum-sorting-select"> | ||||
|                     <ion-button *ngIf="sortingAvailable" id="addon-mod-forum-sort-order-button" | ||||
|                         class="core-button-select button-no-uppercase" | ||||
|                         aria-haspopup="true" aria-controls="addon-mod-forum-sort-order-selector" | ||||
|                         [attr.aria-label]="('core.sort' | translate)" [attr.aria-expanded]="sortOrderSelectorExpanded" | ||||
|                         (click)="showSortOrderSelector($event)"> | ||||
|                         <span class="core-button-select-text">{{ selectedSortOrder.label | translate }}</span> | ||||
|                         <div class="select-icon" slot="end"><div class="select-icon-inner"></div></div> | ||||
|                     </ion-button> | ||||
|                 </div> | ||||
| 
 | ||||
|                 <ion-item *ngFor="let discussion of offlineDiscussions" | ||||
|                     class="ion-text-wrap addon-mod-forum-discussion" detail="true" | ||||
|                     [attr.lines="none"]="discussion.groupname" [class.core-item-selected]="discussion.timecreated == -selectedDiscussion" | ||||
|                     (click)="openNewDiscussion(discussion.timecreated)"> | ||||
|                     <ion-label> | ||||
|                         <div class="addon-mod-forum-discussion-title"> | ||||
|                             <h2> | ||||
|                                 <core-format-text [text]="discussion.subject" contextLevel="module" [contextInstanceId]="module && module.id" [courseId]="courseId"></core-format-text> | ||||
|                             </h2> | ||||
|                         </div> | ||||
|                         <div class="addon-mod-forum-discussion-info"> | ||||
|                             <core-user-avatar [user]="discussion" slot="start" [courseId]="courseId" *ngIf="discussion.userfullname"> | ||||
|                             </core-user-avatar> | ||||
|                             <div class="addon-mod-forum-discussion-author"> | ||||
|                                 <h3 *ngIf="discussion.userfullname">{{discussion.userfullname}}</h3> | ||||
|                                 <p *ngIf="discussion.groupname"><ion-icon name="people"></ion-icon> {{ discussion.groupname }}</p> | ||||
|                                 <p><ion-icon name="time"></ion-icon> {{ 'core.notsent' | translate }}</p> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
| 
 | ||||
|                 <ion-item *ngFor="let discussion of discussions" | ||||
|                     class="addon-mod-forum-discussion" detail="true" | ||||
|                     [class.core-split-item-selected]="discussion.discussion == selectedDiscussion" | ||||
|                     (click)="openDiscussion(discussion)"> | ||||
|                     <ion-label> | ||||
|                         <div class="addon-mod-forum-discussion-title"> | ||||
|                             <h2 class="ion-text-wrap"> | ||||
|                                 <ion-icon name="fa-map-pin" *ngIf="discussion.pinned"> | ||||
|                                 </ion-icon> | ||||
|                                 <ion-icon name="fa-star" class="addon-forum-star" *ngIf="!discussion.pinned && discussion.starred"> | ||||
|                                 </ion-icon> | ||||
|                                 <core-format-text [text]="discussion.subject" contextLevel="module" [contextInstanceId]="module && module.id" [courseId]="courseId"></core-format-text> | ||||
|                             </h2> | ||||
|                             <ion-button *ngIf="canPin || discussion.canlock || discussion.canfavourite" | ||||
|                                 fill="clear" color="dark" | ||||
|                                 [attr.aria-label]="('core.displayoptions' | translate)" | ||||
|                                 (click)="showOptionsMenu($event, discussion)"> | ||||
|                                 <ion-icon name="more" slot="icon-only"> | ||||
|                                 </ion-icon> | ||||
|                             </ion-button> | ||||
|                         </div> | ||||
|                         <div class="addon-mod-forum-discussion-info"> | ||||
|                             <core-user-avatar *ngIf="discussion.userfullname" [user]="discussion" slot="start" [courseId]="courseId"> | ||||
|                             </core-user-avatar> | ||||
|                             <div class="addon-mod-forum-discussion-author"> | ||||
|                                 <h3 *ngIf="discussion.userfullname">{{discussion.userfullname}}</h3> | ||||
|                                 <p *ngIf="discussion.groupname"><ion-icon name="people"></ion-icon> {{ discussion.groupname }}</p> | ||||
|                                 <p>{{discussion.created * 1000 | coreFormatDate: "strftimerecentfull"}}</p> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                         <ion-row class="ion-text-center addon-mod-forum-discussion-more-info"> | ||||
|                             <ion-col class="ion-text-start"> | ||||
|                                 <ion-note> | ||||
|                                     <ion-icon name="time"></ion-icon> {{ 'addon.mod_forum.lastpost' | translate }} | ||||
|                                     <ng-container *ngIf="discussion.timemodified > discussion.created">{{discussion.timemodified | coreTimeAgo}}</ng-container> | ||||
|                                     <ng-container *ngIf="discussion.timemodified <= discussion.created">{{discussion.created | coreTimeAgo}}</ng-container> | ||||
|                                 </ion-note> | ||||
|                             </ion-col> | ||||
|                             <ion-col class="ion-text-end"> | ||||
|                                 <ion-note> | ||||
|                                     <ion-icon name="fas-comments"></ion-icon> {{ 'addon.mod_forum.numreplies' | translate:{numreplies: discussion.numreplies} }} | ||||
|                                     <ion-badge *ngIf="discussion.numunread" class="ion-text-center" | ||||
|                                         [attr.aria-label]="'addon.mod_forum.unreadpostsnumber' | translate:{ '$a' : discussion.numunread}"> | ||||
|                                         {{ discussion.numunread }} | ||||
|                                     </ion-badge> | ||||
|                                 </ion-note> | ||||
|                             </ion-col> | ||||
|                         </ion-row> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
| 
 | ||||
|             </ng-container> | ||||
|         </core-loading> | ||||
| 
 | ||||
|         <ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="forum && canAddDiscussion"> | ||||
|             <ion-fab-button (click)="openNewDiscussion()" [attr.aria-label]="addDiscussionText"> | ||||
|                 <ion-icon name="add"></ion-icon> | ||||
|             </ion-fab-button> | ||||
|         </ion-fab> | ||||
|     </core-split-view> | ||||
| </ion-content> | ||||
							
								
								
									
										65
									
								
								src/addons/mod/forum/components/index/index.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								src/addons/mod/forum/components/index/index.scss
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,65 @@ | ||||
| @import '../../../../../theme/globals.scss'; | ||||
| 
 | ||||
| :host { | ||||
| 
 | ||||
|     .addon-forum-sorting-select { | ||||
|         display: flex; | ||||
| 
 | ||||
|         .core-button-select { | ||||
|             flex: 1; | ||||
|         } | ||||
| 
 | ||||
|         .core-button-select-text { | ||||
|             overflow: hidden; | ||||
|             text-overflow: ellipsis; | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     .addon-forum-star { | ||||
|         color: var(--core-color); | ||||
|     } | ||||
| 
 | ||||
|     .addon-mod-forum-discussion.item { | ||||
| 
 | ||||
|         ion-label { | ||||
|             margin-top: 4px; | ||||
| 
 | ||||
|             h2 { | ||||
|                 margin-top: 8px; | ||||
|                 margin-bottom: 8px; | ||||
|                 font-weight: bold; | ||||
| 
 | ||||
|                 ion-icon { | ||||
|                     @include margin(0, 6px, 0, 0); | ||||
|                 } | ||||
| 
 | ||||
|             } | ||||
| 
 | ||||
|         } | ||||
| 
 | ||||
|         core-user-avatar { | ||||
|             --core-avatar-size: var(--addon-forum-avatar-size); | ||||
| 
 | ||||
|             @include margin(0, 8px, 0, 0); | ||||
|         } | ||||
| 
 | ||||
|         .addon-mod-forum-discussion-title, | ||||
|         .addon-mod-forum-discussion-info { | ||||
|             display: flex; | ||||
|             align-items: center; | ||||
|         } | ||||
| 
 | ||||
|         .addon-mod-forum-discussion-title h2, | ||||
|         .addon-mod-forum-discussion-info .addon-mod-forum-discussion-author { | ||||
|             flex-grow: 1; | ||||
|         } | ||||
| 
 | ||||
|         .addon-mod-forum-discussion-more-info { | ||||
|             font-size: 1.4rem; | ||||
|             clear: both; | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										563
									
								
								src/addons/mod/forum/components/index/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										563
									
								
								src/addons/mod/forum/components/index/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,563 @@ | ||||
| // (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 { Component, Optional, OnInit, OnDestroy } from '@angular/core'; | ||||
| import { IonContent } from '@ionic/angular'; | ||||
| import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component'; | ||||
| import { | ||||
|     AddonModForum, | ||||
|     AddonModForumData, | ||||
|     AddonModForumProvider, | ||||
|     AddonModForumSortOrder, | ||||
|     AddonModForumDiscussion, | ||||
| } from '@addons/mod/forum/services/forum.service'; | ||||
| import { AddonModForumOffline, AddonModForumOfflineDiscussion } from '@addons/mod/forum/services/offline.service'; | ||||
| import { 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 { AddonModForumSyncProvider } 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'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { CoreCourse } from '@features/course/services/course'; | ||||
| 
 | ||||
| /** | ||||
|  * Component that displays a forum entry page. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'addon-mod-forum-index', | ||||
|     templateUrl: 'index.html', | ||||
|     styleUrls: ['index.scss'], | ||||
| }) | ||||
| export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit, OnDestroy { | ||||
| 
 | ||||
|     component = AddonModForumProvider.COMPONENT; | ||||
|     moduleName = 'forum'; | ||||
| 
 | ||||
|     descriptionNote?: string; | ||||
|     forum?: AddonModForumData; | ||||
|     canLoadMore = false; | ||||
|     loadMoreError = false; | ||||
|     discussions: AddonModForumDiscussion[] = []; | ||||
|     offlineDiscussions: AddonModForumOfflineDiscussion[] = []; | ||||
|     selectedDiscussion = 0; // Disucssion ID or negative timecreated if it's an offline discussion.
 | ||||
|     canAddDiscussion = false; | ||||
|     addDiscussionText!: string; | ||||
|     availabilityMessage: string | null = null; | ||||
| 
 | ||||
|     sortingAvailable!: boolean; | ||||
|     sortOrders: AddonModForumSortOrder[] = []; | ||||
|     selectedSortOrder: AddonModForumSortOrder | null = null; | ||||
|     sortOrderSelectorExpanded = false; | ||||
| 
 | ||||
|     protected syncEventName = AddonModForumSyncProvider.AUTO_SYNCED; | ||||
|     protected page = 0; | ||||
|     protected trackPosts = false; | ||||
|     protected usesGroups = false; | ||||
|     protected canPin = false; | ||||
|     protected syncManualObserver: any; // It will observe the sync manual event.
 | ||||
|     protected replyObserver: any; | ||||
|     protected newDiscObserver: any; | ||||
|     protected viewDiscObserver: any; | ||||
|     protected changeDiscObserver: any; | ||||
| 
 | ||||
|     hasOfflineRatings?: boolean; | ||||
|     protected ratingOfflineObserver: any; | ||||
|     protected ratingSyncObserver: any; | ||||
| 
 | ||||
|     constructor( | ||||
|         @Optional() protected content?: IonContent, | ||||
|         @Optional() courseContentsPage?: CoreCourseContentsPage, | ||||
|     ) { | ||||
|         super('AddonModForumIndexComponent', content, courseContentsPage); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being initialized. | ||||
|      */ | ||||
|     async ngOnInit(): Promise<void> { | ||||
|         this.addDiscussionText = Translate.instance.instant('addon.mod_forum.addanewdiscussion'); | ||||
|         this.sortingAvailable = AddonModForum.instance.isDiscussionListSortingAvailable(); | ||||
|         this.sortOrders = AddonModForum.instance.getAvailableSortOrders(); | ||||
| 
 | ||||
|         await super.ngOnInit(); | ||||
|         await this.loadContent(false, true); | ||||
| 
 | ||||
|         if (!this.forum) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         CoreUtils.instance.ignoreErrors( | ||||
|             AddonModForum.instance | ||||
|                 .logView(this.forum.id, this.forum.name) | ||||
|                 .then(async () => { | ||||
|                     CoreCourse.instance.checkModuleCompletion(this.courseId!, this.module!.completiondata); | ||||
| 
 | ||||
|                     return; | ||||
|                 }), | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being destroyed. | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         super.ngOnDestroy(); | ||||
| 
 | ||||
|         this.syncManualObserver && this.syncManualObserver.off(); | ||||
|         this.newDiscObserver && this.newDiscObserver.off(); | ||||
|         this.replyObserver && this.replyObserver.off(); | ||||
|         this.viewDiscObserver && this.viewDiscObserver.off(); | ||||
|         this.changeDiscObserver && this.changeDiscObserver.off(); | ||||
|         this.ratingOfflineObserver && this.ratingOfflineObserver.off(); | ||||
|         this.ratingSyncObserver && this.ratingSyncObserver.off(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Download the component contents. | ||||
|      * | ||||
|      * @param refresh Whether we're refreshing data. | ||||
|      * @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<void> { | ||||
|         this.loadMoreError = false; | ||||
| 
 | ||||
|         const promises: Promise<void>[] = []; | ||||
| 
 | ||||
|         promises.push(this.fetchForum()); | ||||
|         promises.push(this.fetchSortOrderPreference()); | ||||
| 
 | ||||
|         try { | ||||
|             await Promise.all(promises); | ||||
|             await Promise.all([ | ||||
|                 this.fetchOfflineDiscussions(), | ||||
|                 this.fetchDiscussions(refresh), | ||||
|             ]); | ||||
|         } catch (error) { | ||||
|             if (refresh) { | ||||
|                 CoreDomUtils.instance.showErrorModalDefault(error, 'addon.mod_forum.errorgetforum', true); | ||||
| 
 | ||||
|                 this.loadMoreError = 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); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         this.fillContextMenu(refresh); | ||||
|     } | ||||
| 
 | ||||
|     private async fetchForum(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> { | ||||
|         if (!this.courseId || !this.module) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.loadMoreError = false; | ||||
| 
 | ||||
|         const promises: Promise<void>[] = []; | ||||
| 
 | ||||
|         promises.push( | ||||
|             AddonModForum.instance | ||||
|                 .getForum(this.courseId, this.module.id) | ||||
|                 .then(async (forum) => { | ||||
|                     this.forum = forum; | ||||
| 
 | ||||
|                     this.description = forum.intro || this.description; | ||||
|                     this.descriptionNote = Translate.instant('addon.mod_forum.numdiscussions', { | ||||
|                         numdiscussions: forum.numdiscussions, | ||||
|                     }); | ||||
|                     if (typeof forum.istracked != 'undefined') { | ||||
|                         this.trackPosts = forum.istracked; | ||||
|                     } | ||||
| 
 | ||||
|                     this.availabilityMessage = AddonModForumHelper.instance.getAvailabilityMessage(forum); | ||||
| 
 | ||||
|                     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) { | ||||
|                         return; | ||||
|                     } | ||||
| 
 | ||||
|                     // Try to synchronize the forum.
 | ||||
|                     const updated = await this.syncActivity(showErrors); | ||||
| 
 | ||||
|                     if (!updated) { | ||||
|                         return; | ||||
|                     } | ||||
| 
 | ||||
|                     // Sync successful, send event.
 | ||||
|                     CoreEvents.trigger(AddonModForumSyncProvider.MANUAL_SYNCED, { | ||||
|                         forumId: forum.id, | ||||
|                         userId: CoreSites.instance.getCurrentSiteUserId(), | ||||
|                         source: 'index', | ||||
|                     }, CoreSites.instance.getCurrentSiteId()); | ||||
| 
 | ||||
|                     const promises: Promise<void>[] = []; | ||||
| 
 | ||||
|                     // 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 | ||||
|                                     || 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()); | ||||
| 
 | ||||
|         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); | ||||
|             } | ||||
| 
 | ||||
|             CoreDomUtils.instance.showErrorModalDefault(message, 'addon.mod_forum.errorgetforum', true); | ||||
| 
 | ||||
|             this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading.
 | ||||
|         } | ||||
| 
 | ||||
|         this.fillContextMenu(refresh); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Convenience function to fetch offline discussions. | ||||
|      * | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async fetchOfflineDiscussions(): Promise<void> { | ||||
|         const forum = this.forum!; | ||||
|         let offlineDiscussions = await AddonModForumOffline.instance.getNewDiscussions(forum.id); | ||||
|         this.hasOffline = !!offlineDiscussions.length; | ||||
| 
 | ||||
|         if (!this.hasOffline) { | ||||
|             this.offlineDiscussions = []; | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         if (this.usesGroups) { | ||||
|             offlineDiscussions = await AddonModForum.instance.formatDiscussionsGroups(forum.cmid, offlineDiscussions); | ||||
|         } | ||||
| 
 | ||||
|         // Fill user data for Offline discussions (should be already cached).
 | ||||
|         const promises = offlineDiscussions.map(async (discussion: any) => { | ||||
|             if (discussion.parent === 0 || forum.type === 'single') { | ||||
|                 // Do not show author for first post and type single.
 | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             try { | ||||
|                 const user = await CoreUser.instance.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.offlineDiscussions = 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<void> { | ||||
|         const forum = this.forum!; | ||||
|         this.loadMoreError = false; | ||||
| 
 | ||||
|         if (refresh) { | ||||
|             this.page = 0; | ||||
|         } | ||||
| 
 | ||||
|         const response = await AddonModForum.instance.getDiscussions(forum.id, { | ||||
|             cmId: forum.cmid, | ||||
|             sortOrder: this.selectedSortOrder!.value, | ||||
|             page: this.page, | ||||
|         }); | ||||
|         let discussions = response.discussions; | ||||
| 
 | ||||
|         if (this.usesGroups) { | ||||
|             discussions = await AddonModForum.instance.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 as any).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; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         this.discussions = this.page === 0 | ||||
|             ? discussions | ||||
|             : this.discussions.concat(discussions); | ||||
| 
 | ||||
|         this.canLoadMore = response.canLoadMore; | ||||
|         this.page++; | ||||
| 
 | ||||
|         // Check if there are replies for discussions stored in offline.
 | ||||
|         const hasOffline = await AddonModForumOffline.instance.hasForumReplies(forum.id); | ||||
| 
 | ||||
|         this.hasOffline = this.hasOffline || hasOffline; | ||||
| 
 | ||||
|         if (hasOffline) { | ||||
|             // Only update new fetched discussions.
 | ||||
|             const promises = discussions.map(async (discussion: any) => { | ||||
|                 // Get offline discussions.
 | ||||
|                 const replies = await AddonModForumOffline.instance.getDiscussionReplies(discussion.discussion); | ||||
| 
 | ||||
|                 discussion.numreplies = Number(discussion.numreplies) + replies.length; | ||||
|             }); | ||||
| 
 | ||||
|             await Promise.all(promises); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Convenience function to load more forum discussions. | ||||
|      * | ||||
|      * @param infiniteComplete Infinite scroll complete function. Only used from core-infinite-loading. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     fetchMoreDiscussions(infiniteComplete?: any): Promise<any> { | ||||
|         return this.fetchDiscussions(false).catch((message) => { | ||||
|             CoreDomUtils.instance.showErrorModalDefault(message, 'addon.mod_forum.errorgetforum', true); | ||||
| 
 | ||||
|             this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading.
 | ||||
|         }).finally(() => { | ||||
|             infiniteComplete && infiniteComplete(); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Convenience function to fetch the sort order preference. | ||||
|      * | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async fetchSortOrderPreference(): Promise<void> { | ||||
|         const getSortOrder = async () => { | ||||
|             if (!this.sortingAvailable) { | ||||
|                 return null; | ||||
|             } | ||||
| 
 | ||||
|             const value = await CoreUtils.instance.ignoreErrors( | ||||
|                 CoreUser.instance.getUserPreference(AddonModForumProvider.PREFERENCE_SORTORDER), | ||||
|             ); | ||||
| 
 | ||||
|             return value ? parseInt(value, 10) : null; | ||||
|         }; | ||||
| 
 | ||||
|         const value = await getSortOrder(); | ||||
| 
 | ||||
|         this.selectedSortOrder = this.sortOrders.find(sortOrder => sortOrder.value === value) || this.sortOrders[0]; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Perform the invalidate content function. | ||||
|      * | ||||
|      * @return Resolved when done. | ||||
|      */ | ||||
|     protected invalidateContent(): Promise<any> { | ||||
|         const promises: Promise<void>[] = []; | ||||
| 
 | ||||
|         promises.push(AddonModForum.instance.invalidateForumData(this.courseId!)); | ||||
| 
 | ||||
|         if (this.forum) { | ||||
|             promises.push(AddonModForum.instance.invalidateDiscussionsList(this.forum.id)); | ||||
|             promises.push(CoreGroups.instance.invalidateActivityGroupMode(this.forum.cmid)); | ||||
|             promises.push(AddonModForum.instance.invalidateAccessInformation(this.forum.id)); | ||||
|         } | ||||
| 
 | ||||
|         if (this.sortingAvailable) { | ||||
|             promises.push(CoreUser.instance.invalidateUserPreference(AddonModForumProvider.PREFERENCE_SORTORDER)); | ||||
|         } | ||||
| 
 | ||||
|         return Promise.all(promises); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Checks if sync has succeed from result sync data. | ||||
|      * | ||||
|      * @param result Data returned on the sync function. | ||||
|      * @return Whether it succeed or not. | ||||
|      */ | ||||
|     protected hasSyncSucceed(result: any): boolean { | ||||
|         return result.updated; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Opens a discussion. | ||||
|      * | ||||
|      * @param discussion Discussion object. | ||||
|      */ | ||||
|     openDiscussion(discussion: AddonModForumDiscussion): void { | ||||
|         alert(`Open discussion ${discussion.id}: Not implemented!`); | ||||
| 
 | ||||
|         // @todo
 | ||||
|         // const params = {
 | ||||
|         //     courseId: this.courseId,
 | ||||
|         //     cmId: this.module.id,
 | ||||
|         //     forumId: this.forum.id,
 | ||||
|         //     discussion: discussion,
 | ||||
|         //     trackPosts: this.trackPosts,
 | ||||
|         // };
 | ||||
|         // this.splitviewCtrl.push('AddonModForumDiscussionPage', params);
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Opens the new discussion form. | ||||
|      * | ||||
|      * @param timeCreated Creation time of the offline discussion. | ||||
|      */ | ||||
|     openNewDiscussion(timeCreated: number = 0): void { | ||||
|         alert(`Open new discussion at ${timeCreated} not implemented!`); | ||||
| 
 | ||||
|         // @todo
 | ||||
|         // const params = {
 | ||||
|         //     courseId: this.courseId,
 | ||||
|         //     cmId: this.module.id,
 | ||||
|         //     forumId: this.forum.id,
 | ||||
|         //     timeCreated: timeCreated,
 | ||||
|         // };
 | ||||
|         // this.splitviewCtrl.push('AddonModForumNewDiscussionPage', params);
 | ||||
| 
 | ||||
|         this.selectedDiscussion = 0; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Display the sort order selector modal. | ||||
|      * | ||||
|      * @param event Event. | ||||
|      */ | ||||
|     // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||
|     showSortOrderSelector(event: MouseEvent): void { | ||||
|         if (!this.sortingAvailable) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         alert('Show sort order selector not implemented'); | ||||
| 
 | ||||
|         // @todo
 | ||||
|         // const params = { sortOrders: this.sortOrders, selected: this.selectedSortOrder.value };
 | ||||
|         // const modal = this.modalCtrl.create('AddonModForumSortOrderSelectorPage', params);
 | ||||
|         // modal.onDidDismiss((sortOrder) => {
 | ||||
|         //     this.sortOrderSelectorExpanded = false;
 | ||||
| 
 | ||||
|         //     if (sortOrder && sortOrder.value != this.selectedSortOrder.value) {
 | ||||
|         //         this.selectedSortOrder = sortOrder;
 | ||||
|         //         this.page = 0;
 | ||||
|         //         this.userProvider.setUserPreference(AddonModForumProvider.PREFERENCE_SORTORDER, sortOrder.value.toFixed(0))
 | ||||
|         //             .then(() => {
 | ||||
|         //                 this.showLoadingAndFetch();
 | ||||
|         //             }).catch((error) => {
 | ||||
|         //                 this.domUtils.showErrorModalDefault(error, 'Error updating preference.');
 | ||||
|         //             });
 | ||||
|         //     }
 | ||||
|         // });
 | ||||
| 
 | ||||
|         // modal.present({ ev: event });
 | ||||
|         // this.sortOrderSelectorExpanded = true;
 | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										40
									
								
								src/addons/mod/forum/forum-lazy.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/addons/mod/forum/forum-lazy.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,40 @@ | ||||
| // (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 { NgModule } from '@angular/core'; | ||||
| import { RouterModule, Routes } from '@angular/router'; | ||||
| 
 | ||||
| import { CoreSharedModule } from '@/core/shared.module'; | ||||
| 
 | ||||
| import { AddonModForumComponentsModule } from './components/components.module'; | ||||
| import { AddonModForumIndexPage } from './pages/index'; | ||||
| 
 | ||||
| const routes: Routes = [ | ||||
|     { | ||||
|         path: ':courseId/:cmId', | ||||
|         component: AddonModForumIndexPage, | ||||
|     }, | ||||
| ]; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     imports: [ | ||||
|         RouterModule.forChild(routes), | ||||
|         CoreSharedModule, | ||||
|         AddonModForumComponentsModule, | ||||
|     ], | ||||
|     declarations: [ | ||||
|         AddonModForumIndexPage, | ||||
|     ], | ||||
| }) | ||||
| export class AddonModForumLazyModule {} | ||||
							
								
								
									
										51
									
								
								src/addons/mod/forum/forum.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/addons/mod/forum/forum.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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 { APP_INITIALIZER, NgModule } from '@angular/core'; | ||||
| import { Routes } from '@angular/router'; | ||||
| 
 | ||||
| import { CORE_SITE_SCHEMAS } from '@services/sites'; | ||||
| import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; | ||||
| import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; | ||||
| 
 | ||||
| import { AddonModForumComponentsModule } from './components/components.module'; | ||||
| import { AddonModForumModuleHandler, AddonModForumModuleHandlerService } from './services/handlers/module'; | ||||
| import { SITE_SCHEMA } from './services/offline-db'; | ||||
| 
 | ||||
| const routes: Routes = [ | ||||
|     { | ||||
|         path: AddonModForumModuleHandlerService.PAGE_NAME, | ||||
|         loadChildren: () => import('./forum-lazy.module').then(m => m.AddonModForumLazyModule), | ||||
|     }, | ||||
| ]; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     imports: [ | ||||
|         CoreMainMenuTabRoutingModule.forChild(routes), | ||||
|         AddonModForumComponentsModule, | ||||
|     ], | ||||
|     providers: [ | ||||
|         { | ||||
|             provide: CORE_SITE_SCHEMAS, | ||||
|             useValue: [SITE_SCHEMA], | ||||
|             multi: true, | ||||
|         }, | ||||
|         { | ||||
|             provide: APP_INITIALIZER, | ||||
|             multi: true, | ||||
|             useValue: () => CoreCourseModuleDelegate.instance.registerHandler(AddonModForumModuleHandler.instance), | ||||
|         }, | ||||
|     ], | ||||
| }) | ||||
| export class AddonModForumModule {} | ||||
							
								
								
									
										66
									
								
								src/addons/mod/forum/lang.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								src/addons/mod/forum/lang.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,66 @@ | ||||
| { | ||||
|     "addanewdiscussion": "Add a new discussion topic", | ||||
|     "addanewquestion": "Add a new question", | ||||
|     "addanewtopic": "Add a new topic", | ||||
|     "addtofavourites": "Star this discussion", | ||||
|     "advanced": "Advanced", | ||||
|     "cannotadddiscussion": "Adding discussions to this forum requires group membership.", | ||||
|     "cannotadddiscussionall": "You do not have permission to add a new discussion topic for all participants.", | ||||
|     "cannotcreatediscussion": "Could not create new discussion", | ||||
|     "couldnotadd": "Could not add your post due to an unknown error", | ||||
|     "couldnotupdate": "Could not update your post due to an unknown error", | ||||
|     "cutoffdatereached": "The cut-off date for posting to this forum is reached so you can no longer post to it.", | ||||
|     "delete": "Delete", | ||||
|     "deletedpost": "The post has been deleted", | ||||
|     "deletesure": "Are you sure you want to delete this post?", | ||||
|     "discussion": "Discussion", | ||||
|     "discussionlistsortbycreatedasc": "Sort by creation date in ascending order", | ||||
|     "discussionlistsortbycreateddesc": "Sort by creation date in descending order", | ||||
|     "discussionlistsortbylastpostasc": "Sort by last post creation date in ascending order", | ||||
|     "discussionlistsortbylastpostdesc": "Sort by last post creation date in descending order", | ||||
|     "discussionlistsortbyrepliesasc": "Sort by number of replies in ascending order", | ||||
|     "discussionlistsortbyrepliesdesc": "Sort by number of replies in descending order", | ||||
|     "discussionlocked": "This discussion has been locked so you can no longer reply to it.", | ||||
|     "discussionpinned": "Pinned", | ||||
|     "discussionsubscription": "Discussion subscription", | ||||
|     "edit": "Edit", | ||||
|     "erroremptymessage": "Post message cannot be empty", | ||||
|     "erroremptysubject": "Post subject cannot be empty.", | ||||
|     "errorgetforum": "Error getting forum data.", | ||||
|     "errorgetgroups": "Error getting group settings.", | ||||
|     "errorposttoallgroups": "Could not create new discussion in all groups.", | ||||
|     "favouriteupdated": "Your star option has been updated.", | ||||
|     "forumnodiscussionsyet": "There are no discussions yet in this forum.", | ||||
|     "group": "Group", | ||||
|     "lastpost": "Last post", | ||||
|     "lockdiscussion": "Lock this discussion", | ||||
|     "lockupdated": "The lock option has been updated.", | ||||
|     "message": "Message", | ||||
|     "modeflatnewestfirst": "Display replies flat, with newest first", | ||||
|     "modeflatoldestfirst": "Display replies flat, with oldest first", | ||||
|     "modenested": "Display replies in nested form", | ||||
|     "modulenameplural": "Forums", | ||||
|     "numdiscussions": "{{numdiscussions}} discussions", | ||||
|     "numreplies": "{{numreplies}} replies", | ||||
|     "pindiscussion": "Pin this discussion", | ||||
|     "pinupdated": "The pin option has been updated.", | ||||
|     "postisprivatereply": "This is a private reply. It is not visible to other participants.", | ||||
|     "posttoforum": "Post to forum", | ||||
|     "posttomygroups": "Post a copy to all groups", | ||||
|     "privatereply": "Reply privately", | ||||
|     "re": "Re:", | ||||
|     "refreshdiscussions": "Refresh discussions", | ||||
|     "refreshposts": "Refresh posts", | ||||
|     "removefromfavourites": "Unstar this discussion", | ||||
|     "reply": "Reply", | ||||
|     "replyplaceholder": "Write your reply...", | ||||
|     "subject": "Subject", | ||||
|     "tagarea_forum_posts": "Forum posts", | ||||
|     "thisforumhasduedate": "The due date for posting to this forum is {{$a}}.", | ||||
|     "thisforumisdue": "The due date for posting to this forum was {{$a}}.", | ||||
|     "unlockdiscussion": "Unlock this discussion", | ||||
|     "unpindiscussion": "Unpin this discussion", | ||||
|     "unread": "Unread", | ||||
|     "unreadpostsnumber": "{{$a}} unread posts", | ||||
|     "yourreply": "Your reply" | ||||
| } | ||||
							
								
								
									
										18
									
								
								src/addons/mod/forum/pages/index/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/addons/mod/forum/pages/index/index.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | ||||
| <ion-header> | ||||
|     <ion-toolbar> | ||||
|         <ion-buttons slot="start"> | ||||
|             <ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button> | ||||
|         </ion-buttons> | ||||
|         <ion-title> | ||||
|             <core-format-text [text]="title" contextLevel="module" [contextInstanceId]="module?.id" [courseId]="courseId"> | ||||
|             </core-format-text> | ||||
|         </ion-title> | ||||
|         <ion-buttons slot="end"> | ||||
|             <!-- The buttons defined by the component will be added in here. --> | ||||
|         </ion-buttons> | ||||
|     </ion-toolbar> | ||||
| </ion-header> | ||||
| 
 | ||||
| <ion-content> | ||||
|     <addon-mod-forum-index [module]="module" [courseId]="courseId" (dataRetrieved)="updateData($event)"></addon-mod-forum-index> | ||||
| </ion-content> | ||||
							
								
								
									
										49
									
								
								src/addons/mod/forum/pages/index/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/addons/mod/forum/pages/index/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,49 @@ | ||||
| // (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 { Component, OnInit } from '@angular/core'; | ||||
| 
 | ||||
| import { AddonModForumData } from '@addons/mod/forum/services/forum.service'; | ||||
| import { CoreCourseAnyModuleData } from '@features/course/services/course'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| 
 | ||||
| @Component({ | ||||
|     selector: 'page-addon-mod-forum-index', | ||||
|     templateUrl: 'index.html', | ||||
| }) | ||||
| export class AddonModForumIndexPage implements OnInit { | ||||
| 
 | ||||
|     title!: string; | ||||
|     module!: CoreCourseAnyModuleData; | ||||
|     courseId!: number; | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     ngOnInit(): void { | ||||
|         this.module = CoreNavigator.instance.getRouteParam<CoreCourseAnyModuleData>('module')!; | ||||
|         this.courseId = CoreNavigator.instance.getRouteNumberParam('courseId')!; | ||||
|         this.title = this.module?.name; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Update some data based on the forum instance. | ||||
|      * | ||||
|      * @param forum Forum instance. | ||||
|      */ | ||||
|     updateData(forum: AddonModForumData): void { | ||||
|         this.title = forum.name || this.title; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										2055
									
								
								src/addons/mod/forum/services/forum.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2055
									
								
								src/addons/mod/forum/services/forum.service.ts
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -1,132 +0,0 @@ | ||||
| // (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 { CoreSite, CoreSiteWSPreSets } from '@classes/site'; | ||||
| import { CoreSitesCommonWSOptions, CoreSites } from '@services/sites'; | ||||
| import { CoreWSExternalFile } from '@services/ws'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| 
 | ||||
| const ROOT_CACHE_KEY = 'mmaModForum:'; | ||||
| 
 | ||||
| /** | ||||
|  * Service that provides some features for forums. | ||||
|  * | ||||
|  * @todo Add all content. | ||||
|  */ | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class AddonModForumProvider { | ||||
| 
 | ||||
|     static readonly COMPONENT = 'mmaModForum'; | ||||
| 
 | ||||
|     /** | ||||
|      * Get cache key for forum data WS calls. | ||||
|      * | ||||
|      * @param courseId Course ID. | ||||
|      * @return Cache key. | ||||
|      */ | ||||
|     protected getForumDataCacheKey(courseId: number): string { | ||||
|         return ROOT_CACHE_KEY + 'forum:' + courseId; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get all course forums. | ||||
|      * | ||||
|      * @param courseId Course ID. | ||||
|      * @param options Other options. | ||||
|      * @return Promise resolved when the forums are retrieved. | ||||
|      */ | ||||
|     async getCourseForums(courseId: number, options: CoreSitesCommonWSOptions = {}): Promise<AddonModForumData[]> { | ||||
|         const site = await CoreSites.instance.getSite(options.siteId); | ||||
| 
 | ||||
|         const params: AddonModForumGetForumsByCoursesWSParams = { | ||||
|             courseids: [courseId], | ||||
|         }; | ||||
|         const preSets: CoreSiteWSPreSets = { | ||||
|             cacheKey: this.getForumDataCacheKey(courseId), | ||||
|             updateFrequency: CoreSite.FREQUENCY_RARELY, | ||||
|             component: AddonModForumProvider.COMPONENT, | ||||
|             ...CoreSites.instance.getReadingStrategyPreSets(options.readingStrategy), | ||||
|         }; | ||||
| 
 | ||||
|         return site.read('mod_forum_get_forums_by_courses', params, preSets); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Invalidates forum data. | ||||
|      * | ||||
|      * @param courseId Course ID. | ||||
|      * @return Promise resolved when the data is invalidated. | ||||
|      */ | ||||
|     async invalidateForumData(courseId: number): Promise<void> { | ||||
|         await CoreSites.instance.getCurrentSite()?.invalidateWsCacheForKey(this.getForumDataCacheKey(courseId)); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export class AddonModForum extends makeSingleton(AddonModForumProvider) {} | ||||
| 
 | ||||
| /** | ||||
|  * Params of mod_forum_get_forums_by_courses WS. | ||||
|  */ | ||||
| type AddonModForumGetForumsByCoursesWSParams = { | ||||
|     courseids?: number[]; // Array of Course IDs.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * General forum activity data. | ||||
|  */ | ||||
| export type AddonModForumData = { | ||||
|     id: number; // Forum id.
 | ||||
|     course: number; // Course id.
 | ||||
|     type: string; // The forum type.
 | ||||
|     name: string; // Forum name.
 | ||||
|     intro: string; // The forum intro.
 | ||||
|     introformat: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
 | ||||
|     introfiles?: CoreWSExternalFile[]; | ||||
|     duedate?: number; // Duedate for the user.
 | ||||
|     cutoffdate?: number; // Cutoffdate for the user.
 | ||||
|     assessed: number; // Aggregate type.
 | ||||
|     assesstimestart: number; // Assess start time.
 | ||||
|     assesstimefinish: number; // Assess finish time.
 | ||||
|     scale: number; // Scale.
 | ||||
|     // eslint-disable-next-line @typescript-eslint/naming-convention
 | ||||
|     grade_forum: number; // Whole forum grade.
 | ||||
|     // eslint-disable-next-line @typescript-eslint/naming-convention
 | ||||
|     grade_forum_notify: number; // Whether to send notifications to students upon grading by default.
 | ||||
|     maxbytes: number; // Maximum attachment size.
 | ||||
|     maxattachments: number; // Maximum number of attachments.
 | ||||
|     forcesubscribe: number; // Force users to subscribe.
 | ||||
|     trackingtype: number; // Subscription mode.
 | ||||
|     rsstype: number; // RSS feed for this activity.
 | ||||
|     rssarticles: number; // Number of RSS recent articles.
 | ||||
|     timemodified: number; // Time modified.
 | ||||
|     warnafter: number; // Post threshold for warning.
 | ||||
|     blockafter: number; // Post threshold for blocking.
 | ||||
|     blockperiod: number; // Time period for blocking.
 | ||||
|     completiondiscussions: number; // Student must create discussions.
 | ||||
|     completionreplies: number; // Student must post replies.
 | ||||
|     completionposts: number; // Student must post discussions or replies.
 | ||||
|     cmid: number; // Course module id.
 | ||||
|     numdiscussions?: number; // Number of discussions in the forum.
 | ||||
|     cancreatediscussions?: boolean; // If the user can create discussions.
 | ||||
|     lockdiscussionafter?: number; // After what period a discussion is locked.
 | ||||
|     istracked?: boolean; // If the user is tracking the forum.
 | ||||
|     unreadpostscount?: number; // The number of unread posts for tracked forums.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Data returned by mod_forum_get_forums_by_courses WS. | ||||
|  */ | ||||
| export type AddonModForumGetForumsByCoursesWSResponse = AddonModForumData[]; | ||||
							
								
								
									
										173
									
								
								src/addons/mod/forum/services/handlers/module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								src/addons/mod/forum/services/handlers/module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,173 @@ | ||||
| // (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, Type } from '@angular/core'; | ||||
| import { AddonModForum, AddonModForumProvider } from '../forum.service'; | ||||
| import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/course'; | ||||
| import { CoreCourseModule } from '@features/course/services/course-helper'; | ||||
| import { CoreNavigationOptions, CoreNavigator } from '@services/navigator'; | ||||
| import { makeSingleton, Translate } from '@singletons'; | ||||
| import { CoreEvents } from '@singletons/events'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate'; | ||||
| import { CoreConstants } from '@/core/constants'; | ||||
| import { AddonModForumIndexComponent } from '../../components/index'; | ||||
| 
 | ||||
| /** | ||||
|  * Handler to support forum modules. | ||||
|  */ | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class AddonModForumModuleHandlerService implements CoreCourseModuleHandler { | ||||
| 
 | ||||
|     static readonly PAGE_NAME = 'mod_forum'; | ||||
| 
 | ||||
|     name = 'AddonModForum'; | ||||
|     modName = 'forum'; | ||||
| 
 | ||||
|     supportedFeatures = { | ||||
|         [CoreConstants.FEATURE_GROUPS]: true, | ||||
|         [CoreConstants.FEATURE_GROUPINGS]: true, | ||||
|         [CoreConstants.FEATURE_MOD_INTRO]: true, | ||||
|         [CoreConstants.FEATURE_COMPLETION_TRACKS_VIEWS]: true, | ||||
|         [CoreConstants.FEATURE_COMPLETION_HAS_RULES]: true, | ||||
|         [CoreConstants.FEATURE_GRADE_HAS_GRADE]: true, | ||||
|         [CoreConstants.FEATURE_GRADE_OUTCOMES]: true, | ||||
|         [CoreConstants.FEATURE_BACKUP_MOODLE2]: true, | ||||
|         [CoreConstants.FEATURE_SHOW_DESCRIPTION]: true, | ||||
|         [CoreConstants.FEATURE_RATE]: true, | ||||
|         [CoreConstants.FEATURE_PLAGIARISM]: true, | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the handler is enabled on a site level. | ||||
|      * | ||||
|      * @return Whether or not the handler is enabled on a site level. | ||||
|      */ | ||||
|     isEnabled(): Promise<boolean> { | ||||
|         return Promise.resolve(true); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the data required to display the module in the course contents view. | ||||
|      * | ||||
|      * @param module The module object. | ||||
|      * @param courseId The course ID. | ||||
|      * @param sectionId The section ID. | ||||
|      * @return Data to render the module. | ||||
|      */ | ||||
|     getData(module: CoreCourseAnyModuleData, courseId: number): CoreCourseModuleHandlerData { | ||||
|         const data: CoreCourseModuleHandlerData = { | ||||
|             icon: CoreCourse.instance.getModuleIconSrc(this.modName, 'modicon' in module ? module.modicon : undefined), | ||||
|             title: module.name, | ||||
|             class: 'addon-mod_forum-handler', | ||||
|             showDownloadButton: true, | ||||
|             action(_: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): void { | ||||
|                 options = options || {}; | ||||
|                 options.params = options.params || {}; | ||||
|                 Object.assign(options.params, { module }); | ||||
| 
 | ||||
|                 CoreNavigator.instance.navigateToSitePath( | ||||
|                     `${AddonModForumModuleHandlerService.PAGE_NAME}/${courseId}/${module.id}`, | ||||
|                     options, | ||||
|                 ); | ||||
|             }, | ||||
|         }; | ||||
| 
 | ||||
|         if ('afterlink' in module && !!module.afterlink) { | ||||
|             data.extraBadgeColor = ''; | ||||
|             const match = />(\d+)[^<]+/.exec(module.afterlink); | ||||
|             data.extraBadge = match ? Translate.instance.instant('addon.mod_forum.unreadpostsnumber', { $a : match[1] }) : ''; | ||||
|         } else { | ||||
|             this.updateExtraBadge(data, courseId, module.id); | ||||
|         } | ||||
| 
 | ||||
|         const event = CoreEvents.on( | ||||
|             AddonModForumProvider.MARK_READ_EVENT, | ||||
|             (eventData: { courseId?: number; moduleId?: number; siteId?: string }) => { | ||||
|                 if (eventData.courseId !== courseId || eventData.moduleId !== module.id) { | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
|                 this.updateExtraBadge(data, eventData.courseId, eventData.moduleId, eventData.siteId); | ||||
|             }, | ||||
|             CoreSites.instance.getCurrentSiteId(), | ||||
|         ); | ||||
| 
 | ||||
|         data.onDestroy = () => event.off(); | ||||
| 
 | ||||
|         return data; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the component to render the module. This is needed to support singleactivity course format. | ||||
|      * The component returned must implement CoreCourseModuleMainComponent. | ||||
|      * | ||||
|      * @return The component to use, undefined if not found. | ||||
|      */ | ||||
|     async getMainComponent(): Promise<Type<unknown> | undefined> { | ||||
|         return AddonModForumIndexComponent; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Whether to display the course refresher in single activity course format. If it returns false, a refresher must be | ||||
|      * included in the template that calls the doRefresh method of the component. Defaults to true. | ||||
|      * | ||||
|      * @return Whether the refresher should be displayed. | ||||
|      */ | ||||
|     displayRefresherInSingleActivity(): boolean { | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Triggers an update for the extra badge text. | ||||
|      * | ||||
|      * @param data Course Module Handler data. | ||||
|      * @param courseId Course ID. | ||||
|      * @param moduleId Course module ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      */ | ||||
|     async updateExtraBadge(data: CoreCourseModuleHandlerData, courseId: number, moduleId: number, siteId?: string): Promise<void> { | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         if (!siteId) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         data.extraBadge =  Translate.instance.instant('core.loading'); | ||||
|         data.extraBadgeColor = 'light'; | ||||
| 
 | ||||
|         await CoreUtils.instance.ignoreErrors(AddonModForum.instance.invalidateForumData(courseId)); | ||||
| 
 | ||||
|         try { | ||||
|             // Handle unread posts.
 | ||||
|             const forum = await AddonModForum.instance.getForum(courseId, moduleId, { siteId }); | ||||
| 
 | ||||
|             data.extraBadgeColor = ''; | ||||
|             data.extraBadge = forum.unreadpostscount | ||||
|                 ? Translate.instance.instant( | ||||
|                     'addon.mod_forum.unreadpostsnumber', | ||||
|                     { $a : forum.unreadpostscount }, | ||||
|                 ) | ||||
|                 : ''; | ||||
|         } catch (error) { | ||||
|             // Ignore errors.
 | ||||
|             data.extraBadgeColor = ''; | ||||
|             data.extraBadge = ''; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export class AddonModForumModuleHandler extends makeSingleton(AddonModForumModuleHandlerService) {} | ||||
							
								
								
									
										507
									
								
								src/addons/mod/forum/services/helper.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										507
									
								
								src/addons/mod/forum/services/helper.service.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,507 @@ | ||||
| // (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 { CoreFileEntry, CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader'; | ||||
| import { CoreUser } from '@features/user/services/user'; | ||||
| import { CoreApp } from '@services/app'; | ||||
| import { CoreFile } from '@services/file'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreTimeUtils } from '@services/utils/time'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { makeSingleton, Translate } from '@singletons'; | ||||
| import { AddonModForum, AddonModForumData, AddonModForumProvider } from './forum.service'; | ||||
| import { AddonModForumOffline } from './offline.service'; | ||||
| 
 | ||||
| /** | ||||
|  * Service that provides some features for forums. | ||||
|  */ | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class AddonModForumHelperProvider { | ||||
| 
 | ||||
|     /** | ||||
|      * Add a new discussion. | ||||
|      * | ||||
|      * @param forumId Forum ID. | ||||
|      * @param name Forum name. | ||||
|      * @param courseId Course ID the forum belongs to. | ||||
|      * @param subject New discussion's subject. | ||||
|      * @param message New discussion's message. | ||||
|      * @param attachments New discussion's attachments. | ||||
|      * @param options Options (subscribe, pin, ...). | ||||
|      * @param groupIds Groups this discussion belongs to. | ||||
|      * @param timeCreated The time the discussion was created. Only used when editing discussion. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with ids of the created discussions or null if stored offline | ||||
|      */ | ||||
|     async addNewDiscussion( | ||||
|         forumId: number, | ||||
|         name: string, | ||||
|         courseId: number, | ||||
|         subject: string, | ||||
|         message: string, | ||||
|         attachments?: any[], | ||||
|         options?: any, | ||||
|         groupIds?: number[], | ||||
|         timeCreated?: number, | ||||
|         siteId?: string, | ||||
|     ): Promise<number[] | null> { | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
|         groupIds = (groupIds && groupIds.length > 0) ? groupIds : [0]; | ||||
| 
 | ||||
|         let saveOffline = false; | ||||
|         const attachmentsIds: number[] = []; | ||||
|         let offlineAttachments: any; | ||||
| 
 | ||||
|         // Convenience function to store a message to be synchronized later.
 | ||||
|         const storeOffline = async (): Promise<void> => { | ||||
|             // Multiple groups, the discussion is being posted to all groups.
 | ||||
|             const groupId = groupIds!.length > 1 ? AddonModForumProvider.ALL_GROUPS : groupIds![0]; | ||||
| 
 | ||||
|             if (offlineAttachments) { | ||||
|                 options.attachmentsid = offlineAttachments; | ||||
|             } | ||||
| 
 | ||||
|             await AddonModForumOffline.instance.addNewDiscussion( | ||||
|                 forumId, | ||||
|                 name, | ||||
|                 courseId, | ||||
|                 subject, | ||||
|                 message, | ||||
|                 options, | ||||
|                 groupId, | ||||
|                 timeCreated, | ||||
|                 siteId, | ||||
|             ); | ||||
|         }; | ||||
| 
 | ||||
|         // First try to upload attachments, once per group.
 | ||||
|         if (attachments && attachments.length > 0) { | ||||
|             const promises = groupIds.map( | ||||
|                 () => this | ||||
|                     .uploadOrStoreNewDiscussionFiles(forumId, timeCreated || 0, attachments, false) | ||||
|                     .then(attach => attachmentsIds.push(attach)), | ||||
|             ); | ||||
| 
 | ||||
|             try { | ||||
|                 await Promise.all(promises); | ||||
|             } catch (error) { | ||||
|                 // Cannot upload them in online, save them in offline.
 | ||||
|                 saveOffline = true; | ||||
| 
 | ||||
|                 const attach = await this.uploadOrStoreNewDiscussionFiles(forumId, timeCreated || 0, attachments, true); | ||||
| 
 | ||||
|                 offlineAttachments = attach; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // If we are editing an offline discussion, discard previous first.
 | ||||
|         if (timeCreated) { | ||||
|             await AddonModForumOffline.instance.deleteNewDiscussion(forumId, timeCreated, siteId); | ||||
|         } | ||||
| 
 | ||||
|         if (saveOffline || !CoreApp.instance.isOnline()) { | ||||
|             await storeOffline(); | ||||
| 
 | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         const errors: Error[] = []; | ||||
|         const discussionIds: number[] = []; | ||||
|         const promises = groupIds.map(async (groupId, index) => { | ||||
|             const groupOptions = CoreUtils.instance.clone(options); | ||||
| 
 | ||||
|             if (attachmentsIds[index]) { | ||||
|                 groupOptions.attachmentsid = attachmentsIds[index]; | ||||
|             } | ||||
| 
 | ||||
|             try { | ||||
|                 const discussionId = await AddonModForum.instance.addNewDiscussionOnline( | ||||
|                     forumId, | ||||
|                     subject, | ||||
|                     message, | ||||
|                     groupOptions, | ||||
|                     groupId, | ||||
|                     siteId, | ||||
|                 ); | ||||
| 
 | ||||
|                 discussionIds.push(discussionId); | ||||
|             } catch (error) { | ||||
|                 errors.push(error); | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         await Promise.all(promises); | ||||
| 
 | ||||
|         if (errors.length == groupIds.length) { | ||||
|             // All requests have failed.
 | ||||
|             for (let i = 0; i < errors.length; i++) { | ||||
|                 if (CoreUtils.instance.isWebServiceError(errors[i]) || (attachments && attachments.length > 0)) { | ||||
|                     // The WebService has thrown an error or offline not supported, reject.
 | ||||
|                     throw errors[i]; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             // Couldn't connect to server, store offline.
 | ||||
|             await storeOffline(); | ||||
| 
 | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         return discussionIds; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Convert offline reply to online format in order to be compatible with them. | ||||
|      * | ||||
|      * @param offlineReply Offline version of the reply. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the object converted to Online. | ||||
|      */ | ||||
|     convertOfflineReplyToOnline(offlineReply: any, siteId?: string): Promise<any> { | ||||
|         const reply: any = { | ||||
|             id: -offlineReply.timecreated, | ||||
|             discussionid: offlineReply.discussionid, | ||||
|             parentid: offlineReply.postid, | ||||
|             hasparent: !!offlineReply.postid, | ||||
|             author: { | ||||
|                 id: offlineReply.userid, | ||||
|             }, | ||||
|             timecreated: false, | ||||
|             subject: offlineReply.subject, | ||||
|             message: offlineReply.message, | ||||
|             attachments: [], | ||||
|             capabilities: { | ||||
|                 reply: false, | ||||
|             }, | ||||
|             unread: false, | ||||
|             isprivatereply: offlineReply.options && offlineReply.options.private, | ||||
|             tags: null, | ||||
|         }; | ||||
|         const promises: Promise<void>[] = []; | ||||
| 
 | ||||
|         // Treat attachments if any.
 | ||||
|         if (offlineReply.options && offlineReply.options.attachmentsid) { | ||||
|             reply.attachments = offlineReply.options.attachmentsid.online || []; | ||||
| 
 | ||||
|             if (offlineReply.options.attachmentsid.offline) { | ||||
|                 promises.push( | ||||
|                     this | ||||
|                         .getReplyStoredFiles(offlineReply.forumid, reply.parentid, siteId, offlineReply.userid) | ||||
|                         .then(files => reply.attachments = reply.attachments.concat(files)), | ||||
|                 ); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // Get user data.
 | ||||
|         promises.push( | ||||
|             CoreUtils.instance.ignoreErrors( | ||||
|                 CoreUser.instance | ||||
|                     .getProfile(offlineReply.userid, offlineReply.courseid, true) | ||||
|                     .then(user => { | ||||
|                         reply.author.fullname = user.fullname; | ||||
|                         reply.author.urls = { profileimage: user.profileimageurl }; | ||||
| 
 | ||||
|                         return; | ||||
|                     }), | ||||
|             ), | ||||
|         ); | ||||
| 
 | ||||
|         return Promise.all(promises).then(() => { | ||||
|             reply.attachment = reply.attachments.length > 0 ? 1 : 0; | ||||
| 
 | ||||
|             return reply; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete stored attachment files for a new discussion. | ||||
|      * | ||||
|      * @param forumId Forum ID. | ||||
|      * @param timecreated The time the discussion was created. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when deleted. | ||||
|      */ | ||||
|     async deleteNewDiscussionStoredFiles(forumId: number, timecreated: number, siteId?: string): Promise<void> { | ||||
|         const folderPath = await AddonModForumOffline.instance.getNewDiscussionFolder(forumId, timecreated, siteId); | ||||
| 
 | ||||
|         // Ignore any errors, CoreFileProvider.removeDir fails if folder doesn't exist.
 | ||||
|         await CoreUtils.instance.ignoreErrors(CoreFile.instance.removeDir(folderPath)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete stored attachment files for a reply. | ||||
|      * | ||||
|      * @param forumId Forum ID. | ||||
|      * @param postId ID of the post being replied. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @param userId User the reply belongs to. If not defined, current user in site. | ||||
|      * @return Promise resolved when deleted. | ||||
|      */ | ||||
|     async deleteReplyStoredFiles(forumId: number, postId: number, siteId?: string, userId?: number): Promise<void> { | ||||
|         const folderPath = await AddonModForumOffline.instance.getReplyFolder(forumId, postId, siteId, userId); | ||||
| 
 | ||||
|         // Ignore any errors, CoreFileProvider.removeDir fails if folder doesn't exist.
 | ||||
|         await CoreUtils.instance.ignoreErrors(CoreFile.instance.removeDir(folderPath)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns the availability message of the given forum. | ||||
|      * | ||||
|      * @param forum Forum instance. | ||||
|      * @return Message or null if the forum has no cut-off or due date. | ||||
|      */ | ||||
|     getAvailabilityMessage(forum: AddonModForumData): string | null { | ||||
|         if (this.isCutoffDateReached(forum)) { | ||||
|             return Translate.instance.instant('addon.mod_forum.cutoffdatereached'); | ||||
|         } | ||||
| 
 | ||||
|         if (this.isDueDateReached(forum)) { | ||||
|             const dueDate = CoreTimeUtils.instance.userDate(forum.duedate * 1000); | ||||
| 
 | ||||
|             return Translate.instance.instant('addon.mod_forum.thisforumisdue', { $a: dueDate }); | ||||
|         } | ||||
| 
 | ||||
|         if ((forum.duedate ?? 0) > 0) { | ||||
|             const dueDate = CoreTimeUtils.instance.userDate(forum.duedate! * 1000); | ||||
| 
 | ||||
|             return Translate.instance.instant('addon.mod_forum.thisforumhasduedate', { $a: dueDate }); | ||||
|         } | ||||
| 
 | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a forum discussion by id. | ||||
|      * | ||||
|      * This function is inefficient because it needs to fetch all discussion pages in the worst case. | ||||
|      * | ||||
|      * @param forumId Forum ID. | ||||
|      * @param cmId Forum cmid | ||||
|      * @param discussionId Discussion ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the discussion data. | ||||
|      */ | ||||
|     getDiscussionById(forumId: number, cmId: number, discussionId: number, siteId?: string): Promise<any> { | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         const findDiscussion = async (page: number): Promise<any> => { | ||||
|             const response = await AddonModForum.instance.getDiscussions(forumId, { | ||||
|                 cmId, | ||||
|                 page, | ||||
|                 siteId, | ||||
|             }); | ||||
| 
 | ||||
|             if (response.discussions && response.discussions.length > 0) { | ||||
|                 // Note that discussion.id is the main post ID but discussion ID is discussion.discussion.
 | ||||
|                 const discussion = response.discussions.find((discussion) => discussion.discussion == discussionId); | ||||
| 
 | ||||
|                 if (discussion) { | ||||
|                     return discussion; | ||||
|                 } | ||||
| 
 | ||||
|                 if (response.canLoadMore) { | ||||
|                     return findDiscussion(page + 1); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             throw new Error('Discussion not found'); | ||||
|         }; | ||||
| 
 | ||||
|         return findDiscussion(0); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a list of stored attachment files for a new discussion. See AddonModForumHelper#storeNewDiscussionFiles. | ||||
|      * | ||||
|      * @param forumId Forum ID. | ||||
|      * @param timecreated The time the discussion was created. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the files. | ||||
|      */ | ||||
|     async getNewDiscussionStoredFiles(forumId: number, timecreated: number, siteId?: string): Promise<any[]> { | ||||
|         const folderPath = await AddonModForumOffline.instance.getNewDiscussionFolder(forumId, timecreated, siteId); | ||||
| 
 | ||||
|         return CoreFileUploader.instance.getStoredFiles(folderPath); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a list of stored attachment files for a reply. See AddonModForumHelper#storeReplyFiles. | ||||
|      * | ||||
|      * @param forumId Forum ID. | ||||
|      * @param postId ID of the post being replied. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @param userId User the reply belongs to. If not defined, current user in site. | ||||
|      * @return Promise resolved with the files. | ||||
|      */ | ||||
|     async getReplyStoredFiles(forumId: number, postId: number, siteId?: string, userId?: number): Promise<any[]> { | ||||
|         const folderPath = await AddonModForumOffline.instance.getReplyFolder(forumId, postId, siteId, userId); | ||||
| 
 | ||||
|         return CoreFileUploader.instance.getStoredFiles(folderPath); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the data of a post/discussion has changed. | ||||
|      * | ||||
|      * @param post Current data. | ||||
|      * @param original Original ata. | ||||
|      * @return True if data has changed, false otherwise. | ||||
|      */ | ||||
|     hasPostDataChanged(post: any, original?: any): boolean { | ||||
|         if (!original || original.subject == null) { | ||||
|             // There is no original data, assume it hasn't changed.
 | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         if (post.subject != original.subject || post.message != original.message) { | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         if (post.isprivatereply != original.isprivatereply) { | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         return CoreFileUploader.instance.areFileListDifferent(post.files, original.files); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Is the cutoff date for the forum reached? | ||||
|      * | ||||
|      * @param forum Forum instance. | ||||
|      */ | ||||
|     isCutoffDateReached(forum: any): boolean { | ||||
|         const now = Date.now() / 1000; | ||||
| 
 | ||||
|         return forum.cutoffdate > 0 && forum.cutoffdate < now; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Is the due date for the forum reached? | ||||
|      * | ||||
|      * @param forum Forum instance. | ||||
|      */ | ||||
|     isDueDateReached(forum: AddonModForumData): forum is AddonModForumData & { duedate: number } { | ||||
|         const now = Date.now() / 1000; | ||||
|         const duedate = forum.duedate ?? 0; | ||||
| 
 | ||||
|         return duedate > 0 && duedate < now; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Given a list of files (either online files or local files), store the local files in a local folder | ||||
|      * to be submitted later. | ||||
|      * | ||||
|      * @param forumId Forum ID. | ||||
|      * @param timecreated The time the discussion was created. | ||||
|      * @param files List of files. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved if success, rejected otherwise. | ||||
|      */ | ||||
|     async storeNewDiscussionFiles( | ||||
|         forumId: number, | ||||
|         timecreated: number, | ||||
|         files: CoreFileEntry[], | ||||
|         siteId?: string, | ||||
|     ): Promise<CoreFileUploaderStoreFilesResult> { | ||||
|         // Get the folder where to store the files.
 | ||||
|         const folderPath = await AddonModForumOffline.instance.getNewDiscussionFolder(forumId, timecreated, siteId); | ||||
| 
 | ||||
|         return CoreFileUploader.instance.storeFilesToUpload(folderPath, files); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Given a list of files (either online files or local files), store the local files in a local folder | ||||
|      * to be submitted later. | ||||
|      * | ||||
|      * @param forumId Forum ID. | ||||
|      * @param postId ID of the post being replied. | ||||
|      * @param files List of files. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @param userId User the reply belongs to. If not defined, current user in site. | ||||
|      * @return Promise resolved if success, rejected otherwise. | ||||
|      */ | ||||
|     async storeReplyFiles(forumId: number, postId: number, files: any[], siteId?: string, userId?: number): Promise<void> { | ||||
|         // Get the folder where to store the files.
 | ||||
|         const folderPath = await AddonModForumOffline.instance.getReplyFolder(forumId, postId, siteId, userId); | ||||
| 
 | ||||
|         await CoreFileUploader.instance.storeFilesToUpload(folderPath, files); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Upload or store some files for a new discussion, depending if the user is offline or not. | ||||
|      * | ||||
|      * @param forumId Forum ID. | ||||
|      * @param timecreated The time the discussion was created. | ||||
|      * @param files List of files. | ||||
|      * @param offline True if files sould be stored for offline, false to upload them. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved if success. | ||||
|      */ | ||||
|     uploadOrStoreNewDiscussionFiles( | ||||
|         forumId: number, | ||||
|         timecreated: number, | ||||
|         files: CoreFileEntry[], | ||||
|         offline: true, | ||||
|         siteId?: string, | ||||
|     ): Promise<CoreFileUploaderStoreFilesResult>; | ||||
|     uploadOrStoreNewDiscussionFiles( | ||||
|         forumId: number, | ||||
|         timecreated: number, | ||||
|         files: CoreFileEntry[], | ||||
|         offline: false, | ||||
|         siteId?: string, | ||||
|     ): Promise<number>; | ||||
|     uploadOrStoreNewDiscussionFiles( | ||||
|         forumId: number, | ||||
|         timecreated: number, | ||||
|         files: CoreFileEntry[], | ||||
|         offline: boolean, | ||||
|         siteId?: string, | ||||
|     ): Promise<CoreFileUploaderStoreFilesResult | number> { | ||||
|         if (offline) { | ||||
|             return this.storeNewDiscussionFiles(forumId, timecreated, files, siteId); | ||||
|         } else { | ||||
|             return CoreFileUploader.instance.uploadOrReuploadFiles(files, AddonModForumProvider.COMPONENT, forumId, siteId); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Upload or store some files for a reply, depending if the user is offline or not. | ||||
|      * | ||||
|      * @param forumId Forum ID. | ||||
|      * @param postId ID of the post being replied. | ||||
|      * @param files List of files. | ||||
|      * @param offline True if files sould be stored for offline, false to upload them. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @param userId User the reply belongs to. If not defined, current user in site. | ||||
|      * @return Promise resolved if success. | ||||
|      */ | ||||
|     uploadOrStoreReplyFiles( | ||||
|         forumId: number, | ||||
|         postId: number, | ||||
|         files: any[], | ||||
|         offline: boolean, | ||||
|         siteId?: string, | ||||
|         userId?: number, | ||||
|     ): Promise<any> { | ||||
|         if (offline) { | ||||
|             return this.storeReplyFiles(forumId, postId, files, siteId, userId); | ||||
|         } else { | ||||
|             return CoreFileUploader.instance.uploadOrReuploadFiles(files, AddonModForumProvider.COMPONENT, forumId, siteId); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export class AddonModForumHelper extends makeSingleton(AddonModForumHelperProvider) {} | ||||
							
								
								
									
										115
									
								
								src/addons/mod/forum/services/offline-db.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								src/addons/mod/forum/services/offline-db.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,115 @@ | ||||
| // (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 { CoreSiteSchema } from '@services/sites'; | ||||
| 
 | ||||
| /** | ||||
|  * Database variables for AddonModForum service. | ||||
|  */ | ||||
| export const DISCUSSIONS_TABLE = 'addon_mod_forum_discussions'; | ||||
| export const REPLIES_TABLE = 'addon_mod_forum_replies'; | ||||
| export const SITE_SCHEMA: CoreSiteSchema = { | ||||
|     name: 'AddonModForumOfflineProvider', | ||||
|     version: 1, | ||||
|     tables: [ | ||||
|         { | ||||
|             name: DISCUSSIONS_TABLE, | ||||
|             columns: [ | ||||
|                 { | ||||
|                     name: 'forumid', | ||||
|                     type: 'INTEGER', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'name', | ||||
|                     type: 'TEXT', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'courseid', | ||||
|                     type: 'INTEGER', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'subject', | ||||
|                     type: 'TEXT', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'message', | ||||
|                     type: 'TEXT', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'options', | ||||
|                     type: 'TEXT', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'groupid', | ||||
|                     type: 'INTEGER', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'userid', | ||||
|                     type: 'INTEGER', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'timecreated', | ||||
|                     type: 'INTEGER', | ||||
|                 }, | ||||
|             ], | ||||
|             primaryKeys: ['forumid', 'userid', 'timecreated'], | ||||
|         }, | ||||
|         { | ||||
|             name: REPLIES_TABLE, | ||||
|             columns: [ | ||||
|                 { | ||||
|                     name: 'postid', | ||||
|                     type: 'INTEGER', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'discussionid', | ||||
|                     type: 'INTEGER', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'forumid', | ||||
|                     type: 'INTEGER', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'name', | ||||
|                     type: 'TEXT', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'courseid', | ||||
|                     type: 'INTEGER', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'subject', | ||||
|                     type: 'TEXT', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'message', | ||||
|                     type: 'TEXT', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'options', | ||||
|                     type: 'TEXT', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'userid', | ||||
|                     type: 'INTEGER', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'timecreated', | ||||
|                     type: 'INTEGER', | ||||
|                 }, | ||||
|             ], | ||||
|             primaryKeys: ['postid', 'userid'], | ||||
|         }, | ||||
|     ], | ||||
| }; | ||||
							
								
								
									
										436
									
								
								src/addons/mod/forum/services/offline.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										436
									
								
								src/addons/mod/forum/services/offline.service.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,436 @@ | ||||
| // (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 { CoreFile } from '@services/file'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| import { AddonModForumProvider } from './forum.service'; | ||||
| import { DISCUSSIONS_TABLE, REPLIES_TABLE } from './offline-db'; | ||||
| 
 | ||||
| /** | ||||
|  * Service to handle offline forum. | ||||
|  */ | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class AddonModForumOfflineProvider { | ||||
| 
 | ||||
|     /** | ||||
|      * Delete a forum offline discussion. | ||||
|      * | ||||
|      * @param forumId Forum ID. | ||||
|      * @param timeCreated The time the discussion was created. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @param userId User the discussion belongs to. If not defined, current user in site. | ||||
|      * @return Promise resolved if stored, rejected if failure. | ||||
|      */ | ||||
|     async deleteNewDiscussion(forumId: number, timeCreated: number, siteId?: string, userId?: number): Promise<void> { | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
|         const conditions = { | ||||
|             forumid: forumId, | ||||
|             userid: userId || site.getUserId(), | ||||
|             timecreated: timeCreated, | ||||
|         }; | ||||
| 
 | ||||
|         await site.getDb().deleteRecords(DISCUSSIONS_TABLE, conditions); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a forum offline discussion. | ||||
|      * | ||||
|      * @param forumId Forum ID. | ||||
|      * @param timeCreated The time the discussion was created. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @param userId User the discussion belongs to. If not defined, current user in site. | ||||
|      * @return Promise resolved if stored, rejected if failure. | ||||
|      */ | ||||
|     async getNewDiscussion( | ||||
|         forumId: number, | ||||
|         timeCreated: number, | ||||
|         siteId?: string, | ||||
|         userId?: number, | ||||
|     ): Promise<AddonModForumOfflineDiscussion> { | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
|         const conditions = { | ||||
|             forumid: forumId, | ||||
|             userid: userId || site.getUserId(), | ||||
|             timecreated: timeCreated, | ||||
|         }; | ||||
| 
 | ||||
|         const record = await site.getDb().getRecord<AddonModForumOfflineDiscussionRecord>(DISCUSSIONS_TABLE, conditions); | ||||
| 
 | ||||
|         return this.parseRecordOptions(record); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get all offline new discussions. | ||||
|      * | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with discussions. | ||||
|      */ | ||||
|     async getAllNewDiscussions(siteId?: string): Promise<AddonModForumOfflineDiscussion[]> { | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
|         const records = await site.getDb().getRecords<AddonModForumOfflineDiscussionRecord>(DISCUSSIONS_TABLE); | ||||
| 
 | ||||
|         return this.parseRecordsOptions(records); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if there are offline new discussions to send. | ||||
|      * | ||||
|      * @param forumId Forum ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @param userId User the discussions belong to. If not defined, current user in site. | ||||
|      * @return Promise resolved with boolean: true if has offline answers, false otherwise. | ||||
|      */ | ||||
|     async hasNewDiscussions(forumId: number, siteId?: string, userId?: number): Promise<boolean> { | ||||
|         try { | ||||
|             const discussions = await this.getNewDiscussions(forumId, siteId, userId); | ||||
| 
 | ||||
|             return !!discussions.length; | ||||
|         } catch (error) { | ||||
|             // No offline data found, return false.
 | ||||
| 
 | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get new discussions to be synced. | ||||
|      * | ||||
|      * @param forumId Forum ID to get. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @param userId User the discussions belong to. If not defined, current user in site. | ||||
|      * @return Promise resolved with the object to be synced. | ||||
|      */ | ||||
|     async getNewDiscussions(forumId: number, siteId?: string, userId?: number): Promise<AddonModForumOfflineDiscussion[]> { | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
|         const conditions = { | ||||
|             forumid: forumId, | ||||
|             userid: userId || site.getUserId(), | ||||
|         }; | ||||
| 
 | ||||
|         const records = await site.getDb().getRecords<AddonModForumOfflineDiscussionRecord>(DISCUSSIONS_TABLE, conditions); | ||||
| 
 | ||||
|         return this.parseRecordsOptions(records); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Offline version for adding a new discussion to a forum. | ||||
|      * | ||||
|      * @param forumId Forum ID. | ||||
|      * @param name Forum name. | ||||
|      * @param courseId Course ID the forum belongs to. | ||||
|      * @param subject New discussion's subject. | ||||
|      * @param message New discussion's message. | ||||
|      * @param options Options (subscribe, pin, ...). | ||||
|      * @param groupId Group this discussion belongs to. | ||||
|      * @param timeCreated The time the discussion was created. If not defined, current time. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @param userId User the discussion belong to. If not defined, current user in site. | ||||
|      * @return Promise resolved when new discussion is successfully saved. | ||||
|      */ | ||||
|     async addNewDiscussion( | ||||
|         forumId: number, | ||||
|         name: string, | ||||
|         courseId: number, | ||||
|         subject: string, | ||||
|         message: string, | ||||
|         options?: AddonModForumDiscussionOptions, | ||||
|         groupId?: number, | ||||
|         timeCreated?: number, | ||||
|         siteId?: string, | ||||
|         userId?: number, | ||||
|     ): Promise<void> { | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
|         const data: AddonModForumOfflineDiscussionRecord = { | ||||
|             forumid: forumId, | ||||
|             name: name, | ||||
|             courseid: courseId, | ||||
|             subject: subject, | ||||
|             message: message, | ||||
|             options: JSON.stringify(options || {}), | ||||
|             groupid: groupId || AddonModForumProvider.ALL_PARTICIPANTS, | ||||
|             userid: userId || site.getUserId(), | ||||
|             timecreated: timeCreated || new Date().getTime(), | ||||
|         }; | ||||
| 
 | ||||
|         await site.getDb().insertRecord(DISCUSSIONS_TABLE, data); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete forum offline replies. | ||||
|      * | ||||
|      * @param postId ID of the post being replied. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @param userId User the reply belongs to. If not defined, current user in site. | ||||
|      * @return Promise resolved if stored, rejected if failure. | ||||
|      */ | ||||
|     async deleteReply(postId: number, siteId?: string, userId?: number): Promise<void> { | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
|         const conditions = { | ||||
|             postid: postId, | ||||
|             userid: userId || site.getUserId(), | ||||
|         }; | ||||
| 
 | ||||
|         await site.getDb().deleteRecords(REPLIES_TABLE, conditions); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get all offline replies. | ||||
|      * | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with replies. | ||||
|      */ | ||||
|     async getAllReplies(siteId?: string): Promise<AddonModForumOfflineReply[]> { | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
|         const records = await site.getDb().getRecords<AddonModForumOfflineReplyRecord>(REPLIES_TABLE); | ||||
| 
 | ||||
|         return this.parseRecordsOptions(records); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if there is an offline reply for a forum to be synced. | ||||
|      * | ||||
|      * @param forumId ID of the forum being replied. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @param userId User the replies belong to. If not defined, current user in site. | ||||
|      * @return Promise resolved with boolean: true if has offline answers, false otherwise. | ||||
|      */ | ||||
|     async hasForumReplies(forumId: number, siteId?: string, userId?: number): Promise<boolean> { | ||||
|         try { | ||||
|             const replies = await this.getForumReplies(forumId, siteId, userId); | ||||
| 
 | ||||
|             return !!replies.length; | ||||
|         } catch (error) { | ||||
|             // No offline data found, return false.
 | ||||
| 
 | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the replies of a forum to be synced. | ||||
|      * | ||||
|      * @param forumId ID of the forum being replied. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @param userId User the replies belong to. If not defined, current user in site. | ||||
|      * @return Promise resolved with replies. | ||||
|      */ | ||||
|     async getForumReplies(forumId: number, siteId?: string, userId?: number): Promise<AddonModForumOfflineReply[]> { | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
|         const conditions = { | ||||
|             forumid: forumId, | ||||
|             userid: userId || site.getUserId(), | ||||
|         }; | ||||
| 
 | ||||
|         const records = await site.getDb().getRecords<AddonModForumOfflineReplyRecord>(REPLIES_TABLE, conditions); | ||||
| 
 | ||||
|         return this.parseRecordsOptions(records); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if there is an offline reply to be synced. | ||||
|      * | ||||
|      * @param discussionId ID of the discussion the user is replying to. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @param userId User the replies belong to. If not defined, current user in site. | ||||
|      * @return Promise resolved with boolean: true if has offline answers, false otherwise. | ||||
|      */ | ||||
|     async hasDiscussionReplies(discussionId: number, siteId?: string, userId?: number): Promise<boolean> { | ||||
|         try { | ||||
|             const replies = await this.getDiscussionReplies(discussionId, siteId, userId); | ||||
| 
 | ||||
|             return !!replies.length; | ||||
|         } catch (error) { | ||||
|             // No offline data found, return false.
 | ||||
| 
 | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the replies of a discussion to be synced. | ||||
|      * | ||||
|      * @param discussionId ID of the discussion the user is replying to. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @param userId User the replies belong to. If not defined, current user in site. | ||||
|      * @return Promise resolved with discussions. | ||||
|      */ | ||||
|     async getDiscussionReplies(discussionId: number, siteId?: string, userId?: number): Promise<AddonModForumOfflineReply[]> { | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
|         const conditions = { | ||||
|             discussionid: discussionId, | ||||
|             userid: userId || site.getUserId(), | ||||
|         }; | ||||
| 
 | ||||
|         const records = await site.getDb().getRecords<AddonModForumOfflineReplyRecord>(REPLIES_TABLE, conditions); | ||||
| 
 | ||||
|         return this.parseRecordsOptions(records); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Offline version for replying to a certain post. | ||||
|      * | ||||
|      * @param postId ID of the post being replied. | ||||
|      * @param discussionId ID of the discussion the user is replying to. | ||||
|      * @param forumId ID of the forum the user is replying to. | ||||
|      * @param name Forum name. | ||||
|      * @param courseId Course ID the forum belongs to. | ||||
|      * @param subject New post's subject. | ||||
|      * @param message New post's message. | ||||
|      * @param options Options (subscribe, attachments, ...). | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @param userId User the post belong to. If not defined, current user in site. | ||||
|      * @return Promise resolved when the post is created. | ||||
|      */ | ||||
|     async replyPost( | ||||
|         postId: number, | ||||
|         discussionId: number, | ||||
|         forumId: number, | ||||
|         name: string, | ||||
|         courseId: number, | ||||
|         subject: string, | ||||
|         message: string, | ||||
|         options?: AddonModForumReplyOptions, | ||||
|         siteId?: string, | ||||
|         userId?: number, | ||||
|     ): Promise<void> { | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
|         const data: AddonModForumOfflineReplyRecord = { | ||||
|             postid: postId, | ||||
|             discussionid: discussionId, | ||||
|             forumid: forumId, | ||||
|             name: name, | ||||
|             courseid: courseId, | ||||
|             subject: subject, | ||||
|             message: message, | ||||
|             options: JSON.stringify(options || {}), | ||||
|             userid: userId || site.getUserId(), | ||||
|             timecreated: new Date().getTime(), | ||||
|         }; | ||||
| 
 | ||||
|         await site.getDb().insertRecord(REPLIES_TABLE, data); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the path to the folder where to store files for offline attachments in a forum. | ||||
|      * | ||||
|      * @param forumId Forum ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the path. | ||||
|      */ | ||||
|     async getForumFolder(forumId: number, siteId?: string): Promise<string> { | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
|         const siteFolderPath = CoreFile.instance.getSiteFolder(site.getId()); | ||||
| 
 | ||||
|         return CoreTextUtils.instance.concatenatePaths(siteFolderPath, 'offlineforum/' + forumId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the path to the folder where to store files for a new offline discussion. | ||||
|      * | ||||
|      * @param forumId Forum ID. | ||||
|      * @param timeCreated The time the discussion was created. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the path. | ||||
|      */ | ||||
|     async getNewDiscussionFolder(forumId: number, timeCreated: number, siteId?: string): Promise<string> { | ||||
|         const folderPath = await this.getForumFolder(forumId, siteId); | ||||
| 
 | ||||
|         return CoreTextUtils.instance.concatenatePaths(folderPath, 'newdisc_' + timeCreated); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the path to the folder where to store files for a new offline reply. | ||||
|      * | ||||
|      * @param forumId Forum ID. | ||||
|      * @param postId ID of the post being replied. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @param userId User the replies belong to. If not defined, current user in site. | ||||
|      * @return Promise resolved with the path. | ||||
|      */ | ||||
|     async getReplyFolder(forumId: number, postId: number, siteId?: string, userId?: number): Promise<string> { | ||||
|         const folderPath = await this.getForumFolder(forumId, siteId); | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
|         userId = userId || site.getUserId(); | ||||
| 
 | ||||
|         return CoreTextUtils.instance.concatenatePaths(folderPath, 'reply_' + postId + '_' + userId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Parse "options" column of fetched record. | ||||
|      * | ||||
|      * @param records List of records. | ||||
|      * @return List of records with options parsed. | ||||
|      */ | ||||
|     protected parseRecordsOptions< | ||||
|         R extends { options: string }, | ||||
|         O extends Record<string, unknown> = Record<string, unknown> | ||||
|     >(records: R[]): (Omit<R, 'options'> & { options: O })[] { | ||||
|         return records.map(record => this.parseRecordOptions(record)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Parse "options" column of fetched record. | ||||
|      * | ||||
|      * @param record Record. | ||||
|      * @return Record with options parsed. | ||||
|      */ | ||||
|     protected parseRecordOptions< | ||||
|         R extends { options: string }, | ||||
|         O extends Record<string, unknown> = Record<string, unknown> | ||||
|     >(record: R): Omit<R, 'options'> & { options: O } { | ||||
|         record.options = CoreTextUtils.instance.parseJSON(record.options); | ||||
| 
 | ||||
|         return record as unknown as Omit<R, 'options'> & { options: O }; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export class AddonModForumOffline extends makeSingleton(AddonModForumOfflineProvider) {} | ||||
| 
 | ||||
| export type AddonModForumDiscussionOptions = Record<string, unknown>; | ||||
| export type AddonModForumReplyOptions = Record<string, unknown>; | ||||
| 
 | ||||
| export type AddonModForumOfflineDiscussion = { | ||||
|     forumid: number; | ||||
|     name: string; | ||||
|     courseid: number; | ||||
|     subject: string; | ||||
|     message: string; | ||||
|     options: AddonModForumDiscussionOptions; | ||||
|     groupid: number; | ||||
|     userid: number; | ||||
|     timecreated: number; | ||||
| }; | ||||
| export type AddonModForumOfflineReply = { | ||||
|     postid: number; | ||||
|     discussionid: number; | ||||
|     forumid: number; | ||||
|     name: string; | ||||
|     courseid: number; | ||||
|     subject: string; | ||||
|     message: string; | ||||
|     options: AddonModForumReplyOptions; | ||||
|     userid: number; | ||||
|     timecreated: number; | ||||
| }; | ||||
| 
 | ||||
| export type AddonModForumOfflineDiscussionRecord = Omit<AddonModForumOfflineDiscussion, 'options'> & { | ||||
|     options: string; | ||||
| }; | ||||
| export type AddonModForumOfflineReplyRecord = Omit<AddonModForumOfflineReply, 'options'> & { | ||||
|     options: string; | ||||
| }; | ||||
							
								
								
									
										600
									
								
								src/addons/mod/forum/services/sync.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										600
									
								
								src/addons/mod/forum/services/sync.service.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,600 @@ | ||||
| // (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 { 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 { CoreApp } from '@services/app'; | ||||
| import { CoreGroups } from '@services/groups'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreSync } from '@services/sync'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { Translate } from '@singletons'; | ||||
| import { CoreArray } from '@singletons/array'; | ||||
| import { CoreEvents } from '@singletons/events'; | ||||
| import { AddonModForum, AddonModForumProvider } from './forum.service'; | ||||
| import { AddonModForumHelper } from './helper.service'; | ||||
| import { AddonModForumOffline, AddonModForumOfflineDiscussion, AddonModForumOfflineReply } from './offline.service'; | ||||
| 
 | ||||
| /** | ||||
|  * Service to sync forums. | ||||
|  */ | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class AddonModForumSyncProvider extends CoreSyncBaseProvider<AddonModForumSyncResult> { | ||||
| 
 | ||||
|     static readonly AUTO_SYNCED = 'addon_mod_forum_autom_synced'; | ||||
|     static readonly MANUAL_SYNCED = 'addon_mod_forum_manual_synced'; | ||||
| 
 | ||||
|     private _componentTranslate?: string; | ||||
| 
 | ||||
|     constructor() { | ||||
|         super('AddonModForumSyncProvider'); | ||||
|     } | ||||
| 
 | ||||
|     protected get componentTranslate(): string { | ||||
|         if (!this._componentTranslate) { | ||||
|             this._componentTranslate = CoreCourse.instance.translateModuleName('forum'); | ||||
|         } | ||||
| 
 | ||||
|         return this._componentTranslate; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Try to synchronize all the forums in a certain site or in all sites. | ||||
|      * | ||||
|      * @param siteId Site ID to sync. If not defined, sync all sites. | ||||
|      * @param force Wether to force sync not depending on last execution. | ||||
|      * @return Promise resolved if sync is successful, rejected if sync fails. | ||||
|      */ | ||||
|     async syncAllForums(siteId?: string, force?: boolean): Promise<void> { | ||||
|         await this.syncOnSites('all forums', this.syncAllForumsFunc.bind(this, !!force), siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Sync all forums on a site. | ||||
|      * | ||||
|      * @param force Wether to force sync not depending on last execution. | ||||
|      * @param siteId Site ID to sync. | ||||
|      * @return Promise resolved if sync is successful, rejected if sync fails. | ||||
|      */ | ||||
|     protected async syncAllForumsFunc(force: boolean, siteId: string): Promise<void> { | ||||
|         const sitePromises: Promise<void>[] = []; | ||||
| 
 | ||||
|         // Sync all new discussions.
 | ||||
|         const syncDiscussions = async (discussions: AddonModForumOfflineDiscussion[]) => { | ||||
|             // Do not sync same forum twice.
 | ||||
|             const syncedForumIds: number[] = []; | ||||
|             const promises = discussions.map(async discussion => { | ||||
|                 if (CoreArray.contains(syncedForumIds, discussion.forumid)) { | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
|                 syncedForumIds.push(discussion.forumid); | ||||
|                 const result = force | ||||
|                     ? await this.syncForumDiscussions(discussion.forumid, discussion.userid, siteId) | ||||
|                     : await this.syncForumDiscussionsIfNeeded(discussion.forumid, discussion.userid, siteId); | ||||
| 
 | ||||
|                 if (result && result.updated) { | ||||
|                     // Sync successful, send event.
 | ||||
|                     CoreEvents.trigger(AddonModForumSyncProvider.AUTO_SYNCED, { | ||||
|                         forumId: discussion.forumid, | ||||
|                         userId: discussion.userid, | ||||
|                         warnings: result.warnings, | ||||
|                     }, siteId); | ||||
|                 } | ||||
|             }); | ||||
| 
 | ||||
|             await Promise.all(Object.values(promises)); | ||||
|         }; | ||||
| 
 | ||||
|         sitePromises.push( | ||||
|             AddonModForumOffline.instance | ||||
|                 .getAllNewDiscussions(siteId) | ||||
|                 .then(discussions => syncDiscussions(discussions)), | ||||
|         ); | ||||
| 
 | ||||
|         // Sync all discussion replies.
 | ||||
|         const syncReplies = async (replies: AddonModForumOfflineReply[]) => { | ||||
|             // Do not sync same discussion twice.
 | ||||
|             const syncedDiscussionIds: number[] = []; | ||||
|             const promises = replies.map(async reply => { | ||||
|                 if (CoreArray.contains(syncedDiscussionIds, reply.discussionid)) { | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
|                 const result = force | ||||
|                     ? await this.syncDiscussionReplies(reply.discussionid, reply.userid, siteId) | ||||
|                     : await this.syncDiscussionRepliesIfNeeded(reply.discussionid, reply.userid, siteId); | ||||
| 
 | ||||
|                 if (result && result.updated) { | ||||
|                     // Sync successful, send event.
 | ||||
|                     CoreEvents.trigger(AddonModForumSyncProvider.AUTO_SYNCED, { | ||||
|                         forumId: reply.forumid, | ||||
|                         discussionId: reply.discussionid, | ||||
|                         userId: reply.userid, | ||||
|                         warnings: result.warnings, | ||||
|                     }, siteId); | ||||
|                 } | ||||
|             }); | ||||
| 
 | ||||
|             await Promise.all(promises); | ||||
|         }; | ||||
| 
 | ||||
|         sitePromises.push( | ||||
|             AddonModForumOffline.instance | ||||
|                 .getAllReplies(siteId) | ||||
|                 .then(replies => syncReplies(replies)), | ||||
|         ); | ||||
| 
 | ||||
|         // Sync ratings.
 | ||||
|         sitePromises.push(this.syncRatings(undefined, undefined, force, siteId)); | ||||
| 
 | ||||
|         await Promise.all(sitePromises); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Sync a forum only if a certain time has passed since the last time. | ||||
|      * | ||||
|      * @param forumId Forum ID. | ||||
|      * @param userId User the discussion belong to. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when the forum is synced or if it doesn't need to be synced. | ||||
|      */ | ||||
|     async syncForumDiscussionsIfNeeded( | ||||
|         forumId: number, | ||||
|         userId: number, | ||||
|         siteId?: string, | ||||
|     ): Promise<AddonModForumSyncResult | void> { | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         const syncId = this.getForumSyncId(forumId, userId); | ||||
| 
 | ||||
|         const needed = await this.isSyncNeeded(syncId, siteId); | ||||
| 
 | ||||
|         if (needed) { | ||||
|             return this.syncForumDiscussions(forumId, userId, siteId); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Synchronize all offline discussions of a forum. | ||||
|      * | ||||
|      * @param forumId Forum ID to be synced. | ||||
|      * @param userId User the discussions belong to. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved if sync is successful, rejected otherwise. | ||||
|      */ | ||||
|     async syncForumDiscussions( | ||||
|         forumId: number, | ||||
|         userId?: number, | ||||
|         siteId?: string, | ||||
|     ): Promise<AddonModForumSyncResult> { | ||||
|         userId = userId || CoreSites.instance.getCurrentSiteUserId(); | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         const syncId = this.getForumSyncId(forumId, userId); | ||||
| 
 | ||||
|         if (this.isSyncing(syncId, siteId)) { | ||||
|             // There's already a sync ongoing for this discussion, return the promise.
 | ||||
|             return this.getOngoingSync(syncId, siteId)!; | ||||
|         } | ||||
| 
 | ||||
|         // Verify that forum isn't blocked.
 | ||||
|         if (CoreSync.instance.isBlocked(AddonModForumProvider.COMPONENT, syncId, siteId)) { | ||||
|             this.logger.debug('Cannot sync forum ' + forumId + ' because it is blocked.'); | ||||
| 
 | ||||
|             return Promise.reject(Translate.instant('core.errorsyncblocked', { $a: this.componentTranslate })); | ||||
|         } | ||||
| 
 | ||||
|         this.logger.debug('Try to sync forum ' + forumId + ' for user ' + userId); | ||||
| 
 | ||||
|         const result: AddonModForumSyncResult = { | ||||
|             warnings: [], | ||||
|             updated: false, | ||||
|         }; | ||||
| 
 | ||||
|         // Sync offline logs.
 | ||||
|         const syncDiscussions = async (): Promise<{ warnings: string[]; updated: boolean }> => { | ||||
|             await CoreUtils.instance.ignoreErrors( | ||||
|                 CoreCourseLogHelper.instance.syncActivity(AddonModForumProvider.COMPONENT, forumId, siteId), | ||||
|             ); | ||||
| 
 | ||||
|             // Get offline responses to be sent.
 | ||||
|             const discussions = await CoreUtils.instance.ignoreErrors( | ||||
|                 AddonModForumOffline.instance.getNewDiscussions(forumId, siteId, userId), | ||||
|                 [] as AddonModForumOfflineDiscussion[], | ||||
|             ); | ||||
| 
 | ||||
|             if (discussions.length !== 0 && !CoreApp.instance.isOnline()) { | ||||
|                 throw new Error('cannot sync in offline'); | ||||
|             } | ||||
| 
 | ||||
|             const promises = discussions.map(async discussion => { | ||||
|                 const errors: Error[] = []; | ||||
|                 const groupIds = discussion.groupid === AddonModForumProvider.ALL_GROUPS | ||||
|                     ? await AddonModForum.instance | ||||
|                         .getForumById(discussion.courseid, discussion.forumid, { siteId }) | ||||
|                         .then(forum => CoreGroups.instance.getActivityAllowedGroups(forum.cmid)) | ||||
|                         .then(result => result.groups.map((group) => group.id)) | ||||
|                     : [discussion.groupid]; | ||||
| 
 | ||||
|                 await Promise.all(groupIds.map(async groupId => { | ||||
|                     try { | ||||
|                         // First of all upload the attachments (if any).
 | ||||
|                         const itemId = await this.uploadAttachments(forumId, discussion, true, siteId, userId); | ||||
| 
 | ||||
|                         // Now try to add the discussion.
 | ||||
|                         const options = CoreUtils.instance.clone(discussion.options || {}); | ||||
|                         options.attachmentsid = itemId; | ||||
| 
 | ||||
|                         await AddonModForum.instance.addNewDiscussionOnline( | ||||
|                             forumId, | ||||
|                             discussion.subject, | ||||
|                             discussion.message, | ||||
|                             options, | ||||
|                             groupId, | ||||
|                             siteId, | ||||
|                         ); | ||||
|                     } catch (error) { | ||||
|                         errors.push(error); | ||||
|                     } | ||||
|                 })); | ||||
| 
 | ||||
|                 if (errors.length === groupIds.length) { | ||||
|                     // All requests have failed, reject if errors were not returned by WS.
 | ||||
|                     for (const error of errors) { | ||||
|                         if (!CoreUtils.instance.isWebServiceError(error)) { | ||||
|                             throw error; | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 // All requests succeeded, some failed or all failed with a WS error.
 | ||||
|                 result.updated = true; | ||||
| 
 | ||||
|                 await this.deleteNewDiscussion(forumId, discussion.timecreated, siteId, userId); | ||||
| 
 | ||||
|                 if (errors.length === groupIds.length) { | ||||
|                     // All requests failed with WS error.
 | ||||
|                     result.warnings.push(Translate.instant('core.warningofflinedatadeleted', { | ||||
|                         component: this.componentTranslate, | ||||
|                         name: discussion.name, | ||||
|                         error: CoreTextUtils.instance.getErrorMessageFromError(errors[0]), | ||||
|                     })); | ||||
|                 } | ||||
|             }); | ||||
| 
 | ||||
|             await Promise.all(promises); | ||||
| 
 | ||||
|             if (result.updated) { | ||||
|                 // Data has been sent to server. Now invalidate the WS calls.
 | ||||
|                 const promises = [ | ||||
|                     AddonModForum.instance.invalidateDiscussionsList(forumId, siteId), | ||||
|                     AddonModForum.instance.invalidateCanAddDiscussion(forumId, siteId), | ||||
|                 ]; | ||||
| 
 | ||||
|                 await CoreUtils.instance.ignoreErrors(Promise.all(promises)); | ||||
|             } | ||||
| 
 | ||||
|             // Sync finished, set sync time.
 | ||||
|             await CoreUtils.instance.ignoreErrors(this.setSyncTime(syncId, siteId)); | ||||
| 
 | ||||
|             return result; | ||||
|         }; | ||||
| 
 | ||||
|         return this.addOngoingSync(syncId, syncDiscussions(), siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Synchronize forum offline ratings. | ||||
|      * | ||||
|      * @param cmId Course module to be synced. If not defined, sync all forums. | ||||
|      * @param discussionId Discussion id to be synced. If not defined, sync all discussions. | ||||
|      * @param force Wether to force sync not depending on last execution. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved if sync is successful, rejected otherwise. | ||||
|      */ | ||||
|     // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||
|     async syncRatings(cmId?: number, discussionId?: number, force?: boolean, siteId?: string): Promise<void> { | ||||
|         // @todo
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Synchronize all offline discussion replies of a forum. | ||||
|      * | ||||
|      * @param forumId Forum ID to be synced. | ||||
|      * @param userId User the discussions belong to. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved if sync is successful, rejected otherwise. | ||||
|      */ | ||||
|     async syncForumReplies(forumId: number, userId?: number, siteId?: string): Promise<AddonModForumSyncResult> { | ||||
|         // Get offline forum replies to be sent.
 | ||||
|         const replies = await CoreUtils.instance.ignoreErrors( | ||||
|             AddonModForumOffline.instance.getForumReplies(forumId, siteId, userId), | ||||
|             [] as AddonModForumOfflineReply[], | ||||
|         ); | ||||
| 
 | ||||
|         if (!replies.length) { | ||||
|             // Nothing to sync.
 | ||||
|             return { warnings: [], updated: false }; | ||||
|         } else if (!CoreApp.instance.isOnline()) { | ||||
|             // Cannot sync in offline.
 | ||||
|             return Promise.reject(null); | ||||
|         } | ||||
| 
 | ||||
|         const promises: Record<string, Promise<AddonModForumSyncResult>> = {}; | ||||
| 
 | ||||
|         // Do not sync same discussion twice.
 | ||||
|         replies.forEach((reply) => { | ||||
|             if (typeof promises[reply.discussionid] != 'undefined') { | ||||
|                 return; | ||||
|             } | ||||
|             promises[reply.discussionid] = this.syncDiscussionReplies(reply.discussionid, userId, siteId); | ||||
|         }); | ||||
| 
 | ||||
|         const results = await Promise.all(Object.values(promises)); | ||||
| 
 | ||||
|         return results.reduce((a, b) => ({ | ||||
|             warnings: a.warnings.concat(b.warnings), | ||||
|             updated: a.updated || b.updated, | ||||
|         }), { warnings: [], updated: false } as AddonModForumSyncResult); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Sync a forum discussion replies only if a certain time has passed since the last time. | ||||
|      * | ||||
|      * @param discussionId Discussion ID to be synced. | ||||
|      * @param userId User the posts belong to. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when the forum discussion is synced or if it doesn't need to be synced. | ||||
|      */ | ||||
|     async syncDiscussionRepliesIfNeeded( | ||||
|         discussionId: number, | ||||
|         userId?: number, | ||||
|         siteId?: string, | ||||
|     ): Promise<AddonModForumSyncResult | void> { | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         const syncId = this.getDiscussionSyncId(discussionId, userId); | ||||
| 
 | ||||
|         const needed = await this.isSyncNeeded(syncId, siteId); | ||||
| 
 | ||||
|         if (needed) { | ||||
|             return this.syncDiscussionReplies(discussionId, userId, siteId); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Synchronize all offline replies from a discussion. | ||||
|      * | ||||
|      * @param discussionId Discussion ID to be synced. | ||||
|      * @param userId User the posts belong to. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved if sync is successful, rejected otherwise. | ||||
|      */ | ||||
|     async syncDiscussionReplies(discussionId: number, userId?: number, siteId?: string): Promise<AddonModForumSyncResult> { | ||||
|         userId = userId || CoreSites.instance.getCurrentSiteUserId(); | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         const syncId = this.getDiscussionSyncId(discussionId, userId); | ||||
| 
 | ||||
|         if (this.isSyncing(syncId, siteId)) { | ||||
|             // There's already a sync ongoing for this discussion, return the promise.
 | ||||
|             return this.getOngoingSync(syncId, siteId)!; | ||||
|         } | ||||
| 
 | ||||
|         // Verify that forum isn't blocked.
 | ||||
|         if (CoreSync.instance.isBlocked(AddonModForumProvider.COMPONENT, syncId, siteId)) { | ||||
|             this.logger.debug('Cannot sync forum discussion ' + discussionId + ' because it is blocked.'); | ||||
| 
 | ||||
|             throw new Error(Translate.instant('core.errorsyncblocked', { $a: this.componentTranslate })); | ||||
|         } | ||||
| 
 | ||||
|         this.logger.debug('Try to sync forum discussion ' + discussionId + ' for user ' + userId); | ||||
| 
 | ||||
|         let forumId; | ||||
|         const result: AddonModForumSyncResult = { | ||||
|             warnings: [], | ||||
|             updated: false, | ||||
|         }; | ||||
| 
 | ||||
|         // Get offline responses to be sent.
 | ||||
|         const syncReplies = async () => { | ||||
|             const replies = await CoreUtils.instance.ignoreErrors( | ||||
|                 AddonModForumOffline.instance.getDiscussionReplies(discussionId, siteId, userId), | ||||
|                 [] as AddonModForumOfflineReply[], | ||||
|             ); | ||||
| 
 | ||||
|             if (replies.length !== 0 && !CoreApp.instance.isOnline()) { | ||||
|                 throw new Error('Cannot sync in offline'); | ||||
|             } | ||||
| 
 | ||||
|             const promises = replies.map(async reply => { | ||||
|                 forumId = reply.forumid; | ||||
|                 reply.options = reply.options || {}; | ||||
| 
 | ||||
|                 try { | ||||
|                     // First of all upload the attachments (if any).
 | ||||
|                     await this.uploadAttachments(forumId, reply, false, siteId, userId).then((itemId) => { | ||||
|                         // Now try to send the reply.
 | ||||
|                         reply.options.attachmentsid = itemId; | ||||
| 
 | ||||
|                         return AddonModForum.instance.replyPostOnline( | ||||
|                             reply.postid, | ||||
|                             reply.subject, | ||||
|                             reply.message, | ||||
|                             reply.options, | ||||
|                             siteId, | ||||
|                         ); | ||||
|                     }); | ||||
| 
 | ||||
|                     result.updated = true; | ||||
| 
 | ||||
|                     await this.deleteReply(forumId, reply.postid, siteId, userId); | ||||
|                 } catch (error) { | ||||
|                     if (!CoreUtils.instance.isWebServiceError(error)) { | ||||
|                         throw error; | ||||
|                     } | ||||
| 
 | ||||
|                     // The WebService has thrown an error, this means that responses cannot be submitted. Delete them.
 | ||||
|                     result.updated = true; | ||||
| 
 | ||||
|                     await this.deleteReply(forumId, reply.postid, siteId, userId); | ||||
| 
 | ||||
|                     // Responses deleted, add a warning.
 | ||||
|                     result.warnings.push(Translate.instant('core.warningofflinedatadeleted', { | ||||
|                         component: this.componentTranslate, | ||||
|                         name: reply.name, | ||||
|                         error: CoreTextUtils.instance.getErrorMessageFromError(error), | ||||
|                     })); | ||||
|                 } | ||||
|             }); | ||||
| 
 | ||||
|             await Promise.all(promises); | ||||
| 
 | ||||
|             // Data has been sent to server. Now invalidate the WS calls.
 | ||||
|             const invalidationPromises: Promise<void>[] = []; | ||||
| 
 | ||||
|             if (forumId) { | ||||
|                 invalidationPromises.push(AddonModForum.instance.invalidateDiscussionsList(forumId, siteId)); | ||||
|             } | ||||
| 
 | ||||
|             invalidationPromises.push(AddonModForum.instance.invalidateDiscussionPosts(discussionId, forumId, siteId)); | ||||
| 
 | ||||
|             await CoreUtils.instance.ignoreErrors(CoreUtils.instance.allPromises(invalidationPromises)); | ||||
| 
 | ||||
|             // Sync finished, set sync time.
 | ||||
|             await CoreUtils.instance.ignoreErrors(this.setSyncTime(syncId, siteId)); | ||||
| 
 | ||||
|             // All done, return the warnings.
 | ||||
|             return result; | ||||
|         }; | ||||
| 
 | ||||
|         return this.addOngoingSync(syncId, syncReplies(), siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete a new discussion. | ||||
|      * | ||||
|      * @param forumId Forum ID the discussion belongs to. | ||||
|      * @param timecreated The timecreated of the discussion. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @param userId User the discussion belongs to. If not defined, current user in site. | ||||
|      * @return Promise resolved when deleted. | ||||
|      */ | ||||
|     protected async deleteNewDiscussion(forumId: number, timecreated: number, siteId?: string, userId?: number): Promise<void> { | ||||
|         await Promise.all([ | ||||
|             AddonModForumOffline.instance.deleteNewDiscussion(forumId, timecreated, siteId, userId), | ||||
|             CoreUtils.instance.ignoreErrors( | ||||
|                 AddonModForumHelper.instance.deleteNewDiscussionStoredFiles(forumId, timecreated, siteId), | ||||
|             ), | ||||
|         ]); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete a new discussion. | ||||
|      * | ||||
|      * @param forumId Forum ID the discussion belongs to. | ||||
|      * @param postId ID of the post being replied. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @param userId User the discussion belongs to. If not defined, current user in site. | ||||
|      * @return Promise resolved when deleted. | ||||
|      */ | ||||
|     protected async deleteReply(forumId: number, postId: number, siteId?: string, userId?: number): Promise<void> { | ||||
|         await Promise.all([ | ||||
|             AddonModForumOffline.instance.deleteReply(postId, siteId, userId), | ||||
|             CoreUtils.instance.ignoreErrors(AddonModForumHelper.instance.deleteReplyStoredFiles(forumId, postId, siteId, userId)), | ||||
|         ]); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Upload attachments of an offline post/discussion. | ||||
|      * | ||||
|      * @param forumId Forum ID the post belongs to. | ||||
|      * @param post Offline post or discussion. | ||||
|      * @param isDisc True if it's a new discussion, false if it's a reply. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @param userId User the reply belongs to. If not defined, current user in site. | ||||
|      * @return Promise resolved with draftid if uploaded, resolved with undefined if nothing to upload. | ||||
|      */ | ||||
|     protected async uploadAttachments( | ||||
|         forumId: number, | ||||
|         post: any, | ||||
|         isDisc: boolean, | ||||
|         siteId?: string, | ||||
|         userId?: number, | ||||
|     ): Promise<void> { | ||||
|         const attachments = post && post.options && post.options.attachmentsid; | ||||
| 
 | ||||
|         if (!attachments) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // Has some attachments to sync.
 | ||||
|         let files = attachments.online || []; | ||||
| 
 | ||||
|         if (attachments.offline) { | ||||
|             // Has offline files.
 | ||||
|             try { | ||||
|                 const atts = isDisc | ||||
|                     ? await AddonModForumHelper.instance.getNewDiscussionStoredFiles(forumId, post.timecreated, siteId) | ||||
|                     : await AddonModForumHelper.instance.getReplyStoredFiles(forumId, post.postid, siteId, userId); | ||||
| 
 | ||||
|                 files = files.concat(atts); | ||||
|             } catch (error) { | ||||
|                 // Folder not found, no files to add.
 | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         await CoreFileUploader.instance.uploadOrReuploadFiles(files, AddonModForumProvider.COMPONENT, forumId, siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the ID of a forum sync. | ||||
|      * | ||||
|      * @param forumId Forum ID. | ||||
|      * @param userId User the responses belong to.. If not defined, current user. | ||||
|      * @return Sync ID. | ||||
|      */ | ||||
|     getForumSyncId(forumId: number, userId?: number): string { | ||||
|         userId = userId || CoreSites.instance.getCurrentSiteUserId(); | ||||
| 
 | ||||
|         return 'forum#' + forumId + '#' + userId; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the ID of a discussion sync. | ||||
|      * | ||||
|      * @param discussionId Discussion ID. | ||||
|      * @param userId User the responses belong to.. If not defined, current user. | ||||
|      * @return Sync ID. | ||||
|      */ | ||||
|     getDiscussionSyncId(discussionId: number, userId?: number): string { | ||||
|         userId = userId || CoreSites.instance.getCurrentSiteUserId(); | ||||
| 
 | ||||
|         return 'discussion#' + discussionId + '#' + userId; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Result of forum sync. | ||||
|  */ | ||||
| export type AddonModForumSyncResult = { | ||||
|     updated: boolean; | ||||
|     warnings: string[]; | ||||
| }; | ||||
| @ -3110,7 +3110,7 @@ export class AddonModLessonProvider { | ||||
|         const params: AddonModLessonProcessPageWSParams = { | ||||
|             lessonid: lessonId, | ||||
|             pageid: pageId, | ||||
|             data: <ProcessPageData[]> CoreUtils.instance.objectToArrayOfObjects(data, 'name', 'value', true), | ||||
|             data: CoreUtils.instance.objectToArrayOfObjects<ProcessPageData>(data, 'name', 'value', true), | ||||
|             review: !!options.review, | ||||
|         }; | ||||
| 
 | ||||
|  | ||||
| @ -16,6 +16,7 @@ import { NgModule } from '@angular/core'; | ||||
| 
 | ||||
| import { AddonModAssignModule } from './assign/assign.module'; | ||||
| import { AddonModBookModule } from './book/book.module'; | ||||
| import { AddonModForumModule } from './forum/forum.module'; | ||||
| import { AddonModLessonModule } from './lesson/lesson.module'; | ||||
| import { AddonModPageModule } from './page/page.module'; | ||||
| import { AddonModQuizModule } from './quiz/quiz.module'; | ||||
| @ -25,6 +26,7 @@ import { AddonModQuizModule } from './quiz/quiz.module'; | ||||
|     imports: [ | ||||
|         AddonModAssignModule, | ||||
|         AddonModBookModule, | ||||
|         AddonModForumModule, | ||||
|         AddonModLessonModule, | ||||
|         AddonModPageModule, | ||||
|         AddonModQuizModule, | ||||
|  | ||||
| @ -68,7 +68,7 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR | ||||
|      * Component being initialized. | ||||
|      */ | ||||
|     async ngOnInit(): Promise<void> { | ||||
|         super.ngOnInit(); | ||||
|         await super.ngOnInit(); | ||||
| 
 | ||||
|         this.hasOffline = false; | ||||
|         this.syncIcon = CoreConstants.ICON_LOADING; | ||||
|  | ||||
| @ -779,3 +779,5 @@ export type CoreFileUploaderTypeListInfoEntry = { | ||||
|     name?: string; | ||||
|     extlist: string; | ||||
| }; | ||||
| 
 | ||||
| export type CoreFileEntry = CoreWSExternalFile | FileEntry; | ||||
|  | ||||
| @ -19,7 +19,7 @@ import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| import { CoreCourse } from '../../course/services/course'; | ||||
| import { CoreCourses } from '../../courses/services/courses'; | ||||
| import { AddonModForum, AddonModForumData } from '@/addons/mod/forum/services/forum'; | ||||
| import { AddonModForum, AddonModForumData } from '@addons/mod/forum/services/forum.service'; | ||||
| 
 | ||||
| /** | ||||
|  * Items with index 1 and 3 were removed on 2.5 and not being supported in the app. | ||||
|  | ||||
| @ -219,7 +219,7 @@ export class CoreUtilsProvider { | ||||
|         try { | ||||
|             const response = await this.timeoutPromise(window.fetch(url, initOptions), CoreWS.instance.getRequestTimeout()); | ||||
| 
 | ||||
|             return response.redirected; | ||||
|             return !!response && response.redirected; | ||||
|         } catch (error) { | ||||
|             if (error.timeout && controller) { | ||||
|                 // Timeout, abort the request.
 | ||||
| @ -1072,13 +1072,16 @@ export class CoreUtilsProvider { | ||||
|      * @param sortByValue True to sort values alphabetically, false otherwise. | ||||
|      * @return Array of objects with the name & value of each property. | ||||
|      */ | ||||
|     objectToArrayOfObjects<T = Record<string, unknown>>( | ||||
|         obj: Record<string, unknown>, | ||||
|     objectToArrayOfObjects< | ||||
|         A extends Record<string,unknown> = Record<string, unknown>, | ||||
|         O extends Record<string, unknown> = Record<string, unknown> | ||||
|     >( | ||||
|         obj: O, | ||||
|         keyName: string, | ||||
|         valueName: string, | ||||
|         sortByKey?: boolean, | ||||
|         sortByValue?: boolean, | ||||
|     ): T[] { | ||||
|     ): A[] { | ||||
|         // Get the entries from an object or primitive value.
 | ||||
|         const getEntries = (elKey: string, value: unknown): Record<string, unknown>[] | unknown => { | ||||
|             if (typeof value == 'undefined' || value == null) { | ||||
| @ -1114,7 +1117,7 @@ export class CoreUtilsProvider { | ||||
|         } | ||||
| 
 | ||||
|         // "obj" will always be an object, so "entries" will always be an array.
 | ||||
|         const entries = getEntries('', obj) as T[]; | ||||
|         const entries = getEntries('', obj) as A[]; | ||||
|         if (sortByKey || sortByValue) { | ||||
|             return entries.sort((a, b) => { | ||||
|                 if (sortByKey) { | ||||
| @ -1223,7 +1226,7 @@ export class CoreUtilsProvider { | ||||
|     promiseDefer<T>(): PromiseDefer<T> { | ||||
|         const deferred: Partial<PromiseDefer<T>> = {}; | ||||
|         deferred.promise = new Promise((resolve, reject): void => { | ||||
|             deferred.resolve = resolve; | ||||
|             deferred.resolve = resolve as (value?: T | undefined) => void; | ||||
|             deferred.reject = reject; | ||||
|         }); | ||||
| 
 | ||||
| @ -1371,7 +1374,7 @@ export class CoreUtilsProvider { | ||||
|      * @param time Number of milliseconds of the timeout. | ||||
|      * @return Promise with the timeout. | ||||
|      */ | ||||
|     timeoutPromise<T>(promise: Promise<T>, time: number): Promise<T> { | ||||
|     timeoutPromise<T>(promise: Promise<T>, time: number): Promise<T | void> { | ||||
|         return new Promise((resolve, reject): void => { | ||||
|             let timedOut = false; | ||||
|             const resolveBeforeTimeout = () => { | ||||
|  | ||||
| @ -158,4 +158,10 @@ export class NavController extends makeSingleton(NavControllerService) {} | ||||
| export class Router extends makeSingleton(RouterService) {} | ||||
| 
 | ||||
| // Convert external libraries injectables.
 | ||||
| export class Translate extends makeSingleton(TranslateService) {} | ||||
| export class Translate extends makeSingleton(TranslateService) { | ||||
| 
 | ||||
|     static instant(key: string | Array<string>, interpolateParams?: Record<string, unknown>): string | any { | ||||
|         return this.instance.instant(key, interpolateParams); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -183,6 +183,8 @@ | ||||
|     --addon-messages-discussion-badge: var(--custom-messages-discussion-badge, var(--core-color)); | ||||
|     --addon-messages-discussion-badge-text: var(--custom-messages-discussion-badge-text, var(--white)); | ||||
| 
 | ||||
|     --addon-forum-avatar-size: var(--custom-forum-avatar-size, 28px); | ||||
| 
 | ||||
|     --drop-shadow: var(--custom-drop-shadow, 0, 0, 0, 0.2); | ||||
| 
 | ||||
|     --core-menu-box-shadow-end: var(--custom-menu-box-shadow-end, -4px 0px 16px rgba(0, 0, 0, 0.18)); | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user