Merge pull request #2932 from dpalou/MOBILE-3793

Mobile 3793
main
Noel De Martin 2021-09-02 11:30:02 +02:00 committed by GitHub
commit 5dd2f2c93d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 272 additions and 510 deletions

View File

@ -21,7 +21,6 @@ import { CoreTagComponentsModule } from '@features/tag/components/components.mod
import { CoreRatingComponentsModule } from '@features/rating/components/components.module'; import { CoreRatingComponentsModule } from '@features/rating/components/components.module';
import { AddonModForumDiscussionOptionsMenuComponent } from './discussion-options-menu/discussion-options-menu'; import { AddonModForumDiscussionOptionsMenuComponent } from './discussion-options-menu/discussion-options-menu';
import { AddonModForumEditPostComponent } from './edit-post/edit-post';
import { AddonModForumIndexComponent } from './index/index'; import { AddonModForumIndexComponent } from './index/index';
import { AddonModForumPostComponent } from './post/post'; import { AddonModForumPostComponent } from './post/post';
import { AddonModForumPostOptionsMenuComponent } from './post-options-menu/post-options-menu'; import { AddonModForumPostOptionsMenuComponent } from './post-options-menu/post-options-menu';
@ -30,7 +29,6 @@ import { AddonModForumSortOrderSelectorComponent } from './sort-order-selector/s
@NgModule({ @NgModule({
declarations: [ declarations: [
AddonModForumDiscussionOptionsMenuComponent, AddonModForumDiscussionOptionsMenuComponent,
AddonModForumEditPostComponent,
AddonModForumIndexComponent, AddonModForumIndexComponent,
AddonModForumPostComponent, AddonModForumPostComponent,
AddonModForumPostOptionsMenuComponent, AddonModForumPostOptionsMenuComponent,

View File

@ -1,60 +0,0 @@
<ion-header>
<ion-toolbar>
<h2>{{ 'addon.mod_forum.yourreply' | translate }}</h2>
<ion-buttons slot="end">
<ion-button fill="clear" (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
<ion-icon name="fas-times" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<form #editFormEl>
<ion-item>
<ion-label position="stacked">{{ 'addon.mod_forum.subject' | translate }}</ion-label>
<ion-input type="text" [placeholder]="'addon.mod_forum.subject' | translate" [(ngModel)]="replyData.subject" name="subject">
</ion-input>
</ion-item>
<ion-item>
<ion-label position="stacked">{{ 'addon.mod_forum.message' | translate }}</ion-label>
<core-rich-text-editor elementId="message"
[name]="'mod_forum_reply_' + replyData.id" [control]="messageControl"
[placeholder]="'addon.mod_forum.replyplaceholder' | translate" [autoSave]="true"
[component]="component" [componentId]="componentId" [draftExtraParams]="{edit: replyData.id}"
contextLevel="module" [contextInstanceId]="forum.cmid"
(contentChanged)="onMessageChange($event)">
</core-rich-text-editor>
</ion-item>
<ion-item
button class="divider ion-text-wrap"
(click)="toggleAdvanced()"
role="heading"
detail="false"
[attr.aria-expanded]="advanced"
aria-controls="addon-mod-forum-advanced"
[attr.aria-label]="(advanced ? 'core.hideadvanced' : 'core.showadvanced') | translate"
>
<ion-icon *ngIf="!advanced" name="fas-caret-right" flip-rtl slot="start" aria-hidden="true"></ion-icon>
<ion-icon *ngIf="advanced" name="fas-caret-down" slot="start" aria-hidden="true"></ion-icon>
<ion-label><h2>{{ 'addon.mod_forum.advanced' | translate }}</h2></ion-label>
</ion-item>
<div *ngIf="advanced" id="addon-mod-forum-advanced">
<core-attachments *ngIf="forum.id && forum.maxattachments > 0"
[maxSize]="forum.maxbytes" [maxSubmissions]="forum.maxattachments" [allowOffline]="true" [files]="replyData.files"
[component]="component" [componentId]="forum.cmid" [courseId]="forum.course">
</core-attachments>
</div>
<ion-grid>
<ion-row>
<ion-col>
<ion-button expand="block" (click)="reply($event)" [disabled]="replyData.subject == '' || replyData.message == null">
{{ 'core.savechanges' | translate }}
</ion-button>
</ion-col>
<ion-col>
<ion-button expand="block" color="light" (click)="closeModal()">{{ 'core.cancel' | translate }}</ion-button>
</ion-col>
</ion-row>
</ion-grid>
</form>
</ion-content>

View File

@ -1,150 +0,0 @@
// (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, ViewChild, ElementRef, Input, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { ModalController, Translate } from '@singletons';
import { AddonModForumData, AddonModForumPost, AddonModForumReply } from '@addons/mod/forum/services/forum';
import { AddonModForumHelper } from '@addons/mod/forum/services/forum-helper';
import { CoreForms } from '@singletons/form';
import { CoreFileEntry } from '@services/file-helper';
/**
* Page that displays a form to edit discussion post.
*/
@Component({
selector: 'addon-mod-forum-edit-post',
templateUrl: 'edit-post.html',
})
export class AddonModForumEditPostComponent implements OnInit {
@ViewChild('editFormEl') formElement!: ElementRef;
@Input() component!: string; // Component this post belong to.
@Input() componentId!: number; // Component ID.
@Input() forum!: AddonModForumData; // The forum the post belongs to. Required for attachments and offline posts.
@Input() post!: AddonModForumPost;
messageControl = new FormControl();
advanced = false; // Display all form fields.
replyData!: AddonModForumReply;
originalData!: Omit<AddonModForumReply, 'id'>; // Object with the original post data. Usually shared between posts.
protected forceLeave = false; // To allow leaving the page without checking for changes.
ngOnInit(): void {
// @todo Override android back button to show confirmation before dismiss.
this.replyData = {
id: this.post.id,
subject: this.post.subject,
message: this.post.message,
files: this.post.attachments || [],
};
// Delete the local files from the tmp folder if any.
CoreFileUploader.clearTmpFiles(this.replyData.files as CoreFileEntry[]);
// Update rich text editor.
this.messageControl.setValue(this.replyData.message);
// Update original data.
this.originalData = {
subject: this.replyData.subject,
message: this.replyData.message,
files: this.replyData.files.slice(),
};
// Show advanced fields if any of them has not the default value.
this.advanced = this.replyData.files.length > 0;
}
/**
* 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.
*/
async closeModal(data?: AddonModForumReply): Promise<void> {
const confirmDismiss = await this.confirmDismiss();
if (!confirmDismiss) {
return;
}
if (data) {
CoreForms.triggerFormSubmittedEvent(this.formElement, false, CoreSites.getCurrentSiteId());
} else {
CoreForms.triggerFormCancelledEvent(this.formElement, CoreSites.getCurrentSiteId());
}
ModalController.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;
}
/**
* Check if we can leave the page or not.
*
* @return Resolved if we can leave it, rejected if not.
*/
private async confirmDismiss(): Promise<boolean> {
if (this.forceLeave || !AddonModForumHelper.hasPostDataChanged(this.replyData, this.originalData)) {
return true;
}
try {
// Show confirmation if some data has been modified.
await CoreDomUtils.showConfirm(Translate.instant('core.confirmcanceledit'));
// Delete the local files from the tmp folder.
CoreFileUploader.clearTmpFiles(this.replyData.files as CoreFileEntry[]);
return true;
} catch (error) {
return false;
}
}
}

View File

@ -122,11 +122,7 @@ export class AddonModForumPostOptionsMenuComponent implements OnInit, OnDestroy
* Edit a post. * Edit a post.
*/ */
editPost(): void { editPost(): void {
if (!this.offlinePost) {
PopoverController.dismiss({ action: 'edit' }); PopoverController.dismiss({ action: 'edit' });
} else {
PopoverController.dismiss({ action: 'editoffline' });
}
} }
} }

View File

@ -1,4 +1,5 @@
<div class="addon-mod_forum-post"> <div class="addon-mod_forum-post">
<ng-container *ngIf="!formData.isEditing || !showForm">
<ion-card-header class="ion-text-wrap ion-no-padding" id="addon-mod_forum-post-{{post.id}}"> <ion-card-header class="ion-text-wrap ion-no-padding" id="addon-mod_forum-post-{{post.id}}">
<ion-item class="ion-text-wrap" [class.highlight]="highlight" lines="none"> <ion-item class="ion-text-wrap" [class.highlight]="highlight" lines="none">
<ion-label> <ion-label>
@ -16,8 +17,8 @@
contextLevel="module" [contextInstanceId]="forum && forum.cmid" [courseId]="courseId"> contextLevel="module" [contextInstanceId]="forum && forum.cmid" [courseId]="courseId">
</core-format-text> </core-format-text>
</h2> </h2>
<ion-note *ngIf="trackPosts && post.unread" <ion-note *ngIf="trackPosts && post.unread" class="ion-float-end ion-padding-start ion-text-end"
class="ion-float-end ion-padding-start ion-text-end" [attr.aria-label]="'addon.mod_forum.unread' | translate"> [attr.aria-label]="'addon.mod_forum.unread' | translate">
<ion-icon name="fas-circle" color="primary" aria-hidden="true"></ion-icon> <ion-icon name="fas-circle" color="primary" aria-hidden="true"></ion-icon>
</ion-note> </ion-note>
<ion-button *ngIf="optionsMenuEnabled" <ion-button *ngIf="optionsMenuEnabled"
@ -28,7 +29,8 @@
</ion-button> </ion-button>
</div> </div>
<div class="addon-mod-forum-post-info"> <div class="addon-mod-forum-post-info">
<core-user-avatar *ngIf="post.author && post.author.fullname" [user]="post.author" slot="start" [courseId]="courseId"> <core-user-avatar *ngIf="post.author && post.author.fullname" [user]="post.author" slot="start"
[courseId]="courseId">
</core-user-avatar> </core-user-avatar>
<div class="addon-mod-forum-post-author"> <div class="addon-mod-forum-post-author">
<span *ngIf="post.author && post.author.fullname">{{post.author.fullname}}</span> <span *ngIf="post.author && post.author.fullname">{{post.author.fullname}}</span>
@ -44,8 +46,8 @@
</p> </p>
</div> </div>
<ng-container *ngIf="!displaySubject"> <ng-container *ngIf="!displaySubject">
<ion-note *ngIf="trackPosts && post.unread" <ion-note *ngIf="trackPosts && post.unread" class="ion-float-end ion-padding-start ion-text-end"
class="ion-float-end ion-padding-start ion-text-end" [attr.aria-label]="'addon.mod_forum.unread' | translate"> [attr.aria-label]="'addon.mod_forum.unread' | translate">
<ion-icon name="fas-circle" color="primary" aria-hidden="true"></ion-icon> <ion-icon name="fas-circle" color="primary" aria-hidden="true"></ion-icon>
</ion-note> </ion-note>
<ion-button *ngIf="optionsMenuEnabled" <ion-button *ngIf="optionsMenuEnabled"
@ -92,7 +94,7 @@
<ion-label> <ion-label>
<ion-button fill="clear" size="small" <ion-button fill="clear" size="small"
[attr.aria-controls]="'addon-forum-reply-edit-form-' + uniqueId" [attr.aria-controls]="'addon-forum-reply-edit-form-' + uniqueId"
[attr.aria-expanded]="replyData.replyingTo === post.id" [attr.aria-expanded]="formData.replyingTo === post.id"
(click)="showReplyForm($event)"> (click)="showReplyForm($event)">
<ion-icon name="fas-reply" slot="start" aria-hidden="true"></ion-icon> <ion-icon name="fas-reply" slot="start" aria-hidden="true"></ion-icon>
{{ 'addon.mod_forum.reply' | translate }} {{ 'addon.mod_forum.reply' | translate }}
@ -100,12 +102,14 @@
</ion-label> </ion-label>
</ion-item> </ion-item>
</div> </div>
</ng-container>
<form *ngIf="showForm" <form *ngIf="showForm"
[id]="'addon-forum-reply-edit-form-' + uniqueId" #replyFormEl> [id]="'addon-forum-reply-edit-form-' + uniqueId" #replyFormEl>
<ion-item> <ion-item>
<ion-label position="stacked">{{ 'addon.mod_forum.subject' | translate }}</ion-label> <ion-label position="stacked">{{ 'addon.mod_forum.subject' | translate }}</ion-label>
<ion-input type="text" [placeholder]="'addon.mod_forum.subject' | translate" [(ngModel)]="replyData.subject" name="subject"> <ion-input type="text" [placeholder]="'addon.mod_forum.subject' | translate" [(ngModel)]="formData.subject"
name="subject">
</ion-input> </ion-input>
</ion-item> </ion-item>
<ion-item> <ion-item>
@ -119,7 +123,7 @@
</ion-item> </ion-item>
<ion-item class="ion-text-wrap" *ngIf="accessInfo.canpostprivatereply"> <ion-item class="ion-text-wrap" *ngIf="accessInfo.canpostprivatereply">
<ion-label>{{ 'addon.mod_forum.privatereply' | translate }}</ion-label> <ion-label>{{ 'addon.mod_forum.privatereply' | translate }}</ion-label>
<ion-checkbox slot="end" [(ngModel)]="replyData.isprivatereply" name="isprivatereply"></ion-checkbox> <ion-checkbox slot="end" [(ngModel)]="formData.isprivatereply" name="isprivatereply"></ion-checkbox>
</ion-item> </ion-item>
<ng-container *ngIf="forum.id && forum.maxattachments > 0"> <ng-container *ngIf="forum.id && forum.maxattachments > 0">
<ion-item <ion-item
@ -138,7 +142,7 @@
</ion-item> </ion-item>
<div *ngIf="advanced" [id]="'addon-forum-reply-edit-form-advanced-' + uniqueId"> <div *ngIf="advanced" [id]="'addon-forum-reply-edit-form-advanced-' + uniqueId">
<core-attachments <core-attachments
[files]="replyData.files" [maxSize]="forum.maxbytes" [maxSubmissions]="forum.maxattachments" [files]="formData.files" [maxSize]="forum.maxbytes" [maxSubmissions]="forum.maxattachments"
[component]="component" [componentId]="forum.cmid" [allowOffline]="true" [courseId]="courseId"> [component]="component" [componentId]="forum.cmid" [allowOffline]="true" [courseId]="courseId">
</core-attachments> </core-attachments>
</div> </div>
@ -146,7 +150,7 @@
<ion-grid> <ion-grid>
<ion-row> <ion-row>
<ion-col> <ion-col>
<ion-button expand="block" (click)="reply()" [disabled]="replyData.subject == '' || replyData.message == null"> <ion-button expand="block" (click)="send()" [disabled]="formData.subject == '' || formData.message == null">
{{ 'addon.mod_forum.posttoforum' | translate }} {{ 'addon.mod_forum.posttoforum' | translate }}
</ion-button> </ion-button>
</ion-col> </ion-col>

View File

@ -36,8 +36,7 @@ import {
AddonModForumDiscussion, AddonModForumDiscussion,
AddonModForumPost, AddonModForumPost,
AddonModForumProvider, AddonModForumProvider,
AddonModForumReply, AddonModForumPostFormData,
AddonModForumUpdateDiscussionPostWSOptionsObject,
} from '../../services/forum'; } from '../../services/forum';
import { CoreTag } from '@features/tag/services/tag'; import { CoreTag } from '@features/tag/services/tag';
import { Translate } from '@singletons'; import { Translate } from '@singletons';
@ -47,13 +46,13 @@ import { AddonModForumSync } from '../../services/forum-sync';
import { CoreSync } from '@services/sync'; import { CoreSync } from '@services/sync';
import { CoreTextUtils } from '@services/utils/text'; import { CoreTextUtils } from '@services/utils/text';
import { AddonModForumHelper } from '../../services/forum-helper'; import { AddonModForumHelper } from '../../services/forum-helper';
import { AddonModForumOffline, AddonModForumReplyOptions } from '../../services/forum-offline'; import { AddonModForumOffline } from '../../services/forum-offline';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { AddonModForumPostOptionsMenuComponent } from '../post-options-menu/post-options-menu'; import { AddonModForumPostOptionsMenuComponent } from '../post-options-menu/post-options-menu';
import { AddonModForumEditPostComponent } from '../edit-post/edit-post';
import { CoreRatingInfo } from '@features/rating/services/rating'; import { CoreRatingInfo } from '@features/rating/services/rating';
import { CoreForms } from '@singletons/form'; import { CoreForms } from '@singletons/form';
import { CoreFileEntry } from '@services/file-helper'; import { CoreFileEntry } from '@services/file-helper';
import { AddonModForumSharedPostFormData } from '../../pages/discussion/discussion.page';
/** /**
* Components that shows a discussion post, its attachments and the action buttons allowed (reply, etc.). * Components that shows a discussion post, its attachments and the action buttons allowed (reply, etc.).
@ -71,8 +70,8 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
@Input() discussion?: AddonModForumDiscussion; // Post's' discussion, only for starting posts. @Input() discussion?: AddonModForumDiscussion; // Post's' discussion, only for starting posts.
@Input() component!: string; // Component this post belong to. @Input() component!: string; // Component this post belong to.
@Input() componentId!: number; // Component ID. @Input() componentId!: number; // Component ID.
@Input() replyData!: AddonModForumReply; // Object with the new post data. Usually shared between posts. @Input() formData!: AddonModForumSharedPostFormData; // Object with the new post data. Usually shared between posts.
@Input() originalData!: Omit<AddonModForumReply, 'id'>; // Object with the original post data. Usually shared between posts. @Input() originalData!: Omit<AddonModForumPostFormData, 'id'>; // Original post data. Usually shared between posts.
@Input() trackPosts!: boolean; // True if post is being tracked. @Input() trackPosts!: boolean; // True if post is being tracked.
@Input() forum!: AddonModForumData; // The forum the post belongs to. Required for attachments and offline posts. @Input() forum!: AddonModForumData; // The forum the post belongs to. Required for attachments and offline posts.
@Input() accessInfo!: AddonModForumAccessInformation; // Forum access information. @Input() accessInfo!: AddonModForumAccessInformation; // Forum access information.
@ -93,8 +92,6 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
displaySubject = true; displaySubject = true;
optionsMenuEnabled = false; optionsMenuEnabled = false;
protected syncId!: string;
constructor( constructor(
protected elementRef: ElementRef, protected elementRef: ElementRef,
@Optional() protected content?: IonContent, @Optional() protected content?: IonContent,
@ -102,8 +99,9 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
get showForm(): boolean { get showForm(): boolean {
return this.post.id > 0 return this.post.id > 0
? !this.replyData.isEditing && this.replyData.replyingTo === this.post.id ? (!this.formData.isEditing && this.formData.replyingTo === this.post.id) ||
: !!this.replyData.isEditing && this.replyData.replyingTo === this.post.parentid; (!!this.formData.isEditing && this.formData.id === this.post.id)
: !!this.formData.isEditing && this.formData.replyingTo === this.post.parentid;
} }
/** /**
@ -172,44 +170,47 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
} }
/** /**
* Set data to new reply post, clearing temporary files and updating original data. * Set data to new/edit post, clearing temporary files and updating original data.
* *
* @param replyingTo Id of post beeing replied. * @param replyingTo Id of post beeing replied.
* @param isEditing True it's an offline reply beeing edited, false otherwise. * @param isEditing True it's an offline reply beeing edited, false otherwise.
* @param subject Subject of the reply. * @param subject Subject of the reply.
* @param message Message of the reply. * @param message Message of the reply.
* @param isPrivate True if it's private reply.
* @param files Reply attachments. * @param files Reply attachments.
* @param isPrivate True if it's private reply.
* @param postId The post ID if user is editing an online post.
*/ */
protected setReplyFormData( protected setFormData(
replyingTo?: number, replyingTo?: number,
isEditing?: boolean, isEditing?: boolean,
subject?: string, subject?: string,
message?: string, message?: string,
files?: CoreFileEntry[], files?: CoreFileEntry[],
isPrivate?: boolean, isPrivate?: boolean,
postId?: number,
): void { ): void {
// Delete the local files from the tmp folder if any. // Delete the local files from the tmp folder if any.
CoreFileUploader.clearTmpFiles(this.replyData.files); CoreFileUploader.clearTmpFiles(this.formData.files);
this.replyData.replyingTo = replyingTo || 0; this.formData.replyingTo = replyingTo || 0;
this.replyData.isEditing = !!isEditing; this.formData.isEditing = !!isEditing;
this.replyData.subject = subject || this.defaultReplySubject || ''; this.formData.subject = subject || this.defaultReplySubject || '';
this.replyData.message = message || null; this.formData.message = message || null;
this.replyData.files = files || []; this.formData.files = files || [];
this.replyData.isprivatereply = !!isPrivate; this.formData.isprivatereply = !!isPrivate;
this.formData.id = postId;
// Update rich text editor. // Update rich text editor.
this.messageControl.setValue(this.replyData.message); this.messageControl.setValue(this.formData.message);
// Update original data. // Update original data.
this.originalData.subject = this.replyData.subject; this.originalData.subject = this.formData.subject;
this.originalData.message = this.replyData.message; this.originalData.message = this.formData.message;
this.originalData.files = this.replyData.files.slice(); this.originalData.files = this.formData.files.slice();
this.originalData.isprivatereply = this.replyData.isprivatereply; this.originalData.isprivatereply = this.formData.isprivatereply;
// Show advanced fields if any of them has not the default value. // Show advanced fields if any of them has not the default value.
this.advanced = this.replyData.files.length > 0; this.advanced = this.formData.files.length > 0;
} }
/** /**
@ -226,6 +227,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
cmId: this.forum.cmid, cmId: this.forum.cmid,
}, },
event, event,
waitForDismissCompleted: true,
}); });
if (popoverData && popoverData.action) { if (popoverData && popoverData.action) {
@ -233,9 +235,6 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
case 'edit': case 'edit':
this.editPost(); this.editPost();
break; break;
case 'editoffline':
this.editOfflineReply();
break;
case 'delete': case 'delete':
this.deletePost(); this.deletePost();
break; break;
@ -246,65 +245,6 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
} }
} }
/**
* Shows a form modal to edit an online post.
*/
async editPost(): Promise<void> {
const modalData = await CoreDomUtils.openModal<AddonModForumReply>({
component: AddonModForumEditPostComponent,
componentProps: {
post: this.post,
component: this.component,
componentId: this.componentId,
forum: this.forum,
},
backdropDismiss: false,
cssClass: 'core-modal-fullscreen',
});
if (!modalData) {
return;
}
// Add some HTML to the message if needed.
const message = CoreTextUtils.formatHtmlLines(modalData.message!);
const files = modalData.files;
const options: AddonModForumUpdateDiscussionPostWSOptionsObject = {};
const sendingModal = await CoreDomUtils.showModalLoading('core.sending', true);
try {
// Upload attachments first if any.
if (files.length) {
const attachment = await AddonModForumHelper.uploadOrStoreReplyFiles(
this.forum.id,
this.post.id,
files as CoreFileEntry[],
false,
);
options.attachmentsid = attachment;
}
// Try to send it to server.
const sent = await AddonModForum.updatePost(this.post.id, modalData.subject!, message, options);
if (sent && this.forum.id) {
// Data sent to server, delete stored files (if any).
AddonModForumHelper.deleteReplyStoredFiles(this.forum.id, this.post.id);
this.onPostChange.emit();
this.post.subject = modalData.subject!;
this.post.message = message;
this.post.attachments = modalData.files;
}
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_forum.couldnotupdate', true);
} finally {
sendingModal.dismiss();
}
}
/** /**
* Set this post as being replied to. * Set this post as being replied to.
* *
@ -314,21 +254,13 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
if (this.replyData.isEditing) { if (this.formData.isEditing) {
// User is editing a post, data needs to be resetted. Ask confirm if there is unsaved data. // User is editing a post, data needs to be resetted. Ask confirm if there is unsaved data.
try { try {
await this.confirmDiscard(); await this.confirmDiscard();
this.setReplyFormData(this.post.id); this.setFormData(this.post.id);
if (this.content) { this.scrollToForm();
setTimeout(() => {
CoreDomUtils.scrollToElementBySelector(
this.elementRef.nativeElement,
this.content,
'#addon-forum-reply-edit-form-' + this.uniqueId,
);
});
}
} catch { } catch {
// Cancelled. // Cancelled.
} }
@ -336,53 +268,47 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
return; return;
} }
if (!this.replyData.replyingTo) { if (!this.formData.replyingTo) {
// User isn't replying, it's a brand new reply. Initialize the data. // User isn't replying, it's a brand new reply. Initialize the data.
this.setReplyFormData(this.post.id); this.setFormData(this.post.id);
} else { } else {
// The post being replied has changed but the data will be kept. // The post being replied has changed but the data will be kept.
this.replyData.replyingTo = this.post.id; this.formData.replyingTo = this.post.id;
if (this.replyData.subject == this.originalData.subject) { if (this.formData.subject == this.originalData.subject) {
// Update subject only if it hadn't been modified // Update subject only if it hadn't been modified
this.replyData.subject = this.defaultReplySubject; this.formData.subject = this.defaultReplySubject;
this.originalData.subject = this.defaultReplySubject; this.originalData.subject = this.defaultReplySubject;
} }
this.messageControl.setValue(this.replyData.message); this.messageControl.setValue(this.formData.message);
}
if (this.content) {
setTimeout(() => {
CoreDomUtils.scrollToElementBySelector(
this.elementRef.nativeElement,
this.content,
'#addon-forum-reply-edit-form-' + this.uniqueId,
);
});
} }
this.scrollToForm();
} }
/** /**
* Set this post as being edited to. * Set this post as being edited to.
*/ */
async editOfflineReply(): Promise<void> { async editPost(): Promise<void> {
// Ask confirm if there is unsaved data. // Ask confirm if there is unsaved data.
try { try {
await this.confirmDiscard(); await this.confirmDiscard();
this.syncId = AddonModForumSync.getDiscussionSyncId(this.discussionId); this.formData.syncId = AddonModForumSync.getDiscussionSyncId(this.discussionId);
CoreSync.blockOperation(AddonModForumProvider.COMPONENT, this.syncId); CoreSync.blockOperation(AddonModForumProvider.COMPONENT, this.formData.syncId);
this.setReplyFormData( this.setFormData(
this.post.parentid, this.post.parentid,
true, true,
this.post.subject, this.post.subject,
this.post.message, this.post.message,
this.post.attachments, this.post.attachments,
this.post.isprivatereply, this.post.isprivatereply,
this.post.id > 0 ? this.post.id : undefined,
); );
this.scrollToForm(5);
} catch (error) { } catch (error) {
// Cancelled. // Cancelled.
} }
@ -394,53 +320,53 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
* @param text The new text. * @param text The new text.
*/ */
onMessageChange(text: string): void { onMessageChange(text: string): void {
this.replyData.message = text; this.formData.message = text;
} }
/** /**
* Reply to this post. * Reply to this post or edit post data.
*/ */
async reply(): Promise<void> { async send(): Promise<void> {
if (!this.replyData.subject) { if (!this.formData.subject) {
CoreDomUtils.showErrorModal('addon.mod_forum.erroremptysubject', true); CoreDomUtils.showErrorModal('addon.mod_forum.erroremptysubject', true);
return; return;
} }
if (!this.replyData.message) { if (!this.formData.message) {
CoreDomUtils.showErrorModal('addon.mod_forum.erroremptymessage', true); CoreDomUtils.showErrorModal('addon.mod_forum.erroremptymessage', true);
return; return;
} }
let saveOffline = false; let saveOffline = false;
let message = this.replyData.message; let message = this.formData.message;
const subject = this.replyData.subject; const subject = this.formData.subject;
const replyingTo = this.replyData.replyingTo!; const replyingTo = this.formData.replyingTo!;
const files = this.replyData.files || []; const files = this.formData.files || [];
const options: AddonModForumReplyOptions = {}; const isEditOnline = this.formData.id && this.formData.id > 0;
const modal = await CoreDomUtils.showModalLoading('core.sending', true); const modal = await CoreDomUtils.showModalLoading('core.sending', true);
// Add some HTML to the message if needed. // Add some HTML to the message if needed.
message = CoreTextUtils.formatHtmlLines(message); message = CoreTextUtils.formatHtmlLines(message);
// Set private option if checked.
if (this.replyData.isprivatereply) {
options.private = true;
}
// Upload attachments first if any. // Upload attachments first if any.
let attachments; let attachments;
try {
if (files.length) { if (files.length) {
try { try {
attachments = await AddonModForumHelper.uploadOrStoreReplyFiles(this.forum.id, replyingTo, files, false); attachments = await AddonModForumHelper.uploadOrStoreReplyFiles(
this.forum.id,
isEditOnline ? this.formData.id! : replyingTo,
files,
false,
);
} catch (error) { } catch (error) {
// Cannot upload them in online, save them in offline. // Cannot upload them in online, save them in offline.
if (!this.forum.id) { if (!this.forum.id || isEditOnline) {
// Cannot store them in offline without the forum ID. Reject. // Cannot store them in offline. Reject.
return Promise.reject(error); throw error;
} }
saveOffline = true; saveOffline = true;
@ -448,13 +374,13 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
} }
} }
try { let sent = false;
if (attachments) {
options.attachmentsid = attachments;
}
let sent; if (isEditOnline) {
if (saveOffline) { sent = await AddonModForum.updatePost(this.formData.id!, subject, message, {
attachmentsid: attachments,
});
} else if (saveOffline) {
// Save post in offline. // Save post in offline.
await AddonModForumOffline.replyPost( await AddonModForumOffline.replyPost(
replyingTo, replyingTo,
@ -464,7 +390,10 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
this.courseId, this.courseId,
subject, subject,
message, message,
options, {
attachmentsid: attachments,
private: !!this.formData.isprivatereply,
},
); );
// Set sent to false since it wasn't sent to server. // Set sent to false since it wasn't sent to server.
@ -480,7 +409,10 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
this.courseId, this.courseId,
subject, subject,
message, message,
options, {
attachmentsid: attachments,
private: !!this.formData.isprivatereply,
},
undefined, undefined,
!files.length, !files.length,
); );
@ -492,17 +424,19 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
} }
// Reset data. // Reset data.
this.setReplyFormData(); this.setFormData();
this.onPostChange.emit(); this.onPostChange.emit();
CoreForms.triggerFormSubmittedEvent(this.formElement, sent, CoreSites.getCurrentSiteId()); CoreForms.triggerFormSubmittedEvent(this.formElement, sent, CoreSites.getCurrentSiteId());
if (this.syncId) { this.unblockOperation();
CoreSync.unblockOperation(AddonModForumProvider.COMPONENT, this.syncId);
}
} catch (error) { } catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_forum.couldnotadd', true); CoreDomUtils.showErrorModalDefault(
error,
isEditOnline ? 'addon.mod_forum.couldnotupdate' : 'addon.mod_forum.couldnotadd',
true,
);
} finally { } finally {
modal.dismiss(); modal.dismiss();
} }
@ -516,13 +450,11 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
await this.confirmDiscard(); await this.confirmDiscard();
// Reset data. // Reset data.
this.setReplyFormData(); this.setFormData();
CoreForms.triggerFormCancelledEvent(this.formElement, CoreSites.getCurrentSiteId()); CoreForms.triggerFormCancelledEvent(this.formElement, CoreSites.getCurrentSiteId());
if (this.syncId) { this.unblockOperation();
CoreSync.unblockOperation(AddonModForumProvider.COMPONENT, this.syncId);
}
} catch (error) { } catch (error) {
// Cancelled. // Cancelled.
} }
@ -548,13 +480,11 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
await CoreUtils.ignoreErrors(Promise.all(promises)); await CoreUtils.ignoreErrors(Promise.all(promises));
// Reset data. // Reset data.
this.setReplyFormData(); this.setFormData();
this.onPostChange.emit(); this.onPostChange.emit();
if (this.syncId) { this.unblockOperation();
CoreSync.unblockOperation(AddonModForumProvider.COMPONENT, this.syncId);
}
} catch (error) { } catch (error) {
// Cancelled. // Cancelled.
} }
@ -578,9 +508,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
* Component being destroyed. * Component being destroyed.
*/ */
ngOnDestroy(): void { ngOnDestroy(): void {
if (this.syncId) { this.unblockOperation();
CoreSync.unblockOperation(AddonModForumProvider.COMPONENT, this.syncId);
}
} }
/** /**
@ -588,13 +516,45 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
* *
* @return Promise resolved if the user confirms or data was not changed and rejected otherwise. * @return Promise resolved if the user confirms or data was not changed and rejected otherwise.
*/ */
protected confirmDiscard(): Promise<void> { protected async confirmDiscard(): Promise<void> {
if (AddonModForumHelper.hasPostDataChanged(this.replyData, this.originalData)) { if (AddonModForumHelper.hasPostDataChanged(this.formData, this.originalData)) {
// Show confirmation if some data has been modified. // Show confirmation if some data has been modified.
return CoreDomUtils.showConfirm(Translate.instant('core.confirmloss')); await CoreDomUtils.showConfirm(Translate.instant('core.confirmloss'));
} else {
return Promise.resolve();
} }
this.unblockOperation();
}
/**
* Unblock operation if there's any blocked operation.
*/
protected unblockOperation(): void {
if (!this.formData.syncId) {
return;
}
CoreSync.unblockOperation(AddonModForumProvider.COMPONENT, this.formData.syncId);
delete this.formData.syncId;
}
/**
* Scroll to reply/edit form.
*
* @param ticksToWait Number of ticks to wait before scrolling.
* @return Promise resolved when done.
*/
protected async scrollToForm(ticksToWait = 1): Promise<void> {
if (!this.content) {
return;
}
await CoreUtils.nextTicks(ticksToWait);
CoreDomUtils.scrollToElementBySelector(
this.elementRef.nativeElement,
this.content,
'#addon-forum-reply-edit-form-' + this.uniqueId,
);
} }
} }

