628 lines
23 KiB
TypeScript
628 lines
23 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 { Component, OnDestroy, ViewChild, ElementRef, OnInit, Optional } from '@angular/core';
|
|
import { FileEntry } from '@ionic-native/file/ngx';
|
|
import { FormControl } from '@angular/forms';
|
|
import { CoreEvents, CoreEventObserver } from '@singletons/events';
|
|
import { CoreGroup, CoreGroups, CoreGroupsProvider } from '@services/groups';
|
|
import { CoreNavigator } from '@services/navigator';
|
|
import {
|
|
AddonModForum,
|
|
AddonModForumAccessInformation,
|
|
AddonModForumCanAddDiscussion,
|
|
AddonModForumData,
|
|
AddonModForumProvider,
|
|
} from '@addons/mod/forum/services/forum';
|
|
import { CoreEditorRichTextEditorComponent } from '@features/editor/components/rich-text-editor/rich-text-editor';
|
|
import { AddonModForumSync, AddonModForumSyncProvider } from '@addons/mod/forum/services/forum-sync';
|
|
import { CoreSites } from '@services/sites';
|
|
import { CoreDomUtils } from '@services/utils/dom';
|
|
import { Translate } from '@singletons';
|
|
import { CoreSync } from '@services/sync';
|
|
import { AddonModForumDiscussionOptions, AddonModForumOffline } from '@addons/mod/forum/services/forum-offline';
|
|
import { CoreUtils } from '@services/utils/utils';
|
|
import { AddonModForumHelper } from '@addons/mod/forum/services/forum-helper';
|
|
import { IonRefresher } from '@ionic/angular';
|
|
import { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
|
|
import { CoreTextUtils } from '@services/utils/text';
|
|
import { CanLeave } from '@guards/can-leave';
|
|
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
|
import { CoreForms } from '@singletons/form';
|
|
|
|
type NewDiscussionData = {
|
|
subject: string;
|
|
message: string | null; // Null means empty or just white space.
|
|
postToAllGroups: boolean;
|
|
groupId: number;
|
|
subscribe: boolean;
|
|
pin: boolean;
|
|
files: FileEntry[];
|
|
};
|
|
|
|
/**
|
|
* Page that displays the new discussion form.
|
|
*/
|
|
@Component({
|
|
selector: 'page-addon-mod-forum-new-discussion',
|
|
templateUrl: 'new-discussion.html',
|
|
})
|
|
export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLeave {
|
|
|
|
@ViewChild('newDiscFormEl') formElement!: ElementRef;
|
|
@ViewChild(CoreEditorRichTextEditorComponent) messageEditor!: CoreEditorRichTextEditorComponent;
|
|
|
|
component = AddonModForumProvider.COMPONENT;
|
|
messageControl = new FormControl();
|
|
groupsLoaded = false;
|
|
showGroups = false;
|
|
hasOffline = false;
|
|
canCreateAttachments = true; // Assume we can by default.
|
|
canPin = false;
|
|
forum!: AddonModForumData;
|
|
showForm = false;
|
|
groups: CoreGroup[] = [];
|
|
groupIds: number[] = [];
|
|
newDiscussion: NewDiscussionData = {
|
|
subject: '',
|
|
message: null,
|
|
postToAllGroups: false,
|
|
groupId: 0,
|
|
subscribe: true,
|
|
pin: false,
|
|
files: [],
|
|
};
|
|
|
|
advanced = false; // Display all form fields.
|
|
accessInfo: AddonModForumAccessInformation = {};
|
|
courseId!: number;
|
|
|
|
protected cmId!: number;
|
|
protected forumId!: number;
|
|
protected timeCreated!: number;
|
|
protected syncId!: string;
|
|
protected syncObserver?: CoreEventObserver;
|
|
protected isDestroyed = false;
|
|
protected originalData?: Partial<NewDiscussionData>;
|
|
protected forceLeave = false;
|
|
|
|
constructor(@Optional() protected splitView: CoreSplitViewComponent) {}
|
|
|
|
/**
|
|
* Component being initialized.
|
|
*/
|
|
ngOnInit(): void {
|
|
try {
|
|
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
|
this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
|
|
this.forumId = CoreNavigator.getRequiredRouteNumberParam('forumId');
|
|
this.timeCreated = CoreNavigator.getRequiredRouteNumberParam('timeCreated');
|
|
} catch (error) {
|
|
CoreDomUtils.showErrorModal(error);
|
|
|
|
this.goBack();
|
|
|
|
return;
|
|
}
|
|
|
|
this.fetchDiscussionData().finally(() => {
|
|
this.groupsLoaded = true;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* User entered the page that contains the component.
|
|
*/
|
|
ionViewDidEnter(): void {
|
|
if (this.syncObserver) {
|
|
// Already setup.
|
|
return;
|
|
}
|
|
|
|
// Refresh data if this discussion is synchronized automatically.
|
|
this.syncObserver = CoreEvents.on(AddonModForumSyncProvider.AUTO_SYNCED, data => {
|
|
if (data.forumId == this.forumId && data.userId == CoreSites.getCurrentSiteUserId()) {
|
|
CoreDomUtils.showAlertTranslated('core.notice', 'core.contenteditingsynced');
|
|
this.returnToDiscussions();
|
|
}
|
|
}, CoreSites.getCurrentSiteId());
|
|
}
|
|
|
|
/**
|
|
* Fetch if forum uses groups and the groups it uses.
|
|
*
|
|
* @param refresh Whether we're refreshing data.
|
|
* @return Promise resolved when done.
|
|
*/
|
|
protected async fetchDiscussionData(refresh?: boolean): Promise<void> {
|
|
try {
|
|
const mode = await CoreGroups.getActivityGroupMode(this.cmId);
|
|
const promises: Promise<unknown>[] = [];
|
|
|
|
if (mode === CoreGroupsProvider.SEPARATEGROUPS || mode === CoreGroupsProvider.VISIBLEGROUPS) {
|
|
promises.push(
|
|
CoreGroups.instance
|
|
.getActivityAllowedGroups(this.cmId)
|
|
.then((result) => {
|
|
let promise;
|
|
if (mode === CoreGroupsProvider.VISIBLEGROUPS) {
|
|
// We need to check which of the returned groups the user can post to.
|
|
promise = this.validateVisibleGroups(result.groups);
|
|
} else {
|
|
// WS already filters groups, no need to do it ourselves. Add "All participants" if needed.
|
|
promise = this.addAllParticipantsOption(result.groups, true);
|
|
}
|
|
|
|
// eslint-disable-next-line promise/no-nesting
|
|
return promise.then((forumGroups) => {
|
|
if (forumGroups.length > 0) {
|
|
this.groups = forumGroups;
|
|
this.groupIds = forumGroups.map((group) => group.id).filter((id) => id > 0);
|
|
// Do not override group id.
|
|
this.newDiscussion.groupId = this.newDiscussion.groupId || forumGroups[0].id;
|
|
this.showGroups = true;
|
|
if (this.groupIds.length <= 1) {
|
|
this.newDiscussion.postToAllGroups = false;
|
|
}
|
|
|
|
return;
|
|
} else {
|
|
const message = mode === CoreGroupsProvider.SEPARATEGROUPS ?
|
|
'addon.mod_forum.cannotadddiscussionall' : 'addon.mod_forum.cannotadddiscussion';
|
|
|
|
throw new Error(Translate.instant(message));
|
|
}
|
|
});
|
|
}),
|
|
);
|
|
} else {
|
|
this.showGroups = false;
|
|
this.newDiscussion.postToAllGroups = false;
|
|
|
|
// Use the canAddDiscussion WS to check if the user can add attachments and pin discussions.
|
|
promises.push(
|
|
CoreUtils.ignoreErrors(
|
|
AddonModForum.instance
|
|
.canAddDiscussionToAll(this.forumId, { cmId: this.cmId })
|
|
.then((response) => {
|
|
this.canPin = !!response.canpindiscussions;
|
|
this.canCreateAttachments = !!response.cancreateattachment;
|
|
|
|
return;
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Get forum.
|
|
promises.push(AddonModForum.getForum(this.courseId, this.cmId).then((forum) => this.forum = forum));
|
|
|
|
// Get access information.
|
|
promises.push(
|
|
AddonModForum.instance
|
|
.getAccessInformation(this.forumId, { cmId: this.cmId })
|
|
.then((accessInfo) => this.accessInfo = accessInfo),
|
|
);
|
|
|
|
await Promise.all(promises);
|
|
|
|
// If editing a discussion, get offline data.
|
|
if (this.timeCreated && !refresh) {
|
|
this.syncId = AddonModForumSync.getForumSyncId(this.forumId);
|
|
|
|
await AddonModForumSync.waitForSync(this.syncId).then(() => {
|
|
// Do not block if the scope is already destroyed.
|
|
if (!this.isDestroyed) {
|
|
CoreSync.blockOperation(AddonModForumProvider.COMPONENT, this.syncId);
|
|
}
|
|
|
|
// eslint-disable-next-line promise/no-nesting
|
|
return AddonModForumOffline.instance
|
|
.getNewDiscussion(this.forumId, this.timeCreated)
|
|
.then(async (discussion) => {
|
|
this.hasOffline = true;
|
|
discussion.options = discussion.options || {};
|
|
if (discussion.groupid == AddonModForumProvider.ALL_GROUPS) {
|
|
this.newDiscussion.groupId = this.groups[0].id;
|
|
this.newDiscussion.postToAllGroups = true;
|
|
} else {
|
|
this.newDiscussion.groupId = discussion.groupid;
|
|
this.newDiscussion.postToAllGroups = false;
|
|
}
|
|
this.newDiscussion.subject = discussion.subject;
|
|
this.newDiscussion.message = discussion.message;
|
|
this.newDiscussion.subscribe = !!discussion.options.discussionsubscribe;
|
|
this.newDiscussion.pin = !!discussion.options.discussionpinned;
|
|
this.messageControl.setValue(discussion.message);
|
|
|
|
// Treat offline attachments if any.
|
|
if (typeof discussion.options.attachmentsid === 'object' && discussion.options.attachmentsid.offline) {
|
|
const files = await AddonModForumHelper.getNewDiscussionStoredFiles(
|
|
this.forumId,
|
|
this.timeCreated,
|
|
);
|
|
|
|
this.newDiscussion.files = files;
|
|
}
|
|
|
|
// Show advanced fields by default if any of them has not the default value.
|
|
if (
|
|
!this.newDiscussion.subscribe ||
|
|
this.newDiscussion.pin ||
|
|
this.newDiscussion.files.length ||
|
|
this.groups.length > 0 && this.newDiscussion.groupId != this.groups[0].id ||
|
|
this.newDiscussion.postToAllGroups
|
|
) {
|
|
this.advanced = true;
|
|
}
|
|
|
|
return;
|
|
});
|
|
});
|
|
}
|
|
|
|
if (!this.originalData) {
|
|
// Initialize original data.
|
|
this.originalData = {
|
|
subject: this.newDiscussion.subject,
|
|
message: this.newDiscussion.message,
|
|
files: this.newDiscussion.files.slice(),
|
|
};
|
|
}
|
|
|
|
this.showForm = true;
|
|
} catch (error) {
|
|
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_forum.errorgetgroups', true);
|
|
|
|
this.showForm = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate which of the groups returned by getActivityAllowedGroups in visible groups should be shown to post to.
|
|
*
|
|
* @param forumGroups Forum groups.
|
|
* @return Promise resolved with the list of groups.
|
|
*/
|
|
protected async validateVisibleGroups(forumGroups: CoreGroup[]): Promise<CoreGroup[]> {
|
|
let response: AddonModForumCanAddDiscussion;
|
|
|
|
// We first check if the user can post to all the groups.
|
|
try {
|
|
response = await AddonModForum.canAddDiscussionToAll(this.forumId, { cmId: this.cmId });
|
|
} catch (error) {
|
|
// The call failed, let's assume he can't.
|
|
response = {
|
|
status: false,
|
|
canpindiscussions: false,
|
|
cancreateattachment: true,
|
|
};
|
|
}
|
|
|
|
this.canPin = !!response.canpindiscussions;
|
|
this.canCreateAttachments = !!response.cancreateattachment;
|
|
|
|
// The user can post to all groups, add the "All participants" option and return them all.
|
|
if (response.status) {
|
|
return this.addAllParticipantsOption(forumGroups, false);
|
|
}
|
|
|
|
// The user can't post to all groups, let's check which groups he can post to.
|
|
const promises: Promise<unknown>[] = [];
|
|
const filtered: CoreGroup[] = [];
|
|
|
|
forumGroups.forEach((group) => {
|
|
promises.push(
|
|
AddonModForum.instance
|
|
.canAddDiscussion(this.forumId, group.id, { cmId: this.cmId })
|
|
|
|
// The call failed, let's return true so the group is shown.
|
|
// If the user can't post to it an error will be shown when he tries to add the discussion.
|
|
.catch(() =>({ status: true }))
|
|
|
|
.then((response) => {
|
|
if (response.status) {
|
|
filtered.push(group);
|
|
}
|
|
|
|
return;
|
|
}),
|
|
);
|
|
});
|
|
|
|
await Promise.all(promises);
|
|
|
|
return filtered;
|
|
}
|
|
|
|
/**
|
|
* Filter forum groups, returning only those that are inside user groups.
|
|
*
|
|
* @param forumGroups Forum groups.
|
|
* @param userGroups User groups.
|
|
* @return Filtered groups.
|
|
*/
|
|
protected filterGroups(forumGroups: CoreGroup[], userGroups: CoreGroup[]): CoreGroup[] {
|
|
const userGroupsIds = userGroups.map(group => group.id);
|
|
|
|
return forumGroups.filter(forumGroup => userGroupsIds.indexOf(forumGroup.id) > -1);
|
|
}
|
|
|
|
/**
|
|
* Add the "All participants" option to a list of groups if the user can add a discussion to all participants.
|
|
*
|
|
* @param groups Groups.
|
|
* @param check True to check if the user can add a discussion to all participants.
|
|
* @return Promise resolved with the list of groups.
|
|
*/
|
|
protected addAllParticipantsOption(groups: CoreGroup[], check: boolean): Promise<CoreGroup[]> {
|
|
let promise: Promise<boolean>;
|
|
|
|
if (check) {
|
|
// We need to check if the user can add a discussion to all participants.
|
|
promise = AddonModForum.canAddDiscussionToAll(this.forumId, { cmId: this.cmId }).then((response) => {
|
|
this.canPin = !!response.canpindiscussions;
|
|
this.canCreateAttachments = !!response.cancreateattachment;
|
|
|
|
return response.status;
|
|
}).catch(() =>
|
|
// The call failed, let's assume he can't.
|
|
false);
|
|
} else {
|
|
// No need to check, assume the user can.
|
|
promise = Promise.resolve(true);
|
|
}
|
|
|
|
return promise.then((canAdd) => {
|
|
if (canAdd) {
|
|
groups.unshift({
|
|
courseid: this.courseId,
|
|
id: AddonModForumProvider.ALL_PARTICIPANTS,
|
|
name: Translate.instant('core.allparticipants'),
|
|
});
|
|
}
|
|
|
|
return groups;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Pull to refresh.
|
|
*
|
|
* @param refresher Refresher.
|
|
*/
|
|
refreshGroups(refresher?: IonRefresher): void {
|
|
const promises = [
|
|
CoreGroups.invalidateActivityGroupMode(this.cmId),
|
|
CoreGroups.invalidateActivityAllowedGroups(this.cmId),
|
|
AddonModForum.invalidateCanAddDiscussion(this.forumId),
|
|
];
|
|
|
|
Promise.all(promises).finally(() => {
|
|
this.fetchDiscussionData(true).finally(() => {
|
|
refresher?.complete();
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Convenience function to update or return to discussions depending on device.
|
|
*
|
|
* @param discussionIds Ids of the new discussions.
|
|
* @param discTimecreated The time created of the discussion (if offline).
|
|
*/
|
|
protected returnToDiscussions(discussionIds?: number[] | null, discTimecreated?: number): void {
|
|
this.forceLeave = true;
|
|
|
|
// Delete the local files from the tmp folder.
|
|
CoreFileUploader.clearTmpFiles(this.newDiscussion.files);
|
|
|
|
CoreEvents.trigger(
|
|
AddonModForumProvider.NEW_DISCUSSION_EVENT,
|
|
{
|
|
forumId: this.forumId,
|
|
cmId: this.cmId,
|
|
discussionIds: discussionIds,
|
|
discTimecreated: discTimecreated,
|
|
},
|
|
CoreSites.getCurrentSiteId(),
|
|
);
|
|
|
|
if (this.splitView?.outletActivated) {
|
|
// Empty form.
|
|
this.hasOffline = false;
|
|
this.newDiscussion.subject = '';
|
|
this.newDiscussion.message = null;
|
|
this.newDiscussion.files = [];
|
|
this.newDiscussion.postToAllGroups = false;
|
|
this.messageEditor.clearText();
|
|
this.originalData = CoreUtils.clone(this.newDiscussion);
|
|
} else {
|
|
CoreNavigator.back();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Message changed.
|
|
*
|
|
* @param text The new text.
|
|
*/
|
|
onMessageChange(text: string): void {
|
|
this.newDiscussion.message = text;
|
|
}
|
|
|
|
/**
|
|
* Add a new discussion.
|
|
*/
|
|
async add(): Promise<void> {
|
|
const forumName = this.forum.name;
|
|
const subject = this.newDiscussion.subject;
|
|
let message = this.newDiscussion.message || '';
|
|
const pin = this.newDiscussion.pin;
|
|
const attachments = this.newDiscussion.files;
|
|
const discTimecreated = this.timeCreated || Date.now();
|
|
const options: AddonModForumDiscussionOptions = {
|
|
discussionsubscribe: !!this.newDiscussion.subscribe,
|
|
};
|
|
|
|
if (!subject) {
|
|
CoreDomUtils.showErrorModal('addon.mod_forum.erroremptysubject', true);
|
|
|
|
return;
|
|
}
|
|
if (!message) {
|
|
CoreDomUtils.showErrorModal('addon.mod_forum.erroremptymessage', true);
|
|
|
|
return;
|
|
}
|
|
|
|
const modal = await CoreDomUtils.showModalLoading('core.sending', true);
|
|
|
|
// Add some HTML to the message if needed.
|
|
message = CoreTextUtils.formatHtmlLines(message);
|
|
|
|
if (pin) {
|
|
options.discussionpinned = true;
|
|
}
|
|
|
|
const groupIds = this.newDiscussion.postToAllGroups ? this.groupIds : [this.newDiscussion.groupId];
|
|
|
|
try {
|
|
const discussionIds = await AddonModForumHelper.addNewDiscussion(
|
|
this.forumId,
|
|
forumName,
|
|
this.courseId,
|
|
subject,
|
|
message,
|
|
attachments,
|
|
options,
|
|
groupIds,
|
|
discTimecreated,
|
|
);
|
|
|
|
if (discussionIds) {
|
|
// Data sent to server, delete stored files (if any).
|
|
AddonModForumHelper.deleteNewDiscussionStoredFiles(this.forumId, discTimecreated);
|
|
|
|
CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: 'forum' });
|
|
}
|
|
|
|
if (discussionIds && discussionIds.length < groupIds.length) {
|
|
// Some discussions could not be created.
|
|
CoreDomUtils.showErrorModalDefault(null, 'addon.mod_forum.errorposttoallgroups', true);
|
|
}
|
|
|
|
CoreForms.triggerFormSubmittedEvent(
|
|
this.formElement,
|
|
!!discussionIds,
|
|
CoreSites.getCurrentSiteId(),
|
|
);
|
|
|
|
this.returnToDiscussions(discussionIds, discTimecreated);
|
|
} catch (error) {
|
|
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_forum.cannotcreatediscussion', true);
|
|
} finally {
|
|
modal.dismiss();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Discard an offline saved discussion.
|
|
*/
|
|
async discard(): Promise<void> {
|
|
try {
|
|
await CoreDomUtils.showConfirm(Translate.instant('core.areyousure'));
|
|
|
|
const promises: Promise<unknown>[] = [];
|
|
|
|
promises.push(AddonModForumOffline.deleteNewDiscussion(this.forumId, this.timeCreated));
|
|
promises.push(
|
|
CoreUtils.ignoreErrors(
|
|
AddonModForumHelper.deleteNewDiscussionStoredFiles(this.forumId, this.timeCreated),
|
|
),
|
|
);
|
|
|
|
await Promise.all(promises);
|
|
|
|
CoreForms.triggerFormCancelledEvent(this.formElement, CoreSites.getCurrentSiteId());
|
|
|
|
this.returnToDiscussions();
|
|
} catch (error) {
|
|
// Cancelled.
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show or hide advanced form fields.
|
|
*/
|
|
toggleAdvanced(): void {
|
|
this.advanced = !this.advanced;
|
|
}
|
|
|
|
/**
|
|
* Check if we can leave the page or not.
|
|
*
|
|
* @return Resolved if we can leave it, rejected if not.
|
|
*/
|
|
async canLeave(): Promise<boolean> {
|
|
if (this.forceLeave) {
|
|
return true;
|
|
}
|
|
|
|
if (AddonModForumHelper.hasPostDataChanged(this.newDiscussion, 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.newDiscussion.files);
|
|
|
|
if (this.formElement) {
|
|
CoreForms.triggerFormCancelledEvent(this.formElement, CoreSites.getCurrentSiteId());
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Runs when the page is about to leave and no longer be the active page.
|
|
*/
|
|
ionViewWillLeave(): void {
|
|
this.syncObserver && this.syncObserver.off();
|
|
delete this.syncObserver;
|
|
}
|
|
|
|
/**
|
|
* Helper function to go back.
|
|
*/
|
|
protected goBack(): void {
|
|
if (this.splitView?.outletActivated) {
|
|
CoreNavigator.navigate('../../');
|
|
} else {
|
|
CoreNavigator.back();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Page destroyed.
|
|
*/
|
|
ngOnDestroy(): void {
|
|
if (this.syncId) {
|
|
CoreSync.unblockOperation(AddonModForumProvider.COMPONENT, this.syncId);
|
|
}
|
|
this.isDestroyed = true;
|
|
}
|
|
|
|
}
|