MOBILE-3205 forum: Add status actions onto discussion list

main
Pau Ferrer Ocaña 2019-10-24 15:24:21 +02:00
parent c823efe5e4
commit ccf4aeadea
8 changed files with 289 additions and 30 deletions

View File

@ -24,11 +24,13 @@ import { CoreRatingComponentsModule } from '@core/rating/components/components.m
import { CoreTagComponentsModule } from '@core/tag/components/components.module'; import { CoreTagComponentsModule } from '@core/tag/components/components.module';
import { AddonModForumIndexComponent } from './index/index'; import { AddonModForumIndexComponent } from './index/index';
import { AddonModForumPostComponent } from './post/post'; import { AddonModForumPostComponent } from './post/post';
import { AddonForumDiscussionOptionsMenuComponent } from './discussion-options-menu/discussion-options-menu';
@NgModule({ @NgModule({
declarations: [ declarations: [
AddonModForumIndexComponent, AddonModForumIndexComponent,
AddonModForumPostComponent AddonModForumPostComponent,
AddonForumDiscussionOptionsMenuComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -45,10 +47,12 @@ import { AddonModForumPostComponent } from './post/post';
], ],
exports: [ exports: [
AddonModForumIndexComponent, AddonModForumIndexComponent,
AddonModForumPostComponent AddonModForumPostComponent,
AddonForumDiscussionOptionsMenuComponent
], ],
entryComponents: [ entryComponents: [
AddonModForumIndexComponent AddonModForumIndexComponent,
AddonForumDiscussionOptionsMenuComponent
] ]
}) })
export class AddonModForumComponentsModule {} export class AddonModForumComponentsModule {}

View File

@ -0,0 +1,24 @@
<ion-item text-wrap (click)="setLockState(true)" *ngIf="discussion.canlock && !discussion.locked">
<core-icon name="fa-lock" item-start></core-icon>
<h2>{{ 'addon.mod_forum.lockdiscussion' | translate }}</h2>
</ion-item>
<ion-item text-wrap (click)="setLockState(false)" *ngIf="discussion.canlock && discussion.locked">
<core-icon name="fa-unlock" item-start></core-icon>
<h2>{{ 'addon.mod_forum.unlockdiscussion' | translate }}</h2>
</ion-item>
<ion-item text-wrap (click)="setPinState(true)" *ngIf="canPin && !discussion.pinned">
<core-icon name="fa-map-pin" item-start></core-icon>
<h2>{{ 'addon.mod_forum.pindiscussion' | translate }}</h2>
</ion-item>
<ion-item text-wrap (click)="setPinState(false)" *ngIf="canPin && discussion.pinned">
<core-icon name="fa-map-pin" item-start [slash]="true"></core-icon>
<h2>{{ 'addon.mod_forum.unpindiscussion' | translate }}</h2>
</ion-item>
<ion-item text-wrap (click)="toggleFavouriteState(true)" *ngIf="discussion.canfavourite && !discussion.starred">
<core-icon name="fa-star" item-start></core-icon>
<h2>{{ 'addon.mod_forum.addtofavourites' | translate }}</h2>
</ion-item>
<ion-item text-wrap (click)="toggleFavouriteState(false)" *ngIf="discussion.canfavourite && discussion.starred">
<core-icon name="fa-star" item-start [slash]="true"></core-icon>
<h2>{{ 'addon.mod_forum.removefromfavourites' | translate }}</h2>
</ion-item>

View File

