From d29712258a2d4321d44e6612773977e4da075222 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Tue, 20 Mar 2018 11:47:59 +0100 Subject: [PATCH 1/8] MOBILE-2319 notes: Migrate notes --- .../notes/components/components.module.ts | 41 ++ src/addon/notes/components/types/types.html | 15 + src/addon/notes/components/types/types.ts | 62 +++ src/addon/notes/lang/en.json | 13 + src/addon/notes/notes.module.ts | 67 ++++ src/addon/notes/pages/add/add.html | 28 ++ src/addon/notes/pages/add/add.module.ts | 31 ++ src/addon/notes/pages/add/add.ts | 69 ++++ src/addon/notes/pages/list/list.html | 42 ++ src/addon/notes/pages/list/list.module.ts | 35 ++ src/addon/notes/pages/list/list.ts | 173 +++++++++ .../notes/providers/course-option-handler.ts | 90 +++++ src/addon/notes/providers/notes-offline.ts | 232 +++++++++++ src/addon/notes/providers/notes-sync.ts | 198 ++++++++++ src/addon/notes/providers/notes.ts | 367 ++++++++++++++++++ .../notes/providers/sync-cron-handler.ts | 47 +++ src/addon/notes/providers/user-handler.ts | 101 +++++ src/app/app.module.ts | 2 + src/classes/base-sync.ts | 10 +- 19 files changed, 1618 insertions(+), 5 deletions(-) create mode 100644 src/addon/notes/components/components.module.ts create mode 100644 src/addon/notes/components/types/types.html create mode 100644 src/addon/notes/components/types/types.ts create mode 100644 src/addon/notes/lang/en.json create mode 100644 src/addon/notes/notes.module.ts create mode 100644 src/addon/notes/pages/add/add.html create mode 100644 src/addon/notes/pages/add/add.module.ts create mode 100644 src/addon/notes/pages/add/add.ts create mode 100644 src/addon/notes/pages/list/list.html create mode 100644 src/addon/notes/pages/list/list.module.ts create mode 100644 src/addon/notes/pages/list/list.ts create mode 100644 src/addon/notes/providers/course-option-handler.ts create mode 100644 src/addon/notes/providers/notes-offline.ts create mode 100644 src/addon/notes/providers/notes-sync.ts create mode 100644 src/addon/notes/providers/notes.ts create mode 100644 src/addon/notes/providers/sync-cron-handler.ts create mode 100644 src/addon/notes/providers/user-handler.ts diff --git a/src/addon/notes/components/components.module.ts b/src/addon/notes/components/components.module.ts new file mode 100644 index 000000000..4064545e6 --- /dev/null +++ b/src/addon/notes/components/components.module.ts @@ -0,0 +1,41 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '@components/components.module'; +import { AddonNotesTypesComponent } from './types/types'; + +@NgModule({ + declarations: [ + AddonNotesTypesComponent + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + ], + providers: [ + ], + exports: [ + AddonNotesTypesComponent + ], + entryComponents: [ + AddonNotesTypesComponent + ] +}) +export class AddonNotesComponentsModule {} diff --git a/src/addon/notes/components/types/types.html b/src/addon/notes/components/types/types.html new file mode 100644 index 000000000..87c04392c --- /dev/null +++ b/src/addon/notes/components/types/types.html @@ -0,0 +1,15 @@ + + + + + {{ 'addon.notes.sitenotes' | translate }} + + + {{ 'addon.notes.coursenotes' | translate }} + + + {{ 'addon.notes.personalnotes' | translate }} + + + + diff --git a/src/addon/notes/components/types/types.ts b/src/addon/notes/components/types/types.ts new file mode 100644 index 000000000..813546b6d --- /dev/null +++ b/src/addon/notes/components/types/types.ts @@ -0,0 +1,62 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { AfterViewInit, Component, Input, OnInit, ViewChild } from '@angular/core'; +import { CoreSplitViewComponent } from '@components/split-view/split-view'; +import { CoreSitesProvider } from '@providers/sites'; + +/** + * Component that displays the competencies of a course. + */ +@Component({ + selector: 'addon-notes-types', + templateUrl: 'types.html', +}) +export class AddonNotesTypesComponent implements AfterViewInit, OnInit { + @ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent; + + @Input() courseId: number; + + protected type: string; + + constructor(private sitesProvider: CoreSitesProvider) { + } + + /** + * Properties initialized. + */ + ngOnInit(): void { + const siteHomeId = this.sitesProvider.getCurrentSite().getSiteHomeId(); + this.type = (this.courseId == siteHomeId ? 'site' : 'course'); + } + + /** + * View loaded. + */ + ngAfterViewInit(): void { + if (this.splitviewCtrl.isOn()) { + this.openList(this.type); + } + } + + /** + * Opens a list of notes. + * + * @param {string} type + */ + openList(type: string): void { + this.type = type; + this.splitviewCtrl.push('AddonNotesListPage', {courseId: this.courseId, type: type}); + } +} diff --git a/src/addon/notes/lang/en.json b/src/addon/notes/lang/en.json new file mode 100644 index 000000000..761417b79 --- /dev/null +++ b/src/addon/notes/lang/en.json @@ -0,0 +1,13 @@ +{ + "addnewnote": "Add a new note", + "coursenotes": "Course notes", + "eventnotecreated": "Note created", + "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}}" +} \ No newline at end of file diff --git a/src/addon/notes/notes.module.ts b/src/addon/notes/notes.module.ts new file mode 100644 index 000000000..4ea5dd0a8 --- /dev/null +++ b/src/addon/notes/notes.module.ts @@ -0,0 +1,67 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { AddonNotesProvider } from './providers/notes'; +import { AddonNotesOfflineProvider } from './providers/notes-offline'; +import { AddonNotesSyncProvider } from './providers/notes-sync'; +import { AddonNotesCourseOptionHandler } from './providers/course-option-handler'; +import { AddonNotesSyncCronHandler } from './providers/sync-cron-handler'; +import { AddonNotesUserHandler } from './providers/user-handler'; +import { AddonNotesComponentsModule } from './components/components.module'; +import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delegate'; +import { CoreCronDelegate } from '@providers/cron'; +import { CoreCoursesProvider } from '@core/courses/providers/courses'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreUserDelegate } from '@core/user/providers/user-delegate'; +import { CoreUserProvider } from '@core/user/providers/user'; + +@NgModule({ + declarations: [ + ], + imports: [ + AddonNotesComponentsModule + ], + providers: [ + AddonNotesProvider, + AddonNotesOfflineProvider, + AddonNotesSyncProvider, + AddonNotesCourseOptionHandler, + AddonNotesSyncCronHandler, + AddonNotesUserHandler + ] +}) +export class AddonNotesModule { + constructor(courseOptionsDelegate: CoreCourseOptionsDelegate, courseOptionHandler: AddonNotesCourseOptionHandler, + userDelegate: CoreUserDelegate, userHandler: AddonNotesUserHandler, cronDelegate: CoreCronDelegate, + syncHandler: AddonNotesSyncCronHandler, eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider) { + // Register handlers. + courseOptionsDelegate.registerHandler(courseOptionHandler); + userDelegate.registerHandler(userHandler); + cronDelegate.register(syncHandler); + + eventsProvider.on(CoreEventsProvider.LOGOUT, () => { + courseOptionHandler.clearCoursesNavCache(); + }, sitesProvider.getCurrentSiteId()); + + eventsProvider.on(CoreCoursesProvider.EVENT_MY_COURSES_REFRESHED, () => { + courseOptionHandler.clearCoursesNavCache(); + }, sitesProvider.getCurrentSiteId()); + + eventsProvider.on(CoreUserProvider.PROFILE_REFRESHED, () => { + userHandler.clearAddNoteCache(); + }, sitesProvider.getCurrentSiteId()); + } +} diff --git a/src/addon/notes/pages/add/add.html b/src/addon/notes/pages/add/add.html new file mode 100644 index 000000000..59b0bb32d --- /dev/null +++ b/src/addon/notes/pages/add/add.html @@ -0,0 +1,28 @@ + + + {{ 'addon.notes.addnewnote' | translate }} + + + + + + +
+ + {{ 'addon.notes.publishstate' | translate }} + + {{ 'addon.notes.personalnotes' | translate }} + {{ 'addon.notes.coursenotes' | translate }} + {{ 'addon.notes.sitenotes' | translate }} + + + + + + +
+
diff --git a/src/addon/notes/pages/add/add.module.ts b/src/addon/notes/pages/add/add.module.ts new file mode 100644 index 000000000..404012014 --- /dev/null +++ b/src/addon/notes/pages/add/add.module.ts @@ -0,0 +1,31 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { IonicPageModule } from 'ionic-angular'; +import { AddonNotesAddPage } from './add'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +@NgModule({ + declarations: [ + AddonNotesAddPage + ], + imports: [ + CoreDirectivesModule, + IonicPageModule.forChild(AddonNotesAddPage), + TranslateModule.forChild() + ] +}) +export class AddonNotesAddPageModule {} diff --git a/src/addon/notes/pages/add/add.ts b/src/addon/notes/pages/add/add.ts new file mode 100644 index 000000000..f31f44528 --- /dev/null +++ b/src/addon/notes/pages/add/add.ts @@ -0,0 +1,69 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component } from '@angular/core'; +import { IonicPage, ViewController, NavParams } from 'ionic-angular'; +import { CoreAppProvider } from '@providers/app'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { AddonNotesProvider } from '../../providers/notes'; + +/** + * Component that displays a text area for composing a note. + */ +@IonicPage({ segment: 'addon-notes-add' }) +@Component({ + selector: 'page-addon-notes-add', + templateUrl: 'add.html', +}) +export class AddonNotesAddPage { + userId: number; + courseId: number; + publishState = 'personal'; + text = ''; + processing = false; + + constructor(params: NavParams, private viewCtrl: ViewController, private appProvider: CoreAppProvider, + private domUtils: CoreDomUtilsProvider, private notesProvider: AddonNotesProvider) { + this.userId = params.get('userId'); + this.courseId = params.get('courseId'); + } + + /** + * Send the note or store it offline. + */ + addNote(): void { + this.appProvider.closeKeyboard(); + const loadingModal = this.domUtils.showModalLoading('core.sending', true); + // Freeze the add note button. + this.processing = true; + this.notesProvider.addNote(this.userId, this.courseId, this.publishState, this.text).then((sent) => { + this.viewCtrl.dismiss().finally(() => { + const message = sent ? 'addon.notes.eventnotecreated' : 'core.datastoredoffline'; + this.domUtils.showAlertTranslated('core.success', message); + }); + }).catch((error) => { + this.domUtils.showErrorModal(error); + this.processing = false; + }).finally(() => { + loadingModal.dismiss(); + }); + } + + /** + * Close modal. + */ + closeModal(): void { + this.viewCtrl.dismiss(); + } +} diff --git a/src/addon/notes/pages/list/list.html b/src/addon/notes/pages/list/list.html new file mode 100644 index 000000000..02d5f6084 --- /dev/null +++ b/src/addon/notes/pages/list/list.html @@ -0,0 +1,42 @@ + + + {{ 'addon.notes.notes' | translate }} + + + + + + + + + + + + + + + + +

+ {{ 'core.thereisdatatosync' | translate:{$a: 'addon.notes.notes' | translate | lowercase } }} +

+ + + + + + + + + +

{{note.userfullname}}

+

{{note.lastmodified | coreDateDayOrTime}}

+

{{ 'core.notsent' | translate }}

+
+ + + +
+
+
+
diff --git a/src/addon/notes/pages/list/list.module.ts b/src/addon/notes/pages/list/list.module.ts new file mode 100644 index 000000000..56f649dee --- /dev/null +++ b/src/addon/notes/pages/list/list.module.ts @@ -0,0 +1,35 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { IonicPageModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CorePipesModule } from '@pipes/pipes.module'; +import { AddonNotesListPage } from './list'; + +@NgModule({ + declarations: [ + AddonNotesListPage, + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + CorePipesModule, + IonicPageModule.forChild(AddonNotesListPage), + TranslateModule.forChild() + ], +}) +export class AddonNotesListPageModule {} diff --git a/src/addon/notes/pages/list/list.ts b/src/addon/notes/pages/list/list.ts new file mode 100644 index 000000000..fcebe9a34 --- /dev/null +++ b/src/addon/notes/pages/list/list.ts @@ -0,0 +1,173 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnDestroy, Optional, ViewChild } from '@angular/core'; +import { Content, IonicPage, NavController, NavParams } from 'ionic-angular'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreSplitViewComponent } from '@components/split-view/split-view'; +import { AddonNotesProvider } from '../../providers/notes'; +import { AddonNotesSyncProvider } from '../../providers/notes-sync'; + +/** + * Page that displays the list of notes. + */ +@IonicPage({ segment: 'addon-notes-list' }) +@Component({ + selector: 'page-addon-notes-list', + templateUrl: 'list.html', +}) +export class AddonNotesListPage implements OnDestroy { + @ViewChild(Content) content: Content; + + protected courseId = 0; + protected syncObserver: any; + + type = ''; + refreshIcon = 'spinner'; + syncIcon = 'spinner'; + notes: any[]; + hasOffline = false; + notesLoaded = false; + + constructor(navParams: NavParams, private navCtrl: NavController, @Optional() private svComponent: CoreSplitViewComponent, + private domUtils: CoreDomUtilsProvider, private textUtils: CoreTextUtilsProvider, + sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider, + private notesProvider: AddonNotesProvider, private notesSync: AddonNotesSyncProvider) { + this.courseId = navParams.get('courseId') || sitesProvider.getCurrentSiteHomeId(); + this.type = navParams.get('type'); + // Refresh data if notes are synchronized automatically. + this.syncObserver = eventsProvider.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 = 'spinner'; + this.syncIcon = 'spinner'; + + this.content.scrollToTop(); + this.fetchNotes(false); + } + }, sitesProvider.getCurrentSiteId()); + } + + /** + * View loaded. + */ + ionViewDidLoad(): void { + this.fetchNotes(true).then(() => { + this.notesProvider.logView(this.courseId); + }); + } + + /** + * Fetch notes + * @param {boolean} sync When to resync notes. + * @param {boolean} [showErrors] When to display errors or not. + * @return {Promise} Promise with the notes, + */ + private fetchNotes(sync: boolean, showErrors?: boolean): Promise { + const promise = sync ? this.syncNotes(showErrors) : Promise.resolve(); + + return promise.catch(() => { + // Ignore errors. + }).then(() => { + return this.notesProvider.getNotes(this.courseId).then((notes) => { + notes = notes[this.type + 'notes'] || []; + + this.hasOffline = this.notesProvider.hasOfflineNote(notes); + + return this.notesProvider.getNotesUserData(notes, this.courseId).then((notes) => { + this.notes = notes; + }); + }); + }).catch((message) => { + this.domUtils.showErrorModal(message); + }).finally(() => { + this.notesLoaded = true; + this.refreshIcon = 'refresh'; + this.syncIcon = 'loop'; + }); + } + + /** + * Refresh notes on PTR. + * + * @param {boolean} showErrors Whether to display errors or not. + * @param {any} refresher Refresher instance. + */ + refreshNotes(showErrors: boolean, refresher?: any): void { + this.refreshIcon = 'spinner'; + this.syncIcon = 'spinner'; + this.notesProvider.invalidateNotes(this.courseId).finally(() => { + this.fetchNotes(true, showErrors).finally(() => { + if (refresher) { + refresher.complete(); + } + }); + }); + } + + /** + * Tries to syncrhonize course notes. + * + * @param {boolean} showErrors Whether to display errors or not. + * @return {Promise} Promise resolved if sync is successful, rejected otherwise. + */ + private syncNotes(showErrors: boolean): Promise { + return this.notesSync.syncNotes(this.courseId).then((warnings) => { + this.showSyncWarnings(warnings); + }).catch((error) => { + if (showErrors) { + this.domUtils.showErrorModalDefault(error, 'core.errorsync', true); + } + + return Promise.reject(null); + }); + } + + /** + * Show sync warnings if any. + * + * @param {string[]} warnings the warnings + */ + private showSyncWarnings(warnings: string[]): void { + const message = this.textUtils.buildMessage(warnings); + if (message) { + this.domUtils.showErrorModal(message); + } + } + + /** + * Opens the profile of a user. + * + * @param {number} userId + */ + openUserProfile(userId: number): void { + // Decide which navCtrl to use. If this page is inside a split view, use the split view's master nav. + const navCtrl = this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl; + navCtrl.push('CoreUserProfilePage', {userId, courseId: this.courseId}); + } + + /** + * Page destroyed. + */ + ngOnDestroy(): void { + this.syncObserver && this.syncObserver.off(); + } +} diff --git a/src/addon/notes/providers/course-option-handler.ts b/src/addon/notes/providers/course-option-handler.ts new file mode 100644 index 000000000..f71f9e097 --- /dev/null +++ b/src/addon/notes/providers/course-option-handler.ts @@ -0,0 +1,90 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable, Injector } from '@angular/core'; +import { AddonNotesProvider } from './notes'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreCourseOptionsHandler, CoreCourseOptionsHandlerData } from '@core/course/providers/options-delegate'; +import { AddonNotesTypesComponent } from '../components/types/types'; + +/** + * Handler to inject an option into the course main menu. + */ +@Injectable() +export class AddonNotesCourseOptionHandler implements CoreCourseOptionsHandler { + name = 'AddonNotes'; + priority = 200; + protected coursesNavEnabledCache = {}; + + constructor(private notesProvider: AddonNotesProvider) { + } + + /** + * Clear courses nav cache. + */ + clearCoursesNavCache(): void { + this.coursesNavEnabledCache = {}; + } + + /** + * Whether or not the handler is enabled on a site level. + * @return {boolean|Promise} Whether or not the handler is enabled on a site level. + */ + isEnabled(): boolean | Promise { + return this.notesProvider.isPluginViewNotesEnabled(); + } + + /** + * Whether or not the handler is enabled for a certain course. + * + * @param {number} courseId The course ID. + * @param {any} accessData Access type and data. Default, guest, ... + * @param {any} [navOptions] Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions. + * @param {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions. + * @return {boolean|Promise} True or promise resolved with true if enabled. + */ + isEnabledForCourse(courseId: number, accessData: any, navOptions?: any, admOptions?: any): boolean | Promise { + if (accessData && accessData.type == CoreCourseProvider.ACCESS_GUEST) { + return false; // Not enabled for guests. + } + + if (navOptions && typeof navOptions.notes != 'undefined') { + return navOptions.notes; + } + + if (typeof this.coursesNavEnabledCache[courseId] != 'undefined') { + return this.coursesNavEnabledCache[courseId]; + } + + return this.notesProvider.isPluginViewNotesEnabledForCourse(courseId).then((enabled) => { + this.coursesNavEnabledCache[courseId] = enabled; + + return enabled; + }); + } + + /** + * Returns the data needed to render the handler. + * + * @param {number} courseId The course ID. + * @return {CoreCourseOptionsHandlerData} Data. + */ + getDisplayData?(injector: Injector, courseId: number): CoreCourseOptionsHandlerData { + return { + title: 'addon.notes.notes', + class: 'addon-notes-course-handler', + component: AddonNotesTypesComponent, + }; + } +} diff --git a/src/addon/notes/providers/notes-offline.ts b/src/addon/notes/providers/notes-offline.ts new file mode 100644 index 000000000..c285c7428 --- /dev/null +++ b/src/addon/notes/providers/notes-offline.ts @@ -0,0 +1,232 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; + +/** + * Service to handle offline notes. + */ +@Injectable() +export class AddonNotesOfflineProvider { + protected logger; + + // Variables for database. + protected NOTES_TABLE = 'addon_notes_offline_notes'; + protected tablesSchema = [ + { + name: this.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'] + } + ]; + + constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private timeUtils: CoreTimeUtilsProvider) { + this.logger = logger.getInstance('AddonNotesOfflineProvider'); + this.sitesProvider.createTablesFromSchema(this.tablesSchema); + } + + /** + * Delete a note. + * + * @param {number} userId User ID the note is about. + * @param {string} content The note content. + * @param {number} timecreated The time the note was created. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if deleted, rejected if failure. + */ + deleteNote(userId: number, content: string, timecreated: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().deleteRecords(this.NOTES_TABLE, { + userid: userId, + content: content, + created: timecreated + }); + }); + } + + /** + * Get all offline notes. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with notes. + */ + getAllNotes(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecords(this.NOTES_TABLE); + }); + } + + /** + * Get an offline note. + * + * @param {number} userId User ID the note is about. + * @param {string} content The note content. + * @param {number} timecreated The time the note was created. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the notes. + */ + getNote(userId: number, content: string, timecreated: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecord(this.NOTES_TABLE, { + userid: userId, + content: content, + created: timecreated + }); + }); + } + + /** + * Get offline notes for a certain course. + * + * @param {number} courseId Course ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with notes. + */ + getNotesForCourse(courseId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecords(this.NOTES_TABLE, {courseid: courseId}); + }); + } + + /** + * Get offline notes for a certain user. + * + * @param {number} userId User ID the notes are about. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with notes. + */ + getNotesForUser(userId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecords(this.NOTES_TABLE, {userid: userId}); + }); + } + + /** + * Get offline notes with a certain publish state (Personal, Site or Course). + * + * @param {string} state Publish state ('personal', 'site' or 'course'). + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with notes. + */ + getNotesWithPublishState(state: string, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecords(this.NOTES_TABLE, {publishstate: state}); + }); + } + + /** + * Check if there are offline notes for a certain course. + * + * @param {number} courseId Course ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with boolean: true if has offline notes, false otherwise. + */ + hasNotesForCourse(courseId: number, siteId?: string): Promise { + return this.getNotesForCourse(courseId, siteId).then((notes) => { + return !!notes.length; + }); + } + + /** + * Check if there are offline notes for a certain user. + * + * @param {number} userId User ID the notes are about. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with boolean: true if has offline notes, false otherwise. + */ + hasNotesForUser(userId: number, siteId?: string): Promise { + return this.getNotesForUser(userId, siteId).then((notes) => { + return !!notes.length; + }); + } + + /** + * Check if there are offline notes with a certain publish state (Personal, Site or Course). + * + * @param {string} state Publish state ('personal', 'site' or 'course'). + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with boolean: true if has offline notes, false otherwise. + */ + hasNotesWithPublishState(state: string, siteId?: string): Promise { + return this.getNotesWithPublishState(state, siteId).then((notes) => { + return !!notes.length; + }); + } + + /** + * Save a note to be sent later. + * + * @param {number} userId User ID the note is about. + * @param {number} courseId Course ID. + * @param {string} state Publish state ('personal', 'site' or 'course'). + * @param {string} content The note content. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if stored, rejected if failure. + */ + saveNote(userId: number, courseId: number, state: string, content: string, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const now = this.timeUtils.timestamp(); + const data = { + userid: userId, + courseid: courseId, + publishstate: state, + content: content, + format: 1, + created: now, + lastmodified: now + }; + const conditions = { + userid: userId, + content: content, + created: now + }; + + return site.getDb().insertOrUpdateRecord(this.NOTES_TABLE, data, conditions).then(() => { + return data; + }); + }); + } +} diff --git a/src/addon/notes/providers/notes-sync.ts b/src/addon/notes/providers/notes-sync.ts new file mode 100644 index 000000000..d443385db --- /dev/null +++ b/src/addon/notes/providers/notes-sync.ts @@ -0,0 +1,198 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreSyncBaseProvider } from '@classes/base-sync'; +import { CoreAppProvider } from '@providers/app'; +import { AddonNotesOfflineProvider } from './notes-offline'; +import { AddonNotesProvider } from './notes'; +import { CoreCoursesProvider } from '@core/courses/providers/courses'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreSyncProvider } from '@providers/sync'; + +/** + * Service to sync notes. + */ +@Injectable() +export class AddonNotesSyncProvider extends CoreSyncBaseProvider { + + static AUTO_SYNCED = 'addon_notes_autom_synced'; + + constructor(protected sitesProvider: CoreSitesProvider, protected loggerProvider: CoreLoggerProvider, + protected appProvider: CoreAppProvider, private notesOffline: AddonNotesOfflineProvider, + private eventsProvider: CoreEventsProvider, private notesProvider: AddonNotesProvider, + private coursesProvider: CoreCoursesProvider, private translate: TranslateService, private utils: CoreUtilsProvider, + syncProvider: CoreSyncProvider, protected textUtils: CoreTextUtilsProvider) { + super('AddonNotesSync', sitesProvider, loggerProvider, appProvider, syncProvider, textUtils); + } + + /** + * Try to synchronize all the notes in a certain site or in all sites. + * + * @param {string} [siteId] Site ID to sync. If not defined, sync all sites. + * @return {Promise} Promise resolved if sync is successful, rejected if sync fails. + */ + syncAllNotes(siteId?: string): Promise { + return this.syncOnSites('all notes', this.syncAllNotesFunc.bind(this), [], siteId); + } + + /** + * Synchronize all the notes in a certain site + * + * @param {string} siteId Site ID to sync. + * @return {Promise} Promise resolved if sync is successful, rejected if sync fails. + */ + private syncAllNotesFunc(siteId: string): Promise { + return this.notesOffline.getAllNotes(siteId).then((notes) => { + // Get all the courses to be synced. + const courseIds = []; + notes.forEach((note) => { + if (courseIds.indexOf(note.courseid) == -1) { + courseIds.push(note.courseid); + } + }); + + // Sync all courses. + const promises = courseIds.map((courseId) => { + return this.syncNotesIfNeeded(courseId, siteId).then((warnings) => { + if (typeof warnings != 'undefined') { + // Sync successful, send event. + this.eventsProvider.trigger(AddonNotesSyncProvider.AUTO_SYNCED, { + courseId: courseId, + warnings: warnings + }, siteId); + } + }); + }); + + return Promise.all(promises); + }); + } + + /** + * Sync course notes only if a certain time has passed since the last time. + * + * @param {number} courseId Course ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the notes are synced or if they don't need to be synced. + */ + private syncNotesIfNeeded(courseId: number, siteId?: string): Promise { + return this.isSyncNeeded(courseId, siteId).then((needed) => { + if (needed) { + return this.syncNotes(courseId, siteId); + } + }); + } + + /** + * Synchronize notes of a course. + * + * @param {number} courseId Course ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if sync is successful, rejected otherwise. + */ + syncNotes(courseId: number, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.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 warnings = []; + + // Get offline notes to be sent. + const syncPromise = this.notesOffline.getNotesForCourse(courseId, siteId).then((notes) => { + if (!notes.length) { + // Nothing to sync. + return; + } else if (!this.appProvider.isOnline()) { + // Cannot sync in offline. + return Promise.reject(this.translate.instant('core.networkerrormsg')); + } + + const errors = []; + + // Format the notes to be sent. + const notesToSend = notes.map((note) => { + return { + userid: note.userid, + publishstate: note.publishstate, + courseid: note.courseid, + text: note.content, + format: note.format + }; + }); + + // Send the notes. + return this.notesProvider.addNotesOnline(notesToSend, siteId).then((response) => { + // Search errors in the response. + response.forEach((entry) => { + if (entry.noteid === -1 && errors.indexOf(entry.errormessage) == -1) { + errors.push(entry.errormessage); + } + }); + + // Fetch the notes from server to be sure they're up to date. + return this.notesProvider.invalidateNotes(courseId, siteId).then(() => { + return this.notesProvider.getNotes(courseId, false, true, siteId); + }).catch(() => { + // Ignore errors. + }); + }).catch((error) => { + if (this.utils.isWebServiceError(error)) { + // It's a WebService error, this means the user cannot send notes. + errors.push(error); + } else { + // Not a WebService error, reject the synchronization to try again. + return Promise.reject(error); + } + }).then(() => { + // Notes were sent, delete them from local DB. + const promises = notes.map((note) => { + return this.notesOffline.deleteNote(note.userid, note.content, note.created, siteId); + }); + + return Promise.all(promises); + }).then(() => { + if (errors && errors.length) { + // At least an error occurred, get course name and add errors to warnings array. + return this.coursesProvider.getUserCourse(courseId, true, siteId).catch(() => { + // Ignore errors. + return {}; + }).then((course) => { + errors.forEach((error) => { + warnings.push(this.translate.instant('addon.notes.warningnotenotsent', { + course: course.fullname ? course.fullname : courseId, + error: error + })); + }); + }); + } + }); + }).then(() => { + // All done, return the warnings. + return warnings; + }); + + return this.addOngoingSync(courseId, syncPromise, siteId); + } +} diff --git a/src/addon/notes/providers/notes.ts b/src/addon/notes/providers/notes.ts new file mode 100644 index 000000000..1f6e0199d --- /dev/null +++ b/src/addon/notes/providers/notes.ts @@ -0,0 +1,367 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { CoreAppProvider } from '@providers/app'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreSiteWSPreSets } from '@classes/site'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreUserProvider } from '@core/user/providers/user'; +import { AddonNotesOfflineProvider } from './notes-offline'; + +/** + * Service to handle notes. + */ +@Injectable() +export class AddonNotesProvider { + + protected ROOT_CACHE_KEY = 'mmaNotes:'; + protected logger; + + constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private appProvider: CoreAppProvider, + private utilsProvider: CoreUtilsProvider, private translate: TranslateService, private userProvider: CoreUserProvider, + private notesOffline: AddonNotesOfflineProvider) { + this.logger = logger.getInstance('AddonNotesProvider'); + } + + /** + * Add a note. + * + * @param {number} userId User ID of the person to add the note. + * @param {number} courseId Course ID where the note belongs. + * @param {string} publishState Personal, Site or Course. + * @param {string} noteText The note text. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with boolean: true if note was sent to server, false if stored in device. + */ + addNote(userId: number, courseId: number, publishState: string, noteText: string, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + // Convenience function to store a note to be synchronized later. + const storeOffline = (): Promise => { + return this.notesOffline.saveNote(userId, courseId, publishState, noteText, siteId).then(() => { + return false; + }); + }; + + if (!this.appProvider.isOnline()) { + // App is offline, store the note. + return storeOffline(); + } + + // Send note to server. + return this.addNoteOnline(userId, courseId, publishState, noteText, siteId).then(() => { + return true; + }).catch((data) => { + if (data.wserror) { + // It's a WebService error, the user cannot add the note so don't store it. + return Promise.reject(data.error); + } else { + // Error sending note, store it to retry later. + return storeOffline(); + } + }); + } + + /** + * Add a note. It will fail if offline or cannot connect. + * + * @param {number} userId User ID of the person to add the note. + * @param {number} courseId Course ID where the note belongs. + * @param {string} publishState Personal, Site or Course. + * @param {string} noteText The note text. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when added, rejected otherwise. Reject param is an object with: + * - error: The error message. + * - wserror: True if it's an error returned by the WebService, false otherwise. + */ + addNoteOnline(userId: number, courseId: number, publishState: string, noteText: string, siteId?: string): Promise { + const notes = [ + { + courseid: courseId, + format: 1, + publishstate: publishState, + text: noteText, + userid: userId + } + ]; + + return this.addNotesOnline(notes, siteId).catch((error) => { + return Promise.reject({ + error: error, + wserror: this.utilsProvider.isWebServiceError(error) + }); + }).then((response) => { + if (response && response[0] && response[0].noteid === -1) { + // There was an error, and it should be translated already. + return Promise.reject({ + error: response[0].errormessage, + wserror: true + }); + } + + // A note was added, invalidate the course notes. + return this.invalidateNotes(courseId, siteId).catch(() => { + // Ignore errors. + }); + }); + } + + /** + * Add several notes. It will fail if offline or cannot connect. + * + * @param {any[]} notes Notes to save. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} 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. + */ + addNotesOnline(notes: any[], siteId?: string): Promise { + if (!notes || !notes.length) { + return Promise.resolve(); + } + + return this.sitesProvider.getSite(siteId).then((site) => { + const data = { + notes: notes + }; + + return site.write('core_notes_create_notes', data); + }); + } + + /** + * Returns whether or not the add note 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 {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with true if enabled, resolved with false or rejected otherwise. + */ + isPluginAddNoteEnabled(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + if (!site.canUseAdvancedFeature('enablenotes')) { + return false; + } else if (!site.wsAvailable('core_notes_create_notes')) { + return false; + } + + return true; + }); + } + + /** + * Returns whether or not the add note plugin is enabled for a certain course. + * + * @param {number} courseId ID of the course. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with true if enabled, resolved with false or rejected otherwise. + */ + isPluginAddNoteEnabledForCourse(courseId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + // 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 data = { + notes: [ + { + userid: -1, + publishstate: 'personal', + courseid: courseId, + text: '', + format: 1 + } + ] + }; + + /* 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 site.read('core_notes_create_notes', data).then(() => { + // User can add notes. + return true; + }).catch(() => { + return false; + }); + }); + } + + /** + * Returns whether or not the read notes plugin is enabled for the current site. + * + * This method is called quite often and thus should only perform a quick + * check, we should not be calling WS from here. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with true if enabled, resolved with false or rejected otherwise. + */ + isPluginViewNotesEnabled(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + if (!site.canUseAdvancedFeature('enablenotes')) { + return false; + } else if (!site.wsAvailable('core_notes_get_course_notes')) { + return false; + } + + return true; + }); + } + + /** + * Returns whether or not the read notes plugin is enabled for a certain course. + * + * @param {number} courseId ID of the course. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with true if enabled, resolved with false or rejected otherwise. + */ + isPluginViewNotesEnabledForCourse(courseId: number, siteId?: string): Promise { + return this.getNotes(courseId, false, true, siteId).then(() => { + return true; + }).catch(() => { + return false; + }); + } + + /** + * Get the cache key for the get notes call. + * + * @param {number} courseId ID of the course to get the notes from. + * @return {string} Cache key. + */ + getNotesCacheKey(courseId: number): string { + return this.ROOT_CACHE_KEY + 'notes:' + courseId; + } + + /** + * Get users notes for a certain site, course and personal notes. + * + * @param {number} courseId ID of the course to get the notes from. + * @param {boolean} [ignoreCache] True when we should not get the value from the cache. + * @param {boolean} [onlyOnline] True to return only online notes, false to return both online and offline. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise to be resolved when the notes are retrieved. + */ + getNotes(courseId: number, ignoreCache?: boolean, onlyOnline?: boolean, siteId?: string): Promise { + this.logger.debug('Get notes for course ' + courseId); + + return this.sitesProvider.getSite(siteId).then((site) => { + const data = { + courseid: courseId + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getNotesCacheKey(courseId) + }; + + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + + return site.read('core_notes_get_course_notes', data, preSets).then((notes) => { + if (onlyOnline) { + return notes; + } + + // Get offline notes and add them to the list. + return this.notesOffline.getNotesForCourse(courseId, siteId).then((offlineNotes) => { + offlineNotes.forEach((note) => { + 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 user data for notes since they only have userid. + * + * @param {any[]} notes Notes to get the data for. + * @param {number} courseId ID of the course the notes belong to. + * @return {Promise} Promise always resolved. Resolve param is the formatted notes. + */ + getNotesUserData(notes: any[], courseId: number): Promise { + const promises = notes.map((note) => { + // Get the user profile to retrieve the user image. + return this.userProvider.getProfile(note.userid, note.courseid, true).then((user) => { + note.userfullname = user.fullname; + note.userprofileimageurl = user.profileimageurl || null; + }).catch(() => { + note.userfullname = this.translate.instant('addon.notes.userwithid', {id: note.userid}); + }); + }); + + return Promise.all(promises).then(() => { + return notes; + }); + } + + /** + * Given a list of notes, check if any of them is an offline note. + * + * @param {any[]} notes List of notes. + * @return {boolean} True if at least 1 note is offline, false otherwise. + */ + hasOfflineNote(notes: any[]): boolean { + if (!notes || !notes.length) { + return false; + } + + for (let i = 0, len = notes.length; i < len; i++) { + if (notes[i].offline) { + return true; + } + } + + return false; + } + + /** + * Invalidate get notes WS call. + * + * @param {number} courseId Course ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when data is invalidated. + */ + invalidateNotes(courseId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getNotesCacheKey(courseId)); + }); + } + + /** + * Report notes as being viewed. + * + * @param {number} courseId ID of the course. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the WS call is successful. + */ + logView(courseId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + courseid: courseId, + userid: 0 + }; + + return site.write('core_notes_view_notes', params); + }); + } +} diff --git a/src/addon/notes/providers/sync-cron-handler.ts b/src/addon/notes/providers/sync-cron-handler.ts new file mode 100644 index 000000000..b0e1e5da1 --- /dev/null +++ b/src/addon/notes/providers/sync-cron-handler.ts @@ -0,0 +1,47 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { CoreCronHandler } from '@providers/cron'; +import { AddonNotesSyncProvider } from './notes-sync'; + +/** + * Synchronization cron handler. + */ +@Injectable() +export class AddonNotesSyncCronHandler implements CoreCronHandler { + name = 'AddonNotesSyncCronHandler'; + + constructor(private notesSync: AddonNotesSyncProvider) {} + + /** + * Execute the process. + * Receives the ID of the site affected, undefined for all sites. + * + * @param {string} [siteId] ID of the site affected, undefined for all sites. + * @return {Promise} Promise resolved when done, rejected if failure. + */ + execute(siteId?: string): Promise { + return this.notesSync.syncAllNotes(siteId); + } + + /** + * Get the time between consecutive executions. + * + * @return {number} Time between consecutive executions (in ms). + */ + getInterval(): number { + return 600000; // 10 minutes. + } +} diff --git a/src/addon/notes/providers/user-handler.ts b/src/addon/notes/providers/user-handler.ts new file mode 100644 index 000000000..97478efe1 --- /dev/null +++ b/src/addon/notes/providers/user-handler.ts @@ -0,0 +1,101 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { ModalController } from 'ionic-angular'; +import { CoreUserDelegate, CoreUserProfileHandler, CoreUserProfileHandlerData } from '@core/user/providers/user-delegate'; +import { CoreSitesProvider } from '@providers/sites'; +import { AddonNotesProvider } from './notes'; + +/** + * Profile notes handler. + */ +@Injectable() +export class AddonNotesUserHandler implements CoreUserProfileHandler { + name = 'AddonNotes'; + priority = 200; + type = CoreUserDelegate.TYPE_COMMUNICATION; + addNoteEnabledCache = {}; + + constructor(private modalCtrl: ModalController, private sitesProvider: CoreSitesProvider, + private notesProvider: AddonNotesProvider) { + } + + /** + * Clear add note cache. + * If a courseId is specified, it will only delete the entry for that course. + * + * @param {number} [courseId] Course ID. + */ + clearAddNoteCache(courseId?: number): void { + if (courseId) { + delete this.addNoteEnabledCache[courseId]; + } else { + this.addNoteEnabledCache = {}; + } + } + + /** + * Whether or not the handler is enabled on a site level. + * @return {boolean|Promise} Whether or not the handler is enabled on a site level. + */ + isEnabled(): boolean | Promise { + return this.notesProvider.isPluginAddNoteEnabled(); + } + + /** + * Check if handler is enabled for this user in this context. + * + * @param {any} user User to check. + * @param {number} courseId Course ID. + * @param {any} [navOptions] Course navigation options for current user. See $mmCourses#getUserNavigationOptions. + * @param {any} [admOptions] Course admin options for current user. See $mmCourses#getUserAdministrationOptions. + * @return {boolean|Promise} Promise resolved with true if enabled, resolved with false otherwise. + */ + isEnabledForUser(user: any, courseId: number, navOptions?: any, admOptions?: any): boolean | Promise { + // Active course required. + if (!courseId || user.id == this.sitesProvider.getCurrentSiteUserId()) { + return Promise.resolve(false); + } + + if (typeof this.addNoteEnabledCache[courseId] != 'undefined') { + return this.addNoteEnabledCache[courseId]; + } + + return this.notesProvider.isPluginAddNoteEnabledForCourse(courseId).then((enabled) => { + this.addNoteEnabledCache[courseId] = enabled; + + return enabled; + }); + } + + /** + * Returns the data needed to render the handler. + * + * @return {CoreUserProfileHandlerData} Data needed to render the handler. + */ + getDisplayData(user: any, courseId: number): CoreUserProfileHandlerData { + return { + icon: 'list', + title: 'addon.notes.addnewnote', + class: 'addon-notes-handler', + action: (event, navCtrl, user, courseId): void => { + event.preventDefault(); + event.stopPropagation(); + const modal = this.modalCtrl.create('AddonNotesAddPage', { userId: user.id, courseId }); + modal.present(); + } + }; + } +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index eaf546c1a..a8b38ffc0 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -79,6 +79,7 @@ import { AddonModLabelModule } from '@addon/mod/label/label.module'; import { AddonModResourceModule } from '@addon/mod/resource/resource.module'; import { AddonModFolderModule } from '@addon/mod/folder/folder.module'; import { AddonMessagesModule } from '@addon/messages/messages.module'; +import { AddonNotesModule } from '../addon/notes/notes.module'; import { AddonPushNotificationsModule } from '@addon/pushnotifications/pushnotifications.module'; import { AddonRemoteThemesModule } from '@addon/remotethemes/remotethemes.module'; @@ -161,6 +162,7 @@ export const CORE_PROVIDERS: any[] = [ AddonModResourceModule, AddonModFolderModule, AddonMessagesModule, + AddonNotesModule, AddonPushNotificationsModule, AddonRemoteThemesModule ], diff --git a/src/classes/base-sync.ts b/src/classes/base-sync.ts index 654b3bba8..aa413dbcc 100644 --- a/src/classes/base-sync.ts +++ b/src/classes/base-sync.ts @@ -151,11 +151,11 @@ export class CoreSyncBaseProvider { /** * Check if a sync is needed: if a certain time has passed since the last time. * - * @param {string} id Unique sync identifier per component. + * @param {string | number} id Unique sync identifier per component. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved with boolean: whether sync is needed. */ - isSyncNeeded(id: string, siteId?: string): Promise { + isSyncNeeded(id: string | number, siteId?: string): Promise { return this.getSyncTime(id, siteId).then((time) => { return Date.now() - this.syncInterval >= time; }); @@ -178,12 +178,12 @@ export class CoreSyncBaseProvider { /** * Set the synchronization warnings. * - * @param {string} id Unique sync identifier per component. + * @param {string | number} id Unique sync identifier per component. * @param {string[]} warnings Warnings to set. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when done. */ - setSyncWarnings(id: string, warnings: string[], siteId?: string): Promise { + setSyncWarnings(id: string | number, warnings: string[], siteId?: string): Promise { const warningsText = JSON.stringify(warnings || []); return this.syncProvider.insertOrUpdateSyncRecord(this.component, id, { warnings: warningsText }, siteId); @@ -245,5 +245,5 @@ export class CoreSyncBaseProvider { } return Promise.resolve(); -} + } } From d8be7efbc62c0cf6c911308eaecb75ae49aa8d3c Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 20 Mar 2018 13:03:35 +0100 Subject: [PATCH 2/8] MOBILE-2319 contextmenu: Don't merge menus from different tabs --- src/components/context-menu/context-menu.ts | 5 +++-- src/components/navbar-buttons/navbar-buttons.ts | 8 +++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/components/context-menu/context-menu.ts b/src/components/context-menu/context-menu.ts index 630b44f4c..3cf987667 100644 --- a/src/components/context-menu/context-menu.ts +++ b/src/components/context-menu/context-menu.ts @@ -12,12 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Input, OnInit, OnDestroy, ElementRef } from '@angular/core'; +import { Component, Input, OnInit, OnDestroy, ElementRef, Optional } from '@angular/core'; import { PopoverController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreContextMenuItemComponent } from './context-menu-item'; import { CoreContextMenuPopoverComponent } from './context-menu-popover'; +import { CoreTabComponent } from '@components/tabs/tab'; import { Subject } from 'rxjs'; /** @@ -40,7 +41,7 @@ export class CoreContextMenuComponent implements OnInit, OnDestroy { protected parentContextMenu: CoreContextMenuComponent; constructor(private translate: TranslateService, private popoverCtrl: PopoverController, elementRef: ElementRef, - private domUtils: CoreDomUtilsProvider) { + private domUtils: CoreDomUtilsProvider, @Optional() public coreTab: CoreTabComponent) { // Create the stream and subscribe to it. We ignore successive changes during 250ms. this.itemsChangedStream = new Subject(); this.itemsChangedStream.auditTime(250).subscribe(() => { diff --git a/src/components/navbar-buttons/navbar-buttons.ts b/src/components/navbar-buttons/navbar-buttons.ts index d682248a6..7f4ae239b 100644 --- a/src/components/navbar-buttons/navbar-buttons.ts +++ b/src/components/navbar-buttons/navbar-buttons.ts @@ -134,10 +134,12 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy { } // Both containers have a context menu. Merge them to prevent having 2 menus at the same time. - const mainContextMenuInstance = this.domUtils.getInstanceByElement(mainContextMenu), - secondaryContextMenuInstance = this.domUtils.getInstanceByElement(secondaryContextMenu); + const mainContextMenuInstance: CoreContextMenuComponent = this.domUtils.getInstanceByElement(mainContextMenu), + secondaryContextMenuInstance: CoreContextMenuComponent = this.domUtils.getInstanceByElement(secondaryContextMenu); - if (mainContextMenuInstance && secondaryContextMenuInstance) { + // Check that both context menus belong to the same core-tab. We shouldn't merge menus from different tabs. + if (mainContextMenuInstance && secondaryContextMenuInstance && + mainContextMenuInstance.coreTab === secondaryContextMenuInstance.coreTab) { this.mergedContextMenu = secondaryContextMenuInstance; this.mergedContextMenu.mergeContextMenus(mainContextMenuInstance); From 8dc255ea2076769343244a80b14656fd113cec60 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 20 Mar 2018 14:53:25 +0100 Subject: [PATCH 3/8] MOBILE-2319 push: Fix pushID of the device --- src/addon/pushnotifications/providers/pushnotifications.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/addon/pushnotifications/providers/pushnotifications.ts b/src/addon/pushnotifications/providers/pushnotifications.ts index 9482de788..9922269bb 100644 --- a/src/addon/pushnotifications/providers/pushnotifications.ts +++ b/src/addon/pushnotifications/providers/pushnotifications.ts @@ -326,8 +326,8 @@ export class AddonPushNotificationsProvider { this.onMessageReceived(notification); }); - pushObject.on('registration').subscribe((registrationId: any) => { - this.pushID = registrationId; + pushObject.on('registration').subscribe((data: any) => { + this.pushID = data.registrationId; this.registerDeviceOnMoodle().catch((error) => { this.logger.warn('Can\'t register device', error); }); From a6ea996778c9567fa2a28a6e882aa4e9b26451f0 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 20 Mar 2018 15:44:26 +0100 Subject: [PATCH 4/8] MOBILE-2319 db: Use REPLACE in inserts --- src/addon/calendar/providers/calendar.ts | 4 ++-- .../messages/providers/messages-offline.ts | 12 ++--------- src/addon/notes/providers/notes-offline.ts | 7 +------ .../providers/pushnotifications.ts | 2 +- src/classes/site.ts | 2 +- src/classes/sqlitedb.ts | 20 +------------------ src/core/course/providers/course.ts | 2 +- .../providers/module-prefetch-delegate.ts | 4 +++- .../emulator/providers/local-notifications.ts | 2 +- src/core/user/providers/user.ts | 2 +- src/providers/config.ts | 2 +- src/providers/cron.ts | 2 +- src/providers/filepool.ts | 6 +++--- src/providers/local-notifications.ts | 2 +- src/providers/sites.ts | 4 ++-- src/providers/sync.ts | 2 +- 16 files changed, 23 insertions(+), 52 deletions(-) diff --git a/src/addon/calendar/providers/calendar.ts b/src/addon/calendar/providers/calendar.ts index 741160a88..318800396 100644 --- a/src/addon/calendar/providers/calendar.ts +++ b/src/addon/calendar/providers/calendar.ts @@ -514,7 +514,7 @@ export class AddonCalendarProvider { notificationtime: e.notificationtime || -1 }; - return db.insertOrUpdateRecord(this.EVENTS_TABLE, eventRecord, { id: eventRecord.id }); + return db.insertRecord(this.EVENTS_TABLE, eventRecord); })); }); @@ -539,7 +539,7 @@ export class AddonCalendarProvider { event.notificationtime = time; - return site.getDb().insertOrUpdateRecord(this.EVENTS_TABLE, event, { id: event.id }).then(() => { + return site.getDb().insertRecord(this.EVENTS_TABLE, event).then(() => { return this.scheduleEventNotification(event, time); }); }); diff --git a/src/addon/messages/providers/messages-offline.ts b/src/addon/messages/providers/messages-offline.ts index ee8c5a7dd..a2243f6b9 100644 --- a/src/addon/messages/providers/messages-offline.ts +++ b/src/addon/messages/providers/messages-offline.ts @@ -148,11 +148,7 @@ export class AddonMessagesOfflineProvider { deviceoffline: this.appProvider.isOnline() ? 0 : 1 }; - return site.getDb().insertOrUpdateRecord(this.MESSAGES_TABLE, entry, { - touserid: toUserId, - smallmessage: message, - timecreated: entry.timecreated - }).then(() => { + return site.getDb().insertRecord(this.MESSAGES_TABLE, entry).then(() => { return entry; }); }); @@ -173,11 +169,7 @@ export class AddonMessagesOfflineProvider { data = { deviceoffline: value ? 1 : 0 }; messages.forEach((message) => { - promises.push(db.insertOrUpdateRecord(this.MESSAGES_TABLE, data, { - touserid: message.touserid, - smallmessage: message.smallmessage, - timecreated: message.timecreated - })); + promises.push(db.insertRecord(this.MESSAGES_TABLE, data)); }); return Promise.all(promises); diff --git a/src/addon/notes/providers/notes-offline.ts b/src/addon/notes/providers/notes-offline.ts index c285c7428..597458ef8 100644 --- a/src/addon/notes/providers/notes-offline.ts +++ b/src/addon/notes/providers/notes-offline.ts @@ -218,13 +218,8 @@ export class AddonNotesOfflineProvider { created: now, lastmodified: now }; - const conditions = { - userid: userId, - content: content, - created: now - }; - return site.getDb().insertOrUpdateRecord(this.NOTES_TABLE, data, conditions).then(() => { + return site.getDb().insertRecord(this.NOTES_TABLE, data).then(() => { return data; }); }); diff --git a/src/addon/pushnotifications/providers/pushnotifications.ts b/src/addon/pushnotifications/providers/pushnotifications.ts index 9922269bb..eec83e6ff 100644 --- a/src/addon/pushnotifications/providers/pushnotifications.ts +++ b/src/addon/pushnotifications/providers/pushnotifications.ts @@ -402,7 +402,7 @@ export class AddonPushNotificationsProvider { number: value }; - return this.appDB.insertOrUpdateRecord(this.BADGE_TABLE, entry, {siteid: siteId, addon: addon}).then(() => { + return this.appDB.insertRecord(this.BADGE_TABLE, entry).then(() => { return value; }); } diff --git a/src/classes/site.ts b/src/classes/site.ts index 330b59ea3..3846ae746 100644 --- a/src/classes/site.ts +++ b/src/classes/site.ts @@ -780,7 +780,7 @@ export class CoreSite { entry.key = preSets.cacheKey; } - return this.db.insertOrUpdateRecord(this.WS_CACHE_TABLE, entry, { id: id }); + return this.db.insertRecord(this.WS_CACHE_TABLE, entry); }); } diff --git a/src/classes/sqlitedb.ts b/src/classes/sqlitedb.ts index efef5527d..fdf17f59b 100644 --- a/src/classes/sqlitedb.ts +++ b/src/classes/sqlitedb.ts @@ -625,7 +625,7 @@ export class SQLiteDB { questionMarks = ',?'.repeat(keys.length).substr(1); return [ - `INSERT INTO ${table} (${fields}) VALUES (${questionMarks})`, + `INSERT OR REPLACE INTO ${table} (${fields}) VALUES (${questionMarks})`, keys.map((key) => data[key]) ]; } @@ -644,24 +644,6 @@ export class SQLiteDB { }); } - /** - * Insert or update a record. - * - * @param {string} table The database table. - * @param {object} data An object with the fields to insert/update: fieldname=>fieldvalue. - * @param {object} conditions The conditions to check if the record already exists (and to update it). - * @return {Promise} Promise resolved with done. - */ - insertOrUpdateRecord(table: string, data: object, conditions: object): Promise { - return this.getRecord(table, conditions).then(() => { - // It exists, update it. - return this.updateRecords(table, data, conditions); - }).catch(() => { - // Doesn't exist, insert it. - return this.insertRecord(table, data); - }); - } - /** * Insert a record into a table and return the "rowId" field. * diff --git a/src/core/course/providers/course.ts b/src/core/course/providers/course.ts index 083ed039f..a491ab610 100644 --- a/src/core/course/providers/course.ts +++ b/src/core/course/providers/course.ts @@ -699,7 +699,7 @@ export class CoreCourseProvider { previousDownloadTime: previousDownloadTime }; - return site.getDb().insertOrUpdateRecord(this.COURSE_STATUS_TABLE, data, { id: courseId }); + return site.getDb().insertRecord(this.COURSE_STATUS_TABLE, data); } }).then(() => { // Success inserting, trigger event. diff --git a/src/core/course/providers/module-prefetch-delegate.ts b/src/core/course/providers/module-prefetch-delegate.ts index 84f4b0b15..0fac45323 100644 --- a/src/core/course/providers/module-prefetch-delegate.ts +++ b/src/core/course/providers/module-prefetch-delegate.ts @@ -417,7 +417,9 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate { courseId: courseId, time: this.timeUtils.timestamp() }; - site.getDb().insertOrUpdateRecord(this.CHECK_UPDATES_TIMES_TABLE, entry, { courseId: courseId }); + site.getDb().insertRecord(this.CHECK_UPDATES_TIMES_TABLE, entry).catch(() => { + // Ignore errors. + }); return this.treatCheckUpdatesResult(data.toCheck, response, result); }).catch((error) => { diff --git a/src/core/emulator/providers/local-notifications.ts b/src/core/emulator/providers/local-notifications.ts index f59120623..50f9ea52a 100644 --- a/src/core/emulator/providers/local-notifications.ts +++ b/src/core/emulator/providers/local-notifications.ts @@ -671,7 +671,7 @@ export class LocalNotificationsMock extends LocalNotifications { notification = Object.assign({}, notification); // Clone the object. notification.triggered = !!triggered; - return this.appDB.insertOrUpdateRecord(this.DESKTOP_NOTIFS_TABLE, notification, { id: notification.id }); + return this.appDB.insertRecord(this.DESKTOP_NOTIFS_TABLE, notification); } /** diff --git a/src/core/user/providers/user.ts b/src/core/user/providers/user.ts index 128f9fc93..7dfc1ae0e 100644 --- a/src/core/user/providers/user.ts +++ b/src/core/user/providers/user.ts @@ -383,7 +383,7 @@ export class CoreUserProvider { profileimageurl: avatar }; - return site.getDb().insertOrUpdateRecord(this.USERS_TABLE, userRecord, { id: userId }); + return site.getDb().insertRecord(this.USERS_TABLE, userRecord); }); } diff --git a/src/providers/config.ts b/src/providers/config.ts index 554ee3b21..c34166080 100644 --- a/src/providers/config.ts +++ b/src/providers/config.ts @@ -81,6 +81,6 @@ export class CoreConfigProvider { * @return {Promise} Promise resolved when done. */ set(name: string, value: boolean | number | string): Promise { - return this.appDB.insertOrUpdateRecord(this.TABLE_NAME, { name: name, value: value }, { name: name }); + return this.appDB.insertRecord(this.TABLE_NAME, { name: name, value: value }); } } diff --git a/src/providers/cron.ts b/src/providers/cron.ts index 930d324a2..4aa968ded 100644 --- a/src/providers/cron.ts +++ b/src/providers/cron.ts @@ -459,7 +459,7 @@ export class CoreCronDelegate { value: time }; - return this.appDB.insertOrUpdateRecord(this.CRON_TABLE, entry, { id: id }); + return this.appDB.insertRecord(this.CRON_TABLE, entry); } /** diff --git a/src/providers/filepool.ts b/src/providers/filepool.ts index bb1ded4a5..115964a7c 100644 --- a/src/providers/filepool.ts +++ b/src/providers/filepool.ts @@ -477,7 +477,7 @@ export class CoreFilepoolProvider { componentId: componentId || '' }; - return db.insertOrUpdateRecord(this.LINKS_TABLE, newEntry, { fileId: fileId }); + return db.insertRecord(this.LINKS_TABLE, newEntry); }); } @@ -544,7 +544,7 @@ export class CoreFilepoolProvider { values.fileId = fileId; return this.sitesProvider.getSiteDb(siteId).then((db) => { - return db.insertOrUpdateRecord(this.FILES_TABLE, values, { fileId: fileId }); + return db.insertRecord(this.FILES_TABLE, values); }); } @@ -2766,7 +2766,7 @@ export class CoreFilepoolProvider { // The package already has this status, no need to change it. promise = Promise.resolve(); } else { - promise = site.getDb().insertOrUpdateRecord(this.PACKAGES_TABLE, packageEntry, { id: packageId }); + promise = site.getDb().insertRecord(this.PACKAGES_TABLE, packageEntry); } return promise.then(() => { diff --git a/src/providers/local-notifications.ts b/src/providers/local-notifications.ts index c3339dfcf..4d0541514 100644 --- a/src/providers/local-notifications.ts +++ b/src/providers/local-notifications.ts @@ -500,6 +500,6 @@ export class CoreLocalNotificationsProvider { at: parseInt(notification.at, 10) }; - return this.appDB.insertOrUpdateRecord(this.TRIGGERED_TABLE, entry, { id: notification.id }); + return this.appDB.insertRecord(this.TRIGGERED_TABLE, entry); } } diff --git a/src/providers/sites.ts b/src/providers/sites.ts index 53154c12a..ac30cd79f 100644 --- a/src/providers/sites.ts +++ b/src/providers/sites.ts @@ -623,7 +623,7 @@ export class CoreSitesProvider { loggedOut: 0 }; - return this.appDB.insertOrUpdateRecord(this.SITES_TABLE, entry, { id: id }); + return this.appDB.insertRecord(this.SITES_TABLE, entry); } /** @@ -895,7 +895,7 @@ export class CoreSitesProvider { siteId: siteId }; - return this.appDB.insertOrUpdateRecord(this.CURRENT_SITE_TABLE, entry, { id: 1 }).then(() => { + return this.appDB.insertRecord(this.CURRENT_SITE_TABLE, entry).then(() => { this.eventsProvider.trigger(CoreEventsProvider.LOGIN, {}, siteId); }); } diff --git a/src/providers/sync.ts b/src/providers/sync.ts index 2003b488d..9137e997f 100644 --- a/src/providers/sync.ts +++ b/src/providers/sync.ts @@ -142,7 +142,7 @@ export class CoreSyncProvider { data.component = component; data.id = id; - return db.insertOrUpdateRecord(this.SYNC_TABLE, data, { component: component, id: id }); + return db.insertRecord(this.SYNC_TABLE, data); }); } From a0c57f46c033aaf7949ac6bca5924bbfae054437 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Wed, 21 Mar 2018 15:50:05 +0100 Subject: [PATCH 5/8] MOBILE-2319 notes: PR fixes --- src/addon/notes/notes.module.ts | 20 +--- src/addon/notes/pages/add/add.html | 2 +- src/addon/notes/pages/list/list.html | 7 +- src/addon/notes/pages/list/list.ts | 11 ++- .../notes/providers/course-option-handler.ts | 20 +--- src/addon/notes/providers/notes.ts | 98 +++---------------- src/addon/notes/providers/user-handler.ts | 12 ++- 7 files changed, 40 insertions(+), 130 deletions(-) diff --git a/src/addon/notes/notes.module.ts b/src/addon/notes/notes.module.ts index 4ea5dd0a8..7dd83dc35 100644 --- a/src/addon/notes/notes.module.ts +++ b/src/addon/notes/notes.module.ts @@ -22,11 +22,7 @@ import { AddonNotesUserHandler } from './providers/user-handler'; import { AddonNotesComponentsModule } from './components/components.module'; import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delegate'; import { CoreCronDelegate } from '@providers/cron'; -import { CoreCoursesProvider } from '@core/courses/providers/courses'; -import { CoreEventsProvider } from '@providers/events'; -import { CoreSitesProvider } from '@providers/sites'; import { CoreUserDelegate } from '@core/user/providers/user-delegate'; -import { CoreUserProvider } from '@core/user/providers/user'; @NgModule({ declarations: [ @@ -45,23 +41,11 @@ import { CoreUserProvider } from '@core/user/providers/user'; }) export class AddonNotesModule { constructor(courseOptionsDelegate: CoreCourseOptionsDelegate, courseOptionHandler: AddonNotesCourseOptionHandler, - userDelegate: CoreUserDelegate, userHandler: AddonNotesUserHandler, cronDelegate: CoreCronDelegate, - syncHandler: AddonNotesSyncCronHandler, eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider) { + userDelegate: CoreUserDelegate, userHandler: AddonNotesUserHandler, + cronDelegate: CoreCronDelegate, syncHandler: AddonNotesSyncCronHandler) { // Register handlers. courseOptionsDelegate.registerHandler(courseOptionHandler); userDelegate.registerHandler(userHandler); cronDelegate.register(syncHandler); - - eventsProvider.on(CoreEventsProvider.LOGOUT, () => { - courseOptionHandler.clearCoursesNavCache(); - }, sitesProvider.getCurrentSiteId()); - - eventsProvider.on(CoreCoursesProvider.EVENT_MY_COURSES_REFRESHED, () => { - courseOptionHandler.clearCoursesNavCache(); - }, sitesProvider.getCurrentSiteId()); - - eventsProvider.on(CoreUserProvider.PROFILE_REFRESHED, () => { - userHandler.clearAddNoteCache(); - }, sitesProvider.getCurrentSiteId()); } } diff --git a/src/addon/notes/pages/add/add.html b/src/addon/notes/pages/add/add.html index 59b0bb32d..e0a41c7b1 100644 --- a/src/addon/notes/pages/add/add.html +++ b/src/addon/notes/pages/add/add.html @@ -21,7 +21,7 @@ - diff --git a/src/addon/notes/pages/list/list.html b/src/addon/notes/pages/list/list.html index 02d5f6084..1431ca259 100644 --- a/src/addon/notes/pages/list/list.html +++ b/src/addon/notes/pages/list/list.html @@ -17,9 +17,10 @@ -

- {{ 'core.thereisdatatosync' | translate:{$a: 'addon.notes.notes' | translate | lowercase } }} -

+
+ + {{ 'core.thereisdatatosync' | translate:{$a: 'addon.notes.notes' | translate | lowercase } }} +
diff --git a/src/addon/notes/pages/list/list.ts b/src/addon/notes/pages/list/list.ts index fcebe9a34..d64b578b9 100644 --- a/src/addon/notes/pages/list/list.ts +++ b/src/addon/notes/pages/list/list.ts @@ -76,10 +76,11 @@ export class AddonNotesListPage implements OnDestroy { } /** - * Fetch notes + * Fetch notes. + * * @param {boolean} sync When to resync notes. * @param {boolean} [showErrors] When to display errors or not. - * @return {Promise} Promise with the notes, + * @return {Promise} Promise with the notes. */ private fetchNotes(sync: boolean, showErrors?: boolean): Promise { const promise = sync ? this.syncNotes(showErrors) : Promise.resolve(); @@ -90,7 +91,7 @@ export class AddonNotesListPage implements OnDestroy { return this.notesProvider.getNotes(this.courseId).then((notes) => { notes = notes[this.type + 'notes'] || []; - this.hasOffline = this.notesProvider.hasOfflineNote(notes); + this.hasOffline = notes.some((note) => note.offline); return this.notesProvider.getNotesUserData(notes, this.courseId).then((notes) => { this.notes = notes; @@ -101,7 +102,7 @@ export class AddonNotesListPage implements OnDestroy { }).finally(() => { this.notesLoaded = true; this.refreshIcon = 'refresh'; - this.syncIcon = 'loop'; + this.syncIcon = 'sync'; }); } @@ -124,7 +125,7 @@ export class AddonNotesListPage implements OnDestroy { } /** - * Tries to syncrhonize course notes. + * Tries to synchronize course notes. * * @param {boolean} showErrors Whether to display errors or not. * @return {Promise} Promise resolved if sync is successful, rejected otherwise. diff --git a/src/addon/notes/providers/course-option-handler.ts b/src/addon/notes/providers/course-option-handler.ts index f71f9e097..8ab6dbbd1 100644 --- a/src/addon/notes/providers/course-option-handler.ts +++ b/src/addon/notes/providers/course-option-handler.ts @@ -25,24 +25,16 @@ import { AddonNotesTypesComponent } from '../components/types/types'; export class AddonNotesCourseOptionHandler implements CoreCourseOptionsHandler { name = 'AddonNotes'; priority = 200; - protected coursesNavEnabledCache = {}; constructor(private notesProvider: AddonNotesProvider) { } - /** - * Clear courses nav cache. - */ - clearCoursesNavCache(): void { - this.coursesNavEnabledCache = {}; - } - /** * Whether or not the handler is enabled on a site level. * @return {boolean|Promise} Whether or not the handler is enabled on a site level. */ isEnabled(): boolean | Promise { - return this.notesProvider.isPluginViewNotesEnabled(); + return this.notesProvider.isPluginEnabled(); } /** @@ -63,15 +55,7 @@ export class AddonNotesCourseOptionHandler implements CoreCourseOptionsHandler { return navOptions.notes; } - if (typeof this.coursesNavEnabledCache[courseId] != 'undefined') { - return this.coursesNavEnabledCache[courseId]; - } - - return this.notesProvider.isPluginViewNotesEnabledForCourse(courseId).then((enabled) => { - this.coursesNavEnabledCache[courseId] = enabled; - - return enabled; - }); + return this.notesProvider.isPluginViewNotesEnabledForCourse(courseId); } /** diff --git a/src/addon/notes/providers/notes.ts b/src/addon/notes/providers/notes.ts index 1f6e0199d..9e065ca01 100644 --- a/src/addon/notes/providers/notes.ts +++ b/src/addon/notes/providers/notes.ts @@ -32,7 +32,7 @@ export class AddonNotesProvider { protected logger; constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private appProvider: CoreAppProvider, - private utilsProvider: CoreUtilsProvider, private translate: TranslateService, private userProvider: CoreUserProvider, + private utils: CoreUtilsProvider, private translate: TranslateService, private userProvider: CoreUserProvider, private notesOffline: AddonNotesOfflineProvider) { this.logger = logger.getInstance('AddonNotesProvider'); } @@ -65,14 +65,14 @@ export class AddonNotesProvider { // Send note to server. return this.addNoteOnline(userId, courseId, publishState, noteText, siteId).then(() => { return true; - }).catch((data) => { - if (data.wserror) { - // It's a WebService error, the user cannot add the note so don't store it. - return Promise.reject(data.error); - } else { - // Error sending note, store it to retry later. - return storeOffline(); + }).catch((error) => { + if (this.utils.isWebServiceError(error)) { + // It's a WebService error, the user cannot send the message so don't store it. + return Promise.reject(error); } + + // Error sending note, store it to retry later. + return storeOffline(); }); } @@ -84,9 +84,7 @@ export class AddonNotesProvider { * @param {string} publishState Personal, Site or Course. * @param {string} noteText The note text. * @param {string} [siteId] Site ID. If not defined, current site. - * @return {Promise} Promise resolved when added, rejected otherwise. Reject param is an object with: - * - error: The error message. - * - wserror: True if it's an error returned by the WebService, false otherwise. + * @return {Promise} Promise resolved when added, rejected otherwise. */ addNoteOnline(userId: number, courseId: number, publishState: string, noteText: string, siteId?: string): Promise { const notes = [ @@ -99,18 +97,10 @@ export class AddonNotesProvider { } ]; - return this.addNotesOnline(notes, siteId).catch((error) => { - return Promise.reject({ - error: error, - wserror: this.utilsProvider.isWebServiceError(error) - }); - }).then((response) => { + return this.addNotesOnline(notes, siteId).then((response) => { if (response && response[0] && response[0].noteid === -1) { // There was an error, and it should be translated already. - return Promise.reject({ - error: response[0].errormessage, - wserror: true - }); + return Promise.reject(this.utils.createFakeWSError(response[0].errormessage)); } // A note was added, invalidate the course notes. @@ -143,7 +133,7 @@ export class AddonNotesProvider { } /** - * Returns whether or not the add note plugin is enabled for a certain site. + * 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. @@ -151,15 +141,9 @@ export class AddonNotesProvider { * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved with true if enabled, resolved with false or rejected otherwise. */ - isPluginAddNoteEnabled(siteId?: string): Promise { + isPluginEnabled(siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { - if (!site.canUseAdvancedFeature('enablenotes')) { - return false; - } else if (!site.wsAvailable('core_notes_create_notes')) { - return false; - } - - return true; + return site.canUseAdvancedFeature('enablenotes'); }); } @@ -188,33 +172,7 @@ export class AddonNotesProvider { /* 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 site.read('core_notes_create_notes', data).then(() => { - // User can add notes. - return true; - }).catch(() => { - return false; - }); - }); - } - - /** - * Returns whether or not the read notes plugin is enabled for the current site. - * - * This method is called quite often and thus should only perform a quick - * check, we should not be calling WS from here. - * - * @param {string} [siteId] Site ID. If not defined, current site. - * @return {Promise} Promise resolved with true if enabled, resolved with false or rejected otherwise. - */ - isPluginViewNotesEnabled(siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - if (!site.canUseAdvancedFeature('enablenotes')) { - return false; - } else if (!site.wsAvailable('core_notes_get_course_notes')) { - return false; - } - - return true; + return this.utils.promiseWorks(site.read('core_notes_create_notes', data)); }); } @@ -226,11 +184,7 @@ export class AddonNotesProvider { * @return {Promise} Promise resolved with true if enabled, resolved with false or rejected otherwise. */ isPluginViewNotesEnabledForCourse(courseId: number, siteId?: string): Promise { - return this.getNotes(courseId, false, true, siteId).then(() => { - return true; - }).catch(() => { - return false; - }); + return this.utils.promiseWorks(this.getNotes(courseId, false, true, siteId)); } /** @@ -314,26 +268,6 @@ export class AddonNotesProvider { }); } - /** - * Given a list of notes, check if any of them is an offline note. - * - * @param {any[]} notes List of notes. - * @return {boolean} True if at least 1 note is offline, false otherwise. - */ - hasOfflineNote(notes: any[]): boolean { - if (!notes || !notes.length) { - return false; - } - - for (let i = 0, len = notes.length; i < len; i++) { - if (notes[i].offline) { - return true; - } - } - - return false; - } - /** * Invalidate get notes WS call. * diff --git a/src/addon/notes/providers/user-handler.ts b/src/addon/notes/providers/user-handler.ts index 97478efe1..ad48c0f06 100644 --- a/src/addon/notes/providers/user-handler.ts +++ b/src/addon/notes/providers/user-handler.ts @@ -14,6 +14,8 @@ import { Injectable } from '@angular/core'; import { ModalController } from 'ionic-angular'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreUserProvider } from '@core/user/providers/user'; import { CoreUserDelegate, CoreUserProfileHandler, CoreUserProfileHandlerData } from '@core/user/providers/user-delegate'; import { CoreSitesProvider } from '@providers/sites'; import { AddonNotesProvider } from './notes'; @@ -29,7 +31,11 @@ export class AddonNotesUserHandler implements CoreUserProfileHandler { addNoteEnabledCache = {}; constructor(private modalCtrl: ModalController, private sitesProvider: CoreSitesProvider, - private notesProvider: AddonNotesProvider) { + private notesProvider: AddonNotesProvider, eventsProvider: CoreEventsProvider) { + eventsProvider.on(CoreEventsProvider.LOGOUT, this.clearAddNoteCache.bind(this)); + eventsProvider.on(CoreUserProvider.PROFILE_REFRESHED, (data) => { + this.clearAddNoteCache(data.courseId); + }); } /** @@ -38,7 +44,7 @@ export class AddonNotesUserHandler implements CoreUserProfileHandler { * * @param {number} [courseId] Course ID. */ - clearAddNoteCache(courseId?: number): void { + private clearAddNoteCache(courseId?: number): void { if (courseId) { delete this.addNoteEnabledCache[courseId]; } else { @@ -51,7 +57,7 @@ export class AddonNotesUserHandler implements CoreUserProfileHandler { * @return {boolean|Promise} Whether or not the handler is enabled on a site level. */ isEnabled(): boolean | Promise { - return this.notesProvider.isPluginAddNoteEnabled(); + return this.notesProvider.isPluginEnabled(); } /** From 4cefb204550a6527d2b9590beb73bb3dfc9b95f8 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Wed, 21 Mar 2018 16:10:59 +0100 Subject: [PATCH 6/8] MOBILE-2319 competency: Moved event handlers of delegate handlers --- src/addon/competency/competency.module.ts | 20 +------------------ .../providers/course-option-handler.ts | 18 +---------------- .../competency/providers/user-handler.ts | 8 ++++++-- 3 files changed, 8 insertions(+), 38 deletions(-) diff --git a/src/addon/competency/competency.module.ts b/src/addon/competency/competency.module.ts index 83ae4bd28..7d31bae23 100644 --- a/src/addon/competency/competency.module.ts +++ b/src/addon/competency/competency.module.ts @@ -22,10 +22,6 @@ import { AddonCompetencyComponentsModule } from './components/components.module' import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delegate'; import { CoreMainMenuDelegate } from '@core/mainmenu/providers/delegate'; import { CoreUserDelegate } from '@core/user/providers/user-delegate'; -import { CoreUserProvider } from '@core/user/providers/user'; -import { CoreEventsProvider } from '@providers/events'; -import { CoreSitesProvider } from '@providers/sites'; -import { CoreCoursesProvider } from '@core/courses/providers/courses'; @NgModule({ declarations: [ @@ -44,24 +40,10 @@ import { CoreCoursesProvider } from '@core/courses/providers/courses'; export class AddonCompetencyModule { constructor(mainMenuDelegate: CoreMainMenuDelegate, mainMenuHandler: AddonCompetencyMainMenuHandler, courseOptionsDelegate: CoreCourseOptionsDelegate, courseOptionHandler: AddonCompetencyCourseOptionHandler, - userDelegate: CoreUserDelegate, userHandler: AddonCompetencyUserHandler, - eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider) { + userDelegate: CoreUserDelegate, userHandler: AddonCompetencyUserHandler) { mainMenuDelegate.registerHandler(mainMenuHandler); courseOptionsDelegate.registerHandler(courseOptionHandler); userDelegate.registerHandler(userHandler); - - eventsProvider.on(CoreEventsProvider.LOGOUT, () => { - courseOptionHandler.clearCoursesNavCache(); - userHandler.clearUsersNavCache(); - }, sitesProvider.getCurrentSiteId()); - - eventsProvider.on(CoreCoursesProvider.EVENT_MY_COURSES_REFRESHED, () => { - courseOptionHandler.clearCoursesNavCache(); - }, sitesProvider.getCurrentSiteId()); - - eventsProvider.on(CoreUserProvider.PROFILE_REFRESHED, () => { - userHandler.clearUsersNavCache(); - }, sitesProvider.getCurrentSiteId()); } } diff --git a/src/addon/competency/providers/course-option-handler.ts b/src/addon/competency/providers/course-option-handler.ts index fe7bdd9d8..4aee5b3dc 100644 --- a/src/addon/competency/providers/course-option-handler.ts +++ b/src/addon/competency/providers/course-option-handler.ts @@ -26,17 +26,8 @@ export class AddonCompetencyCourseOptionHandler implements CoreCourseOptionsHand name = 'AddonCompetency'; priority = 700; - protected coursesNavEnabledCache = {}; - constructor(private competencyProvider: AddonCompetencyProvider) {} - /** - * Clear courses nav cache. - */ - clearCoursesNavCache(): void { - this.coursesNavEnabledCache = {}; - } - /** * Whether or not the handler is enabled ona site level. * @return {boolean|Promise} Whether or not the handler is enabled on a site level. @@ -63,15 +54,8 @@ export class AddonCompetencyCourseOptionHandler implements CoreCourseOptionsHand return navOptions.competencies; } - if (typeof this.coursesNavEnabledCache[courseId] != 'undefined') { - return this.coursesNavEnabledCache[courseId]; - } - return this.competencyProvider.isPluginForCourseEnabled(courseId).then((competencies) => { - const enabled = competencies ? !competencies.canmanagecoursecompetencies : false; - this.coursesNavEnabledCache[courseId] = enabled; - - return enabled; + return competencies ? !competencies.canmanagecoursecompetencies : false; }); } diff --git a/src/addon/competency/providers/user-handler.ts b/src/addon/competency/providers/user-handler.ts index a80b6fa57..ffc4a7c51 100644 --- a/src/addon/competency/providers/user-handler.ts +++ b/src/addon/competency/providers/user-handler.ts @@ -16,6 +16,8 @@ import { Injectable } from '@angular/core'; import { CoreUserDelegate, CoreUserProfileHandler, CoreUserProfileHandlerData } from '@core/user/providers/user-delegate'; import { CoreSitesProvider } from '@providers/sites'; import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreUserProvider } from '@core/user/providers/user'; import { AddonCompetencyProvider } from './competency'; /** @@ -30,13 +32,15 @@ export class AddonCompetencyUserHandler implements CoreUserProfileHandler { usersNavEnabledCache = {}; constructor(private linkHelper: CoreContentLinksHelperProvider, protected sitesProvider: CoreSitesProvider, - private competencyProvider: AddonCompetencyProvider) { + private competencyProvider: AddonCompetencyProvider, eventsProvider: CoreEventsProvider) { + eventsProvider.on(CoreEventsProvider.LOGOUT, this.clearUsersNavCache.bind(this)); + eventsProvider.on(CoreUserProvider.PROFILE_REFRESHED, this.clearUsersNavCache.bind(this)); } /** * Clear users nav cache. */ - clearUsersNavCache(): void { + private clearUsersNavCache(): void { this.participantsNavEnabledCache = {}; this.usersNavEnabledCache = {}; } From 2c643fb84269853241e41997b2beacc09118d10f Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Wed, 21 Mar 2018 16:13:08 +0100 Subject: [PATCH 7/8] MOBILE-2319 core: Changed demo sites to HTTPS --- src/config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.json b/src/config.json index 163458ae7..275d3c206 100644 --- a/src/config.json +++ b/src/config.json @@ -9,7 +9,7 @@ "languages": {"ar": "عربي", "bg": "Български", "ca": "Català", "cs": "Čeština", "da": "Dansk", "de": "Deutsch", "de-du": "Deutsch - Du", "el": "Ελληνικά", "en": "English", "es": "Español", "es-mx": "Español - México", "eu": "Euskara", "fa": "فارسی", "fr" : "Français", "he" : "עברית", "hu": "magyar", "it": "Italiano", "lt" : "Lietuvių", "ja": "日本語","nl": "Nederlands", "pl": "Polski", "pt-br": "Português - Brasil", "pt": "Português - Portugal", "ro": "Română", "ru": "Русский", "sr-cr": "Српски", "sr-lt": "Srpski", "sv": "Svenska", "tr" : "Türkçe", "uk" : "Українська", "zh-cn" : "简体中文", "zh-tw" : "正體中文"}, "wsservice" : "moodle_mobile_app", "wsextservice" : "local_mobile", - "demo_sites": {"student": {"url": "http://school.demo.moodle.net", "username": "student", "password": "moodle"}, "teacher": {"url": "http://school.demo.moodle.net", "username": "teacher", "password": "moodle"}}, + "demo_sites": {"student": {"url": "https://school.demo.moodle.net", "username": "student", "password": "moodle"}, "teacher": {"url": "https://school.demo.moodle.net", "username": "teacher", "password": "moodle"}}, "gcmpn": "694767596569", "customurlscheme": "moodlemobile", "siteurl": "", From 53fe0d899cb1ecc8cb211d88f681f544abfe1bc3 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Thu, 22 Mar 2018 15:46:30 +0100 Subject: [PATCH 8/8] MOBILE-2329 notes: Split view changed to a combo box. --- .../notes/components/components.module.ts | 12 ++-- .../{pages => components}/list/list.html | 26 ++++---- .../notes/{pages => components}/list/list.ts | 49 +++++++-------- src/addon/notes/components/types/types.html | 15 ----- src/addon/notes/components/types/types.ts | 62 ------------------- src/addon/notes/pages/list/list.module.ts | 35 ----------- .../notes/providers/course-option-handler.ts | 4 +- 7 files changed, 46 insertions(+), 157 deletions(-) rename src/addon/notes/{pages => components}/list/list.html (56%) rename src/addon/notes/{pages => components}/list/list.ts (79%) delete mode 100644 src/addon/notes/components/types/types.html delete mode 100644 src/addon/notes/components/types/types.ts delete mode 100644 src/addon/notes/pages/list/list.module.ts diff --git a/src/addon/notes/components/components.module.ts b/src/addon/notes/components/components.module.ts index 4064545e6..53a7a2457 100644 --- a/src/addon/notes/components/components.module.ts +++ b/src/addon/notes/components/components.module.ts @@ -17,25 +17,29 @@ import { CommonModule } from '@angular/common'; import { IonicModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreComponentsModule } from '@components/components.module'; -import { AddonNotesTypesComponent } from './types/types'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CorePipesModule } from '@pipes/pipes.module'; +import { AddonNotesListComponent } from './list/list'; @NgModule({ declarations: [ - AddonNotesTypesComponent + AddonNotesListComponent ], imports: [ CommonModule, IonicModule, TranslateModule.forChild(), CoreComponentsModule, + CoreDirectivesModule, + CorePipesModule ], providers: [ ], exports: [ - AddonNotesTypesComponent + AddonNotesListComponent ], entryComponents: [ - AddonNotesTypesComponent + AddonNotesListComponent ] }) export class AddonNotesComponentsModule {} diff --git a/src/addon/notes/pages/list/list.html b/src/addon/notes/components/list/list.html similarity index 56% rename from src/addon/notes/pages/list/list.html rename to src/addon/notes/components/list/list.html index 1431ca259..5b2fd8ebc 100644 --- a/src/addon/notes/pages/list/list.html +++ b/src/addon/notes/components/list/list.html @@ -1,15 +1,9 @@ - - - {{ 'addon.notes.notes' | translate }} - - - - - - - - - + + + + + + @@ -17,6 +11,12 @@ + + {{ 'addon.notes.sitenotes' | translate }} + {{ 'addon.notes.coursenotes' | translate }} + {{ 'addon.notes.personalnotes' | translate }} + +
{{ 'core.thereisdatatosync' | translate:{$a: 'addon.notes.notes' | translate | lowercase } }} @@ -28,7 +28,7 @@ - +

{{note.userfullname}}

{{note.lastmodified | coreDateDayOrTime}}

diff --git a/src/addon/notes/pages/list/list.ts b/src/addon/notes/components/list/list.ts similarity index 79% rename from src/addon/notes/pages/list/list.ts rename to src/addon/notes/components/list/list.ts index d64b578b9..f41af7dec 100644 --- a/src/addon/notes/pages/list/list.ts +++ b/src/addon/notes/components/list/list.ts @@ -12,43 +12,39 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnDestroy, Optional, ViewChild } from '@angular/core'; -import { Content, IonicPage, NavController, NavParams } from 'ionic-angular'; +import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { Content } from 'ionic-angular'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreEventsProvider } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; import { CoreTextUtilsProvider } from '@providers/utils/text'; -import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { AddonNotesProvider } from '../../providers/notes'; import { AddonNotesSyncProvider } from '../../providers/notes-sync'; /** - * Page that displays the list of notes. + * Component that displays the notes of a course. */ -@IonicPage({ segment: 'addon-notes-list' }) @Component({ - selector: 'page-addon-notes-list', + selector: 'addon-notes-list', templateUrl: 'list.html', }) -export class AddonNotesListPage implements OnDestroy { +export class AddonNotesListComponent implements OnInit, OnDestroy { + @Input() courseId: number; + @ViewChild(Content) content: Content; - protected courseId = 0; protected syncObserver: any; - type = ''; + type = 'course'; refreshIcon = 'spinner'; syncIcon = 'spinner'; notes: any[]; hasOffline = false; notesLoaded = false; - constructor(navParams: NavParams, private navCtrl: NavController, @Optional() private svComponent: CoreSplitViewComponent, - private domUtils: CoreDomUtilsProvider, private textUtils: CoreTextUtilsProvider, + constructor(private domUtils: CoreDomUtilsProvider, private textUtils: CoreTextUtilsProvider, sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider, private notesProvider: AddonNotesProvider, private notesSync: AddonNotesSyncProvider) { - this.courseId = navParams.get('courseId') || sitesProvider.getCurrentSiteHomeId(); - this.type = navParams.get('type'); // Refresh data if notes are synchronized automatically. this.syncObserver = eventsProvider.on(AddonNotesSyncProvider.AUTO_SYNCED, (data) => { if (data.courseId == this.courseId) { @@ -67,9 +63,9 @@ export class AddonNotesListPage implements OnDestroy { } /** - * View loaded. + * Component being initialized. */ - ionViewDidLoad(): void { + ngOnInit(): void { this.fetchNotes(true).then(() => { this.notesProvider.logView(this.courseId); }); @@ -124,6 +120,18 @@ export class AddonNotesListPage implements OnDestroy { }); } + /** + * Function called when the type has changed. + */ + typeChanged(): void { + this.notesLoaded = false; + this.refreshIcon = 'spinner'; + this.syncIcon = 'spinner'; + this.fetchNotes(true).then(() => { + this.notesProvider.logView(this.courseId); + }); + } + /** * Tries to synchronize course notes. * @@ -154,17 +162,6 @@ export class AddonNotesListPage implements OnDestroy { } } - /** - * Opens the profile of a user. - * - * @param {number} userId - */ - openUserProfile(userId: number): void { - // Decide which navCtrl to use. If this page is inside a split view, use the split view's master nav. - const navCtrl = this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl; - navCtrl.push('CoreUserProfilePage', {userId, courseId: this.courseId}); - } - /** * Page destroyed. */ diff --git a/src/addon/notes/components/types/types.html b/src/addon/notes/components/types/types.html deleted file mode 100644 index 87c04392c..000000000 --- a/src/addon/notes/components/types/types.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - {{ 'addon.notes.sitenotes' | translate }} - - - {{ 'addon.notes.coursenotes' | translate }} - - - {{ 'addon.notes.personalnotes' | translate }} - - - - diff --git a/src/addon/notes/components/types/types.ts b/src/addon/notes/components/types/types.ts deleted file mode 100644 index 813546b6d..000000000 --- a/src/addon/notes/components/types/types.ts +++ /dev/null @@ -1,62 +0,0 @@ -// (C) Copyright 2015 Martin Dougiamas -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { AfterViewInit, Component, Input, OnInit, ViewChild } from '@angular/core'; -import { CoreSplitViewComponent } from '@components/split-view/split-view'; -import { CoreSitesProvider } from '@providers/sites'; - -/** - * Component that displays the competencies of a course. - */ -@Component({ - selector: 'addon-notes-types', - templateUrl: 'types.html', -}) -export class AddonNotesTypesComponent implements AfterViewInit, OnInit { - @ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent; - - @Input() courseId: number; - - protected type: string; - - constructor(private sitesProvider: CoreSitesProvider) { - } - - /** - * Properties initialized. - */ - ngOnInit(): void { - const siteHomeId = this.sitesProvider.getCurrentSite().getSiteHomeId(); - this.type = (this.courseId == siteHomeId ? 'site' : 'course'); - } - - /** - * View loaded. - */ - ngAfterViewInit(): void { - if (this.splitviewCtrl.isOn()) { - this.openList(this.type); - } - } - - /** - * Opens a list of notes. - * - * @param {string} type - */ - openList(type: string): void { - this.type = type; - this.splitviewCtrl.push('AddonNotesListPage', {courseId: this.courseId, type: type}); - } -} diff --git a/src/addon/notes/pages/list/list.module.ts b/src/addon/notes/pages/list/list.module.ts deleted file mode 100644 index 56f649dee..000000000 --- a/src/addon/notes/pages/list/list.module.ts +++ /dev/null @@ -1,35 +0,0 @@ -// (C) Copyright 2015 Martin Dougiamas -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { NgModule } from '@angular/core'; -import { IonicPageModule } from 'ionic-angular'; -import { TranslateModule } from '@ngx-translate/core'; -import { CoreComponentsModule } from '@components/components.module'; -import { CoreDirectivesModule } from '@directives/directives.module'; -import { CorePipesModule } from '@pipes/pipes.module'; -import { AddonNotesListPage } from './list'; - -@NgModule({ - declarations: [ - AddonNotesListPage, - ], - imports: [ - CoreComponentsModule, - CoreDirectivesModule, - CorePipesModule, - IonicPageModule.forChild(AddonNotesListPage), - TranslateModule.forChild() - ], -}) -export class AddonNotesListPageModule {} diff --git a/src/addon/notes/providers/course-option-handler.ts b/src/addon/notes/providers/course-option-handler.ts index 8ab6dbbd1..34500726a 100644 --- a/src/addon/notes/providers/course-option-handler.ts +++ b/src/addon/notes/providers/course-option-handler.ts @@ -16,7 +16,7 @@ import { Injectable, Injector } from '@angular/core'; import { AddonNotesProvider } from './notes'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseOptionsHandler, CoreCourseOptionsHandlerData } from '@core/course/providers/options-delegate'; -import { AddonNotesTypesComponent } from '../components/types/types'; +import { AddonNotesListComponent } from '../components/list/list'; /** * Handler to inject an option into the course main menu. @@ -68,7 +68,7 @@ export class AddonNotesCourseOptionHandler implements CoreCourseOptionsHandler { return { title: 'addon.notes.notes', class: 'addon-notes-course-handler', - component: AddonNotesTypesComponent, + component: AddonNotesListComponent, }; } }