MOBILE-3157 forum: Add delete post option

main
Pau Ferrer Ocaña 2019-10-29 10:59:01 +01:00
parent e3e559da6f
commit f96b5e9b1c
15 changed files with 419 additions and 82 deletions

View File

@ -572,6 +572,9 @@
"addon.mod_forum.cannotcreatediscussion": "forum",
"addon.mod_forum.couldnotadd": "forum",
"addon.mod_forum.cutoffdatereached": "forum",
"addon.mod_forum.delete": "forum",
"addon.mod_forum.deletedpost": "forum",
"addon.mod_forum.deletesure": "forum",
"addon.mod_forum.discussion": "forum",
"addon.mod_forum.discussionlistsortbycreatedasc": "forum",
"addon.mod_forum.discussionlistsortbycreateddesc": "forum",
@ -1718,6 +1721,7 @@
"core.nocomments": "moodle",
"core.nograde": "moodle",
"core.none": "moodle",
"core.nooptionavailable": "local_moodlemobileapp",
"core.nopasswordchangeforced": "local_moodlemobileapp",
"core.nopermissionerror": "local_moodlemobileapp",
"core.nopermissions": "error",

View File

@ -25,12 +25,14 @@ import { CoreTagComponentsModule } from '@core/tag/components/components.module'
import { AddonModForumIndexComponent } from './index/index';
import { AddonModForumPostComponent } from './post/post';
import { AddonForumDiscussionOptionsMenuComponent } from './discussion-options-menu/discussion-options-menu';
import { AddonForumPostOptionsMenuComponent } from './post-options-menu/post-options-menu';
@NgModule({
declarations: [
AddonModForumIndexComponent,
AddonModForumPostComponent,
AddonForumDiscussionOptionsMenuComponent
AddonForumDiscussionOptionsMenuComponent,
AddonForumPostOptionsMenuComponent
],
imports: [
CommonModule,
@ -48,11 +50,13 @@ import { AddonForumDiscussionOptionsMenuComponent } from './discussion-options-m
exports: [
AddonModForumIndexComponent,
AddonModForumPostComponent,
AddonForumDiscussionOptionsMenuComponent
AddonForumDiscussionOptionsMenuComponent,
AddonForumPostOptionsMenuComponent
],
entryComponents: [
AddonModForumIndexComponent,
AddonForumDiscussionOptionsMenuComponent
AddonForumDiscussionOptionsMenuComponent,
AddonForumPostOptionsMenuComponent
]
})
export class AddonModForumComponentsModule {}

View File

@ -13,7 +13,7 @@
// limitations under the License.
import { Component, Optional, Injector, ViewChild } from '@angular/core';
import { Content, ModalController, NavController, PopoverController } from 'ionic-angular';
import { Content, ModalController, PopoverController } from 'ionic-angular';
import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component';
import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate';
@ -75,7 +75,6 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
constructor(injector: Injector,
@Optional() protected content: Content,
protected navCtrl: NavController,
protected modalCtrl: ModalController,
protected groupsProvider: CoreGroupsProvider,
protected userProvider: CoreUserProvider,
@ -110,28 +109,36 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
this.replyObserver = this.eventsProvider.on(AddonModForumProvider.REPLY_DISCUSSION_EVENT,
this.eventReceived.bind(this, false));
this.changeDiscObserver = this.eventsProvider.on(AddonModForumProvider.CHANGE_DISCUSSION_EVENT, (data) => {
this.forumProvider.invalidateDiscussionsList(this.forum.id).finally(() => {
// If it's a new discussion in tablet mode, try to open it.
if (data.discussionId) {
// Discussion sent to server, search it in the list of discussions.
const discussion = this.discussions.find((disc) => {
return data.discussionId = disc.discussion;
});
if ((this.forum && this.forum.id === data.forumId) || data.cmId === this.module.id) {
this.forumProvider.invalidateDiscussionsList(this.forum.id).finally(() => {
if (data.discussionId) {
// Discussion changed, search it in the list of discussions.
const discussion = this.discussions.find((disc) => {
return data.discussionId = disc.discussion;
});
if (discussion) {
if (typeof data.locked != 'undefined') {
discussion.locked = data.locked;
}
if (typeof data.pinned != 'undefined') {
discussion.pinned = data.pinned;
}
if (typeof data.starred != 'undefined') {
discussion.starred = data.starred;
if (discussion) {
if (typeof data.locked != 'undefined') {
discussion.locked = data.locked;
}
if (typeof data.pinned != 'undefined') {
discussion.pinned = data.pinned;
}
if (typeof data.starred != 'undefined') {
discussion.starred = data.starred;
}
}
}
}
});
if (typeof data.deleted != 'undefined' && data.deleted) {
if (data.post.parent == 0 && this.splitviewCtrl && this.splitviewCtrl.isOn()) {
// Discussion deleted, clear details page.
this.splitviewCtrl.emptyDetails();
}
this.showLoadingAndRefresh(false);
}
});
}
});
// Select the current opened discussion.

View File

@ -0,0 +1,17 @@
<core-loading [hideUntil]="loaded" class="core-loading-center">
<ion-item text-wrap (click)="editPost()" *ngIf="canEdit">
<ion-icon name="create" item-start></ion-icon>
<h2>{{ 'addon.mod_forum.edit' | translate }}</h2>
</ion-item>
<ion-item text-wrap (click)="deletePost()" *ngIf="canDelete">
<ion-icon name="trash" item-start></ion-icon>
<h2 *ngIf="post.id">{{ 'addon.mod_forum.delete' | translate }}</h2>
<h2 *ngIf="!post.id">{{ 'core.discard' | translate }}</h2>
</ion-item>
<ion-item text-wrap (click)="dismiss()" *ngIf="wordCount">
<h2>{{ 'core.numwords' | translate: {'$a': wordCount} }}</h2>
</ion-item>
<ion-item text-wrap (click)="dismiss()" *ngIf="!canEdit && !canDelete && !wordCount">
<h2>{{ 'core.nooptionavailable' | translate }}</h2>
</ion-item>
</core-loading>

View File

@ -0,0 +1,97 @@
// (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, OnInit } from '@angular/core';
import { NavParams, ViewController } from 'ionic-angular';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { AddonModForumProvider } from '../../providers/forum';
/**
* This component is meant to display a popover with the post options.
*/
@Component({
selector: 'addon-forum-post-options-menu',
templateUrl: 'addon-forum-post-options-menu.html'
})
export class AddonForumPostOptionsMenuComponent implements OnInit {
post: any; // The post.
forumId: number; // The forum Id.
wordCount: number; // Number of words when available.
canEdit = false;
canDelete = false;
loaded = false;
constructor(navParams: NavParams,
protected viewCtrl: ViewController,
protected domUtils: CoreDomUtilsProvider,
protected forumProvider: AddonModForumProvider) {
this.post = navParams.get('post');
this.forumId = navParams.get('forumId');
}
/**
* Component being initialized.
*/
ngOnInit(): void {
if (this.forumId) {
if (this.post.id) {
this.forumProvider.getDiscussionPost(this.forumId, this.post.discussion, this.post.id).then((post) => {
this.canDelete = post.capabilities.delete && this.forumProvider.isDeletePostAvailable();
this.canEdit = false;
this.wordCount = post.wordcount;
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'Error getting discussion post.');
}).finally(() => {
this.loaded = true;
});
} else {
// Offline post, you can edit or discard the post.
this.canEdit = true;
this.canDelete = true;
this.loaded = true;
}
} else {
this.loaded = true;
}
}
/**
* Close the popover.
*/
dismiss(): void {
this.viewCtrl.dismiss();
}
/**
* Delete a post.
*/
deletePost(): void {
if (this.post.id) {
this.viewCtrl.dismiss({action: 'delete'});
} else {
this.viewCtrl.dismiss({action: 'deleteoffline'});
}
}
/**
* Edit a post.
*/
editPost(): void {
if (this.post.id) {
this.viewCtrl.dismiss({action: 'edit'});
} else {
this.viewCtrl.dismiss({action: 'editoffline'});
}
}
}

View File

@ -7,6 +7,9 @@
<core-icon name="fa-star" class="addon-forum-star" *ngIf="post.parent == 0 && !post.pinned && post.starred"></core-icon>
<core-format-text [text]="post.subject" contextLevel="module" [contextInstanceId]="forum && forum.cmid" [courseId]="courseId"></core-format-text>
</h2>
<button ion-button icon-only clear color="dark" (click)="showOptionsMenu($event)" *ngIf="optionsMenuEnabled">
<core-icon name="more"></core-icon>
</button>
</div>
<div class="addon-mod-forum-post-info">
<ion-avatar *ngIf="post.userfullname" core-user-avatar [user]="post" item-start [courseId]="courseId"></ion-avatar>
@ -19,12 +22,15 @@
<ion-note float-end padding-left text-end *ngIf="trackPosts && !post.postread" [attr.aria-label]="'addon.mod_forum.unread' | translate">
<core-icon name="fa-circle" color="primary"></core-icon>
</ion-note>
<button ion-button icon-only clear color="dark" (click)="showOptionsMenu($event)" *ngIf="!displaySubject && optionsMenuEnabled">
<core-icon name="more"></core-icon>
</button>
</div>
</ion-item>
</ion-card-header>
<ion-card-content [attr.padding-top]="post.parent == 0 || null">
<div padding-bottom *ngIf="post.isprivatereply">
<ion-note>{{ 'addon.mod_forum.postisprivatereply' | translate }}</ion-note>
<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 no-lines *ngIf="post.attachments && post.attachments.length > 0">
@ -40,16 +46,11 @@
<core-rating-aggregate *ngIf="forum && ratingInfo" [ratingInfo]="ratingInfo" contextLevel="module" [instanceId]="componentId" [itemId]="post.id" [courseId]="courseId" [aggregateMethod]="forum.assessed" [scaleId]="forum.scale"></core-rating-aggregate>
<ion-item no-padding text-end *ngIf="post.id && post.canreply && !post.isprivatereply" class="addon-forum-reply-button">
<button ion-button icon-left clear small (click)="showReply()" [attr.aria-controls]="'addon-forum-reply-edit-form-' + uniqueId" [attr.aria-expanded]="replyData.replyingTo === post.id">
<ion-icon name="undo"></ion-icon> {{ 'addon.mod_forum.reply' | translate }}
<button ion-button icon-left clear small (click)="showReplyForm()" [attr.aria-controls]="'addon-forum-reply-edit-form-' + uniqueId" [attr.aria-expanded]="replyData.replyingTo === post.id">
<core-icon name="fa-reply"></core-icon> {{ 'addon.mod_forum.reply' | translate }}
</button>
</ion-item>
</div>
<ion-item text-end *ngIf="!post.id && (!replyData.isEditing || replyData.replyingTo != post.parent)">
<button ion-button icon-left clear small (click)="editReply()" [attr.aria-controls]="'addon-forum-reply-edit-form-' + uniqueId" [attr.aria-expanded]="replyData.replyingTo === post.parent">
<ion-icon name="create"></ion-icon> {{ 'addon.mod_forum.edit' | translate }}
</button>
</ion-item>
<ion-list [id]="'addon-forum-reply-edit-form-' + uniqueId" *ngIf="(post.id && !replyData.isEditing && replyData.replyingTo == post.id) || (!post.id && replyData.isEditing && replyData.replyingTo == post.parent)">
<ion-item>
@ -81,11 +82,6 @@
<button ion-button block color="light" (click)="cancel()">{{ 'core.cancel' | translate }}</button>
</ion-col>
</ion-row>
<ion-row *ngIf="replyData.isEditing">
<ion-col>
<button ion-button block color="light" (click)="discard()">{{ 'core.discard' | translate }}</button>
</ion-col>
</ion-row>
</ion-grid>
</ion-list>
</div>

View File

@ -14,18 +14,21 @@
import { Component, Input, Output, Optional, EventEmitter, OnInit, OnDestroy } from '@angular/core';
import { FormControl } from '@angular/forms';
import { Content } from 'ionic-angular';
import { Content, PopoverController } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader';
import { CoreSyncProvider } from '@providers/sync';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites';
import { AddonModForumProvider } from '../../providers/forum';
import { AddonModForumHelperProvider } from '../../providers/helper';
import { AddonModForumOfflineProvider } from '../../providers/offline';
import { AddonModForumSyncProvider } from '../../providers/sync';
import { CoreRatingInfo } from '@core/rating/providers/rating';
import { CoreTagProvider } from '@core/tag/providers/tag';
import { AddonForumPostOptionsMenuComponent } from '../post-options-menu/post-options-menu';
/**
* Components that shows a discussion post, its attachments and the action buttons allowed (reply, etc.).
@ -55,6 +58,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy {
advanced = false; // Display all form fields.
tagsEnabled: boolean;
displaySubject = true;
optionsMenuEnabled = false;
protected syncId: string;
@ -69,7 +73,10 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy {
private forumOffline: AddonModForumOfflineProvider,
private forumSync: AddonModForumSyncProvider,
private tagProvider: CoreTagProvider,
@Optional() private content: Content) {
@Optional() private content: Content,
protected popoverCtrl: PopoverController,
protected eventsProvider: CoreEventsProvider,
protected sitesProvider: CoreSitesProvider) {
this.onPostChange = new EventEmitter<void>();
this.tagsEnabled = this.tagProvider.areTagsAvailableInSite();
}
@ -84,10 +91,44 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy {
this.displaySubject = this.post.parent == 0 ||
(this.post.subject != this.defaultSubject && this.post.subject != 'Re: ' + this.defaultSubject &&
this.post.subject != reTranslated + this.defaultSubject);
this.optionsMenuEnabled = !this.post.id || (this.forumProvider.isGetDiscussionPostAvailable() &&
(this.forumProvider.isDeletePostAvailable()));
}
/**
* Set data to new post, clearing temporary files and updating original data.
* Deletes an online post.
*/
deletePost(): void {
this.domUtils.showDeleteConfirm('addon.mod_forum.deletesure').then(() => {
const modal = this.domUtils.showModalLoading('core.deleting', true);
this.forumProvider.deletePost(this.post.id).then((response) => {
const data = {
forumId: this.forum.id,
discussionId: this.discussionId,
cmId: this.forum.cmid,
deleted: response.status,
post: this.post
};
this.eventsProvider.trigger(AddonModForumProvider.CHANGE_DISCUSSION_EVENT, data,
this.sitesProvider.getCurrentSiteId());
this.domUtils.showToast('addon.mod_forum.deletedpost', true);
}).catch((error) => {
this.domUtils.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.
@ -96,7 +137,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy {
* @param isPrivate True if it's private reply.
* @param files Reply attachments.
*/
protected setReplyData(replyingTo?: number, isEditing?: boolean, subject?: string, message?: string, files?: any[],
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.
this.uploaderProvider.clearTmpFiles(this.replyData.files);
@ -121,14 +162,52 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy {
this.advanced = this.replyData.files.length > 0;
}
/**
* Show the context menu.
*
* @param e Click Event.
*/
showOptionsMenu(e: Event): void {
e.preventDefault();
e.stopPropagation();
const popover = this.popoverCtrl.create(AddonForumPostOptionsMenuComponent, {
post: this.post,
forumId: this.forum.id
});
popover.onDidDismiss((data) => {
if (data && data.action) {
switch (data.action) {
case 'edit':
// Not implemented.
break;
case 'editoffline':
this.editOfflineReply();
break;
case 'delete':
this.deletePost();
break;
case 'deleteoffline':
this.discardOfflineReply();
break;
default:
break;
}
}
});
popover.present({
ev: e
});
}
/**
* Set this post as being replied to.
*/
showReply(): void {
showReplyForm(): void {
if (this.replyData.isEditing) {
// User is editing a post, data needs to be resetted. Ask confirm if there is unsaved data.
this.confirmDiscard().then(() => {
this.setReplyData(this.post.id);
this.setReplyFormData(this.post.id);
if (this.content) {
setTimeout(() => {
@ -143,7 +222,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy {
return;
} else if (!this.replyData.replyingTo) {
// User isn't replying, it's a brand new reply. Initialize the data.
this.setReplyData(this.post.id);
this.setReplyFormData(this.post.id);
} else {
// The post being replied has changed but the data will be kept.
this.replyData.replyingTo = this.post.id;
@ -162,13 +241,13 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy {
/**
* Set this post as being edited to.
*/
editReply(): void {
editOfflineReply(): void {
// Ask confirm if there is unsaved data.
this.confirmDiscard().then(() => {
this.syncId = this.forumSync.getDiscussionSyncId(this.discussionId);
this.syncProvider.blockOperation(AddonModForumProvider.COMPONENT, this.syncId);
this.setReplyData(this.post.parent, true, this.post.subject, this.post.message, this.post.attachments,
this.setReplyFormData(this.post.parent, true, this.post.subject, this.post.message, this.post.attachments,
this.post.isprivatereply);
}).catch(() => {
// Cancelled.
@ -259,7 +338,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy {
}
// Reset data.
this.setReplyData();
this.setReplyFormData();
this.onPostChange.emit();
@ -279,7 +358,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy {
cancel(): void {
this.confirmDiscard().then(() => {
// Reset data.
this.setReplyData();
this.setReplyFormData();
if (this.syncId) {
this.syncProvider.unblockOperation(AddonModForumProvider.COMPONENT, this.syncId);
@ -292,8 +371,8 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy {
/**
* Discard offline reply.
*/
discard(): void {
this.domUtils.showConfirm(this.translate.instant('core.areyousure')).then(() => {
discardOfflineReply(): void {
this.domUtils.showDeleteConfirm().then(() => {
const promises = [];
promises.push(this.forumOffline.deleteReply(this.post.parent));
@ -305,7 +384,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy {
return Promise.all(promises).finally(() => {
// Reset data.
this.setReplyData();
this.setReplyFormData();
this.onPostChange.emit();
@ -322,7 +401,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy {
* Function called when rating is updated online.
*/
ratingUpdated(): void {
this.forumProvider.invalidateDiscussionPosts(this.discussionId);
this.forumProvider.invalidateDiscussionPosts(this.discussionId, this.forum.id);
}
/**

View File

@ -9,6 +9,9 @@
"cannotcreatediscussion": "Could not create new discussion",
"couldnotadd": "Could not add your post due to an unknown error",
"cutoffdatereached": "The cut-off date for posting to this forum is reached so you can no longer post to it.",
"delete": "Delete",
"deletedpost": "The post has been deleted",
"deletesure": "Are you sure you want to delete this post?",
"discussion": "Discussion",
"discussionlistsortbycreatedasc": "Sort by creation date in ascending order",
"discussionlistsortbycreateddesc": "Sort by creation date in descending order",

View File

@ -13,7 +13,7 @@
// limitations under the License.
import { Component, Optional, OnDestroy, ViewChild, NgZone } from '@angular/core';
import { IonicPage, NavParams, Content } from 'ionic-angular';
import { IonicPage, NavParams, Content, NavController } from 'ionic-angular';
import { Network } from '@ionic-native/network';
import { TranslateService } from '@ngx-translate/core';
import { CoreAppProvider } from '@providers/app';
@ -46,8 +46,8 @@ export class AddonModForumDiscussionPage implements OnDestroy {
courseId: number;
discussionId: number;
forum: any;
accessInfo: any;
forum: any = {};
accessInfo: any = {};
discussion: any;
posts: any[];
discussionLoaded = false;
@ -106,7 +106,8 @@ export class AddonModForumDiscussionPage implements OnDestroy {
private forumHelper: AddonModForumHelperProvider,
private forumSync: AddonModForumSyncProvider,
private ratingOffline: CoreRatingOfflineProvider,
@Optional() private svComponent: CoreSplitViewComponent) {
@Optional() private svComponent: CoreSplitViewComponent,
protected navCtrl: NavController) {
this.courseId = navParams.get('courseId');
this.cmId = navParams.get('cmId');
this.forumId = navParams.get('forumId');
@ -190,17 +191,32 @@ export class AddonModForumDiscussionPage implements OnDestroy {
});
this.changeDiscObserver = this.eventsProvider.on(AddonModForumProvider.CHANGE_DISCUSSION_EVENT, (data) => {
this.forumProvider.invalidateDiscussionsList(this.forum.id).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 ((this.forum && this.forum.id === data.forumId) || data.cmId === this.cmId) {
this.forumProvider.invalidateDiscussionsList(this.forum.id).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.parent == 0) {
if (this.svComponent && this.svComponent.isOn()) {
this.svComponent.emptyDetails();
} else {
this.navCtrl.pop();
}
} else {
this.discussionLoaded = false;
this.refreshPosts();
}
}
});
}
});
}
@ -358,8 +374,6 @@ export class AddonModForumDiscussionPage implements OnDestroy {
return Promise.all(promises);
}).catch(() => {
// Ignore errors.
this.forum = {};
this.accessInfo = {};
}).then(() => {
this.defaultSubject = this.translate.instant('addon.mod_forum.re') + ' ' +
(this.discussion ? this.discussion.subject : '');
@ -496,7 +510,7 @@ export class AddonModForumDiscussionPage implements OnDestroy {
const promises = [
this.forumProvider.invalidateForumData(this.courseId),
this.forumProvider.invalidateDiscussionPosts(this.discussionId),
this.forumProvider.invalidateDiscussionPosts(this.discussionId, this.forumId),
this.forumProvider.invalidateAccessInformation(this.forumId),
this.forumProvider.invalidateCanAddDiscussion(this.forumId)
];

View File

@ -74,6 +74,7 @@ export class AddonModForumProvider {
/**
* Get common part of cache key for can add discussion WS calls.
* TODO: Use getForumDataCacheKey as a prefix.
*
* @param forumId Forum ID.
* @return Cache key.
@ -82,6 +83,38 @@ export class AddonModForumProvider {
return this.ROOT_CACHE_KEY + 'canadddiscussion:' + forumId + ':';
}
/**
* Get prefix cache key for all forum activity data WS calls.
*
* @param forumId Forum ID.
* @return Cache key.
*/
protected getForumDataPrefixCacheKey(forumId: number): string {
return this.ROOT_CACHE_KEY + forumId;
}
/**
* Get cache key for discussion post data WS calls.
*
* @param forumId Forum ID.
* @param discussionId Discussion ID.
* @param postId Course ID.
* @return Cache key.
*/
protected getDiscussionPostDataCacheKey(forumId: number, discussionId: number, postId: number): string {
return this.getForumDiscussionDataCacheKey(forumId, discussionId) + ':post:' + postId;
}
/**
* Get cache key for forum data WS calls.
*
* @param courseId Course ID.
* @return Cache key.
*/
protected getForumDiscussionDataCacheKey(forumId: number, discussionId: number): string {
return this.getForumDataPrefixCacheKey(forumId) + ':discussion:' + discussionId;
}
/**
* Get cache key for forum data WS calls.
*
@ -94,6 +127,7 @@ export class AddonModForumProvider {
/**
* Get cache key for forum access information WS calls.
* TODO: Use getForumDataCacheKey as a prefix.
*
* @param forumId Forum ID.
* @return Cache key.
@ -104,6 +138,7 @@ export class AddonModForumProvider {
/**
* Get cache key for forum discussion posts WS calls.
* TODO: Use getForumDiscussionDataCacheKey instead.
*
* @param discussionId Discussion ID.
* @return Cache key.
@ -218,6 +253,24 @@ export class AddonModForumProvider {
return this.canAddDiscussion(forumId, AddonModForumProvider.ALL_PARTICIPANTS);
}
/**
* Delete a post.
*
* @param postId Post id.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
* @since 3.8
*/
deletePost(postId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const params = {
postid: postId
};
return site.write('mod_forum_delete_post', params);
});
}
/**
* Extract the starting post of a discussion from a list of posts. The post is removed from the array passed as a parameter.
*
@ -244,6 +297,26 @@ export class AddonModForumProvider {
return this.sitesProvider.getCurrentSite().isVersionGreaterEqualThan(['3.1.5', '3.2.2']);
}
/**
* Returns whether or not getDiscussionPost WS available or not.
*
* @return If WS is avalaible.
* @since 3.8
*/
isGetDiscussionPostAvailable(): boolean {
return this.sitesProvider.wsAvailableInCurrentSite('mod_forum_get_discussion_post');
}
/**
* Returns whether or not deletePost WS available or not.
*
* @return If WS is avalaible.
* @since 3.8
*/
isDeletePostAvailable(): boolean {
return this.sitesProvider.wsAvailableInCurrentSite('mod_forum_delete_post');
}
/**
* Format discussions, setting groupname if the discussion group is valid.
*
@ -306,6 +379,35 @@ export class AddonModForumProvider {
});
}
/**
* Get a particular discussion post.
*
* @param forumId Forum ID.
* @param discussionId Discussion ID.
* @param postId Post ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the post is retrieved.
*/
getDiscussionPost(forumId: number, discussionId: number, postId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const params = {
postid: postId
};
const preSets = {
cacheKey: this.getDiscussionPostDataCacheKey(forumId, discussionId, postId),
updateFrequency: CoreSite.FREQUENCY_RARELY
};
return site.read('mod_forum_get_discussion_post', params, preSets).then((response) => {
if (response.post) {
return response.post;
} else {
return Promise.reject(null);
}
});
});
}
/**
* Get a forum by course module ID.
*
@ -641,7 +743,7 @@ export class AddonModForumProvider {
const promises = [];
response.discussions.forEach((discussion) => {
promises.push(this.invalidateDiscussionPosts(discussion.discussion));
promises.push(this.invalidateDiscussionPosts(discussion.discussion, forum.id));
});
return this.utils.allPromises(promises);
@ -673,12 +775,19 @@ export class AddonModForumProvider {
* Invalidates forum discussion posts.
*
* @param discussionId Discussion ID.
* @param forumId Forum ID. If not set, we can't invalidate individual post information.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the data is invalidated.
*/
invalidateDiscussionPosts(discussionId: number, siteId?: string): Promise<any> {
invalidateDiscussionPosts(discussionId: number, forumId?: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.invalidateWsCacheForKey(this.getDiscussionPostsCacheKey(discussionId));
const promises = [site.invalidateWsCacheForKey(this.getDiscussionPostsCacheKey(discussionId))];
if (forumId) {
promises.push(site.invalidateWsCacheForKeyStartingWith(this.getForumDiscussionDataCacheKey(forumId, discussionId)));
}
return this.utils.allPromises(promises);
});
}
@ -844,7 +953,7 @@ export class AddonModForumProvider {
* @param discussionId DIscussion id.
* @param locked True to lock, false to unlock.
* @param siteId Site ID. If not defined, current site.
* @return Promise resvoled when done.
* @return Promise resolved when done.
* @since 3.7
*/
setLockState(forumId: number, discussionId: number, locked: boolean, siteId?: string): Promise<any> {
@ -878,7 +987,7 @@ export class AddonModForumProvider {
* @param discussionId Discussion id.
* @param locked True to pin, false to unpin.
* @param siteId Site ID. If not defined, current site.
* @return Promise resvoled when done.
* @return Promise resolved when done.
* @since 3.7
*/
setPinState(discussionId: number, pinned: boolean, siteId?: string): Promise<any> {
@ -898,7 +1007,7 @@ export class AddonModForumProvider {
* @param discussionId Discussion id.
* @param starred True to star, false to unstar.
* @param siteId Site ID. If not defined, current site.
* @return Promise resvoled when done.
* @return Promise resolved when done.
* @since 3.7
*/
toggleFavouriteState(discussionId: number, starred: boolean, siteId?: string): Promise<any> {

View File

@ -62,7 +62,7 @@ export class AddonModForumPushClickHandler implements CorePushNotificationsClick
pageParams.postId = Number(data.postid || contextUrlParams.urlHash.replace('p', ''));
}
return this.forumProvider.invalidateDiscussionPosts(pageParams.discussionId, notification.site).catch(() => {
return this.forumProvider.invalidateDiscussionPosts(pageParams.discussionId, undefined, notification.site).catch(() => {
// Ignore errors.
}).then(() => {
return this.linkHelper.goInSite(undefined, 'AddonModForumDiscussionPage', pageParams, notification.site);

View File

@ -326,7 +326,7 @@ export class AddonModForumSyncProvider extends CoreSyncBaseProvider {
updated = true;
// Invalidate discussions of updated ratings.
promises.push(this.forumProvider.invalidateDiscussionPosts(result.itemSet.itemSetId, siteId));
promises.push(this.forumProvider.invalidateDiscussionPosts(result.itemSet.itemSetId, undefined, siteId));
}
if (result.warnings.length) {
// Fetch forum to construct the warning message.
@ -502,7 +502,7 @@ export class AddonModForumSyncProvider extends CoreSyncBaseProvider {
if (forumId) {
promises.push(this.forumProvider.invalidateDiscussionsList(forumId, siteId));
}
promises.push(this.forumProvider.invalidateDiscussionPosts(discussionId, siteId));
promises.push(this.forumProvider.invalidateDiscussionPosts(discussionId, forumId, siteId));
return this.utils.allPromises(promises).catch(() => {
// Ignore errors.

View File

@ -571,6 +571,9 @@
"addon.mod_forum.cannotcreatediscussion": "Could not create new discussion",
"addon.mod_forum.couldnotadd": "Could not add your post due to an unknown error",
"addon.mod_forum.cutoffdatereached": "The cut-off date for posting to this forum is reached so you can no longer post to it.",
"addon.mod_forum.delete": "Delete",
"addon.mod_forum.deletedpost": "The post has been deleted",
"addon.mod_forum.deletesure": "Are you sure you want to delete this post?",
"addon.mod_forum.discussion": "Discussion",
"addon.mod_forum.discussionlistsortbycreatedasc": "Sort by creation date in ascending order",
"addon.mod_forum.discussionlistsortbycreateddesc": "Sort by creation date in descending order",
@ -1713,6 +1716,7 @@
"core.nocomments": "No comments",
"core.nograde": "No grade",
"core.none": "None",
"core.nooptionavailable": "No option available",
"core.nopasswordchangeforced": "You cannot proceed without changing your password.",
"core.nopermissionerror": "Sorry, but you do not currently have permissions to do that",
"core.nopermissions": "Sorry, but you do not currently have permissions to do that ({{$a}}).",

View File

@ -13,7 +13,6 @@
// limitations under the License.
import { Component, Input, Output, OnInit, EventEmitter } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { CoreFileProvider } from '@providers/file';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype';
@ -48,9 +47,12 @@ export class CoreLocalFileComponent implements OnInit {
editMode: boolean;
relativePath: string;
constructor(private mimeUtils: CoreMimetypeUtilsProvider, private utils: CoreUtilsProvider, private translate: TranslateService,
private textUtils: CoreTextUtilsProvider, private fileProvider: CoreFileProvider,
private domUtils: CoreDomUtilsProvider, private timeUtils: CoreTimeUtilsProvider) {
constructor(private mimeUtils: CoreMimetypeUtilsProvider,
private utils: CoreUtilsProvider,
private textUtils: CoreTextUtilsProvider,
private fileProvider: CoreFileProvider,
private domUtils: CoreDomUtilsProvider,
private timeUtils: CoreTimeUtilsProvider) {
this.onDelete = new EventEmitter();
this.onRename = new EventEmitter();
this.onClick = new EventEmitter();

View File

@ -182,6 +182,7 @@
"notapplicable": "n/a",
"notenrolledprofile": "This profile is not available because this user is not enrolled in this course.",
"notice": "Notice",
"nooptionavailable": "No option available",
"notingroup": "Sorry, but you need to be part of a group to see this page.",
"notsent": "Not sent",
"now": "now",