Merge pull request #2703 from crazyserver/MOBILE-3632
MOBILE-3632 notes: Add notes funcionality
This commit is contained in:
		
						commit
						3dc1834a6b
					
				| @ -29,6 +29,7 @@ import { AddonQbehaviourModule } from './qbehaviour/qbehaviour.module'; | ||||
| import { AddonQtypeModule } from './qtype/qtype.module'; | ||||
| import { AddonBlogModule } from './blog/blog.module'; | ||||
| import { AddonRemoteThemesModule } from './remotethemes/remotethemes.module'; | ||||
| import { AddonNotesModule } from './notes/notes.module'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     imports: [ | ||||
| @ -44,6 +45,7 @@ import { AddonRemoteThemesModule } from './remotethemes/remotethemes.module'; | ||||
|         AddonNotificationsModule, | ||||
|         AddonMessageOutputModule, | ||||
|         AddonModModule, | ||||
|         AddonNotesModule, | ||||
|         AddonQbehaviourModule, | ||||
|         AddonQtypeModule, | ||||
|         AddonRemoteThemesModule, | ||||
|  | ||||
| @ -110,7 +110,7 @@ | ||||
|                                 {{discussion.created * 1000 | coreFormatDate: "strftimerecentfull"}} | ||||
|                             </p> | ||||
|                             <p *ngIf="discussions.isOfflineDiscussion(discussion)"> | ||||
|                                 <ion-icon name="time"></ion-icon> | ||||
|                                 <ion-icon name="fas-clock"></ion-icon> | ||||
|                                 {{ 'core.notsent' | translate }} | ||||
|                             </p> | ||||
|                         </div> | ||||
| @ -119,7 +119,7 @@ | ||||
|                         class="ion-text-center addon-mod-forum-discussion-more-info"> | ||||
|                         <ion-col class="ion-text-start"> | ||||
|                             <ion-note> | ||||
|                                 <ion-icon name="time"></ion-icon> {{ 'addon.mod_forum.lastpost' | translate }} | ||||
|                                 <ion-icon name="fas-clock"></ion-icon> {{ 'addon.mod_forum.lastpost' | translate }} | ||||
|                                 <ng-container *ngIf="discussion.timemodified > discussion.created"> | ||||
|                                     {{ discussion.timemodified | coreTimeAgo }} | ||||
|                                 </ng-container> | ||||
| @ -151,7 +151,7 @@ | ||||
| 
 | ||||
|     <ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="forum && canAddDiscussion"> | ||||
|         <ion-fab-button (click)="openNewDiscussion()" [attr.aria-label]="addDiscussionText"> | ||||
|             <ion-icon name="add"></ion-icon> | ||||
|             <ion-icon name="fas-plus"></ion-icon> | ||||
|         </ion-fab-button> | ||||
|     </ion-fab> | ||||
| </core-split-view> | ||||
|  | ||||
| @ -6,7 +6,7 @@ | ||||
|         </ion-label> | ||||
|     </ion-item> | ||||
|     <ion-item class="ion-text-wrap" (click)="deletePost()" *ngIf="offlinePost || (canDelete && isOnline)"> | ||||
|         <ion-icon name="trash" slot="start"></ion-icon> | ||||
|         <ion-icon name="fas-trash" slot="start"></ion-icon> | ||||
|         <ion-label> | ||||
|             <h2 *ngIf="!offlinePost">{{ 'addon.mod_forum.delete' | translate }}</h2> | ||||
|             <h2 *ngIf="offlinePost">{{ 'core.discard' | translate }}</h2> | ||||
|  | ||||
| @ -37,7 +37,7 @@ | ||||
|                             </ng-container> | ||||
|                         </p> | ||||
|                         <p *ngIf="post.timecreated">{{post.timecreated * 1000 | coreFormatDate: "strftimerecentfull"}}</p> | ||||
|                         <p *ngIf="!post.timecreated"><ion-icon name="time"></ion-icon> {{ 'core.notsent' | translate }}</p> | ||||
|                         <p *ngIf="!post.timecreated"><ion-icon name="fas-clock"></ion-icon> {{ 'core.notsent' | translate }}</p> | ||||
|                     </div> | ||||
|                     <ng-container *ngIf="!displaySubject"> | ||||
|                         <ion-note *ngIf="trackPosts && post.unread" | ||||
|  | ||||
							
								
								
									
										34
									
								
								src/addons/notes/components/add/add-modal.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/addons/notes/components/add/add-modal.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,34 @@ | ||||
| <ion-header> | ||||
|     <ion-toolbar> | ||||
|         <ion-title>{{ 'addon.notes.addnewnote' | translate }}</ion-title> | ||||
|         <ion-buttons slot="end"> | ||||
|             <ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate"> | ||||
|                 <ion-icon name="fas-times" slot="icon-only"></ion-icon> | ||||
|             </ion-button> | ||||
|         </ion-buttons> | ||||
|     </ion-toolbar> | ||||
| </ion-header> | ||||
| <ion-content> | ||||
|     <form name="itemEdit" (ngSubmit)="addNote($event)" #itemEdit> | ||||
|         <ion-item> | ||||
|             <ion-label>{{ 'addon.notes.publishstate' | translate }}</ion-label> | ||||
|             <ion-select [(ngModel)]="type" name="publishState" interface="popover"> | ||||
|                 <ion-select-option value="personal">{{ 'addon.notes.personalnotes' | translate }}</ion-select-option> | ||||
|                 <ion-select-option value="course">{{ 'addon.notes.coursenotes' | translate }}</ion-select-option> | ||||
|                 <ion-select-option value="site">{{ 'addon.notes.sitenotes' | translate }}</ion-select-option> | ||||
|             </ion-select> | ||||
|         </ion-item> | ||||
|         <ion-item> | ||||
|             <ion-label> | ||||
|                 <ion-textarea placeholder="{{ 'addon.notes.note' | translate }}" rows="5" [(ngModel)]="text" name="text" | ||||
|                     required="required"> | ||||
|                 </ion-textarea> | ||||
|             </ion-label> | ||||
|         </ion-item> | ||||
|         <div class="ion-padding"> | ||||
|             <ion-button expand="block" type="submit" [disabled]="processing || text.length < 2"> | ||||
|                 {{ 'addon.notes.addnewnote' | translate }} | ||||
|             </ion-button> | ||||
|         </div> | ||||
|     </form> | ||||
| </ion-content> | ||||
							
								
								
									
										78
									
								
								src/addons/notes/components/add/add-modal.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								src/addons/notes/components/add/add-modal.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,78 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { AddonNotes } from '@addons/notes/services/notes'; | ||||
| import { Component, ViewChild, ElementRef, Input } from '@angular/core'; | ||||
| import { CoreApp } from '@services/app'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { ModalController } from '@singletons'; | ||||
| 
 | ||||
| /** | ||||
|  * Component that displays a text area for composing a note. | ||||
|  */ | ||||
| @Component({ | ||||
|     templateUrl: 'add-modal.html', | ||||
| }) | ||||
| export class AddonNotesAddComponent { | ||||
| 
 | ||||
|     @ViewChild('itemEdit') formElement?: ElementRef; | ||||
| 
 | ||||
|     @Input() protected courseId!: number; | ||||
|     @Input() protected userId?: number; | ||||
|     @Input() type = 'personal'; | ||||
|     text = ''; | ||||
|     processing = false; | ||||
| 
 | ||||
|     /** | ||||
|      * Send the note or store it offline. | ||||
|      * | ||||
|      * @param e Event. | ||||
|      */ | ||||
|     async addNote(e: Event): Promise<void> { | ||||
|         e.preventDefault(); | ||||
|         e.stopPropagation(); | ||||
| 
 | ||||
|         CoreApp.closeKeyboard(); | ||||
|         const loadingModal = await CoreDomUtils.showModalLoading('core.sending', true); | ||||
| 
 | ||||
|         // Freeze the add note button.
 | ||||
|         this.processing = true; | ||||
|         try { | ||||
|             this.userId = this.userId || CoreSites.getCurrentSiteUserId(); | ||||
|             const sent = await AddonNotes.addNote(this.userId, this.courseId, this.type, this.text); | ||||
| 
 | ||||
|             CoreDomUtils.triggerFormSubmittedEvent(this.formElement, sent, CoreSites.getCurrentSiteId()); | ||||
| 
 | ||||
|             ModalController.dismiss({ type: this.type, sent: true }).finally(() => { | ||||
|                 CoreDomUtils.showToast(sent ? 'addon.notes.eventnotecreated' : 'core.datastoredoffline', true, 3000); | ||||
|             }); | ||||
|         } catch (error){ | ||||
|             CoreDomUtils.showErrorModal(error); | ||||
|             this.processing = false; | ||||
|         } finally { | ||||
|             loadingModal.dismiss(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Close modal. | ||||
|      */ | ||||
|     closeModal(): void { | ||||
|         CoreDomUtils.triggerFormCancelledEvent(this.formElement, CoreSites.getCurrentSiteId()); | ||||
| 
 | ||||
|         ModalController.dismiss({ type: this.type }); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										30
									
								
								src/addons/notes/components/components.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/addons/notes/components/components.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,30 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { CoreSharedModule } from '@/core/shared.module'; | ||||
| import { NgModule } from '@angular/core'; | ||||
| import { AddonNotesAddComponent } from './add/add-modal'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
|         AddonNotesAddComponent, | ||||
|     ], | ||||
|     imports: [ | ||||
|         CoreSharedModule, | ||||
|     ], | ||||
|     exports: [ | ||||
|         AddonNotesAddComponent, | ||||
|     ], | ||||
| }) | ||||
| export class AddonNotesComponentsModule {} | ||||
							
								
								
									
										15
									
								
								src/addons/notes/lang.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/addons/notes/lang.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,15 @@ | ||||
| { | ||||
|     "addnewnote": "Add a new note", | ||||
|     "coursenotes": "Course notes", | ||||
|     "deleteconfirm": "Delete this note?", | ||||
|     "eventnotecreated": "Note created", | ||||
|     "eventnotedeleted": "Note deleted", | ||||
|     "nonotes": "There are no notes of this type yet", | ||||
|     "note": "Note", | ||||
|     "notes": "Notes", | ||||
|     "personalnotes": "Personal notes", | ||||
|     "publishstate": "Context", | ||||
|     "sitenotes": "Site notes", | ||||
|     "userwithid": "User with ID {{id}}", | ||||
|     "warningnotenotsent": "Couldn't add note(s) to course {{course}}. {{error}}" | ||||
| } | ||||
							
								
								
									
										41
									
								
								src/addons/notes/notes-lazy.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/addons/notes/notes-lazy.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,41 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { CoreSharedModule } from '@/core/shared.module'; | ||||
| import { NgModule } from '@angular/core'; | ||||
| import { Routes, RouterModule } from '@angular/router'; | ||||
| import { CoreCommentsComponentsModule } from '@features/comments/components/components.module'; | ||||
| import { CoreTagComponentsModule } from '@features/tag/components/components.module'; | ||||
| import { AddonNotesListPage } from './pages/list/list.page'; | ||||
| 
 | ||||
| const routes: Routes = [ | ||||
|     { | ||||
|         path: '', | ||||
|         component: AddonNotesListPage, | ||||
|     }, | ||||
| ]; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     imports: [ | ||||
|         RouterModule.forChild(routes), | ||||
|         CoreSharedModule, | ||||
|         CoreCommentsComponentsModule, | ||||
|         CoreTagComponentsModule, | ||||
|     ], | ||||
|     exports: [RouterModule], | ||||
|     declarations: [ | ||||
|         AddonNotesListPage, | ||||
|     ], | ||||
| }) | ||||
| export class AddonNotesLazyModule {} | ||||
							
								
								
									
										70
									
								
								src/addons/notes/notes.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								src/addons/notes/notes.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,70 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { APP_INITIALIZER, NgModule, Type } from '@angular/core'; | ||||
| import { AddonNotesProvider } from './services/notes'; | ||||
| import { AddonNotesOfflineProvider } from './services/notes-offline'; | ||||
| import { AddonNotesSyncProvider } from './services/notes-sync'; | ||||
| import { CoreCronDelegate } from '@services/cron'; | ||||
| import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate'; | ||||
| import { CoreUserDelegate } from '@features/user/services/user-delegate'; | ||||
| import { AddonNotesCourseOptionHandler } from './services/handlers/course-option'; | ||||
| import { AddonNotesSyncCronHandler } from './services/handlers/sync-cron'; | ||||
| import { AddonNotesUserHandler } from './services/handlers/user'; | ||||
| import { CORE_SITE_SCHEMAS } from '@services/sites'; | ||||
| import { NOTES_OFFLINE_SITE_SCHEMA } from './services/database/notes'; | ||||
| import { AddonNotesComponentsModule } from './components/components.module'; | ||||
| import { Routes } from '@angular/router'; | ||||
| import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; | ||||
| import { CoreCourseIndexRoutingModule } from '@features/course/pages/index/index-routing.module'; | ||||
| 
 | ||||
| // List of providers (without handlers).
 | ||||
| export const ADDON_NOTES_SERVICES: Type<unknown>[] = [ | ||||
|     AddonNotesProvider, | ||||
|     AddonNotesOfflineProvider, | ||||
|     AddonNotesSyncProvider, | ||||
| ]; | ||||
| 
 | ||||
| const routes: Routes = [ | ||||
|     { | ||||
|         path: 'notes', | ||||
|         loadChildren: () => import('@addons/notes/notes-lazy.module').then(m => m.AddonNotesLazyModule), | ||||
|     }, | ||||
| ]; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     imports: [ | ||||
|         CoreMainMenuTabRoutingModule.forChild(routes), | ||||
|         CoreCourseIndexRoutingModule.forChild({ children: routes }), | ||||
|         AddonNotesComponentsModule, | ||||
|     ], | ||||
|     providers: [ | ||||
|         { | ||||
|             provide: CORE_SITE_SCHEMAS, | ||||
|             useValue: [NOTES_OFFLINE_SITE_SCHEMA], | ||||
|             multi: true, | ||||
|         }, | ||||
|         { | ||||
|             provide: APP_INITIALIZER, | ||||
|             multi: true, | ||||
|             deps: [], | ||||
|             useFactory: () => async () => { | ||||
|                 CoreUserDelegate.registerHandler(AddonNotesUserHandler.instance); | ||||
|                 CoreCourseOptionsDelegate.registerHandler(AddonNotesCourseOptionHandler.instance); | ||||
|                 CoreCronDelegate.register(AddonNotesSyncCronHandler.instance); | ||||
|             }, | ||||
|         }, | ||||
|     ], | ||||
| }) | ||||
| export class AddonNotesModule {} | ||||
							
								
								
									
										100
									
								
								src/addons/notes/pages/list/list.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								src/addons/notes/pages/list/list.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,100 @@ | ||||
| <ion-header> | ||||
|     <ion-toolbar> | ||||
|         <ion-buttons slot="start"> | ||||
|             <ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button> | ||||
|         </ion-buttons> | ||||
| 
 | ||||
|         <ion-title>{{ 'addon.notes.notes' | translate }}</ion-title> | ||||
|         <ion-buttons slot="end"> | ||||
|         </ion-buttons> | ||||
|     </ion-toolbar> | ||||
| </ion-header> | ||||
| <core-navbar-buttons slot="end"> | ||||
|     <ion-button [hidden]="!canDeleteNotes" slot="end" fill="clear" (click)="toggleDelete()" | ||||
|         [attr.aria-label]="'core.delete' | translate"> | ||||
|         <ion-icon name="fas-pen" slot="icon-only"></ion-icon> | ||||
|     </ion-button> | ||||
|     <core-context-menu> | ||||
|         <core-context-menu-item [hidden]="!(notesLoaded && !hasOffline)" [priority]="100" | ||||
|             [content]="'core.refresh' | translate" (action)="refreshNotes(false)" | ||||
|             [iconAction]="refreshIcon" [closeOnClick]="true"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item [hidden]="!(notesLoaded && hasOffline)" [priority]="100" | ||||
|             [content]="'core.settings.synchronizenow' | translate" (action)="refreshNotes(true)" | ||||
|             [iconAction]="syncIcon" [closeOnClick]="false"></core-context-menu-item> | ||||
|     </core-context-menu> | ||||
| </core-navbar-buttons> | ||||
| <ion-content> | ||||
|     <ion-refresher slot="fixed" [disabled]="!notesLoaded" (ionRefresh)="refreshNotes(false, $event.target)"> | ||||
|         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> | ||||
|     </ion-refresher> | ||||
| 
 | ||||
|     <core-loading [hideUntil]="notesLoaded" class="core-loading-center"> | ||||
|         <ion-item class="ion-text-wrap" *ngIf="user"> | ||||
|             <core-user-avatar [user]="user" [courseId]="courseId" slot="start" [linkProfile]="false"></core-user-avatar> | ||||
|             <ion-label><h2>{{user!.fullname}}</h2></ion-label> | ||||
|         </ion-item> | ||||
| 
 | ||||
|         <div class="ion-padding"> | ||||
|             <ion-select [(ngModel)]="type" (ngModelChange)="typeChanged()" interface="popover" class="core-button-select"> | ||||
|                 <ion-select-option value="site">{{ 'addon.notes.sitenotes' | translate }}</ion-select-option> | ||||
|                 <ion-select-option value="course">{{ 'addon.notes.coursenotes' | translate }}</ion-select-option> | ||||
|                 <ion-select-option value="personal">{{ 'addon.notes.personalnotes' | translate }}</ion-select-option> | ||||
|             </ion-select> | ||||
|         </div> | ||||
| 
 | ||||
|         <ion-card class="core-warning-card" *ngIf="hasOffline"> | ||||
|             <ion-item> | ||||
|                 <ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon> | ||||
|                 <ion-label> | ||||
|                     {{ 'core.thereisdatatosync' | translate:{$a: 'addon.notes.notes' | translate | lowercase } }} | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|         </ion-card> | ||||
| 
 | ||||
|         <core-empty-box *ngIf="notes && notes.length == 0" icon="fas-receipt" [message]="'addon.notes.nonotes' | translate"> | ||||
|         </core-empty-box> | ||||
| 
 | ||||
|         <ng-container *ngIf="notes && notes.length > 0"> | ||||
|             <ion-card *ngFor="let note of notes"> | ||||
|                 <ion-item class="ion-text-wrap"> | ||||
|                     <core-user-avatar [user]="note" [courseId]="courseId" slot="start" *ngIf="!userId"></core-user-avatar> | ||||
|                     <ion-label> | ||||
|                         <h2 *ngIf="!userId">{{note.userfullname}}</h2> | ||||
|                         <p *ngIf="!note.deleted && !note.offline" slot="end"> | ||||
|                             <span class="ion-text-wrap">{{note.lastmodified | coreDateDayOrTime}}</span> | ||||
|                         </p> | ||||
|                     </ion-label> | ||||
|                     <p *ngIf="note.offline" slot="end"> | ||||
|                         <ion-icon name="far-clock"></ion-icon> <span class="ion-text-wrap"> | ||||
|                             {{ 'core.notsent' | translate }} | ||||
|                         </span> | ||||
|                     </p> | ||||
|                     <p *ngIf="note.deleted" slot="end"> | ||||
|                         <ion-icon name="fas-trash"></ion-icon> <span class="ion-text-wrap"> | ||||
|                             {{ 'core.deletedoffline' | translate }} | ||||
|                         </span> | ||||
|                     </p> | ||||
|                     <ion-button *ngIf="note.deleted" slot="end" fill="clear" color="danger" (click)="undoDeleteNote($event, note)" | ||||
|                         [attr.aria-label]="'core.restore' | translate"> | ||||
|                         <ion-icon name="fas-undo-alt" slot="icon-only"></ion-icon> | ||||
|                     </ion-button> | ||||
|                     <ion-button *ngIf="showDelete && !note.deleted && (type != 'personal' || note.usermodified == currentUserId)" | ||||
|                         slot="end" fill="clear" [@coreSlideInOut]="'fromRight'" color="danger" (click)="deleteNote($event, note)" | ||||
|                         [attr.aria-label]="'core.delete' | translate"> | ||||
|                         <ion-icon name="fas-trash" slot="icon-only"></ion-icon> | ||||
|                     </ion-button> | ||||
|                 </ion-item> | ||||
|                 <ion-item class="ion-text-wrap"> | ||||
|                     <ion-label><core-format-text [text]="note.content" [filter]="false"></core-format-text></ion-label> | ||||
|                 </ion-item> | ||||
|             </ion-card> | ||||
|         </ng-container> | ||||
|     </core-loading> | ||||
| 
 | ||||
|     <ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="userId && notesLoaded"> | ||||
|         <ion-fab-button (click)="addNote($event)" [attr.aria-label]="'addon.notes.addnewnote' |translate"> | ||||
|             <ion-icon name="fas-plus"></ion-icon> | ||||
|         </ion-fab-button> | ||||
|     </ion-fab> | ||||
| </ion-content> | ||||
							
								
								
									
										295
									
								
								src/addons/notes/pages/list/list.page.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										295
									
								
								src/addons/notes/pages/list/list.page.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,295 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { CoreConstants } from '@/core/constants'; | ||||
| import { AddonNotesAddComponent } from '@addons/notes/components/add/add-modal'; | ||||
| import { AddonNotes, AddonNotesNoteFormatted } from '@addons/notes/services/notes'; | ||||
| import { AddonNotesOffline } from '@addons/notes/services/notes-offline'; | ||||
| import { AddonNotesSync, AddonNotesSyncProvider } from '@addons/notes/services/notes-sync'; | ||||
| import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; | ||||
| import { CoreAnimations } from '@components/animations'; | ||||
| import { CoreUser, CoreUserProfile } from '@features/user/services/user'; | ||||
| import { IonContent, IonRefresher } from '@ionic/angular'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { ModalController } from '@singletons'; | ||||
| import { CoreEventObserver, CoreEvents } from '@singletons/events'; | ||||
| 
 | ||||
| /** | ||||
|  * Page that displays a list of notes. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'page-addon-notes-list-page', | ||||
|     templateUrl: 'list.html', | ||||
|     animations: [CoreAnimations.SLIDE_IN_OUT], | ||||
| }) | ||||
| export class AddonNotesListPage implements OnInit, OnDestroy { | ||||
| 
 | ||||
|      @ViewChild(IonContent) content?: IonContent; | ||||
| 
 | ||||
|     courseId: number; | ||||
|     userId?: number; | ||||
|     type = 'course'; | ||||
|     refreshIcon = CoreConstants.ICON_LOADING; | ||||
|     syncIcon = CoreConstants.ICON_LOADING; | ||||
|     notes: AddonNotesNoteFormatted[] = []; | ||||
|     hasOffline = false; | ||||
|     notesLoaded = false; | ||||
|     user?: CoreUserProfile; | ||||
|     showDelete = false; | ||||
|     canDeleteNotes = false; | ||||
|     currentUserId: number; | ||||
| 
 | ||||
|     protected syncObserver: CoreEventObserver; | ||||
| 
 | ||||
|     constructor() { | ||||
|         this.courseId = CoreNavigator.getRouteNumberParam('courseId')!; | ||||
|         this.userId = CoreNavigator.getRouteNumberParam('userId'); | ||||
| 
 | ||||
|         // Refresh data if notes are synchronized automatically.
 | ||||
|         this.syncObserver = CoreEvents.on(AddonNotesSyncProvider.AUTO_SYNCED, (data) => { | ||||
|             if (data.courseId == this.courseId) { | ||||
|                 // Show the sync warnings.
 | ||||
|                 this.showSyncWarnings(data.warnings); | ||||
| 
 | ||||
|                 // Refresh the data.
 | ||||
|                 this.notesLoaded = false; | ||||
|                 this.refreshIcon = CoreConstants.ICON_LOADING; | ||||
|                 this.syncIcon = CoreConstants.ICON_LOADING; | ||||
| 
 | ||||
|                 this.content?.scrollToTop(); | ||||
|                 this.fetchNotes(false); | ||||
|             } | ||||
|         }, CoreSites.getCurrentSiteId()); | ||||
| 
 | ||||
|         this.currentUserId = CoreSites.getCurrentSiteUserId(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being initialized. | ||||
|      */ | ||||
|     async ngOnInit(): Promise<void> { | ||||
|         await this.fetchNotes(true); | ||||
| 
 | ||||
|         CoreUtils.ignoreErrors(AddonNotes.logView(this.courseId, this.userId)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch notes. | ||||
|      * | ||||
|      * @param sync When to resync notes. | ||||
|      * @param showErrors When to display errors or not. | ||||
|      * @return Promise with the notes. | ||||
|      */ | ||||
|     protected async fetchNotes(sync = false, showErrors = false): Promise<void> { | ||||
|         if (sync) { | ||||
|             await this.syncNotes(showErrors); | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             const allNotes = await AddonNotes.getNotes(this.courseId, this.userId); | ||||
| 
 | ||||
|             const notesList: AddonNotesNoteFormatted[] = allNotes[this.type + 'notes'] || []; | ||||
| 
 | ||||
|             notesList.forEach((note) => { | ||||
|                 note.content = CoreTextUtils.decodeHTML(note.content); | ||||
|             }); | ||||
| 
 | ||||
|             await AddonNotes.setOfflineDeletedNotes(notesList, this.courseId); | ||||
| 
 | ||||
|             this.hasOffline = notesList.some((note) => note.offline || note.deleted); | ||||
| 
 | ||||
|             if (this.userId) { | ||||
|                 this.notes = notesList; | ||||
| 
 | ||||
|                 // Get the user profile to retrieve the user image.
 | ||||
|                 this.user = await CoreUser.getProfile(this.userId, this.courseId, true); | ||||
|             } else { | ||||
|                 this.notes = await AddonNotes.getNotesUserData(notesList); | ||||
|             } | ||||
|         } catch (error) { | ||||
|             CoreDomUtils.showErrorModal(error); | ||||
|         } finally { | ||||
|             let canDelete = this.notes && this.notes.length > 0; | ||||
|             if (canDelete && this.type == 'personal') { | ||||
|                 canDelete = !!this.notes.find((note) =>  note.usermodified == this.currentUserId); | ||||
|             } | ||||
|             this.canDeleteNotes = canDelete; | ||||
| 
 | ||||
|             this.notesLoaded = true; | ||||
|             this.refreshIcon = CoreConstants.ICON_REFRESH; | ||||
|             this.syncIcon = CoreConstants.ICON_SYNC; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Refresh notes on PTR. | ||||
|      * | ||||
|      * @param showErrors Whether to display errors or not. | ||||
|      * @param refresher Refresher instance. | ||||
|      */ | ||||
|     refreshNotes(showErrors: boolean, refresher?: IonRefresher): void { | ||||
|         this.refreshIcon = CoreConstants.ICON_LOADING; | ||||
|         this.syncIcon = CoreConstants.ICON_LOADING; | ||||
| 
 | ||||
|         AddonNotes.invalidateNotes(this.courseId, this.userId).finally(() => { | ||||
|             this.fetchNotes(true, showErrors).finally(() => { | ||||
|                 if (refresher) { | ||||
|                     refresher?.complete(); | ||||
|                 } | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Function called when the type has changed. | ||||
|      */ | ||||
|     async typeChanged(): Promise<void> { | ||||
|         this.notesLoaded = false; | ||||
|         this.refreshIcon = CoreConstants.ICON_LOADING; | ||||
|         this.syncIcon = CoreConstants.ICON_LOADING; | ||||
| 
 | ||||
|         await this.fetchNotes(true); | ||||
|         CoreUtils.ignoreErrors(AddonNotes.logView(this.courseId, this.userId)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Add a new Note to user and course. | ||||
|      * | ||||
|      * @param e Event. | ||||
|      */ | ||||
|     async addNote(e: Event): Promise<void> { | ||||
|         e.preventDefault(); | ||||
|         e.stopPropagation(); | ||||
| 
 | ||||
|         const modal = await ModalController.create({ | ||||
|             component: AddonNotesAddComponent, | ||||
|             componentProps: { | ||||
|                 userId: this.userId, | ||||
|                 courseId: this.courseId, | ||||
|                 type: this.type, | ||||
|             }, | ||||
|         }); | ||||
| 
 | ||||
|         await modal.present(); | ||||
| 
 | ||||
|         const result = await modal.onDidDismiss(); | ||||
| 
 | ||||
|         if (typeof result.data != 'undefined') { | ||||
| 
 | ||||
|             if (result.data.sent && result.data.type) { | ||||
|                 if (result.data.type != this.type) { | ||||
|                     this.type = result.data.type; | ||||
|                     this.notesLoaded = false; | ||||
|                 } | ||||
| 
 | ||||
|                 this.refreshNotes(false); | ||||
|             } else if (result.data.type && result.data.type != this.type) { | ||||
|                 this.type = result.data.type; | ||||
|                 this.typeChanged(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete a note. | ||||
|      * | ||||
|      * @param e Click event. | ||||
|      * @param note Note to delete. | ||||
|      */ | ||||
|     async deleteNote(e: Event, note: AddonNotesNoteFormatted): Promise<void> { | ||||
|         e.preventDefault(); | ||||
|         e.stopPropagation(); | ||||
| 
 | ||||
|         try { | ||||
|             await CoreDomUtils.showDeleteConfirm('addon.notes.deleteconfirm'); | ||||
|             try { | ||||
|                 await AddonNotes.deleteNote(note, this.courseId); | ||||
|                 this.showDelete = false; | ||||
| 
 | ||||
|                 this.refreshNotes(false); | ||||
| 
 | ||||
|                 CoreDomUtils.showToast('addon.notes.eventnotedeleted', true, 3000); | ||||
| 
 | ||||
|             } catch (error) { | ||||
|                 CoreDomUtils.showErrorModalDefault(error, 'Delete note failed.'); | ||||
|             } | ||||
|         } catch { | ||||
|             // User cancelled, nothing to do.
 | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Restore a note. | ||||
|      * | ||||
|      * @param e Click event. | ||||
|      * @param note Note to delete. | ||||
|      */ | ||||
|     async undoDeleteNote(e: Event, note: AddonNotesNoteFormatted): Promise<void> { | ||||
|         e.preventDefault(); | ||||
|         e.stopPropagation(); | ||||
| 
 | ||||
|         await AddonNotesOffline.undoDeleteNote(note.id); | ||||
|         this.refreshNotes(true); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Toggle delete. | ||||
|      */ | ||||
|     toggleDelete(): void { | ||||
|         this.showDelete = !this.showDelete; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Tries to synchronize course notes. | ||||
|      * | ||||
|      * @param showErrors Whether to display errors or not. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async syncNotes(showErrors: boolean): Promise<void> { | ||||
|         try { | ||||
|             const result = await AddonNotesSync.syncNotes(this.courseId); | ||||
| 
 | ||||
|             this.showSyncWarnings(result.warnings); | ||||
|         } catch (error) { | ||||
|             if (showErrors) { | ||||
|                 CoreDomUtils.showErrorModalDefault(error, 'core.errorsync', true); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Show sync warnings if any. | ||||
|      * | ||||
|      * @param warnings the warnings | ||||
|      */ | ||||
|     protected showSyncWarnings(warnings: string[]): void { | ||||
|         const message = CoreTextUtils.buildMessage(warnings); | ||||
| 
 | ||||
|         if (message) { | ||||
|             CoreDomUtils.showErrorModal(message); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Page destroyed. | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         this.syncObserver && this.syncObserver.off(); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										95
									
								
								src/addons/notes/services/database/notes.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								src/addons/notes/services/database/notes.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,95 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { CoreSiteSchema } from '@services/sites'; | ||||
| 
 | ||||
| /** | ||||
|  * Database variables for AddonNotesOfflineProvider. | ||||
|  */ | ||||
| export const NOTES_TABLE = 'addon_notes_offline_notes'; | ||||
| export const NOTES_DELETED_TABLE = 'addon_notes_deleted_offline_notes'; | ||||
| export const NOTES_OFFLINE_SITE_SCHEMA: CoreSiteSchema = { | ||||
|     name: 'AddonNotesOfflineProvider', | ||||
|     version: 2, | ||||
|     tables: [ | ||||
|         { | ||||
|             name: NOTES_TABLE, | ||||
|             columns: [ | ||||
|                 { | ||||
|                     name: 'userid', | ||||
|                     type: 'INTEGER', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'courseid', | ||||
|                     type: 'INTEGER', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'publishstate', | ||||
|                     type: 'TEXT', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'content', | ||||
|                     type: 'TEXT', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'format', | ||||
|                     type: 'INTEGER', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'created', | ||||
|                     type: 'INTEGER', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'lastmodified', | ||||
|                     type: 'INTEGER', | ||||
|                 }, | ||||
|             ], | ||||
|             primaryKeys: ['userid', 'content', 'created'], | ||||
|         }, | ||||
|         { | ||||
|             name: NOTES_DELETED_TABLE, | ||||
|             columns: [ | ||||
|                 { | ||||
|                     name: 'noteid', | ||||
|                     type: 'INTEGER', | ||||
|                     primaryKey: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'deleted', | ||||
|                     type: 'INTEGER', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'courseid', | ||||
|                     type: 'INTEGER', | ||||
|                 }, | ||||
|             ], | ||||
|         }, | ||||
|     ], | ||||
| }; | ||||
| 
 | ||||
| export type AddonNotesDBRecord = { | ||||
|     userid: number; // Primary key.
 | ||||
|     content: string; // Primary key.
 | ||||
|     created: number; // Primary key.
 | ||||
|     courseid: number; | ||||
|     publishstate: string; | ||||
|     format: number; | ||||
|     lastmodified: number; | ||||
| }; | ||||
| 
 | ||||
| export type AddonNotesDeletedDBRecord = { | ||||
|     noteid: number; // Primary key.
 | ||||
|     deleted: number; | ||||
|     courseid: number; | ||||
| }; | ||||
							
								
								
									
										81
									
								
								src/addons/notes/services/handlers/course-option.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								src/addons/notes/services/handlers/course-option.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,81 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { CoreCourseProvider } from '@features/course/services/course'; | ||||
| import { | ||||
|     CoreCourseAccess, | ||||
|     CoreCourseOptionsHandler, | ||||
|     CoreCourseOptionsHandlerData, | ||||
| } from '@features/course/services/course-options-delegate'; | ||||
| import { CoreCourseUserAdminOrNavOptionIndexed } from '@features/courses/services/courses'; | ||||
| import { CoreEnrolledCourseDataWithExtraInfoAndOptions } from '@features/courses/services/courses-helper'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| import { AddonNotes } from '../notes'; | ||||
| 
 | ||||
| /** | ||||
|  * Handler to inject an option into the course main menu. | ||||
|  */ | ||||
| @Injectable( { providedIn: 'root' } ) | ||||
| export class AddonNotesCourseOptionHandlerService implements CoreCourseOptionsHandler { | ||||
| 
 | ||||
|     name = 'AddonNotes'; | ||||
|     priority = 200; | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async isEnabled(): Promise<boolean> { | ||||
|         return AddonNotes.isPluginEnabled(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async isEnabledForCourse( | ||||
|         courseId: number, | ||||
|         accessData: CoreCourseAccess, | ||||
|         navOptions?: CoreCourseUserAdminOrNavOptionIndexed, | ||||
|     ): Promise<boolean> { | ||||
|         if (accessData && accessData.type == CoreCourseProvider.ACCESS_GUEST) { | ||||
|             return false; // Not enabled for guests.
 | ||||
|         } | ||||
| 
 | ||||
|         if (navOptions && typeof navOptions.notes != 'undefined') { | ||||
|             return navOptions.notes; | ||||
|         } | ||||
| 
 | ||||
|         return AddonNotes.isPluginViewNotesEnabledForCourse(courseId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     getDisplayData(): CoreCourseOptionsHandlerData { | ||||
|         return { | ||||
|             title: 'addon.notes.notes', | ||||
|             class: 'addon-notes-course-handler', | ||||
|             page: 'notes', | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async prefetch(course: CoreEnrolledCourseDataWithExtraInfoAndOptions): Promise<void> { | ||||
|         await AddonNotes.getNotes(course.id, undefined, true); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| export const AddonNotesCourseOptionHandler = makeSingleton(AddonNotesCourseOptionHandlerService); | ||||
							
								
								
									
										43
									
								
								src/addons/notes/services/handlers/sync-cron.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src/addons/notes/services/handlers/sync-cron.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,43 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { CoreCronHandler } from '@services/cron'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| import { AddonNotesSync } from '../notes-sync'; | ||||
| 
 | ||||
| /** | ||||
|  * Synchronization cron handler. | ||||
|  */ | ||||
| @Injectable( { providedIn: 'root' } ) | ||||
| export class AddonNotesSyncCronHandlerService implements CoreCronHandler { | ||||
| 
 | ||||
|     name = 'AddonNotesSyncCronHandler'; | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     execute(siteId?: string, force?: boolean): Promise<void> { | ||||
|         return AddonNotesSync.syncAllNotes(siteId, force); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     getInterval(): number { | ||||
|         return 300000; // 5 minutes.
 | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| export const AddonNotesSyncCronHandler = makeSingleton(AddonNotesSyncCronHandlerService); | ||||
							
								
								
									
										73
									
								
								src/addons/notes/services/handlers/user.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								src/addons/notes/services/handlers/user.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,73 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { CoreUserProfile } from '@features/user/services/user'; | ||||
| import { CoreUserProfileHandler, CoreUserDelegateService, CoreUserProfileHandlerData } from '@features/user/services/user-delegate'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| import { AddonNotes } from '../notes'; | ||||
| 
 | ||||
| /** | ||||
|  * Profile notes handler. | ||||
|  */ | ||||
| @Injectable( { providedIn: 'root' } ) | ||||
| export class AddonNotesUserHandlerService implements CoreUserProfileHandler { | ||||
| 
 | ||||
|     name = 'AddonNotes:notes'; | ||||
|     priority = 100; | ||||
|     type = CoreUserDelegateService.TYPE_NEW_PAGE; | ||||
|     cacheEnabled = true; | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async isEnabled(): Promise<boolean> { | ||||
|         return AddonNotes.isPluginEnabled(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async isEnabledForUser(user: CoreUserProfile, courseId?: number): Promise<boolean> { | ||||
|         // Active course required.
 | ||||
|         if (!courseId || user.id == CoreSites.getCurrentSiteUserId()) { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         // We are not using isEnabledForCourse because we need to cache the call.
 | ||||
|         return AddonNotes.isPluginViewNotesEnabledForCourse(courseId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     getDisplayData(): CoreUserProfileHandlerData { | ||||
|         return { | ||||
|             icon: 'fas-receipt', | ||||
|             title: 'addon.notes.notes', | ||||
|             class: 'addon-notes-handler', | ||||
|             action: (event, user, courseId): void => { | ||||
|                 event.preventDefault(); | ||||
|                 event.stopPropagation(); | ||||
|                 CoreNavigator.navigateToSitePath('/notes', { | ||||
|                     params: { courseId, userId: user.id }, | ||||
|                 }); | ||||
|             }, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| export const AddonNotesUserHandler = makeSingleton(AddonNotesUserHandlerService); | ||||
							
								
								
									
										259
									
								
								src/addons/notes/services/notes-offline.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										259
									
								
								src/addons/notes/services/notes-offline.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,259 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreTimeUtils } from '@services/utils/time'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| import { AddonNotesDBRecord, AddonNotesDeletedDBRecord, NOTES_DELETED_TABLE, NOTES_TABLE } from './database/notes'; | ||||
| 
 | ||||
| /** | ||||
|  * Service to handle offline notes. | ||||
|  */ | ||||
| @Injectable( { providedIn: 'root' } ) | ||||
| export class AddonNotesOfflineProvider { | ||||
| 
 | ||||
|     /** | ||||
|      * Delete an offline note. | ||||
|      * | ||||
|      * @param userId User ID the note is about. | ||||
|      * @param content The note content. | ||||
|      * @param timecreated The time the note was created. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved if deleted, rejected if failure. | ||||
|      */ | ||||
|     async deleteOfflineNote(userId: number, content: string, timecreated: number, siteId?: string): Promise<void> { | ||||
|         const site = await CoreSites.getSite(siteId); | ||||
| 
 | ||||
|         await site.getDb().deleteRecords(NOTES_TABLE, { | ||||
|             userid: userId, | ||||
|             content: content, | ||||
|             created: timecreated, | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get all offline deleted notes. | ||||
|      * | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with notes. | ||||
|      */ | ||||
|     async getAllDeletedNotes(siteId?: string): Promise<AddonNotesDeletedDBRecord[]> { | ||||
|         const site = await CoreSites.getSite(siteId); | ||||
| 
 | ||||
|         return site.getDb().getRecords(NOTES_DELETED_TABLE); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get course offline deleted notes. | ||||
|      * | ||||
|      * @param courseId Course ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with notes. | ||||
|      */ | ||||
|     async getCourseDeletedNotes(courseId: number, siteId?: string): Promise<AddonNotesDeletedDBRecord[]> { | ||||
|         const site = await CoreSites.getSite(siteId); | ||||
| 
 | ||||
|         return site.getDb().getRecords(NOTES_DELETED_TABLE, { courseid: courseId }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get all offline notes. | ||||
|      * | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with notes. | ||||
|      */ | ||||
|     async getAllNotes(siteId?: string): Promise<AddonNotesDBRecord[]> { | ||||
|         const site = await CoreSites.getSite(siteId); | ||||
| 
 | ||||
|         return site.getDb().getRecords(NOTES_TABLE); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get an offline note. | ||||
|      * | ||||
|      * @param userId User ID the note is about. | ||||
|      * @param content The note content. | ||||
|      * @param timecreated The time the note was created. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the notes. | ||||
|      */ | ||||
|     async getNote(userId: number, content: string, timecreated: number, siteId?: string): Promise<AddonNotesDBRecord> { | ||||
|         const site = await CoreSites.getSite(siteId); | ||||
| 
 | ||||
|         return site.getDb().getRecord(NOTES_TABLE, { | ||||
|             userid: userId, | ||||
|             content: content, | ||||
|             created: timecreated, | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get offline notes for a certain course and user. | ||||
|      * | ||||
|      * @param courseId Course ID. | ||||
|      * @param userId User ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with notes. | ||||
|      */ | ||||
|     async getNotesForCourseAndUser(courseId: number, userId?: number, siteId?: string): Promise<AddonNotesDBRecord[]> { | ||||
|         if (!userId) { | ||||
|             return this.getNotesForCourse(courseId, siteId); | ||||
|         } | ||||
| 
 | ||||
|         const site = await CoreSites.getSite(siteId); | ||||
| 
 | ||||
|         return await site.getDb().getRecords(NOTES_TABLE, { userid: userId, courseid: courseId }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get offline notes for a certain course. | ||||
|      * | ||||
|      * @param courseId Course ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with notes. | ||||
|      */ | ||||
|     async getNotesForCourse(courseId: number, siteId?: string): Promise<AddonNotesDBRecord[]> { | ||||
|         const site = await CoreSites.getSite(siteId); | ||||
| 
 | ||||
|         return site.getDb().getRecords(NOTES_TABLE, { courseid: courseId }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get offline notes for a certain user. | ||||
|      * | ||||
|      * @param userId User ID the notes are about. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with notes. | ||||
|      */ | ||||
|     async getNotesForUser(userId: number, siteId?: string): Promise<AddonNotesDBRecord[]> { | ||||
|         const site = await CoreSites.getSite(siteId); | ||||
| 
 | ||||
|         return await site.getDb().getRecords(NOTES_TABLE, { userid: userId }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get offline notes with a certain publish state (Personal, Site or Course). | ||||
|      * | ||||
|      * @param state Publish state ('personal', 'site' or 'course'). | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with notes. | ||||
|      */ | ||||
|     async getNotesWithPublishState(state: string, siteId?: string): Promise<AddonNotesDBRecord[]> { | ||||
|         const site = await CoreSites.getSite(siteId); | ||||
| 
 | ||||
|         return await site.getDb().getRecords(NOTES_TABLE, { publishstate: state }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if there are offline notes for a certain course. | ||||
|      * | ||||
|      * @param courseId Course ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with boolean: true if has offline notes, false otherwise. | ||||
|      */ | ||||
|     async hasNotesForCourse(courseId: number, siteId?: string): Promise<boolean> { | ||||
|         const notes = await this.getNotesForCourse(courseId, siteId); | ||||
| 
 | ||||
|         return !!notes.length; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if there are offline notes for a certain user. | ||||
|      * | ||||
|      * @param userId User ID the notes are about. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with boolean: true if has offline notes, false otherwise. | ||||
|      */ | ||||
|     async hasNotesForUser(userId: number, siteId?: string): Promise<boolean> { | ||||
|         const notes = await this.getNotesForUser(userId, siteId); | ||||
| 
 | ||||
|         return !!notes.length; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if there are offline notes with a certain publish state (Personal, Site or Course). | ||||
|      * | ||||
|      * @param state Publish state ('personal', 'site' or 'course'). | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with boolean: true if has offline notes, false otherwise. | ||||
|      */ | ||||
|     async hasNotesWithPublishState(state: string, siteId?: string): Promise<boolean> { | ||||
|         const notes = await this.getNotesWithPublishState(state, siteId); | ||||
| 
 | ||||
|         return !!notes.length; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Save a note to be sent later. | ||||
|      * | ||||
|      * @param userId User ID the note is about. | ||||
|      * @param courseId Course ID. | ||||
|      * @param state Publish state ('personal', 'site' or 'course'). | ||||
|      * @param content The note content. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved if stored, rejected if failure. | ||||
|      */ | ||||
|     async saveNote(userId: number, courseId: number, state: string, content: string, siteId?: string): Promise<void> { | ||||
|         const site = await CoreSites.getSite(siteId); | ||||
| 
 | ||||
|         const now = CoreTimeUtils.timestamp(); | ||||
|         const data: AddonNotesDBRecord = { | ||||
|             userid: userId, | ||||
|             courseid: courseId, | ||||
|             publishstate: state, | ||||
|             content: content, | ||||
|             format: 1, | ||||
|             created: now, | ||||
|             lastmodified: now, | ||||
|         }; | ||||
| 
 | ||||
|         await site.getDb().insertRecord(NOTES_TABLE, data); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete a note offline to be sent later. | ||||
|      * | ||||
|      * @param noteId Note ID. | ||||
|      * @param courseId Course ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved if stored, rejected if failure. | ||||
|      */ | ||||
|     async deleteNote(noteId: number, courseId: number, siteId?: string): Promise<void> { | ||||
|         const site = await CoreSites.getSite(siteId); | ||||
| 
 | ||||
|         const data: AddonNotesDeletedDBRecord = { | ||||
|             noteid: noteId, | ||||
|             courseid: courseId, | ||||
|             deleted: CoreTimeUtils.timestamp(), | ||||
|         }; | ||||
| 
 | ||||
|         await site.getDb().insertRecord(NOTES_DELETED_TABLE, data); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Undo delete a note. | ||||
|      * | ||||
|      * @param noteId Note ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved if deleted, rejected if failure. | ||||
|      */ | ||||
|     async undoDeleteNote(noteId: number, siteId?: string): Promise<void> { | ||||
|         const site = await CoreSites.getSite(siteId); | ||||
| 
 | ||||
|         await site.getDb().deleteRecords(NOTES_DELETED_TABLE, { noteid: noteId }); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| export const AddonNotesOffline = makeSingleton(AddonNotesOfflineProvider); | ||||
							
								
								
									
										270
									
								
								src/addons/notes/services/notes-sync.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										270
									
								
								src/addons/notes/services/notes-sync.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,270 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { CoreSyncBaseProvider } from '@classes/base-sync'; | ||||
| import { CoreNetworkError } from '@classes/errors/network-error'; | ||||
| import { CoreCourses } from '@features/courses/services/courses'; | ||||
| import { CoreApp } from '@services/app'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { Translate, makeSingleton } from '@singletons'; | ||||
| import { CoreEvents } from '@singletons/events'; | ||||
| import { AddonNotesDBRecord, AddonNotesDeletedDBRecord } from './database/notes'; | ||||
| import { AddonNotes, AddonNotesCreateNoteData } from './notes'; | ||||
| import { AddonNotesOffline } from './notes-offline'; | ||||
| 
 | ||||
| /** | ||||
|  * Service to sync notes. | ||||
|  */ | ||||
| @Injectable( { providedIn: 'root' } ) | ||||
| export class AddonNotesSyncProvider extends CoreSyncBaseProvider<AddonNotesSyncResult> { | ||||
| 
 | ||||
|     static readonly AUTO_SYNCED = 'addon_notes_autom_synced'; | ||||
| 
 | ||||
|     constructor() { | ||||
|         super('AddonNotesSync'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Try to synchronize all the notes in a certain site or in all sites. | ||||
|      * | ||||
|      * @param siteId Site ID to sync. If not defined, sync all sites. | ||||
|      * @param force Wether to force sync not depending on last execution. | ||||
|      * @return Promise resolved if sync is successful, rejected if sync fails. | ||||
|      */ | ||||
|     syncAllNotes(siteId?: string, force?: boolean): Promise<void> { | ||||
|         return this.syncOnSites('all notes', this.syncAllNotesFunc.bind(this, siteId, force), siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Synchronize all the notes in a certain site | ||||
|      * | ||||
|      * @param siteId Site ID to sync. | ||||
|      * @param force Wether to force sync not depending on last execution. | ||||
|      * @return Promise resolved if sync is successful, rejected if sync fails. | ||||
|      */ | ||||
|     protected async syncAllNotesFunc(siteId: string, force: boolean): Promise<void> { | ||||
|         const notesArray = await Promise.all([ | ||||
|             AddonNotesOffline.getAllNotes(siteId), | ||||
|             AddonNotesOffline.getAllDeletedNotes(siteId), | ||||
|         ]); | ||||
| 
 | ||||
|         // Get all the courses to be synced.
 | ||||
|         const courseIds: number[] = []; | ||||
|         notesArray.forEach((notes: (AddonNotesDeletedDBRecord | AddonNotesDBRecord)[]) => { | ||||
|             const courseIds = notes.map((note) => note.courseid); | ||||
| 
 | ||||
|             courseIds.concat(courseIds); | ||||
|         }, []); | ||||
| 
 | ||||
|         CoreUtils.uniqueArray(courseIds); | ||||
| 
 | ||||
|         // Sync all courses.
 | ||||
|         const promises = courseIds.map(async (courseId) => { | ||||
|             const result = await (force | ||||
|                 ? this.syncNotes(courseId, siteId) | ||||
|                 : this.syncNotesIfNeeded(courseId, siteId)); | ||||
| 
 | ||||
|             if (typeof result != 'undefined') { | ||||
|                 // Sync successful, send event.
 | ||||
|                 CoreEvents.trigger(AddonNotesSyncProvider.AUTO_SYNCED, { | ||||
|                     courseId, | ||||
|                     warnings: result.warnings, | ||||
|                 }, siteId); | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         await Promise.all(promises); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Sync course notes only if a certain time has passed since the last time. | ||||
|      * | ||||
|      * @param courseId Course ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when the notes are synced or if they don't need to be synced. | ||||
|      */ | ||||
|     protected async syncNotesIfNeeded(courseId: number, siteId?: string): Promise<AddonNotesSyncResult | undefined> { | ||||
|         const needed = await this.isSyncNeeded(courseId, siteId); | ||||
| 
 | ||||
|         if (needed) { | ||||
|             return this.syncNotes(courseId, siteId); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Synchronize notes of a course. | ||||
|      * | ||||
|      * @param courseId Course ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved if sync is successful, rejected otherwise. | ||||
|      */ | ||||
|     syncNotes(courseId: number, siteId?: string): Promise<AddonNotesSyncResult> { | ||||
|         siteId = siteId || CoreSites.getCurrentSiteId(); | ||||
| 
 | ||||
|         if (this.isSyncing(courseId, siteId)) { | ||||
|             // There's already a sync ongoing for notes, return the promise.
 | ||||
|             return this.getOngoingSync(courseId, siteId)!; | ||||
|         } | ||||
| 
 | ||||
|         this.logger.debug('Try to sync notes for course ' + courseId); | ||||
| 
 | ||||
|         const syncPromise = this.performSyncNotes(courseId, siteId); | ||||
| 
 | ||||
|         return this.addOngoingSync(courseId, syncPromise, siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Perform the synchronization of the notes of a course. | ||||
|      * | ||||
|      * @param courseId Course ID. | ||||
|      * @param siteId Site ID. | ||||
|      * @return Promise resolved if sync is successful, rejected otherwise. | ||||
|      */ | ||||
|     async performSyncNotes(courseId: number, siteId?: string): Promise<AddonNotesSyncResult> { | ||||
|         const result: AddonNotesSyncResult = { | ||||
|             warnings: [], | ||||
|         }; | ||||
| 
 | ||||
|         // Get offline notes to be sent and deleted.
 | ||||
|         const [offlineNotes, deletedNotes] = await Promise.all([ | ||||
|             AddonNotesOffline.getAllNotes(siteId), | ||||
|             AddonNotesOffline.getAllDeletedNotes(siteId), | ||||
|         ]); | ||||
| 
 | ||||
|         if (!offlineNotes.length && !deletedNotes.length) { | ||||
|             // Nothing to sync.
 | ||||
|             return result; | ||||
|         } | ||||
| 
 | ||||
|         if (!CoreApp.isOnline()) { | ||||
|             // Cannot sync in offline.
 | ||||
|             throw new CoreNetworkError(); | ||||
|         } | ||||
| 
 | ||||
|         const errors: string[] = []; | ||||
|         const promises: Promise<void>[] = []; | ||||
| 
 | ||||
|         // Format the notes to be sent.
 | ||||
|         const notesToSend: AddonNotesCreateNoteData[] = offlineNotes.map((note) => ({ | ||||
|             userid: note.userid, | ||||
|             publishstate: note.publishstate, | ||||
|             courseid: note.courseid, | ||||
|             text: note.content, | ||||
|             format: 1, | ||||
|         })); | ||||
| 
 | ||||
|         // Send the notes.
 | ||||
|         promises.push(AddonNotes.addNotesOnline(notesToSend, siteId).then((response) => { | ||||
|             // Search errors in the response.
 | ||||
|             response.forEach((entry) => { | ||||
|                 if (entry.noteid === -1 && entry.errormessage && errors.indexOf(entry.errormessage) == -1) { | ||||
|                     errors.push(entry.errormessage); | ||||
|                 } | ||||
|             }); | ||||
| 
 | ||||
|             return; | ||||
|         }).catch((error) => { | ||||
|             if (CoreUtils.isWebServiceError(error)) { | ||||
|                 // It's a WebService error, this means the user cannot send notes.
 | ||||
|                 errors.push(error); | ||||
| 
 | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             // Not a WebService error, reject the synchronization to try again.
 | ||||
|             throw error; | ||||
|         }).then(async () => { | ||||
|             // Notes were sent, delete them from local DB.
 | ||||
|             const promises: Promise<void>[] = offlineNotes.map((note) => | ||||
|                 AddonNotesOffline.deleteOfflineNote(note.userid, note.content, note.created, siteId)); | ||||
| 
 | ||||
|             await Promise.all(promises); | ||||
| 
 | ||||
|             return; | ||||
|         })); | ||||
| 
 | ||||
|         // Format the notes to be sent.
 | ||||
|         const notesToDelete = deletedNotes.map((note) => note.noteid); | ||||
| 
 | ||||
|         // Delete the notes.
 | ||||
|         promises.push(AddonNotes.deleteNotesOnline(notesToDelete, courseId, siteId).catch((error) => { | ||||
|             if (CoreUtils.isWebServiceError(error)) { | ||||
|                 // It's a WebService error, this means the user cannot send notes.
 | ||||
|                 errors.push(error); | ||||
| 
 | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             // Not a WebService error, reject the synchronization to try again.
 | ||||
|             throw error; | ||||
|         }).then(async () => { | ||||
|             // Notes were sent, delete them from local DB.
 | ||||
|             const promises = notesToDelete.map((noteId) => AddonNotesOffline.undoDeleteNote(noteId, siteId)); | ||||
| 
 | ||||
|             await Promise.all(promises); | ||||
| 
 | ||||
|             return; | ||||
|         })); | ||||
| 
 | ||||
|         await Promise.all(promises); | ||||
| 
 | ||||
|         // Fetch the notes from server to be sure they're up to date.
 | ||||
|         await CoreUtils.ignoreErrors(AddonNotes.invalidateNotes(courseId, undefined, siteId)); | ||||
| 
 | ||||
|         await CoreUtils.ignoreErrors(AddonNotes.getNotes(courseId, undefined, false, true, siteId)); | ||||
| 
 | ||||
|         if (errors && errors.length) { | ||||
|             // At least an error occurred, get course name and add errors to warnings array.
 | ||||
|             const course = await CoreUtils.ignoreErrors(CoreCourses.getUserCourse(courseId, true, siteId), {}); | ||||
| 
 | ||||
|             result.warnings = errors.map((error) => | ||||
|                 Translate.instant('addon.notes.warningnotenotsent', { | ||||
|                     course: 'fullname' in course ? course.fullname : courseId, | ||||
|                     error: error, | ||||
|                 })); | ||||
|         } | ||||
| 
 | ||||
|         // All done, return the warnings.
 | ||||
|         return result; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| export const AddonNotesSync = makeSingleton(AddonNotesSyncProvider); | ||||
| 
 | ||||
| export type AddonNotesSyncResult = { | ||||
|     warnings: string[]; // List of warnings.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Data passed to AUTO_SYNCED event. | ||||
|  */ | ||||
| export type AddonNotesSyncAutoSyncData = { | ||||
|     courseId: number; | ||||
|     warnings: string[]; | ||||
| }; | ||||
| 
 | ||||
| declare module '@singletons/events' { | ||||
| 
 | ||||
|     /** | ||||
|      * Augment CoreEventsData interface with events specific to this service. | ||||
|      * | ||||
|      * @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
 | ||||
|      */ | ||||
|     export interface CoreEventsData { | ||||
|         [AddonNotesSyncProvider.AUTO_SYNCED]: AddonNotesSyncAutoSyncData; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										506
									
								
								src/addons/notes/services/notes.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										506
									
								
								src/addons/notes/services/notes.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,506 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { CoreWSError } from '@classes/errors/wserror'; | ||||
| import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; | ||||
| import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications'; | ||||
| import { CoreUser } from '@features/user/services/user'; | ||||
| import { CoreApp } from '@services/app'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { CoreWSExternalWarning } from '@services/ws'; | ||||
| import { makeSingleton, Translate } from '@singletons'; | ||||
| import { AddonNotesOffline } from './notes-offline'; | ||||
| 
 | ||||
| const ROOT_CACHE_KEY = 'mmaNotes:'; | ||||
| 
 | ||||
| /** | ||||
|  * Service to handle notes. | ||||
|  */ | ||||
| @Injectable( { providedIn: 'root' } ) | ||||
| export class AddonNotesProvider { | ||||
| 
 | ||||
|     /** | ||||
|      * Add a note. | ||||
|      * | ||||
|      * @param userId User ID of the person to add the note. | ||||
|      * @param courseId Course ID where the note belongs. | ||||
|      * @param publishState Personal, Site or Course. | ||||
|      * @param noteText The note text. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with boolean: true if note was sent to server, false if stored in device. | ||||
|      */ | ||||
|     async addNote(userId: number, courseId: number, publishState: string, noteText: string, siteId?: string): Promise<boolean> { | ||||
|         siteId = siteId || CoreSites.getCurrentSiteId(); | ||||
| 
 | ||||
|         // Convenience function to store a note to be synchronized later.
 | ||||
|         const storeOffline = async (): Promise<boolean> => { | ||||
|             await AddonNotesOffline.saveNote(userId, courseId, publishState, noteText, siteId); | ||||
| 
 | ||||
|             return false; | ||||
|         }; | ||||
| 
 | ||||
|         if (!CoreApp.isOnline()) { | ||||
|             // App is offline, store the note.
 | ||||
|             return storeOffline(); | ||||
|         } | ||||
| 
 | ||||
|         // Send note to server.
 | ||||
|         try { | ||||
|             await this.addNoteOnline(userId, courseId, publishState, noteText, siteId); | ||||
| 
 | ||||
|             return true; | ||||
|         } catch (error) { | ||||
|             if (CoreUtils.isWebServiceError(error)) { | ||||
|                 // It's a WebService error, the user cannot send the message so don't store it.
 | ||||
|                 throw error; | ||||
|             } | ||||
| 
 | ||||
|             return storeOffline(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Add a note. It will fail if offline or cannot connect. | ||||
|      * | ||||
|      * @param userId User ID of the person to add the note. | ||||
|      * @param courseId Course ID where the note belongs. | ||||
|      * @param publishState Personal, Site or Course. | ||||
|      * @param noteText The note text. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when added, rejected otherwise. | ||||
|      */ | ||||
|     async addNoteOnline(userId: number, courseId: number, publishState: string, noteText: string, siteId?: string): Promise<void> { | ||||
|         const notes: AddonNotesCreateNoteData[] = [ | ||||
|             { | ||||
|                 courseid: courseId, | ||||
|                 format: 1, | ||||
|                 publishstate: publishState, | ||||
|                 text: noteText, | ||||
|                 userid: userId, | ||||
|             }, | ||||
|         ]; | ||||
| 
 | ||||
|         const response = await this.addNotesOnline(notes, siteId); | ||||
|         if (response && response[0] && response[0].noteid === -1) { | ||||
|             // There was an error, and it should be translated already.
 | ||||
|             throw new CoreWSError({ message: response[0].errormessage }); | ||||
|         } | ||||
| 
 | ||||
|         await CoreUtils.ignoreErrors(this.invalidateNotes(courseId, undefined, siteId)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Add several notes. It will fail if offline or cannot connect. | ||||
|      * | ||||
|      * @param notes Notes to save. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when added, rejected otherwise. Promise resolved doesn't mean that notes | ||||
|      *         have been added, the resolve param can contain errors for notes not sent. | ||||
|      */ | ||||
|     async addNotesOnline(notes: AddonNotesCreateNoteData[], siteId?: string): Promise<AddonNotesCreateNotesWSResponse> { | ||||
|         if (!notes || !notes.length) { | ||||
|             return []; | ||||
|         } | ||||
| 
 | ||||
|         const site = await CoreSites.getSite(siteId); | ||||
| 
 | ||||
|         const data: AddonNotesCreateNotesWSParams = { | ||||
|             notes: notes, | ||||
|         }; | ||||
| 
 | ||||
|         return site.write('core_notes_create_notes', data); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete a note. | ||||
|      * | ||||
|      * @param note Note object to delete. | ||||
|      * @param courseId Course ID where the note belongs. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when deleted, rejected otherwise. Promise resolved doesn't mean that notes | ||||
|      *         have been deleted, the resolve param can contain errors for notes not deleted. | ||||
|      */ | ||||
|     async deleteNote(note: AddonNotesNoteFormatted, courseId: number, siteId?: string): Promise<boolean> { | ||||
|         siteId = siteId || CoreSites.getCurrentSiteId(); | ||||
| 
 | ||||
|         if (note.offline) { | ||||
|             await AddonNotesOffline.deleteOfflineNote(note.userid, note.content, note.created, siteId); | ||||
| 
 | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         // Convenience function to store the action to be synchronized later.
 | ||||
|         const storeOffline = async (): Promise<boolean> => { | ||||
|             await AddonNotesOffline.deleteNote(note.id, courseId, siteId); | ||||
| 
 | ||||
|             return false; | ||||
|         }; | ||||
| 
 | ||||
|         if (!CoreApp.isOnline()) { | ||||
|             // App is offline, store the note.
 | ||||
|             return storeOffline(); | ||||
|         } | ||||
| 
 | ||||
|         // Send note to server.
 | ||||
|         try { | ||||
|             await this.deleteNotesOnline([note.id], courseId, siteId); | ||||
| 
 | ||||
|             return true; | ||||
|         } catch (error) { | ||||
|             if (CoreUtils.isWebServiceError(error)) { | ||||
|                 // It's a WebService error, the user cannot send the note so don't store it.
 | ||||
|                 throw error; | ||||
|             } | ||||
| 
 | ||||
|             return await storeOffline(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete a note. It will fail if offline or cannot connect. | ||||
|      * | ||||
|      * @param noteIds Note IDs to delete. | ||||
|      * @param courseId Course ID where the note belongs. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when deleted, rejected otherwise. Promise resolved doesn't mean that notes | ||||
|      *         have been deleted, the resolve param can contain errors for notes not deleted. | ||||
|      */ | ||||
|     async deleteNotesOnline(noteIds: number[], courseId: number, siteId?: string): Promise<void> { | ||||
|         const site = await CoreSites.getSite(siteId); | ||||
| 
 | ||||
|         const params: AddonNotesDeleteNotesWSParams = { | ||||
|             notes: noteIds, | ||||
|         }; | ||||
| 
 | ||||
|         await site.write('core_notes_delete_notes', params); | ||||
| 
 | ||||
|         CoreUtils.ignoreErrors(this.invalidateNotes(courseId, undefined, siteId)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns whether or not the notes plugin is enabled for a certain site. | ||||
|      * | ||||
|      * This method is called quite often and thus should only perform a quick | ||||
|      * check, we should not be calling WS from here. | ||||
|      * | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with true if enabled, resolved with false or rejected otherwise. | ||||
|      */ | ||||
|     async isPluginEnabled(siteId?: string): Promise<boolean> { | ||||
|         const site = await CoreSites.getSite(siteId); | ||||
| 
 | ||||
|         return site.canUseAdvancedFeature('enablenotes'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns whether or not the add note plugin is enabled for a certain course. | ||||
|      * | ||||
|      * @param courseId ID of the course. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with true if enabled, resolved with false or rejected otherwise. | ||||
|      */ | ||||
|     async isPluginAddNoteEnabledForCourse(courseId: number, siteId?: string): Promise<boolean> { | ||||
|         const site = await CoreSites.getSite(siteId); | ||||
| 
 | ||||
|         // The only way to detect if it's enabled is to perform a WS call.
 | ||||
|         // We use an invalid user ID (-1) to avoid saving the note if the user has permissions.
 | ||||
|         const params: AddonNotesCreateNotesWSParams = { | ||||
|             notes: [ | ||||
|                 { | ||||
|                     userid: -1, | ||||
|                     publishstate: 'personal', | ||||
|                     courseid: courseId, | ||||
|                     text: '', | ||||
|                     format: 1, | ||||
|                 }, | ||||
|             ], | ||||
|         }; | ||||
|         const preSets: CoreSiteWSPreSets = { | ||||
|             updateFrequency: CoreSite.FREQUENCY_RARELY, | ||||
|         }; | ||||
| 
 | ||||
|         // Use .read to cache data and be able to check it in offline. This means that, if a user loses the capabilities
 | ||||
|         // to add notes, he'll still see the option in the app.
 | ||||
|         return CoreUtils.promiseWorks(site.read('core_notes_create_notes', params, preSets)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns whether or not the read notes plugin is enabled for a certain course. | ||||
|      * | ||||
|      * @param courseId ID of the course. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with true if enabled, resolved with false or rejected otherwise. | ||||
|      */ | ||||
|     isPluginViewNotesEnabledForCourse(courseId: number, siteId?: string): Promise<boolean> { | ||||
|         return CoreUtils.promiseWorks(this.getNotes(courseId, undefined, false, true, siteId)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get prefix cache key for course notes. | ||||
|      * | ||||
|      * @param courseId ID of the course to get the notes from. | ||||
|      * @return Cache key. | ||||
|      */ | ||||
|     getNotesPrefixCacheKey(courseId: number): string { | ||||
|         return ROOT_CACHE_KEY + 'notes:' + courseId + ':'; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the cache key for the get notes call. | ||||
|      * | ||||
|      * @param courseId ID of the course to get the notes from. | ||||
|      * @param userId ID of the user to get the notes from if requested. | ||||
|      * @return Cache key. | ||||
|      */ | ||||
|     getNotesCacheKey(courseId: number, userId?: number): string { | ||||
|         return this.getNotesPrefixCacheKey(courseId) + (userId ? userId : ''); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get users notes for a certain site, course and personal notes. | ||||
|      * | ||||
|      * @param courseId ID of the course to get the notes from. | ||||
|      * @param userId ID of the user to get the notes from if requested. | ||||
|      * @param ignoreCache True when we should not get the value from the cache. | ||||
|      * @param onlyOnline True to return only online notes, false to return both online and offline. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise to be resolved when the notes are retrieved. | ||||
|      */ | ||||
|     async getNotes( | ||||
|         courseId: number, | ||||
|         userId?: number, | ||||
|         ignoreCache = false, | ||||
|         onlyOnline = false, | ||||
|         siteId?: string, | ||||
|     ): Promise<AddonNotesGetCourseNotesWSResponse> { | ||||
| 
 | ||||
|         const site = await CoreSites.getSite(siteId); | ||||
|         const params: AddonNotesGetCourseNotesWSParams = { | ||||
|             courseid: courseId, | ||||
|         }; | ||||
|         if (userId) { | ||||
|             params.userid = userId; | ||||
|         } | ||||
| 
 | ||||
|         const preSets: CoreSiteWSPreSets = { | ||||
|             cacheKey: this.getNotesCacheKey(courseId, userId), | ||||
|             updateFrequency: CoreSite.FREQUENCY_SOMETIMES, | ||||
|         }; | ||||
| 
 | ||||
|         if (ignoreCache) { | ||||
|             preSets.getFromCache = false; | ||||
|             preSets.emergencyCache = false; | ||||
|         } | ||||
|         const notes = await site.read<AddonNotesGetCourseNotesWSResponse>('core_notes_get_course_notes', params, preSets); | ||||
|         if (onlyOnline) { | ||||
|             return notes; | ||||
|         } | ||||
| 
 | ||||
|         const offlineNotes = await AddonNotesOffline.getNotesForCourseAndUser(courseId, userId, siteId); | ||||
|         offlineNotes.forEach((note: AddonNotesNote) => { | ||||
|             const fieldName = note.publishstate + 'notes'; | ||||
|             if (!notes[fieldName]) { | ||||
|                 notes[fieldName] = []; | ||||
|             } | ||||
|             note.offline = true; | ||||
|             // Add note to the start of array since last notes are shown first.
 | ||||
|             notes[fieldName].unshift(note); | ||||
|         }); | ||||
| 
 | ||||
|         return notes; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get offline deleted notes and set the state. | ||||
|      * | ||||
|      * @param notes Array of notes. | ||||
|      * @param courseId ID of the course the notes belong to. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async setOfflineDeletedNotes( | ||||
|         notes: AddonNotesNoteFormatted[], | ||||
|         courseId: number, | ||||
|         siteId?: string, | ||||
|     ): Promise<void> { | ||||
|         const deletedNotes = await AddonNotesOffline.getCourseDeletedNotes(courseId, siteId); | ||||
| 
 | ||||
|         notes.forEach((note) => { | ||||
|             note.deleted = deletedNotes.some((n) => n.noteid == note.id); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get user data for notes since they only have userid. | ||||
|      * | ||||
|      * @param notes Notes to get the data for. | ||||
|      * @return Promise always resolved. Resolve param is the formatted notes. | ||||
|      */ | ||||
|     async getNotesUserData(notes: AddonNotesNoteFormatted[]): Promise<AddonNotesNoteFormatted[]> { | ||||
|         const promises = notes.map((note) => | ||||
|             // Get the user profile to retrieve the user image.
 | ||||
|             CoreUser.getProfile(note.userid, note.courseid, true).then((user) => { | ||||
|                 note.userfullname = user.fullname; | ||||
|                 note.userprofileimageurl = user.profileimageurl; | ||||
| 
 | ||||
|                 return; | ||||
|             }).catch(() => { | ||||
|                 note.userfullname = Translate.instant('addon.notes.userwithid', { id: note.userid }); | ||||
|             })); | ||||
| 
 | ||||
|         await Promise.all(promises); | ||||
| 
 | ||||
|         return notes; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Invalidate get notes WS call. | ||||
|      * | ||||
|      * @param courseId Course ID. | ||||
|      * @param userId User ID if needed. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when data is invalidated. | ||||
|      */ | ||||
|     async invalidateNotes(courseId: number, userId?: number, siteId?: string): Promise<void> { | ||||
|         const site = await CoreSites.getSite(siteId); | ||||
| 
 | ||||
|         if (userId) { | ||||
|             await site.invalidateWsCacheForKey(this.getNotesCacheKey(courseId, userId)); | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         await site.invalidateWsCacheForKeyStartingWith(this.getNotesPrefixCacheKey(courseId)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Report notes as being viewed. | ||||
|      * | ||||
|      * @param courseId ID of the course. | ||||
|      * @param userId User ID if needed. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when the WS call is successful. | ||||
|      */ | ||||
|     async logView(courseId: number, userId?: number, siteId?: string): Promise<void> { | ||||
|         const site = await CoreSites.getSite(siteId); | ||||
| 
 | ||||
|         const params: AddonNotesViewNotesWSParams = { | ||||
|             courseid: courseId, | ||||
|             userid: userId || 0, | ||||
|         }; | ||||
| 
 | ||||
|         CorePushNotifications.logViewListEvent('notes', 'core_notes_view_notes', params, site.getId()); | ||||
| 
 | ||||
|         await site.write('core_notes_view_notes', params); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| export const AddonNotes = makeSingleton(AddonNotesProvider); | ||||
| 
 | ||||
| /** | ||||
|  * Params of core_notes_view_notes WS. | ||||
|  */ | ||||
| type AddonNotesViewNotesWSParams = { | ||||
|     courseid: number; // Course id, 0 for notes at system level.
 | ||||
|     userid?: number; // User id, 0 means view all the user notes.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Params of core_notes_get_course_notes WS. | ||||
|  */ | ||||
| export type AddonNotesGetCourseNotesWSParams = { | ||||
|     courseid: number; // Course id, 0 for SITE.
 | ||||
|     userid?: number; // User id.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Note data returned by core_notes_get_course_notes. | ||||
|  */ | ||||
| export type AddonNotesNote = { | ||||
|     id: number; // Id of this note.
 | ||||
|     courseid: number; // Id of the course.
 | ||||
|     userid: number; // User id.
 | ||||
|     content: string; // The content text formated.
 | ||||
|     format: number; // Content format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
 | ||||
|     created: number; // Time created (timestamp).
 | ||||
|     lastmodified: number; // Time of last modification (timestamp).
 | ||||
|     usermodified: number; // User id of the creator of this note.
 | ||||
|     publishstate: string; // State of the note (i.e. draft, public, site).
 | ||||
|     offline?: boolean; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Result of WS core_notes_get_course_notes. | ||||
|  */ | ||||
| export type AddonNotesGetCourseNotesWSResponse = { | ||||
|     sitenotes?: AddonNotesNote[]; // Site notes.
 | ||||
|     coursenotes?: AddonNotesNote[]; // Couse notes.
 | ||||
|     personalnotes?: AddonNotesNote[]; // Personal notes.
 | ||||
|     canmanagesystemnotes?: boolean; // @since 3.7. Whether the user can manage notes at system level.
 | ||||
|     canmanagecoursenotes?: boolean; // @since 3.7. Whether the user can manage notes at the given course.
 | ||||
|     warnings?: CoreWSExternalWarning[]; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Result of WS core_notes_view_notes. | ||||
|  */ | ||||
| export type AddonNotesViewNotesResult = { | ||||
|     status: boolean; // Status: true if success.
 | ||||
|     warnings?: CoreWSExternalWarning[]; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Notes with some calculated data. | ||||
|  */ | ||||
| export type AddonNotesNoteFormatted = AddonNotesNote & { | ||||
|     offline?: boolean; // Calculated in the app. Whether it's an offline note.
 | ||||
|     deleted?: boolean; // Calculated in the app. Whether the note was deleted in offline.
 | ||||
|     userfullname?: string; // Calculated in the app. Full name of the user the note refers to.
 | ||||
|     userprofileimageurl?: string; // Calculated in the app. Avatar url of the user the note refers to.
 | ||||
| }; | ||||
| 
 | ||||
| export type AddonNotesCreateNoteData = { | ||||
|     userid: number; // Id of the user the note is about.
 | ||||
|     publishstate: string; // 'personal', 'course' or 'site'.
 | ||||
|     courseid: number; // Course id of the note (in Moodle a note can only be created into a course,
 | ||||
|     // even for site and personal notes).
 | ||||
|     text: string; // The text of the message - text or HTML.
 | ||||
|     format?: number; // Text format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
 | ||||
|     clientnoteid?: string; // Your own client id for the note. If this id is provided, the fail message id will be returned to you.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Params of core_notes_create_notes WS. | ||||
|  */ | ||||
| type AddonNotesCreateNotesWSParams = { | ||||
|     notes: AddonNotesCreateNoteData[]; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Note returned by WS core_notes_create_notes. | ||||
|  */ | ||||
| export type AddonNotesCreateNotesWSResponse = { | ||||
|     clientnoteid?: string; // Your own id for the note.
 | ||||
|     noteid: number; // ID of the created note when successful, -1 when failed.
 | ||||
|     errormessage?: string; // Error message - if failed.
 | ||||
| }[]; | ||||
| 
 | ||||
| /** | ||||
|  * Params of core_notes_delete_notes WS. | ||||
|  */ | ||||
| type AddonNotesDeleteNotesWSParams = { | ||||
|     notes: number[]; // Array of Note Ids to be deleted.
 | ||||
| }; | ||||
| @ -27,8 +27,7 @@ export class CoreWSError extends CoreError { | ||||
|     debuginfo?: string; // Debug info. Only if debug mode is enabled.
 | ||||
|     backtrace?: string; // Backtrace. Only if debug mode is enabled.
 | ||||
| 
 | ||||
|     // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | ||||
|     constructor(error: any) { | ||||
|     constructor(error: CoreWSErrorData) { | ||||
|         super(error.message); | ||||
| 
 | ||||
|         this.exception = error.exception; | ||||
| @ -41,3 +40,14 @@ export class CoreWSError extends CoreError { | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| type CoreWSErrorData = { | ||||
|     message?: string; | ||||
|     exception?: string; // Name of the Moodle exception.
 | ||||
|     errorcode?: string; | ||||
|     warningcode?: string; | ||||
|     link?: string; // Link to the site.
 | ||||
|     moreinfourl?: string; // Link to a page with more info.
 | ||||
|     debuginfo?: string; // Debug info. Only if debug mode is enabled.
 | ||||
|     backtrace?: string; // Backtrace. Only if debug mode is enabled.
 | ||||
| }; | ||||
|  | ||||
| @ -141,10 +141,10 @@ import { ADDON_MOD_RESOURCE_SERVICES } from '@addons/mod/resource/resource.modul | ||||
| import { ADDON_MOD_URL_SERVICES } from '@addons/mod/url/url.module'; | ||||
| // @todo import { ADDON_MOD_WIKI_SERVICES } from '@addons/mod/wiki/wiki.module';
 | ||||
| // @todo import { ADDON_MOD_WORKSHOP_SERVICES } from '@addons/mod/workshop/workshop.module';
 | ||||
| // @todo import { ADDON_NOTES_SERVICES } from '@addons/notes/notes.module';
 | ||||
| import { ADDON_NOTES_SERVICES } from '@addons/notes/notes.module'; | ||||
| import { ADDON_NOTIFICATIONS_SERVICES } from '@addons/notifications/notifications.module'; | ||||
| import { ADDON_PRIVATEFILES_SERVICES } from '@addons/privatefiles/privatefiles.module'; | ||||
| // @todo import { ADDON_REMOTETHEMES_SERVICES } from '@addons/remotethemes/remotethemes.module';
 | ||||
| import { ADDON_REMOTETHEMES_SERVICES } from '@addons/remotethemes/remotethemes.module'; | ||||
| 
 | ||||
| // Import some addon modules that define components, directives and pipes. Only import the important ones.
 | ||||
| import { AddonModAssignComponentsModule } from '@addons/mod/assign/components/components.module'; | ||||
| @ -306,10 +306,10 @@ export class CoreCompileProvider { | ||||
|             ...ADDON_MOD_URL_SERVICES, | ||||
|             // @todo ...ADDON_MOD_WIKI_SERVICES,
 | ||||
|             // @todo ...ADDON_MOD_WORKSHOP_SERVICES,
 | ||||
|             // @todo ...ADDON_NOTES_SERVICES,
 | ||||
|             ...ADDON_NOTES_SERVICES, | ||||
|             ...ADDON_NOTIFICATIONS_SERVICES, | ||||
|             ...ADDON_PRIVATEFILES_SERVICES, | ||||
|             // @todo ...ADDON_REMOTETHEMES_SERVICES,
 | ||||
|             ...ADDON_REMOTETHEMES_SERVICES, | ||||
|         ]; | ||||
| 
 | ||||
|         // We cannot inject anything to this constructor. Use the Injector to inject all the providers into the instance.
 | ||||
|  | ||||
| @ -24,7 +24,7 @@ import { CoreUtils } from '@services/utils/utils'; | ||||
| import { CoreSiteWSPreSets, CoreSite } from '@classes/site'; | ||||
| import { CoreConstants } from '@/core/constants'; | ||||
| import { makeSingleton, Platform, Translate } from '@singletons'; | ||||
| import { CoreStatusWithWarningsWSResponse, CoreWSExternalFile } from '@services/ws'; | ||||
| import { CoreStatusWithWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws'; | ||||
| 
 | ||||
| import { CoreCourseStatusDBRecord, COURSE_STATUS_TABLE } from './database/course'; | ||||
| import { CoreCourseOffline } from './course-offline'; | ||||
| @ -1404,7 +1404,7 @@ type CoreCourseGetCourseModuleByInstanceWSParams = { | ||||
|  */ | ||||
| export type CoreCourseGetCourseModuleWSResponse = { | ||||
|     cm: CoreCourseModuleBasicInfo; | ||||
|     warnings?: CoreStatusWithWarningsWSResponse[]; | ||||
|     warnings?: CoreWSExternalWarning[]; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  | ||||
| @ -1086,7 +1086,7 @@ export class CoreLoginHelperProvider { | ||||
|                 ); | ||||
| 
 | ||||
|                 if (!result.status) { | ||||
|                     throw new CoreWSError(result.warnings?.[0]); | ||||
|                     throw new CoreWSError(result.warnings![0]); | ||||
|                 } | ||||
| 
 | ||||
|                 const message = Translate.instant('core.login.emailconfirmsentsuccess'); | ||||
|  | ||||
| @ -209,7 +209,10 @@ class CoreUserParticipantsManager extends CorePageItemsListManager<CoreUserParti | ||||
|      */ | ||||
|     async select(participant: CoreUserParticipant | CoreUserData): Promise<void> { | ||||
|         if (CoreScreen.isMobile) { | ||||
|             await CoreNavigator.navigateToSitePath('/user/profile', { params: { userId: participant.id } }); | ||||
|             await CoreNavigator.navigateToSitePath( | ||||
|                 '/user/profile', | ||||
|                 { params: { userId: participant.id, courseId: this.courseId } }, | ||||
|             ); | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user