@ -0,0 +1,145 @@
// (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 { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites';
import { AddonModForumProvider } from '../../providers/forum';
/**
* This component is meant to display a popover with the discussion options.
*/
@Component({
selector: 'addon-forum-discussion-options-menu',
templateUrl: 'addon-forum-discussion-options-menu.html'
})
export class AddonForumDiscussionOptionsMenuComponent implements OnInit {
discussion: any; // The discussion.
forumId: number; // The forum Id.
cmId: number; // The component module Id.
canPin = false;
constructor(navParams: NavParams,
protected viewCtrl: ViewController,
protected forumProvider: AddonModForumProvider,
protected domUtils: CoreDomUtilsProvider,
protected eventsProvider: CoreEventsProvider,
protected sitesProvider: CoreSitesProvider) {
this.discussion = navParams.get('discussion');
this.forumId = navParams.get('forumId');
this.cmId = navParams.get('cmId');
}
/**
* Component being initialized.
*/
ngOnInit(): void {
if (this.forumProvider.isSetPinStateAvailableForSite()) {
// Use the canAddDiscussion WS to check if the user can pin discussions.
this.forumProvider.canAddDiscussionToAll(this.forumId).then((response) => {
this.canPin = !!response.canpindiscussions;
}).catch(() => {
this.canPin = false;
});
} else {
this.canPin = false;
}
}
/**
* Lock or unlock the discussion.
*
* @param locked True to lock the discussion, false to unlock.
*/
setLockState(locked: boolean): void {
const modal = this.domUtils.showModalLoading('core.sending', true);
this.forumProvider.setLockState(this.forumId, this.discussion.discussion, locked).then((response) => {
this.viewCtrl.dismiss({action: 'lock', value: locked});
const data = {
forumId: this.forumId,
discussionId: this.discussion.discussion,
cmId: this.cmId,
locked: response.locked
};
this.eventsProvider.trigger(AddonModForumProvider.CHANGE_DISCUSSION_EVENT, data, this.sitesProvider.getCurrentSiteId());
this.domUtils.showToast('addon.mod_forum.lockupdated', true);
}).catch((error) => {
this.domUtils.showErrorModal(error);
this.viewCtrl.dismiss();
}).finally(() => {
modal.dismiss();
});
}
/**
* Pin or unpin the discussion.
*
* @param pinned True to pin the discussion, false to unpin it.
*/
setPinState(pinned: boolean): void {
const modal = this.domUtils.showModalLoading('core.sending', true);
this.forumProvider.setPinState(this.discussion.discussion, pinned).then(() => {
this.viewCtrl.dismiss({action: 'pin', value: pinned});
const data = {
forumId: this.forumId,
discussionId: this.discussion.discussion,
cmId: this.cmId,
pinned: pinned
};
this.eventsProvider.trigger(AddonModForumProvider.CHANGE_DISCUSSION_EVENT, data, this.sitesProvider.getCurrentSiteId());
this.domUtils.showToast('addon.mod_forum.pinupdated', true);
}).catch((error) => {
this.domUtils.showErrorModal(error);
this.viewCtrl.dismiss();
}).finally(() => {
modal.dismiss();
});
}
/**
* Star or unstar the discussion.
*
* @param starred True to star the discussion, false to unstar it.
*/
toggleFavouriteState(starred: boolean): void {
const modal = this.domUtils.showModalLoading('core.sending', true);
this.forumProvider.toggleFavouriteState(this.discussion.discussion, starred).then(() => {
this.viewCtrl.dismiss({action: 'star', value: starred});
const data = {
forumId: this.forumId,
discussionId: this.discussion.discussion,
cmId: this.cmId,
starred: starred
};
this.eventsProvider.trigger(AddonModForumProvider.CHANGE_DISCUSSION_EVENT, data, this.sitesProvider.getCurrentSiteId());
this.domUtils.showToast('addon.mod_forum.favouriteupdated', true);
}).catch((error) => {
this.domUtils.showErrorModal(error);
this.viewCtrl.dismiss();
}).finally(() => {
modal.dismiss();
});
}
}

View File

