Vmeda.Online/src/addons/mod/forum/pages/discussion/discussion.ts

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;
}
}