MOBILE-3643 forum: Migrate discussion page
parent
b318b0e4a5
commit
2dd0aa4815
|
@ -14,23 +14,28 @@
|
|||
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { CoreSharedModule } from '@/core/shared.module';
|
||||
import { CoreCourseComponentsModule } from '@features/course/components/components.module';
|
||||
import { CoreEditorComponentsModule } from '@features/editor/components/components.module';
|
||||
import { CoreSharedModule } from '@/core/shared.module';
|
||||
import { CoreTagComponentsModule } from '@features/tag/components/components.module';
|
||||
|
||||
import { AddonModForumIndexComponent } from './index/index';
|
||||
import { AddonModForumPostComponent } from './post/post';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModForumIndexComponent,
|
||||
AddonModForumPostComponent,
|
||||
],
|
||||
imports: [
|
||||
CoreSharedModule,
|
||||
CoreCourseComponentsModule,
|
||||
CoreTagComponentsModule,
|
||||
CoreEditorComponentsModule,
|
||||
],
|
||||
exports: [
|
||||
AddonModForumIndexComponent,
|
||||
AddonModForumPostComponent,
|
||||
],
|
||||
})
|
||||
export class AddonModForumComponentsModule {}
|
||||
|
|
|
@ -0,0 +1,136 @@
|
|||
<div class="addon-mod_forum-post">
|
||||
<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-label>
|
||||
<div class="addon-mod-forum-post-title" *ngIf="displaySubject">
|
||||
<h2 class="ion-text-wrap">
|
||||
<ion-icon name="fa-map-pin" *ngIf="discussion && !post.parentid && discussion.pinned">
|
||||
</ion-icon>
|
||||
<ion-icon name="fa-star" class="addon-forum-star"
|
||||
*ngIf="discussion && !post.parentid && !discussion.pinned && discussion.starred">
|
||||
</ion-icon>
|
||||
<core-format-text [text]="post.subject" contextLevel="module" [contextInstanceId]="forum && forum.cmid" [courseId]="courseId">
|
||||
</core-format-text>
|
||||
</h2>
|
||||
<ion-note *ngIf="trackPosts && post.unread"
|
||||
class="ion-float-end ion-padding-left ion-text-end" [attr.aria-label]="'addon.mod_forum.unread' | translate">
|
||||
<ion-icon name="fa-circle" color="primary">
|
||||
</ion-icon>
|
||||
</ion-note>
|
||||
<ion-button *ngIf="optionsMenuEnabled"
|
||||
fill="clear" color="dark" (click)="showOptionsMenu($event)" [attr.aria-label]="('core.displayoptions' | translate)">
|
||||
<ion-icon name="more" slot="icon-only">
|
||||
</ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
<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>
|
||||
<div class="addon-mod-forum-post-author">
|
||||
<h3 *ngIf="post.author && post.author.fullname">{{post.author.fullname}}</h3>
|
||||
<p *ngIf="post.author && post.author.groups">
|
||||
<ng-container *ngFor="let group of post.author.groups">
|
||||
<ion-icon name="people"></ion-icon> {{ group.name }}
|
||||
</ng-container>
|
||||
</p>
|
||||
<p *ngIf="post.timecreated">{{post.timecreated * 1000 | coreFormatDate: "strftimerecentfull"}}</p>
|
||||
<p *ngIf="!post.timecreated"><ion-icon name="time"></ion-icon> {{ 'core.notsent' | translate }}</p>
|
||||
</div>
|
||||
<ng-container *ngIf="!displaySubject">
|
||||
<ion-note *ngIf="trackPosts && post.unread"
|
||||
class="ion-float-end ion-padding-left ion-text-end" [attr.aria-label]="'addon.mod_forum.unread' | translate">
|
||||
<ion-icon name="fa-circle" color="primary">
|
||||
</ion-icon>
|
||||
</ion-note>
|
||||
<ion-button *ngIf="optionsMenuEnabled"
|
||||
fill="clear" color="dark" (click)="showOptionsMenu($event)" [attr.aria-label]="('core.displayoptions' | translate)">
|
||||
<ion-icon name="more" slot="icon-only">
|
||||
</ion-icon>
|
||||
</ion-button>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-card-header>
|
||||
<ion-card-content [class]="post.parentid == 0 ? 'ion-padding-top' : ''">
|
||||
<div class="ion-padding-bottom" *ngIf="post.isprivatereply">
|
||||
<ion-note color="danger">{{ 'addon.mod_forum.postisprivatereply' | translate }}</ion-note>
|
||||
</div>
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="post.message"
|
||||
contextLevel="module" [contextInstanceId]="forum && forum.cmid" [courseId]="courseId">
|
||||
</core-format-text>
|
||||
<div lines="none" *ngIf="post.attachments && post.attachments.length > 0">
|
||||
<core-files [files]="post.attachments" [component]="component" [componentId]="componentId" showInline="true">
|
||||
</core-files>
|
||||
</div>
|
||||
</ion-card-content>
|
||||
<div class="addon-mod-forum-post-more-info">
|
||||
<ion-item class="ion-text-wrap" *ngIf="tagsEnabled && post.tags && post.tags.length > 0" lines="none">
|
||||
<div slot="start">{{ 'core.tag.tags' | translate }}:</div>
|
||||
<ion-label>
|
||||
<core-tag-list [tags]="post.tags"></core-tag-list>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item *ngIf="post.id > 0 && post.capabilities.reply && !post.isprivatereply"
|
||||
class="ion-no-padding ion-text-end addon-forum-reply-button">
|
||||
<ion-label>
|
||||
<ion-button fill="clear" size="small" (click)="showReplyForm()"
|
||||
[attr.aria-controls]="'addon-forum-reply-edit-form-' + uniqueId" [attr.aria-expanded]="replyData.replyingTo === post.id">
|
||||
<ion-icon name="fa-reply" slot="start">
|
||||
</ion-icon> {{ 'addon.mod_forum.reply' | translate }}
|
||||
</ion-button>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</div>
|
||||
|
||||
<form *ngIf="(post.id > 0 && !replyData.isEditing && replyData.replyingTo == post.id) || (post.id <=0 && replyData.isEditing && replyData.replyingTo == post.parentid)"
|
||||
ion-list [id]="'addon-forum-reply-edit-form-' + uniqueId" #replyFormEl>
|
||||
<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 item-content elementId="message" contextLevel="module"
|
||||
[control]="messageControl" [placeholder]="'addon.mod_forum.replyplaceholder' | translate"
|
||||
[name]="'mod_forum_reply_' + post.id" [component]="component" [componentId]="componentId" [autoSave]="true"
|
||||
[contextInstanceId]="forum && forum.cmid" [draftExtraParams]="{reply: post.id}"
|
||||
(contentChanged)="onMessageChange($event)">
|
||||
</core-rich-text-editor>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="accessInfo.canpostprivatereply">
|
||||
<ion-label>{{ 'addon.mod_forum.privatereply' | translate }}</ion-label>
|
||||
<ion-checkbox slot="end" [(ngModel)]="replyData.isprivatereply" name="isprivatereply"></ion-checkbox>
|
||||
</ion-item>
|
||||
<ng-container *ngIf="forum.id && forum.maxattachments > 0">
|
||||
<ion-item-divider class="core-expandable ion-text-wrap" (click)="toggleAdvanced()">
|
||||
<ion-label>
|
||||
<ion-icon *ngIf="!advanced" name="fa-caret-right" slot="start">
|
||||
</ion-icon>
|
||||
<ion-icon *ngIf="advanced" name="fa-caret-down" slot="start">
|
||||
</ion-icon>
|
||||
{{ 'addon.mod_forum.advanced' | translate }}
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
<ng-container *ngIf="advanced">
|
||||
<core-attachments
|
||||
[files]="replyData.files" [maxSize]="forum.maxbytes" [maxSubmissions]="forum.maxattachments"
|
||||
[component]="component" [componentId]="forum.cmid" [allowOffline]="true">
|
||||
</core-attachments>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
<ion-col>
|
||||
<ion-button expand="block" (click)="reply()" [disabled]="replyData.subject == '' || replyData.message == null">
|
||||
{{ 'addon.mod_forum.posttoforum' | translate }}
|
||||
</ion-button>
|
||||
</ion-col>
|
||||
<ion-col>
|
||||
<ion-button expand="block" color="light" (click)="cancel()">{{ 'core.cancel' | translate }}</ion-button>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
</form>
|
||||
</div>
|
|
@ -0,0 +1,72 @@
|
|||
@import "../../../../../theme/globals.scss";
|
||||
|
||||
:host .addon-mod_forum-post {
|
||||
background-color: var(--white);
|
||||
border-bottom: 1px solid var(--addon-forum-border-color);
|
||||
|
||||
.addon-forum-star {
|
||||
color: var(--core-color);
|
||||
}
|
||||
|
||||
ion-card-header .item {
|
||||
|
||||
&.highlight::part(native) {
|
||||
background-color: var(--addon-forum-highlight-color);
|
||||
}
|
||||
|
||||
ion-label {
|
||||
margin-top: 4px;
|
||||
|
||||
h2 {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
font-weight: bold;
|
||||
|
||||
ion-icon {
|
||||
@include margin(0, 6px, 0, 0);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
core-user-avatar {
|
||||
--core-avatar-size: var(--addon-forum-avatar-size);
|
||||
|
||||
@include margin(0, 8px, 0, 0);
|
||||
}
|
||||
|
||||
.addon-mod-forum-post-title,
|
||||
.addon-mod-forum-post-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.addon-mod-forum-post-info {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.addon-mod-forum-post-title + .addon-mod-forum-post-info {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.addon-mod-forum-post-title h2,
|
||||
.addon-mod-forum-post-info .addon-mod-forum-post-author {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ion-card-content {
|
||||
padding-top: 14px;
|
||||
}
|
||||
|
||||
.item .item-inner {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.addon-mod-forum-post-more-info div {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,510 @@
|
|||
// (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,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Optional,
|
||||
Output,
|
||||
SimpleChange,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreEvents } from '@singletons/events';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import {
|
||||
AddonModForum,
|
||||
AddonModForumAccessInformation,
|
||||
AddonModForumData,
|
||||
AddonModForumDiscussion,
|
||||
AddonModForumPost,
|
||||
AddonModForumProvider,
|
||||
} from '../../services/forum.service';
|
||||
import { CoreTag } from '@features/tag/services/tag';
|
||||
import { Translate } from '@singletons';
|
||||
import { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
|
||||
import { IonContent } from '@ionic/angular';
|
||||
import { AddonModForumSync } from '../../services/sync.service';
|
||||
import { CoreSync } from '@services/sync';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
import { AddonModForumHelper } from '../../services/helper.service';
|
||||
import { AddonModForumOffline, AddonModForumReplyOptions } from '../../services/offline.service';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
|
||||
/**
|
||||
* Components that shows a discussion post, its attachments and the action buttons allowed (reply, etc.).
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-mod-forum-post',
|
||||
templateUrl: 'post.html',
|
||||
styleUrls: ['post.scss'],
|
||||
})
|
||||
export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges {
|
||||
|
||||
@Input() post!: AddonModForumPost; // Post.
|
||||
@Input() courseId!: number; // Post's course ID.
|
||||
@Input() discussionId!: number; // Post's' discussion ID.
|
||||
@Input() discussion?: AddonModForumDiscussion; // Post's' discussion, only for starting posts.
|
||||
@Input() component!: string; // Component this post belong to.
|
||||
@Input() componentId!: number; // Component ID.
|
||||
@Input() replyData: any; // Object with the new post data. Usually shared between posts.
|
||||
@Input() originalData: any; // Object with the original post data. Usually shared between posts.
|
||||
@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() accessInfo!: AddonModForumAccessInformation; // Forum access information.
|
||||
@Input() parentSubject?: string; // Subject of parent post.
|
||||
@Input() ratingInfo?: any; // TODO CoreRatingInfo; // Rating info item.
|
||||
@Input() leavingPage?: boolean; // Whether the page that contains this post is being left and will be destroyed.
|
||||
@Input() highlight = false;
|
||||
@Output() onPostChange: EventEmitter<void> = new EventEmitter<void>(); // Event emitted when a reply is posted or modified.
|
||||
|
||||
@ViewChild('replyFormEl') formElement!: ElementRef;
|
||||
|
||||
messageControl = new FormControl();
|
||||
|
||||
uniqueId!: string;
|
||||
defaultReplySubject!: string;
|
||||
advanced = false; // Display all form fields.
|
||||
tagsEnabled!: boolean;
|
||||
displaySubject = true;
|
||||
optionsMenuEnabled = false;
|
||||
|
||||
protected syncId!: string;
|
||||
|
||||
constructor(
|
||||
protected elementRef: ElementRef,
|
||||
@Optional() protected content?: IonContent,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.tagsEnabled = CoreTag.instance.areTagsAvailableInSite();
|
||||
this.uniqueId = this.post.id > 0 ? 'reply' + this.post.id : 'edit' + this.post.parentid;
|
||||
|
||||
const reTranslated = Translate.instant('addon.mod_forum.re');
|
||||
this.displaySubject = !this.parentSubject ||
|
||||
(this.post.subject != this.parentSubject && this.post.subject != `Re: ${this.parentSubject}` &&
|
||||
this.post.subject != `${reTranslated} ${this.parentSubject}`);
|
||||
this.defaultReplySubject = this.post.replysubject || ((this.post.subject.startsWith('Re: ') ||
|
||||
this.post.subject.startsWith(reTranslated)) ? this.post.subject : `${reTranslated} ${this.post.subject}`);
|
||||
|
||||
this.optionsMenuEnabled = this.post.id < 0 || (AddonModForum.instance.isGetDiscussionPostAvailable() &&
|
||||
(AddonModForum.instance.isDeletePostAvailable() || AddonModForum.instance.isUpdatePostAvailable()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect changes on input properties.
|
||||
*/
|
||||
ngOnChanges(changes: {[name: string]: SimpleChange}): void {
|
||||
if (changes.leavingPage && this.leavingPage) {
|
||||
// Download all courses is enabled now, initialize it.
|
||||
CoreDomUtils.instance.triggerFormCancelledEvent(this.formElement, CoreSites.instance.getCurrentSiteId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an online post.
|
||||
*/
|
||||
async deletePost(): Promise<void> {
|
||||
try {
|
||||
await CoreDomUtils.instance.showDeleteConfirm('addon.mod_forum.deletesure');
|
||||
|
||||
const modal = await CoreDomUtils.instance.showModalLoading('core.deleting', true);
|
||||
|
||||
try {
|
||||
const response = await AddonModForum.instance.deletePost(this.post.id);
|
||||
|
||||
const data = {
|
||||
forumId: this.forum.id,
|
||||
discussionId: this.discussionId,
|
||||
cmId: this.forum.cmid,
|
||||
deleted: response.status,
|
||||
post: this.post,
|
||||
};
|
||||
|
||||
CoreEvents.trigger(
|
||||
AddonModForumProvider.CHANGE_DISCUSSION_EVENT,
|
||||
data,
|
||||
CoreSites.instance.getCurrentSiteId(),
|
||||
);
|
||||
|
||||
CoreDomUtils.instance.showToast('addon.mod_forum.deletedpost', true);
|
||||
} catch (error) {
|
||||
CoreDomUtils.instance.showErrorModal(error);
|
||||
} finally {
|
||||
modal.dismiss();
|
||||
}
|
||||
} catch (error) {
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set data to new reply post, clearing temporary files and updating original data.
|
||||
*
|
||||
* @param replyingTo Id of post beeing replied.
|
||||
* @param isEditing True it's an offline reply beeing edited, false otherwise.
|
||||
* @param subject Subject of the reply.
|
||||
* @param message Message of the reply.
|
||||
* @param isPrivate True if it's private reply.
|
||||
* @param files Reply attachments.
|
||||
*/
|
||||
protected setReplyFormData(
|
||||
replyingTo?: number,
|
||||
isEditing?: boolean,
|
||||
subject?: string,
|
||||
message?: string,
|
||||
files?: any[],
|
||||
isPrivate?: boolean,
|
||||
): void {
|
||||
// Delete the local files from the tmp folder if any.
|
||||
CoreFileUploader.instance.clearTmpFiles(this.replyData.files);
|
||||
|
||||
this.replyData.replyingTo = replyingTo || 0;
|
||||
this.replyData.isEditing = !!isEditing;
|
||||
this.replyData.subject = subject || this.defaultReplySubject || '';
|
||||
this.replyData.message = message || null;
|
||||
this.replyData.files = files || [];
|
||||
this.replyData.isprivatereply = !!isPrivate;
|
||||
|
||||
// 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();
|
||||
this.originalData.isprivatereply = this.replyData.isprivatereply;
|
||||
|
||||
// Show advanced fields if any of them has not the default value.
|
||||
this.advanced = this.replyData.files.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the context menu.
|
||||
*
|
||||
* @param e Click Event.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
showOptionsMenu(e: Event): void {
|
||||
alert('Options menu not implemented');
|
||||
|
||||
// @todo
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a form modal to edit an online post.
|
||||
*/
|
||||
editPost(): void {
|
||||
alert('Edit post not implemented');
|
||||
|
||||
// @todo
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this post as being replied to.
|
||||
*/
|
||||
async showReplyForm(): Promise<void> {
|
||||
if (this.replyData.isEditing) {
|
||||
// User is editing a post, data needs to be resetted. Ask confirm if there is unsaved data.
|
||||
try {
|
||||
await this.confirmDiscard();
|
||||
this.setReplyFormData(this.post.id);
|
||||
|
||||
if (this.content) {
|
||||
setTimeout(() => {
|
||||
CoreDomUtils.instance.scrollToElementBySelector(
|
||||
this.elementRef.nativeElement,
|
||||
this.content,
|
||||
'#addon-forum-reply-edit-form-' + this.uniqueId,
|
||||
);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// Cancelled.
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.replyData.replyingTo) {
|
||||
// User isn't replying, it's a brand new reply. Initialize the data.
|
||||
this.setReplyFormData(this.post.id);
|
||||
} else {
|
||||
// The post being replied has changed but the data will be kept.
|
||||
this.replyData.replyingTo = this.post.id;
|
||||
|
||||
if (this.replyData.subject == this.originalData.subject) {
|
||||
// Update subject only if it hadn't been modified
|
||||
this.replyData.subject = this.defaultReplySubject;
|
||||
this.originalData.subject = this.defaultReplySubject;
|
||||
}
|
||||
|
||||
this.messageControl.setValue(this.replyData.message);
|
||||
}
|
||||
|
||||
if (this.content) {
|
||||
setTimeout(() => {
|
||||
CoreDomUtils.instance.scrollToElementBySelector(
|
||||
this.elementRef.nativeElement,
|
||||
this.content,
|
||||
'#addon-forum-reply-edit-form-' + this.uniqueId,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this post as being edited to.
|
||||
*/
|
||||
async editOfflineReply(): Promise<void> {
|
||||
// Ask confirm if there is unsaved data.
|
||||
try {
|
||||
await this.confirmDiscard();
|
||||
|
||||
this.syncId = AddonModForumSync.instance.getDiscussionSyncId(this.discussionId);
|
||||
CoreSync.instance.blockOperation(AddonModForumProvider.COMPONENT, this.syncId);
|
||||
|
||||
this.setReplyFormData(
|
||||
this.post.parentid,
|
||||
true,
|
||||
this.post.subject,
|
||||
this.post.message,
|
||||
this.post.attachments,
|
||||
this.post.isprivatereply,
|
||||
);
|
||||
} catch (error) {
|
||||
// Cancelled.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Message changed.
|
||||
*
|
||||
* @param text The new text.
|
||||
*/
|
||||
onMessageChange(text: string): void {
|
||||
this.replyData.message = text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reply to this post.
|
||||
*/
|
||||
async reply(): Promise<void> {
|
||||
if (!this.replyData.subject) {
|
||||
CoreDomUtils.instance.showErrorModal('addon.mod_forum.erroremptysubject', true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.replyData.message) {
|
||||
CoreDomUtils.instance.showErrorModal('addon.mod_forum.erroremptymessage', true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let saveOffline = false;
|
||||
let message = this.replyData.message;
|
||||
const subject = this.replyData.subject;
|
||||
const replyingTo = this.replyData.replyingTo;
|
||||
const files = this.replyData.files || [];
|
||||
const options: AddonModForumReplyOptions = {};
|
||||
const modal = await CoreDomUtils.instance.showModalLoading('core.sending', true);
|
||||
|
||||
// Add some HTML to the message if needed.
|
||||
message = CoreTextUtils.instance.formatHtmlLines(message);
|
||||
|
||||
// Set private option if checked.
|
||||
if (this.replyData.isprivatereply) {
|
||||
options.private = true;
|
||||
}
|
||||
|
||||
// Upload attachments first if any.
|
||||
let attachments;
|
||||
|
||||
if (files.length) {
|
||||
try {
|
||||
attachments = await AddonModForumHelper.instance.uploadOrStoreReplyFiles(this.forum.id, replyingTo, files, false);
|
||||
} catch (error) {
|
||||
|
||||
// Cannot upload them in online, save them in offline.
|
||||
if (!this.forum.id) {
|
||||
// Cannot store them in offline without the forum ID. Reject.
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
saveOffline = true;
|
||||
attachments = await AddonModForumHelper.instance.uploadOrStoreReplyFiles(this.forum.id, replyingTo, files, true);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (attachments) {
|
||||
options.attachmentsid = attachments;
|
||||
}
|
||||
|
||||
let sent;
|
||||
if (saveOffline) {
|
||||
// Save post in offline.
|
||||
await AddonModForumOffline.instance.replyPost(
|
||||
replyingTo,
|
||||
this.discussionId,
|
||||
this.forum.id,
|
||||
this.forum.name,
|
||||
this.courseId,
|
||||
subject,
|
||||
message,
|
||||
options,
|
||||
);
|
||||
|
||||
// Set sent to false since it wasn't sent to server.
|
||||
sent = false;
|
||||
} else {
|
||||
// Try to send it to server.
|
||||
// Don't allow offline if there are attachments since they were uploaded fine.
|
||||
sent = await AddonModForum.instance.replyPost(
|
||||
replyingTo,
|
||||
this.discussionId,
|
||||
this.forum.id,
|
||||
this.forum.name,
|
||||
this.courseId,
|
||||
subject,
|
||||
message,
|
||||
options,
|
||||
undefined,
|
||||
!files.length,
|
||||
);
|
||||
}
|
||||
|
||||
if (sent && this.forum.id) {
|
||||
// Data sent to server, delete stored files (if any).
|
||||
AddonModForumHelper.instance.deleteReplyStoredFiles(this.forum.id, replyingTo);
|
||||
}
|
||||
|
||||
// Reset data.
|
||||
this.setReplyFormData();
|
||||
|
||||
this.onPostChange.emit();
|
||||
|
||||
CoreDomUtils.instance.triggerFormSubmittedEvent(this.formElement, sent, CoreSites.instance.getCurrentSiteId());
|
||||
|
||||
if (this.syncId) {
|
||||
CoreSync.instance.unblockOperation(AddonModForumProvider.COMPONENT, this.syncId);
|
||||
}
|
||||
} catch (error) {
|
||||
CoreDomUtils.instance.showErrorModalDefault(error, 'addon.mod_forum.couldnotadd', true);
|
||||
} finally {
|
||||
modal.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel reply.
|
||||
*/
|
||||
async cancel(): Promise<void> {
|
||||
try {
|
||||
await this.confirmDiscard();
|
||||
|
||||
// Reset data.
|
||||
this.setReplyFormData();
|
||||
|
||||
CoreDomUtils.instance.triggerFormCancelledEvent(this.formElement, CoreSites.instance.getCurrentSiteId());
|
||||
|
||||
if (this.syncId) {
|
||||
CoreSync.instance.unblockOperation(AddonModForumProvider.COMPONENT, this.syncId);
|
||||
}
|
||||
} catch (error) {
|
||||
// Cancelled.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Discard offline reply.
|
||||
*/
|
||||
async discardOfflineReply(): Promise<void> {
|
||||
try {
|
||||
await CoreDomUtils.instance.showDeleteConfirm();
|
||||
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
promises.push(AddonModForumOffline.instance.deleteReply(this.post.parentid!));
|
||||
|
||||
if (this.forum.id) {
|
||||
promises.push(AddonModForumHelper.instance.deleteReplyStoredFiles(this.forum.id, this.post.parentid!).catch(() => {
|
||||
// Ignore errors, maybe there are no files.
|
||||
}));
|
||||
}
|
||||
|
||||
await CoreUtils.instance.ignoreErrors(Promise.all(promises));
|
||||
|
||||
// Reset data.
|
||||
this.setReplyFormData();
|
||||
|
||||
this.onPostChange.emit();
|
||||
|
||||
if (this.syncId) {
|
||||
CoreSync.instance.unblockOperation(AddonModForumProvider.COMPONENT, this.syncId);
|
||||
}
|
||||
} catch (error) {
|
||||
// Cancelled.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function called when rating is updated online.
|
||||
*/
|
||||
ratingUpdated(): void {
|
||||
AddonModForum.instance.invalidateDiscussionPosts(this.discussionId, this.forum.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show or hide advanced form fields.
|
||||
*/
|
||||
toggleAdvanced(): void {
|
||||
this.advanced = !this.advanced;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
if (this.syncId) {
|
||||
CoreSync.instance.unblockOperation(AddonModForumProvider.COMPONENT, this.syncId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm discard changes if any.
|
||||
*
|
||||
* @return Promise resolved if the user confirms or data was not changed and rejected otherwise.
|
||||
*/
|
||||
protected confirmDiscard(): Promise<void> {
|
||||
if (AddonModForumHelper.instance.hasPostDataChanged(this.replyData, this.originalData)) {
|
||||
// Show confirmation if some data has been modified.
|
||||
return CoreDomUtils.instance.showConfirm(Translate.instant('core.confirmloss'));
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -19,12 +19,37 @@ import { CoreSharedModule } from '@/core/shared.module';
|
|||
|
||||
import { AddonModForumComponentsModule } from './components/components.module';
|
||||
import { AddonModForumIndexPage } from './pages/index';
|
||||
import { AddonModForumDiscussionPage } from './pages/discussion/discussion';
|
||||
import { conditionalRoutes } from '@/app/app-routing.module';
|
||||
import { CoreScreen } from '@services/screen';
|
||||
|
||||
const routes: Routes = [
|
||||
const mobileRoutes: Routes = [
|
||||
{
|
||||
path: ':courseId/:cmId',
|
||||
component: AddonModForumIndexPage,
|
||||
},
|
||||
{
|
||||
path: ':courseId/:cmId/:discussionId',
|
||||
component: AddonModForumDiscussionPage,
|
||||
},
|
||||
];
|
||||
|
||||
const tabletRoutes: Routes = [
|
||||
{
|
||||
path: ':courseId/:cmId',
|
||||
component: AddonModForumIndexPage,
|
||||
children: [
|
||||
{
|
||||
path: ':discussionId',
|
||||
component: AddonModForumDiscussionPage,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const routes: Routes = [
|
||||
...conditionalRoutes(mobileRoutes, () => CoreScreen.instance.isMobile),
|
||||
...conditionalRoutes(tabletRoutes, () => CoreScreen.instance.isTablet),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@ -35,6 +60,7 @@ const routes: Routes = [
|
|||
],
|
||||
declarations: [
|
||||
AddonModForumIndexPage,
|
||||
AddonModForumDiscussionPage,
|
||||
],
|
||||
})
|
||||
export class AddonModForumLazyModule {}
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title *ngIf="startingPost">
|
||||
<core-format-text contextLevel="module" [text]="startingPost.subject" [contextInstanceId]="cmId" [courseId]="courseId">
|
||||
</core-format-text>
|
||||
</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<!-- The context menu will be added in here. -->
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<core-navbar-buttons slot="end">
|
||||
<core-context-menu>
|
||||
<core-context-menu-item [priority]="650" *ngIf="discussionLoaded && !postHasOffline && isOnline" [content]="'addon.mod_forum.refreshposts' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"></core-context-menu-item>
|
||||
<core-context-menu-item [priority]="550" *ngIf="discussionLoaded && !isSplitViewOn && postHasOffline && isOnline" [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(null, $event, true)" [iconAction]="syncIcon" [closeOnClick]="false"></core-context-menu-item>
|
||||
<core-context-menu-item [hidden]="sort == 'flat-oldest'" [priority]="500" [content]="'addon.mod_forum.modeflatoldestfirst' | translate" (action)="changeSort('flat-oldest')" iconAction="arrow-round-down"></core-context-menu-item>
|
||||
<core-context-menu-item [hidden]="sort == 'flat-newest'" [priority]="450" [content]="'addon.mod_forum.modeflatnewestfirst' | translate" (action)="changeSort('flat-newest')" iconAction="arrow-round-up"></core-context-menu-item>
|
||||
<core-context-menu-item [hidden]="sort == 'nested'" [priority]="400" [content]="'addon.mod_forum.modenested' | translate" (action)="changeSort('nested')" iconAction="swap"></core-context-menu-item>
|
||||
<core-context-menu-item [hidden]="!discussion || !discussion.canlock || discussion.locked" [priority]="300" [content]="'addon.mod_forum.lockdiscussion' | translate" (action)="setLockState(true)" iconAction="fa-lock"></core-context-menu-item>
|
||||
<core-context-menu-item [hidden]="!discussion || !discussion.canlock || !discussion.locked" [priority]="300" [content]="'addon.mod_forum.unlockdiscussion' | translate" (action)="setLockState(false)" iconAction="fa-unlock"></core-context-menu-item>
|
||||
<core-context-menu-item [hidden]="!discussion || !canPin || discussion.pinned" [priority]="250" [content]="'addon.mod_forum.pindiscussion' | translate" (action)="setPinState(true)" iconAction="fa-map-pin"></core-context-menu-item>
|
||||
<core-context-menu-item [hidden]="!discussion || !canPin || !discussion.pinned" [priority]="250" [content]="'addon.mod_forum.unpindiscussion' | translate" (action)="setPinState(false)" iconAction="fa-map-pin" [iconSlash]="true"></core-context-menu-item>
|
||||
<core-context-menu-item [hidden]="!discussion || !discussion.canfavourite || discussion.starred" [priority]="200" [content]="'addon.mod_forum.addtofavourites' | translate" (action)="toggleFavouriteState(true)" iconAction="fa-star"></core-context-menu-item>
|
||||
<core-context-menu-item [hidden]="!discussion || !discussion.canfavourite || !discussion.starred" [priority]="200" [content]="'addon.mod_forum.removefromfavourites' | translate" (action)="toggleFavouriteState(false)" iconAction="fa-star" [iconSlash]="true"></core-context-menu-item>
|
||||
</core-context-menu>
|
||||
</core-navbar-buttons>
|
||||
<ion-content>
|
||||
<ion-refresher slot="fixed" [disabled]="!discussionLoaded" (ionRefresh)="doRefresh($event)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
|
||||
<core-loading [hideUntil]="discussionLoaded">
|
||||
<!-- Discussion replies found to be synchronized -->
|
||||
<ion-card class="core-warning-card" *ngIf="postHasOffline || hasOfflineRatings">
|
||||
<ion-item>
|
||||
<ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon>
|
||||
<ion-label>{{ 'core.hasdatatosync' | translate:{$a: discussionStr} }}</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
|
||||
<!-- Cut-off date or due date message -->
|
||||
<ion-card class="core-info-card" *ngIf="availabilityMessage">
|
||||
<ion-item>
|
||||
<ion-icon name="information-circle" slot="start"></ion-icon>
|
||||
<ion-label>{{ availabilityMessage }}</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
|
||||
<ion-card class="core-info-card" *ngIf="discussion && discussion.locked">
|
||||
<ion-item>
|
||||
<ion-icon name="fa-lock" slot="start"></ion-icon>
|
||||
<ion-label>{{ 'addon.mod_forum.discussionlocked' | translate }}</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
|
||||
<div *ngIf="startingPost" class="ion-margin-bottom">
|
||||
<addon-mod-forum-post
|
||||
[post]="startingPost" [discussion]="discussion" [courseId]="courseId" [highlight]="true"
|
||||
[discussionId]="discussionId" [component]="component" [componentId]="cmId"
|
||||
[replyData]="replyData" [originalData]="originalData" [forum]="forum" [accessInfo]="accessInfo"
|
||||
[trackPosts]="trackPosts" [ratingInfo]="ratingInfo" [leavingPage]="leavingPage"
|
||||
(onPostChange)="postListChanged()">
|
||||
</addon-mod-forum-post>
|
||||
</div>
|
||||
|
||||
<ion-card *ngIf="sort != 'nested'">
|
||||
<ng-container *ngFor="let post of posts; first as first">
|
||||
<ion-item-divider *ngIf="!first"><ion-label></ion-label></ion-item-divider>
|
||||
<addon-mod-forum-post
|
||||
[post]="post" [courseId]="courseId" [discussionId]="discussionId"
|
||||
[component]="component" [componentId]="cmId" [replyData]="replyData"
|
||||
[originalData]="originalData" [parentSubject]="postSubjects[post.parentid]"
|
||||
[forum]="forum" [accessInfo]="accessInfo" [trackPosts]="trackPosts" [ratingInfo]="ratingInfo"
|
||||
[leavingPage]="leavingPage"
|
||||
(onPostChange)="postListChanged()">
|
||||
</addon-mod-forum-post>
|
||||
</ng-container>
|
||||
</ion-card>
|
||||
|
||||
<ng-container *ngIf="sort == 'nested'">
|
||||
<ng-container *ngFor="let post of posts">
|
||||
<ng-container *ngTemplateOutlet="nestedPosts; context: {post: post}"></ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #nestedPosts let-post="post">
|
||||
<ion-card>
|
||||
<addon-mod-forum-post
|
||||
[post]="post" [courseId]="courseId" [discussionId]="discussionId" [component]="component"
|
||||
[componentId]="cmId" [replyData]="replyData" [originalData]="originalData"
|
||||
[parentSubject]="postSubjects[post.parentid]" [forum]="forum" [accessInfo]="accessInfo"
|
||||
[trackPosts]="trackPosts" [ratingInfo]="ratingInfo" [leavingPage]="leavingPage"
|
||||
(onPostChange)="postListChanged()">
|
||||
</addon-mod-forum-post>
|
||||
</ion-card>
|
||||
<div class="ion-padding-left" *ngIf="post.children.length && post.children[0].subject">
|
||||
<ng-container *ngFor="let child of post.children">
|
||||
<ng-container *ngTemplateOutlet="nestedPosts; context: {post: child}"></ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-template>
|
||||
</core-loading>
|
||||
</ion-content>
|
|
@ -0,0 +1,7 @@
|
|||
:host {
|
||||
|
||||
.addon-forum-reply-button .label {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,758 @@
|
|||
// (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, OnDestroy, ViewChild, OnInit, AfterViewInit, ElementRef } from '@angular/core';
|
||||
import { CoreUser } from '@features/user/services/user';
|
||||
import { IonContent } from '@ionic/angular';
|
||||
import { CoreApp } from '@services/app';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { Network, NgZone, Translate } from '@singletons';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import { Subscription } from 'rxjs';
|
||||
import {
|
||||
AddonModForum,
|
||||
AddonModForumAccessInformation,
|
||||
AddonModForumData,
|
||||
AddonModForumDiscussion,
|
||||
AddonModForumPost,
|
||||
AddonModForumProvider,
|
||||
AddonModForumRatingInfo,
|
||||
} from '../../services/forum.service';
|
||||
import { AddonModForumHelper } from '../../services/helper.service';
|
||||
import { AddonModForumOffline } from '../../services/offline.service';
|
||||
import { AddonModForumSync, AddonModForumSyncProvider } from '../../services/sync.service';
|
||||
|
||||
type SortType = 'flat-newest' | 'flat-oldest' | 'nested';
|
||||
|
||||
type Post = AddonModForumPost & { children?: Post[] };
|
||||
|
||||
/**
|
||||
* Page that displays a forum discussion.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'page-addon-mod-forum-discussion',
|
||||
templateUrl: 'discussion.html',
|
||||
styleUrls: ['discussion.scss'],
|
||||
})
|
||||
export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
@ViewChild(IonContent) content!: IonContent;
|
||||
|
||||
courseId!: number;
|
||||
discussionId!: number;
|
||||
forum: Partial<AddonModForumData> = {};
|
||||
accessInfo: AddonModForumAccessInformation = {};
|
||||
discussion!: AddonModForumDiscussion;
|
||||
startingPost?: Post;
|
||||
posts!: Post[];
|
||||
discussionLoaded = false;
|
||||
postSubjects!: { [id: string]: string };
|
||||
isOnline!: boolean;
|
||||
postHasOffline!: boolean;
|
||||
sort: SortType = 'nested';
|
||||
trackPosts!: boolean;
|
||||
replyData = {
|
||||
replyingTo: 0,
|
||||
isEditing: false,
|
||||
subject: '',
|
||||
message: null, // Null means empty or just white space.
|
||||
files: [],
|
||||
isprivatereply: false,
|
||||
};
|
||||
|
||||
originalData = {
|
||||
subject: null, // Null means original data is not set.
|
||||
message: null, // Null means empty or just white space.
|
||||
files: [],
|
||||
isprivatereply: false,
|
||||
};
|
||||
|
||||
refreshIcon = 'spinner';
|
||||
syncIcon = 'spinner';
|
||||
discussionStr = '';
|
||||
component = AddonModForumProvider.COMPONENT;
|
||||
cmId!: number;
|
||||
canPin = false;
|
||||
availabilityMessage: string | null = null;
|
||||
leavingPage = false;
|
||||
|
||||
protected forumId!: number;
|
||||
protected postId!: number;
|
||||
protected parent!: number;
|
||||
protected onlineObserver?: Subscription;
|
||||
protected syncObserver?: CoreEventObserver;
|
||||
protected syncManualObserver?: CoreEventObserver;
|
||||
|
||||
ratingInfo?: AddonModForumRatingInfo;
|
||||
hasOfflineRatings!: boolean;
|
||||
protected ratingOfflineObserver?: CoreEventObserver;
|
||||
protected ratingSyncObserver?: CoreEventObserver;
|
||||
protected changeDiscObserver?: CoreEventObserver;
|
||||
|
||||
constructor(protected elementRef: ElementRef) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.courseId = CoreNavigator.instance.getRouteNumberParam('courseId')!;
|
||||
this.cmId = CoreNavigator.instance.getRouteNumberParam('cmId')!;
|
||||
this.forumId = CoreNavigator.instance.getRouteNumberParam('forumId')!;
|
||||
this.discussion = CoreNavigator.instance.getRouteParam<AddonModForumDiscussion>('discussion')!;
|
||||
this.discussionId = this.discussion
|
||||
? this.discussion.discussion
|
||||
: CoreNavigator.instance.getRouteNumberParam('discussionId')!;
|
||||
this.trackPosts = CoreNavigator.instance.getRouteBooleanParam('trackPosts')!;
|
||||
this.postId = CoreNavigator.instance.getRouteNumberParam('postId')!;
|
||||
this.parent = CoreNavigator.instance.getRouteNumberParam('parent')!;
|
||||
|
||||
this.isOnline = CoreApp.instance.isOnline();
|
||||
this.onlineObserver = Network.instance.onChange().subscribe(() => {
|
||||
// Execute the callback in the Angular zone, so change detection doesn't stop working.
|
||||
NgZone.instance.run(() => {
|
||||
this.isOnline = CoreApp.instance.isOnline();
|
||||
});
|
||||
});
|
||||
|
||||
this.discussionStr = Translate.instant('addon.mod_forum.discussion');
|
||||
}
|
||||
|
||||
/**
|
||||
* View loaded.
|
||||
*/
|
||||
async ngAfterViewInit(): Promise<void> {
|
||||
if (this.parent) {
|
||||
this.sort = 'nested'; // Force nested order.
|
||||
} else {
|
||||
this.sort = await this.getUserSort();
|
||||
}
|
||||
|
||||
await this.fetchPosts(true, false, true);
|
||||
|
||||
const scrollTo = this.postId || this.parent;
|
||||
if (scrollTo) {
|
||||
// Scroll to the post.
|
||||
setTimeout(() => {
|
||||
CoreDomUtils.instance.scrollToElementBySelector(
|
||||
this.elementRef.nativeElement,
|
||||
this.content,
|
||||
'#addon-mod_forum-post-' + scrollTo,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sort type configured by the current user.
|
||||
*
|
||||
* @return Promise resolved with the sort type.
|
||||
*/
|
||||
protected async getUserSort(): Promise<SortType> {
|
||||
try {
|
||||
const value = await CoreSites.instance.getCurrentSite()!.getLocalSiteConfig<SortType>('AddonModForumDiscussionSort');
|
||||
|
||||
return value;
|
||||
} catch (error) {
|
||||
try {
|
||||
const value = await CoreUser.instance.getUserPreference('forum_displaymode');
|
||||
|
||||
switch (Number(value)) {
|
||||
case 1:
|
||||
return 'flat-oldest';
|
||||
case -1:
|
||||
return 'flat-newest';
|
||||
case 3:
|
||||
return 'nested';
|
||||
case 2: // Threaded not implemented.
|
||||
default:
|
||||
// Not set, use default sort.
|
||||
// @TODO add fallback to $CFG->forum_displaymode.
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore errors.
|
||||
}
|
||||
}
|
||||
|
||||
return 'flat-oldest';
|
||||
}
|
||||
|
||||
/**
|
||||
* User entered the page that contains the component.
|
||||
*/
|
||||
ionViewDidEnter(): void {
|
||||
if (this.syncObserver) {
|
||||
// Already setup.
|
||||
return;
|
||||
}
|
||||
|
||||
// Refresh data if this discussion is synchronized automatically.
|
||||
this.syncObserver = CoreEvents.on(AddonModForumSyncProvider.AUTO_SYNCED, (data: any) => {
|
||||
if (data.forumId == this.forumId && this.discussionId == data.discussionId
|
||||
&& data.userId == CoreSites.instance.getCurrentSiteUserId()) {
|
||||
// Refresh the data.
|
||||
this.discussionLoaded = false;
|
||||
this.refreshPosts();
|
||||
}
|
||||
}, CoreSites.instance.getCurrentSiteId());
|
||||
|
||||
// Refresh data if this forum discussion is synchronized from discussions list.
|
||||
this.syncManualObserver = CoreEvents.on(AddonModForumSyncProvider.MANUAL_SYNCED, (data: any) => {
|
||||
if (data.source != 'discussion' && data.forumId == this.forumId &&
|
||||
data.userId == CoreSites.instance.getCurrentSiteUserId()) {
|
||||
// Refresh the data.
|
||||
this.discussionLoaded = false;
|
||||
this.refreshPosts();
|
||||
}
|
||||
}, CoreSites.instance.getCurrentSiteId());
|
||||
|
||||
// Trigger view event, to highlight the current opened discussion in the split view.
|
||||
CoreEvents.trigger(AddonModForumProvider.VIEW_DISCUSSION_EVENT, {
|
||||
forumId: this.forumId,
|
||||
discussion: this.discussionId,
|
||||
}, CoreSites.instance.getCurrentSiteId());
|
||||
|
||||
this.changeDiscObserver = CoreEvents.on(AddonModForumProvider.CHANGE_DISCUSSION_EVENT, (data: any) => {
|
||||
if ((this.forumId && this.forumId === data.forumId) || data.cmId === this.cmId) {
|
||||
AddonModForum.instance.invalidateDiscussionsList(this.forumId).finally(() => {
|
||||
if (typeof data.locked != 'undefined') {
|
||||
this.discussion.locked = data.locked;
|
||||
}
|
||||
if (typeof data.pinned != 'undefined') {
|
||||
this.discussion.pinned = data.pinned;
|
||||
}
|
||||
if (typeof data.starred != 'undefined') {
|
||||
this.discussion.starred = data.starred;
|
||||
}
|
||||
|
||||
if (typeof data.deleted != 'undefined' && data.deleted) {
|
||||
if (!data.post.parentid) {
|
||||
// @todo
|
||||
// if (this.svComponent && this.svComponent.isOn()) {
|
||||
// this.svComponent.emptyDetails();
|
||||
// } else {
|
||||
// this.navCtrl.pop();
|
||||
// }
|
||||
} else {
|
||||
this.discussionLoaded = false;
|
||||
this.refreshPosts();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// @todo
|
||||
// /**
|
||||
// * Check if we can leave the page or not.
|
||||
// *
|
||||
// * @return Resolved if we can leave it, rejected if not.
|
||||
// */
|
||||
// async ionViewCanLeave(): Promise<void> {
|
||||
|
||||
// if (AddonModForumHelper.instance.hasPostDataChanged(this.replyData, this.originalData)) {
|
||||
// // Show confirmation if some data has been modified.
|
||||
// await CoreDomUtils.instance.showConfirm(this.translate.instant('core.confirmcanceledit'));
|
||||
// }
|
||||
|
||||
// // Delete the local files from the tmp folder.
|
||||
// this.uploaderProvider.clearTmpFiles(this.replyData.files);
|
||||
|
||||
// this.leavingPage = true;
|
||||
// }
|
||||
|
||||
/**
|
||||
* Convenience function to get the forum.
|
||||
*
|
||||
* @return Promise resolved with the forum.
|
||||
*/
|
||||
protected fetchForum(): Promise<AddonModForumData> {
|
||||
if (this.courseId && this.cmId) {
|
||||
return AddonModForum.instance.getForum(this.courseId, this.cmId);
|
||||
}
|
||||
|
||||
if (this.courseId && this.forumId) {
|
||||
return AddonModForum.instance.getForumById(this.courseId, this.forumId);
|
||||
}
|
||||
|
||||
throw new Error('Cannot get the forum');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to get the posts.
|
||||
*
|
||||
* @param sync Whether to try to synchronize the discussion.
|
||||
* @param showErrors Whether to show errors in a modal.
|
||||
* @param forceMarkAsRead Whether to mark all posts as read.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async fetchPosts(sync?: boolean, showErrors?: boolean, forceMarkAsRead?: boolean): Promise<void> {
|
||||
let onlinePosts: AddonModForumPost[] = [];
|
||||
const offlineReplies: AddonModForumPost[] = [];
|
||||
let hasUnreadPosts = false;
|
||||
|
||||
try {
|
||||
if (sync) {
|
||||
// Try to synchronize the forum.
|
||||
await CoreUtils.instance.ignoreErrors(this.syncDiscussion(!!showErrors));
|
||||
}
|
||||
|
||||
const response = await AddonModForum.instance.getDiscussionPosts(this.discussionId, { cmId: this.cmId });
|
||||
const replies = await AddonModForumOffline.instance.getDiscussionReplies(this.discussionId);
|
||||
const ratingInfo = response.ratinginfo;
|
||||
onlinePosts = response.posts;
|
||||
this.courseId = response.courseid || this.courseId;
|
||||
this.forumId = response.forumid || this.forumId;
|
||||
|
||||
// Check if there are responses stored in offline.
|
||||
this.postHasOffline = !!replies.length;
|
||||
const convertPromises: Promise<void>[] = [];
|
||||
|
||||
// Index posts to allow quick access. Also check unread field.
|
||||
const onlinePostsMap: Record<string, AddonModForumPost> = {};
|
||||
onlinePosts.forEach((post) => {
|
||||
onlinePostsMap[post.id] = post;
|
||||
hasUnreadPosts = hasUnreadPosts || !!post.unread;
|
||||
});
|
||||
|
||||
replies.forEach((offlineReply) => {
|
||||
// If we don't have forumId and courseId, get it from the post.
|
||||
if (!this.forumId) {
|
||||
this.forumId = offlineReply.forumid;
|
||||
}
|
||||
if (!this.courseId) {
|
||||
this.courseId = offlineReply.courseid;
|
||||
}
|
||||
|
||||
convertPromises.push(
|
||||
AddonModForumHelper.instance
|
||||
.convertOfflineReplyToOnline(offlineReply)
|
||||
.then(async reply => {
|
||||
offlineReplies.push(reply);
|
||||
|
||||
// Disable reply of the parent. Reply in offline to the same post is not allowed, edit instead.
|
||||
posts[reply.parentid!].capabilities.reply = false;
|
||||
|
||||
return;
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
await Promise.all(convertPromises);
|
||||
|
||||
// Convert back to array.
|
||||
onlinePosts = CoreUtils.instance.objectToArray(onlinePostsMap);
|
||||
|
||||
let posts = offlineReplies.concat(onlinePosts);
|
||||
|
||||
this.startingPost = AddonModForum.instance.extractStartingPost(posts);
|
||||
|
||||
// If sort type is nested, normal sorting is disabled and nested posts will be displayed.
|
||||
if (this.sort == 'nested') {
|
||||
// Sort first by creation date to make format tree work.
|
||||
AddonModForum.instance.sortDiscussionPosts(posts, 'ASC');
|
||||
|
||||
const rootId = this.startingPost ? this.startingPost.id : (this.discussion ? this.discussion.id : 0);
|
||||
posts = CoreUtils.instance.formatTree(posts, 'parentid', 'id', rootId);
|
||||
} else {
|
||||
// Set default reply subject.
|
||||
const direction = this.sort == 'flat-newest' ? 'DESC' : 'ASC';
|
||||
AddonModForum.instance.sortDiscussionPosts(posts, direction);
|
||||
}
|
||||
|
||||
try {
|
||||
// Now try to get the forum.
|
||||
const forum = await this.fetchForum();
|
||||
// "forum.istracked" is more reliable than "trackPosts".
|
||||
if (typeof forum.istracked != 'undefined') {
|
||||
this.trackPosts = forum.istracked;
|
||||
}
|
||||
|
||||
this.forumId = forum.id;
|
||||
this.cmId = forum.cmid;
|
||||
this.courseId = forum.course;
|
||||
this.forum = forum;
|
||||
this.availabilityMessage = AddonModForumHelper.instance.getAvailabilityMessage(forum);
|
||||
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
promises.push(
|
||||
AddonModForum.instance
|
||||
.getAccessInformation(this.forumId, { cmId: this.cmId })
|
||||
.then(async accessInfo => {
|
||||
this.accessInfo = accessInfo;
|
||||
|
||||
// Disallow replying if cut-off date is reached and the user has not the capability to override it.
|
||||
// Just in case the posts were fetched from WS when the cut-off date was not reached but it is now.
|
||||
if (AddonModForumHelper.instance.isCutoffDateReached(forum) && !accessInfo.cancanoverridecutoff) {
|
||||
posts.forEach((post) => {
|
||||
post.capabilities.reply = false;
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}),
|
||||
);
|
||||
|
||||
// The discussion object was not passed as parameter and there is no starting post. Should not happen.
|
||||
if (!this.discussion) {
|
||||
promises.push(this.loadDiscussion(this.forumId, this.cmId, this.discussionId));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
} catch (error) {
|
||||
// Ignore errors.
|
||||
}
|
||||
|
||||
if (!this.discussion && !this.startingPost) {
|
||||
// The discussion object was not passed as parameter and there is no starting post. Should not happen.
|
||||
throw new Error('Invalid forum discussion.');
|
||||
}
|
||||
|
||||
if (this.startingPost && this.startingPost.author && this.forum.type == 'single') {
|
||||
// Hide author and groups for first post and type single.
|
||||
delete this.startingPost.author.fullname;
|
||||
delete this.startingPost.author.groups;
|
||||
}
|
||||
|
||||
this.posts = posts;
|
||||
this.ratingInfo = ratingInfo;
|
||||
this.postSubjects = this.getAllPosts().reduce(
|
||||
(postSubjects, post) => {
|
||||
postSubjects[post.id] = post.subject;
|
||||
|
||||
return postSubjects;
|
||||
},
|
||||
this.startingPost
|
||||
? { [this.startingPost.id]: this.startingPost.subject }
|
||||
: {},
|
||||
);
|
||||
|
||||
if (AddonModForum.instance.isSetPinStateAvailableForSite()) {
|
||||
// Use the canAddDiscussion WS to check if the user can pin discussions.
|
||||
try {
|
||||
const response = await AddonModForum.instance.canAddDiscussionToAll(this.forumId, { cmId: this.cmId });
|
||||
|
||||
this.canPin = !!response.canpindiscussions;
|
||||
} catch (error) {
|
||||
this.canPin = false;
|
||||
}
|
||||
} else {
|
||||
this.canPin = false;
|
||||
}
|
||||
} catch (error) {
|
||||
CoreDomUtils.instance.showErrorModal(error);
|
||||
} finally {
|
||||
this.discussionLoaded = true;
|
||||
this.refreshIcon = 'refresh';
|
||||
this.syncIcon = 'sync';
|
||||
|
||||
if (forceMarkAsRead || (hasUnreadPosts && this.trackPosts)) {
|
||||
// // Add log in Moodle and mark unread posts as readed.
|
||||
AddonModForum.instance.logDiscussionView(this.discussionId, this.forumId || -1, this.forum.name).catch(() => {
|
||||
// Ignore errors.
|
||||
}).finally(() => {
|
||||
// Trigger mark read posts.
|
||||
CoreEvents.trigger(AddonModForumProvider.MARK_READ_EVENT, {
|
||||
courseId: this.courseId,
|
||||
moduleId: this.cmId,
|
||||
}, CoreSites.instance.getCurrentSiteId());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to load discussion.
|
||||
*
|
||||
* @param forumId Forum ID.
|
||||
* @param cmId Forum cmid.
|
||||
* @param discussionId Discussion ID.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async loadDiscussion(forumId: number, cmId: number, discussionId: number): Promise<void> {
|
||||
// Fetch the discussion if not passed as parameter.
|
||||
if (this.discussion || !forumId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const discussion = await AddonModForumHelper.instance.getDiscussionById(forumId, cmId, discussionId);
|
||||
|
||||
this.discussion = discussion;
|
||||
this.discussionId = this.discussion.discussion;
|
||||
} catch (error) {
|
||||
// Ignore errors.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to synchronize the posts discussion.
|
||||
*
|
||||
* @param showErrors Whether to show errors in a modal.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async syncDiscussion(showErrors: boolean): Promise<void> {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
promises.push(
|
||||
AddonModForumSync.instance
|
||||
.syncDiscussionReplies(this.discussionId)
|
||||
.then((result) => {
|
||||
if (result.warnings && result.warnings.length) {
|
||||
CoreDomUtils.instance.showErrorModal(result.warnings[0]);
|
||||
}
|
||||
|
||||
if (result && result.updated) {
|
||||
// Sync successful, send event.
|
||||
CoreEvents.trigger(AddonModForumSyncProvider.MANUAL_SYNCED, {
|
||||
forumId: this.forumId,
|
||||
userId: CoreSites.instance.getCurrentSiteUserId(),
|
||||
source: 'discussion',
|
||||
}, CoreSites.instance.getCurrentSiteId());
|
||||
}
|
||||
|
||||
return;
|
||||
}),
|
||||
);
|
||||
|
||||
promises.push(
|
||||
AddonModForumSync.instance
|
||||
.syncRatings(this.cmId, this.discussionId)
|
||||
.then((result) => {
|
||||
if (result.warnings && result.warnings.length) {
|
||||
CoreDomUtils.instance.showErrorModal(result.warnings[0]);
|
||||
}
|
||||
|
||||
return;
|
||||
}),
|
||||
);
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
} catch (error) {
|
||||
if (showErrors) {
|
||||
CoreDomUtils.instance.showErrorModalDefault(error, 'core.errorsync', true);
|
||||
}
|
||||
|
||||
throw new Error('Failed syncing discussion');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the data.
|
||||
*
|
||||
* @param refresher Refresher.
|
||||
* @param done Function to call when done.
|
||||
* @param showErrors If show errors to the user of hide them.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async doRefresh(refresher?: any, done?: () => void, showErrors: boolean = false): Promise<void> {
|
||||
if (this.discussionLoaded) {
|
||||
await this.refreshPosts(true, showErrors).finally(() => {
|
||||
refresher && refresher.complete();
|
||||
done && done();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh posts.
|
||||
*
|
||||
* @param sync Whether to try to synchronize the discussion.
|
||||
* @param showErrors Whether to show errors in a modal.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
refreshPosts(sync?: boolean, showErrors?: boolean): Promise<void> {
|
||||
this.content.scrollToTop();
|
||||
this.refreshIcon = 'spinner';
|
||||
this.syncIcon = 'spinner';
|
||||
|
||||
const promises = [
|
||||
AddonModForum.instance.invalidateForumData(this.courseId),
|
||||
AddonModForum.instance.invalidateDiscussionPosts(this.discussionId, this.forumId),
|
||||
AddonModForum.instance.invalidateAccessInformation(this.forumId),
|
||||
AddonModForum.instance.invalidateCanAddDiscussion(this.forumId),
|
||||
];
|
||||
|
||||
return CoreUtils.instance.allPromises(promises).catch(() => {
|
||||
// Ignore errors.
|
||||
}).then(() => this.fetchPosts(sync, showErrors));
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to change posts sorting
|
||||
*
|
||||
* @param type Sort type.
|
||||
* @return Promised resolved when done.
|
||||
*/
|
||||
changeSort(type: SortType): Promise<any> {
|
||||
this.discussionLoaded = false;
|
||||
this.sort = type;
|
||||
CoreSites.instance.getCurrentSite()!.setLocalSiteConfig('AddonModForumDiscussionSort', this.sort);
|
||||
this.content.scrollToTop();
|
||||
|
||||
return this.fetchPosts();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock or unlock the discussion.
|
||||
*
|
||||
* @param locked True to lock the discussion, false to unlock.
|
||||
*/
|
||||
async setLockState(locked: boolean): Promise<void> {
|
||||
const modal = await CoreDomUtils.instance.showModalLoading('core.sending', true);
|
||||
|
||||
try {
|
||||
const response = await AddonModForum.instance.setLockState(this.forumId, this.discussionId, locked);
|
||||
this.discussion.locked = response.locked;
|
||||
|
||||
const data = {
|
||||
forumId: this.forumId,
|
||||
discussionId: this.discussionId,
|
||||
cmId: this.cmId,
|
||||
locked: this.discussion.locked,
|
||||
};
|
||||
CoreEvents.trigger(AddonModForumProvider.CHANGE_DISCUSSION_EVENT, data, CoreSites.instance.getCurrentSiteId());
|
||||
|
||||
CoreDomUtils.instance.showToast('addon.mod_forum.lockupdated', true);
|
||||
} catch (error) {
|
||||
CoreDomUtils.instance.showErrorModal(error);
|
||||
} finally {
|
||||
modal.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pin or unpin the discussion.
|
||||
*
|
||||
* @param pinned True to pin the discussion, false to unpin it.
|
||||
*/
|
||||
async setPinState(pinned: boolean): Promise<void> {
|
||||
const modal = await CoreDomUtils.instance.showModalLoading('core.sending', true);
|
||||
|
||||
try {
|
||||
await AddonModForum.instance.setPinState(this.discussionId, pinned);
|
||||
|
||||
this.discussion.pinned = pinned;
|
||||
|
||||
const data = {
|
||||
forumId: this.forumId,
|
||||
discussionId: this.discussionId,
|
||||
cmId: this.cmId,
|
||||
pinned: this.discussion.pinned,
|
||||
};
|
||||
CoreEvents.trigger(AddonModForumProvider.CHANGE_DISCUSSION_EVENT, data, CoreSites.instance.getCurrentSiteId());
|
||||
|
||||
CoreDomUtils.instance.showToast('addon.mod_forum.pinupdated', true);
|
||||
} catch (error) {
|
||||
CoreDomUtils.instance.showErrorModal(error);
|
||||
} finally {
|
||||
modal.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Star or unstar the discussion.
|
||||
*
|
||||
* @param starred True to star the discussion, false to unstar it.
|
||||
*/
|
||||
async toggleFavouriteState(starred: boolean): Promise<void> {
|
||||
const modal = await CoreDomUtils.instance.showModalLoading('core.sending', true);
|
||||
|
||||
try {
|
||||
await AddonModForum.instance.toggleFavouriteState(this.discussionId, starred);
|
||||
|
||||
this.discussion.starred = starred;
|
||||
|
||||
const data = {
|
||||
forumId: this.forumId,
|
||||
discussionId: this.discussionId,
|
||||
cmId: this.cmId,
|
||||
starred: this.discussion.starred,
|
||||
};
|
||||
CoreEvents.trigger(AddonModForumProvider.CHANGE_DISCUSSION_EVENT, data, CoreSites.instance.getCurrentSiteId());
|
||||
|
||||
CoreDomUtils.instance.showToast('addon.mod_forum.favouriteupdated', true);
|
||||
} catch (error) {
|
||||
CoreDomUtils.instance.showErrorModal(error);
|
||||
} finally {
|
||||
modal.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* New post added.
|
||||
*/
|
||||
postListChanged(): void {
|
||||
// Trigger an event to notify a new reply.
|
||||
const data = {
|
||||
forumId: this.forumId,
|
||||
discussionId: this.discussionId,
|
||||
cmId: this.cmId,
|
||||
};
|
||||
CoreEvents.trigger(AddonModForumProvider.REPLY_DISCUSSION_EVENT, data, CoreSites.instance.getCurrentSiteId());
|
||||
|
||||
this.discussionLoaded = false;
|
||||
this.refreshPosts().finally(() => {
|
||||
this.discussionLoaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs when the page is about to leave and no longer be the active page.
|
||||
*/
|
||||
ionViewWillLeave(): void {
|
||||
this.syncObserver && this.syncObserver.off();
|
||||
this.syncManualObserver && this.syncManualObserver.off();
|
||||
this.ratingOfflineObserver && this.ratingOfflineObserver.off();
|
||||
this.ratingSyncObserver && this.ratingSyncObserver.off();
|
||||
this.changeDiscObserver && this.changeDiscObserver.off();
|
||||
delete this.syncObserver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Page destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.onlineObserver && this.onlineObserver.unsubscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the posts contained in the discussion.
|
||||
*
|
||||
* @return Array containing all the posts of the discussion.
|
||||
*/
|
||||
protected getAllPosts(): Post[] {
|
||||
return this.posts.map(this.flattenPostHierarchy.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten a post's hierarchy into an array.
|
||||
*
|
||||
* @param parent Parent post.
|
||||
* @return Array containing all the posts within the hierarchy (including the parent).
|
||||
*/
|
||||
protected flattenPostHierarchy(parent: Post): Post[] {
|
||||
const posts = [parent];
|
||||
const children = parent.children || [];
|
||||
|
||||
for (const child of children) {
|
||||
posts.push(...this.flattenPostHierarchy(child));
|
||||
}
|
||||
|
||||
return posts;
|
||||
}
|
||||
|
||||
}
|
|
@ -16,6 +16,7 @@ import { Injectable } from '@angular/core';
|
|||
import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
|
||||
import { CoreCourseCommonModWSOptions } from '@features/course/services/course';
|
||||
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
|
||||
import { CoreFileEntry } from '@features/fileuploader/services/fileuploader';
|
||||
import { CoreUser } from '@features/user/services/user';
|
||||
import { CoreApp } from '@services/app';
|
||||
import { CoreFilepool } from '@services/filepool';
|
||||
|
@ -25,7 +26,7 @@ import { CoreUrlUtils } from '@services/utils/url';
|
|||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreStatusWithWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws';
|
||||
import { makeSingleton, Translate } from '@singletons';
|
||||
import { AddonModForumOffline, AddonModForumReplyOptions } from './offline.service';
|
||||
import { AddonModForumOffline, AddonModForumOfflineDiscussion, AddonModForumReplyOptions } from './offline.service';
|
||||
|
||||
const ROOT_CACHE_KEY = 'mmaModForum:';
|
||||
|
||||
|
@ -275,13 +276,13 @@ export class AddonModForumProvider {
|
|||
* @return Promise resolved when done.
|
||||
* @since 3.8
|
||||
*/
|
||||
async deletePost(postId: number, siteId?: string): Promise<void> {
|
||||
async deletePost(postId: number, siteId?: string): Promise<AddonModForumDeletePostWSResponse> {
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
const params: AddonModForumDeletePostWSParams = {
|
||||
postid: postId,
|
||||
};
|
||||
|
||||
await site.write<AddonModForumDeletePostWSResponse>('mod_forum_delete_post', params);
|
||||
return site.write<AddonModForumDeletePostWSResponse>('mod_forum_delete_post', params);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -355,7 +356,12 @@ export class AddonModForumProvider {
|
|||
* @param discussions List of discussions to format.
|
||||
* @return Promise resolved with the formatted discussions.
|
||||
*/
|
||||
formatDiscussionsGroups(cmId: number, discussions: any[]): Promise<any[]> {
|
||||
formatDiscussionsGroups(cmId: number, discussions: AddonModForumDiscussion[]): Promise<AddonModForumDiscussion[]>;
|
||||
formatDiscussionsGroups(cmId: number, discussions: AddonModForumOfflineDiscussion[]): Promise<AddonModForumOfflineDiscussion[]>;
|
||||
formatDiscussionsGroups(
|
||||
cmId: number,
|
||||
discussions: AddonModForumDiscussion[] | AddonModForumOfflineDiscussion[],
|
||||
): Promise<AddonModForumDiscussion[] | AddonModForumOfflineDiscussion[]> {
|
||||
discussions = CoreUtils.instance.clone(discussions);
|
||||
|
||||
return CoreGroups.instance.getActivityAllowedGroups(cmId).then((result) => {
|
||||
|
@ -447,7 +453,7 @@ export class AddonModForumProvider {
|
|||
throw new Error('Post not found');
|
||||
}
|
||||
|
||||
return response.post;
|
||||
return this.translateWSPost(response.post);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -539,9 +545,9 @@ export class AddonModForumProvider {
|
|||
ratinginfo?: AddonModForumRatingInfo;
|
||||
}> {
|
||||
// Convenience function to translate legacy data to new format.
|
||||
const translateLegacyPostsFormat = (posts: any[]): any[] => posts.map((post) => {
|
||||
const newPost = {
|
||||
id: post.id ,
|
||||
const translateLegacyPostsFormat = (posts: AddonModForumLegacyPost[]): AddonModForumPost[] => posts.map((post) => {
|
||||
const newPost: AddonModForumPost = {
|
||||
id: post.id,
|
||||
discussionid: post.discussion,
|
||||
parentid: post.parent,
|
||||
hasparent: !!post.parent,
|
||||
|
@ -563,8 +569,8 @@ export class AddonModForumProvider {
|
|||
tags: post.tags,
|
||||
};
|
||||
|
||||
if (post.groupname) {
|
||||
newPost.author['groups'] = [{ name: post.groupname }];
|
||||
if ('groupname' in post && typeof post['groupname'] === 'string') {
|
||||
newPost.author['groups'] = [{ name: post['groupname'] }];
|
||||
}
|
||||
|
||||
return newPost;
|
||||
|
@ -572,26 +578,10 @@ export class AddonModForumProvider {
|
|||
|
||||
// For some reason, the new WS doesn't use the tags exporter so it returns a different format than other WebServices.
|
||||
// Convert the new format to the exporter one so it's the same as in other WebServices.
|
||||
const translateTagsFormatToLegacy = (posts: any[]): any[] => {
|
||||
posts.forEach((post) => {
|
||||
post.tags = post.tags.map((tag) => {
|
||||
const viewUrl = (tag.urls && tag.urls.view) || '';
|
||||
const params = CoreUrlUtils.instance.extractUrlParams(viewUrl);
|
||||
const translateTagsFormatToLegacy = (posts: AddonModForumWSPost[]): AddonModForumPost[] => {
|
||||
posts.forEach(post => this.translateWSPost(post));
|
||||
|
||||
return {
|
||||
id: tag.tagid,
|
||||
taginstanceid: tag.id,
|
||||
flag: tag.flag ? 1 : 0,
|
||||
isstandard: tag.isstandard,
|
||||
rawname: tag.displayname,
|
||||
name: tag.displayname,
|
||||
tagcollid: params.tc ? Number(params.tc) : undefined,
|
||||
taginstancecontextid: params.from ? Number(params.from) : undefined,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
return posts;
|
||||
return posts as unknown as AddonModForumPost[];
|
||||
};
|
||||
|
||||
const params: AddonModForumGetDiscussionPostsWSParams | AddonModForumGetForumDiscussionPostsWSParams = {
|
||||
|
@ -619,15 +609,16 @@ export class AddonModForumProvider {
|
|||
throw new Error('Could not get forum posts');
|
||||
}
|
||||
|
||||
if (isGetDiscussionPostsAvailable) {
|
||||
response.posts = translateTagsFormatToLegacy((response as AddonModForumGetDiscussionPostsWSResponse).posts);
|
||||
} else {
|
||||
response.posts = translateLegacyPostsFormat((response as AddonModForumGetForumDiscussionPostsWSResponse).posts);
|
||||
}
|
||||
const posts = isGetDiscussionPostsAvailable
|
||||
? translateTagsFormatToLegacy((response as AddonModForumGetDiscussionPostsWSResponse).posts)
|
||||
: translateLegacyPostsFormat((response as AddonModForumGetForumDiscussionPostsWSResponse).posts);
|
||||
|
||||
this.storeUserData(response.posts);
|
||||
this.storeUserData(posts);
|
||||
|
||||
return response as AddonModForumGetDiscussionPostsWSResponse;
|
||||
return {
|
||||
...response,
|
||||
posts,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -790,7 +781,7 @@ export class AddonModForumProvider {
|
|||
throw new Error('Could not get discussions');
|
||||
}
|
||||
|
||||
await this.storeUserData(response.discussions);
|
||||
this.storeUserData(response.discussions);
|
||||
|
||||
return {
|
||||
discussions: response.discussions,
|
||||
|
@ -1093,7 +1084,13 @@ export class AddonModForumProvider {
|
|||
// If there's already a reply to be sent to the server, discard it first.
|
||||
try {
|
||||
await AddonModForumOffline.instance.deleteReply(postId, siteId);
|
||||
await this.replyPostOnline(postId, subject, message, options, siteId);
|
||||
await this.replyPostOnline(
|
||||
postId,
|
||||
subject,
|
||||
message,
|
||||
options as unknown as AddonModForumAddDiscussionPostWSOptionsObject,
|
||||
siteId,
|
||||
);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
|
@ -1157,7 +1154,12 @@ export class AddonModForumProvider {
|
|||
* @return Promise resolved when done.
|
||||
* @since 3.7
|
||||
*/
|
||||
async setLockState(forumId: number, discussionId: number, locked: boolean, siteId?: string): Promise<void> {
|
||||
async setLockState(
|
||||
forumId: number,
|
||||
discussionId: number,
|
||||
locked: boolean,
|
||||
siteId?: string,
|
||||
): Promise<AddonModForumSetLockStateWSResponse> {
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
const params: AddonModForumSetLockStateWSParams = {
|
||||
forumid: forumId,
|
||||
|
@ -1165,7 +1167,7 @@ export class AddonModForumProvider {
|
|||
targetstate: locked ? 0 : 1,
|
||||
};
|
||||
|
||||
await site.write<AddonModForumSetLockStateWSResponse>('mod_forum_set_lock_state', params);
|
||||
return site.write<AddonModForumSetLockStateWSResponse>('mod_forum_set_lock_state', params);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1211,7 +1213,7 @@ export class AddonModForumProvider {
|
|||
const site = await CoreSites.instance.getSite(siteId);
|
||||
const params: AddonModForumToggleFavouriteStateWSParams = {
|
||||
discussionid: discussionId,
|
||||
targetstate: starred ? 1 : 0 as any,
|
||||
targetstate: starred,
|
||||
};
|
||||
|
||||
await site.write<AddonModForumToggleFavouriteStateWSResponse>('mod_forum_toggle_favourite_state', params);
|
||||
|
@ -1222,30 +1224,30 @@ export class AddonModForumProvider {
|
|||
*
|
||||
* @param list Array of posts or discussions.
|
||||
*/
|
||||
protected storeUserData(list: any[]): void {
|
||||
protected storeUserData(list: AddonModForumPost[] | AddonModForumDiscussion[]): void {
|
||||
const users = {};
|
||||
|
||||
list.forEach((entry) => {
|
||||
if (entry.author) {
|
||||
list.forEach((entry: AddonModForumPost | AddonModForumDiscussion) => {
|
||||
if ('author' in entry) {
|
||||
const authorId = Number(entry.author.id);
|
||||
if (!isNaN(authorId) && !users[authorId]) {
|
||||
users[authorId] = {
|
||||
id: entry.author.id,
|
||||
fullname: entry.author.fullname,
|
||||
profileimageurl: entry.author.urls.profileimage,
|
||||
profileimageurl: entry.author.urls?.profileimage,
|
||||
};
|
||||
}
|
||||
}
|
||||
const userId = parseInt(entry.userid);
|
||||
if (!isNaN(userId) && !users[userId]) {
|
||||
const userId = parseInt(entry['userid']);
|
||||
if ('userid' in entry && !isNaN(userId) && !users[userId]) {
|
||||
users[userId] = {
|
||||
id: userId,
|
||||
fullname: entry.userfullname,
|
||||
profileimageurl: entry.userpictureurl,
|
||||
};
|
||||
}
|
||||
const userModified = parseInt(entry.usermodified);
|
||||
if (!isNaN(userModified) && !users[userModified]) {
|
||||
const userModified = parseInt(entry['usermodified']);
|
||||
if ('usermodified' in entry && !isNaN(userModified) && !users[userModified]) {
|
||||
users[userModified] = {
|
||||
id: userModified,
|
||||
fullname: entry.usermodifiedfullname,
|
||||
|
@ -1293,6 +1295,33 @@ export class AddonModForumProvider {
|
|||
return response && response.status;
|
||||
}
|
||||
|
||||
/**
|
||||
* For some reason, the new WS doesn't use the tags exporter so it returns a different format than other WebServices.
|
||||
* Convert the new format to the exporter one so it's the same as in other WebServices.
|
||||
*
|
||||
* @param post Post returned by the new WS.
|
||||
* @return Post using the same format as other WebServices.
|
||||
*/
|
||||
protected translateWSPost(post: AddonModForumWSPost): AddonModForumPost {
|
||||
(post as unknown as AddonModForumPost).tags = (post.tags || []).map((tag) => {
|
||||
const viewUrl = (tag.urls && tag.urls.view) || '';
|
||||
const params = CoreUrlUtils.instance.extractUrlParams(viewUrl);
|
||||
|
||||
return {
|
||||
id: tag.tagid,
|
||||
taginstanceid: tag.id,
|
||||
flag: tag.flag ? 1 : 0,
|
||||
isstandard: tag.isstandard,
|
||||
rawname: tag.displayname,
|
||||
name: tag.displayname,
|
||||
tagcollid: params.tc ? Number(params.tc) : undefined,
|
||||
taginstancecontextid: params.from ? Number(params.from) : undefined,
|
||||
};
|
||||
});
|
||||
|
||||
return post as unknown as AddonModForumPost;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class AddonModForum extends makeSingleton(AddonModForumProvider) {}
|
||||
|
@ -1353,6 +1382,7 @@ export type AddonModForumDiscussion = {
|
|||
id: number; // Post id.
|
||||
name: string; // Discussion name.
|
||||
groupid: number; // Group id.
|
||||
groupname?: string; // Group name (not returned by WS).
|
||||
timemodified: number; // Time modified.
|
||||
usermodified: number; // The id of the user who last modified.
|
||||
timestart: number; // Time discussion can start.
|
||||
|
@ -1386,6 +1416,49 @@ export type AddonModForumDiscussion = {
|
|||
canfavourite?: boolean; // Can the user star the discussion.
|
||||
};
|
||||
|
||||
/**
|
||||
* Forum post data returned by web services.
|
||||
*/
|
||||
export type AddonModForumPost = {
|
||||
id: number; // Id.
|
||||
subject: string; // Subject.
|
||||
replysubject?: string; // Replysubject.
|
||||
message: string; // Message.
|
||||
author: {
|
||||
id?: number; // Id.
|
||||
fullname?: string; // Fullname.
|
||||
urls?: {
|
||||
profileimage?: string; // The URL for the use profile image.
|
||||
};
|
||||
groups?: { // Groups.
|
||||
name: string; // Name.
|
||||
}[];
|
||||
};
|
||||
discussionid: number; // Discussionid.
|
||||
hasparent: boolean; // Hasparent.
|
||||
parentid?: number; // Parentid.
|
||||
timecreated: number | false; // Timecreated.
|
||||
unread?: boolean; // Unread.
|
||||
isprivatereply: boolean; // Isprivatereply.
|
||||
capabilities: {
|
||||
reply: boolean; // Whether the user can reply to the post.
|
||||
};
|
||||
attachment?: 0 | 1;
|
||||
attachments?: (CoreFileEntry | AddonModForumWSPostAttachment)[];
|
||||
tags?: { // Tags.
|
||||
id: number; // Tag id.
|
||||
name: string; // Tag name.
|
||||
rawname: string; // The raw, unnormalised name for the tag as entered by users.
|
||||
// isstandard: boolean; // Whether this tag is standard.
|
||||
tagcollid?: number; // Tag collection id.
|
||||
taginstanceid: number; // Tag instance id.
|
||||
taginstancecontextid?: number; // Context the tag instance belongs to.
|
||||
// itemid: number; // Id of the record tagged.
|
||||
// ordering: number; // Tag ordering.
|
||||
flag: number; // Whether the tag is flagged as inappropriate.
|
||||
}[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Legacy forum post data.
|
||||
*/
|
||||
|
@ -1427,112 +1500,6 @@ export type AddonModForumLegacyPost = {
|
|||
}[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Forum post data.
|
||||
*/
|
||||
export type AddonModForumPost = {
|
||||
id: number; // Id.
|
||||
subject: string; // Subject.
|
||||
replysubject: string; // Replysubject.
|
||||
message: string; // Message.
|
||||
messageformat: number; // Message format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
|
||||
author: {
|
||||
id?: number; // Id.
|
||||
fullname?: string; // Fullname.
|
||||
isdeleted?: boolean; // Isdeleted.
|
||||
groups?: { // Groups.
|
||||
id: number; // Id.
|
||||
name: string; // Name.
|
||||
urls: {
|
||||
image?: string; // Image.
|
||||
};
|
||||
}[];
|
||||
urls: {
|
||||
profile?: string; // The URL for the use profile page.
|
||||
profileimage?: string; // The URL for the use profile image.
|
||||
};
|
||||
};
|
||||
discussionid: number; // Discussionid.
|
||||
hasparent: boolean; // Hasparent.
|
||||
parentid?: number; // Parentid.
|
||||
timecreated: number; // Timecreated.
|
||||
unread?: boolean; // Unread.
|
||||
isdeleted: boolean; // Isdeleted.
|
||||
isprivatereply: boolean; // Isprivatereply.
|
||||
haswordcount: boolean; // Haswordcount.
|
||||
wordcount?: number; // Wordcount.
|
||||
charcount?: number; // Charcount.
|
||||
capabilities: {
|
||||
view: boolean; // Whether the user can view the post.
|
||||
edit: boolean; // Whether the user can edit the post.
|
||||
delete: boolean; // Whether the user can delete the post.
|
||||
split: boolean; // Whether the user can split the post.
|
||||
reply: boolean; // Whether the user can reply to the post.
|
||||
selfenrol: boolean; // Whether the user can self enrol into the course.
|
||||
export: boolean; // Whether the user can export the post.
|
||||
controlreadstatus: boolean; // Whether the user can control the read status of the post.
|
||||
canreplyprivately: boolean; // Whether the user can post a private reply.
|
||||
};
|
||||
urls?: {
|
||||
view?: string; // The URL used to view the post.
|
||||
viewisolated?: string; // The URL used to view the post in isolation.
|
||||
viewparent?: string; // The URL used to view the parent of the post.
|
||||
edit?: string; // The URL used to edit the post.
|
||||
delete?: string; // The URL used to delete the post.
|
||||
|
||||
// The URL used to split the discussion with the selected post being the first post in the new discussion.
|
||||
split?: string;
|
||||
|
||||
reply?: string; // The URL used to reply to the post.
|
||||
export?: string; // The URL used to export the post.
|
||||
markasread?: string; // The URL used to mark the post as read.
|
||||
markasunread?: string; // The URL used to mark the post as unread.
|
||||
discuss?: string; // Discuss.
|
||||
};
|
||||
attachments: { // Attachments.
|
||||
contextid: number; // Contextid.
|
||||
component: string; // Component.
|
||||
filearea: string; // Filearea.
|
||||
itemid: number; // Itemid.
|
||||
filepath: string; // Filepath.
|
||||
filename: string; // Filename.
|
||||
isdir: boolean; // Isdir.
|
||||
isimage: boolean; // Isimage.
|
||||
timemodified: number; // Timemodified.
|
||||
timecreated: number; // Timecreated.
|
||||
filesize: number; // Filesize.
|
||||
author: string; // Author.
|
||||
license: string; // License.
|
||||
filenameshort: string; // Filenameshort.
|
||||
filesizeformatted: string; // Filesizeformatted.
|
||||
icon: string; // Icon.
|
||||
timecreatedformatted: string; // Timecreatedformatted.
|
||||
timemodifiedformatted: string; // Timemodifiedformatted.
|
||||
url: string; // Url.
|
||||
urls: {
|
||||
export?: string; // The URL used to export the attachment.
|
||||
};
|
||||
html: {
|
||||
plagiarism?: string; // The HTML source for the Plagiarism Response.
|
||||
};
|
||||
}[];
|
||||
tags?: { // Tags.
|
||||
id: number; // The ID of the Tag.
|
||||
tagid: number; // The tagid.
|
||||
isstandard: boolean; // Whether this is a standard tag.
|
||||
displayname: string; // The display name of the tag.
|
||||
flag: boolean; // Wehther this tag is flagged.
|
||||
urls: {
|
||||
view: string; // The URL to view the tag.
|
||||
};
|
||||
}[];
|
||||
html?: {
|
||||
rating?: string; // The HTML source to rate the post.
|
||||
taglist?: string; // The HTML source to view the list of tags.
|
||||
authorsubheading?: string; // The HTML source to view the author details.
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Forum rating info.
|
||||
*/
|
||||
|
@ -1640,6 +1607,117 @@ export type AddonModForumSortOrder = {
|
|||
value: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Forum post attachement data returned by web services.
|
||||
*/
|
||||
export type AddonModForumWSPostAttachment = {
|
||||
contextid: number; // Contextid.
|
||||
component: string; // Component.
|
||||
filearea: string; // Filearea.
|
||||
itemid: number; // Itemid.
|
||||
filepath: string; // Filepath.
|
||||
filename: string; // Filename.
|
||||
isdir: boolean; // Isdir.
|
||||
isimage: boolean; // Isimage.
|
||||
timemodified: number; // Timemodified.
|
||||
timecreated: number; // Timecreated.
|
||||
filesize: number; // Filesize.
|
||||
author: string; // Author.
|
||||
license: string; // License.
|
||||
filenameshort: string; // Filenameshort.
|
||||
filesizeformatted: string; // Filesizeformatted.
|
||||
icon: string; // Icon.
|
||||
timecreatedformatted: string; // Timecreatedformatted.
|
||||
timemodifiedformatted: string; // Timemodifiedformatted.
|
||||
url: string; // Url.
|
||||
urls: {
|
||||
export?: string; // The URL used to export the attachment.
|
||||
};
|
||||
html: {
|
||||
plagiarism?: string; // The HTML source for the Plagiarism Response.
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Forum post data returned by web services.
|
||||
*/
|
||||
export type AddonModForumWSPost = {
|
||||
id: number; // Id.
|
||||
subject: string; // Subject.
|
||||
replysubject: string; // Replysubject.
|
||||
message: string; // Message.
|
||||
messageformat: number; // Message format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
|
||||
author: {
|
||||
id?: number; // Id.
|
||||
fullname?: string; // Fullname.
|
||||
isdeleted?: boolean; // Isdeleted.
|
||||
groups?: { // Groups.
|
||||
id: number; // Id.
|
||||
name: string; // Name.
|
||||
urls: {
|
||||
image?: string; // Image.
|
||||
};
|
||||
}[];
|
||||
urls: {
|
||||
profile?: string; // The URL for the use profile page.
|
||||
profileimage?: string; // The URL for the use profile image.
|
||||
};
|
||||
};
|
||||
discussionid: number; // Discussionid.
|
||||
hasparent: boolean; // Hasparent.
|
||||
parentid?: number; // Parentid.
|
||||
timecreated: number; // Timecreated.
|
||||
unread?: boolean; // Unread.
|
||||
isdeleted: boolean; // Isdeleted.
|
||||
isprivatereply: boolean; // Isprivatereply.
|
||||
haswordcount: boolean; // Haswordcount.
|
||||
wordcount?: number; // Wordcount.
|
||||
charcount?: number; // Charcount.
|
||||
capabilities: {
|
||||
view: boolean; // Whether the user can view the post.
|
||||
edit: boolean; // Whether the user can edit the post.
|
||||
delete: boolean; // Whether the user can delete the post.
|
||||
split: boolean; // Whether the user can split the post.
|
||||
reply: boolean; // Whether the user can reply to the post.
|
||||
selfenrol: boolean; // Whether the user can self enrol into the course.
|
||||
export: boolean; // Whether the user can export the post.
|
||||
controlreadstatus: boolean; // Whether the user can control the read status of the post.
|
||||
canreplyprivately: boolean; // Whether the user can post a private reply.
|
||||
};
|
||||
urls?: {
|
||||
view?: string; // The URL used to view the post.
|
||||
viewisolated?: string; // The URL used to view the post in isolation.
|
||||
viewparent?: string; // The URL used to view the parent of the post.
|
||||
edit?: string; // The URL used to edit the post.
|
||||
delete?: string; // The URL used to delete the post.
|
||||
|
||||
// The URL used to split the discussion with the selected post being the first post in the new discussion.
|
||||
split?: string;
|
||||
|
||||
reply?: string; // The URL used to reply to the post.
|
||||
export?: string; // The URL used to export the post.
|
||||
markasread?: string; // The URL used to mark the post as read.
|
||||
markasunread?: string; // The URL used to mark the post as unread.
|
||||
discuss?: string; // Discuss.
|
||||
};
|
||||
attachments: AddonModForumWSPostAttachment[]; // Attachments.
|
||||
tags?: { // Tags.
|
||||
id: number; // The ID of the Tag.
|
||||
tagid: number; // The tagid.
|
||||
isstandard: boolean; // Whether this is a standard tag.
|
||||
displayname: string; // The display name of the tag.
|
||||
flag: boolean; // Wehther this tag is flagged.
|
||||
urls: {
|
||||
view: string; // The URL to view the tag.
|
||||
};
|
||||
}[];
|
||||
html?: {
|
||||
rating?: string; // The HTML source to rate the post.
|
||||
taglist?: string; // The HTML source to view the list of tags.
|
||||
authorsubheading?: string; // The HTML source to view the author details.
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Params of mod_forum_get_forum_discussions WS.
|
||||
*/
|
||||
|
@ -1799,7 +1877,7 @@ export type AddonModForumAddDiscussionPostWSParams = {
|
|||
export type AddonModForumAddDiscussionPostWSResponse = {
|
||||
postid: number; // New post id.
|
||||
warnings?: CoreWSExternalWarning[];
|
||||
post: AddonModForumPost;
|
||||
post: AddonModForumWSPost;
|
||||
messages?: { // List of warnings.
|
||||
type: string; // The classification to be used in the client side.
|
||||
message: string; // Untranslated english message to explain the warning.
|
||||
|
@ -1859,7 +1937,7 @@ export type AddonModForumGetDiscussionPostWSParams = {
|
|||
* Data returned by mod_forum_get_discussion_post WS.
|
||||
*/
|
||||
export type AddonModForumGetDiscussionPostWSResponse = {
|
||||
post: AddonModForumPost;
|
||||
post: AddonModForumWSPost;
|
||||
warnings?: CoreWSExternalWarning[];
|
||||
};
|
||||
|
||||
|
@ -1877,7 +1955,7 @@ export type AddonModForumGetDiscussionPostsWSParams = {
|
|||
* Data returned by mod_forum_get_discussion_posts WS.
|
||||
*/
|
||||
export type AddonModForumGetDiscussionPostsWSResponse = {
|
||||
posts: AddonModForumPost[];
|
||||
posts: AddonModForumWSPost[];
|
||||
forumid: number; // The forum id.
|
||||
courseid: number; // The forum course id.
|
||||
ratinginfo?: AddonModForumRatingInfo; // Rating information.
|
||||
|
|
|
@ -21,8 +21,15 @@ import { CoreSites } from '@services/sites';
|
|||
import { CoreTimeUtils } from '@services/utils/time';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { makeSingleton, Translate } from '@singletons';
|
||||
import { AddonModForum, AddonModForumData, AddonModForumProvider } from './forum.service';
|
||||
import { AddonModForumOffline } from './offline.service';
|
||||
import {
|
||||
AddonModForum,
|
||||
AddonModForumAddDiscussionWSOptionsObject,
|
||||
AddonModForumData,
|
||||
AddonModForumDiscussion,
|
||||
AddonModForumPost,
|
||||
AddonModForumProvider,
|
||||
} from './forum.service';
|
||||
import { AddonModForumDiscussionOptions, AddonModForumOffline, AddonModForumOfflineReply } from './offline.service';
|
||||
|
||||
/**
|
||||
* Service that provides some features for forums.
|
||||
|
@ -51,8 +58,8 @@ export class AddonModForumHelperProvider {
|
|||
courseId: number,
|
||||
subject: string,
|
||||
message: string,
|
||||
attachments?: any[],
|
||||
options?: any,
|
||||
attachments?: CoreFileEntry[],
|
||||
options?: AddonModForumDiscussionOptions,
|
||||
groupIds?: number[],
|
||||
timeCreated?: number,
|
||||
siteId?: string,
|
||||
|
@ -62,14 +69,14 @@ export class AddonModForumHelperProvider {
|
|||
|
||||
let saveOffline = false;
|
||||
const attachmentsIds: number[] = [];
|
||||
let offlineAttachments: any;
|
||||
let offlineAttachments: CoreFileUploaderStoreFilesResult;
|
||||
|
||||
// Convenience function to store a message to be synchronized later.
|
||||
const storeOffline = async (): Promise<void> => {
|
||||
// Multiple groups, the discussion is being posted to all groups.
|
||||
const groupId = groupIds!.length > 1 ? AddonModForumProvider.ALL_GROUPS : groupIds![0];
|
||||
|
||||
if (offlineAttachments) {
|
||||
if (offlineAttachments && options) {
|
||||
options.attachmentsid = offlineAttachments;
|
||||
}
|
||||
|
||||
|
@ -122,7 +129,7 @@ export class AddonModForumHelperProvider {
|
|||
const promises = groupIds.map(async (groupId, index) => {
|
||||
const groupOptions = CoreUtils.instance.clone(options);
|
||||
|
||||
if (attachmentsIds[index]) {
|
||||
if (groupOptions && attachmentsIds[index]) {
|
||||
groupOptions.attachmentsid = attachmentsIds[index];
|
||||
}
|
||||
|
||||
|
@ -131,7 +138,7 @@ export class AddonModForumHelperProvider {
|
|||
forumId,
|
||||
subject,
|
||||
message,
|
||||
groupOptions,
|
||||
groupOptions as unknown as AddonModForumAddDiscussionWSOptionsObject,
|
||||
groupId,
|
||||
siteId,
|
||||
);
|
||||
|
@ -169,8 +176,8 @@ export class AddonModForumHelperProvider {
|
|||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with the object converted to Online.
|
||||
*/
|
||||
convertOfflineReplyToOnline(offlineReply: any, siteId?: string): Promise<any> {
|
||||
const reply: any = {
|
||||
convertOfflineReplyToOnline(offlineReply: AddonModForumOfflineReply, siteId?: string): Promise<AddonModForumPost> {
|
||||
const reply: AddonModForumPost = {
|
||||
id: -offlineReply.timecreated,
|
||||
discussionid: offlineReply.discussionid,
|
||||
parentid: offlineReply.postid,
|
||||
|
@ -186,20 +193,25 @@ export class AddonModForumHelperProvider {
|
|||
reply: false,
|
||||
},
|
||||
unread: false,
|
||||
isprivatereply: offlineReply.options && offlineReply.options.private,
|
||||
tags: null,
|
||||
isprivatereply: !!offlineReply.options?.private,
|
||||
};
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
// Treat attachments if any.
|
||||
if (offlineReply.options && offlineReply.options.attachmentsid) {
|
||||
reply.attachments = offlineReply.options.attachmentsid.online || [];
|
||||
const attachments = offlineReply.options.attachmentsid;
|
||||
|
||||
if (offlineReply.options.attachmentsid.offline) {
|
||||
reply.attachments = typeof attachments === 'object' && 'online' in attachments ? attachments.online : [];
|
||||
|
||||
if (typeof attachments === 'object' && attachments.offline) {
|
||||
promises.push(
|
||||
this
|
||||
.getReplyStoredFiles(offlineReply.forumid, reply.parentid, siteId, offlineReply.userid)
|
||||
.then(files => reply.attachments = reply.attachments.concat(files)),
|
||||
.getReplyStoredFiles(offlineReply.forumid, reply.parentid!, siteId, offlineReply.userid)
|
||||
.then(files => {
|
||||
reply.attachments = reply.attachments!.concat(files as unknown as []);
|
||||
|
||||
return;
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -219,7 +231,7 @@ export class AddonModForumHelperProvider {
|
|||
);
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
reply.attachment = reply.attachments.length > 0 ? 1 : 0;
|
||||
reply.attachment = reply.attachments!.length > 0 ? 1 : 0;
|
||||
|
||||
return reply;
|
||||
});
|
||||
|
@ -293,10 +305,10 @@ export class AddonModForumHelperProvider {
|
|||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with the discussion data.
|
||||
*/
|
||||
getDiscussionById(forumId: number, cmId: number, discussionId: number, siteId?: string): Promise<any> {
|
||||
getDiscussionById(forumId: number, cmId: number, discussionId: number, siteId?: string): Promise<AddonModForumDiscussion> {
|
||||
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||||
|
||||
const findDiscussion = async (page: number): Promise<any> => {
|
||||
const findDiscussion = async (page: number): Promise<AddonModForumDiscussion> => {
|
||||
const response = await AddonModForum.instance.getDiscussions(forumId, {
|
||||
cmId,
|
||||
page,
|
||||
|
@ -330,7 +342,7 @@ export class AddonModForumHelperProvider {
|
|||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with the files.
|
||||
*/
|
||||
async getNewDiscussionStoredFiles(forumId: number, timecreated: number, siteId?: string): Promise<any[]> {
|
||||
async getNewDiscussionStoredFiles(forumId: number, timecreated: number, siteId?: string): Promise<FileEntry[]> {
|
||||
const folderPath = await AddonModForumOffline.instance.getNewDiscussionFolder(forumId, timecreated, siteId);
|
||||
|
||||
return CoreFileUploader.instance.getStoredFiles(folderPath);
|
||||
|
@ -345,7 +357,7 @@ export class AddonModForumHelperProvider {
|
|||
* @param userId User the reply belongs to. If not defined, current user in site.
|
||||
* @return Promise resolved with the files.
|
||||
*/
|
||||
async getReplyStoredFiles(forumId: number, postId: number, siteId?: string, userId?: number): Promise<any[]> {
|
||||
async getReplyStoredFiles(forumId: number, postId: number, siteId?: string, userId?: number): Promise<FileEntry[]> {
|
||||
const folderPath = await AddonModForumOffline.instance.getReplyFolder(forumId, postId, siteId, userId);
|
||||
|
||||
return CoreFileUploader.instance.getStoredFiles(folderPath);
|
||||
|
@ -380,10 +392,10 @@ export class AddonModForumHelperProvider {
|
|||
*
|
||||
* @param forum Forum instance.
|
||||
*/
|
||||
isCutoffDateReached(forum: any): boolean {
|
||||
isCutoffDateReached(forum: AddonModForumData): boolean {
|
||||
const now = Date.now() / 1000;
|
||||
|
||||
return forum.cutoffdate > 0 && forum.cutoffdate < now;
|
||||
return !!forum.cutoffdate && forum.cutoffdate > 0 && forum.cutoffdate < now;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
|
||||
import { CoreFile } from '@services/file';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
|
@ -401,8 +402,14 @@ export class AddonModForumOfflineProvider {
|
|||
|
||||
export class AddonModForumOffline extends makeSingleton(AddonModForumOfflineProvider) {}
|
||||
|
||||
export type AddonModForumDiscussionOptions = Record<string, unknown>;
|
||||
export type AddonModForumReplyOptions = Record<string, unknown>;
|
||||
export type AddonModForumDiscussionOptions = {
|
||||
attachmentsid: number | CoreFileUploaderStoreFilesResult;
|
||||
};
|
||||
|
||||
export type AddonModForumReplyOptions = {
|
||||
private?: boolean;
|
||||
attachmentsid?: number | CoreFileUploaderStoreFilesResult;
|
||||
};
|
||||
|
||||
export type AddonModForumOfflineDiscussion = {
|
||||
forumid: number;
|
||||
|
@ -412,6 +419,7 @@ export type AddonModForumOfflineDiscussion = {
|
|||
message: string;
|
||||
options: AddonModForumDiscussionOptions;
|
||||
groupid: number;
|
||||
groupname?: string;
|
||||
userid: number;
|
||||
timecreated: number;
|
||||
};
|
||||
|
|
|
@ -23,10 +23,15 @@ import { CoreSites } from '@services/sites';
|
|||
import { CoreSync } from '@services/sync';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { Translate } from '@singletons';
|
||||
import { makeSingleton, Translate } from '@singletons';
|
||||
import { CoreArray } from '@singletons/array';
|
||||
import { CoreEvents } from '@singletons/events';
|
||||
import { AddonModForum, AddonModForumProvider } from './forum.service';
|
||||
import {
|
||||
AddonModForum,
|
||||
AddonModForumAddDiscussionPostWSOptionsObject,
|
||||
AddonModForumAddDiscussionWSOptionsObject,
|
||||
AddonModForumProvider,
|
||||
} from './forum.service';
|
||||
import { AddonModForumHelper } from './helper.service';
|
||||
import { AddonModForumOffline, AddonModForumOfflineDiscussion, AddonModForumOfflineReply } from './offline.service';
|
||||
|
||||
|
@ -72,7 +77,7 @@ export class AddonModForumSyncProvider extends CoreSyncBaseProvider<AddonModForu
|
|||
* @return Promise resolved if sync is successful, rejected if sync fails.
|
||||
*/
|
||||
protected async syncAllForumsFunc(force: boolean, siteId: string): Promise<void> {
|
||||
const sitePromises: Promise<void>[] = [];
|
||||
const sitePromises: Promise<unknown>[] = [];
|
||||
|
||||
// Sync all new discussions.
|
||||
const syncDiscussions = async (discussions: AddonModForumOfflineDiscussion[]) => {
|
||||
|
@ -239,13 +244,13 @@ export class AddonModForumSyncProvider extends CoreSyncBaseProvider<AddonModForu
|
|||
|
||||
// Now try to add the discussion.
|
||||
const options = CoreUtils.instance.clone(discussion.options || {});
|
||||
options.attachmentsid = itemId;
|
||||
options.attachmentsid = itemId!;
|
||||
|
||||
await AddonModForum.instance.addNewDiscussionOnline(
|
||||
forumId,
|
||||
discussion.subject,
|
||||
discussion.message,
|
||||
options,
|
||||
options as unknown as AddonModForumAddDiscussionWSOptionsObject,
|
||||
groupId,
|
||||
siteId,
|
||||
);
|
||||
|
@ -309,8 +314,13 @@ export class AddonModForumSyncProvider extends CoreSyncBaseProvider<AddonModForu
|
|||
* @return Promise resolved if sync is successful, rejected otherwise.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async syncRatings(cmId?: number, discussionId?: number, force?: boolean, siteId?: string): Promise<void> {
|
||||
async syncRatings(cmId?: number, discussionId?: number, force?: boolean, siteId?: string): Promise<{
|
||||
updated: boolean;
|
||||
warnings: string[];
|
||||
}> {
|
||||
// @todo
|
||||
|
||||
return { updated: true, warnings: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -437,7 +447,7 @@ export class AddonModForumSyncProvider extends CoreSyncBaseProvider<AddonModForu
|
|||
reply.postid,
|
||||
reply.subject,
|
||||
reply.message,
|
||||
reply.options,
|
||||
reply.options as unknown as AddonModForumAddDiscussionPostWSOptionsObject,
|
||||
siteId,
|
||||
);
|
||||
});
|
||||
|
@ -526,18 +536,18 @@ export class AddonModForumSyncProvider extends CoreSyncBaseProvider<AddonModForu
|
|||
*
|
||||
* @param forumId Forum ID the post belongs to.
|
||||
* @param post Offline post or discussion.
|
||||
* @param isDisc True if it's a new discussion, false if it's a reply.
|
||||
* @param isDiscussion True if it's a new discussion, false if it's a reply.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param userId User the reply belongs to. If not defined, current user in site.
|
||||
* @return Promise resolved with draftid if uploaded, resolved with undefined if nothing to upload.
|
||||
*/
|
||||
protected async uploadAttachments(
|
||||
forumId: number,
|
||||
post: any,
|
||||
isDisc: boolean,
|
||||
post: AddonModForumOfflineDiscussion | AddonModForumOfflineReply,
|
||||
isDiscussion: boolean,
|
||||
siteId?: string,
|
||||
userId?: number,
|
||||
): Promise<void> {
|
||||
): Promise<number | undefined> {
|
||||
const attachments = post && post.options && post.options.attachmentsid;
|
||||
|
||||
if (!attachments) {
|
||||
|
@ -545,22 +555,31 @@ export class AddonModForumSyncProvider extends CoreSyncBaseProvider<AddonModForu
|
|||
}
|
||||
|
||||
// Has some attachments to sync.
|
||||
let files = attachments.online || [];
|
||||
let files = typeof attachments === 'object' && attachments.online ? attachments.online : [];
|
||||
|
||||
if (attachments.offline) {
|
||||
if (typeof attachments === 'object' && attachments.offline) {
|
||||
// Has offline files.
|
||||
try {
|
||||
const atts = isDisc
|
||||
? await AddonModForumHelper.instance.getNewDiscussionStoredFiles(forumId, post.timecreated, siteId)
|
||||
: await AddonModForumHelper.instance.getReplyStoredFiles(forumId, post.postid, siteId, userId);
|
||||
const postAttachments = isDiscussion
|
||||
? await AddonModForumHelper.instance.getNewDiscussionStoredFiles(
|
||||
forumId,
|
||||
(post as AddonModForumOfflineDiscussion).timecreated,
|
||||
siteId,
|
||||
)
|
||||
: await AddonModForumHelper.instance.getReplyStoredFiles(
|
||||
forumId,
|
||||
(post as AddonModForumOfflineReply).postid,
|
||||
siteId,
|
||||
userId,
|
||||
);
|
||||
|
||||
files = files.concat(atts);
|
||||
files = files.concat(postAttachments as unknown as []);
|
||||
} catch (error) {
|
||||
// Folder not found, no files to add.
|
||||
}
|
||||
}
|
||||
|
||||
await CoreFileUploader.instance.uploadOrReuploadFiles(files, AddonModForumProvider.COMPONENT, forumId, siteId);
|
||||
return CoreFileUploader.instance.uploadOrReuploadFiles(files, AddonModForumProvider.COMPONENT, forumId, siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -591,6 +610,8 @@ export class AddonModForumSyncProvider extends CoreSyncBaseProvider<AddonModForu
|
|||
|
||||
}
|
||||
|
||||
export class AddonModForumSync extends makeSingleton(AddonModForumSyncProvider) {}
|
||||
|
||||
/**
|
||||
* Result of forum sync.
|
||||
*/
|
||||
|
|
|
@ -93,4 +93,7 @@
|
|||
--core-question-feedback-background-color: var(--yellow-dark);
|
||||
|
||||
--core-dd-question-selected-shadow: 2px 2px 4px var(--gray-light);
|
||||
|
||||
--addon-forum-border-color: var(--custom-forum-border-color, var(--gray-darker));
|
||||
--addon-forum-highlight-color: var(--custom-forum-highlight-color, var(--gray-dark));
|
||||
}
|
||||
|
|
|
@ -184,6 +184,8 @@
|
|||
--addon-messages-discussion-badge-text: var(--custom-messages-discussion-badge-text, var(--white));
|
||||
|
||||
--addon-forum-avatar-size: var(--custom-forum-avatar-size, 28px);
|
||||
--addon-forum-border-color: var(--custom-forum-border-color, var(--gray));
|
||||
--addon-forum-highlight-color: var(--custom-forum-highlight-color, var(--gray-lighter));
|
||||
|
||||
--drop-shadow: var(--custom-drop-shadow, 0, 0, 0, 0.2);
|
||||
|
||||
|
|
Loading…
Reference in New Issue