diff --git a/scripts/langindex.json b/scripts/langindex.json index 11613e9a0..f6be29269 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -389,7 +389,6 @@ "addon.mod_assign.numwords": "moodle", "addon.mod_assign.outof": "assign", "addon.mod_assign.overdue": "assign", - "addon.mod_assign.savechanges": "assign", "addon.mod_assign.submission": "assign", "addon.mod_assign.submissioneditable": "assign", "addon.mod_assign.submissionnoteditable": "assign", @@ -572,6 +571,7 @@ "addon.mod_forum.cannotadddiscussionall": "forum", "addon.mod_forum.cannotcreatediscussion": "forum", "addon.mod_forum.couldnotadd": "forum", + "addon.mod_forum.couldnotupdate": "forum", "addon.mod_forum.cutoffdatereached": "forum", "addon.mod_forum.delete": "forum", "addon.mod_forum.deletedpost": "forum", @@ -625,6 +625,7 @@ "addon.mod_forum.unpindiscussion": "forum", "addon.mod_forum.unread": "forum", "addon.mod_forum.unreadpostsnumber": "forum", + "addon.mod_forum.yourreply": "forum", "addon.mod_glossary.addentry": "glossary", "addon.mod_glossary.aliases": "glossary", "addon.mod_glossary.attachment": "glossary", @@ -1792,6 +1793,7 @@ "core.restricted": "moodle", "core.retry": "local_moodlemobileapp", "core.save": "moodle", + "core.savechanges": "assign", "core.search": "moodle", "core.searching": "local_moodlemobileapp", "core.searchresults": "moodle", diff --git a/src/addon/mod/assign/lang/en.json b/src/addon/mod/assign/lang/en.json index fb0908c5d..fd076f605 100644 --- a/src/addon/mod/assign/lang/en.json +++ b/src/addon/mod/assign/lang/en.json @@ -71,7 +71,6 @@ "numwords": "{{$a}} words", "outof": "{{$a.current}} out of {{$a.total}}", "overdue": "Assignment is overdue by: {{$a}}", - "savechanges": "Save changes", "submissioneditable": "Student can edit this submission", "submissionnoteditable": "Student cannot edit this submission", "submissionnotsupported": "This submission is not supported by the app and may not contain all the information.", diff --git a/src/addon/mod/forum/components/index/addon-mod-forum-index.html b/src/addon/mod/forum/components/index/addon-mod-forum-index.html index ecce58789..986c6da0c 100644 --- a/src/addon/mod/forum/components/index/addon-mod-forum-index.html +++ b/src/addon/mod/forum/components/index/addon-mod-forum-index.html @@ -51,9 +51,11 @@
-

{{discussion.userfullname}}

-

{{ discussion.groupname }}

-

{{ 'core.notsent' | translate }}

+
+

{{discussion.userfullname}}

+

{{ discussion.groupname }}

+

{{ 'core.notsent' | translate }}