@ -65,7 +65,7 @@
<core-icon name="fa-star" class="addon-forum-star" *ngIf="!discussion.pinned && discussion.starred"></core-icon> <core-icon name="fa-star" class="addon-forum-star" *ngIf="!discussion.pinned && discussion.starred"></core-icon>
<core-format-text [text]="discussion.subject" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"></core-format-text> <core-format-text [text]="discussion.subject" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"></core-format-text>
</h2> </h2>
<button ion-button icon-only clear color="dark" (click)="showOptionsMenu($event, discussion)"> <button ion-button icon-only clear color="dark" (click)="showOptionsMenu($event, discussion)" *ngIf="canPin || discussion.canlock || discussion.canfavourite">
<core-icon name="more"></core-icon> <core-icon name="more"></core-icon>
</button> </button>
</div> </div>
@ -74,18 +74,18 @@
<div class="addon-mod-forum-discussion-author"> <div class="addon-mod-forum-discussion-author">
<h3>{{discussion.userfullname}}</h3> <h3>{{discussion.userfullname}}</h3>
<p *ngIf="discussion.groupname"><ion-icon name="people"></ion-icon> {{ discussion.groupname }}</p> <p *ngIf="discussion.groupname"><ion-icon name="people"></ion-icon> {{ discussion.groupname }}</p>
<p>{{discussion.created | coreFormatDate: "strftimedatetimeshort"}}</p> <p>{{discussion.created * 1000 | coreFormatDate: "strftimerecentfull"}}</p>
</div> </div>
</div> </div>
<ion-row text-center class="addon-mod-forum-discussion-more-info"> <ion-row text-center class="addon-mod-forum-discussion-more-info">
<ion-col> <ion-col text-start>
<ion-note> <ion-note>
<ion-icon name="time"></ion-icon> {{ 'addon.mod_forum.lastpost' | translate }} <ion-icon name="time"></ion-icon> {{ 'addon.mod_forum.lastpost' | translate }}
<ng-container *ngIf="discussion.timemodified > discussion.created">{{discussion.timemodified | coreTimeAgo}}</ng-container> <ng-container *ngIf="discussion.timemodified > discussion.created">{{discussion.timemodified | coreTimeAgo}}</ng-container>
<ng-container *ngIf="discussion.timemodified <= discussion.created">{{discussion.created | coreTimeAgo}}</ng-container> <ng-container *ngIf="discussion.timemodified <= discussion.created">{{discussion.created | coreTimeAgo}}</ng-container>
</ion-note> </ion-note>
</ion-col> </ion-col>
<ion-col> <ion-col text-end>
<ion-note> <ion-note>
<ion-icon name="chatboxes"></ion-icon> {{ 'addon.mod_forum.numreplies' | translate:{numreplies: discussion.numreplies} }} <ion-icon name="chatboxes"></ion-icon> {{ 'addon.mod_forum.numreplies' | translate:{numreplies: discussion.numreplies} }}
<ion-badge text-center *ngIf="discussion.numunread" [attr.aria-label]="'addon.mod_forum.unreadpostsnumber' | translate:{ '$a' : discussion.numunread}">{{ discussion.numunread }}</ion-badge> <ion-badge text-center *ngIf="discussion.numunread" [attr.aria-label]="'addon.mod_forum.unreadpostsnumber' | translate:{ '$a' : discussion.numunread}">{{ discussion.numunread }}</ion-badge>

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import { Component, Optional, Injector, ViewChild } from '@angular/core'; import { Component, Optional, Injector, ViewChild } from '@angular/core';
import { Content, ModalController, NavController } from 'ionic-angular'; import { Content, ModalController, NavController, PopoverController } from 'ionic-angular';
import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component'; import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component';
import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate';
@ -27,6 +27,7 @@ import { AddonModForumHelperProvider } from '../../providers/helper';
import { AddonModForumOfflineProvider } from '../../providers/offline'; import { AddonModForumOfflineProvider } from '../../providers/offline';
import { AddonModForumSyncProvider } from '../../providers/sync'; import { AddonModForumSyncProvider } from '../../providers/sync';
import { AddonModForumPrefetchHandler } from '../../providers/prefetch-handler'; import { AddonModForumPrefetchHandler } from '../../providers/prefetch-handler';
import { AddonForumDiscussionOptionsMenuComponent } from '../discussion-options-menu/discussion-options-menu';
/** /**
* Component that displays a forum entry page. * Component that displays a forum entry page.
@ -61,6 +62,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
protected page = 0; protected page = 0;
protected trackPosts = false; protected trackPosts = false;
protected usesGroups = false; protected usesGroups = false;
protected canPin = false;
protected syncManualObserver: any; // It will observe the sync manual event. protected syncManualObserver: any; // It will observe the sync manual event.
protected replyObserver: any; protected replyObserver: any;
protected newDiscObserver: any; protected newDiscObserver: any;
@ -83,7 +85,8 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
protected forumSync: AddonModForumSyncProvider, protected forumSync: AddonModForumSyncProvider,
protected prefetchDelegate: CoreCourseModulePrefetchDelegate, protected prefetchDelegate: CoreCourseModulePrefetchDelegate,
protected prefetchHandler: AddonModForumPrefetchHandler, protected prefetchHandler: AddonModForumPrefetchHandler,
protected ratingOffline: CoreRatingOfflineProvider) { protected ratingOffline: CoreRatingOfflineProvider,
protected popoverCtrl: PopoverController) {
super(injector); super(injector);
this.sortingAvailable = this.forumProvider.isDiscussionListSortingAvailable(); this.sortingAvailable = this.forumProvider.isDiscussionListSortingAvailable();
@ -106,8 +109,30 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
this.eventReceived.bind(this, true)); this.eventReceived.bind(this, true));
this.replyObserver = this.eventsProvider.on(AddonModForumProvider.REPLY_DISCUSSION_EVENT, this.replyObserver = this.eventsProvider.on(AddonModForumProvider.REPLY_DISCUSSION_EVENT,
this.eventReceived.bind(this, false)); this.eventReceived.bind(this, false));
this.changeDiscObserver = this.eventsProvider.on(AddonModForumProvider.CHANGE_DISCUSSION_EVENT, this.changeDiscObserver = this.eventsProvider.on(AddonModForumProvider.CHANGE_DISCUSSION_EVENT, (data) => {
this.eventReceived.bind(this, false)); 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 (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;
}
}
}
});
});
// Select the current opened discussion. // Select the current opened discussion.
this.viewDiscObserver = this.eventsProvider.on(AddonModForumProvider.VIEW_DISCUSSION_EVENT, (data) => { this.viewDiscObserver = this.eventsProvider.on(AddonModForumProvider.VIEW_DISCUSSION_EVENT, (data) => {
@ -211,18 +236,30 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
}); });
} }
}).then(() => { }).then(() => {
return Promise.all([ const promises = [];
// Check if the activity uses groups. // Check if the activity uses groups.
this.groupsProvider.getActivityGroupMode(this.forum.cmid).then((mode) => { promises.push(this.groupsProvider.getActivityGroupMode(this.forum.cmid).then((mode) => {
this.usesGroups = (mode === CoreGroupsProvider.SEPARATEGROUPS || mode === CoreGroupsProvider.VISIBLEGROUPS); this.usesGroups = (mode === CoreGroupsProvider.SEPARATEGROUPS || mode === CoreGroupsProvider.VISIBLEGROUPS);
}), }));
this.forumProvider.getAccessInformation(this.forum.id).then((accessInfo) => { promises.push(this.forumProvider.getAccessInformation(this.forum.id).then((accessInfo) => {
// Disallow adding discussions if cut-off date is reached and the user has not the capability to override it. // Disallow adding discussions if cut-off date is reached and the user has not the capability to override it.
// Just in case the forum was fetched from WS when the cut-off date was not reached but it is now. // Just in case the forum was fetched from WS when the cut-off date was not reached but it is now.
const cutoffDateReached = this.forumHelper.isCutoffDateReached(this.forum) && !accessInfo.cancanoverridecutoff; const cutoffDateReached = this.forumHelper.isCutoffDateReached(this.forum) && !accessInfo.cancanoverridecutoff;
this.canAddDiscussion = this.forum.cancreatediscussions && !cutoffDateReached; this.canAddDiscussion = this.forum.cancreatediscussions && !cutoffDateReached;
}), }));
]);
if (this.forumProvider.isSetPinStateAvailableForSite()) {
// Use the canAddDiscussion WS to check if the user can pin discussions.
promises.push(this.forumProvider.canAddDiscussionToAll(this.forum.id).then((response) => {
this.canPin = !!response.canpindiscussions;
}).catch(() => {
this.canPin = false;
}));
} else {
this.canPin = false;
}
return Promise.all(promises);
})); }));
promises.push(this.fetchSortOrderPreference()); promises.push(this.fetchSortOrderPreference());
@ -559,6 +596,42 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
this.sortOrderSelectorExpanded = true; this.sortOrderSelectorExpanded = true;
} }
/**
* Show the context menu.
*
* @param e Click Event.
*/
showOptionsMenu(e: Event, discussion: any): void {
e.preventDefault();
e.stopPropagation();
const popover = this.popoverCtrl.create(AddonForumDiscussionOptionsMenuComponent, {
discussion: discussion,
forumId: this.forum.id,
cmId: this.module.id
});
popover.onDidDismiss((data) => {
if (data && data.action) {
switch (data.action) {
case 'lock':
discussion.locked = data.value;
break;
case 'pin':
discussion.pinned = data.value;
break;
case 'star':
discussion.starred = data.value;
break;
default:
break;
}
}
});
popover.present({
ev: e
});
}
/** /**
* Component being destroyed. * Component being destroyed.
*/ */