View File

@ -93,7 +93,7 @@
<addon-mod-forum-post <addon-mod-forum-post
[post]="startingPost" [discussion]="discussion" [courseId]="courseId" [highlight]="true" [post]="startingPost" [discussion]="discussion" [courseId]="courseId" [highlight]="true"
[discussionId]="discussionId" [component]="component" [componentId]="cmId" [discussionId]="discussionId" [component]="component" [componentId]="cmId"
[replyData]="replyData" [originalData]="originalData" [forum]="forum" [accessInfo]="accessInfo" [formData]="formData" [originalData]="originalData" [forum]="forum" [accessInfo]="accessInfo"
[trackPosts]="trackPosts" [ratingInfo]="ratingInfo" [leavingPage]="leavingPage" [trackPosts]="trackPosts" [ratingInfo]="ratingInfo" [leavingPage]="leavingPage"
(onPostChange)="postListChanged()"> (onPostChange)="postListChanged()">
</addon-mod-forum-post> </addon-mod-forum-post>
@ -104,7 +104,7 @@
<core-spacer *ngIf="!first"></core-spacer> <core-spacer *ngIf="!first"></core-spacer>
<addon-mod-forum-post <addon-mod-forum-post
[post]="post" [courseId]="courseId" [discussionId]="discussionId" [post]="post" [courseId]="courseId" [discussionId]="discussionId"
[component]="component" [componentId]="cmId" [replyData]="replyData" [component]="component" [componentId]="cmId" [formData]="formData"
[originalData]="originalData" [parentSubject]="postSubjects[post.parentid]" [originalData]="originalData" [parentSubject]="postSubjects[post.parentid]"
[forum]="forum" [accessInfo]="accessInfo" [trackPosts]="trackPosts" [ratingInfo]="ratingInfo" [forum]="forum" [accessInfo]="accessInfo" [trackPosts]="trackPosts" [ratingInfo]="ratingInfo"
[leavingPage]="leavingPage" [leavingPage]="leavingPage"
@ -123,7 +123,7 @@
<ion-card> <ion-card>
<addon-mod-forum-post <addon-mod-forum-post
[post]="post" [courseId]="courseId" [discussionId]="discussionId" [component]="component" [post]="post" [courseId]="courseId" [discussionId]="discussionId" [component]="component"
[componentId]="cmId" [replyData]="replyData" [originalData]="originalData" [componentId]="cmId" [formData]="formData" [originalData]="originalData"
[parentSubject]="postSubjects[post.parentid]" [forum]="forum" [accessInfo]="accessInfo" [parentSubject]="postSubjects[post.parentid]" [forum]="forum" [accessInfo]="accessInfo"
[trackPosts]="trackPosts" [ratingInfo]="ratingInfo" [leavingPage]="leavingPage" [trackPosts]="trackPosts" [ratingInfo]="ratingInfo" [leavingPage]="leavingPage"
(onPostChange)="postListChanged()"> (onPostChange)="postListChanged()">

