From 8a56dc84b841b1743187f14c2f4a093e21232a13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 3 Jul 2019 11:36:23 +0200 Subject: [PATCH] MOBILE-2877 comments: Delete comments --- src/addon/blog/components/entries/entries.ts | 8 +- src/assets/lang/en.json | 3 + src/core/comments/lang/en.json | 5 +- src/core/comments/pages/add/add.ts | 2 +- src/core/comments/pages/viewer/viewer.html | 19 +- src/core/comments/pages/viewer/viewer.ts | 120 ++++++++-- src/core/comments/providers/comments.ts | 97 +++++++- src/core/comments/providers/offline.ts | 223 ++++++++++++++++--- src/core/comments/providers/sync.ts | 82 ++++--- 9 files changed, 451 insertions(+), 108 deletions(-) diff --git a/src/addon/blog/components/entries/entries.ts b/src/addon/blog/components/entries/entries.ts index d737c8e1b..804538d85 100644 --- a/src/addon/blog/components/entries/entries.ts +++ b/src/addon/blog/components/entries/entries.ts @@ -169,11 +169,13 @@ export class AddonBlogEntriesComponent implements OnInit { * @param {any} refresher Refresher instance. */ refresh(refresher?: any): void { - this.entries.forEach((entry) => { - this.commentsProvider.invalidateCommentsData('user', entry.userid, this.component, entry.id, 'format_blog'); + const promises = this.entries.map((entry) => { + return this.commentsProvider.invalidateCommentsData('user', entry.userid, this.component, entry.id, 'format_blog'); }); - this.blogProvider.invalidateEntries(this.filter).finally(() => { + promises.push(this.blogProvider.invalidateEntries(this.filter)); + + Promise.all(promises).finally(() => { this.fetchEntries(true).finally(() => { if (refresher) { refresher.complete(); diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 4ada12bc3..5b481b28a 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -1269,9 +1269,12 @@ "core.comments.comments": "Comments", "core.comments.commentscount": "Comments ({{$a}})", "core.comments.commentsnotworking": "Comments cannot be retrieved", + "core.comments.deletecommentbyon": "Delete comment posted by {{$a.user}} on {{$a.time}}", "core.comments.eventcommentcreated": "Comment created", + "core.comments.eventcommentdeleted": "Comment deleted", "core.comments.nocomments": "No comments", "core.comments.savecomment": "Save comment", + "core.comments.warningcommentsnotsent": "Couldn't sync comments. {{error}}", "core.completion-alt-auto-fail": "Completed: {{$a}} (did not achieve pass grade)", "core.completion-alt-auto-n": "Not completed: {{$a}}", "core.completion-alt-auto-n-override": "Not completed: {{$a.modname}} (set by {{$a.overrideuser}})", diff --git a/src/core/comments/lang/en.json b/src/core/comments/lang/en.json index b6c99e4c3..c48dcce17 100644 --- a/src/core/comments/lang/en.json +++ b/src/core/comments/lang/en.json @@ -3,7 +3,10 @@ "comments": "Comments", "commentscount": "Comments ({{$a}})", "commentsnotworking": "Comments cannot be retrieved", + "deletecommentbyon": "Delete comment posted by {{$a.user}} on {{$a.time}}", "eventcommentcreated": "Comment created", + "eventcommentdeleted": "Comment deleted", "nocomments": "No comments", - "savecomment": "Save comment" + "savecomment": "Save comment", + "warningcommentsnotsent": "Couldn't sync comments. {{error}}" } \ No newline at end of file diff --git a/src/core/comments/pages/add/add.ts b/src/core/comments/pages/add/add.ts index 1d58db46f..66510fb79 100644 --- a/src/core/comments/pages/add/add.ts +++ b/src/core/comments/pages/add/add.ts @@ -57,7 +57,7 @@ export class CoreCommentsAddPage { this.appProvider.closeKeyboard(); const loadingModal = this.domUtils.showModalLoading('core.sending', true); - // Freeze the add note button. + // Freeze the add comment button. this.processing = true; this.commentsProvider.addComment(this.content, this.contextLevel, this.instanceId, this.componentName, this.itemId, this.area).then((commentsResponse) => { diff --git a/src/core/comments/pages/viewer/viewer.html b/src/core/comments/pages/viewer/viewer.html index 812838695..2a023a419 100644 --- a/src/core/comments/pages/viewer/viewer.html +++ b/src/core/comments/pages/viewer/viewer.html @@ -2,6 +2,9 @@ + @@ -21,13 +24,16 @@ {{ 'core.thereisdatatosync' | translate:{$a: 'core.comments.comments' | translate | lowercase } }} - +

{{ offlineComment.fullname }}

{{ 'core.notsent' | translate }}

+
@@ -38,7 +44,16 @@

{{ comment.fullname }}

-

{{ comment.timecreated * 1000 | coreFormatDate: 'strftimerecentfull' }}

+

{{ comment.timecreated * 1000 | coreFormatDate: 'strftimerecentfull' }}

+

+ {{ 'core.deletedoffline' | translate }} +

+ +
diff --git a/src/core/comments/pages/viewer/viewer.ts b/src/core/comments/pages/viewer/viewer.ts index 50bbf004f..0f606feee 100644 --- a/src/core/comments/pages/viewer/viewer.ts +++ b/src/core/comments/pages/viewer/viewer.ts @@ -15,9 +15,11 @@ import { Component, ViewChild, OnDestroy } from '@angular/core'; import { IonicPage, Content, NavParams, ModalController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; +import { coreSlideInOut } from '@classes/animations'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreEventsProvider } from '@providers/events'; import { CoreUserProvider } from '@core/user/providers/user'; import { CoreCommentsProvider } from '../../providers/comments'; @@ -31,6 +33,7 @@ import { CoreCommentsSyncProvider } from '../../providers/sync'; @Component({ selector: 'page-core-comments-viewer', templateUrl: 'viewer.html', + animations: [coreSlideInOut] }) export class CoreCommentsViewerPage implements OnDestroy { @ViewChild(Content) content: Content; @@ -47,12 +50,15 @@ export class CoreCommentsViewerPage implements OnDestroy { canLoadMore = false; loadMoreError = false; canAddComments = false; + canDeleteComments = false; + showDelete = false; hasOffline = false; refreshIcon = 'spinner'; syncIcon = 'spinner'; offlineComment: any; + currentUserId: number; - protected addCommentsAvailable = false; + protected addDeleteCommentsAvailable = false; protected syncObserver: any; protected currentUser: any; @@ -60,7 +66,7 @@ export class CoreCommentsViewerPage implements OnDestroy { private domUtils: CoreDomUtilsProvider, private translate: TranslateService, private modalCtrl: ModalController, private commentsProvider: CoreCommentsProvider, private offlineComments: CoreCommentsOfflineProvider, eventsProvider: CoreEventsProvider, private commentsSync: CoreCommentsSyncProvider, - private textUtils: CoreTextUtilsProvider) { + private textUtils: CoreTextUtilsProvider, private timeUtils: CoreTimeUtilsProvider) { this.contextLevel = navParams.get('contextLevel'); this.instanceId = navParams.get('instanceId'); @@ -96,34 +102,35 @@ export class CoreCommentsViewerPage implements OnDestroy { */ ionViewDidLoad(): void { this.commentsProvider.isAddCommentsAvailable().then((enabled) => { - this.addCommentsAvailable = enabled; + // Is implicit the user can delete if he can add. + this.addDeleteCommentsAvailable = enabled; }); + this.currentUserId = this.sitesProvider.getCurrentSiteUserId(); this.fetchComments(true); } /** * Fetches the comments. * - * @param {boolean} sync When to resync notes. + * @param {boolean} sync When to resync comments. * @param {boolean} [showErrors] When to display errors or not. * @return {Promise} Resolved when done. */ protected fetchComments(sync: boolean, showErrors?: boolean): Promise { this.loadMoreError = false; - const promise = sync ? this.syncComment(showErrors) : Promise.resolve(); + const promise = sync ? this.syncComments(showErrors) : Promise.resolve(); return promise.catch(() => { // Ignore errors. }).then(() => { return this.offlineComments.getComment(this.contextLevel, this.instanceId, this.componentName, this.itemId, this.area).then((offlineComment) => { - this.hasOffline = !!offlineComment; this.offlineComment = offlineComment; - if (this.hasOffline && !this.currentUser) { - return this.userProvider.getProfile(this.sitesProvider.getCurrentSiteUserId(), undefined, true).then((user) => { + if (offlineComment && !this.currentUser) { + return this.userProvider.getProfile(this.currentUserId, undefined, true).then((user) => { this.currentUser = user; this.offlineComment.profileimageurl = user.profileimageurl; this.offlineComment.fullname = user.fullname; @@ -131,32 +138,53 @@ export class CoreCommentsViewerPage implements OnDestroy { }).catch(() => { // Ignore errors. }); - } else if (this.hasOffline) { + } else if (offlineComment) { this.offlineComment.profileimageurl = this.currentUser.profileimageurl; this.offlineComment.fullname = this.currentUser.fullname; this.offlineComment.userid = this.currentUser.id; } + + return this.offlineComments.getDeletedComments(this.contextLevel, this.instanceId, this.componentName, this.itemId, + this.area); }); - }).then(() => { + }).then((deletedComments) => { + this.hasOffline = !!this.offlineComment || deletedComments.length > 0; // Get comments data. return this.commentsProvider.getComments(this.contextLevel, this.instanceId, this.componentName, this.itemId, this.area, this.page).then((response) => { - this.canAddComments = this.addCommentsAvailable && response.canpost; + this.canAddComments = this.addDeleteCommentsAvailable && response.canpost; const comments = response.comments.sort((a, b) => b.timecreated - a.timecreated); this.canLoadMore = comments.length >= CoreCommentsProvider.pageSize; - this.comments.forEach((comment) => { + return Promise.all(comments.map((comment) => { // Get the user profile image. - this.userProvider.getProfile(comment.userid, undefined, true).then((user) => { + return this.userProvider.getProfile(comment.userid, undefined, true).then((user) => { comment.profileimageurl = user.profileimageurl; + + return comment; }).catch(() => { // Ignore errors. + return comment; }); + })); + }).then((comments) => { + this.comments = this.comments.concat(comments); + + deletedComments && deletedComments.forEach((deletedComment) => { + const comment = this.comments.find((comment) => { + return comment.id == deletedComment.commentid; + }); + + if (comment) { + comment.deleted = deletedComment.deleted; + } }); - this.comments = this.comments.concat(comments); + this.canDeleteComments = this.addDeleteCommentsAvailable && (this.hasOffline || this.comments.some((comment) => { + return !!comment.delete; + })); }); }).catch((error) => { this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading. @@ -174,7 +202,7 @@ export class CoreCommentsViewerPage implements OnDestroy { } /** - * Function to load more cp,,emts. + * Function to load more commemts. * * @param {any} [infiniteComplete] Infinite scroll complete function. Only used from core-infinite-loading. * @return {Promise} Resolved when done. @@ -228,8 +256,8 @@ export class CoreCommentsViewerPage implements OnDestroy { * @param {boolean} showErrors Whether to display errors or not. * @return {Promise} Promise resolved if sync is successful, rejected otherwise. */ - private syncComment(showErrors: boolean): Promise { - return this.commentsSync.syncComment(this.contextLevel, this.instanceId, this.componentName, this.itemId, + private syncComments(showErrors: boolean): Promise { + return this.commentsSync.syncComments(this.contextLevel, this.instanceId, this.componentName, this.itemId, this.area).then((warnings) => { this.showSyncWarnings(warnings); }).catch((error) => { @@ -263,6 +291,7 @@ export class CoreCommentsViewerPage implements OnDestroy { modal.onDidDismiss((data) => { if (data && data.comments) { this.comments = data.comments.concat(this.comments); + this.canDeleteComments = this.addDeleteCommentsAvailable; } else if (data && !data.comments) { this.fetchComments(false); } @@ -270,6 +299,63 @@ export class CoreCommentsViewerPage implements OnDestroy { modal.present(); } + /** + * Delete a comment. + * + * @param {Event} e Click event. + * @param {any} comment Comment to delete. + */ + deleteComment(e: Event, comment: any): void { + e.preventDefault(); + e.stopPropagation(); + + const time = this.timeUtils.userDate((comment.lastmodified || comment.timecreated) * 1000, 'core.strftimerecentfull'); + + comment.contextlevel = this.contextLevel; + comment.instanceid = this.instanceId; + comment.component = this.componentName; + comment.itemid = this.itemId; + comment.area = this.area; + + this.domUtils.showConfirm(this.translate.instant('core.comments.deletecommentbyon', {$a: + { user: comment.fullname || '', time: time } })).then(() => { + this.commentsProvider.deleteComment(comment).then(() => { + this.showDelete = false; + + this.refreshComments(true); + + this.domUtils.showToast('core.comments.eventcommentdeleted', true, 3000); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'Delete comment failed.'); + }); + }).catch(() => { + // User cancelled, nothing to do. + }); + } + + /** + * Restore a comment. + * + * @param {Event} e Click event. + * @param {any} comment Comment to delete. + */ + undoDeleteComment(e: Event, comment: any): void { + e.preventDefault(); + e.stopPropagation(); + + this.offlineComments.undoDeleteComment(comment.id).then(() => { + comment.deleted = false; + this.showDelete = false; + }); + } + + /** + * Toggle delete. + */ + toggleDelete(): void { + this.showDelete = !this.showDelete; + } + /** * Page destroyed. */ diff --git a/src/core/comments/providers/comments.ts b/src/core/comments/providers/comments.ts index a17748b08..eb5613c45 100644 --- a/src/core/comments/providers/comments.ts +++ b/src/core/comments/providers/comments.ts @@ -48,7 +48,7 @@ export class CoreCommentsProvider { siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); - // Convenience function to store a note to be synchronized later. + // Convenience function to store a comment to be synchronized later. const storeOffline = (): Promise => { return this.commentsOffline.saveComment(content, contextLevel, instanceId, component, itemId, area, siteId).then(() => { return Promise.resolve(false); @@ -56,11 +56,11 @@ export class CoreCommentsProvider { }; if (!this.appProvider.isOnline()) { - // App is offline, store the note. + // App is offline, store the comment. return storeOffline(); } - // Send note to server. + // Send comment to server. return this.addCommentOnline(content, contextLevel, instanceId, component, itemId, area, siteId).then((comments) => { return comments; }).catch((error) => { @@ -69,7 +69,7 @@ export class CoreCommentsProvider { return Promise.reject(error); } - // Error sending note, store it to retry later. + // Error sending comment, store it to retry later. return storeOffline(); }); } @@ -115,7 +115,7 @@ export class CoreCommentsProvider { * @param {any[]} comments Comments 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 comments - * have been added, the resolve param can contain errors for notes not sent. + * have been added, the resolve param can contain errors for comments not sent. */ addCommentsOnline(comments: any[], siteId?: string): Promise { if (!comments || !comments.length) { @@ -155,6 +155,79 @@ export class CoreCommentsProvider { }); } + /** + * Delete a comment. + * + * @param {any} comment Comment object to delete. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when deleted, rejected otherwise. Promise resolved doesn't mean that comments + * have been deleted, the resolve param can contain errors for comments not deleted. + */ + deleteComment(comment: any, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + if (!comment.id) { + return this.commentsOffline.removeComment(comment.contextlevel, comment.instanceid, comment.component, comment.itemid, + comment.area, siteId); + } + + // Convenience function to store the action to be synchronized later. + const storeOffline = (): Promise => { + return this.commentsOffline.deleteComment(comment.id, comment.contextlevel, comment.instanceid, comment.component, + comment.itemid, comment.area, siteId).then(() => { + return false; + }); + }; + + if (!this.appProvider.isOnline()) { + // App is offline, store the comment. + return storeOffline(); + } + + // Send comment to server. + return this.deleteCommentsOnline([comment.id], comment.contextlevel, comment.instanceid, comment.component, comment.itemid, + comment.area, siteId).then(() => { + return true; + }).catch((error) => { + if (this.utils.isWebServiceError(error)) { + // It's a WebService error, the user cannot send the comment so don't store it. + return Promise.reject(error); + } + + // Error sending comment, store it to retry later. + return storeOffline(); + }); + } + + /** + * Delete a comment. It will fail if offline or cannot connect. + * + * @param {number[]} commentIds Comment IDs to delete. + * @param {string} contextLevel Contextlevel system, course, user... + * @param {number} instanceId The Instance id of item associated with the context level. + * @param {string} component Component name. + * @param {number} itemId Associated id. + * @param {string} [area=''] String comment area. Default empty. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when deleted, rejected otherwise. Promise resolved doesn't mean that comments + * have been deleted, the resolve param can contain errors for comments not deleted. + */ + deleteCommentsOnline(commentIds: number[], contextLevel: string, instanceId: number, component: string, itemId: number, + area: string = '', siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const data = { + comments: commentIds + }; + + return site.write('core_comment_delete_comments', data).then((response) => { + // A comment was deleted, invalidate comments. + return this.invalidateCommentsData(contextLevel, instanceId, component, itemId, area, siteId).catch(() => { + // Ignore errors. + }); + }); + }); + } + /** * Returns whether WS to add/delete comments are available in site. * @@ -239,7 +312,7 @@ export class CoreCommentsProvider { } /** - * Get comments count number to show ont he comments component. + * Get comments count number to show on the comments component. * * @param {string} contextLevel Contextlevel system, course, user... * @param {number} instanceId The Instance id of item associated with the context level. @@ -284,7 +357,6 @@ export class CoreCommentsProvider { return getCommentsPageCount(1).then((countMore) => { // Page limit was reached on the previous call. if (countMore > 0) { - CoreCommentsProvider.pageSizeOK = true; return (CoreCommentsProvider.pageSize - 1) + '+'; } @@ -308,11 +380,14 @@ export class CoreCommentsProvider { invalidateCommentsData(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { - // This is done with starting with to avoid conflicts with previous keys that were including page. - site.invalidateWsCacheForKeyStartingWith(this.getCommentsCacheKey(contextLevel, instanceId, component, itemId, - area) + ':'); - return site.invalidateWsCacheForKey(this.getCommentsCacheKey(contextLevel, instanceId, component, itemId, area)); + return this.utils.allPromises([ + // This is done with starting with to avoid conflicts with previous keys that were including page. + site.invalidateWsCacheForKeyStartingWith(this.getCommentsCacheKey(contextLevel, instanceId, component, itemId, + area) + ':'), + + site.invalidateWsCacheForKey(this.getCommentsCacheKey(contextLevel, instanceId, component, itemId, area)) + ]); }); } diff --git a/src/core/comments/providers/offline.ts b/src/core/comments/providers/offline.ts index 82dbd6894..94caf9fb3 100644 --- a/src/core/comments/providers/offline.ts +++ b/src/core/comments/providers/offline.ts @@ -24,6 +24,7 @@ export class CoreCommentsOfflineProvider { // Variables for database. static COMMENTS_TABLE = 'core_comments_offline_comments'; + static COMMENTS_DELETED_TABLE = 'core_comments_deleted_offline_comments'; protected siteSchema: CoreSiteSchema = { name: 'CoreCommentsOfflineProvider', version: 1, @@ -55,16 +56,46 @@ export class CoreCommentsOfflineProvider { name: 'content', type: 'TEXT' }, - { - name: 'action', - type: 'TEXT' - }, { name: 'lastmodified', type: 'INTEGER' } ], primaryKeys: ['contextlevel', 'instanceid', 'component', 'itemid', 'area'] + }, + { + name: CoreCommentsOfflineProvider.COMMENTS_DELETED_TABLE, + columns: [ + { + name: 'commentid', + type: 'INTEGER', + primaryKey: true + }, + { + name: 'contextlevel', + type: 'TEXT' + }, + { + name: 'instanceid', + type: 'INTEGER' + }, + { + name: 'component', + type: 'TEXT', + }, + { + name: 'itemid', + type: 'INTEGER' + }, + { + name: 'area', + type: 'TEXT' + }, + { + name: 'deleted', + type: 'INTEGER' + } + ] } ] }; @@ -73,30 +104,6 @@ export class CoreCommentsOfflineProvider { this.sitesProvider.registerSiteSchema(this.siteSchema); } - /** - * Delete a comment. - * - * @param {string} contextLevel Contextlevel system, course, user... - * @param {number} instanceId The Instance id of item associated with the context level. - * @param {string} component Component name. - * @param {number} itemId Associated id. - * @param {string} [area=''] String comment area. Default empty. - * @param {string} [siteId] Site ID. If not defined, current site. - * @return {Promise} Promise resolved if deleted, rejected if failure. - */ - removeComment(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', - siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - return site.getDb().deleteRecords(CoreCommentsOfflineProvider.COMMENTS_TABLE, { - contextlevel: contextLevel, - instanceid: instanceId, - component: component, - itemid: itemId, - area: area - }); - }); - } - /** * Get all offline comments. * @@ -105,7 +112,10 @@ export class CoreCommentsOfflineProvider { */ getAllComments(siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { - return site.getDb().getRecords(CoreCommentsOfflineProvider.COMMENTS_TABLE); + return Promise.all([site.getDb().getRecords(CoreCommentsOfflineProvider.COMMENTS_TABLE), + site.getDb().getRecords(CoreCommentsOfflineProvider.COMMENTS_DELETED_TABLE)]).then((results) => { + return [].concat.apply([], results); + }); }); } @@ -136,19 +146,116 @@ export class CoreCommentsOfflineProvider { } /** - * Check if there are offline comments. + * Get all offline comments added or deleted of a special area. * * @param {string} contextLevel Contextlevel system, course, user... * @param {number} instanceId The Instance id of item associated with the context level. * @param {string} component Component name. * @param {number} itemId Associated id. * @param {string} [area=''] String comment area. Default empty. - * @return {Promise} Promise resolved with boolean: true if has offline comments, false otherwise. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the comments. */ - hasComments(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', - siteId?: string): Promise { - return this.getComment(contextLevel, instanceId, component, itemId, area, siteId).then((comments) => { - return !!comments.length; + getComments(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', + siteId?: string): Promise { + let comments = []; + + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + return this.getComment(contextLevel, instanceId, component, itemId, area, siteId).then((comment) => { + comments = comment ? [comment] : []; + + return this.getDeletedComments(contextLevel, instanceId, component, itemId, area, siteId); + }).then((deletedComments) => { + comments = comments.concat(deletedComments); + + return comments; + }); + } + + /** + * Get all offline deleted comments. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with comments. + */ + getAllDeletedComments(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecords(CoreCommentsOfflineProvider.COMMENTS_DELETED_TABLE); + }); + } + + /** + * Get an offline comment. + * + * @param {string} contextLevel Contextlevel system, course, user... + * @param {number} instanceId The Instance id of item associated with the context level. + * @param {string} component Component name. + * @param {number} itemId Associated id. + * @param {string} [area=''] String comment area. Default empty. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the comments. + */ + getDeletedComments(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', + siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecords(CoreCommentsOfflineProvider.COMMENTS_DELETED_TABLE, { + contextlevel: contextLevel, + instanceid: instanceId, + component: component, + itemid: itemId, + area: area + }); + }).catch(() => { + return false; + }); + } + + /** + * Remove an offline comment. + * + * @param {string} contextLevel Contextlevel system, course, user... + * @param {number} instanceId The Instance id of item associated with the context level. + * @param {string} component Component name. + * @param {number} itemId Associated id. + * @param {string} [area=''] String comment area. Default empty. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if deleted, rejected if failure. + */ + removeComment(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', + siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().deleteRecords(CoreCommentsOfflineProvider.COMMENTS_TABLE, { + contextlevel: contextLevel, + instanceid: instanceId, + component: component, + itemid: itemId, + area: area + }); + }); + } + + /** + * Remove an offline deleted comment. + * + * @param {string} contextLevel Contextlevel system, course, user... + * @param {number} instanceId The Instance id of item associated with the context level. + * @param {string} component Component name. + * @param {number} itemId Associated id. + * @param {string} [area=''] String comment area. Default empty. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if deleted, rejected if failure. + */ + removeDeletedComments(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', + siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().deleteRecords(CoreCommentsOfflineProvider.COMMENTS_DELETED_TABLE, { + contextlevel: contextLevel, + instanceid: instanceId, + component: component, + itemid: itemId, + area: area + }); }); } @@ -175,7 +282,6 @@ export class CoreCommentsOfflineProvider { itemid: itemId, area: area, content: content, - action: 'add', lastmodified: now }; @@ -184,4 +290,49 @@ export class CoreCommentsOfflineProvider { }); }); } + + /** + * Delete a comment offline to be sent later. + * + * @param {number} commentId Comment ID. + * @param {string} contextLevel Contextlevel system, course, user... + * @param {number} instanceId The Instance id of item associated with the context level. + * @param {string} component Component name. + * @param {number} itemId Associated id. + * @param {string} [area=''] String comment area. Default empty. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if stored, rejected if failure. + */ + deleteComment(commentId: number, contextLevel: string, instanceId: number, component: string, itemId: number, + area: string = '', siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const now = this.timeUtils.timestamp(); + const data = { + contextlevel: contextLevel, + instanceid: instanceId, + component: component, + itemid: itemId, + area: area, + commentid: commentId, + deleted: now + }; + + return site.getDb().insertRecord(CoreCommentsOfflineProvider.COMMENTS_DELETED_TABLE, data).then(() => { + return data; + }); + }); + } + + /** + * Undo delete a comment. + * + * @param {number} commentId Comment ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if deleted, rejected if failure. + */ + undoDeleteComment(commentId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().deleteRecords(CoreCommentsOfflineProvider.COMMENTS_DELETED_TABLE, { commentid: commentId }); + }); + } } diff --git a/src/core/comments/providers/sync.ts b/src/core/comments/providers/sync.ts index ab43a6d4b..c8466cac7 100644 --- a/src/core/comments/providers/sync.ts +++ b/src/core/comments/providers/sync.ts @@ -19,7 +19,6 @@ import { CoreSyncBaseProvider } from '@classes/base-sync'; import { CoreAppProvider } from '@providers/app'; import { CoreCommentsOfflineProvider } from './offline'; import { CoreCommentsProvider } from './comments'; -import { CoreCoursesProvider } from '@core/courses/providers/courses'; import { CoreEventsProvider } from '@providers/events'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; @@ -39,7 +38,7 @@ export class CoreCommentsSyncProvider extends CoreSyncBaseProvider { syncProvider: CoreSyncProvider, textUtils: CoreTextUtilsProvider, translate: TranslateService, private commentsOffline: CoreCommentsOfflineProvider, private utils: CoreUtilsProvider, private eventsProvider: CoreEventsProvider, private commentsProvider: CoreCommentsProvider, - private coursesProvider: CoreCoursesProvider, timeUtils: CoreTimeUtilsProvider) { + timeUtils: CoreTimeUtilsProvider) { super('CoreCommentsSync', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate, timeUtils); } @@ -64,10 +63,19 @@ export class CoreCommentsSyncProvider extends CoreSyncBaseProvider { */ private syncAllCommentsFunc(siteId: string, force: boolean): Promise { return this.commentsOffline.getAllComments(siteId).then((comments) => { + + // Get Unique array. + comments.forEach((comment) => { + comment.syncId = this.getSyncId(comment.contextlevel, comment.instanceid, comment.component, comment.itemid, + comment.area); + }); + + comments = this.utils.uniqueArray(comments, 'syncId'); + // Sync all courses. const promises = comments.map((comment) => { - const promise = force ? this.syncComment(comment.contextlevel, comment.instanceid, comment.component, - comment.itemid, comment.area, siteId) : this.syncCommentIfNeeded(comment.contextlevel, comment.instanceid, + const promise = force ? this.syncComments(comment.contextlevel, comment.instanceid, comment.component, + comment.itemid, comment.area, siteId) : this.syncCommentsIfNeeded(comment.contextlevel, comment.instanceid, comment.component, comment.itemid, comment.area, siteId); return promise.then((warnings) => { @@ -90,7 +98,7 @@ export class CoreCommentsSyncProvider extends CoreSyncBaseProvider { } /** - * Sync course notes only if a certain time has passed since the last time. + * Sync course comments only if a certain time has passed since the last time. * * @param {string} contextLevel Contextlevel system, course, user... * @param {number} instanceId The Instance id of item associated with the context level. @@ -98,21 +106,21 @@ export class CoreCommentsSyncProvider extends CoreSyncBaseProvider { * @param {number} itemId Associated id. * @param {string} [area=''] String comment area. Default empty. * @param {string} [siteId] Site ID. If not defined, current site. - * @return {Promise} Promise resolved when the notes are synced or if they don't need to be synced. + * @return {Promise} Promise resolved when the comments are synced or if they don't need to be synced. */ - private syncCommentIfNeeded(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', + private syncCommentsIfNeeded(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', siteId?: string): Promise { const syncId = this.getSyncId(contextLevel, instanceId, component, itemId, area); return this.isSyncNeeded(syncId, siteId).then((needed) => { if (needed) { - return this.syncComment(contextLevel, instanceId, component, itemId, area, siteId); + return this.syncComments(contextLevel, instanceId, component, itemId, area, siteId); } }); } /** - * Synchronize notes of a course. + * Synchronize comments in a particular area. * * @param {string} contextLevel Contextlevel system, course, user... * @param {number} instanceId The Instance id of item associated with the context level. @@ -122,14 +130,14 @@ export class CoreCommentsSyncProvider extends CoreSyncBaseProvider { * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved if sync is successful, rejected otherwise. */ - syncComment(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', + syncComments(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); const syncId = this.getSyncId(contextLevel, instanceId, component, itemId, area); if (this.isSyncing(syncId, siteId)) { - // There's already a sync ongoing for notes, return the promise. + // There's already a sync ongoing for comments, return the promise. return this.getOngoingSync(syncId, siteId); } @@ -138,9 +146,9 @@ export class CoreCommentsSyncProvider extends CoreSyncBaseProvider { const warnings = []; // Get offline comments to be sent. - const syncPromise = this.commentsOffline.getComment(contextLevel, instanceId, component, itemId, area, siteId) - .then((comment) => { - if (!comment) { + const syncPromise = this.commentsOffline.getComments(contextLevel, instanceId, component, itemId, area, siteId) + .then((comments) => { + if (!comments.length) { // Nothing to sync. return; } else if (!this.appProvider.isOnline()) { @@ -148,19 +156,31 @@ export class CoreCommentsSyncProvider extends CoreSyncBaseProvider { return Promise.reject(this.translate.instant('core.networkerrormsg')); } - const errors = []; - let commentsResponse = []; - let promise; + const errors = [], + promises = [], + deleteCommentIds = []; - if (comment.action == 'add') { - promise = this.commentsProvider.addCommentOnline(comment.content, contextLevel, instanceId, component, itemId, area, - siteId); + comments.forEach((comment) => { + if (comment.commentid) { + deleteCommentIds.push(comment.commentid); + } else { + promises.push(this.commentsProvider.addCommentOnline(comment.content, contextLevel, instanceId, component, + itemId, area, siteId).then((response) => { + return this.commentsOffline.removeComment(contextLevel, instanceId, component, itemId, area, siteId); + })); + } + }); + + if (deleteCommentIds.length > 0) { + promises.push(this.commentsProvider.deleteCommentsOnline(deleteCommentIds, contextLevel, instanceId, component, + itemId, area, siteId).then((response) => { + return this.commentsOffline.removeDeletedComments(contextLevel, instanceId, component, itemId, area, + siteId); + })); } // Send the comments. - return promise.then((response) => { - commentsResponse = response; - + return Promise.all(promises).then(() => { // Fetch the comments from server to be sure they're up to date. return this.commentsProvider.invalidateCommentsData(contextLevel, instanceId, component, itemId, area, siteId) .then(() => { @@ -171,27 +191,15 @@ export class CoreCommentsSyncProvider extends CoreSyncBaseProvider { }).catch((error) => { if (this.utils.isWebServiceError(error)) { // It's a WebService error, this means the user cannot send comments. - errors.push(error); + errors.push(error.message); } 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 = commentsResponse.map((comment) => { - return this.commentsOffline.removeComment(contextLevel, instanceId, component, itemId, area, siteId); - }); - - return Promise.all(promises); }).then(() => { if (errors && errors.length) { errors.forEach((error) => { - warnings.push(this.translate.instant('addon.notes.warningnotenotsent', { - contextLevel: contextLevel, - instanceId: instanceId, - componentName: component, - itemId: itemId, - area: area, + warnings.push(this.translate.instant('core.comments.warningcommentsnotsent', { error: error })); });