MOBILE-3632 notes: Add notes funcionality
parent
37d6bb0464
commit
9867a48461
|
@ -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"
|
||||
|
|
|
@ -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>
|
|
@ -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 });
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {}
|
|
@ -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}}"
|
||||
}
|
|
@ -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 {}
|
|
@ -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 {}
|
|
@ -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>
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
};
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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…
Reference in New Issue