View File

@ -39,7 +39,7 @@ import {
AddonModForumDiscussion, AddonModForumDiscussion,
AddonModForumPost, AddonModForumPost,
AddonModForumProvider, AddonModForumProvider,
AddonModForumReply, AddonModForumPostFormData,
} from '../../services/forum'; } from '../../services/forum';
import { AddonModForumHelper } from '../../services/forum-helper'; import { AddonModForumHelper } from '../../services/forum-helper';
import { AddonModForumOffline } from '../../services/forum-offline'; import { AddonModForumOffline } from '../../services/forum-offline';
@ -74,7 +74,7 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
postHasOffline!: boolean; postHasOffline!: boolean;
sort: SortType = 'nested'; sort: SortType = 'nested';
trackPosts!: boolean; trackPosts!: boolean;
replyData: Omit<AddonModForumReply, 'id'> = { formData: AddonModForumSharedPostFormData = {
replyingTo: 0, replyingTo: 0,
isEditing: false, isEditing: false,
subject: '', subject: '',
@ -83,7 +83,7 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
isprivatereply: false, isprivatereply: false,
}; };
originalData: Omit<AddonModForumReply, 'id'> = { originalData: Omit<AddonModForumPostFormData, 'id'> = {
subject: null, subject: null,
message: null, message: null,
files: [], files: [],
@ -259,13 +259,13 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
* @return Resolved if we can leave it, rejected if not. * @return Resolved if we can leave it, rejected if not.
*/ */
async canLeave(): Promise<boolean> { async canLeave(): Promise<boolean> {
if (AddonModForumHelper.hasPostDataChanged(this.replyData, this.originalData)) { if (AddonModForumHelper.hasPostDataChanged(this.formData, this.originalData)) {
// Show confirmation if some data has been modified. // Show confirmation if some data has been modified.
await CoreDomUtils.showConfirm(Translate.instant('core.confirmcanceledit')); await CoreDomUtils.showConfirm(Translate.instant('core.confirmcanceledit'));
} }
// Delete the local files from the tmp folder. // Delete the local files from the tmp folder.
CoreFileUploader.clearTmpFiles(this.replyData.files); CoreFileUploader.clearTmpFiles(this.formData.files);
this.leavingPage = true; this.leavingPage = true;
@ -795,3 +795,11 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
} }
} }
/**
* Reply data shared by post.
*/
export type AddonModForumSharedPostFormData = Omit<AddonModForumPostFormData, 'id'> & {
id?: number; // ID when editing an online reply.
syncId?: string; // Sync ID if some post has blocked synchronization.
};

