// (C) Copyright 2015 Moodle Pty Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { Injectable } from '@angular/core'; import { CoreWSError } from '@classes/errors/wserror'; import { CoreSite } from '@classes/sites/site'; import { CoreUser } from '@features/user/services/user'; import { CoreNetwork } from '@services/network'; import { CoreSites } from '@services/sites'; import { CoreUtils } from '@services/utils/utils'; import { CoreWSExternalWarning } from '@services/ws'; import { makeSingleton, Translate } from '@singletons'; import { AddonNotesOffline } from './notes-offline'; import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; const ROOT_CACHE_KEY = 'mmaNotes:'; /** * Service to handle notes. */ @Injectable( { providedIn: 'root' } ) export class AddonNotesProvider { /** * Add a note. * * @param userId User ID of the person to add the note. * @param courseId Course ID where the note belongs. * @param publishState Personal, Site or Course. * @param noteText The note text. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved with boolean: true if note was sent to server, false if stored in device. */ async addNote( userId: number, courseId: number, publishState: AddonNotesPublishState, noteText: string, siteId?: string, ): Promise<boolean> { siteId = siteId || CoreSites.getCurrentSiteId(); // Convenience function to store a note to be synchronized later. const storeOffline = async (): Promise<boolean> => { await AddonNotesOffline.saveNote(userId, courseId, publishState, noteText, siteId); return false; }; if (!CoreNetwork.isOnline()) { // App is offline, store the note. return storeOffline(); } // Send note to server. try { await this.addNoteOnline(userId, courseId, publishState, noteText, siteId); return true; } catch (error) { if (CoreUtils.isWebServiceError(error)) { // It's a WebService error, the user cannot send the message so don't store it. throw error; } return storeOffline(); } } /** * Add a note. It will fail if offline or cannot connect. * * @param userId User ID of the person to add the note. * @param courseId Course ID where the note belongs. * @param publishState Personal, Site or Course. * @param noteText The note text. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when added, rejected otherwise. */ async addNoteOnline( userId: number, courseId: number, publishState: AddonNotesPublishState, noteText: string, siteId?: string, ): Promise<void> { const notes: AddonNotesCreateNoteData[] = [ { courseid: courseId, format: 1, publishstate: publishState, text: noteText, userid: userId, }, ]; const response = await this.addNotesOnline(notes, siteId); if (response && response[0] && response[0].noteid === -1) { // There was an error, and it should be translated already. throw new CoreWSError({ message: response[0].errormessage }); } await CoreUtils.ignoreErrors(this.invalidateNotes(courseId, undefined, siteId)); } /** * Add several notes. It will fail if offline or cannot connect. * * @param notes Notes to save. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when added, rejected otherwise. Promise resolved doesn't mean that notes * have been added, the resolve param can contain errors for notes not sent. */ async addNotesOnline(notes: AddonNotesCreateNoteData[], siteId?: string): Promise<AddonNotesCreateNotesWSResponse> { if (!notes || !notes.length) { return []; } const site = await CoreSites.getSite(siteId); const data: AddonNotesCreateNotesWSParams = { notes: notes, }; return site.write('core_notes_create_notes', data); } /** * Delete a note. * * @param note Note object to delete. * @param courseId Course ID where the note belongs. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when deleted, rejected otherwise. Promise resolved doesn't mean that notes * have been deleted, the resolve param can contain errors for notes not deleted. */ async deleteNote(note: AddonNotesNoteFormatted, courseId: number, siteId?: string): Promise<boolean> { siteId = siteId || CoreSites.getCurrentSiteId(); if (note.offline) { await AddonNotesOffline.deleteOfflineNote(note.userid, note.content, note.created, siteId); return true; } // Convenience function to store the action to be synchronized later. const storeOffline = async (): Promise<boolean> => { await AddonNotesOffline.deleteNote(note.id, courseId, siteId); return false; }; if (!CoreNetwork.isOnline()) { // App is offline, store the note. return storeOffline(); } // Send note to server. try { await this.deleteNotesOnline([note.id], courseId, siteId); return true; } catch (error) { if (CoreUtils.isWebServiceError(error)) { // It's a WebService error, the user cannot send the note so don't store it. throw error; } return storeOffline(); } } /** * Delete a note. It will fail if offline or cannot connect. * * @param noteIds Note IDs to delete. * @param courseId Course ID where the note belongs. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when deleted, rejected otherwise. Promise resolved doesn't mean that notes * have been deleted, the resolve param can contain errors for notes not deleted. */ async deleteNotesOnline(noteIds: number[], courseId: number, siteId?: string): Promise<void> { const site = await CoreSites.getSite(siteId); const params: AddonNotesDeleteNotesWSParams = { notes: noteIds, }; await site.write('core_notes_delete_notes', params); CoreUtils.ignoreErrors(this.invalidateNotes(courseId, undefined, siteId)); } /** * Returns whether or not the notes plugin is enabled for a certain site. * * This method is called quite often and thus should only perform a quick * check, we should not be calling WS from here. * * @param siteId Site ID. If not defined, current site. * @returns Promise resolved with true if enabled, resolved with false or rejected otherwise. */ async isPluginEnabled(siteId?: string): Promise<boolean> { const site = await CoreSites.getSite(siteId); return site.canUseAdvancedFeature('enablenotes'); } /** * Returns whether or not the add note plugin is enabled for a certain course. * * @param courseId ID of the course. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved with true if enabled, resolved with false or rejected otherwise. */ async isPluginAddNoteEnabledForCourse(courseId: number, siteId?: string): Promise<boolean> { const site = await CoreSites.getSite(siteId); // The only way to detect if it's enabled is to perform a WS call. // We use an invalid user ID (-1) to avoid saving the note if the user has permissions. const params: AddonNotesCreateNotesWSParams = { notes: [ { userid: -1, publishstate: 'personal', courseid: courseId, text: '', format: 1, }, ], }; const preSets: CoreSiteWSPreSets = { updateFrequency: CoreSite.FREQUENCY_RARELY, }; // Use .read to cache data and be able to check it in offline. This means that, if a user loses the capabilities // to add notes, he'll still see the option in the app. return CoreUtils.promiseWorks(site.read('core_notes_create_notes', params, preSets)); } /** * Returns whether or not the read notes plugin is enabled for a certain course. * * @param courseId ID of the course. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved with true if enabled, resolved with false or rejected otherwise. */ isPluginViewNotesEnabledForCourse(courseId: number, siteId?: string): Promise<boolean> { return CoreUtils.promiseWorks(this.getNotes(courseId, undefined, false, true, siteId)); } /** * Get prefix cache key for course notes. * * @param courseId ID of the course to get the notes from. * @returns Cache key. */ getNotesPrefixCacheKey(courseId: number): string { return ROOT_CACHE_KEY + 'notes:' + courseId + ':'; } /** * Get the cache key for the get notes call. * * @param courseId ID of the course to get the notes from. * @param userId ID of the user to get the notes from if requested. * @returns Cache key. */ getNotesCacheKey(courseId: number, userId?: number): string { return this.getNotesPrefixCacheKey(courseId) + (userId ? userId : ''); } /** * Get users notes for a certain site, course and personal notes. * * @param courseId ID of the course to get the notes from. * @param userId ID of the user to get the notes from if requested. * @param ignoreCache True when we should not get the value from the cache. * @param onlyOnline True to return only online notes, false to return both online and offline. * @param siteId Site ID. If not defined, current site. * @returns Promise to be resolved when the notes are retrieved. */ async getNotes( courseId: number, userId?: number, ignoreCache = false, onlyOnline = false, siteId?: string, ): Promise<AddonNotesGetCourseNotesWSResponse> { const site = await CoreSites.getSite(siteId); const params: AddonNotesGetCourseNotesWSParams = { courseid: courseId, }; if (userId) { params.userid = userId; } const preSets: CoreSiteWSPreSets = { cacheKey: this.getNotesCacheKey(courseId, userId), updateFrequency: CoreSite.FREQUENCY_SOMETIMES, }; if (ignoreCache) { preSets.getFromCache = false; preSets.emergencyCache = false; } const notes = await site.read<AddonNotesGetCourseNotesWSResponse>('core_notes_get_course_notes', params, preSets); if (onlyOnline) { return notes; } const offlineNotes = await AddonNotesOffline.getNotesForCourseAndUser(courseId, userId, siteId); offlineNotes.forEach((note: AddonNotesNote) => { const fieldName = note.publishstate + 'notes'; if (!notes[fieldName]) { notes[fieldName] = []; } note.offline = true; // Add note to the start of array since last notes are shown first. notes[fieldName].unshift(note); }); return notes; } /** * Get offline deleted notes and set the state. * * @param notes Array of notes. * @param courseId ID of the course the notes belong to. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when done. */ async setOfflineDeletedNotes( notes: AddonNotesNoteFormatted[], courseId: number, siteId?: string, ): Promise<void> { const deletedNotes = await AddonNotesOffline.getCourseDeletedNotes(courseId, siteId); notes.forEach((note) => { note.deleted = deletedNotes.some((n) => n.noteid == note.id); }); } /** * Get user data for notes since they only have userid. * * @param notes Notes to get the data for. * @returns Promise always resolved. Resolve param is the formatted notes. */ async getNotesUserData(notes: AddonNotesNoteFormatted[]): Promise<AddonNotesNoteFormatted[]> { const promises = notes.map((note) => // Get the user profile to retrieve the user image. CoreUser.getProfile(note.userid, note.courseid, true).then((user) => { note.userfullname = user.fullname; note.userprofileimageurl = user.profileimageurl; return; }).catch(() => { note.userfullname = Translate.instant('core.user.userwithid', { id: note.userid }); })); await Promise.all(promises); return notes; } /** * Invalidate get notes WS call. * * @param courseId Course ID. * @param userId User ID if needed. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when data is invalidated. */ async invalidateNotes(courseId: number, userId?: number, siteId?: string): Promise<void> { const site = await CoreSites.getSite(siteId); if (userId) { await site.invalidateWsCacheForKey(this.getNotesCacheKey(courseId, userId)); return; } await site.invalidateWsCacheForKeyStartingWith(this.getNotesPrefixCacheKey(courseId)); } /** * Report notes as being viewed. * * @param courseId ID of the course. * @param userId User ID if needed. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ async logView(courseId: number, userId?: number, siteId?: string): Promise<void> { const site = await CoreSites.getSite(siteId); const params: AddonNotesViewNotesWSParams = { courseid: courseId, userid: userId || 0, }; await site.write('core_notes_view_notes', params); } } export const AddonNotes = makeSingleton(AddonNotesProvider); /** * Params of core_notes_view_notes WS. */ type AddonNotesViewNotesWSParams = { courseid: number; // Course id, 0 for notes at system level. userid?: number; // User id, 0 means view all the user notes. }; /** * Params of core_notes_get_course_notes WS. */ export type AddonNotesGetCourseNotesWSParams = { courseid: number; // Course id, 0 for SITE. userid?: number; // User id. }; /** * Note data returned by core_notes_get_course_notes. */ export type AddonNotesNote = { id: number; // Id of this note. courseid: number; // Id of the course. userid: number; // User id. content: string; // The content text formated. format: number; // Content format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). created: number; // Time created (timestamp). lastmodified: number; // Time of last modification (timestamp). usermodified: number; // User id of the creator of this note. publishstate: AddonNotesPublishState; // State of the note (i.e. draft, public, site). offline?: boolean; }; /** * Result of WS core_notes_get_course_notes. */ export type AddonNotesGetCourseNotesWSResponse = { sitenotes?: AddonNotesNote[]; // Site notes. coursenotes?: AddonNotesNote[]; // Couse notes. personalnotes?: AddonNotesNote[]; // Personal notes. canmanagesystemnotes?: boolean; // @since 3.7. Whether the user can manage notes at system level. canmanagecoursenotes?: boolean; // @since 3.7. Whether the user can manage notes at the given course. warnings?: CoreWSExternalWarning[]; }; /** * Result of WS core_notes_view_notes. */ export type AddonNotesViewNotesResult = { status: boolean; // Status: true if success. warnings?: CoreWSExternalWarning[]; }; /** * Notes with some calculated data. */ export type AddonNotesNoteFormatted = AddonNotesNote & { offline?: boolean; // Calculated in the app. Whether it's an offline note. deleted?: boolean; // Calculated in the app. Whether the note was deleted in offline. userfullname?: string; // Calculated in the app. Full name of the user the note refers to. userprofileimageurl?: string; // Calculated in the app. Avatar url of the user the note refers to. }; export type AddonNotesCreateNoteData = { userid: number; // Id of the user the note is about. publishstate: AddonNotesPublishState; // 'personal', 'course' or 'site'. courseid: number; // Course id of the note (in Moodle a note can only be created into a course, // even for site and personal notes). text: string; // The text of the message - text or HTML. format?: number; // Text format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). clientnoteid?: string; // Your own client id for the note. If this id is provided, the fail message id will be returned to you. }; /** * Params of core_notes_create_notes WS. */ type AddonNotesCreateNotesWSParams = { notes: AddonNotesCreateNoteData[]; }; /** * Note returned by WS core_notes_create_notes. */ export type AddonNotesCreateNotesWSResponse = { clientnoteid?: string; // Your own id for the note. noteid: number; // ID of the created note when successful, -1 when failed. errormessage?: string; // Error message - if failed. }[]; /** * Params of core_notes_delete_notes WS. */ type AddonNotesDeleteNotesWSParams = { notes: number[]; // Array of Note Ids to be deleted. }; export type AddonNotesPublishState = 'personal' | 'site' | 'course';