From 9e91cf14e62f240225d81c2112bed2930950080f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 1 Jul 2019 15:38:14 +0200 Subject: [PATCH 1/5] MOBILE-2877 comments: Add button --- scripts/langindex.json | 5 +++++ src/assets/lang/en.json | 9 +++++---- .../comments/components/comments/comments.scss | 4 ++++ .../comments/components/comments/comments.ts | 2 +- .../components/comments/core-comments.html | 6 +++--- src/core/comments/lang/en.json | 7 +++++++ src/core/comments/pages/viewer/viewer.html | 8 +++++++- src/core/comments/pages/viewer/viewer.ts | 9 +++++++-- src/core/comments/providers/comments.ts | 18 ++++++++++++++++++ src/lang/en.json | 4 ---- 10 files changed, 57 insertions(+), 15 deletions(-) create mode 100644 src/core/comments/components/comments/comments.scss create mode 100644 src/core/comments/lang/en.json diff --git a/scripts/langindex.json b/scripts/langindex.json index 5351ef509..9056e7451 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -1266,6 +1266,11 @@ "core.clicktoseefull": "local_moodlemobileapp", "core.close": "repository", "core.comments": "moodle", + "core.comments.addcomment": "moodle", + "core.comments.comments": "moodle", + "core.comments.commentscount": "moodle", + "core.comments.commentsnotworking": "local_moodlemobileapp", + "core.comments.nocomments": "moodle", "core.commentscount": "moodle", "core.commentsnotworking": "local_moodlemobileapp", "core.completion-alt-auto-fail": "completion", diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 21810db95..42b0102c1 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -1265,9 +1265,11 @@ "core.clicktohideshow": "Click to expand or collapse", "core.clicktoseefull": "Click to see full contents.", "core.close": "Close", - "core.comments": "Comments", - "core.commentscount": "Comments ({{$a}})", - "core.commentsnotworking": "Comments cannot be retrieved", + "core.comments.addcomment": "Add a comment...", + "core.comments.comments": "Comments", + "core.comments.commentscount": "Comments ({{$a}})", + "core.comments.commentsnotworking": "Comments cannot be retrieved", + "core.comments.nocomments": "No comments", "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}})", @@ -1618,7 +1620,6 @@ "core.never": "Never", "core.next": "Next", "core.no": "No", - "core.nocomments": "No comments", "core.nograde": "No grade", "core.none": "None", "core.nopasswordchangeforced": "You cannot proceed without changing your password.", diff --git a/src/core/comments/components/comments/comments.scss b/src/core/comments/components/comments/comments.scss new file mode 100644 index 000000000..cd7b0655a --- /dev/null +++ b/src/core/comments/components/comments/comments.scss @@ -0,0 +1,4 @@ +core-comments .core-comments-clickable { + pointer-events: auto; + cursor: pointer; +} \ No newline at end of file diff --git a/src/core/comments/components/comments/comments.ts b/src/core/comments/components/comments/comments.ts index 6538a0874..fed935d1d 100644 --- a/src/core/comments/components/comments/comments.ts +++ b/src/core/comments/components/comments/comments.ts @@ -100,7 +100,7 @@ export class CoreCommentsCommentsComponent implements OnChanges, OnDestroy { * Opens the comments page. */ openComments(): void { - if (!this.disabled && this.commentsCount > 0) { + if (!this.disabled && this.commentsCount >= 0) { // Open a new state with the interpolated contents. this.navCtrl.push('CoreCommentsViewerPage', { contextLevel: this.contextLevel, diff --git a/src/core/comments/components/comments/core-comments.html b/src/core/comments/components/comments/core-comments.html index e7b71e041..8642ffbb0 100644 --- a/src/core/comments/components/comments/core-comments.html +++ b/src/core/comments/components/comments/core-comments.html @@ -1,8 +1,8 @@ -
- {{ 'core.commentscount' | translate : {'$a': commentsCount} }} +
+ {{ 'core.comments.commentscount' | translate : {'$a': commentsCount} }}
- {{ 'core.commentsnotworking' | translate }} + {{ 'core.comments.commentsnotworking' | translate }}
diff --git a/src/core/comments/lang/en.json b/src/core/comments/lang/en.json new file mode 100644 index 000000000..0eb280877 --- /dev/null +++ b/src/core/comments/lang/en.json @@ -0,0 +1,7 @@ +{ + "addcomment": "Add a comment...", + "comments": "Comments", + "commentscount": "Comments ({{$a}})", + "commentsnotworking": "Comments cannot be retrieved", + "nocomments": "No comments" +} \ No newline at end of file diff --git a/src/core/comments/pages/viewer/viewer.html b/src/core/comments/pages/viewer/viewer.html index bcd909ecd..41e56725e 100644 --- a/src/core/comments/pages/viewer/viewer.html +++ b/src/core/comments/pages/viewer/viewer.html @@ -8,7 +8,7 @@ - + @@ -21,4 +21,10 @@ + + + + diff --git a/src/core/comments/pages/viewer/viewer.ts b/src/core/comments/pages/viewer/viewer.ts index 6652b879b..e9533a7d2 100644 --- a/src/core/comments/pages/viewer/viewer.ts +++ b/src/core/comments/pages/viewer/viewer.ts @@ -40,6 +40,7 @@ export class CoreCommentsViewerPage { area: string; page: number; title: string; + addCommentsAvailable = false; constructor(navParams: NavParams, sitesProvider: CoreSitesProvider, private userProvider: CoreUserProvider, private domUtils: CoreDomUtilsProvider, private translate: TranslateService, @@ -51,13 +52,17 @@ export class CoreCommentsViewerPage { this.itemId = navParams.get('itemId'); this.area = navParams.get('area') || ''; this.page = navParams.get('page') || 0; - this.title = navParams.get('title') || this.translate.instant('core.comments'); + this.title = navParams.get('title') || this.translate.instant('core.comments.comments'); } /** * View loaded. */ ionViewDidLoad(): void { + this.commentsProvider.isAddCommentsAvailable().then((enabled) => { + this.addCommentsAvailable = enabled; + }); + this.fetchComments().finally(() => { this.commentsLoaded = true; }); @@ -84,7 +89,7 @@ export class CoreCommentsViewerPage { }); }).catch((error) => { if (error && this.component == 'assignsubmission_comments') { - this.domUtils.showAlertTranslated('core.notice', 'core.commentsnotworking'); + this.domUtils.showAlertTranslated('core.notice', 'core.comments.commentsnotworking'); } else { this.domUtils.showErrorModalDefault(error, this.translate.instant('core.error') + ': get_comments'); } diff --git a/src/core/comments/providers/comments.ts b/src/core/comments/providers/comments.ts index 9808a0c83..cd671d84d 100644 --- a/src/core/comments/providers/comments.ts +++ b/src/core/comments/providers/comments.ts @@ -50,6 +50,24 @@ export class CoreCommentsProvider { }); } + /** + * Returns whether WS to add/delete comments are available in site. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with true if available, resolved with false or rejected otherwise. + * @since 3.8 + */ + isAddCommentsAvailable(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + // First check if it's disabled. + if (this.areCommentsDisabledInSite(site)) { + return false; + } + + return site.wsAvailable('core_comment_add_comments'); + }); + } + /** * Get cache key for get comments data WS calls. * diff --git a/src/lang/en.json b/src/lang/en.json index c4111f1be..3aceb76c7 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -25,9 +25,6 @@ "clicktohideshow": "Click to expand or collapse", "clicktoseefull": "Click to see full contents.", "close": "Close", - "comments": "Comments", - "commentscount": "Comments ({{$a}})", - "commentsnotworking": "Comments cannot be retrieved", "completion-alt-auto-fail": "Completed: {{$a}} (did not achieve pass grade)", "completion-alt-auto-n": "Not completed: {{$a}}", "completion-alt-auto-n-override": "Not completed: {{$a.modname}} (set by {{$a.overrideuser}})", @@ -168,7 +165,6 @@ "never": "Never", "next": "Next", "no": "No", - "nocomments": "No comments", "nograde": "No grade", "none": "None", "nopasswordchangeforced": "You cannot proceed without changing your password.", From dc65d4a00deb65547c89e3ec176dfa93e7a84234 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 1 Jul 2019 15:56:24 +0200 Subject: [PATCH 2/5] MOBILE-2877 comments: Invalidate comments count --- src/addon/blog/components/entries/entries.ts | 4 ++++ src/addon/mod/data/pages/entry/entry.ts | 4 ++++ .../comments/components/comments/comments.ts | 4 +--- src/core/comments/pages/viewer/viewer.ts | 2 +- src/core/comments/providers/comments.ts | 22 ++++++++++--------- 5 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/addon/blog/components/entries/entries.ts b/src/addon/blog/components/entries/entries.ts index b66db02a3..d737c8e1b 100644 --- a/src/addon/blog/components/entries/entries.ts +++ b/src/addon/blog/components/entries/entries.ts @@ -169,6 +169,10 @@ 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'); + }); + this.blogProvider.invalidateEntries(this.filter).finally(() => { this.fetchEntries(true).finally(() => { if (refresher) { diff --git a/src/addon/mod/data/pages/entry/entry.ts b/src/addon/mod/data/pages/entry/entry.ts index 51e95ce9c..2a938cbb1 100644 --- a/src/addon/mod/data/pages/entry/entry.ts +++ b/src/addon/mod/data/pages/entry/entry.ts @@ -218,6 +218,10 @@ export class AddonModDataEntryPage implements OnDestroy { promises.push(this.dataProvider.invalidateDatabaseData(this.courseId)); if (this.data) { + if (this.data.comments && this.entry && this.entry.id > 0 && this.commentsEnabled) { + promises.push(this.commentsProvider.invalidateCommentsData('module', this.data.coursemodule, 'mod_data', + this.entry.id, 'database_entry')); + } promises.push(this.dataProvider.invalidateEntryData(this.data.id, this.entryId)); promises.push(this.groupsProvider.invalidateActivityGroupInfo(this.data.coursemodule)); promises.push(this.dataProvider.invalidateEntriesData(this.data.id)); diff --git a/src/core/comments/components/comments/comments.ts b/src/core/comments/components/comments/comments.ts index fed935d1d..a0146c85a 100644 --- a/src/core/comments/components/comments/comments.ts +++ b/src/core/comments/components/comments/comments.ts @@ -31,7 +31,6 @@ export class CoreCommentsCommentsComponent implements OnChanges, OnDestroy { @Input() component: string; @Input() itemId: number; @Input() area = ''; - @Input() page = 0; @Input() title?: string; @Input() displaySpinner = true; // Whether to display the loading spinner. @Output() onLoading: EventEmitter; // Eevent that indicates whether the component is loading data. @@ -72,7 +71,7 @@ export class CoreCommentsCommentsComponent implements OnChanges, OnDestroy { */ ngOnChanges(changes: { [name: string]: SimpleChange }): void { // If something change, update the fields. - if (changes) { + if (changes && this.commentsLoaded) { this.fetchData(); } } @@ -108,7 +107,6 @@ export class CoreCommentsCommentsComponent implements OnChanges, OnDestroy { component: this.component, itemId: this.itemId, area: this.area, - page: this.page, title: this.title, }); } diff --git a/src/core/comments/pages/viewer/viewer.ts b/src/core/comments/pages/viewer/viewer.ts index e9533a7d2..74f31663a 100644 --- a/src/core/comments/pages/viewer/viewer.ts +++ b/src/core/comments/pages/viewer/viewer.ts @@ -51,8 +51,8 @@ export class CoreCommentsViewerPage { this.component = navParams.get('component'); this.itemId = navParams.get('itemId'); this.area = navParams.get('area') || ''; - this.page = navParams.get('page') || 0; this.title = navParams.get('title') || this.translate.instant('core.comments.comments'); + this.page = 0; } /** diff --git a/src/core/comments/providers/comments.ts b/src/core/comments/providers/comments.ts index cd671d84d..0279c711e 100644 --- a/src/core/comments/providers/comments.ts +++ b/src/core/comments/providers/comments.ts @@ -76,12 +76,11 @@ export class CoreCommentsProvider { * @param {string} component Component name. * @param {number} itemId Associated id. * @param {string} [area=''] String comment area. Default empty. - * @param {number} [page=0] Page number (0 based). Default 0. * @return {string} Cache key. */ - protected getCommentsCacheKey(contextLevel: string, instanceId: number, component: string, - itemId: number, area: string = '', page: number = 0): string { - return this.getCommentsPrefixCacheKey(contextLevel, instanceId) + ':' + component + ':' + itemId + ':' + area + ':' + page; + protected getCommentsCacheKey(contextLevel: string, instanceId: number, component: string, itemId: number, + area: string = ''): string { + return this.getCommentsPrefixCacheKey(contextLevel, instanceId) + ':' + component + ':' + itemId + ':' + area; } /** @@ -107,8 +106,8 @@ export class CoreCommentsProvider { * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved with the comments. */ - getComments(contextLevel: string, instanceId: number, component: string, itemId: number, - area: string = '', page: number = 0, siteId?: string): Promise { + getComments(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', page: number = 0, + siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params: any = { contextlevel: contextLevel, @@ -120,7 +119,7 @@ export class CoreCommentsProvider { }; const preSets = { - cacheKey: this.getCommentsCacheKey(contextLevel, instanceId, component, itemId, area, page), + cacheKey: this.getCommentsCacheKey(contextLevel, instanceId, component, itemId, area), updateFrequency: CoreSite.FREQUENCY_SOMETIMES }; @@ -142,14 +141,17 @@ export class CoreCommentsProvider { * @param {string} component Component name. * @param {number} itemId Associated id. * @param {string} [area=''] String comment area. Default empty. - * @param {number} [page=0] Page number (0 based). Default 0. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when the data is invalidated. */ invalidateCommentsData(contextLevel: string, instanceId: number, component: string, itemId: number, - area: string = '', page: number = 0, siteId?: string): Promise { + area: string = '', siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { - return site.invalidateWsCacheForKey(this.getCommentsCacheKey(contextLevel, instanceId, component, itemId, area, page)); + // 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)); }); } From 797b0d7931827b71fc1fcf0f88b6c5f49b0e5a18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 2 Jul 2019 12:46:59 +0200 Subject: [PATCH 3/5] MOBILE-2877 comments: Add comments pagination --- src/core/comments/comments.module.ts | 11 +++- .../comments/components/comments/comments.ts | 21 ++++--- .../components/comments/core-comments.html | 4 +- src/core/comments/pages/viewer/viewer.html | 4 +- src/core/comments/pages/viewer/viewer.ts | 41 +++++++++++-- src/core/comments/providers/comments.ts | 60 ++++++++++++++++++- 6 files changed, 120 insertions(+), 21 deletions(-) diff --git a/src/core/comments/comments.module.ts b/src/core/comments/comments.module.ts index 980e458b8..3dc800d05 100644 --- a/src/core/comments/comments.module.ts +++ b/src/core/comments/comments.module.ts @@ -14,6 +14,7 @@ import { NgModule } from '@angular/core'; import { CoreCommentsProvider } from './providers/comments'; +import { CoreEventsProvider } from '@providers/events'; @NgModule({ declarations: [ @@ -24,4 +25,12 @@ import { CoreCommentsProvider } from './providers/comments'; CoreCommentsProvider ] }) -export class CoreCommentsModule {} +export class CoreCommentsModule { + constructor(eventsProvider: CoreEventsProvider) { + // Reset comments page size. + eventsProvider.on(CoreEventsProvider.LOGIN, () => { + CoreCommentsProvider.pageSize = null; + CoreCommentsProvider.pageSizeOK = false; + }); + } +} \ No newline at end of file diff --git a/src/core/comments/components/comments/comments.ts b/src/core/comments/components/comments/comments.ts index a0146c85a..b380d8f32 100644 --- a/src/core/comments/components/comments/comments.ts +++ b/src/core/comments/components/comments/comments.ts @@ -36,7 +36,8 @@ export class CoreCommentsCommentsComponent implements OnChanges, OnDestroy { @Output() onLoading: EventEmitter; // Eevent that indicates whether the component is loading data. commentsLoaded = false; - commentsCount: number; + commentsCount: string; + countError = false; disabled = false; protected updateSiteObserver; @@ -84,22 +85,20 @@ export class CoreCommentsCommentsComponent implements OnChanges, OnDestroy { this.commentsLoaded = false; this.onLoading.emit(true); - this.commentsProvider.getComments(this.contextLevel, this.instanceId, this.component, this.itemId, this.area, this.page) - .then((comments) => { - this.commentsCount = comments && comments.length ? comments.length : 0; - }).catch(() => { - this.commentsCount = -1; - }).finally(() => { - this.commentsLoaded = true; - this.onLoading.emit(false); - }); + this.commentsProvider.getCommentsCount(this.contextLevel, this.instanceId, this.component, this.itemId, this.area) + .then((commentsCount) => { + this.commentsCount = commentsCount; + this.countError = parseInt(this.commentsCount, 10) < 0; + this.commentsLoaded = true; + this.onLoading.emit(false); + }); } /** * Opens the comments page. */ openComments(): void { - if (!this.disabled && this.commentsCount >= 0) { + if (!this.disabled && !this.countError) { // Open a new state with the interpolated contents. this.navCtrl.push('CoreCommentsViewerPage', { contextLevel: this.contextLevel, diff --git a/src/core/comments/components/comments/core-comments.html b/src/core/comments/components/comments/core-comments.html index 8642ffbb0..2c2c8efeb 100644 --- a/src/core/comments/components/comments/core-comments.html +++ b/src/core/comments/components/comments/core-comments.html @@ -1,8 +1,8 @@ -
+
{{ 'core.comments.commentscount' | translate : {'$a': commentsCount} }}
-
+
{{ 'core.comments.commentsnotworking' | translate }}
diff --git a/src/core/comments/pages/viewer/viewer.html b/src/core/comments/pages/viewer/viewer.html index 41e56725e..cb825c4dd 100644 --- a/src/core/comments/pages/viewer/viewer.html +++ b/src/core/comments/pages/viewer/viewer.html @@ -20,9 +20,11 @@ + + - + diff --git a/src/core/comments/pages/viewer/viewer.ts b/src/core/comments/pages/viewer/viewer.ts index 74f31663a..d253fbcb8 100644 --- a/src/core/comments/pages/viewer/viewer.ts +++ b/src/core/comments/pages/viewer/viewer.ts @@ -40,7 +40,11 @@ export class CoreCommentsViewerPage { area: string; page: number; title: string; - addCommentsAvailable = false; + canLoadMore = false; + loadMoreError = false; + canAddComments = false; + + protected addCommentsAvailable = false; constructor(navParams: NavParams, sitesProvider: CoreSitesProvider, private userProvider: CoreUserProvider, private domUtils: CoreDomUtilsProvider, private translate: TranslateService, @@ -74,11 +78,16 @@ export class CoreCommentsViewerPage { * @return {Promise} Resolved when done. */ protected fetchComments(): Promise { + this.loadMoreError = false; + // Get comments data. return this.commentsProvider.getComments(this.contextLevel, this.instanceId, this.component, this.itemId, - this.area, this.page).then((comments) => { - this.comments = comments; - this.comments.sort((a, b) => b.timecreated - a.timecreated); + this.area, this.page).then((response) => { + this.canAddComments = this.addCommentsAvailable && response.canpost; + + const comments = response.comments.sort((a, b) => b.timecreated - a.timecreated); + this.canLoadMore = comments.length >= CoreCommentsProvider.pageSize; + this.comments.forEach((comment) => { // Get the user profile image. this.userProvider.getProfile(comment.userid, undefined, true).then((user) => { @@ -87,7 +96,11 @@ export class CoreCommentsViewerPage { // Ignore errors. }); }); + + this.comments = this.comments.concat(comments); + }).catch((error) => { + this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading. if (error && this.component == 'assignsubmission_comments') { this.domUtils.showAlertTranslated('core.notice', 'core.comments.commentsnotworking'); } else { @@ -96,6 +109,21 @@ export class CoreCommentsViewerPage { }); } + /** + * Function to load more cp,,emts. + * + * @param {any} [infiniteComplete] Infinite scroll complete function. Only used from core-infinite-loading. + * @return {Promise} Resolved when done. + */ + loadMore(infiniteComplete?: any): Promise { + this.page++; + this.canLoadMore = false; + + return this.fetchComments().finally(() => { + infiniteComplete && infiniteComplete(); + }); + } + /** * Refresh the comments. * @@ -103,7 +131,10 @@ export class CoreCommentsViewerPage { */ refreshComments(refresher: any): void { this.commentsProvider.invalidateCommentsData(this.contextLevel, this.instanceId, this.component, - this.itemId, this.area, this.page).finally(() => { + this.itemId, this.area).finally(() => { + this.page = 0; + this.comments = []; + return this.fetchComments().finally(() => { refresher.complete(); }); diff --git a/src/core/comments/providers/comments.ts b/src/core/comments/providers/comments.ts index 0279c711e..e5634bf26 100644 --- a/src/core/comments/providers/comments.ts +++ b/src/core/comments/providers/comments.ts @@ -23,6 +23,8 @@ import { CoreSite } from '@classes/site'; export class CoreCommentsProvider { protected ROOT_CACHE_KEY = 'mmComments:'; + static pageSize = null; + static pageSizeOK = false; // If true, the pageSize is definitive. If not, it's a temporal value to reduce WS calls. constructor(private sitesProvider: CoreSitesProvider) {} @@ -125,7 +127,7 @@ export class CoreCommentsProvider { return site.read('core_comment_get_comments', params, preSets).then((response) => { if (response.comments) { - return response.comments; + return response; } return Promise.reject(null); @@ -133,6 +135,62 @@ export class CoreCommentsProvider { }); } + /** + * Get comments count number to show ont he comments component. + * + * @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} Comments count with plus sign if needed. + */ + getCommentsCount(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', + siteId?: string): Promise { + + siteId = siteId ? siteId : this.sitesProvider.getCurrentSiteId(); + + // Convenience function to get comments number on a page. + const getCommentsPageCount = (page: number): Promise => { + return this.getComments(contextLevel, instanceId, component, itemId, area, page, siteId).then((response) => { + if (response.comments) { + // Update pageSize with the greatest count at the moment. + if (response.comments && response.comments.length > CoreCommentsProvider.pageSize) { + CoreCommentsProvider.pageSize = response.comments.length; + } + + return response.comments && response.comments.length ? response.comments.length : 0; + } + + return -1; + }).catch(() => { + return -1; + }); + }; + + return getCommentsPageCount(0).then((count) => { + if (CoreCommentsProvider.pageSizeOK && count >= CoreCommentsProvider.pageSize) { + // Page Size is ok, show + in case it reached the limit. + return (CoreCommentsProvider.pageSize - 1) + '+'; + } else if (count < 0 || (CoreCommentsProvider.pageSize && count < CoreCommentsProvider.pageSize)) { + return count + ''; + } + + // Call to update page size. + return getCommentsPageCount(1).then((countMore) => { + // Page limit was reached on the previous call. + if (countMore > 0) { + CoreCommentsProvider.pageSizeOK = true; + + return (CoreCommentsProvider.pageSize - 1) + '+'; + } + + return count + ''; + }); + }); + } + /** * Invalidates comments data. * From 79bdd4ed02f7ca76ff22b56cfb0f8ee053c61e0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 2 Jul 2019 14:04:13 +0200 Subject: [PATCH 4/5] MOBILE-2877 comments: Add comments --- src/assets/lang/en.json | 2 + src/core/comments/comments.module.ts | 17 +- .../comments/components/comments/comments.ts | 2 +- src/core/comments/lang/en.json | 4 +- src/core/comments/pages/add/add.html | 22 ++ src/core/comments/pages/add/add.module.ts | 31 +++ src/core/comments/pages/add/add.ts | 82 +++++++ src/core/comments/pages/viewer/viewer.html | 30 ++- .../comments/pages/viewer/viewer.module.ts | 2 + src/core/comments/pages/viewer/viewer.ts | 202 +++++++++++++--- src/core/comments/providers/comments.ts | 105 ++++++++- src/core/comments/providers/offline.ts | 187 +++++++++++++++ .../comments/providers/sync-cron-handler.ts | 48 ++++ src/core/comments/providers/sync.ts | 221 ++++++++++++++++++ 14 files changed, 912 insertions(+), 43 deletions(-) create mode 100644 src/core/comments/pages/add/add.html create mode 100644 src/core/comments/pages/add/add.module.ts create mode 100644 src/core/comments/pages/add/add.ts create mode 100644 src/core/comments/providers/offline.ts create mode 100644 src/core/comments/providers/sync-cron-handler.ts create mode 100644 src/core/comments/providers/sync.ts diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 42b0102c1..4ada12bc3 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -1269,7 +1269,9 @@ "core.comments.comments": "Comments", "core.comments.commentscount": "Comments ({{$a}})", "core.comments.commentsnotworking": "Comments cannot be retrieved", + "core.comments.eventcommentcreated": "Comment created", "core.comments.nocomments": "No comments", + "core.comments.savecomment": "Save comment", "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/comments.module.ts b/src/core/comments/comments.module.ts index 3dc800d05..d19d2ccf3 100644 --- a/src/core/comments/comments.module.ts +++ b/src/core/comments/comments.module.ts @@ -13,8 +13,12 @@ // limitations under the License. import { NgModule } from '@angular/core'; -import { CoreCommentsProvider } from './providers/comments'; import { CoreEventsProvider } from '@providers/events'; +import { CoreCronDelegate } from '@providers/cron'; +import { CoreCommentsProvider } from './providers/comments'; +import { CoreCommentsOfflineProvider } from './providers/offline'; +import { CoreCommentsSyncCronHandler } from './providers/sync-cron-handler'; +import { CoreCommentsSyncProvider } from './providers/sync'; @NgModule({ declarations: [ @@ -22,15 +26,20 @@ import { CoreEventsProvider } from '@providers/events'; imports: [ ], providers: [ - CoreCommentsProvider + CoreCommentsProvider, + CoreCommentsOfflineProvider, + CoreCommentsSyncProvider, + CoreCommentsSyncCronHandler ] }) export class CoreCommentsModule { - constructor(eventsProvider: CoreEventsProvider) { + constructor(eventsProvider: CoreEventsProvider, cronDelegate: CoreCronDelegate, syncHandler: CoreCommentsSyncCronHandler) { // Reset comments page size. eventsProvider.on(CoreEventsProvider.LOGIN, () => { CoreCommentsProvider.pageSize = null; CoreCommentsProvider.pageSizeOK = false; }); + + cronDelegate.register(syncHandler); } -} \ No newline at end of file +} diff --git a/src/core/comments/components/comments/comments.ts b/src/core/comments/components/comments/comments.ts index b380d8f32..6f7a22444 100644 --- a/src/core/comments/components/comments/comments.ts +++ b/src/core/comments/components/comments/comments.ts @@ -103,7 +103,7 @@ export class CoreCommentsCommentsComponent implements OnChanges, OnDestroy { this.navCtrl.push('CoreCommentsViewerPage', { contextLevel: this.contextLevel, instanceId: this.instanceId, - component: this.component, + componentName: this.component, itemId: this.itemId, area: this.area, title: this.title, diff --git a/src/core/comments/lang/en.json b/src/core/comments/lang/en.json index 0eb280877..b6c99e4c3 100644 --- a/src/core/comments/lang/en.json +++ b/src/core/comments/lang/en.json @@ -3,5 +3,7 @@ "comments": "Comments", "commentscount": "Comments ({{$a}})", "commentsnotworking": "Comments cannot be retrieved", - "nocomments": "No comments" + "eventcommentcreated": "Comment created", + "nocomments": "No comments", + "savecomment": "Save comment" } \ No newline at end of file diff --git a/src/core/comments/pages/add/add.html b/src/core/comments/pages/add/add.html new file mode 100644 index 000000000..b5ca49440 --- /dev/null +++ b/src/core/comments/pages/add/add.html @@ -0,0 +1,22 @@ + + + {{ 'core.comments.addcomment' | translate }} + + + + + + +
+ + + +
+ +
+
+
diff --git a/src/core/comments/pages/add/add.module.ts b/src/core/comments/pages/add/add.module.ts new file mode 100644 index 000000000..a6b6661a0 --- /dev/null +++ b/src/core/comments/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 { CoreCommentsAddPage } from './add'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +@NgModule({ + declarations: [ + CoreCommentsAddPage + ], + imports: [ + CoreDirectivesModule, + IonicPageModule.forChild(CoreCommentsAddPage), + TranslateModule.forChild() + ] +}) +export class CoreCommentsAddPageModule {} diff --git a/src/core/comments/pages/add/add.ts b/src/core/comments/pages/add/add.ts new file mode 100644 index 000000000..1d58db46f --- /dev/null +++ b/src/core/comments/pages/add/add.ts @@ -0,0 +1,82 @@ +// (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 { CoreCommentsProvider } from '../../providers/comments'; + +/** + * Component that displays a text area for composing a comment. + */ +@IonicPage({ segment: 'core-comments-add' }) +@Component({ + selector: 'page-core-comments-add', + templateUrl: 'add.html', +}) +export class CoreCommentsAddPage { + protected contextLevel: string; + protected instanceId: number; + protected componentName: string; + protected itemId: number; + protected area = ''; + + content = ''; + processing = false; + + constructor(params: NavParams, private viewCtrl: ViewController, private appProvider: CoreAppProvider, + private domUtils: CoreDomUtilsProvider, private commentsProvider: CoreCommentsProvider) { + this.contextLevel = params.get('contextLevel'); + this.instanceId = params.get('instanceId'); + this.componentName = params.get('componentName'); + this.itemId = params.get('itemId'); + this.area = params.get('area') || ''; + this.content = params.get('content') || ''; + } + + /** + * Send the comment or store it offline. + * + * @param {Event} e Event. + */ + addComment(e: Event): void { + e.preventDefault(); + e.stopPropagation(); + + this.appProvider.closeKeyboard(); + const loadingModal = this.domUtils.showModalLoading('core.sending', true); + // Freeze the add note button. + this.processing = true; + this.commentsProvider.addComment(this.content, this.contextLevel, this.instanceId, this.componentName, this.itemId, + this.area).then((commentsResponse) => { + this.viewCtrl.dismiss({comments: commentsResponse}).finally(() => { + this.domUtils.showToast(commentsResponse ? 'core.comments.eventcommentcreated' : 'core.datastoredoffline', true, + 3000); + }); + }).catch((error) => { + this.domUtils.showErrorModal(error); + this.processing = false; + }).finally(() => { + loadingModal.dismiss(); + }); + } + + /** + * Close modal. + */ + closeModal(): void { + this.viewCtrl.dismiss(); + } +} diff --git a/src/core/comments/pages/viewer/viewer.html b/src/core/comments/pages/viewer/viewer.html index cb825c4dd..812838695 100644 --- a/src/core/comments/pages/viewer/viewer.html +++ b/src/core/comments/pages/viewer/viewer.html @@ -1,20 +1,44 @@ + + + + + + - + +
+ + {{ 'core.thereisdatatosync' | translate:{$a: 'core.comments.comments' | translate | lowercase } }} +
+ + + + +

{{ offlineComment.fullname }}

+

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

+
+ + + +
+

{{ comment.fullname }}

-

{{ comment.time }}

+

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

@@ -25,7 +49,7 @@
- diff --git a/src/core/comments/pages/viewer/viewer.module.ts b/src/core/comments/pages/viewer/viewer.module.ts index ca5267970..3326cfe19 100644 --- a/src/core/comments/pages/viewer/viewer.module.ts +++ b/src/core/comments/pages/viewer/viewer.module.ts @@ -18,6 +18,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { CoreCommentsViewerPage } from './viewer'; import { CoreComponentsModule } from '@components/components.module'; import { CoreDirectivesModule } from '@directives/directives.module'; +import { CorePipesModule } from '@pipes/pipes.module'; import { CoreCommentsComponentsModule } from '../../components/components.module'; @NgModule({ @@ -27,6 +28,7 @@ import { CoreCommentsComponentsModule } from '../../components/components.module imports: [ CoreComponentsModule, CoreDirectivesModule, + CorePipesModule, CoreCommentsComponentsModule, IonicPageModule.forChild(CoreCommentsViewerPage), TranslateModule.forChild() diff --git a/src/core/comments/pages/viewer/viewer.ts b/src/core/comments/pages/viewer/viewer.ts index d253fbcb8..50bbf004f 100644 --- a/src/core/comments/pages/viewer/viewer.ts +++ b/src/core/comments/pages/viewer/viewer.ts @@ -12,13 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, ViewChild } from '@angular/core'; -import { IonicPage, Content, NavParams } from 'ionic-angular'; +import { Component, ViewChild, OnDestroy } from '@angular/core'; +import { IonicPage, Content, NavParams, ModalController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreEventsProvider } from '@providers/events'; import { CoreUserProvider } from '@core/user/providers/user'; import { CoreCommentsProvider } from '../../providers/comments'; +import { CoreCommentsOfflineProvider } from '../../providers/offline'; +import { CoreCommentsSyncProvider } from '../../providers/sync'; /** * Page that displays comments. @@ -28,14 +32,14 @@ import { CoreCommentsProvider } from '../../providers/comments'; selector: 'page-core-comments-viewer', templateUrl: 'viewer.html', }) -export class CoreCommentsViewerPage { +export class CoreCommentsViewerPage implements OnDestroy { @ViewChild(Content) content: Content; comments = []; commentsLoaded = false; contextLevel: string; instanceId: number; - component: string; + componentName: string; itemId: number; area: string; page: number; @@ -43,20 +47,48 @@ export class CoreCommentsViewerPage { canLoadMore = false; loadMoreError = false; canAddComments = false; + hasOffline = false; + refreshIcon = 'spinner'; + syncIcon = 'spinner'; + offlineComment: any; protected addCommentsAvailable = false; + protected syncObserver: any; + protected currentUser: any; - constructor(navParams: NavParams, sitesProvider: CoreSitesProvider, private userProvider: CoreUserProvider, - private domUtils: CoreDomUtilsProvider, private translate: TranslateService, - private commentsProvider: CoreCommentsProvider) { + constructor(navParams: NavParams, private sitesProvider: CoreSitesProvider, private userProvider: CoreUserProvider, + private domUtils: CoreDomUtilsProvider, private translate: TranslateService, private modalCtrl: ModalController, + private commentsProvider: CoreCommentsProvider, private offlineComments: CoreCommentsOfflineProvider, + eventsProvider: CoreEventsProvider, private commentsSync: CoreCommentsSyncProvider, + private textUtils: CoreTextUtilsProvider) { this.contextLevel = navParams.get('contextLevel'); this.instanceId = navParams.get('instanceId'); - this.component = navParams.get('component'); + this.componentName = navParams.get('componentName'); this.itemId = navParams.get('itemId'); this.area = navParams.get('area') || ''; this.title = navParams.get('title') || this.translate.instant('core.comments.comments'); this.page = 0; + + // Refresh data if comments are synchronized automatically. + this.syncObserver = eventsProvider.on(CoreCommentsSyncProvider.AUTO_SYNCED, (data) => { + if (data.contextLevel == this.contextLevel && data.instanceId == this.instanceId && + data.componentName == this.componentName && data.itemId == this.itemId && data.area == this.area) { + // Show the sync warnings. + this.showSyncWarnings(data.warnings); + + // Refresh the data. + this.commentsLoaded = false; + this.refreshIcon = 'spinner'; + this.syncIcon = 'spinner'; + + this.domUtils.scrollToTop(this.content); + + this.page = 0; + this.comments = []; + this.fetchComments(false); + } + }, sitesProvider.getCurrentSiteId()); } /** @@ -67,46 +99,78 @@ export class CoreCommentsViewerPage { this.addCommentsAvailable = enabled; }); - this.fetchComments().finally(() => { - this.commentsLoaded = true; - }); + this.fetchComments(true); } /** * Fetches the comments. * + * @param {boolean} sync When to resync notes. + * @param {boolean} [showErrors] When to display errors or not. * @return {Promise} Resolved when done. */ - protected fetchComments(): Promise { + protected fetchComments(sync: boolean, showErrors?: boolean): Promise { this.loadMoreError = false; - // Get comments data. - return this.commentsProvider.getComments(this.contextLevel, this.instanceId, this.component, this.itemId, - this.area, this.page).then((response) => { - this.canAddComments = this.addCommentsAvailable && response.canpost; + const promise = sync ? this.syncComment(showErrors) : Promise.resolve(); - const comments = response.comments.sort((a, b) => b.timecreated - a.timecreated); - this.canLoadMore = comments.length >= CoreCommentsProvider.pageSize; + 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; - this.comments.forEach((comment) => { - // Get the user profile image. - this.userProvider.getProfile(comment.userid, undefined, true).then((user) => { - comment.profileimageurl = user.profileimageurl; - }).catch(() => { - // Ignore errors. - }); + if (this.hasOffline && !this.currentUser) { + return this.userProvider.getProfile(this.sitesProvider.getCurrentSiteUserId(), undefined, true).then((user) => { + this.currentUser = user; + this.offlineComment.profileimageurl = user.profileimageurl; + this.offlineComment.fullname = user.fullname; + this.offlineComment.userid = user.id; + }).catch(() => { + // Ignore errors. + }); + } else if (this.hasOffline) { + this.offlineComment.profileimageurl = this.currentUser.profileimageurl; + this.offlineComment.fullname = this.currentUser.fullname; + this.offlineComment.userid = this.currentUser.id; + } }); + }).then(() => { - this.comments = this.comments.concat(comments); + // 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; + const comments = response.comments.sort((a, b) => b.timecreated - a.timecreated); + this.canLoadMore = comments.length >= CoreCommentsProvider.pageSize; + + this.comments.forEach((comment) => { + // Get the user profile image. + this.userProvider.getProfile(comment.userid, undefined, true).then((user) => { + comment.profileimageurl = user.profileimageurl; + }).catch(() => { + // Ignore errors. + }); + }); + + this.comments = this.comments.concat(comments); + }); }).catch((error) => { this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading. - if (error && this.component == 'assignsubmission_comments') { + if (error && this.componentName == 'assignsubmission_comments') { this.domUtils.showAlertTranslated('core.notice', 'core.comments.commentsnotworking'); } else { this.domUtils.showErrorModalDefault(error, this.translate.instant('core.error') + ': get_comments'); } + }).finally(() => { + this.commentsLoaded = true; + this.refreshIcon = 'refresh'; + this.syncIcon = 'sync'; }); + } /** @@ -119,7 +183,7 @@ export class CoreCommentsViewerPage { this.page++; this.canLoadMore = false; - return this.fetchComments().finally(() => { + return this.fetchComments(true).finally(() => { infiniteComplete && infiniteComplete(); }); } @@ -127,17 +191,89 @@ export class CoreCommentsViewerPage { /** * Refresh the comments. * - * @param {any} refresher Refresher. + * @param {boolean} showErrors Whether to display errors or not. + * @param {any} [refresher] Refresher. + * @return {Promise} Resolved when done. */ - refreshComments(refresher: any): void { - this.commentsProvider.invalidateCommentsData(this.contextLevel, this.instanceId, this.component, + refreshComments(showErrors: boolean, refresher?: any): Promise { + this.refreshIcon = 'spinner'; + this.syncIcon = 'spinner'; + + return this.commentsProvider.invalidateCommentsData(this.contextLevel, this.instanceId, this.componentName, this.itemId, this.area).finally(() => { this.page = 0; this.comments = []; - return this.fetchComments().finally(() => { - refresher.complete(); + return this.fetchComments(true, showErrors).finally(() => { + refresher && refresher.complete(); }); }); } + + /** + * 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); + } + } + + /** + * Tries to synchronize comments. + * + * @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, + this.area).then((warnings) => { + this.showSyncWarnings(warnings); + }).catch((error) => { + if (showErrors) { + this.domUtils.showErrorModalDefault(error, 'core.errorsync', true); + } + + return Promise.reject(null); + }); + } + + /** + * Add a new comment to the list. + * + * @param {Event} e Event. + */ + addComment(e: Event): void { + e.preventDefault(); + e.stopPropagation(); + + const params = { + contextLevel: this.contextLevel, + instanceId: this.instanceId, + componentName: this.componentName, + itemId: this.itemId, + area: this.area, + content: this.hasOffline ? this.offlineComment.content : '' + }; + + const modal = this.modalCtrl.create('CoreCommentsAddPage', params); + modal.onDidDismiss((data) => { + if (data && data.comments) { + this.comments = data.comments.concat(this.comments); + } else if (data && !data.comments) { + this.fetchComments(false); + } + }); + modal.present(); + } + + /** + * Page destroyed. + */ + ngOnDestroy(): void { + this.syncObserver && this.syncObserver.off(); + } } diff --git a/src/core/comments/providers/comments.ts b/src/core/comments/providers/comments.ts index e5634bf26..a17748b08 100644 --- a/src/core/comments/providers/comments.ts +++ b/src/core/comments/providers/comments.ts @@ -13,8 +13,11 @@ // limitations under the License. import { Injectable } from '@angular/core'; +import { CoreAppProvider } from '@providers/app'; +import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreSitesProvider } from '@providers/sites'; import { CoreSite } from '@classes/site'; +import { CoreCommentsOfflineProvider } from './offline'; /** * Service that provides some features regarding comments. @@ -26,7 +29,107 @@ export class CoreCommentsProvider { static pageSize = null; static pageSizeOK = false; // If true, the pageSize is definitive. If not, it's a temporal value to reduce WS calls. - constructor(private sitesProvider: CoreSitesProvider) {} + constructor(private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider, private appProvider: CoreAppProvider, + private commentsOffline: CoreCommentsOfflineProvider) {} + + /** + * Add a comment. + * + * @param {string} content Comment text. + * @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 boolean: true if comment was sent to server, false if stored in device. + */ + addComment(content: string, contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', + siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + // Convenience function to store a note to be synchronized later. + const storeOffline = (): Promise => { + return this.commentsOffline.saveComment(content, contextLevel, instanceId, component, itemId, area, siteId).then(() => { + return Promise.resolve(false); + }); + }; + + if (!this.appProvider.isOnline()) { + // App is offline, store the note. + return storeOffline(); + } + + // Send note to server. + return this.addCommentOnline(content, contextLevel, instanceId, component, itemId, area, siteId).then((comments) => { + return comments; + }).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(); + }); + } + + /** + * Add a comment. It will fail if offline or cannot connect. + * + * @param {string} content Comment text. + * @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 added, rejected otherwise. + */ + addCommentOnline(content: string, contextLevel: string, instanceId: number, component: string, itemId: number, + area: string = '', siteId?: string): Promise { + const comments = [ + { + contextlevel: contextLevel, + instanceid: instanceId, + component: component, + itemid: itemId, + area: area, + content: content + } + ]; + + return this.addCommentsOnline(comments, siteId).then((commentsResponse) => { + // A cooment was added, invalidate them. + return this.invalidateCommentsData(contextLevel, instanceId, component, itemId, area, siteId).catch(() => { + // Ignore errors. + }).then(() => { + return commentsResponse; + }); + }); + } + + /** + * Add several comments. It will fail if offline or cannot connect. + * + * @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. + */ + addCommentsOnline(comments: any[], siteId?: string): Promise { + if (!comments || !comments.length) { + return Promise.resolve(); + } + + return this.sitesProvider.getSite(siteId).then((site) => { + const data = { + comments: comments + }; + + return site.write('core_comment_add_comments', data); + }); + } /** * Check if Calendar is disabled in a certain site. diff --git a/src/core/comments/providers/offline.ts b/src/core/comments/providers/offline.ts new file mode 100644 index 000000000..82dbd6894 --- /dev/null +++ b/src/core/comments/providers/offline.ts @@ -0,0 +1,187 @@ +// (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 { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; + +/** + * Service to handle offline comments. + */ +@Injectable() +export class CoreCommentsOfflineProvider { + + // Variables for database. + static COMMENTS_TABLE = 'core_comments_offline_comments'; + protected siteSchema: CoreSiteSchema = { + name: 'CoreCommentsOfflineProvider', + version: 1, + tables: [ + { + name: CoreCommentsOfflineProvider.COMMENTS_TABLE, + columns: [ + { + name: 'contextlevel', + type: 'TEXT' + }, + { + name: 'instanceid', + type: 'INTEGER' + }, + { + name: 'component', + type: 'TEXT', + }, + { + name: 'itemid', + type: 'INTEGER' + }, + { + name: 'area', + type: 'TEXT' + }, + { + name: 'content', + type: 'TEXT' + }, + { + name: 'action', + type: 'TEXT' + }, + { + name: 'lastmodified', + type: 'INTEGER' + } + ], + primaryKeys: ['contextlevel', 'instanceid', 'component', 'itemid', 'area'] + } + ] + }; + + constructor( private sitesProvider: CoreSitesProvider, private timeUtils: CoreTimeUtilsProvider) { + 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. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with comments. + */ + getAllComments(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecords(CoreCommentsOfflineProvider.COMMENTS_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. + */ + getComment(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', + siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecord(CoreCommentsOfflineProvider.COMMENTS_TABLE, { + contextlevel: contextLevel, + instanceid: instanceId, + component: component, + itemid: itemId, + area: area + }); + }).catch(() => { + return false; + }); + } + + /** + * Check if there are offline comments. + * + * @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. + */ + 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; + }); + } + + /** + * Save a comment to be sent later. + * + * @param {string} content Comment text. + * @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. + */ + saveComment(content: string, 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, + content: content, + action: 'add', + lastmodified: now + }; + + return site.getDb().insertRecord(CoreCommentsOfflineProvider.COMMENTS_TABLE, data).then(() => { + return data; + }); + }); + } +} diff --git a/src/core/comments/providers/sync-cron-handler.ts b/src/core/comments/providers/sync-cron-handler.ts new file mode 100644 index 000000000..5803f936e --- /dev/null +++ b/src/core/comments/providers/sync-cron-handler.ts @@ -0,0 +1,48 @@ +// (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 { CoreCommentsSyncProvider } from './sync'; + +/** + * Synchronization cron handler. + */ +@Injectable() +export class CoreCommentsSyncCronHandler implements CoreCronHandler { + name = 'CoreCommentsSyncCronHandler'; + + constructor(private commentsSync: CoreCommentsSyncProvider) {} + + /** + * 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. + * @param {boolean} [force] Wether the execution is forced (manual sync). + * @return {Promise} Promise resolved when done, rejected if failure. + */ + execute(siteId?: string, force?: boolean): Promise { + return this.commentsSync.syncAllComments(siteId, force); + } + + /** + * Get the time between consecutive executions. + * + * @return {number} Time between consecutive executions (in ms). + */ + getInterval(): number { + return 300000; // 5 minutes. + } +} diff --git a/src/core/comments/providers/sync.ts b/src/core/comments/providers/sync.ts new file mode 100644 index 000000000..ab43a6d4b --- /dev/null +++ b/src/core/comments/providers/sync.ts @@ -0,0 +1,221 @@ +// (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 { 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'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreSyncProvider } from '@providers/sync'; + +/** + * Service to sync omments. + */ +@Injectable() +export class CoreCommentsSyncProvider extends CoreSyncBaseProvider { + + static AUTO_SYNCED = 'core_comments_autom_synced'; + + constructor(loggerProvider: CoreLoggerProvider, sitesProvider: CoreSitesProvider, appProvider: CoreAppProvider, + syncProvider: CoreSyncProvider, textUtils: CoreTextUtilsProvider, translate: TranslateService, + private commentsOffline: CoreCommentsOfflineProvider, private utils: CoreUtilsProvider, + private eventsProvider: CoreEventsProvider, private commentsProvider: CoreCommentsProvider, + private coursesProvider: CoreCoursesProvider, timeUtils: CoreTimeUtilsProvider) { + + super('CoreCommentsSync', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate, timeUtils); + } + + /** + * Try to synchronize all the comments in a certain site or in all sites. + * + * @param {string} [siteId] Site ID to sync. If not defined, sync all sites. + * @param {boolean} [force] Wether to force sync not depending on last execution. + * @return {Promise} Promise resolved if sync is successful, rejected if sync fails. + */ + syncAllComments(siteId?: string, force?: boolean): Promise { + return this.syncOnSites('all comments', this.syncAllCommentsFunc.bind(this), [force], siteId); + } + + /** + * Synchronize all the comments in a certain site + * + * @param {string} siteId Site ID to sync. + * @param {boolean} force Wether to force sync not depending on last execution. + * @return {Promise} Promise resolved if sync is successful, rejected if sync fails. + */ + private syncAllCommentsFunc(siteId: string, force: boolean): Promise { + return this.commentsOffline.getAllComments(siteId).then((comments) => { + // 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, + comment.component, comment.itemid, comment.area, siteId); + + return promise.then((warnings) => { + if (typeof warnings != 'undefined') { + // Sync successful, send event. + this.eventsProvider.trigger(CoreCommentsSyncProvider.AUTO_SYNCED, { + contextLevel: comment.contextlevel, + instanceId: comment.instanceid, + componentName: comment.component, + itemId: comment.itemid, + area: comment.area, + warnings: warnings + }, siteId); + } + }); + }); + + return Promise.all(promises); + }); + } + + /** + * Sync course notes 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. + * @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 the notes are synced or if they don't need to be synced. + */ + private syncCommentIfNeeded(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); + } + }); + } + + /** + * Synchronize notes of a course. + * + * @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 sync is successful, rejected otherwise. + */ + syncComment(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. + return this.getOngoingSync(syncId, siteId); + } + + this.logger.debug('Try to sync comments ' + syncId); + + const warnings = []; + + // Get offline comments to be sent. + const syncPromise = this.commentsOffline.getComment(contextLevel, instanceId, component, itemId, area, siteId) + .then((comment) => { + if (!comment) { + // Nothing to sync. + return; + } else if (!this.appProvider.isOnline()) { + // Cannot sync in offline. + return Promise.reject(this.translate.instant('core.networkerrormsg')); + } + + const errors = []; + let commentsResponse = []; + let promise; + + if (comment.action == 'add') { + promise = this.commentsProvider.addCommentOnline(comment.content, contextLevel, instanceId, component, itemId, area, + siteId); + } + + // Send the comments. + return promise.then((response) => { + commentsResponse = response; + + // Fetch the comments from server to be sure they're up to date. + return this.commentsProvider.invalidateCommentsData(contextLevel, instanceId, component, itemId, area, siteId) + .then(() => { + return this.commentsProvider.getComments(contextLevel, instanceId, component, itemId, area, 0, siteId); + }).catch(() => { + // Ignore errors. + }); + }).catch((error) => { + if (this.utils.isWebServiceError(error)) { + // It's a WebService error, this means the user cannot send comments. + 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 = 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, + error: error + })); + }); + } + }); + }).then(() => { + // All done, return the warnings. + return warnings; + }); + + return this.addOngoingSync(syncId, syncPromise, siteId); + } + + /** + * Get the ID of a comments sync. + * + * @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 {string} Sync ID. + */ + protected getSyncId(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = ''): string { + return contextLevel + '#' + instanceId + '#' + component + '#' + itemId + '#' + area; + } +} 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 5/5] 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 })); });