+
diff --git a/src/addon/mod/forum/components/post-options-menu/post-options-menu.scss b/src/addon/mod/forum/components/post-options-menu/post-options-menu.scss new file mode 100644 index 000000000..f790d9f8d --- /dev/null +++ b/src/addon/mod/forum/components/post-options-menu/post-options-menu.scss @@ -0,0 +1,11 @@ +addon-forum-post-options-menu { + core-loading:not(.core-loading-loaded) > .core-loading-container { + position: relative !important; + padding-top: 10px !important; + padding-bottom: 10px !important; + overflow: hidden; + } + core-loading > .core-loading-container .core-loading-message { + display: none; + } +} \ No newline at end of file diff --git a/src/addon/mod/forum/components/post-options-menu/post-options-menu.ts b/src/addon/mod/forum/components/post-options-menu/post-options-menu.ts index b92b8d6d8..483c01b11 100644 --- a/src/addon/mod/forum/components/post-options-menu/post-options-menu.ts +++ b/src/addon/mod/forum/components/post-options-menu/post-options-menu.ts @@ -46,9 +46,9 @@ export class AddonForumPostOptionsMenuComponent implements OnInit { ngOnInit(): void { if (this.forumId) { if (this.post.id) { - this.forumProvider.getDiscussionPost(this.forumId, this.post.discussion, this.post.id).then((post) => { + this.forumProvider.getDiscussionPost(this.forumId, this.post.discussion, this.post.id, true).then((post) => { this.canDelete = post.capabilities.delete && this.forumProvider.isDeletePostAvailable(); - this.canEdit = false; + this.canEdit = post.capabilities.edit && this.forumProvider.isUpdatePostAvailable(); this.wordCount = post.wordcount; }).catch((error) => { this.domUtils.showErrorModalDefault(error, 'Error getting discussion post.'); diff --git a/src/addon/mod/forum/components/post/addon-mod-forum-post.html b/src/addon/mod/forum/components/post/addon-mod-forum-post.html index 48bf2e818..4aec682b6 100644 --- a/src/addon/mod/forum/components/post/addon-mod-forum-post.html +++ b/src/addon/mod/forum/components/post/addon-mod-forum-post.html @@ -7,6 +7,9 @@ + + + @@ -19,12 +22,14 @@

{{post.modified * 1000 | coreFormatDate: "strftimerecentfull"}}

{{ 'core.notsent' | translate }}

- - - - + + + + + + diff --git a/src/addon/mod/forum/components/post/post.ts b/src/addon/mod/forum/components/post/post.ts index da7e383d7..ced5507c8 100644 --- a/src/addon/mod/forum/components/post/post.ts +++ b/src/addon/mod/forum/components/post/post.ts @@ -14,7 +14,7 @@ import { Component, Input, Output, Optional, EventEmitter, OnInit, OnDestroy } from '@angular/core'; import { FormControl } from '@angular/forms'; -import { Content, PopoverController } from 'ionic-angular'; +import { Content, PopoverController, ModalController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader'; import { CoreSyncProvider } from '@providers/sync'; @@ -75,6 +75,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy { private tagProvider: CoreTagProvider, @Optional() private content: Content, protected popoverCtrl: PopoverController, + protected modalCtrl: ModalController, protected eventsProvider: CoreEventsProvider, protected sitesProvider: CoreSitesProvider) { this.onPostChange = new EventEmitter(); @@ -93,7 +94,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy { this.post.subject != reTranslated + this.defaultSubject); this.optionsMenuEnabled = !this.post.id || (this.forumProvider.isGetDiscussionPostAvailable() && - (this.forumProvider.isDeletePostAvailable())); + (this.forumProvider.isDeletePostAvailable() || this.forumProvider.isUpdatePostAvailable())); } /** @@ -179,7 +180,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy { if (data && data.action) { switch (data.action) { case 'edit': - // Not implemented. + this.editPost(); break; case 'editoffline': this.editOfflineReply(); @@ -200,6 +201,61 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy { }); } + /** + * Shows a form modal to edit an online post. + */ + editPost(): void { + const modal = this.modalCtrl.create('AddonModForumEditPostPage', { + post: this.post, + component: this.component, + componentId: this.componentId, + forum: this.forum + }); + + modal.present(); + modal.onDidDismiss((data) => { + if (typeof data != 'undefined') { + // Add some HTML to the message if needed. + const message = this.textUtils.formatHtmlLines(data.message); + const files = data.files || []; + const sendingModal = this.domUtils.showModalLoading('core.sending', true); + let promise; + + // Upload attachments first if any. + if (files.length) { + promise = this.forumHelper.uploadOrStoreReplyFiles(this.forum.id, this.post.id, files, false); + } else { + promise = Promise.resolve(); + } + + promise.then((attach) => { + const options: any = {}; + + if (attach) { + options.attachmentsid = attach; + } + + // Try to send it to server. + return this.forumProvider.updatePost(this.post.id, data.subject, message, options); + }).then((sent) => { + if (sent && this.forum.id) { + // Data sent to server, delete stored files (if any). + this.forumHelper.deleteReplyStoredFiles(this.forum.id, this.post.id); + + this.onPostChange.emit(); + this.post.subject = data.subject; + this.post.message = message; + this.post.attachments = data.files; + } + }).catch((message) => { + this.domUtils.showErrorModalDefault(message, 'addon.mod_forum.couldnotupdate', true); + }).finally(() => { + sendingModal.dismiss(); + }); + } + }); + } + /** * Set this post as being replied to. */ diff --git a/src/addon/mod/forum/lang/en.json b/src/addon/mod/forum/lang/en.json index 14c06e9b8..ba17e36b8 100644 --- a/src/addon/mod/forum/lang/en.json +++ b/src/addon/mod/forum/lang/en.json @@ -8,6 +8,7 @@ "cannotadddiscussionall": "You do not have permission to add a new discussion topic for all participants.", "cannotcreatediscussion": "Could not create new discussion", "couldnotadd": "Could not add your post due to an unknown error", + "couldnotupdate": "Could not update your post due to an unknown error", "cutoffdatereached": "The cut-off date for posting to this forum is reached so you can no longer post to it.", "delete": "Delete", "deletedpost": "The post has been deleted", @@ -60,5 +61,6 @@ "unlockdiscussion": "Unlock this discussion", "unpindiscussion": "Unpin this discussion", "unread": "Unread", - "unreadpostsnumber": "{{$a}} unread posts" + "unreadpostsnumber": "{{$a}} unread posts", + "yourreply": "Your reply" } \ No newline at end of file diff --git a/src/addon/mod/forum/pages/edit-post/addon-mod-forum-edit-post.html b/src/addon/mod/forum/pages/edit-post/addon-mod-forum-edit-post.html new file mode 100644 index 000000000..0a934fa27 --- /dev/null +++ b/src/addon/mod/forum/pages/edit-post/addon-mod-forum-edit-post.html @@ -0,0 +1,40 @@ + + + {{ 'addon.mod_forum.yourreply' | translate }} + + + + + + + + + {{ 'addon.mod_forum.subject' | translate }} + + + + {{ 'addon.mod_forum.message' | translate }} + + + + + + {{ 'addon.mod_forum.advanced' | translate }} + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/addon/mod/forum/pages/edit-post/edit-post.module.ts b/src/addon/mod/forum/pages/edit-post/edit-post.module.ts new file mode 100644 index 000000000..07f409742 --- /dev/null +++ b/src/addon/mod/forum/pages/edit-post/edit-post.module.ts @@ -0,0 +1,35 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { IonicPageModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { AddonModForumComponentsModule } from '../../components/components.module'; +import { AddonModForumEditPostPage } from './edit-post'; + +@NgModule({ + declarations: [ + AddonModForumEditPostPage, + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + AddonModForumComponentsModule, + IonicPageModule.forChild(AddonModForumEditPostPage), + TranslateModule.forChild() + ], +}) +export class AddonModForumEditPostPageModule {} diff --git a/src/addon/mod/forum/pages/edit-post/edit-post.ts b/src/addon/mod/forum/pages/edit-post/edit-post.ts new file mode 100644 index 000000000..6795c92dc --- /dev/null +++ b/src/addon/mod/forum/pages/edit-post/edit-post.ts @@ -0,0 +1,141 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component } from '@angular/core'; +import { FormControl } from '@angular/forms'; +import { IonicPage, ViewController, NavParams } from 'ionic-angular'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { AddonModForumProvider } from '../../providers/forum'; +import { AddonModForumHelperProvider } from '../../providers/helper'; + +/** + * Page that displays a form to edit discussion post. + */ +@IonicPage({ segment: 'addon-mod-edit-post' }) +@Component({ + selector: 'addon-mod-forum-edit-post', + templateUrl: 'addon-mod-forum-edit-post.html', +}) +export class AddonModForumEditPostPage { + component: string; // Component this post belong to. + componentId: number; // Component ID. + forum: any; // The forum the post belongs to. Required for attachments and offline posts. + + messageControl = new FormControl(); + advanced = false; // Display all form fields. + replyData: any = {}; + originalData: any = {}; // Object with the original post data. Usually shared between posts. + + protected forceLeave = false; // To allow leaving the page without checking for changes. + + constructor( + params: NavParams, + protected forumProvider: AddonModForumProvider, + protected viewCtrl: ViewController, + protected domUtils: CoreDomUtilsProvider, + protected uploaderProvider: CoreFileUploaderProvider, + protected forumHelper: AddonModForumHelperProvider, + protected translate: TranslateService) { + + const post = params.get('post'); + this.component = params.get('component'); + this.componentId = params.get('componentId'); + this.forum = params.get('forum'); + + this.replyData.id = post.id; + this.replyData.subject = post.subject; + this.replyData.message = post.message; + this.replyData.files = post.attachments || []; + + // Delete the local files from the tmp folder if any. + this.uploaderProvider.clearTmpFiles(this.replyData.files); + + // Update rich text editor. + this.messageControl.setValue(this.replyData.message); + + // Update original data. + this.originalData.subject = this.replyData.subject; + this.originalData.message = this.replyData.message; + this.originalData.files = this.replyData.files.slice(); + + // Show advanced fields if any of them has not the default value. + this.advanced = this.replyData.files.length > 0; + } + + /** + * Check if we can leave the page or not. + * + * @return Resolved if we can leave it, rejected if not. + */ + ionViewCanLeave(): boolean | Promise { + if (this.forceLeave) { + return true; + } + + let promise: any; + + if (this.forumHelper.hasPostDataChanged(this.replyData, this.originalData)) { + // Show confirmation if some data has been modified. + promise = this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit')); + } else { + promise = Promise.resolve(); + } + + return promise.then(() => { + // Delete the local files from the tmp folder. + this.uploaderProvider.clearTmpFiles(this.replyData.files); + }); + } + + /** + * Message changed. + * + * @param text The new text. + */ + onMessageChange(text: string): void { + this.replyData.message = text; + } + + /** + * Close modal. + * + * @param data Data to return to the page. + */ + closeModal(data: any): void { + this.viewCtrl.dismiss(data); + } + + /** + * Reply to this post. + * + * @param e Click event. + */ + reply(e: Event): void { + e.preventDefault(); + e.stopPropagation(); + + // Close the modal, sending the input data. + this.forceLeave = true; + this.closeModal(this.replyData); + } + + /** + * Show or hide advanced form fields. + */ + toggleAdvanced(): void { + this.advanced = !this.advanced; + } +} diff --git a/src/addon/mod/forum/providers/forum.ts b/src/addon/mod/forum/providers/forum.ts index 3afc7b4c2..cae8c5ac3 100644 --- a/src/addon/mod/forum/providers/forum.ts +++ b/src/addon/mod/forum/providers/forum.ts @@ -317,6 +317,16 @@ export class AddonModForumProvider { return this.sitesProvider.wsAvailableInCurrentSite('mod_forum_delete_post'); } + /** + * Returns whether or not updatePost WS available or not. + * + * @return If WS is avalaible. + * @since 3.8 + */ + isUpdatePostAvailable(): boolean { + return this.sitesProvider.wsAvailableInCurrentSite('mod_forum_update_discussion_post'); + } + /** * Format discussions, setting groupname if the discussion group is valid. * @@ -385,18 +395,24 @@ export class AddonModForumProvider { * @param forumId Forum ID. * @param discussionId Discussion ID. * @param postId Post ID. + * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the post is retrieved. */ - getDiscussionPost(forumId: number, discussionId: number, postId: number, siteId?: string): Promise { + getDiscussionPost(forumId: number, discussionId: number, postId: number, ignoreCache?: boolean, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { - postid: postId - }; - const preSets = { - cacheKey: this.getDiscussionPostDataCacheKey(forumId, discussionId, postId), - updateFrequency: CoreSite.FREQUENCY_RARELY - }; + postid: postId + }, + preSets: CoreSiteWSPreSets = { + cacheKey: this.getDiscussionPostDataCacheKey(forumId, discussionId, postId), + updateFrequency: CoreSite.FREQUENCY_USUALLY + }; + + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } return site.read('mod_forum_get_discussion_post', params, preSets).then((response) => { if (response.post) { @@ -1050,4 +1066,29 @@ export class AddonModForumProvider { this.userProvider.storeUsers(this.utils.objectToArray(users)); } + + /** + * Update a certain post. + * + * @param postId ID of the post being edited. + * @param subject New post's subject. + * @param message New post's message. + * @param options Options (subscribe, attachments, ...). + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with success boolean when done. + */ + updatePost(postId: number, subject: string, message: string, options?: any, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + postid: postId, + subject: subject, + message: message, + options: this.utils.objectToArrayOfObjects(options, 'name', 'value') + }; + + return site.write('mod_forum_update_discussion_post', params).then((response) => { + return response && response.status; + }); + }); + } } diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index c6a0d0d41..2a5ba7432 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -388,7 +388,6 @@ "addon.mod_assign.numwords": "{{$a}} words", "addon.mod_assign.outof": "{{$a.current}} out of {{$a.total}}", "addon.mod_assign.overdue": "Assignment is overdue by: {{$a}}", - "addon.mod_assign.savechanges": "Save changes", "addon.mod_assign.submission": "Submission", "addon.mod_assign.submissioneditable": "Student can edit this submission", "addon.mod_assign.submissionnoteditable": "Student cannot edit this submission", @@ -571,6 +570,7 @@ "addon.mod_forum.cannotadddiscussionall": "You do not have permission to add a new discussion topic for all participants.", "addon.mod_forum.cannotcreatediscussion": "Could not create new discussion", "addon.mod_forum.couldnotadd": "Could not add your post due to an unknown error", + "addon.mod_forum.couldnotupdate": "Could not update your post due to an unknown error", "addon.mod_forum.cutoffdatereached": "The cut-off date for posting to this forum is reached so you can no longer post to it.", "addon.mod_forum.delete": "Delete", "addon.mod_forum.deletedpost": "The post has been deleted", @@ -624,6 +624,7 @@ "addon.mod_forum.unpindiscussion": "Unpin this discussion", "addon.mod_forum.unread": "Unread", "addon.mod_forum.unreadpostsnumber": "{{$a}} unread posts", + "addon.mod_forum.yourreply": "Your reply", "addon.mod_glossary.addentry": "Add a new entry", "addon.mod_glossary.aliases": "Keyword(s)", "addon.mod_glossary.attachment": "Attachment", @@ -1787,6 +1788,7 @@ "core.restricted": "Restricted", "core.retry": "Retry", "core.save": "Save", + "core.savechanges": "Save changes", "core.search": "Search", "core.searching": "Searching", "core.searchresults": "Search results", diff --git a/src/lang/en.json b/src/lang/en.json index 328b287a5..bb56a4a34 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -216,6 +216,7 @@ "restricted": "Restricted", "retry": "Retry", "save": "Save", + "savechanges": "Save changes", "search": "Search", "searching": "Searching", "searchresults": "Search results", diff --git a/src/providers/utils/mimetype.ts b/src/providers/utils/mimetype.ts index 47dc09037..19c54139c 100644 --- a/src/providers/utils/mimetype.ts +++ b/src/providers/utils/mimetype.ts @@ -146,24 +146,25 @@ export class CoreMimetypeUtilsProvider { */ getEmbeddedHtml(file: any, path?: string): string { let ext; + const filename = file.filename || file.name; if (file.mimetype) { ext = this.getExtension(file.mimetype); } else { - ext = this.getFileExtension(file.filename); + ext = this.getFileExtension(filename); file.mimetype = this.getMimeType(ext); } if (this.canBeEmbedded(ext)) { file.embedType = this.getExtensionType(ext); - path = path || file.fileurl; + path = path || file.fileurl || (file.toURL && file.toURL()); if (file.embedType == 'image') { return ''; } if (file.embedType == 'audio' || file.embedType == 'video') { - return '<' + file.embedType + ' controls title="' + file.filename + '" src="' + path + '">' + + return '<' + file.embedType + ' controls title="' + filename + '" src="' + path + '">' + '' + ''; }