commit
						3d5e3267c0
					
				| @ -1266,6 +1266,11 @@ | ||||
|   "core.clicktoseefull": "local_moodlemobileapp", | ||||
|   "core.close": "repository", | ||||
|   "core.comments": "moodle", | ||||
|   "core.comments.addcomment": "moodle", | ||||
|   "core.comments.comments": "moodle", | ||||
|   "core.comments.commentscount": "moodle", | ||||
|   "core.comments.commentsnotworking": "local_moodlemobileapp", | ||||
|   "core.comments.nocomments": "moodle", | ||||
|   "core.commentscount": "moodle", | ||||
|   "core.commentsnotworking": "local_moodlemobileapp", | ||||
|   "core.completion-alt-auto-fail": "completion", | ||||
|  | ||||
| @ -169,7 +169,13 @@ export class AddonBlogEntriesComponent implements OnInit { | ||||
|      * @param {any}     refresher  Refresher instance. | ||||
|      */ | ||||
|     refresh(refresher?: any): void { | ||||
|         this.blogProvider.invalidateEntries(this.filter).finally(() => { | ||||
|         const promises = this.entries.map((entry) => { | ||||
|             return this.commentsProvider.invalidateCommentsData('user', entry.userid, this.component, entry.id, 'format_blog'); | ||||
|         }); | ||||
| 
 | ||||
|         promises.push(this.blogProvider.invalidateEntries(this.filter)); | ||||
| 
 | ||||
|         Promise.all(promises).finally(() => { | ||||
|             this.fetchEntries(true).finally(() => { | ||||
|                 if (refresher) { | ||||
|                     refresher.complete(); | ||||
|  | ||||
| @ -218,6 +218,10 @@ export class AddonModDataEntryPage implements OnDestroy { | ||||
| 
 | ||||
|         promises.push(this.dataProvider.invalidateDatabaseData(this.courseId)); | ||||
|         if (this.data) { | ||||
|             if (this.data.comments && this.entry && this.entry.id > 0 && this.commentsEnabled) { | ||||
|                 promises.push(this.commentsProvider.invalidateCommentsData('module', this.data.coursemodule, 'mod_data', | ||||
|                     this.entry.id, 'database_entry')); | ||||
|             } | ||||
|             promises.push(this.dataProvider.invalidateEntryData(this.data.id, this.entryId)); | ||||
|             promises.push(this.groupsProvider.invalidateActivityGroupInfo(this.data.coursemodule)); | ||||
|             promises.push(this.dataProvider.invalidateEntriesData(this.data.id)); | ||||
|  | ||||
| @ -1265,9 +1265,16 @@ | ||||
|     "core.clicktohideshow": "Click to expand or collapse", | ||||
|     "core.clicktoseefull": "Click to see full contents.", | ||||
|     "core.close": "Close", | ||||
|     "core.comments": "Comments", | ||||
|     "core.commentscount": "Comments ({{$a}})", | ||||
|     "core.commentsnotworking": "Comments cannot be retrieved", | ||||
|     "core.comments.addcomment": "Add a comment...", | ||||
|     "core.comments.comments": "Comments", | ||||
|     "core.comments.commentscount": "Comments ({{$a}})", | ||||
|     "core.comments.commentsnotworking": "Comments cannot be retrieved", | ||||
|     "core.comments.deletecommentbyon": "Delete comment posted by {{$a.user}} on {{$a.time}}", | ||||
|     "core.comments.eventcommentcreated": "Comment created", | ||||
|     "core.comments.eventcommentdeleted": "Comment deleted", | ||||
|     "core.comments.nocomments": "No comments", | ||||
|     "core.comments.savecomment": "Save comment", | ||||
|     "core.comments.warningcommentsnotsent": "Couldn't sync comments. {{error}}", | ||||
|     "core.completion-alt-auto-fail": "Completed: {{$a}} (did not achieve pass grade)", | ||||
|     "core.completion-alt-auto-n": "Not completed: {{$a}}", | ||||
|     "core.completion-alt-auto-n-override": "Not completed: {{$a.modname}} (set by {{$a.overrideuser}})", | ||||
| @ -1618,7 +1625,6 @@ | ||||
|     "core.never": "Never", | ||||
|     "core.next": "Next", | ||||
|     "core.no": "No", | ||||
|     "core.nocomments": "No comments", | ||||
|     "core.nograde": "No grade", | ||||
|     "core.none": "None", | ||||
|     "core.nopasswordchangeforced": "You cannot proceed without changing your password.", | ||||
|  | ||||
| @ -13,7 +13,12 @@ | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { NgModule } from '@angular/core'; | ||||
| import { CoreEventsProvider } from '@providers/events'; | ||||
| import { CoreCronDelegate } from '@providers/cron'; | ||||
| import { CoreCommentsProvider } from './providers/comments'; | ||||
| import { CoreCommentsOfflineProvider } from './providers/offline'; | ||||
| import { CoreCommentsSyncCronHandler } from './providers/sync-cron-handler'; | ||||
| import { CoreCommentsSyncProvider } from './providers/sync'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
| @ -21,7 +26,20 @@ import { CoreCommentsProvider } from './providers/comments'; | ||||
|     imports: [ | ||||
|     ], | ||||
|     providers: [ | ||||
|         CoreCommentsProvider | ||||
|         CoreCommentsProvider, | ||||
|         CoreCommentsOfflineProvider, | ||||
|         CoreCommentsSyncProvider, | ||||
|         CoreCommentsSyncCronHandler | ||||
|     ] | ||||
| }) | ||||
| export class CoreCommentsModule {} | ||||
| export class CoreCommentsModule { | ||||
|     constructor(eventsProvider: CoreEventsProvider, cronDelegate: CoreCronDelegate, syncHandler: CoreCommentsSyncCronHandler) { | ||||
|         // Reset comments page size.
 | ||||
|         eventsProvider.on(CoreEventsProvider.LOGIN, () => { | ||||
|             CoreCommentsProvider.pageSize = null; | ||||
|             CoreCommentsProvider.pageSizeOK = false; | ||||
|         }); | ||||
| 
 | ||||
|         cronDelegate.register(syncHandler); | ||||
|     } | ||||
| } | ||||
|  | ||||
							
								
								
									
										4
									
								
								src/core/comments/components/comments/comments.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/core/comments/components/comments/comments.scss
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | ||||
| core-comments .core-comments-clickable { | ||||
|     pointer-events: auto; | ||||
|     cursor: pointer; | ||||
| } | ||||
| @ -31,13 +31,13 @@ export class CoreCommentsCommentsComponent implements OnChanges, OnDestroy { | ||||
|     @Input() component: string; | ||||
|     @Input() itemId: number; | ||||
|     @Input() area = ''; | ||||
|     @Input() page = 0; | ||||
|     @Input() title?: string; | ||||
|     @Input() displaySpinner = true; // Whether to display the loading spinner.
 | ||||
|     @Output() onLoading: EventEmitter<boolean>; // Eevent that indicates whether the component is loading data.
 | ||||
| 
 | ||||
|     commentsLoaded = false; | ||||
|     commentsCount: number; | ||||
|     commentsCount: string; | ||||
|     countError = false; | ||||
|     disabled = false; | ||||
| 
 | ||||
|     protected updateSiteObserver; | ||||
| @ -72,7 +72,7 @@ export class CoreCommentsCommentsComponent implements OnChanges, OnDestroy { | ||||
|      */ | ||||
|     ngOnChanges(changes: { [name: string]: SimpleChange }): void { | ||||
|         // If something change, update the fields.
 | ||||
|         if (changes) { | ||||
|         if (changes && this.commentsLoaded) { | ||||
|             this.fetchData(); | ||||
|         } | ||||
|     } | ||||
| @ -85,30 +85,27 @@ export class CoreCommentsCommentsComponent implements OnChanges, OnDestroy { | ||||
|         this.commentsLoaded = false; | ||||
|         this.onLoading.emit(true); | ||||
| 
 | ||||
|         this.commentsProvider.getComments(this.contextLevel, this.instanceId, this.component, this.itemId, this.area, this.page) | ||||
|             .then((comments) => { | ||||
|                 this.commentsCount = comments && comments.length ? comments.length : 0; | ||||
|             }).catch(() => { | ||||
|                 this.commentsCount = -1; | ||||
|             }).finally(() => { | ||||
|                 this.commentsLoaded = true; | ||||
|                 this.onLoading.emit(false); | ||||
|             }); | ||||
|         this.commentsProvider.getCommentsCount(this.contextLevel, this.instanceId, this.component, this.itemId, this.area) | ||||
|                 .then((commentsCount) => { | ||||
|             this.commentsCount = commentsCount; | ||||
|             this.countError = parseInt(this.commentsCount, 10) < 0; | ||||
|             this.commentsLoaded = true; | ||||
|             this.onLoading.emit(false); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Opens the comments page. | ||||
|      */ | ||||
|     openComments(): void { | ||||
|         if (!this.disabled && this.commentsCount > 0) { | ||||
|         if (!this.disabled && !this.countError) { | ||||
|             // Open a new state with the interpolated contents.
 | ||||
|             this.navCtrl.push('CoreCommentsViewerPage', { | ||||
|                 contextLevel: this.contextLevel, | ||||
|                 instanceId: this.instanceId, | ||||
|                 component: this.component, | ||||
|                 componentName: this.component, | ||||
|                 itemId: this.itemId, | ||||
|                 area: this.area, | ||||
|                 page: this.page, | ||||
|                 title: this.title, | ||||
|             }); | ||||
|         } | ||||
|  | ||||
| @ -1,8 +1,8 @@ | ||||
| <core-loading *ngIf="!disabled" [hideUntil]="commentsLoaded || !displaySpinner"> | ||||
|     <div (click)="openComments()" *ngIf="commentsCount >= 0"> | ||||
|         {{ 'core.commentscount' | translate : {'$a': commentsCount} }} | ||||
|     <div (click)="openComments()" *ngIf="!countError" [class.core-comments-clickable]="!disabled"> | ||||
|         {{ 'core.comments.commentscount' | translate : {'$a': commentsCount} }} | ||||
|     </div> | ||||
|     <div *ngIf="commentsCount < 0"> | ||||
|         {{ 'core.commentsnotworking' | translate }} | ||||
|     <div *ngIf="countError"> | ||||
|         {{ 'core.comments.commentsnotworking' | translate }} | ||||
|     </div> | ||||
| </core-loading> | ||||
|  | ||||
							
								
								
									
										12
									
								
								src/core/comments/lang/en.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/core/comments/lang/en.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | ||||
| { | ||||
|     "addcomment": "Add a comment...", | ||||
|     "comments": "Comments", | ||||
|     "commentscount": "Comments ({{$a}})", | ||||
|     "commentsnotworking": "Comments cannot be retrieved", | ||||
|     "deletecommentbyon": "Delete comment posted by {{$a.user}} on {{$a.time}}", | ||||
|     "eventcommentcreated": "Comment created", | ||||
|     "eventcommentdeleted": "Comment deleted", | ||||
|     "nocomments": "No comments", | ||||
|     "savecomment": "Save comment", | ||||
|     "warningcommentsnotsent": "Couldn't sync comments. {{error}}" | ||||
| } | ||||
							
								
								
									
										22
									
								
								src/core/comments/pages/add/add.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/core/comments/pages/add/add.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | ||||
| <ion-header> | ||||
|     <ion-navbar core-back-button> | ||||
|         <ion-title>{{ 'core.comments.addcomment' | translate }}</ion-title> | ||||
|         <ion-buttons end> | ||||
|             <button ion-button icon-only (click)="closeModal()" [attr.aria-label]="'core.close' | translate"> | ||||
|                 <ion-icon name="close"></ion-icon> | ||||
|             </button> | ||||
|         </ion-buttons> | ||||
|     </ion-navbar> | ||||
| </ion-header> | ||||
| <ion-content> | ||||
|     <form name="itemEdit" (ngSubmit)="addComment($event)"> | ||||
|         <ion-item> | ||||
|             <ion-textarea placeholder="{{ 'core.comments.addcomment' | translate }}" rows="5" [(ngModel)]="content" name="content" required="required"></ion-textarea> | ||||
|         </ion-item> | ||||
|         <div padding> | ||||
|             <button ion-button block type="submit" [disabled]="processing || content.length < 1"> | ||||
|                 {{ 'core.comments.savecomment' | translate }} | ||||
|             </button> | ||||
|         </div> | ||||
|     </form> | ||||
| </ion-content> | ||||
							
								
								
									
										31
									
								
								src/core/comments/pages/add/add.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/core/comments/pages/add/add.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,31 @@ | ||||
| // (C) Copyright 2015 Martin Dougiamas
 | ||||
| //
 | ||||
| // 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 { IonicPageModule } from 'ionic-angular'; | ||||
| import { CoreCommentsAddPage } from './add'; | ||||
| import { TranslateModule } from '@ngx-translate/core'; | ||||
| import { CoreDirectivesModule } from '@directives/directives.module'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
|         CoreCommentsAddPage | ||||
|     ], | ||||
|     imports: [ | ||||
|         CoreDirectivesModule, | ||||
|         IonicPageModule.forChild(CoreCommentsAddPage), | ||||
|         TranslateModule.forChild() | ||||
|     ] | ||||
| }) | ||||
| export class CoreCommentsAddPageModule {} | ||||
							
								
								
									
										82
									
								
								src/core/comments/pages/add/add.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								src/core/comments/pages/add/add.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,82 @@ | ||||
| // (C) Copyright 2015 Martin Dougiamas
 | ||||
| //
 | ||||
| // 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 } from '@angular/core'; | ||||
| import { IonicPage, ViewController, NavParams } from 'ionic-angular'; | ||||
| import { CoreAppProvider } from '@providers/app'; | ||||
| import { CoreDomUtilsProvider } from '@providers/utils/dom'; | ||||
| import { CoreCommentsProvider } from '../../providers/comments'; | ||||
| 
 | ||||
| /** | ||||
|  * Component that displays a text area for composing a comment. | ||||
|  */ | ||||
| @IonicPage({ segment: 'core-comments-add' }) | ||||
| @Component({ | ||||
|     selector: 'page-core-comments-add', | ||||
|     templateUrl: 'add.html', | ||||
| }) | ||||
| export class CoreCommentsAddPage { | ||||
|     protected contextLevel: string; | ||||
|     protected instanceId: number; | ||||
|     protected componentName: string; | ||||
|     protected itemId: number; | ||||
|     protected area = ''; | ||||
| 
 | ||||
|     content = ''; | ||||
|     processing = false; | ||||
| 
 | ||||
|     constructor(params: NavParams, private viewCtrl: ViewController, private appProvider: CoreAppProvider, | ||||
|             private domUtils: CoreDomUtilsProvider, private commentsProvider: CoreCommentsProvider) { | ||||
|         this.contextLevel = params.get('contextLevel'); | ||||
|         this.instanceId = params.get('instanceId'); | ||||
|         this.componentName = params.get('componentName'); | ||||
|         this.itemId = params.get('itemId'); | ||||
|         this.area = params.get('area') || ''; | ||||
|         this.content = params.get('content') || ''; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Send the comment or store it offline. | ||||
|      * | ||||
|      * @param {Event} e Event. | ||||
|      */ | ||||
|     addComment(e: Event): void { | ||||
|         e.preventDefault(); | ||||
|         e.stopPropagation(); | ||||
| 
 | ||||
|         this.appProvider.closeKeyboard(); | ||||
|         const loadingModal = this.domUtils.showModalLoading('core.sending', true); | ||||
|         // Freeze the add comment button.
 | ||||
|         this.processing = true; | ||||
|         this.commentsProvider.addComment(this.content, this.contextLevel, this.instanceId, this.componentName, this.itemId, | ||||
|                 this.area).then((commentsResponse) => { | ||||
|             this.viewCtrl.dismiss({comments: commentsResponse}).finally(() => { | ||||
|                 this.domUtils.showToast(commentsResponse ? 'core.comments.eventcommentcreated' : 'core.datastoredoffline', true, | ||||
|                     3000); | ||||
|             }); | ||||
|         }).catch((error) => { | ||||
|             this.domUtils.showErrorModal(error); | ||||
|             this.processing = false; | ||||
|         }).finally(() => { | ||||
|             loadingModal.dismiss(); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Close modal. | ||||
|      */ | ||||
|     closeModal(): void { | ||||
|         this.viewCtrl.dismiss(); | ||||
|     } | ||||
| } | ||||
| @ -1,24 +1,71 @@ | ||||
| <ion-header> | ||||
|     <ion-navbar core-back-button> | ||||
|         <ion-title><core-format-text [text]="title"></core-format-text></ion-title> | ||||
|         <ion-buttons end> | ||||
|             <button *ngIf="canDeleteComments" item-end ion-button icon-only clear (click)="toggleDelete($event)" [attr.aria-label]="'core.delete' | translate"> | ||||
|                 <ion-icon name="create" ios="md-create"></ion-icon> | ||||
|             </button> | ||||
|             <core-context-menu> | ||||
|                 <core-context-menu-item [hidden]="!(commentsLoaded && !hasOffline)" [priority]="100" [content]="'core.refresh' | translate" (action)="refreshComments(false)" [iconAction]="refreshIcon" [closeOnClick]="true"></core-context-menu-item> | ||||
|                 <core-context-menu-item [hidden]="!(commentsLoaded && hasOffline)" [priority]="100" [content]="'core.settings.synchronizenow' | translate" (action)="refreshComments(true)" [iconAction]="syncIcon" [closeOnClick]="false"></core-context-menu-item> | ||||
|             </core-context-menu> | ||||
|         </ion-buttons> | ||||
|     </ion-navbar> | ||||
| </ion-header> | ||||
| <ion-content> | ||||
|     <ion-refresher [enabled]="commentsLoaded" (ionRefresh)="refreshComments($event)"> | ||||
|     <ion-refresher [enabled]="commentsLoaded" (ionRefresh)="refreshComments(false, $event)"> | ||||
|         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> | ||||
|     </ion-refresher> | ||||
|     <core-loading [hideUntil]="commentsLoaded"> | ||||
|         <core-empty-box *ngIf="!comments || !comments.length" icon="chatbubbles" [message]="'core.nocomments' | translate"></core-empty-box> | ||||
|         <core-empty-box *ngIf="!comments || !comments.length" icon="chatbubbles" [message]="'core.comments.nocomments' | translate"></core-empty-box> | ||||
| 
 | ||||
|         <div class="core-warning-card" icon-start *ngIf="hasOffline"> | ||||
|             <ion-icon name="warning"></ion-icon> | ||||
|             {{ 'core.thereisdatatosync' | translate:{$a: 'core.comments.comments' | translate | lowercase } }} | ||||
|         </div> | ||||
| 
 | ||||
|         <ion-card *ngIf="offlineComment" (click)="addComment($event)"> | ||||
|             <ion-item text-wrap> | ||||
|                 <ion-avatar core-user-avatar [user]="offlineComment" item-start></ion-avatar> | ||||
|                 <h2>{{ offlineComment.fullname }}</h2> | ||||
|                 <p> | ||||
|                     <ion-icon name="time"></ion-icon> {{ 'core.notsent' | translate }} | ||||
|                 </p> | ||||
|                 <button *ngIf="showDelete" item-end ion-button icon-only clear [@coreSlideInOut]="'fromRight'" color="danger" (click)="deleteComment($event, offlineComment)" [attr.aria-label]="'core.delete' | translate"> | ||||
|                     <ion-icon name="trash"></ion-icon> | ||||
|                 </button> | ||||
|             </ion-item> | ||||
|             <ion-item text-wrap> | ||||
|                 <core-format-text clean="true" [text]="offlineComment.content"></core-format-text> | ||||
|             </ion-item> | ||||
|         </ion-card> | ||||
| 
 | ||||
|         <ion-card *ngFor="let comment of comments"> | ||||
|             <ion-item text-wrap> | ||||
|                 <ion-avatar core-user-avatar [user]="comment" item-start></ion-avatar> | ||||
|                 <h2>{{ comment.fullname }}</h2> | ||||
|                 <p>{{ comment.time }}</p> | ||||
|                 <p *ngIf="!comment.deleted">{{ comment.timecreated * 1000 | coreFormatDate: 'strftimerecentfull' }}</p> | ||||
|                 <p *ngIf="comment.deleted"> | ||||
|                     <ion-icon name="trash"></ion-icon> <span text-wrap>{{ 'core.deletedoffline' | translate }}</span> | ||||
|                 </p> | ||||
|                 <button *ngIf="showDelete && !comment.deleted && comment.delete" item-end ion-button icon-only clear [@coreSlideInOut]="'fromRight'" color="danger" (click)="deleteComment($event, comment)" [attr.aria-label]="'core.delete' | translate"> | ||||
|                     <ion-icon name="trash"></ion-icon> | ||||
|                 </button> | ||||
|                 <button *ngIf="showDelete && comment.deleted" item-end ion-button icon-only clear color="danger" (click)="undoDeleteComment($event, comment)" [attr.aria-label]="'core.restore' | translate"> | ||||
|                     <ion-icon name="undo"></ion-icon> | ||||
|                 </button> | ||||
|             </ion-item> | ||||
|             <ion-item text-wrap> | ||||
|                 <core-format-text clean="true" [text]="comment.content"></core-format-text> | ||||
|             </ion-item> | ||||
|         </ion-card> | ||||
| 
 | ||||
|         <core-infinite-loading [enabled]="canLoadMore" (action)="loadMore($event)" [error]="loadMoreError"></core-infinite-loading> | ||||
|     </core-loading> | ||||
| 
 | ||||
|     <ion-fab core-fab bottom end *ngIf="canAddComments"> | ||||
|         <button ion-fab (click)="addComment($event)" [attr.aria-label]="'core.comments.addcomment' | translate"> | ||||
|             <ion-icon name="add"></ion-icon> | ||||
|         </button> | ||||
|     </ion-fab> | ||||
| </ion-content> | ||||
|  | ||||
| @ -18,6 +18,7 @@ import { TranslateModule } from '@ngx-translate/core'; | ||||
| import { CoreCommentsViewerPage } from './viewer'; | ||||
| import { CoreComponentsModule } from '@components/components.module'; | ||||
| import { CoreDirectivesModule } from '@directives/directives.module'; | ||||
| import { CorePipesModule } from '@pipes/pipes.module'; | ||||
| import { CoreCommentsComponentsModule } from '../../components/components.module'; | ||||
| 
 | ||||
| @NgModule({ | ||||
| @ -27,6 +28,7 @@ import { CoreCommentsComponentsModule } from '../../components/components.module | ||||
|     imports: [ | ||||
|         CoreComponentsModule, | ||||
|         CoreDirectivesModule, | ||||
|         CorePipesModule, | ||||
|         CoreCommentsComponentsModule, | ||||
|         IonicPageModule.forChild(CoreCommentsViewerPage), | ||||
|         TranslateModule.forChild() | ||||
|  | ||||
| @ -12,13 +12,19 @@ | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Component, ViewChild } from '@angular/core'; | ||||
| import { IonicPage, Content, NavParams } from 'ionic-angular'; | ||||
| import { Component, ViewChild, OnDestroy } from '@angular/core'; | ||||
| import { IonicPage, Content, NavParams, ModalController } from 'ionic-angular'; | ||||
| import { TranslateService } from '@ngx-translate/core'; | ||||
| import { coreSlideInOut } from '@classes/animations'; | ||||
| import { CoreSitesProvider } from '@providers/sites'; | ||||
| import { CoreDomUtilsProvider } from '@providers/utils/dom'; | ||||
| import { CoreTextUtilsProvider } from '@providers/utils/text'; | ||||
| import { CoreTimeUtilsProvider } from '@providers/utils/time'; | ||||
| import { CoreEventsProvider } from '@providers/events'; | ||||
| import { CoreUserProvider } from '@core/user/providers/user'; | ||||
| import { CoreCommentsProvider } from '../../providers/comments'; | ||||
| import { CoreCommentsOfflineProvider } from '../../providers/offline'; | ||||
| import { CoreCommentsSyncProvider } from '../../providers/sync'; | ||||
| 
 | ||||
| /** | ||||
|  * Page that displays comments. | ||||
| @ -27,81 +33,333 @@ import { CoreCommentsProvider } from '../../providers/comments'; | ||||
| @Component({ | ||||
|     selector: 'page-core-comments-viewer', | ||||
|     templateUrl: 'viewer.html', | ||||
|     animations: [coreSlideInOut] | ||||
| }) | ||||
| export class CoreCommentsViewerPage { | ||||
| export class CoreCommentsViewerPage implements OnDestroy { | ||||
|     @ViewChild(Content) content: Content; | ||||
| 
 | ||||
|     comments = []; | ||||
|     commentsLoaded = false; | ||||
|     contextLevel: string; | ||||
|     instanceId: number; | ||||
|     component: string; | ||||
|     componentName: string; | ||||
|     itemId: number; | ||||
|     area: string; | ||||
|     page: number; | ||||
|     title: string; | ||||
|     canLoadMore = false; | ||||
|     loadMoreError = false; | ||||
|     canAddComments = false; | ||||
|     canDeleteComments = false; | ||||
|     showDelete = false; | ||||
|     hasOffline = false; | ||||
|     refreshIcon = 'spinner'; | ||||
|     syncIcon = 'spinner'; | ||||
|     offlineComment: any; | ||||
|     currentUserId: number; | ||||
| 
 | ||||
|     constructor(navParams: NavParams, sitesProvider: CoreSitesProvider, private userProvider: CoreUserProvider, | ||||
|              private domUtils: CoreDomUtilsProvider, private translate: TranslateService, | ||||
|              private commentsProvider: CoreCommentsProvider) { | ||||
|     protected addDeleteCommentsAvailable = false; | ||||
|     protected syncObserver: any; | ||||
|     protected currentUser: any; | ||||
| 
 | ||||
|     constructor(navParams: NavParams, private sitesProvider: CoreSitesProvider, private userProvider: CoreUserProvider, | ||||
|              private domUtils: CoreDomUtilsProvider, private translate: TranslateService, private modalCtrl: ModalController, | ||||
|              private commentsProvider: CoreCommentsProvider, private offlineComments: CoreCommentsOfflineProvider, | ||||
|              eventsProvider: CoreEventsProvider, private commentsSync: CoreCommentsSyncProvider, | ||||
|              private textUtils: CoreTextUtilsProvider, private timeUtils: CoreTimeUtilsProvider) { | ||||
| 
 | ||||
|         this.contextLevel = navParams.get('contextLevel'); | ||||
|         this.instanceId = navParams.get('instanceId'); | ||||
|         this.component = navParams.get('component'); | ||||
|         this.componentName = navParams.get('componentName'); | ||||
|         this.itemId = navParams.get('itemId'); | ||||
|         this.area = navParams.get('area') || ''; | ||||
|         this.page = navParams.get('page') || 0; | ||||
|         this.title = navParams.get('title') || this.translate.instant('core.comments'); | ||||
|         this.title = navParams.get('title') || this.translate.instant('core.comments.comments'); | ||||
|         this.page = 0; | ||||
| 
 | ||||
|         // Refresh data if comments are synchronized automatically.
 | ||||
|         this.syncObserver = eventsProvider.on(CoreCommentsSyncProvider.AUTO_SYNCED, (data) => { | ||||
|             if (data.contextLevel == this.contextLevel && data.instanceId == this.instanceId && | ||||
|                     data.componentName == this.componentName && data.itemId == this.itemId && data.area == this.area) { | ||||
|                 // Show the sync warnings.
 | ||||
|                 this.showSyncWarnings(data.warnings); | ||||
| 
 | ||||
|                 // Refresh the data.
 | ||||
|                 this.commentsLoaded = false; | ||||
|                 this.refreshIcon = 'spinner'; | ||||
|                 this.syncIcon = 'spinner'; | ||||
| 
 | ||||
|                 this.domUtils.scrollToTop(this.content); | ||||
| 
 | ||||
|                 this.page = 0; | ||||
|                 this.comments = []; | ||||
|                 this.fetchComments(false); | ||||
|             } | ||||
|         }, sitesProvider.getCurrentSiteId()); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * View loaded. | ||||
|      */ | ||||
|     ionViewDidLoad(): void { | ||||
|         this.fetchComments().finally(() => { | ||||
|             this.commentsLoaded = true; | ||||
|         this.commentsProvider.isAddCommentsAvailable().then((enabled) => { | ||||
|             // Is implicit the user can delete if he can add.
 | ||||
|             this.addDeleteCommentsAvailable = enabled; | ||||
|         }); | ||||
| 
 | ||||
|         this.currentUserId = this.sitesProvider.getCurrentSiteUserId(); | ||||
|         this.fetchComments(true); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetches the comments. | ||||
|      * | ||||
|      * @param  {boolean} sync         When to resync comments. | ||||
|      * @param  {boolean} [showErrors] When to display errors or not. | ||||
|      * @return {Promise<any>} Resolved when done. | ||||
|      */ | ||||
|     protected fetchComments(): Promise<any> { | ||||
|         // Get comments data.
 | ||||
|         return this.commentsProvider.getComments(this.contextLevel, this.instanceId, this.component, this.itemId, | ||||
|                 this.area, this.page).then((comments) => { | ||||
|             this.comments = comments; | ||||
|             this.comments.sort((a, b) => b.timecreated - a.timecreated); | ||||
|             this.comments.forEach((comment) => { | ||||
|                 // Get the user profile image.
 | ||||
|                 this.userProvider.getProfile(comment.userid, undefined, true).then((user) => { | ||||
|                     comment.profileimageurl = user.profileimageurl; | ||||
|                 }).catch(() => { | ||||
|                     // Ignore errors.
 | ||||
|     protected fetchComments(sync: boolean, showErrors?: boolean): Promise<any> { | ||||
|         this.loadMoreError = false; | ||||
| 
 | ||||
|         const promise = sync ? this.syncComments(showErrors) : Promise.resolve(); | ||||
| 
 | ||||
|         return promise.catch(() => { | ||||
|             // Ignore errors.
 | ||||
|         }).then(() => { | ||||
|             return this.offlineComments.getComment(this.contextLevel, this.instanceId, this.componentName, this.itemId, | ||||
|                     this.area).then((offlineComment) => { | ||||
|                 this.offlineComment = offlineComment; | ||||
| 
 | ||||
|                 if (offlineComment && !this.currentUser) { | ||||
|                     return this.userProvider.getProfile(this.currentUserId, undefined, true).then((user) => { | ||||
|                         this.currentUser = user; | ||||
|                         this.offlineComment.profileimageurl = user.profileimageurl; | ||||
|                         this.offlineComment.fullname = user.fullname; | ||||
|                         this.offlineComment.userid = user.id; | ||||
|                     }).catch(() => { | ||||
|                         // Ignore errors.
 | ||||
|                     }); | ||||
|                 } else if (offlineComment) { | ||||
|                     this.offlineComment.profileimageurl = this.currentUser.profileimageurl; | ||||
|                     this.offlineComment.fullname = this.currentUser.fullname; | ||||
|                     this.offlineComment.userid = this.currentUser.id; | ||||
|                 } | ||||
| 
 | ||||
|                 return this.offlineComments.getDeletedComments(this.contextLevel, this.instanceId, this.componentName, this.itemId, | ||||
|                     this.area); | ||||
|             }); | ||||
|         }).then((deletedComments) => { | ||||
|             this.hasOffline = !!this.offlineComment || deletedComments.length > 0; | ||||
| 
 | ||||
|             // Get comments data.
 | ||||
|             return this.commentsProvider.getComments(this.contextLevel, this.instanceId, this.componentName, this.itemId, | ||||
|                     this.area, this.page).then((response) => { | ||||
|                 this.canAddComments = this.addDeleteCommentsAvailable && response.canpost; | ||||
| 
 | ||||
|                 const comments = response.comments.sort((a, b) => b.timecreated - a.timecreated); | ||||
|                 this.canLoadMore = comments.length >= CoreCommentsProvider.pageSize; | ||||
| 
 | ||||
|                 return Promise.all(comments.map((comment) => { | ||||
|                     // Get the user profile image.
 | ||||
|                     return this.userProvider.getProfile(comment.userid, undefined, true).then((user) => { | ||||
|                         comment.profileimageurl = user.profileimageurl; | ||||
| 
 | ||||
|                         return comment; | ||||
|                     }).catch(() => { | ||||
|                         // Ignore errors.
 | ||||
|                         return comment; | ||||
|                     }); | ||||
|                 })); | ||||
|             }).then((comments) => { | ||||
|                 this.comments = this.comments.concat(comments); | ||||
| 
 | ||||
|                 deletedComments && deletedComments.forEach((deletedComment) => { | ||||
|                     const comment = this.comments.find((comment) => { | ||||
|                         return comment.id == deletedComment.commentid; | ||||
|                     }); | ||||
| 
 | ||||
|                     if (comment) { | ||||
|                         comment.deleted = deletedComment.deleted; | ||||
|                     } | ||||
|                 }); | ||||
| 
 | ||||
|                 this.canDeleteComments = this.addDeleteCommentsAvailable && (this.hasOffline || this.comments.some((comment) => { | ||||
|                     return !!comment.delete; | ||||
|                 })); | ||||
|             }); | ||||
|         }).catch((error) => { | ||||
|             if (error && this.component == 'assignsubmission_comments') { | ||||
|                 this.domUtils.showAlertTranslated('core.notice', 'core.commentsnotworking'); | ||||
|             this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading.
 | ||||
|             if (error && this.componentName == 'assignsubmission_comments') { | ||||
|                 this.domUtils.showAlertTranslated('core.notice', 'core.comments.commentsnotworking'); | ||||
|             } else { | ||||
|                 this.domUtils.showErrorModalDefault(error, this.translate.instant('core.error') + ': get_comments'); | ||||
|             } | ||||
|         }).finally(() => { | ||||
|             this.commentsLoaded = true; | ||||
|             this.refreshIcon = 'refresh'; | ||||
|             this.syncIcon = 'sync'; | ||||
|         }); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Function to load more commemts. | ||||
|      * | ||||
|      * @param {any} [infiniteComplete] Infinite scroll complete function. Only used from core-infinite-loading. | ||||
|      * @return {Promise<any>} Resolved when done. | ||||
|      */ | ||||
|     loadMore(infiniteComplete?: any): Promise<any> { | ||||
|         this.page++; | ||||
|         this.canLoadMore = false; | ||||
| 
 | ||||
|         return this.fetchComments(true).finally(() => { | ||||
|             infiniteComplete && infiniteComplete(); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Refresh the comments. | ||||
|      * | ||||
|      * @param {any} refresher Refresher. | ||||
|      * @param {boolean} showErrors Whether to display errors or not. | ||||
|      * @param {any} [refresher] Refresher. | ||||
|      * @return {Promise<any>} Resolved when done. | ||||
|      */ | ||||
|     refreshComments(refresher: any): void { | ||||
|         this.commentsProvider.invalidateCommentsData(this.contextLevel, this.instanceId, this.component, | ||||
|                 this.itemId, this.area, this.page).finally(() => { | ||||
|             return this.fetchComments().finally(() => { | ||||
|                 refresher.complete(); | ||||
|     refreshComments(showErrors: boolean, refresher?: any): Promise<any> { | ||||
|         this.refreshIcon = 'spinner'; | ||||
|         this.syncIcon = 'spinner'; | ||||
| 
 | ||||
|         return this.commentsProvider.invalidateCommentsData(this.contextLevel, this.instanceId, this.componentName, | ||||
|                 this.itemId, this.area).finally(() => { | ||||
|             this.page = 0; | ||||
|             this.comments = []; | ||||
| 
 | ||||
|             return this.fetchComments(true, showErrors).finally(() => { | ||||
|                 refresher && refresher.complete(); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Show sync warnings if any. | ||||
|      * | ||||
|      * @param {string[]} warnings the warnings | ||||
|      */ | ||||
|     private showSyncWarnings(warnings: string[]): void { | ||||
|         const message = this.textUtils.buildMessage(warnings); | ||||
|         if (message) { | ||||
|             this.domUtils.showErrorModal(message); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Tries to synchronize comments. | ||||
|      * | ||||
|      * @param  {boolean} showErrors Whether to display errors or not. | ||||
|      * @return {Promise<any>}       Promise resolved if sync is successful, rejected otherwise. | ||||
|      */ | ||||
|     private syncComments(showErrors: boolean): Promise<any> { | ||||
|         return this.commentsSync.syncComments(this.contextLevel, this.instanceId, this.componentName, this.itemId, | ||||
|                 this.area).then((warnings) => { | ||||
|             this.showSyncWarnings(warnings); | ||||
|         }).catch((error) => { | ||||
|             if (showErrors) { | ||||
|                 this.domUtils.showErrorModalDefault(error, 'core.errorsync', true); | ||||
|             } | ||||
| 
 | ||||
|             return Promise.reject(null); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Add a new comment to the list. | ||||
|      * | ||||
|      * @param {Event} e Event. | ||||
|      */ | ||||
|     addComment(e: Event): void { | ||||
|         e.preventDefault(); | ||||
|         e.stopPropagation(); | ||||
| 
 | ||||
|         const params = { | ||||
|             contextLevel: this.contextLevel, | ||||
|             instanceId: this.instanceId, | ||||
|             componentName: this.componentName, | ||||
|             itemId: this.itemId, | ||||
|             area: this.area, | ||||
|             content: this.hasOffline ? this.offlineComment.content : '' | ||||
|         }; | ||||
| 
 | ||||
|         const modal = this.modalCtrl.create('CoreCommentsAddPage', params); | ||||
|         modal.onDidDismiss((data) => { | ||||
|             if (data && data.comments) { | ||||
|                 this.comments = data.comments.concat(this.comments); | ||||
|                 this.canDeleteComments = this.addDeleteCommentsAvailable; | ||||
|             } else if (data && !data.comments) { | ||||
|                 this.fetchComments(false); | ||||
|             } | ||||
|         }); | ||||
|         modal.present(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete a comment. | ||||
|      * | ||||
|      * @param {Event} e     Click event. | ||||
|      * @param {any} comment Comment to delete. | ||||
|      */ | ||||
|     deleteComment(e: Event, comment: any): void { | ||||
|         e.preventDefault(); | ||||
|         e.stopPropagation(); | ||||
| 
 | ||||
|         const time = this.timeUtils.userDate((comment.lastmodified || comment.timecreated) * 1000, 'core.strftimerecentfull'); | ||||
| 
 | ||||
|         comment.contextlevel = this.contextLevel; | ||||
|         comment.instanceid = this.instanceId; | ||||
|         comment.component = this.componentName; | ||||
|         comment.itemid = this.itemId; | ||||
|         comment.area = this.area; | ||||
| 
 | ||||
|         this.domUtils.showConfirm(this.translate.instant('core.comments.deletecommentbyon', {$a: | ||||
|                 { user: comment.fullname || '', time: time } })).then(() => { | ||||
|             this.commentsProvider.deleteComment(comment).then(() => { | ||||
|                 this.showDelete = false; | ||||
| 
 | ||||
|                 this.refreshComments(true); | ||||
| 
 | ||||
|                 this.domUtils.showToast('core.comments.eventcommentdeleted', true, 3000); | ||||
|             }).catch((error) => { | ||||
|                 this.domUtils.showErrorModalDefault(error, 'Delete comment failed.'); | ||||
|             }); | ||||
|         }).catch(() => { | ||||
|             // User cancelled, nothing to do.
 | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Restore a comment. | ||||
|      * | ||||
|      * @param {Event} e Click event. | ||||
|      * @param {any} comment Comment to delete. | ||||
|      */ | ||||
|     undoDeleteComment(e: Event, comment: any): void { | ||||
|         e.preventDefault(); | ||||
|         e.stopPropagation(); | ||||
| 
 | ||||
|         this.offlineComments.undoDeleteComment(comment.id).then(() => { | ||||
|             comment.deleted = false; | ||||
|             this.showDelete = false; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Toggle delete. | ||||
|      */ | ||||
|     toggleDelete(): void { | ||||
|         this.showDelete = !this.showDelete; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Page destroyed. | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         this.syncObserver && this.syncObserver.off(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -13,8 +13,11 @@ | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { CoreAppProvider } from '@providers/app'; | ||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||
| import { CoreSitesProvider } from '@providers/sites'; | ||||
| import { CoreSite } from '@classes/site'; | ||||
| import { CoreCommentsOfflineProvider } from './offline'; | ||||
| 
 | ||||
| /** | ||||
|  * Service that provides some features regarding comments. | ||||
| @ -23,8 +26,110 @@ import { CoreSite } from '@classes/site'; | ||||
| export class CoreCommentsProvider { | ||||
| 
 | ||||
|     protected ROOT_CACHE_KEY = 'mmComments:'; | ||||
|     static pageSize = null; | ||||
|     static pageSizeOK = false; // If true, the pageSize is definitive. If not, it's a temporal value to reduce WS calls.
 | ||||
| 
 | ||||
|     constructor(private sitesProvider: CoreSitesProvider) {} | ||||
|     constructor(private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider, private appProvider: CoreAppProvider, | ||||
|         private commentsOffline: CoreCommentsOfflineProvider) {} | ||||
| 
 | ||||
|     /** | ||||
|      * Add a comment. | ||||
|      * | ||||
|      * @param  {string} content      Comment text. | ||||
|      * @param  {string} contextLevel Contextlevel system, course, user... | ||||
|      * @param  {number} instanceId   The Instance id of item associated with the context level. | ||||
|      * @param  {string} component    Component name. | ||||
|      * @param  {number} itemId       Associated id. | ||||
|      * @param  {string} [area='']    String comment area. Default empty. | ||||
|      * @param  {string} [siteId]     Site ID. If not defined, current site. | ||||
|      * @return {Promise<boolean>}    Promise resolved with boolean: true if comment was sent to server, false if stored in device. | ||||
|      */ | ||||
|     addComment(content: string, contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', | ||||
|             siteId?: string): Promise<boolean> { | ||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||
| 
 | ||||
|         // Convenience function to store a comment to be synchronized later.
 | ||||
|         const storeOffline = (): Promise<any> => { | ||||
|             return this.commentsOffline.saveComment(content, contextLevel, instanceId, component, itemId, area, siteId).then(() => { | ||||
|                 return Promise.resolve(false); | ||||
|             }); | ||||
|         }; | ||||
| 
 | ||||
|         if (!this.appProvider.isOnline()) { | ||||
|             // App is offline, store the comment.
 | ||||
|             return storeOffline(); | ||||
|         } | ||||
| 
 | ||||
|         // Send comment to server.
 | ||||
|         return this.addCommentOnline(content, contextLevel, instanceId, component, itemId, area, siteId).then((comments) => { | ||||
|             return comments; | ||||
|         }).catch((error) => { | ||||
|             if (this.utils.isWebServiceError(error)) { | ||||
|                 // It's a WebService error, the user cannot send the message so don't store it.
 | ||||
|                 return Promise.reject(error); | ||||
|             } | ||||
| 
 | ||||
|             // Error sending comment, store it to retry later.
 | ||||
|             return storeOffline(); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Add a comment. It will fail if offline or cannot connect. | ||||
|      * | ||||
|      * @param  {string} content      Comment text. | ||||
|      * @param  {string} contextLevel Contextlevel system, course, user... | ||||
|      * @param  {number} instanceId   The Instance id of item associated with the context level. | ||||
|      * @param  {string} component    Component name. | ||||
|      * @param  {number} itemId       Associated id. | ||||
|      * @param  {string} [area='']    String comment area. Default empty. | ||||
|      * @param  {string} [siteId]     Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>}        Promise resolved when added, rejected otherwise. | ||||
|      */ | ||||
|     addCommentOnline(content: string, contextLevel: string, instanceId: number, component: string, itemId: number, | ||||
|             area: string = '', siteId?: string): Promise<any> { | ||||
|         const comments = [ | ||||
|             { | ||||
|                 contextlevel: contextLevel, | ||||
|                 instanceid: instanceId, | ||||
|                 component: component, | ||||
|                 itemid: itemId, | ||||
|                 area: area, | ||||
|                 content: content | ||||
|             } | ||||
|         ]; | ||||
| 
 | ||||
|         return this.addCommentsOnline(comments, siteId).then((commentsResponse) => { | ||||
|                // A cooment was added, invalidate them.
 | ||||
|             return this.invalidateCommentsData(contextLevel, instanceId, component, itemId, area, siteId).catch(() => { | ||||
|                 // Ignore errors.
 | ||||
|             }).then(() => { | ||||
|                 return commentsResponse; | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Add several comments. It will fail if offline or cannot connect. | ||||
|      * | ||||
|      * @param  {any[]}  comments Comments to save. | ||||
|      * @param  {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>}    Promise resolved when added, rejected otherwise. Promise resolved doesn't mean that comments | ||||
|      *                           have been added, the resolve param can contain errors for comments not sent. | ||||
|      */ | ||||
|     addCommentsOnline(comments: any[], siteId?: string): Promise<any> { | ||||
|         if (!comments || !comments.length) { | ||||
|             return Promise.resolve(); | ||||
|         } | ||||
| 
 | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             const data = { | ||||
|                 comments: comments | ||||
|             }; | ||||
| 
 | ||||
|             return site.write('core_comment_add_comments', data); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if Calendar is disabled in a certain site. | ||||
| @ -50,6 +155,97 @@ export class CoreCommentsProvider { | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete a comment. | ||||
|      * | ||||
|      * @param  {any} comment         Comment object to delete. | ||||
|      * @param  {string} [siteId]     Site ID. If not defined, current site. | ||||
|      * @return {Promise<void>}       Promise resolved when deleted, rejected otherwise. Promise resolved doesn't mean that comments | ||||
|      *                               have been deleted, the resolve param can contain errors for comments not deleted. | ||||
|      */ | ||||
|     deleteComment(comment: any, siteId?: string): Promise<void> { | ||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||
| 
 | ||||
|         if (!comment.id) { | ||||
|             return this.commentsOffline.removeComment(comment.contextlevel, comment.instanceid, comment.component, comment.itemid, | ||||
|                     comment.area, siteId); | ||||
|         } | ||||
| 
 | ||||
|         // Convenience function to store the action to be synchronized later.
 | ||||
|         const storeOffline = (): Promise<any> => { | ||||
|             return this.commentsOffline.deleteComment(comment.id, comment.contextlevel, comment.instanceid, comment.component, | ||||
|                     comment.itemid, comment.area, siteId).then(() => { | ||||
|                 return false; | ||||
|             }); | ||||
|         }; | ||||
| 
 | ||||
|         if (!this.appProvider.isOnline()) { | ||||
|             // App is offline, store the comment.
 | ||||
|             return storeOffline(); | ||||
|         } | ||||
| 
 | ||||
|         // Send comment to server.
 | ||||
|         return this.deleteCommentsOnline([comment.id], comment.contextlevel, comment.instanceid, comment.component, comment.itemid, | ||||
|                 comment.area, siteId).then(() => { | ||||
|             return true; | ||||
|         }).catch((error) => { | ||||
|             if (this.utils.isWebServiceError(error)) { | ||||
|                 // It's a WebService error, the user cannot send the comment so don't store it.
 | ||||
|                 return Promise.reject(error); | ||||
|             } | ||||
| 
 | ||||
|             // Error sending comment, store it to retry later.
 | ||||
|             return storeOffline(); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete a comment. It will fail if offline or cannot connect. | ||||
|      * | ||||
|      * @param  {number[]} commentIds Comment IDs to delete. | ||||
|      * @param  {string} contextLevel Contextlevel system, course, user... | ||||
|      * @param  {number} instanceId   The Instance id of item associated with the context level. | ||||
|      * @param  {string} component    Component name. | ||||
|      * @param  {number} itemId       Associated id. | ||||
|      * @param  {string} [area='']    String comment area. Default empty. | ||||
|      * @param  {string} [siteId]     Site ID. If not defined, current site. | ||||
|      * @return {Promise<void>}       Promise resolved when deleted, rejected otherwise. Promise resolved doesn't mean that comments | ||||
|      *                               have been deleted, the resolve param can contain errors for comments not deleted. | ||||
|      */ | ||||
|     deleteCommentsOnline(commentIds: number[], contextLevel: string, instanceId: number, component: string, itemId: number, | ||||
|             area: string = '', siteId?: string): Promise<void> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             const data = { | ||||
|                 comments: commentIds | ||||
|             }; | ||||
| 
 | ||||
|             return site.write('core_comment_delete_comments', data).then((response) => { | ||||
|                 // A comment was deleted, invalidate comments.
 | ||||
|                 return this.invalidateCommentsData(contextLevel, instanceId, component, itemId, area, siteId).catch(() => { | ||||
|                     // Ignore errors.
 | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns whether WS to add/delete comments are available in site. | ||||
|      * | ||||
|      * @param {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<boolean>} Promise resolved with true if available, resolved with false or rejected otherwise. | ||||
|      * @since 3.8 | ||||
|      */ | ||||
|     isAddCommentsAvailable(siteId?: string): Promise<boolean> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             // First check if it's disabled.
 | ||||
|             if (this.areCommentsDisabledInSite(site)) { | ||||
|                 return false; | ||||
|             } | ||||
| 
 | ||||
|             return site.wsAvailable('core_comment_add_comments'); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get cache key for get comments data WS calls. | ||||
|      * | ||||
| @ -58,12 +254,11 @@ export class CoreCommentsProvider { | ||||
|      * @param  {string} component    Component name. | ||||
|      * @param  {number} itemId       Associated id. | ||||
|      * @param  {string} [area='']    String comment area. Default empty. | ||||
|      * @param  {number} [page=0]     Page number (0 based). Default 0. | ||||
|      * @return {string} Cache key. | ||||
|      */ | ||||
|     protected getCommentsCacheKey(contextLevel: string, instanceId: number, component: string, | ||||
|             itemId: number, area: string = '', page: number = 0): string { | ||||
|         return this.getCommentsPrefixCacheKey(contextLevel, instanceId) + ':' + component + ':' + itemId + ':' + area + ':' + page; | ||||
|     protected getCommentsCacheKey(contextLevel: string, instanceId: number, component: string, itemId: number, | ||||
|             area: string = ''): string { | ||||
|         return this.getCommentsPrefixCacheKey(contextLevel, instanceId) + ':' + component + ':' + itemId + ':' + area; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -89,8 +284,8 @@ export class CoreCommentsProvider { | ||||
|      * @param  {string} [siteId]     Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>} Promise resolved with the comments. | ||||
|      */ | ||||
|     getComments(contextLevel: string, instanceId: number, component: string, itemId: number, | ||||
|             area: string = '', page: number = 0, siteId?: string): Promise<any> { | ||||
|     getComments(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', page: number = 0, | ||||
|             siteId?: string): Promise<any> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             const params: any = { | ||||
|                 contextlevel: contextLevel, | ||||
| @ -102,13 +297,13 @@ export class CoreCommentsProvider { | ||||
|             }; | ||||
| 
 | ||||
|             const preSets = { | ||||
|                 cacheKey: this.getCommentsCacheKey(contextLevel, instanceId, component, itemId, area, page), | ||||
|                 cacheKey: this.getCommentsCacheKey(contextLevel, instanceId, component, itemId, area), | ||||
|                 updateFrequency: CoreSite.FREQUENCY_SOMETIMES | ||||
|             }; | ||||
| 
 | ||||
|             return site.read('core_comment_get_comments', params, preSets).then((response) => { | ||||
|                 if (response.comments) { | ||||
|                     return response.comments; | ||||
|                     return response; | ||||
|                 } | ||||
| 
 | ||||
|                 return Promise.reject(null); | ||||
| @ -116,6 +311,61 @@ export class CoreCommentsProvider { | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get comments count number to show on the comments component. | ||||
|      * | ||||
|      * @param  {string} contextLevel Contextlevel system, course, user... | ||||
|      * @param  {number} instanceId   The Instance id of item associated with the context level. | ||||
|      * @param  {string} component    Component name. | ||||
|      * @param  {number} itemId       Associated id. | ||||
|      * @param  {string} [area='']    String comment area. Default empty. | ||||
|      * @param  {string} [siteId]     Site ID. If not defined, current site. | ||||
|      * @return {Promise<string>}     Comments count with plus sign if needed. | ||||
|      */ | ||||
|     getCommentsCount(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', | ||||
|             siteId?: string): Promise<string> { | ||||
| 
 | ||||
|         siteId = siteId ? siteId : this.sitesProvider.getCurrentSiteId(); | ||||
| 
 | ||||
|         // Convenience function to get comments number on a page.
 | ||||
|         const getCommentsPageCount = (page: number): Promise<number> => { | ||||
|             return this.getComments(contextLevel, instanceId, component, itemId, area, page, siteId).then((response) => { | ||||
|                 if (response.comments) { | ||||
|                     // Update pageSize with the greatest count at the moment.
 | ||||
|                     if (response.comments && response.comments.length > CoreCommentsProvider.pageSize) { | ||||
|                         CoreCommentsProvider.pageSize = response.comments.length; | ||||
|                     } | ||||
| 
 | ||||
|                     return response.comments && response.comments.length ? response.comments.length : 0; | ||||
|                 } | ||||
| 
 | ||||
|                 return -1; | ||||
|             }).catch(() => { | ||||
|                 return -1; | ||||
|             }); | ||||
|         }; | ||||
| 
 | ||||
|         return getCommentsPageCount(0).then((count) => { | ||||
|             if (CoreCommentsProvider.pageSizeOK && count >= CoreCommentsProvider.pageSize) { | ||||
|                 // Page Size is ok, show + in case it reached the limit.
 | ||||
|                 return (CoreCommentsProvider.pageSize - 1) + '+'; | ||||
|             } else if (count < 0 || (CoreCommentsProvider.pageSize && count < CoreCommentsProvider.pageSize)) { | ||||
|                 return count + ''; | ||||
|             } | ||||
| 
 | ||||
|             // Call to update page size.
 | ||||
|             return getCommentsPageCount(1).then((countMore) => { | ||||
|                 // Page limit was reached on the previous call.
 | ||||
|                 if (countMore > 0) { | ||||
| 
 | ||||
|                     return (CoreCommentsProvider.pageSize - 1) + '+'; | ||||
|                 } | ||||
| 
 | ||||
|                 return count + ''; | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Invalidates comments data. | ||||
|      * | ||||
| @ -124,14 +374,20 @@ export class CoreCommentsProvider { | ||||
|      * @param  {string} component    Component name. | ||||
|      * @param  {number} itemId       Associated id. | ||||
|      * @param  {string} [area='']    String comment area. Default empty. | ||||
|      * @param  {number} [page=0]     Page number (0 based). Default 0. | ||||
|      * @param  {string} [siteId]     Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>} Promise resolved when the data is invalidated. | ||||
|      */ | ||||
|     invalidateCommentsData(contextLevel: string, instanceId: number, component: string, itemId: number, | ||||
|             area: string = '', page: number = 0, siteId?: string): Promise<any> { | ||||
|             area: string = '', siteId?: string): Promise<any> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             return site.invalidateWsCacheForKey(this.getCommentsCacheKey(contextLevel, instanceId, component, itemId, area, page)); | ||||
| 
 | ||||
|             return this.utils.allPromises([ | ||||
|                 // This is done with starting with to avoid conflicts with previous keys that were including page.
 | ||||
|                 site.invalidateWsCacheForKeyStartingWith(this.getCommentsCacheKey(contextLevel, instanceId, component, itemId, | ||||
|                     area) + ':'), | ||||
| 
 | ||||
|                 site.invalidateWsCacheForKey(this.getCommentsCacheKey(contextLevel, instanceId, component, itemId, area)) | ||||
|             ]); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										338
									
								
								src/core/comments/providers/offline.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										338
									
								
								src/core/comments/providers/offline.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,338 @@ | ||||
| // (C) Copyright 2015 Martin Dougiamas
 | ||||
| //
 | ||||
| // 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 { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; | ||||
| import { CoreTimeUtilsProvider } from '@providers/utils/time'; | ||||
| 
 | ||||
| /** | ||||
|  * Service to handle offline comments. | ||||
|  */ | ||||
| @Injectable() | ||||
| export class CoreCommentsOfflineProvider { | ||||
| 
 | ||||
|     // Variables for database.
 | ||||
|     static COMMENTS_TABLE = 'core_comments_offline_comments'; | ||||
|     static COMMENTS_DELETED_TABLE = 'core_comments_deleted_offline_comments'; | ||||
|     protected siteSchema: CoreSiteSchema = { | ||||
|         name: 'CoreCommentsOfflineProvider', | ||||
|         version: 1, | ||||
|         tables: [ | ||||
|             { | ||||
|                 name: CoreCommentsOfflineProvider.COMMENTS_TABLE, | ||||
|                 columns: [ | ||||
|                     { | ||||
|                         name: 'contextlevel', | ||||
|                         type: 'TEXT' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'instanceid', | ||||
|                         type: 'INTEGER' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'component', | ||||
|                         type: 'TEXT', | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'itemid', | ||||
|                         type: 'INTEGER' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'area', | ||||
|                         type: 'TEXT' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'content', | ||||
|                         type: 'TEXT' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'lastmodified', | ||||
|                         type: 'INTEGER' | ||||
|                     } | ||||
|                 ], | ||||
|                 primaryKeys: ['contextlevel', 'instanceid', 'component', 'itemid', 'area'] | ||||
|             }, | ||||
|             { | ||||
|                 name: CoreCommentsOfflineProvider.COMMENTS_DELETED_TABLE, | ||||
|                 columns: [ | ||||
|                     { | ||||
|                         name: 'commentid', | ||||
|                         type: 'INTEGER', | ||||
|                         primaryKey: true | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'contextlevel', | ||||
|                         type: 'TEXT' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'instanceid', | ||||
|                         type: 'INTEGER' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'component', | ||||
|                         type: 'TEXT', | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'itemid', | ||||
|                         type: 'INTEGER' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'area', | ||||
|                         type: 'TEXT' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'deleted', | ||||
|                         type: 'INTEGER' | ||||
|                     } | ||||
|                 ] | ||||
|             } | ||||
|         ] | ||||
|     }; | ||||
| 
 | ||||
|     constructor( private sitesProvider: CoreSitesProvider, private timeUtils: CoreTimeUtilsProvider) { | ||||
|         this.sitesProvider.registerSiteSchema(this.siteSchema); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get all offline comments. | ||||
|      * | ||||
|      * @param  {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>}    Promise resolved with comments. | ||||
|      */ | ||||
|     getAllComments(siteId?: string): Promise<any> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             return Promise.all([site.getDb().getRecords(CoreCommentsOfflineProvider.COMMENTS_TABLE), | ||||
|                 site.getDb().getRecords(CoreCommentsOfflineProvider.COMMENTS_DELETED_TABLE)]).then((results) => { | ||||
|                     return [].concat.apply([], results); | ||||
|                 }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get an offline comment. | ||||
|      * | ||||
|      * @param  {string} contextLevel Contextlevel system, course, user... | ||||
|      * @param  {number} instanceId   The Instance id of item associated with the context level. | ||||
|      * @param  {string} component    Component name. | ||||
|      * @param  {number} itemId       Associated id. | ||||
|      * @param  {string} [area='']    String comment area. Default empty. | ||||
|      * @param  {string} [siteId]    Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>}       Promise resolved with the comments. | ||||
|      */ | ||||
|     getComment(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', | ||||
|             siteId?: string): Promise<any> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             return site.getDb().getRecord(CoreCommentsOfflineProvider.COMMENTS_TABLE, { | ||||
|                 contextlevel: contextLevel, | ||||
|                 instanceid: instanceId, | ||||
|                 component: component, | ||||
|                 itemid: itemId, | ||||
|                 area: area | ||||
|             }); | ||||
|         }).catch(() => { | ||||
|             return false; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get all offline comments added or deleted of a special area. | ||||
|      * | ||||
|      * @param  {string} contextLevel Contextlevel system, course, user... | ||||
|      * @param  {number} instanceId   The Instance id of item associated with the context level. | ||||
|      * @param  {string} component    Component name. | ||||
|      * @param  {number} itemId       Associated id. | ||||
|      * @param  {string} [area='']    String comment area. Default empty. | ||||
|      * @param  {string} [siteId]    Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>}       Promise resolved with the comments. | ||||
|      */ | ||||
|     getComments(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', | ||||
|             siteId?: string): Promise<any> { | ||||
|         let comments = []; | ||||
| 
 | ||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||
| 
 | ||||
|         return this.getComment(contextLevel, instanceId, component, itemId, area, siteId).then((comment) => { | ||||
|             comments = comment ? [comment] : []; | ||||
| 
 | ||||
|             return this.getDeletedComments(contextLevel, instanceId, component, itemId, area, siteId); | ||||
|         }).then((deletedComments) => { | ||||
|             comments = comments.concat(deletedComments); | ||||
| 
 | ||||
|             return comments; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get all offline deleted comments. | ||||
|      * | ||||
|      * @param  {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>}    Promise resolved with comments. | ||||
|      */ | ||||
|     getAllDeletedComments(siteId?: string): Promise<any> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             return site.getDb().getRecords(CoreCommentsOfflineProvider.COMMENTS_DELETED_TABLE); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get an offline comment. | ||||
|      * | ||||
|      * @param  {string} contextLevel Contextlevel system, course, user... | ||||
|      * @param  {number} instanceId   The Instance id of item associated with the context level. | ||||
|      * @param  {string} component    Component name. | ||||
|      * @param  {number} itemId       Associated id. | ||||
|      * @param  {string} [area='']    String comment area. Default empty. | ||||
|      * @param  {string} [siteId]    Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>}       Promise resolved with the comments. | ||||
|      */ | ||||
|     getDeletedComments(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', | ||||
|             siteId?: string): Promise<any> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             return site.getDb().getRecords(CoreCommentsOfflineProvider.COMMENTS_DELETED_TABLE, { | ||||
|                 contextlevel: contextLevel, | ||||
|                 instanceid: instanceId, | ||||
|                 component: component, | ||||
|                 itemid: itemId, | ||||
|                 area: area | ||||
|             }); | ||||
|         }).catch(() => { | ||||
|             return false; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Remove an offline comment. | ||||
|      * | ||||
|      * @param  {string} contextLevel Contextlevel system, course, user... | ||||
|      * @param  {number} instanceId   The Instance id of item associated with the context level. | ||||
|      * @param  {string} component    Component name. | ||||
|      * @param  {number} itemId       Associated id. | ||||
|      * @param  {string} [area='']    String comment area. Default empty. | ||||
|      * @param  {string} [siteId]    Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>}       Promise resolved if deleted, rejected if failure. | ||||
|      */ | ||||
|     removeComment(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', | ||||
|             siteId?: string): Promise<any> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             return site.getDb().deleteRecords(CoreCommentsOfflineProvider.COMMENTS_TABLE, { | ||||
|                 contextlevel: contextLevel, | ||||
|                 instanceid: instanceId, | ||||
|                 component: component, | ||||
|                 itemid: itemId, | ||||
|                 area: area | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Remove an offline deleted comment. | ||||
|      * | ||||
|      * @param  {string} contextLevel Contextlevel system, course, user... | ||||
|      * @param  {number} instanceId   The Instance id of item associated with the context level. | ||||
|      * @param  {string} component    Component name. | ||||
|      * @param  {number} itemId       Associated id. | ||||
|      * @param  {string} [area='']    String comment area. Default empty. | ||||
|      * @param  {string} [siteId]    Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>}       Promise resolved if deleted, rejected if failure. | ||||
|      */ | ||||
|     removeDeletedComments(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', | ||||
|             siteId?: string): Promise<any> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             return site.getDb().deleteRecords(CoreCommentsOfflineProvider.COMMENTS_DELETED_TABLE, { | ||||
|                 contextlevel: contextLevel, | ||||
|                 instanceid: instanceId, | ||||
|                 component: component, | ||||
|                 itemid: itemId, | ||||
|                 area: area | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Save a comment to be sent later. | ||||
|      * | ||||
|      * @param  {string} content      Comment text. | ||||
|      * @param  {string} contextLevel Contextlevel system, course, user... | ||||
|      * @param  {number} instanceId   The Instance id of item associated with the context level. | ||||
|      * @param  {string} component    Component name. | ||||
|      * @param  {number} itemId       Associated id. | ||||
|      * @param  {string} [area='']    String comment area. Default empty. | ||||
|      * @param  {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>}    Promise resolved if stored, rejected if failure. | ||||
|      */ | ||||
|     saveComment(content: string, contextLevel: string, instanceId: number, component: string, itemId: number, | ||||
|             area: string = '', siteId?: string): Promise<any> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             const now = this.timeUtils.timestamp(); | ||||
|             const data = { | ||||
|                 contextlevel: contextLevel, | ||||
|                 instanceid: instanceId, | ||||
|                 component: component, | ||||
|                 itemid: itemId, | ||||
|                 area: area, | ||||
|                 content: content, | ||||
|                 lastmodified: now | ||||
|             }; | ||||
| 
 | ||||
|             return site.getDb().insertRecord(CoreCommentsOfflineProvider.COMMENTS_TABLE, data).then(() => { | ||||
|                 return data; | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete a comment offline to be sent later. | ||||
|      * | ||||
|      * @param  {number} commentId    Comment ID. | ||||
|      * @param  {string} contextLevel Contextlevel system, course, user... | ||||
|      * @param  {number} instanceId   The Instance id of item associated with the context level. | ||||
|      * @param  {string} component    Component name. | ||||
|      * @param  {number} itemId       Associated id. | ||||
|      * @param  {string} [area='']    String comment area. Default empty. | ||||
|      * @param  {string} [siteId]     Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>}        Promise resolved if stored, rejected if failure. | ||||
|      */ | ||||
|     deleteComment(commentId: number, contextLevel: string, instanceId: number, component: string, itemId: number, | ||||
|             area: string = '', siteId?: string): Promise<any> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             const now = this.timeUtils.timestamp(); | ||||
|             const data = { | ||||
|                 contextlevel: contextLevel, | ||||
|                 instanceid: instanceId, | ||||
|                 component: component, | ||||
|                 itemid: itemId, | ||||
|                 area: area, | ||||
|                 commentid: commentId, | ||||
|                 deleted: now | ||||
|             }; | ||||
| 
 | ||||
|             return site.getDb().insertRecord(CoreCommentsOfflineProvider.COMMENTS_DELETED_TABLE, data).then(() => { | ||||
|                 return data; | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Undo delete a comment. | ||||
|      * | ||||
|      * @param  {number} commentId   Comment ID. | ||||
|      * @param  {string} [siteId]    Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>}       Promise resolved if deleted, rejected if failure. | ||||
|      */ | ||||
|     undoDeleteComment(commentId: number, siteId?: string): Promise<any> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             return site.getDb().deleteRecords(CoreCommentsOfflineProvider.COMMENTS_DELETED_TABLE, { commentid: commentId }); | ||||
|         }); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										48
									
								
								src/core/comments/providers/sync-cron-handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/core/comments/providers/sync-cron-handler.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,48 @@ | ||||
| // (C) Copyright 2015 Martin Dougiamas
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { CoreCronHandler } from '@providers/cron'; | ||||
| import { CoreCommentsSyncProvider } from './sync'; | ||||
| 
 | ||||
| /** | ||||
|  * Synchronization cron handler. | ||||
|  */ | ||||
| @Injectable() | ||||
| export class CoreCommentsSyncCronHandler implements CoreCronHandler { | ||||
|     name = 'CoreCommentsSyncCronHandler'; | ||||
| 
 | ||||
|     constructor(private commentsSync: CoreCommentsSyncProvider) {} | ||||
| 
 | ||||
|     /** | ||||
|      * Execute the process. | ||||
|      * Receives the ID of the site affected, undefined for all sites. | ||||
|      * | ||||
|      * @param  {string} [siteId] ID of the site affected, undefined for all sites. | ||||
|      * @param {boolean} [force] Wether the execution is forced (manual sync). | ||||
|      * @return {Promise<any>}         Promise resolved when done, rejected if failure. | ||||
|      */ | ||||
|     execute(siteId?: string, force?: boolean): Promise<any> { | ||||
|         return this.commentsSync.syncAllComments(siteId, force); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the time between consecutive executions. | ||||
|      * | ||||
|      * @return {number} Time between consecutive executions (in ms). | ||||
|      */ | ||||
|     getInterval(): number { | ||||
|         return 300000; // 5 minutes.
 | ||||
|     } | ||||
| } | ||||
							
								
								
									
										229
									
								
								src/core/comments/providers/sync.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										229
									
								
								src/core/comments/providers/sync.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,229 @@ | ||||
| // (C) Copyright 2015 Martin Dougiamas
 | ||||
| //
 | ||||
| // 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 { CoreLoggerProvider } from '@providers/logger'; | ||||
| import { CoreSitesProvider } from '@providers/sites'; | ||||
| import { CoreSyncBaseProvider } from '@classes/base-sync'; | ||||
| import { CoreAppProvider } from '@providers/app'; | ||||
| import { CoreCommentsOfflineProvider } from './offline'; | ||||
| import { CoreCommentsProvider } from './comments'; | ||||
| import { CoreEventsProvider } from '@providers/events'; | ||||
| import { CoreTextUtilsProvider } from '@providers/utils/text'; | ||||
| import { CoreTimeUtilsProvider } from '@providers/utils/time'; | ||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||
| import { TranslateService } from '@ngx-translate/core'; | ||||
| import { CoreSyncProvider } from '@providers/sync'; | ||||
| 
 | ||||
| /** | ||||
|  * Service to sync omments. | ||||
|  */ | ||||
| @Injectable() | ||||
| export class CoreCommentsSyncProvider extends CoreSyncBaseProvider { | ||||
| 
 | ||||
|     static AUTO_SYNCED = 'core_comments_autom_synced'; | ||||
| 
 | ||||
|     constructor(loggerProvider: CoreLoggerProvider, sitesProvider: CoreSitesProvider, appProvider: CoreAppProvider, | ||||
|             syncProvider: CoreSyncProvider, textUtils: CoreTextUtilsProvider, translate: TranslateService, | ||||
|             private commentsOffline: CoreCommentsOfflineProvider, private utils: CoreUtilsProvider, | ||||
|             private eventsProvider: CoreEventsProvider,  private commentsProvider: CoreCommentsProvider, | ||||
|             timeUtils: CoreTimeUtilsProvider) { | ||||
| 
 | ||||
|         super('CoreCommentsSync', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate, timeUtils); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Try to synchronize all the comments in a certain site or in all sites. | ||||
|      * | ||||
|      * @param  {string} [siteId] Site ID to sync. If not defined, sync all sites. | ||||
|      * @param {boolean} [force] Wether to force sync not depending on last execution. | ||||
|      * @return {Promise<any>}    Promise resolved if sync is successful, rejected if sync fails. | ||||
|      */ | ||||
|     syncAllComments(siteId?: string, force?: boolean): Promise<any> { | ||||
|         return this.syncOnSites('all comments', this.syncAllCommentsFunc.bind(this), [force], siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Synchronize all the comments in a certain site | ||||
|      * | ||||
|      * @param  {string} siteId Site ID to sync. | ||||
|      * @param  {boolean} force Wether to force sync not depending on last execution. | ||||
|      * @return {Promise<any>}  Promise resolved if sync is successful, rejected if sync fails. | ||||
|      */ | ||||
|     private syncAllCommentsFunc(siteId: string, force: boolean): Promise<any> { | ||||
|         return this.commentsOffline.getAllComments(siteId).then((comments) => { | ||||
| 
 | ||||
|             // Get Unique array.
 | ||||
|             comments.forEach((comment) => { | ||||
|                 comment.syncId = this.getSyncId(comment.contextlevel, comment.instanceid, comment.component, comment.itemid, | ||||
|                     comment.area); | ||||
|             }); | ||||
| 
 | ||||
|             comments = this.utils.uniqueArray(comments, 'syncId'); | ||||
| 
 | ||||
|             // Sync all courses.
 | ||||
|             const promises = comments.map((comment) => { | ||||
|                 const promise = force ? this.syncComments(comment.contextlevel, comment.instanceid, comment.component, | ||||
|                     comment.itemid, comment.area, siteId) : this.syncCommentsIfNeeded(comment.contextlevel, comment.instanceid, | ||||
|                     comment.component, comment.itemid, comment.area, siteId); | ||||
| 
 | ||||
|                 return promise.then((warnings) => { | ||||
|                     if (typeof warnings != 'undefined') { | ||||
|                         // Sync successful, send event.
 | ||||
|                         this.eventsProvider.trigger(CoreCommentsSyncProvider.AUTO_SYNCED, { | ||||
|                             contextLevel: comment.contextlevel, | ||||
|                             instanceId: comment.instanceid, | ||||
|                             componentName: comment.component, | ||||
|                             itemId: comment.itemid, | ||||
|                             area: comment.area, | ||||
|                             warnings: warnings | ||||
|                         }, siteId); | ||||
|                     } | ||||
|                 }); | ||||
|             }); | ||||
| 
 | ||||
|             return Promise.all(promises); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Sync course comments only if a certain time has passed since the last time. | ||||
|      * | ||||
|      * @param  {string} contextLevel Contextlevel system, course, user... | ||||
|      * @param  {number} instanceId   The Instance id of item associated with the context level. | ||||
|      * @param  {string} component    Component name. | ||||
|      * @param  {number} itemId       Associated id. | ||||
|      * @param  {string} [area='']    String comment area. Default empty. | ||||
|      * @param  {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>}    Promise resolved when the comments are synced or if they don't need to be synced. | ||||
|      */ | ||||
|     private syncCommentsIfNeeded(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', | ||||
|             siteId?: string): Promise<void> { | ||||
|         const syncId = this.getSyncId(contextLevel, instanceId, component, itemId, area); | ||||
| 
 | ||||
|         return this.isSyncNeeded(syncId, siteId).then((needed) => { | ||||
|             if (needed) { | ||||
|                 return this.syncComments(contextLevel, instanceId, component, itemId, area, siteId); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Synchronize comments in a particular area. | ||||
|      * | ||||
|      * @param  {string} contextLevel Contextlevel system, course, user... | ||||
|      * @param  {number} instanceId   The Instance id of item associated with the context level. | ||||
|      * @param  {string} component    Component name. | ||||
|      * @param  {number} itemId       Associated id. | ||||
|      * @param  {string} [area='']    String comment area. Default empty. | ||||
|      * @param  {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>}    Promise resolved if sync is successful, rejected otherwise. | ||||
|      */ | ||||
|     syncComments(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', | ||||
|             siteId?: string): Promise<any> { | ||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||
| 
 | ||||
|         const syncId = this.getSyncId(contextLevel, instanceId, component, itemId, area); | ||||
| 
 | ||||
|         if (this.isSyncing(syncId, siteId)) { | ||||
|             // There's already a sync ongoing for comments, return the promise.
 | ||||
|             return this.getOngoingSync(syncId, siteId); | ||||
|         } | ||||
| 
 | ||||
|         this.logger.debug('Try to sync comments ' + syncId); | ||||
| 
 | ||||
|         const warnings = []; | ||||
| 
 | ||||
|         // Get offline comments to be sent.
 | ||||
|         const syncPromise = this.commentsOffline.getComments(contextLevel, instanceId, component, itemId, area, siteId) | ||||
|                 .then((comments) => { | ||||
|             if (!comments.length) { | ||||
|                 // Nothing to sync.
 | ||||
|                 return; | ||||
|             } else if (!this.appProvider.isOnline()) { | ||||
|                 // Cannot sync in offline.
 | ||||
|                 return Promise.reject(this.translate.instant('core.networkerrormsg')); | ||||
|             } | ||||
| 
 | ||||
|             const errors = [], | ||||
|                 promises = [], | ||||
|                 deleteCommentIds = []; | ||||
| 
 | ||||
|             comments.forEach((comment) => { | ||||
|                 if (comment.commentid) { | ||||
|                     deleteCommentIds.push(comment.commentid); | ||||
|                 } else { | ||||
|                     promises.push(this.commentsProvider.addCommentOnline(comment.content, contextLevel, instanceId, component, | ||||
|                         itemId, area, siteId).then((response) => { | ||||
|                             return this.commentsOffline.removeComment(contextLevel, instanceId, component, itemId, area, siteId); | ||||
|                     })); | ||||
|                 } | ||||
|             }); | ||||
| 
 | ||||
|             if (deleteCommentIds.length > 0) { | ||||
|                 promises.push(this.commentsProvider.deleteCommentsOnline(deleteCommentIds, contextLevel, instanceId, component, | ||||
|                     itemId, area, siteId).then((response) => { | ||||
|                         return this.commentsOffline.removeDeletedComments(contextLevel, instanceId, component, itemId, area, | ||||
|                             siteId); | ||||
|                     })); | ||||
|             } | ||||
| 
 | ||||
|             // Send the comments.
 | ||||
|             return Promise.all(promises).then(() => { | ||||
|                 // Fetch the comments from server to be sure they're up to date.
 | ||||
|                 return this.commentsProvider.invalidateCommentsData(contextLevel, instanceId, component, itemId, area, siteId) | ||||
|                         .then(() => { | ||||
|                     return this.commentsProvider.getComments(contextLevel, instanceId, component, itemId, area, 0, siteId); | ||||
|                 }).catch(() => { | ||||
|                     // Ignore errors.
 | ||||
|                 }); | ||||
|             }).catch((error) => { | ||||
|                 if (this.utils.isWebServiceError(error)) { | ||||
|                     // It's a WebService error, this means the user cannot send comments.
 | ||||
|                     errors.push(error.message); | ||||
|                 } else { | ||||
|                     // Not a WebService error, reject the synchronization to try again.
 | ||||
|                     return Promise.reject(error); | ||||
|                 } | ||||
|             }).then(() => { | ||||
|                 if (errors && errors.length) { | ||||
|                     errors.forEach((error) => { | ||||
|                         warnings.push(this.translate.instant('core.comments.warningcommentsnotsent', { | ||||
|                             error: error | ||||
|                         })); | ||||
|                     }); | ||||
|                 } | ||||
|             }); | ||||
|         }).then(() => { | ||||
|             // All done, return the warnings.
 | ||||
|             return warnings; | ||||
|         }); | ||||
| 
 | ||||
|         return this.addOngoingSync(syncId, syncPromise, siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the ID of a comments sync. | ||||
|      * | ||||
|      * @param  {string} contextLevel Contextlevel system, course, user... | ||||
|      * @param  {number} instanceId   The Instance id of item associated with the context level. | ||||
|      * @param  {string} component    Component name. | ||||
|      * @param  {number} itemId       Associated id. | ||||
|      * @param  {string} [area='']    String comment area. Default empty. | ||||
|      * @return {string} Sync ID. | ||||
|      */ | ||||
|     protected getSyncId(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = ''): string { | ||||
|         return contextLevel + '#' + instanceId + '#' + component + '#' + itemId + '#' + area; | ||||
|     } | ||||
| } | ||||
| @ -25,9 +25,6 @@ | ||||
|     "clicktohideshow": "Click to expand or collapse", | ||||
|     "clicktoseefull": "Click to see full contents.", | ||||
|     "close": "Close", | ||||
|     "comments": "Comments", | ||||
|     "commentscount": "Comments ({{$a}})", | ||||
|     "commentsnotworking": "Comments cannot be retrieved", | ||||
|     "completion-alt-auto-fail": "Completed: {{$a}} (did not achieve pass grade)", | ||||
|     "completion-alt-auto-n": "Not completed: {{$a}}", | ||||
|     "completion-alt-auto-n-override": "Not completed: {{$a.modname}} (set by {{$a.overrideuser}})", | ||||
| @ -168,7 +165,6 @@ | ||||
|     "never": "Never", | ||||
|     "next": "Next", | ||||
|     "no": "No", | ||||
|     "nocomments": "No comments", | ||||
|     "nograde": "No grade", | ||||
|     "none": "None", | ||||
|     "nopasswordchangeforced": "You cannot proceed without changing your password.", | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user