View File

@ -6,19 +6,16 @@
<core-icon name="fa-star" class="addon-forum-star" *ngIf="post.parent == 0 && !post.pinned && post.starred"></core-icon> <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> <core-format-text [text]="post.subject" contextLevel="module" [contextInstanceId]="forum && forum.cmid" [courseId]="courseId"></core-format-text>
</h2> </h2>
<ion-badge float-end padding-left text-end *ngIf="trackPosts && !post.postread" [attr.aria-label]="'addon.mod_forum.unread' | translate"> <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> <core-icon name="fa-circle" color="primary"></core-icon>
</ion-badge> </ion-note>
<button ion-button icon-only clear color="dark" (click)="showCourseOptionsMenu($event)">
<core-icon name="more"></core-icon>
</button>
</div> </div>
<div class="addon-mod-forum-post-info"> <div class="addon-mod-forum-post-info">
<ion-avatar core-user-avatar [user]="post" item-start [courseId]="courseId"></ion-avatar> <ion-avatar core-user-avatar [user]="post" item-start [courseId]="courseId"></ion-avatar>
<div class="addon-mod-forum-post-author"> <div class="addon-mod-forum-post-author">
<h3>{{post.userfullname}}</h3> <h3>{{post.userfullname}}</h3>
<p *ngIf="post.groupname"><ion-icon name="people"></ion-icon> {{ post.groupname }}</p> <p *ngIf="post.groupname"><ion-icon name="people"></ion-icon> {{ post.groupname }}</p>
<p *ngIf="post.modified">{{post.modified | coreFormatDate: "strftimedatetimeshort"}}</p> <p *ngIf="post.modified">{{post.modified * 1000 | coreFormatDate: "strftimerecentfull"}}</p>
<p *ngIf="!post.modified"><ion-icon name="time"></ion-icon> {{ 'core.notsent' | translate }}</p> <p *ngIf="!post.modified"><ion-icon name="time"></ion-icon> {{ 'core.notsent' | translate }}</p>
</div> </div>
</div> </div>

