902 lines
33 KiB
TypeScript
902 lines
33 KiB
TypeScript
// (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 { ContextLevel, CoreConstants } from '@/core/constants';
|
|
import { Component, OnDestroy, ViewChild, OnInit, AfterViewInit, ElementRef, Optional } from '@angular/core';
|
|
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
|
|
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
|
|
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
|
import { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
|
|
import { CoreRatingInfo, CoreRatingProvider } from '@features/rating/services/rating';
|
|
import { CoreRatingOffline } from '@features/rating/services/rating-offline';
|
|
import { CoreRatingSyncProvider } from '@features/rating/services/rating-sync';
|
|
import { CoreUser } from '@features/user/services/user';
|
|
import { CanLeave } from '@guards/can-leave';
|
|
import { IonContent } from '@ionic/angular';
|
|
import { CoreNetwork } from '@services/network';
|
|
import { CoreNavigator } from '@services/navigator';
|
|
import { CoreScreen } from '@services/screen';
|
|
import { CoreSites } from '@services/sites';
|
|
import { CoreDomUtils } from '@services/utils/dom';
|
|
import { CoreUtils } from '@services/utils/utils';
|
|
import { NgZone, Translate } from '@singletons';
|
|
import { CoreDom } from '@singletons/dom';
|
|
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
|
import { Subscription } from 'rxjs';
|
|
import { AddonModForumDiscussionsSource } from '../../classes/forum-discussions-source';
|
|
import { AddonModForumDiscussionsSwipeManager } from '../../classes/forum-discussions-swipe-manager';
|
|
import {
|
|
AddonModForum,
|
|
AddonModForumAccessInformation,
|
|
AddonModForumData,
|
|
AddonModForumDiscussion,
|
|
AddonModForumPost,
|
|
AddonModForumProvider,
|
|
AddonModForumPostFormData,
|
|
AddonModForumChangeDiscussionData,
|
|
AddonModForumReplyDiscussionData,
|
|
} from '../../services/forum';
|
|
import { AddonModForumHelper } from '../../services/forum-helper';
|
|
import { AddonModForumOffline } from '../../services/forum-offline';
|
|
import { AddonModForumSync, AddonModForumSyncProvider } from '../../services/forum-sync';
|
|
import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics';
|
|
|
|
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, CanLeave {
|
|
|
|
@ViewChild(IonContent) content!: IonContent;
|
|
|
|
courseId?: number;
|
|
discussionId!: number;
|
|
forum: Partial<AddonModForumData> = {};
|
|
accessInfo: AddonModForumAccessInformation = {};
|
|
discussion?: AddonModForumDiscussion;
|
|
discussions?: AddonModForumDiscussionDiscussionsSwipeManager;
|
|
startingPost?: Post;
|
|
posts: Post[] = [];
|
|
discussionLoaded = false;
|
|
postSubjects!: { [id: string]: string };
|
|
isOnline!: boolean;
|
|
postHasOffline!: boolean;
|
|
sort: SortType = 'nested';
|
|
trackPosts!: boolean;
|
|
formData: AddonModForumSharedPostFormData = {
|
|
replyingTo: 0,
|
|
isEditing: false,
|
|
subject: '',
|
|
message: null,
|
|
files: [],
|
|
isprivatereply: false,
|
|
};
|
|
|
|
originalData: Omit<AddonModForumPostFormData, 'id'> = {
|
|
subject: null,
|
|
message: null,
|
|
files: [],
|
|
isprivatereply: false,
|
|
};
|
|
|
|
refreshIcon = CoreConstants.ICON_LOADING;
|
|
syncIcon = CoreConstants.ICON_LOADING;
|
|
discussionStr = '';
|
|
component = AddonModForumProvider.COMPONENT;
|
|
cmId?: number;
|
|
canPin = false;
|
|
availabilityMessage: string | null = null;
|
|
showQAMessage = false;
|
|
leavingPage = false;
|
|
externalUrl?: string;
|
|
|
|
protected forumId?: number;
|
|
protected postId?: number;
|
|
protected parent?: number;
|
|
protected onlineObserver?: Subscription;
|
|
protected syncObserver?: CoreEventObserver;
|
|
protected syncManualObserver?: CoreEventObserver;
|
|
|
|
ratingInfo?: CoreRatingInfo;
|
|
hasOfflineRatings = false;
|
|
protected ratingOfflineObserver?: CoreEventObserver;
|
|
protected ratingSyncObserver?: CoreEventObserver;
|
|
protected changeDiscObserver?: CoreEventObserver;
|
|
|
|
constructor(
|
|
@Optional() protected splitView: CoreSplitViewComponent,
|
|
protected elementRef: ElementRef,
|
|
protected route: ActivatedRoute,
|
|
) {}
|
|
|
|
get isMobile(): boolean {
|
|
return CoreScreen.isMobile;
|
|
}
|
|
|
|
async ngOnInit(): Promise<void> {
|
|
try {
|
|
const routeData = this.route.snapshot.data;
|
|
this.courseId = CoreNavigator.getRouteNumberParam('courseId');
|
|
this.cmId = CoreNavigator.getRouteNumberParam('cmId');
|
|
this.forumId = CoreNavigator.getRouteNumberParam('forumId');
|
|
this.discussion = CoreNavigator.getRouteParam<AddonModForumDiscussion>('discussion');
|
|
this.discussionId = this.discussion
|
|
? this.discussion.discussion
|
|
: CoreNavigator.getRequiredRouteNumberParam('discussionId');
|
|
this.trackPosts = CoreNavigator.getRouteBooleanParam('trackPosts') || false;
|
|
this.postId = CoreNavigator.getRouteNumberParam('postId');
|
|
this.parent = CoreNavigator.getRouteNumberParam('parent');
|
|
|
|
if (this.courseId && this.cmId && (routeData.swipeEnabled ?? true)) {
|
|
this.discussions = new AddonModForumDiscussionDiscussionsSwipeManager(
|
|
CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
|
|
AddonModForumDiscussionsSource,
|
|
[this.courseId, this.cmId, routeData.discussionsPathPrefix ?? ''],
|
|
),
|
|
);
|
|
|
|
await this.discussions.start();
|
|
}
|
|
} catch (error) {
|
|
CoreDomUtils.showErrorModal(error);
|
|
|
|
this.goBack();
|
|
|
|
return;
|
|
}
|
|
|
|
const currentSite = CoreSites.getCurrentSite();
|
|
this.isOnline = CoreNetwork.isOnline();
|
|
this.externalUrl = currentSite && currentSite.shouldDisplayInformativeLinks() ?
|
|
currentSite.createSiteUrl('/mod/forum/discuss.php', { d: this.discussionId.toString() }) :
|
|
undefined;
|
|
this.onlineObserver = CoreNetwork.onChange().subscribe(() => {
|
|
// Execute the callback in the Angular zone, so change detection doesn't stop working.
|
|
NgZone.run(() => {
|
|
this.isOnline = CoreNetwork.isOnline();
|
|
});
|
|
});
|
|
|
|
this.discussionStr = Translate.instant('addon.mod_forum.discussion');
|
|
}
|
|
|
|
/**
|
|
* View loaded.
|
|
*/
|
|
async ngAfterViewInit(): Promise<void> {
|
|
this.sort = this.parent
|
|
? 'nested' // Force nested order.
|
|
: await this.getUserSort();
|
|
|
|
await this.fetchPosts(true, false, true);
|
|
|
|
const scrollTo = this.postId || this.parent;
|
|
if (scrollTo) {
|
|
// Scroll to the post.
|
|
CoreDom.scrollToElement(
|
|
this.elementRef.nativeElement,
|
|
'#addon-mod_forum-post-' + scrollTo,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* User entered the page that contains the component.
|
|
*/
|
|
async ionViewDidEnter(): Promise<void> {
|
|
if (this.syncObserver) {
|
|
// Already setup.
|
|
return;
|
|
}
|
|
|
|
// The discussion object was not passed as parameter.
|
|
if (!this.discussion) {
|
|
await this.loadDiscussion(this.discussionId, this.forumId, this.cmId);
|
|
}
|
|
|
|
const discussion = this.discussion;
|
|
|
|
// Refresh data if this discussion is synchronized automatically.
|
|
this.syncObserver = CoreEvents.on(AddonModForumSyncProvider.AUTO_SYNCED, data => {
|
|
if (data.forumId == this.forumId && this.discussionId == data.discussionId
|
|
&& data.userId == CoreSites.getCurrentSiteUserId()) {
|
|
// Refresh the data.
|
|
this.discussionLoaded = false;
|
|
this.refreshPosts();
|
|
}
|
|
}, CoreSites.getCurrentSiteId());
|
|
|
|
// Refresh data if this forum discussion is synchronized from discussions list.
|
|
this.syncManualObserver = CoreEvents.on(AddonModForumSyncProvider.MANUAL_SYNCED, data => {
|
|
if (data.source != 'discussion' && data.forumId == this.forumId &&
|
|
data.userId == CoreSites.getCurrentSiteUserId()) {
|
|
// Refresh the data.
|
|
this.discussionLoaded = false;
|
|
this.refreshPosts();
|
|
}
|
|
}, CoreSites.getCurrentSiteId());
|
|
|
|
// Invalidate discussion list if it was not read.
|
|
if (this.forumId && discussion && discussion.numunread > 0) {
|
|
AddonModForum.invalidateDiscussionsList(this.forumId);
|
|
}
|
|
|
|
// Listen for offline ratings saved and synced.
|
|
this.ratingOfflineObserver = CoreEvents.on(CoreRatingProvider.RATING_SAVED_EVENT, (data) => {
|
|
if (data.component == 'mod_forum' && data.ratingArea == 'post' && data.contextLevel == ContextLevel.MODULE &&
|
|
data.instanceId == this.cmId && data.itemSetId == this.discussionId) {
|
|
this.hasOfflineRatings = true;
|
|
}
|
|
});
|
|
|
|
this.ratingSyncObserver = CoreEvents.on(CoreRatingSyncProvider.SYNCED_EVENT, async (data) => {
|
|
if (data.component == 'mod_forum' && data.ratingArea == 'post' && data.contextLevel == ContextLevel.MODULE &&
|
|
data.instanceId == this.cmId && data.itemSetId == this.discussionId) {
|
|
this.hasOfflineRatings = false;
|
|
}
|
|
});
|
|
|
|
this.changeDiscObserver = CoreEvents.on(AddonModForumProvider.CHANGE_DISCUSSION_EVENT, data => {
|
|
if (discussion && this.forumId && (this.forumId === data.forumId || data.cmId === this.cmId)) {
|
|
AddonModForum.invalidateDiscussionsList(this.forumId).finally(() => {
|
|
if (data.locked !== undefined) {
|
|
discussion.locked = data.locked;
|
|
}
|
|
if (data.pinned !== undefined) {
|
|
discussion.pinned = data.pinned;
|
|
}
|
|
if (data.starred !== undefined) {
|
|
discussion.starred = data.starred;
|
|
}
|
|
|
|
if (data.deleted !== undefined && data.deleted) {
|
|
if (!data.post?.parentid) {
|
|
this.goBack();
|
|
} else {
|
|
this.discussionLoaded = false;
|
|
this.refreshPosts();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Check if we can leave the page or not.
|
|
*
|
|
* @returns Resolved if we can leave it, rejected if not.
|
|
*/
|
|
async canLeave(): Promise<boolean> {
|
|
if (AddonModForumHelper.hasPostDataChanged(this.formData, this.originalData)) {
|
|
// Show confirmation if some data has been modified.
|
|
await CoreDomUtils.showConfirm(Translate.instant('core.confirmcanceledit'));
|
|
}
|
|
|
|
// Delete the local files from the tmp folder.
|
|
CoreFileUploader.clearTmpFiles(this.formData.files);
|
|
|
|
this.leavingPage = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Helper function to go back.
|
|
*/
|
|
protected goBack(): void {
|
|
if (this.leavingPage) {
|
|
return;
|
|
}
|
|
|
|
if (this.splitView?.outletActivated) {
|
|
CoreNavigator.navigate('../');
|
|
} else {
|
|
CoreNavigator.back();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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();
|
|
this.discussions && this.discussions.destroy();
|
|
}
|
|
|
|
/**
|
|
* Get sort type configured by the current user.
|
|
*
|
|
* @returns Promise resolved with the sort type.
|
|
*/
|
|
protected async getUserSort(): Promise<SortType> {
|
|
try {
|
|
const value = await CoreSites.getRequiredCurrentSite().getLocalSiteConfig<SortType>('AddonModForumDiscussionSort');
|
|
|
|
return value;
|
|
} catch (error) {
|
|
try {
|
|
const value = await CoreUser.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 {
|
|
// Ignore errors.
|
|
}
|
|
}
|
|
|
|
return 'flat-oldest';
|
|
}
|
|
|
|
/**
|
|
* Convenience function to get the forum.
|
|
*
|
|
* @returns Promise resolved with the forum.
|
|
*/
|
|
protected fetchForum(): Promise<AddonModForumData> {
|
|
if (this.courseId && this.cmId) {
|
|
return AddonModForum.getForum(this.courseId, this.cmId);
|
|
}
|
|
|
|
if (this.courseId && this.forumId) {
|
|
return AddonModForum.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.
|
|
* @returns 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.ignoreErrors(this.syncDiscussion(!!showErrors));
|
|
}
|
|
|
|
const response = await AddonModForum.getDiscussionPosts(this.discussionId, { cmId: this.cmId });
|
|
const replies = await AddonModForumOffline.getDiscussionReplies(this.discussionId);
|
|
this.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.
|
|
reply.parentid && (onlinePostsMap[reply.parentid].capabilities.reply = false);
|
|
|
|
return;
|
|
}),
|
|
);
|
|
});
|
|
|
|
await Promise.all(convertPromises);
|
|
|
|
// Convert back to array.
|
|
onlinePosts = CoreUtils.objectToArray(onlinePostsMap);
|
|
|
|
let posts = offlineReplies.concat(onlinePosts);
|
|
|
|
this.startingPost = AddonModForum.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.sortDiscussionPosts(posts, 'ASC');
|
|
|
|
const rootId = this.startingPost ? this.startingPost.id : (this.discussion ? this.discussion.id : 0);
|
|
posts = CoreUtils.formatTree(posts, 'parentid', 'id', rootId);
|
|
} else {
|
|
// Set default reply subject.
|
|
const direction = this.sort == 'flat-newest' ? 'DESC' : 'ASC';
|
|
AddonModForum.sortDiscussionPosts(posts, direction);
|
|
}
|
|
|
|
try {
|
|
// Now try to get the forum.
|
|
const forum = await this.fetchForum();
|
|
// "forum.istracked" is more reliable than "trackPosts".
|
|
if (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.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.isCutoffDateReached(forum) && !accessInfo.cancanoverridecutoff) {
|
|
posts.forEach((post) => {
|
|
post.capabilities.reply = false;
|
|
});
|
|
}
|
|
|
|
// Show Q&A message if user hasn't posted.
|
|
const currentUserId = CoreSites.getCurrentSiteUserId();
|
|
this.showQAMessage = forum.type === 'qanda' && !accessInfo.canviewqandawithoutposting &&
|
|
!posts.some(post => post.author.id === currentUserId);
|
|
|
|
return;
|
|
}),
|
|
);
|
|
|
|
// The discussion object was not passed as parameter and there is no starting post.
|
|
if (!this.discussion) {
|
|
promises.push(this.loadDiscussion(this.discussionId, this.forumId, this.cmId));
|
|
}
|
|
|
|
await Promise.all(promises);
|
|
} catch {
|
|
// 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.postSubjects = this.getAllPosts().reduce(
|
|
(postSubjects, post) => {
|
|
postSubjects[post.id] = post.subject;
|
|
|
|
return postSubjects;
|
|
},
|
|
this.startingPost
|
|
? { [this.startingPost.id]: this.startingPost.subject }
|
|
: {},
|
|
);
|
|
|
|
if (AddonModForum.isSetPinStateAvailableForSite() && this.forumId) {
|
|
// Use the canAddDiscussion WS to check if the user can pin discussions.
|
|
try {
|
|
const response = await AddonModForum.canAddDiscussionToAll(this.forumId, { cmId: this.cmId });
|
|
|
|
this.canPin = !!response.canpindiscussions;
|
|
} catch (error) {
|
|
this.canPin = false;
|
|
}
|
|
} else {
|
|
this.canPin = false;
|
|
}
|
|
|
|
this.hasOfflineRatings =
|
|
await CoreRatingOffline.hasRatings('mod_forum', 'post', ContextLevel.MODULE, this.cmId, this.discussionId);
|
|
} catch (error) {
|
|
CoreDomUtils.showErrorModal(error);
|
|
} finally {
|
|
this.discussionLoaded = true;
|
|
this.refreshIcon = CoreConstants.ICON_REFRESH;
|
|
this.syncIcon = CoreConstants.ICON_SYNC;
|
|
|
|
if (forceMarkAsRead || (hasUnreadPosts && this.trackPosts)) {
|
|
// Add log in Moodle and mark unread posts as readed.
|
|
this.logDiscussionView(forceMarkAsRead);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convenience function to load discussion.
|
|
*
|
|
* @param discussionId Discussion ID.
|
|
* @param forumId Forum ID.
|
|
* @param cmId Forum cmid.
|
|
* @returns Promise resolved when done.
|
|
*/
|
|
protected async loadDiscussion(discussionId: number, forumId?: number, cmId?: number): Promise<void> {
|
|
// Fetch the discussion if not passed as parameter.
|
|
if (this.discussion || !forumId || ! cmId) {
|
|
return;
|
|
}
|
|
|
|
this.discussion = await AddonModForumHelper.getDiscussionById(forumId, cmId, discussionId);
|
|
this.discussionId = this.discussion.discussion;
|
|
}
|
|
|
|
/**
|
|
* Tries to synchronize the posts discussion.
|
|
*
|
|
* @param showErrors Whether to show errors in a modal.
|
|
* @returns 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.showAlert(undefined, result.warnings[0]);
|
|
}
|
|
|
|
if (result && result.updated && this.forumId) {
|
|
// Sync successful, send event.
|
|
CoreEvents.trigger(AddonModForumSyncProvider.MANUAL_SYNCED, {
|
|
forumId: this.forumId,
|
|
userId: CoreSites.getCurrentSiteUserId(),
|
|
source: 'discussion',
|
|
}, CoreSites.getCurrentSiteId());
|
|
}
|
|
|
|
return;
|
|
}),
|
|
);
|
|
|
|
promises.push(
|
|
AddonModForumSync.instance
|
|
.syncRatings(this.cmId, this.discussionId)
|
|
.then((result) => {
|
|
if (result.warnings && result.warnings.length) {
|
|
CoreDomUtils.showAlert(undefined, result.warnings[0]);
|
|
}
|
|
|
|
return;
|
|
}),
|
|
);
|
|
|
|
try {
|
|
await Promise.all(promises);
|
|
} catch (error) {
|
|
if (showErrors) {
|
|
CoreDomUtils.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.
|
|
* @returns Promise resolved when done.
|
|
*/
|
|
async doRefresh(refresher?: HTMLIonRefresherElement | null, done?: () => void, showErrors: boolean = false): Promise<void> {
|
|
if (this.discussionLoaded) {
|
|
await this.refreshPosts(true, showErrors).finally(() => {
|
|
refresher?.complete();
|
|
done && done();
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Refresh posts.
|
|
*
|
|
* @param sync Whether to try to synchronize the discussion.
|
|
* @param showErrors Whether to show errors in a modal.
|
|
* @returns Promise resolved when done.
|
|
*/
|
|
async refreshPosts(sync?: boolean, showErrors?: boolean): Promise<void> {
|
|
this.content.scrollToTop();
|
|
this.refreshIcon = CoreConstants.ICON_LOADING;
|
|
this.syncIcon = CoreConstants.ICON_LOADING;
|
|
|
|
const promises: Promise<void>[] = [];
|
|
|
|
this.courseId && promises.push(AddonModForum.invalidateForumData(this.courseId));
|
|
promises.push(AddonModForum.invalidateDiscussionPosts(this.discussionId, this.forumId));
|
|
this.forumId && promises.push(AddonModForum.invalidateAccessInformation(this.forumId));
|
|
this.forumId && promises.push(AddonModForum.invalidateCanAddDiscussion(this.forumId));
|
|
|
|
await CoreUtils.ignoreErrors(CoreUtils.allPromises(promises));
|
|
|
|
await this.fetchPosts(sync, showErrors);
|
|
}
|
|
|
|
/**
|
|
* Function to change posts sorting
|
|
*
|
|
* @param type Sort type.
|
|
* @returns Promised resolved when done.
|
|
*/
|
|
changeSort(type: SortType): Promise<void> {
|
|
this.discussionLoaded = false;
|
|
this.sort = type;
|
|
CoreSites.getRequiredCurrentSite().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> {
|
|
if (!this.discussion || !this.forumId || !this.cmId) {
|
|
return;
|
|
}
|
|
|
|
const modal = await CoreDomUtils.showModalLoading('core.sending', true);
|
|
|
|
try {
|
|
const response = await AddonModForum.setLockState(this.forumId, this.discussionId, locked);
|
|
this.discussion.locked = response.locked;
|
|
|
|
const data: AddonModForumChangeDiscussionData = {
|
|
forumId: this.forumId,
|
|
discussionId: this.discussionId,
|
|
cmId: this.cmId,
|
|
locked: this.discussion.locked,
|
|
};
|
|
CoreEvents.trigger(AddonModForumProvider.CHANGE_DISCUSSION_EVENT, data, CoreSites.getCurrentSiteId());
|
|
|
|
CoreDomUtils.showToast('addon.mod_forum.lockupdated', true);
|
|
} catch (error) {
|
|
CoreDomUtils.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> {
|
|
if (!this.discussion || !this.forumId || !this.cmId) {
|
|
return;
|
|
}
|
|
|
|
const modal = await CoreDomUtils.showModalLoading('core.sending', true);
|
|
|
|
try {
|
|
await AddonModForum.setPinState(this.discussionId, pinned);
|
|
|
|
this.discussion.pinned = pinned;
|
|
|
|
const data: AddonModForumChangeDiscussionData = {
|
|
forumId: this.forumId,
|
|
discussionId: this.discussionId,
|
|
cmId: this.cmId,
|
|
pinned: this.discussion.pinned,
|
|
};
|
|
CoreEvents.trigger(AddonModForumProvider.CHANGE_DISCUSSION_EVENT, data, CoreSites.getCurrentSiteId());
|
|
|
|
CoreDomUtils.showToast('addon.mod_forum.pinupdated', true);
|
|
} catch (error) {
|
|
CoreDomUtils.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> {
|
|
if (!this.discussion || !this.forumId || !this.cmId) {
|
|
return;
|
|
}
|
|
|
|
const modal = await CoreDomUtils.showModalLoading('core.sending', true);
|
|
|
|
try {
|
|
await AddonModForum.toggleFavouriteState(this.discussionId, starred);
|
|
|
|
this.discussion.starred = starred;
|
|
|
|
const data: AddonModForumChangeDiscussionData = {
|
|
forumId: this.forumId,
|
|
discussionId: this.discussionId,
|
|
cmId: this.cmId,
|
|
starred: this.discussion.starred,
|
|
};
|
|
CoreEvents.trigger(AddonModForumProvider.CHANGE_DISCUSSION_EVENT, data, CoreSites.getCurrentSiteId());
|
|
|
|
CoreDomUtils.showToast('addon.mod_forum.favouriteupdated', true);
|
|
} catch (error) {
|
|
CoreDomUtils.showErrorModal(error);
|
|
} finally {
|
|
modal.dismiss();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* New post added.
|
|
*/
|
|
postListChanged(): void {
|
|
if (!this.forumId || !this.cmId) {
|
|
return;
|
|
}
|
|
|
|
// Trigger an event to notify a new reply.
|
|
const data: AddonModForumReplyDiscussionData = {
|
|
forumId: this.forumId,
|
|
discussionId: this.discussionId,
|
|
cmId: this.cmId,
|
|
};
|
|
CoreEvents.trigger(AddonModForumProvider.REPLY_DISCUSSION_EVENT, data, CoreSites.getCurrentSiteId());
|
|
|
|
this.discussionLoaded = false;
|
|
this.refreshPosts().finally(() => {
|
|
this.discussionLoaded = true;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get all the posts contained in the discussion.
|
|
*
|
|
* @returns Array containing all the posts of the discussion.
|
|
*/
|
|
protected getAllPosts(): Post[] {
|
|
const allPosts = this.posts.map(post => this.flattenPostHierarchy(post));
|
|
|
|
return allPosts.flat();
|
|
}
|
|
|
|
/**
|
|
* Flatten a post's hierarchy into an array.
|
|
*
|
|
* @param parent Parent post.
|
|
* @returns 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;
|
|
}
|
|
|
|
/**
|
|
* Log discussion as viewed. This will also mark the posts as read.
|
|
*
|
|
* @param logAnalytics Whether to log analytics too or not.
|
|
*/
|
|
protected async logDiscussionView(logAnalytics = false): Promise<void> {
|
|
await CoreUtils.ignoreErrors(AddonModForum.logDiscussionView(this.discussionId, this.forumId || -1));
|
|
|
|
if (logAnalytics) {
|
|
CoreAnalytics.logEvent({
|
|
type: CoreAnalyticsEventType.VIEW_ITEM,
|
|
ws: 'mod_forum_view_forum_discussion',
|
|
name: this.startingPost?.subject ?? this.forum.name ?? '',
|
|
data: { id: this.discussionId, forumid: this.forumId, category: 'forum' },
|
|
url: `/mod/forum/discuss.php?d=${this.discussionId}` + (this.postId ? `#p${this.postId}` : ''),
|
|
});
|
|
}
|
|
|
|
if (!this.courseId || !this.cmId) {
|
|
return;
|
|
}
|
|
|
|
// Trigger mark read posts.
|
|
CoreEvents.trigger(AddonModForumProvider.MARK_READ_EVENT, {
|
|
courseId: this.courseId,
|
|
moduleId: this.cmId,
|
|
}, CoreSites.getCurrentSiteId());
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Reply data shared by post.
|
|
*/
|
|
export type AddonModForumSharedPostFormData = Omit<AddonModForumPostFormData, 'id'> & {
|
|
id?: number; // ID when editing an online reply.
|
|
syncId?: string; // Sync ID if some post has blocked synchronization.
|
|
};
|
|
|
|
/**
|
|
* Helper to manage swiping within a collection of discussions.
|
|
*/
|
|
class AddonModForumDiscussionDiscussionsSwipeManager extends AddonModForumDiscussionsSwipeManager {
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot | ActivatedRoute): string | null {
|
|
const snapshot = route instanceof ActivatedRouteSnapshot ? route : route.snapshot;
|
|
|
|
return this.getSource().DISCUSSIONS_PATH_PREFIX + snapshot.params.discussionId;
|
|
}
|
|
|
|
}
|