View File

@ -1559,9 +1559,9 @@ export type AddonModForumAccessInformation = {
}; };
/** /**
* Reply info. * Post creation or edition data.
*/ */
export type AddonModForumReply = { export type AddonModForumPostFormData = {
id: number; id: number;
subject: string | null; // Null means original data is not set. subject: string | null; // Null means original data is not set.
message: string | null; // Null means empty or just white space. message: string | null; // Null means empty or just white space.

View File

@ -1728,12 +1728,12 @@ export class CoreDomUtilsProvider {
/** /**
* Opens a popover. * Opens a popover.
* *
* @param popoverOptions Modal Options. * @param options Options.
* @return Promise resolved when the popover is dismissed or will be dismissed.
*/ */
async openPopover<T = void>( async openPopover<T = void>(options: OpenPopoverOptions): Promise<T | undefined> {
popoverOptions: PopoverOptions,
): Promise<T | undefined> {
const { waitForDismissCompleted, ...popoverOptions } = options;
const popover = await PopoverController.create(popoverOptions); const popover = await PopoverController.create(popoverOptions);
const zoomLevel = await CoreConfig.get(CoreConstants.SETTINGS_ZOOM_LEVEL, CoreZoomLevel.NORMAL); const zoomLevel = await CoreConfig.get(CoreConstants.SETTINGS_ZOOM_LEVEL, CoreZoomLevel.NORMAL);
@ -1743,16 +1743,15 @@ export class CoreDomUtilsProvider {
if (zoomLevel !== CoreZoomLevel.NORMAL) { if (zoomLevel !== CoreZoomLevel.NORMAL) {
switch (getMode()) { switch (getMode()) {
case 'ios': case 'ios':
fixIOSPopoverPosition(popover, popoverOptions.event); fixIOSPopoverPosition(popover, options.event);
break; break;
case 'md': case 'md':
fixMDPopoverPosition(popover, popoverOptions.event); fixMDPopoverPosition(popover, options.event);
break; break;
} }
} }
// If onDidDismiss is nedded we can add a new param to the function to wait one function or the other. const result = waitForDismissCompleted ? await popover.onDidDismiss<T>() : await popover.onWillDismiss<T>();
const result = await popover.onWillDismiss<T>();
if (result?.data) { if (result?.data) {
return result?.data; return result?.data;
} }
@ -2046,3 +2045,10 @@ export const CoreDomUtils = makeSingleton(CoreDomUtilsProvider);
type AnchorOrMediaElement = type AnchorOrMediaElement =
HTMLAnchorElement | HTMLImageElement | HTMLAudioElement | HTMLVideoElement | HTMLSourceElement | HTMLTrackElement; HTMLAnchorElement | HTMLImageElement | HTMLAudioElement | HTMLVideoElement | HTMLSourceElement | HTMLTrackElement;
/**
* Options for the openPopover function.
*/
export type OpenPopoverOptions = PopoverOptions & {
waitForDismissCompleted?: boolean;
};