View File

@ -89,6 +89,7 @@ export class AddonModForumDiscussionPage implements OnDestroy {
hasOfflineRatings: boolean; hasOfflineRatings: boolean;
protected ratingOfflineObserver: any; protected ratingOfflineObserver: any;
protected ratingSyncObserver: any; protected ratingSyncObserver: any;
protected changeDiscObserver: any;
constructor(navParams: NavParams, constructor(navParams: NavParams,
network: Network, network: Network,
@ -183,6 +184,20 @@ export class AddonModForumDiscussionPage implements OnDestroy {
this.hasOfflineRatings = false; this.hasOfflineRatings = false;
} }
}); });
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;
}
});
});
} }
/** /**
@ -610,6 +625,7 @@ export class AddonModForumDiscussionPage implements OnDestroy {
this.syncManualObserver && this.syncManualObserver.off(); this.syncManualObserver && this.syncManualObserver.off();
this.ratingOfflineObserver && this.ratingOfflineObserver.off(); this.ratingOfflineObserver && this.ratingOfflineObserver.off();
this.ratingSyncObserver && this.ratingSyncObserver.off(); this.ratingSyncObserver && this.ratingSyncObserver.off();
this.changeDiscObserver && this.changeDiscObserver.off();
} }
/** /**

View File

@ -35,7 +35,7 @@ export class AddonModForumProvider {
static NEW_DISCUSSION_EVENT = 'addon_mod_forum_new_discussion'; static NEW_DISCUSSION_EVENT = 'addon_mod_forum_new_discussion';
static REPLY_DISCUSSION_EVENT = 'addon_mod_forum_reply_discussion'; static REPLY_DISCUSSION_EVENT = 'addon_mod_forum_reply_discussion';
static VIEW_DISCUSSION_EVENT = 'addon_mod_forum_view_discussion'; static VIEW_DISCUSSION_EVENT = 'addon_mod_forum_view_discussion';
static CHANGE_DISCUSSION_EVENT = 'addon_mod_forum_lock_discussion'; static CHANGE_DISCUSSION_EVENT = 'addon_mod_forum_change_discussion_status';
static MARK_READ_EVENT = 'addon_mod_forum_mark_read'; static MARK_READ_EVENT = 'addon_mod_forum_mark_read';
static PREFERENCE_SORTORDER = 'forum_discussionlistsortorder'; static PREFERENCE_SORTORDER = 'forum_discussionlistsortorder';