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 }}
+
+
+ 0" [files]="replyData.files" [maxSize]="forum.maxbytes" [maxSubmissions]="forum.maxattachments" [component]="component" [componentId]="forum.cmid" [allowOffline]="true">
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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 + '">' +
'