MOBILE-3643 forum: Migrate discussion page
parent
b318b0e4a5
commit
2dd0aa4815
|
@ -14,23 +14,28 @@
|
||||||
|
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
|
|
||||||
import { CoreSharedModule } from '@/core/shared.module';
|
|
||||||
import { CoreCourseComponentsModule } from '@features/course/components/components.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 { CoreTagComponentsModule } from '@features/tag/components/components.module';
|
||||||
|
|
||||||
import { AddonModForumIndexComponent } from './index/index';
|
import { AddonModForumIndexComponent } from './index/index';
|
||||||
|
import { AddonModForumPostComponent } from './post/post';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
AddonModForumIndexComponent,
|
AddonModForumIndexComponent,
|
||||||
|
AddonModForumPostComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CoreSharedModule,
|
CoreSharedModule,
|
||||||
CoreCourseComponentsModule,
|
CoreCourseComponentsModule,
|
||||||
CoreTagComponentsModule,
|
CoreTagComponentsModule,
|
||||||
|
CoreEditorComponentsModule,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
AddonModForumIndexComponent,
|
AddonModForumIndexComponent,
|
||||||
|
AddonModForumPostComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AddonModForumComponentsModule {}
|
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 { AddonModForumComponentsModule } from './components/components.module';
|
||||||
import { AddonModForumIndexPage } from './pages/index';
|
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',
|
path: ':courseId/:cmId',
|
||||||
component: AddonModForumIndexPage,
|
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({
|
@NgModule({
|
||||||
|
@ -35,6 +60,7 @@ const routes: Routes = [
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
AddonModForumIndexPage,
|
AddonModForumIndexPage,
|
||||||
|
AddonModForumDiscussionPage,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AddonModForumLazyModule {}
|
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 { CoreSite, CoreSiteWSPreSets } from '@classes/site';
|
||||||
import { CoreCourseCommonModWSOptions } from '@features/course/services/course';
|
import { CoreCourseCommonModWSOptions } from '@features/course/services/course';
|
||||||
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
|
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
|
||||||
|
import { CoreFileEntry } from '@features/fileuploader/services/fileuploader';
|
||||||
import { CoreUser } from '@features/user/services/user';
|
import { CoreUser } from '@features/user/services/user';
|
||||||
import { CoreApp } from '@services/app';
|
import { CoreApp } from '@services/app';
|
||||||
import { CoreFilepool } from '@services/filepool';
|
import { CoreFilepool } from '@services/filepool';
|
||||||
|
@ -25,7 +26,7 @@ import { CoreUrlUtils } from '@services/utils/url';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
import { CoreStatusWithWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws';
|
import { CoreStatusWithWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws';
|
||||||
import { makeSingleton, Translate } from '@singletons';
|
import { makeSingleton, Translate } from '@singletons';
|
||||||
import { AddonModForumOffline, AddonModForumReplyOptions } from './offline.service';
|
import { AddonModForumOffline, AddonModForumOfflineDiscussion, AddonModForumReplyOptions } from './offline.service';
|
||||||
|
|
||||||
const ROOT_CACHE_KEY = 'mmaModForum:';
|
const ROOT_CACHE_KEY = 'mmaModForum:';
|
||||||
|
|
||||||
|
@ -275,13 +276,13 @@ export class AddonModForumProvider {
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
* @since 3.8
|
* @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 site = await CoreSites.instance.getSite(siteId);
|
||||||
const params: AddonModForumDeletePostWSParams = {
|
const params: AddonModForumDeletePostWSParams = {
|
||||||
postid: postId,
|
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.
|
* @param discussions List of discussions to format.
|
||||||
* @return Promise resolved with the formatted discussions.
|
* @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);
|
discussions = CoreUtils.instance.clone(discussions);
|
||||||
|
|
||||||
return CoreGroups.instance.getActivityAllowedGroups(cmId).then((result) => {
|
return CoreGroups.instance.getActivityAllowedGroups(cmId).then((result) => {
|
||||||
|
@ -447,7 +453,7 @@ export class AddonModForumProvider {
|
||||||
throw new Error('Post not found');
|
throw new Error('Post not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.post;
|
return this.translateWSPost(response.post);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -539,9 +545,9 @@ export class AddonModForumProvider {
|
||||||
ratinginfo?: AddonModForumRatingInfo;
|
ratinginfo?: AddonModForumRatingInfo;
|
||||||
}> {
|
}> {
|
||||||
// Convenience function to translate legacy data to new format.
|
// Convenience function to translate legacy data to new format.
|
||||||
const translateLegacyPostsFormat = (posts: any[]): any[] => posts.map((post) => {
|
const translateLegacyPostsFormat = (posts: AddonModForumLegacyPost[]): AddonModForumPost[] => posts.map((post) => {
|
||||||
const newPost = {
|
const newPost: AddonModForumPost = {
|
||||||
id: post.id ,
|
id: post.id,
|
||||||
discussionid: post.discussion,
|
discussionid: post.discussion,
|
||||||
parentid: post.parent,
|
parentid: post.parent,
|
||||||
hasparent: !!post.parent,
|
hasparent: !!post.parent,
|
||||||
|
@ -563,8 +569,8 @@ export class AddonModForumProvider {
|
||||||
tags: post.tags,
|
tags: post.tags,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (post.groupname) {
|
if ('groupname' in post && typeof post['groupname'] === 'string') {
|
||||||
newPost.author['groups'] = [{ name: post.groupname }];
|
newPost.author['groups'] = [{ name: post['groupname'] }];
|
||||||
}
|
}
|
||||||
|
|
||||||
return newPost;
|
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.
|
// 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.
|
// Convert the new format to the exporter one so it's the same as in other WebServices.
|
||||||
const translateTagsFormatToLegacy = (posts: any[]): any[] => {
|
const translateTagsFormatToLegacy = (posts: AddonModForumWSPost[]): AddonModForumPost[] => {
|
||||||
posts.forEach((post) => {
|
posts.forEach(post => this.translateWSPost(post));
|
||||||
post.tags = post.tags.map((tag) => {
|
|
||||||
const viewUrl = (tag.urls && tag.urls.view) || '';
|
|
||||||
const params = CoreUrlUtils.instance.extractUrlParams(viewUrl);
|
|
||||||
|
|
||||||
return {
|
return posts as unknown as AddonModForumPost[];
|
||||||
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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const params: AddonModForumGetDiscussionPostsWSParams | AddonModForumGetForumDiscussionPostsWSParams = {
|
const params: AddonModForumGetDiscussionPostsWSParams | AddonModForumGetForumDiscussionPostsWSParams = {
|
||||||
|
@ -619,15 +609,16 @@ export class AddonModForumProvider {
|
||||||
throw new Error('Could not get forum posts');
|
throw new Error('Could not get forum posts');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isGetDiscussionPostsAvailable) {
|
const posts = isGetDiscussionPostsAvailable
|
||||||
response.posts = translateTagsFormatToLegacy((response as AddonModForumGetDiscussionPostsWSResponse).posts);
|
? translateTagsFormatToLegacy((response as AddonModForumGetDiscussionPostsWSResponse).posts)
|
||||||
} else {
|
: translateLegacyPostsFormat((response as AddonModForumGetForumDiscussionPostsWSResponse).posts);
|
||||||
response.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');
|
throw new Error('Could not get discussions');
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.storeUserData(response.discussions);
|
this.storeUserData(response.discussions);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
discussions: response.discussions,
|
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.
|
// If there's already a reply to be sent to the server, discard it first.
|
||||||
try {
|
try {
|
||||||
await AddonModForumOffline.instance.deleteReply(postId, siteId);
|
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;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -1157,7 +1154,12 @@ export class AddonModForumProvider {
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
* @since 3.7
|
* @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 site = await CoreSites.instance.getSite(siteId);
|
||||||
const params: AddonModForumSetLockStateWSParams = {
|
const params: AddonModForumSetLockStateWSParams = {
|
||||||
forumid: forumId,
|
forumid: forumId,
|
||||||
|
@ -1165,7 +1167,7 @@ export class AddonModForumProvider {
|
||||||
targetstate: locked ? 0 : 1,
|
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 site = await CoreSites.instance.getSite(siteId);
|
||||||
const params: AddonModForumToggleFavouriteStateWSParams = {
|
const params: AddonModForumToggleFavouriteStateWSParams = {
|
||||||
discussionid: discussionId,
|
discussionid: discussionId,
|
||||||
targetstate: starred ? 1 : 0 as any,
|
targetstate: starred,
|
||||||
};
|
};
|
||||||
|
|
||||||
await site.write<AddonModForumToggleFavouriteStateWSResponse>('mod_forum_toggle_favourite_state', params);
|
await site.write<AddonModForumToggleFavouriteStateWSResponse>('mod_forum_toggle_favourite_state', params);
|
||||||
|
@ -1222,30 +1224,30 @@ export class AddonModForumProvider {
|
||||||
*
|
*
|
||||||
* @param list Array of posts or discussions.
|
* @param list Array of posts or discussions.
|
||||||
*/
|
*/
|
||||||
protected storeUserData(list: any[]): void {
|
protected storeUserData(list: AddonModForumPost[] | AddonModForumDiscussion[]): void {
|
||||||
const users = {};
|
const users = {};
|
||||||
|
|
||||||
list.forEach((entry) => {
|
list.forEach((entry: AddonModForumPost | AddonModForumDiscussion) => {
|
||||||
if (entry.author) {
|
if ('author' in entry) {
|
||||||
const authorId = Number(entry.author.id);
|
const authorId = Number(entry.author.id);
|
||||||
if (!isNaN(authorId) && !users[authorId]) {
|
if (!isNaN(authorId) && !users[authorId]) {
|
||||||
users[authorId] = {
|
users[authorId] = {
|
||||||
id: entry.author.id,
|
id: entry.author.id,
|
||||||
fullname: entry.author.fullname,
|
fullname: entry.author.fullname,
|
||||||
profileimageurl: entry.author.urls.profileimage,
|
profileimageurl: entry.author.urls?.profileimage,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const userId = parseInt(entry.userid);
|
const userId = parseInt(entry['userid']);
|
||||||
if (!isNaN(userId) && !users[userId]) {
|
if ('userid' in entry && !isNaN(userId) && !users[userId]) {
|
||||||
users[userId] = {
|
users[userId] = {
|
||||||
id: userId,
|
id: userId,
|
||||||
fullname: entry.userfullname,
|
fullname: entry.userfullname,
|
||||||
profileimageurl: entry.userpictureurl,
|
profileimageurl: entry.userpictureurl,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const userModified = parseInt(entry.usermodified);
|
const userModified = parseInt(entry['usermodified']);
|
||||||
if (!isNaN(userModified) && !users[userModified]) {
|
if ('usermodified' in entry && !isNaN(userModified) && !users[userModified]) {
|
||||||
users[userModified] = {
|
users[userModified] = {
|
||||||
id: userModified,
|
id: userModified,
|
||||||
fullname: entry.usermodifiedfullname,
|
fullname: entry.usermodifiedfullname,
|
||||||
|
@ -1293,6 +1295,33 @@ export class AddonModForumProvider {
|
||||||
return response && response.status;
|
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) {}
|
export class AddonModForum extends makeSingleton(AddonModForumProvider) {}
|
||||||
|
@ -1353,6 +1382,7 @@ export type AddonModForumDiscussion = {
|
||||||
id: number; // Post id.
|
id: number; // Post id.
|
||||||
name: string; // Discussion name.
|
name: string; // Discussion name.
|
||||||
groupid: number; // Group id.
|
groupid: number; // Group id.
|
||||||
|
groupname?: string; // Group name (not returned by WS).
|
||||||
timemodified: number; // Time modified.
|
timemodified: number; // Time modified.
|
||||||
usermodified: number; // The id of the user who last modified.
|
usermodified: number; // The id of the user who last modified.
|
||||||
timestart: number; // Time discussion can start.
|
timestart: number; // Time discussion can start.
|
||||||
|
@ -1386,6 +1416,49 @@ export type AddonModForumDiscussion = {
|
||||||
canfavourite?: boolean; // Can the user star the discussion.
|
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.
|
* 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.
|
* Forum rating info.
|
||||||
*/
|
*/
|
||||||
|
@ -1640,6 +1607,117 @@ export type AddonModForumSortOrder = {
|
||||||
value: number;
|
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.
|
* Params of mod_forum_get_forum_discussions WS.
|
||||||
*/
|
*/
|
||||||
|
@ -1799,7 +1877,7 @@ export type AddonModForumAddDiscussionPostWSParams = {
|
||||||
export type AddonModForumAddDiscussionPostWSResponse = {
|
export type AddonModForumAddDiscussionPostWSResponse = {
|
||||||
postid: number; // New post id.
|
postid: number; // New post id.
|
||||||
warnings?: CoreWSExternalWarning[];
|
warnings?: CoreWSExternalWarning[];
|
||||||
post: AddonModForumPost;
|
post: AddonModForumWSPost;
|
||||||
messages?: { // List of warnings.
|
messages?: { // List of warnings.
|
||||||
type: string; // The classification to be used in the client side.
|
type: string; // The classification to be used in the client side.
|
||||||
message: string; // Untranslated english message to explain the warning.
|
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.
|
* Data returned by mod_forum_get_discussion_post WS.
|
||||||
*/
|
*/
|
||||||
export type AddonModForumGetDiscussionPostWSResponse = {
|
export type AddonModForumGetDiscussionPostWSResponse = {
|
||||||
post: AddonModForumPost;
|
post: AddonModForumWSPost;
|
||||||
warnings?: CoreWSExternalWarning[];
|
warnings?: CoreWSExternalWarning[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1877,7 +1955,7 @@ export type AddonModForumGetDiscussionPostsWSParams = {
|
||||||
* Data returned by mod_forum_get_discussion_posts WS.
|
* Data returned by mod_forum_get_discussion_posts WS.
|
||||||
*/
|
*/
|
||||||
export type AddonModForumGetDiscussionPostsWSResponse = {
|
export type AddonModForumGetDiscussionPostsWSResponse = {
|
||||||
posts: AddonModForumPost[];
|
posts: AddonModForumWSPost[];
|
||||||
forumid: number; // The forum id.
|
forumid: number; // The forum id.
|
||||||
courseid: number; // The forum course id.
|
courseid: number; // The forum course id.
|
||||||
ratinginfo?: AddonModForumRatingInfo; // Rating information.
|
ratinginfo?: AddonModForumRatingInfo; // Rating information.
|
||||||
|
|
|
@ -21,8 +21,15 @@ import { CoreSites } from '@services/sites';
|
||||||
import { CoreTimeUtils } from '@services/utils/time';
|
import { CoreTimeUtils } from '@services/utils/time';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
import { makeSingleton, Translate } from '@singletons';
|
import { makeSingleton, Translate } from '@singletons';
|
||||||
import { AddonModForum, AddonModForumData, AddonModForumProvider } from './forum.service';
|
import {
|
||||||
import { AddonModForumOffline } from './offline.service';
|
AddonModForum,
|
||||||
|
AddonModForumAddDiscussionWSOptionsObject,
|
||||||
|
AddonModForumData,
|
||||||
|
AddonModForumDiscussion,
|
||||||
|
AddonModForumPost,
|
||||||
|
AddonModForumProvider,
|
||||||
|
} from './forum.service';
|
||||||
|
import { AddonModForumDiscussionOptions, AddonModForumOffline, AddonModForumOfflineReply } from './offline.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service that provides some features for forums.
|
* Service that provides some features for forums.
|
||||||
|
@ -51,8 +58,8 @@ export class AddonModForumHelperProvider {
|
||||||
courseId: number,
|
courseId: number,
|
||||||
subject: string,
|
subject: string,
|
||||||
message: string,
|
message: string,
|
||||||
attachments?: any[],
|
attachments?: CoreFileEntry[],
|
||||||
options?: any,
|
options?: AddonModForumDiscussionOptions,
|
||||||
groupIds?: number[],
|
groupIds?: number[],
|
||||||
timeCreated?: number,
|
timeCreated?: number,
|
||||||
siteId?: string,
|
siteId?: string,
|
||||||
|
@ -62,14 +69,14 @@ export class AddonModForumHelperProvider {
|
||||||
|
|
||||||
let saveOffline = false;
|
let saveOffline = false;
|
||||||
const attachmentsIds: number[] = [];
|
const attachmentsIds: number[] = [];
|
||||||
let offlineAttachments: any;
|
let offlineAttachments: CoreFileUploaderStoreFilesResult;
|
||||||
|
|
||||||
// Convenience function to store a message to be synchronized later.
|
// Convenience function to store a message to be synchronized later.
|
||||||
const storeOffline = async (): Promise<void> => {
|
const storeOffline = async (): Promise<void> => {
|
||||||
// Multiple groups, the discussion is being posted to all groups.
|
// Multiple groups, the discussion is being posted to all groups.
|
||||||
const groupId = groupIds!.length > 1 ? AddonModForumProvider.ALL_GROUPS : groupIds![0];
|
const groupId = groupIds!.length > 1 ? AddonModForumProvider.ALL_GROUPS : groupIds![0];
|
||||||
|
|
||||||
if (offlineAttachments) {
|
if (offlineAttachments && options) {
|
||||||
options.attachmentsid = offlineAttachments;
|
options.attachmentsid = offlineAttachments;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,7 +129,7 @@ export class AddonModForumHelperProvider {
|
||||||
const promises = groupIds.map(async (groupId, index) => {
|
const promises = groupIds.map(async (groupId, index) => {
|
||||||
const groupOptions = CoreUtils.instance.clone(options);
|
const groupOptions = CoreUtils.instance.clone(options);
|
||||||
|
|
||||||
if (attachmentsIds[index]) {
|
if (groupOptions && attachmentsIds[index]) {
|
||||||
groupOptions.attachmentsid = attachmentsIds[index];
|
groupOptions.attachmentsid = attachmentsIds[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,7 +138,7 @@ export class AddonModForumHelperProvider {
|
||||||
forumId,
|
forumId,
|
||||||
subject,
|
subject,
|
||||||
message,
|
message,
|
||||||
groupOptions,
|
groupOptions as unknown as AddonModForumAddDiscussionWSOptionsObject,
|
||||||
groupId,
|
groupId,
|
||||||
siteId,
|
siteId,
|
||||||
);
|
);
|
||||||
|
@ -169,8 +176,8 @@ export class AddonModForumHelperProvider {
|
||||||
* @param siteId Site ID. If not defined, current site.
|
* @param siteId Site ID. If not defined, current site.
|
||||||
* @return Promise resolved with the object converted to Online.
|
* @return Promise resolved with the object converted to Online.
|
||||||
*/
|
*/
|
||||||
convertOfflineReplyToOnline(offlineReply: any, siteId?: string): Promise<any> {
|
convertOfflineReplyToOnline(offlineReply: AddonModForumOfflineReply, siteId?: string): Promise<AddonModForumPost> {
|
||||||
const reply: any = {
|
const reply: AddonModForumPost = {
|
||||||
id: -offlineReply.timecreated,
|
id: -offlineReply.timecreated,
|
||||||
discussionid: offlineReply.discussionid,
|
discussionid: offlineReply.discussionid,
|
||||||
parentid: offlineReply.postid,
|
parentid: offlineReply.postid,
|
||||||
|
@ -186,20 +193,25 @@ export class AddonModForumHelperProvider {
|
||||||
reply: false,
|
reply: false,
|
||||||
},
|
},
|
||||||
unread: false,
|
unread: false,
|
||||||
isprivatereply: offlineReply.options && offlineReply.options.private,
|
isprivatereply: !!offlineReply.options?.private,
|
||||||
tags: null,
|
|
||||||
};
|
};
|
||||||
const promises: Promise<void>[] = [];
|
const promises: Promise<void>[] = [];
|
||||||
|
|
||||||
// Treat attachments if any.
|
// Treat attachments if any.
|
||||||
if (offlineReply.options && offlineReply.options.attachmentsid) {
|
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(
|
promises.push(
|
||||||
this
|
this
|
||||||
.getReplyStoredFiles(offlineReply.forumid, reply.parentid, siteId, offlineReply.userid)
|
.getReplyStoredFiles(offlineReply.forumid, reply.parentid!, siteId, offlineReply.userid)
|
||||||
.then(files => reply.attachments = reply.attachments.concat(files)),
|
.then(files => {
|
||||||
|
reply.attachments = reply.attachments!.concat(files as unknown as []);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -219,7 +231,7 @@ export class AddonModForumHelperProvider {
|
||||||
);
|
);
|
||||||
|
|
||||||
return Promise.all(promises).then(() => {
|
return Promise.all(promises).then(() => {
|
||||||
reply.attachment = reply.attachments.length > 0 ? 1 : 0;
|
reply.attachment = reply.attachments!.length > 0 ? 1 : 0;
|
||||||
|
|
||||||
return reply;
|
return reply;
|
||||||
});
|
});
|
||||||
|
@ -293,10 +305,10 @@ export class AddonModForumHelperProvider {
|
||||||
* @param siteId Site ID. If not defined, current site.
|
* @param siteId Site ID. If not defined, current site.
|
||||||
* @return Promise resolved with the discussion data.
|
* @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();
|
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, {
|
const response = await AddonModForum.instance.getDiscussions(forumId, {
|
||||||
cmId,
|
cmId,
|
||||||
page,
|
page,
|
||||||
|
@ -330,7 +342,7 @@ export class AddonModForumHelperProvider {
|
||||||
* @param siteId Site ID. If not defined, current site.
|
* @param siteId Site ID. If not defined, current site.
|
||||||
* @return Promise resolved with the files.
|
* @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);
|
const folderPath = await AddonModForumOffline.instance.getNewDiscussionFolder(forumId, timecreated, siteId);
|
||||||
|
|
||||||
return CoreFileUploader.instance.getStoredFiles(folderPath);
|
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.
|
* @param userId User the reply belongs to. If not defined, current user in site.
|
||||||
* @return Promise resolved with the files.
|
* @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);
|
const folderPath = await AddonModForumOffline.instance.getReplyFolder(forumId, postId, siteId, userId);
|
||||||
|
|
||||||
return CoreFileUploader.instance.getStoredFiles(folderPath);
|
return CoreFileUploader.instance.getStoredFiles(folderPath);
|
||||||
|
@ -380,10 +392,10 @@ export class AddonModForumHelperProvider {
|
||||||
*
|
*
|
||||||
* @param forum Forum instance.
|
* @param forum Forum instance.
|
||||||
*/
|
*/
|
||||||
isCutoffDateReached(forum: any): boolean {
|
isCutoffDateReached(forum: AddonModForumData): boolean {
|
||||||
const now = Date.now() / 1000;
|
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.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
import { CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
|
||||||
import { CoreFile } from '@services/file';
|
import { CoreFile } from '@services/file';
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
import { CoreTextUtils } from '@services/utils/text';
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
|
@ -401,8 +402,14 @@ export class AddonModForumOfflineProvider {
|
||||||
|
|
||||||
export class AddonModForumOffline extends makeSingleton(AddonModForumOfflineProvider) {}
|
export class AddonModForumOffline extends makeSingleton(AddonModForumOfflineProvider) {}
|
||||||
|
|
||||||
export type AddonModForumDiscussionOptions = Record<string, unknown>;
|
export type AddonModForumDiscussionOptions = {
|
||||||
export type AddonModForumReplyOptions = Record<string, unknown>;
|
attachmentsid: number | CoreFileUploaderStoreFilesResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AddonModForumReplyOptions = {
|
||||||
|
private?: boolean;
|
||||||
|
attachmentsid?: number | CoreFileUploaderStoreFilesResult;
|
||||||
|
};
|
||||||
|
|
||||||
export type AddonModForumOfflineDiscussion = {
|
export type AddonModForumOfflineDiscussion = {
|
||||||
forumid: number;
|
forumid: number;
|
||||||
|
@ -412,6 +419,7 @@ export type AddonModForumOfflineDiscussion = {
|
||||||
message: string;
|
message: string;
|
||||||
options: AddonModForumDiscussionOptions;
|
options: AddonModForumDiscussionOptions;
|
||||||
groupid: number;
|
groupid: number;
|
||||||
|
groupname?: string;
|
||||||
userid: number;
|
userid: number;
|
||||||
timecreated: number;
|
timecreated: number;
|
||||||
};
|
};
|
||||||
|
|
|
@ -23,10 +23,15 @@ import { CoreSites } from '@services/sites';
|
||||||
import { CoreSync } from '@services/sync';
|
import { CoreSync } from '@services/sync';
|
||||||
import { CoreTextUtils } from '@services/utils/text';
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
import { Translate } from '@singletons';
|
import { makeSingleton, Translate } from '@singletons';
|
||||||
import { CoreArray } from '@singletons/array';
|
import { CoreArray } from '@singletons/array';
|
||||||
import { CoreEvents } from '@singletons/events';
|
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 { AddonModForumHelper } from './helper.service';
|
||||||
import { AddonModForumOffline, AddonModForumOfflineDiscussion, AddonModForumOfflineReply } from './offline.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.
|
* @return Promise resolved if sync is successful, rejected if sync fails.
|
||||||
*/
|
*/
|
||||||
protected async syncAllForumsFunc(force: boolean, siteId: string): Promise<void> {
|
protected async syncAllForumsFunc(force: boolean, siteId: string): Promise<void> {
|
||||||
const sitePromises: Promise<void>[] = [];
|
const sitePromises: Promise<unknown>[] = [];
|
||||||
|
|
||||||
// Sync all new discussions.
|
// Sync all new discussions.
|
||||||
const syncDiscussions = async (discussions: AddonModForumOfflineDiscussion[]) => {
|
const syncDiscussions = async (discussions: AddonModForumOfflineDiscussion[]) => {
|
||||||
|
@ -239,13 +244,13 @@ export class AddonModForumSyncProvider extends CoreSyncBaseProvider<AddonModForu
|
||||||
|
|
||||||
// Now try to add the discussion.
|
// Now try to add the discussion.
|
||||||
const options = CoreUtils.instance.clone(discussion.options || {});
|
const options = CoreUtils.instance.clone(discussion.options || {});
|
||||||
options.attachmentsid = itemId;
|
options.attachmentsid = itemId!;
|
||||||
|
|
||||||
await AddonModForum.instance.addNewDiscussionOnline(
|
await AddonModForum.instance.addNewDiscussionOnline(
|
||||||
forumId,
|
forumId,
|
||||||
discussion.subject,
|
discussion.subject,
|
||||||
discussion.message,
|
discussion.message,
|
||||||
options,
|
options as unknown as AddonModForumAddDiscussionWSOptionsObject,
|
||||||
groupId,
|
groupId,
|
||||||
siteId,
|
siteId,
|
||||||
);
|
);
|
||||||
|
@ -309,8 +314,13 @@ export class AddonModForumSyncProvider extends CoreSyncBaseProvider<AddonModForu
|
||||||
* @return Promise resolved if sync is successful, rejected otherwise.
|
* @return Promise resolved if sync is successful, rejected otherwise.
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// 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
|
// @todo
|
||||||
|
|
||||||
|
return { updated: true, warnings: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -437,7 +447,7 @@ export class AddonModForumSyncProvider extends CoreSyncBaseProvider<AddonModForu
|
||||||
reply.postid,
|
reply.postid,
|
||||||
reply.subject,
|
reply.subject,
|
||||||
reply.message,
|
reply.message,
|
||||||
reply.options,
|
reply.options as unknown as AddonModForumAddDiscussionPostWSOptionsObject,
|
||||||
siteId,
|
siteId,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -526,18 +536,18 @@ export class AddonModForumSyncProvider extends CoreSyncBaseProvider<AddonModForu
|
||||||
*
|
*
|
||||||
* @param forumId Forum ID the post belongs to.
|
* @param forumId Forum ID the post belongs to.
|
||||||
* @param post Offline post or discussion.
|
* @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 siteId Site ID. If not defined, current site.
|
||||||
* @param userId User the reply belongs to. If not defined, current user in 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.
|
* @return Promise resolved with draftid if uploaded, resolved with undefined if nothing to upload.
|
||||||
*/
|
*/
|
||||||
protected async uploadAttachments(
|
protected async uploadAttachments(
|
||||||
forumId: number,
|
forumId: number,
|
||||||
post: any,
|
post: AddonModForumOfflineDiscussion | AddonModForumOfflineReply,
|
||||||
isDisc: boolean,
|
isDiscussion: boolean,
|
||||||
siteId?: string,
|
siteId?: string,
|
||||||
userId?: number,
|
userId?: number,
|
||||||
): Promise<void> {
|
): Promise<number | undefined> {
|
||||||
const attachments = post && post.options && post.options.attachmentsid;
|
const attachments = post && post.options && post.options.attachmentsid;
|
||||||
|
|
||||||
if (!attachments) {
|
if (!attachments) {
|
||||||
|
@ -545,22 +555,31 @@ export class AddonModForumSyncProvider extends CoreSyncBaseProvider<AddonModForu
|
||||||
}
|
}
|
||||||
|
|
||||||
// Has some attachments to sync.
|
// 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.
|
// Has offline files.
|
||||||
try {
|
try {
|
||||||
const atts = isDisc
|
const postAttachments = isDiscussion
|
||||||
? await AddonModForumHelper.instance.getNewDiscussionStoredFiles(forumId, post.timecreated, siteId)
|
? await AddonModForumHelper.instance.getNewDiscussionStoredFiles(
|
||||||
: await AddonModForumHelper.instance.getReplyStoredFiles(forumId, post.postid, siteId, userId);
|
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) {
|
} catch (error) {
|
||||||
// Folder not found, no files to add.
|
// 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.
|
* Result of forum sync.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -93,4 +93,7 @@
|
||||||
--core-question-feedback-background-color: var(--yellow-dark);
|
--core-question-feedback-background-color: var(--yellow-dark);
|
||||||
|
|
||||||
--core-dd-question-selected-shadow: 2px 2px 4px var(--gray-light);
|
--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-messages-discussion-badge-text: var(--custom-messages-discussion-badge-text, var(--white));
|
||||||
|
|
||||||
--addon-forum-avatar-size: var(--custom-forum-avatar-size, 28px);
|
--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);
|
--drop-shadow: var(--custom-drop-shadow, 0, 0, 0, 0.2);
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue