MOBILE-3643 forum: Migrate new discussion form

main
Noel De Martin 2021-02-23 18:51:06 +01:00
parent fbad712136
commit d4bc77b386
7 changed files with 731 additions and 42 deletions

View File

@ -40,6 +40,14 @@ import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { AddonModForumDiscussionOptionsMenuComponent } from '../discussion-options-menu/discussion-options-menu'; import { AddonModForumDiscussionOptionsMenuComponent } from '../discussion-options-menu/discussion-options-menu';
import { AddonModForumSortOrderSelectorComponent } from '../sort-order-selector/sort-order-selector'; import { AddonModForumSortOrderSelectorComponent } from '../sort-order-selector/sort-order-selector';
/**
* Type to use for selecting new discussion form in the discussions manager.
*/
type NewDiscussionForm = {
newDiscussion: true;
timeCreated: number;
};
/** /**
* Component that displays a forum entry page. * Component that displays a forum entry page.
*/ */
@ -193,17 +201,16 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
.getForum(this.courseId, this.module.id) .getForum(this.courseId, this.module.id)
.then(async (forum) => { .then(async (forum) => {
this.forum = forum; this.forum = forum;
this.description = forum.intro || this.description; this.description = forum.intro || this.description;
this.availabilityMessage = AddonModForumHelper.instance.getAvailabilityMessage(forum);
this.descriptionNote = Translate.instant('addon.mod_forum.numdiscussions', { this.descriptionNote = Translate.instant('addon.mod_forum.numdiscussions', {
numdiscussions: forum.numdiscussions, numdiscussions: forum.numdiscussions,
}); });
if (typeof forum.istracked != 'undefined') { if (typeof forum.istracked != 'undefined') {
this.trackPosts = forum.istracked; this.trackPosts = forum.istracked;
} }
this.availabilityMessage = AddonModForumHelper.instance.getAvailabilityMessage(forum);
this.dataRetrieved.emit(forum); this.dataRetrieved.emit(forum);
switch (forum.type) { switch (forum.type) {
@ -218,23 +225,19 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
this.addDiscussionText = Translate.instant('addon.mod_forum.addanewdiscussion'); this.addDiscussionText = Translate.instant('addon.mod_forum.addanewdiscussion');
} }
if (!sync) { if (sync) {
return;
}
// Try to synchronize the forum. // Try to synchronize the forum.
const updated = await this.syncActivity(showErrors); const updated = await this.syncActivity(showErrors);
if (!updated) { if (updated) {
return;
}
// Sync successful, send event. // Sync successful, send event.
CoreEvents.trigger(AddonModForumSyncProvider.MANUAL_SYNCED, { CoreEvents.trigger(AddonModForumSyncProvider.MANUAL_SYNCED, {
forumId: forum.id, forumId: forum.id,
userId: CoreSites.instance.getCurrentSiteUserId(), userId: CoreSites.instance.getCurrentSiteUserId(),
source: 'index', source: 'index',
}, CoreSites.instance.getCurrentSiteId()); }, CoreSites.instance.getCurrentSiteId());
}
}
const promises: Promise<void>[] = []; const promises: Promise<void>[] = [];
@ -507,18 +510,10 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
* @param timeCreated Creation time of the offline discussion. * @param timeCreated Creation time of the offline discussion.
*/ */
openNewDiscussion(timeCreated: number = 0): void { openNewDiscussion(timeCreated: number = 0): void {
alert(`Open new discussion at ${timeCreated} not implemented!`); this.discussions.select({
newDiscussion: true,
// @todo timeCreated,
// const params = { });
// courseId: this.courseId,
// cmId: this.module.id,
// forumId: this.forum.id,
// timeCreated: timeCreated,
// };
// this.splitviewCtrl.push('AddonModForumNewDiscussionPage', params);
this.selectedDiscussion = 0;
} }
/** /**
@ -598,7 +593,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
} }
class AddonModForumDiscussionsManager extends CorePageItemsListManager<AddonModForumDiscussion> { class AddonModForumDiscussionsManager extends CorePageItemsListManager<AddonModForumDiscussion | NewDiscussionForm> {
private discussionsPathPrefix: string; private discussionsPathPrefix: string;
private component: AddonModForumIndexComponent; private component: AddonModForumIndexComponent;
@ -610,18 +605,21 @@ class AddonModForumDiscussionsManager extends CorePageItemsListManager<AddonModF
this.discussionsPathPrefix = discussionsPathPrefix; this.discussionsPathPrefix = discussionsPathPrefix;
} }
getItemQueryParams(discussion: AddonModForumDiscussion): Params { getItemQueryParams(discussion: AddonModForumDiscussion | NewDiscussionForm): Params {
return { return {
discussion,
courseId: this.component.courseId, courseId: this.component.courseId,
cmId: this.component.module!.id, cmId: this.component.module!.id,
forumId: this.component.forum!.id, forumId: this.component.forum!.id,
trackPosts: this.component.trackPosts, ...(
this.isNewDiscussionForm(discussion)
? { timeCreated: discussion.timeCreated }
: { discussion, trackPosts: this.component.trackPosts }
),
}; };
} }
protected getItemPath(discussion: AddonModForumDiscussion): string { protected getItemPath(discussion: AddonModForumDiscussion | NewDiscussionForm): string {
const discussionId = discussion.id; const discussionId = this.isNewDiscussionForm(discussion) ? 'new' : discussion.id;
return this.discussionsPathPrefix + discussionId; return this.discussionsPathPrefix + discussionId;
} }
@ -632,4 +630,8 @@ class AddonModForumDiscussionsManager extends CorePageItemsListManager<AddonModF
return discussionId ? this.discussionsPathPrefix + discussionId : null; return discussionId ? this.discussionsPathPrefix + discussionId : null;
} }
private isNewDiscussionForm(discussion: AddonModForumDiscussion | NewDiscussionForm): discussion is NewDiscussionForm {
return 'newDiscussion' in discussion;
}
} }

View File

@ -16,21 +16,26 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { CoreSharedModule } from '@/core/shared.module'; import { CoreSharedModule } from '@/core/shared.module';
import { CoreEditorComponentsModule } from '@features/editor/components/components.module';
import { AddonModForumComponentsModule } from './components/components.module'; import { AddonModForumComponentsModule } from './components/components.module';
import { AddonModForumIndexPage } from './pages/index'; import { AddonModForumIndexPage } from './pages/index';
import { conditionalRoutes } from '@/app/app-routing.module'; import { conditionalRoutes } from '@/app/app-routing.module';
import { CoreScreen } from '@services/screen'; import { CoreScreen } from '@services/screen';
import { AddonModForumNewDiscussionPage } from './pages/new-discussion/new-discussion';
const mobileRoutes: Routes = [ const mobileRoutes: Routes = [
{ {
path: ':courseId/:cmId', path: ':courseId/:cmId',
component: AddonModForumIndexPage, component: AddonModForumIndexPage,
}, },
{
path: ':courseId/:cmId/new',
component: AddonModForumNewDiscussionPage,
},
{ {
path: ':courseId/:cmId/:discussionId', path: ':courseId/:cmId/:discussionId',
loadChildren: () => import('./pages/discussion/discussion.module').then(m => m.AddonForumDiscussionPageModule), loadChildren: () => import('./pages/discussion/discussion.module').then(m => m.AddonForumDiscussionPageModule),
}, },
]; ];
@ -39,6 +44,10 @@ const tabletRoutes: Routes = [
path: ':courseId/:cmId', path: ':courseId/:cmId',
component: AddonModForumIndexPage, component: AddonModForumIndexPage,
children: [ children: [
{
path: 'new',
component: AddonModForumNewDiscussionPage,
},
{ {
path: ':discussionId', path: ':discussionId',
loadChildren: () => import('./pages/discussion/discussion.module').then(m => m.AddonForumDiscussionPageModule), loadChildren: () => import('./pages/discussion/discussion.module').then(m => m.AddonForumDiscussionPageModule),
@ -58,9 +67,11 @@ const routes: Routes = [
RouterModule.forChild(routes), RouterModule.forChild(routes),
CoreSharedModule, CoreSharedModule,
AddonModForumComponentsModule, AddonModForumComponentsModule,
CoreEditorComponentsModule,
], ],
declarations: [ declarations: [
AddonModForumIndexPage, AddonModForumIndexPage,
AddonModForumNewDiscussionPage,
], ],
}) })
export class AddonModForumLazyModule {} export class AddonModForumLazyModule {}

View File

@ -0,0 +1,79 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title>{{ 'addon.mod_forum.addanewdiscussion' | translate }}</ion-title>
<ion-buttons slot="end">
<!-- The context menu will be added in here. -->
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!groupsLoaded" (ionRefresh)="refreshGroups($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="groupsLoaded">
<form ion-list *ngIf="showForm" #newDiscFormEl>
<ion-item>
<ion-label position="stacked">{{ 'addon.mod_forum.subject' | translate }}</ion-label>
<ion-input type="text" [placeholder]="'addon.mod_forum.subject' | translate" [(ngModel)]="newDiscussion.subject" name="subject"></ion-input>
</ion-item>
<ion-item>
<ion-label position="stacked">{{ 'addon.mod_forum.message' | translate }}</ion-label>
<core-rich-text-editor item-content name="addon_mod_forum_new_discussion" contextLevel="module" elementId="message"
[control]="messageControl" [placeholder]="'addon.mod_forum.message' | translate" [component]="component"
[componentId]="forum.cmid" [autoSave]="true" [contextInstanceId]="forum.cmid"
(contentChanged)="onMessageChange($event)">
</core-rich-text-editor>
</ion-item>
<ion-item-divider class="ion-text-wrap core-expandable" (click)="toggleAdvanced()">
<ion-icon *ngIf="!advanced" name="fa-caret-right" slot="start">
</ion-icon>
<ion-icon *ngIf="advanced" name="fa-caret-down" slot="start">
</ion-icon>
<ion-label>{{ 'addon.mod_forum.advanced' | translate }}</ion-label>
</ion-item-divider>
<ng-container *ngIf="advanced">
<ion-item *ngIf="showGroups && groupIds.length > 1 && accessInfo.cancanposttomygroups">
<ion-label>{{ 'addon.mod_forum.posttomygroups' | translate }}</ion-label>
<ion-toggle [(ngModel)]="newDiscussion.postToAllGroups" name="postallgroups"></ion-toggle>
</ion-item>
<ion-item *ngIf="showGroups">
<ion-label id="addon-mod-forum-groupslabel">{{ 'addon.mod_forum.group' | translate }}</ion-label>
<ion-select [(ngModel)]="newDiscussion.groupId" [disabled]="newDiscussion.postToAllGroups"
aria-labelledby="addon-mod-forum-groupslabel" interface="action-sheet" name="groupid">
<ion-select-option *ngFor="let group of groups" [value]="group.id">{{ group.name }}</ion-select-option>
</ion-select>
</ion-item>
<ion-item>
<ion-label>{{ 'addon.mod_forum.discussionsubscription' | translate }}</ion-label>
<ion-toggle [(ngModel)]="newDiscussion.subscribe" name="subscribe"></ion-toggle>
</ion-item>
<ion-item *ngIf="canPin">
<ion-label>{{ 'addon.mod_forum.discussionpinned' | translate }}</ion-label>
<ion-toggle [(ngModel)]="newDiscussion.pin" name="pin"></ion-toggle>
</ion-item>
<core-attachments *ngIf="canCreateAttachments && forum && forum.maxattachments > 0"
[files]="newDiscussion.files" [maxSize]="forum.maxbytes" [maxSubmissions]="forum.maxattachments"
[component]="component" [componentId]="forum.cmid" [allowOffline]="true">
</core-attachments>
</ng-container>
<ion-item>
<ion-label>
<ion-row>
<ion-col>
<ion-button expand="block" (click)="add()" [disabled]="newDiscussion.subject == '' || newDiscussion.message == null">
{{ 'addon.mod_forum.posttoforum' | translate }}
</ion-button>
</ion-col>
<ion-col *ngIf="hasOffline">
<ion-button expand="block" color="light" (click)="discard()">{{ 'core.discard' | translate }}</ion-button>
</ion-col>
</ion-row>
</ion-label>
</ion-item>
</form>
</core-loading>
</ion-content>

View File

@ -0,0 +1,594 @@
// (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 } 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.service';
import { CoreEditorRichTextEditorComponent } from '@features/editor/components/rich-text-editor/rich-text-editor';
import { AddonModForumSync, AddonModForumSyncProvider } from '@addons/mod/forum/services/sync.service';
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/offline.service';
import { CoreUtils } from '@services/utils/utils';
import { AddonModForumHelper } from '@addons/mod/forum/services/helper.service';
import { IonRefresher } from '@ionic/angular';
import { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
import { CoreTextUtils } from '@services/utils/text';
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 {
@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 = {};
protected 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;
/**
* Component being initialized.
*/
ngOnInit(): void {
this.courseId = CoreNavigator.instance.getRouteNumberParam('courseId')!;
this.cmId = CoreNavigator.instance.getRouteNumberParam('cmId')!;
this.forumId = CoreNavigator.instance.getRouteNumberParam('forumId')!;
this.timeCreated = CoreNavigator.instance.getRouteNumberParam('timeCreated')!;
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: any) => {
if (data.forumId == this.forumId && data.userId == CoreSites.instance.getCurrentSiteUserId()) {
CoreDomUtils.instance.showAlertTranslated('core.notice', 'core.contenteditingsynced');
this.returnToDiscussions();
}
}, CoreSites.instance.getCurrentSiteId());
// Trigger view event, to highlight the current opened discussion in the split view.
CoreEvents.trigger(AddonModForumProvider.VIEW_DISCUSSION_EVENT, {
forumId: this.forumId,
discussion: -this.timeCreated,
}, CoreSites.instance.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.instance.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.instance.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.instance.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.instance.getForumSyncId(this.forumId);
await AddonModForumSync.instance.waitForSync(this.syncId).then(() => {
// Do not block if the scope is already destroyed.
if (!this.isDestroyed) {
CoreSync.instance.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.instance.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.instance.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.instance.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[]> {
if (!AddonModForum.instance.isAllParticipantsFixed()) {
// All participants has a bug, don't add it.
return Promise.resolve(groups);
}
let promise;
if (check) {
// We need to check if the user can add a discussion to all participants.
promise = AddonModForum.instance.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.instance.invalidateActivityGroupMode(this.cmId),
CoreGroups.instance.invalidateActivityAllowedGroups(this.cmId),
AddonModForum.instance.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 {
const data: any = {
forumId: this.forumId,
cmId: this.cmId,
discussionIds: discussionIds,
discTimecreated: discTimecreated,
};
CoreEvents.trigger(AddonModForumProvider.NEW_DISCUSSION_EVENT, data, CoreSites.instance.getCurrentSiteId());
// Delete the local files from the tmp folder.
CoreFileUploader.instance.clearTmpFiles(this.newDiscussion.files);
}
/**
* 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.instance.showErrorModal('addon.mod_forum.erroremptysubject', true);
return;
}
if (!message) {
CoreDomUtils.instance.showErrorModal('addon.mod_forum.erroremptymessage', true);
return;
}
const modal = await CoreDomUtils.instance.showModalLoading('core.sending', true);
// Add some HTML to the message if needed.
message = CoreTextUtils.instance.formatHtmlLines(message);
if (pin) {
options.discussionpinned = true;
}
const groupIds = this.newDiscussion.postToAllGroups ? this.groupIds : [this.newDiscussion.groupId];
try {
const discussionIds = await AddonModForumHelper.instance.addNewDiscussion(
this.forumId,
forumName,
this.courseId,
subject,
message,
attachments,
options,
groupIds,
discTimecreated,
);
if (discussionIds) {
// Data sent to server, delete stored files (if any).
AddonModForumHelper.instance.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.instance.showErrorModalDefault(null, 'addon.mod_forum.errorposttoallgroups', true);
}
CoreDomUtils.instance.triggerFormSubmittedEvent(
this.formElement,
!!discussionIds,
CoreSites.instance.getCurrentSiteId(),
);
this.returnToDiscussions(discussionIds, discTimecreated);
} catch (error) {
CoreDomUtils.instance.showErrorModalDefault(error, 'addon.mod_forum.cannotcreatediscussion', true);
} finally {
modal.dismiss();
}
}
/**
* Discard an offline saved discussion.
*/
async discard(): Promise<void> {
try {
await CoreDomUtils.instance.showConfirm(Translate.instant('core.areyousure'));
const promises: Promise<unknown>[] = [];
promises.push(AddonModForumOffline.instance.deleteNewDiscussion(this.forumId, this.timeCreated));
promises.push(
CoreUtils.instance.ignoreErrors(
AddonModForumHelper.instance.deleteNewDiscussionStoredFiles(this.forumId, this.timeCreated),
),
);
await Promise.all(promises);
CoreDomUtils.instance.triggerFormCancelledEvent(this.formElement, CoreSites.instance.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 ionViewCanLeave(): Promise<void> {
if (this.forceLeave) {
return;
}
if (AddonModForumHelper.instance.hasPostDataChanged(this.newDiscussion, this.originalData)) {
// Show confirmation if some data has been modified.
await CoreDomUtils.instance.showConfirm(Translate.instant('core.confirmcanceledit'));
}
// Delete the local files from the tmp folder.
CoreFileUploader.instance.clearTmpFiles(this.newDiscussion.files);
if (this.formElement) {
CoreDomUtils.instance.triggerFormCancelledEvent(this.formElement, CoreSites.instance.getCurrentSiteId());
}
}
/**
* 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;
}
/**
* Page destroyed.
*/
ngOnDestroy(): void {
if (this.syncId) {
CoreSync.instance.unblockOperation(AddonModForumProvider.COMPONENT, this.syncId);
}
this.isDestroyed = true;
}
}

View File

@ -13,6 +13,7 @@
// limitations under the License. // limitations under the License.
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { FileEntry } from '@ionic-native/file/ngx';
import { CoreFileEntry, CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader'; import { CoreFileEntry, CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
import { CoreUser } from '@features/user/services/user'; import { CoreUser } from '@features/user/services/user';
import { CoreApp } from '@services/app'; import { CoreApp } from '@services/app';

View File

@ -403,7 +403,9 @@ export class AddonModForumOfflineProvider {
export class AddonModForumOffline extends makeSingleton(AddonModForumOfflineProvider) {} export class AddonModForumOffline extends makeSingleton(AddonModForumOfflineProvider) {}
export type AddonModForumDiscussionOptions = { export type AddonModForumDiscussionOptions = {
attachmentsid: number | CoreFileUploaderStoreFilesResult; attachmentsid?: number | CoreFileUploaderStoreFilesResult;
discussionsubscribe?: boolean;
discussionpinned?: boolean;
}; };
export type AddonModForumReplyOptions = { export type AddonModForumReplyOptions = {

View File

@ -743,7 +743,7 @@ export class CoreDomUtilsProvider {
* @param error Error to check. * @param error Error to check.
* @return Whether it's a canceled error. * @return Whether it's a canceled error.
*/ */
isCanceledError(error: CoreError | CoreTextErrorObject | string): boolean { isCanceledError(error: CoreError | CoreTextErrorObject | string | null): boolean {
return error instanceof CoreCanceledError; return error instanceof CoreCanceledError;
} }
@ -1393,7 +1393,7 @@ export class CoreDomUtilsProvider {
* @return Promise resolved with the alert modal. * @return Promise resolved with the alert modal.
*/ */
async showErrorModalDefault( async showErrorModalDefault(
error: CoreError | CoreTextErrorObject | string, error: CoreError | CoreTextErrorObject | string | null,
defaultError: string, defaultError: string,
needsTranslate?: boolean, needsTranslate?: boolean,
autocloseTime?: number, autocloseTime?: number,
@ -1409,7 +1409,7 @@ export class CoreDomUtilsProvider {
errorMessage = CoreTextUtils.instance.getErrorMessageFromError(error); errorMessage = CoreTextUtils.instance.getErrorMessageFromError(error);
} }
return this.showErrorModal(typeof errorMessage == 'string' ? error : defaultError, needsTranslate, autocloseTime); return this.showErrorModal(typeof errorMessage == 'string' ? error! : defaultError, needsTranslate, autocloseTime);
} }
/** /**