MOBILE-3643 forum: Migrate index page
parent
549735bf98
commit
b318b0e4a5
|
@ -29,7 +29,7 @@
|
|||
<ion-item class="ion-text-wrap addon-messages-conversation-item"
|
||||
*ngFor="let contact of confirmedContacts" [title]="contact.fullname" detail
|
||||
(click)="selectUser(contact.id)" [class.core-selected-item]="contact.id == selectedUserId">
|
||||
<core-user-avatar slot="start" core-user-avatar [user]="contact"
|
||||
<core-user-avatar slot="start" [user]="contact"
|
||||
[checkOnline]="contact.showonlinestatus" [linkProfile]="false">
|
||||
</core-user-avatar>
|
||||
<ion-label>
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
// (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 { NgModule } from '@angular/core';
|
||||
|
||||
import { CoreSharedModule } from '@/core/shared.module';
|
||||
import { CoreCourseComponentsModule } from '@features/course/components/components.module';
|
||||
import { CoreTagComponentsModule } from '@features/tag/components/components.module';
|
||||
|
||||
import { AddonModForumIndexComponent } from './index/index';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModForumIndexComponent,
|
||||
],
|
||||
imports: [
|
||||
CoreSharedModule,
|
||||
CoreCourseComponentsModule,
|
||||
CoreTagComponentsModule,
|
||||
],
|
||||
exports: [
|
||||
AddonModForumIndexComponent,
|
||||
],
|
||||
})
|
||||
export class AddonModForumComponentsModule {}
|
|
@ -0,0 +1,127 @@
|
|||
<!-- Content. -->
|
||||
<ion-content>
|
||||
<core-split-view>
|
||||
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($event)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
|
||||
<core-loading [hideUntil]="loaded" class="core-loading-center">
|
||||
<core-course-module-description *ngIf="forum && forum.type != 'single'"
|
||||
[description]="description" [component]="component" [componentId]="componentId" [note]="descriptionNote"
|
||||
contextLevel="module" [contextInstanceId]="module && module.id" [courseId]="courseId">
|
||||
</core-course-module-description>
|
||||
|
||||
<!-- Forum discussions found to be synchronized -->
|
||||
<ion-card class="core-warning-card" *ngIf="hasOffline || hasOfflineRatings">
|
||||
<ion-item>
|
||||
<ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon>
|
||||
<ion-label>{{ 'core.hasdatatosync' | translate:{$a: moduleName} }}</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
|
||||
<!-- Cut-off date or due date message -->
|
||||
<ion-card class="core-info-card" *ngIf="availabilityMessage">
|
||||
<ion-item>
|
||||
<ion-icon name="fas-info-circle" slot="start"></ion-icon>
|
||||
<ion-label>{{ availabilityMessage }}</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
|
||||
<ng-container *ngIf="forum">
|
||||
<core-empty-box *ngIf="discussions.length == 0 && offlineDiscussions.length == 0" icon="chatbubbles" [message]="'addon.mod_forum.forumnodiscussionsyet' | translate">
|
||||
</core-empty-box>
|
||||
|
||||
<div *ngIf="discussions.length > 0 && sortingAvailable && selectedSortOrder" class="ion-text-wrap addon-forum-sorting-select">
|
||||
<ion-button *ngIf="sortingAvailable" id="addon-mod-forum-sort-order-button"
|
||||
class="core-button-select button-no-uppercase"
|
||||
aria-haspopup="true" aria-controls="addon-mod-forum-sort-order-selector"
|
||||
[attr.aria-label]="('core.sort' | translate)" [attr.aria-expanded]="sortOrderSelectorExpanded"
|
||||
(click)="showSortOrderSelector($event)">
|
||||
<span class="core-button-select-text">{{ selectedSortOrder.label | translate }}</span>
|
||||
<div class="select-icon" slot="end"><div class="select-icon-inner"></div></div>
|
||||
</ion-button>
|
||||
</div>
|
||||
|
||||
<ion-item *ngFor="let discussion of offlineDiscussions"
|
||||
class="ion-text-wrap addon-mod-forum-discussion" detail="true"
|
||||
[attr.lines="none"]="discussion.groupname" [class.core-item-selected]="discussion.timecreated == -selectedDiscussion"
|
||||
(click)="openNewDiscussion(discussion.timecreated)">
|
||||
<ion-label>
|
||||
<div class="addon-mod-forum-discussion-title">
|
||||
<h2>
|
||||
<core-format-text [text]="discussion.subject" contextLevel="module" [contextInstanceId]="module && module.id" [courseId]="courseId"></core-format-text>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="addon-mod-forum-discussion-info">
|
||||
<core-user-avatar [user]="discussion" slot="start" [courseId]="courseId" *ngIf="discussion.userfullname">
|
||||
</core-user-avatar>
|
||||
<div class="addon-mod-forum-discussion-author">
|
||||
<h3 *ngIf="discussion.userfullname">{{discussion.userfullname}}</h3>
|
||||
<p *ngIf="discussion.groupname"><ion-icon name="people"></ion-icon> {{ discussion.groupname }}</p>
|
||||
<p><ion-icon name="time"></ion-icon> {{ 'core.notsent' | translate }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item *ngFor="let discussion of discussions"
|
||||
class="addon-mod-forum-discussion" detail="true"
|
||||
[class.core-split-item-selected]="discussion.discussion == selectedDiscussion"
|
||||
(click)="openDiscussion(discussion)">
|
||||
<ion-label>
|
||||
<div class="addon-mod-forum-discussion-title">
|
||||
<h2 class="ion-text-wrap">
|
||||
<ion-icon name="fa-map-pin" *ngIf="discussion.pinned">
|
||||
</ion-icon>
|
||||
<ion-icon name="fa-star" class="addon-forum-star" *ngIf="!discussion.pinned && discussion.starred">
|
||||
</ion-icon>
|
||||
<core-format-text [text]="discussion.subject" contextLevel="module" [contextInstanceId]="module && module.id" [courseId]="courseId"></core-format-text>
|
||||
</h2>
|
||||
<ion-button *ngIf="canPin || discussion.canlock || discussion.canfavourite"
|
||||
fill="clear" color="dark"
|
||||
[attr.aria-label]="('core.displayoptions' | translate)"
|
||||
(click)="showOptionsMenu($event, discussion)">
|
||||
<ion-icon name="more" slot="icon-only">
|
||||
</ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
<div class="addon-mod-forum-discussion-info">
|
||||
<core-user-avatar *ngIf="discussion.userfullname" [user]="discussion" slot="start" [courseId]="courseId">
|
||||
</core-user-avatar>
|
||||
<div class="addon-mod-forum-discussion-author">
|
||||
<h3 *ngIf="discussion.userfullname">{{discussion.userfullname}}</h3>
|
||||
<p *ngIf="discussion.groupname"><ion-icon name="people"></ion-icon> {{ discussion.groupname }}</p>
|
||||
<p>{{discussion.created * 1000 | coreFormatDate: "strftimerecentfull"}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<ion-row class="ion-text-center addon-mod-forum-discussion-more-info">
|
||||
<ion-col class="ion-text-start">
|
||||
<ion-note>
|
||||
<ion-icon name="time"></ion-icon> {{ 'addon.mod_forum.lastpost' | translate }}
|
||||
<ng-container *ngIf="discussion.timemodified > discussion.created">{{discussion.timemodified | coreTimeAgo}}</ng-container>
|
||||
<ng-container *ngIf="discussion.timemodified <= discussion.created">{{discussion.created | coreTimeAgo}}</ng-container>
|
||||
</ion-note>
|
||||
</ion-col>
|
||||
<ion-col class="ion-text-end">
|
||||
<ion-note>
|
||||
<ion-icon name="fas-comments"></ion-icon> {{ 'addon.mod_forum.numreplies' | translate:{numreplies: discussion.numreplies} }}
|
||||
<ion-badge *ngIf="discussion.numunread" class="ion-text-center"
|
||||
[attr.aria-label]="'addon.mod_forum.unreadpostsnumber' | translate:{ '$a' : discussion.numunread}">
|
||||
{{ discussion.numunread }}
|
||||
</ion-badge>
|
||||
</ion-note>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
</ng-container>
|
||||
</core-loading>
|
||||
|
||||
<ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="forum && canAddDiscussion">
|
||||
<ion-fab-button (click)="openNewDiscussion()" [attr.aria-label]="addDiscussionText">
|
||||
<ion-icon name="add"></ion-icon>
|
||||
</ion-fab-button>
|
||||
</ion-fab>
|
||||
</core-split-view>
|
||||
</ion-content>
|
|
@ -0,0 +1,65 @@
|
|||
@import '../../../../../theme/globals.scss';
|
||||
|
||||
:host {
|
||||
|
||||
.addon-forum-sorting-select {
|
||||
display: flex;
|
||||
|
||||
.core-button-select {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.core-button-select-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.addon-forum-star {
|
||||
color: var(--core-color);
|
||||
}
|
||||
|
||||
.addon-mod-forum-discussion.item {
|
||||
|
||||
ion-label {
|
||||
margin-top: 4px;
|
||||
|
||||
h2 {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
font-weight: bold;
|
||||
|
||||
ion-icon {
|
||||
@include margin(0, 6px, 0, 0);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
core-user-avatar {
|
||||
--core-avatar-size: var(--addon-forum-avatar-size);
|
||||
|
||||
@include margin(0, 8px, 0, 0);
|
||||
}
|
||||
|
||||
.addon-mod-forum-discussion-title,
|
||||
.addon-mod-forum-discussion-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.addon-mod-forum-discussion-title h2,
|
||||
.addon-mod-forum-discussion-info .addon-mod-forum-discussion-author {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.addon-mod-forum-discussion-more-info {
|
||||
font-size: 1.4rem;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,563 @@
|
|||
// (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, Optional, OnInit, OnDestroy } from '@angular/core';
|
||||
import { IonContent } from '@ionic/angular';
|
||||
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
|
||||
import {
|
||||
AddonModForum,
|
||||
AddonModForumData,
|
||||
AddonModForumProvider,
|
||||
AddonModForumSortOrder,
|
||||
AddonModForumDiscussion,
|
||||
} from '@addons/mod/forum/services/forum.service';
|
||||
import { AddonModForumOffline, AddonModForumOfflineDiscussion } from '@addons/mod/forum/services/offline.service';
|
||||
import { Translate } from '@singletons';
|
||||
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
|
||||
import { AddonModForumHelper } from '@addons/mod/forum/services/helper.service';
|
||||
import { CoreGroups, CoreGroupsProvider } from '@services/groups';
|
||||
import { CoreEvents } from '@singletons/events';
|
||||
import { AddonModForumSyncProvider } from '@addons/mod/forum/services/sync.service';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreUser } from '@features/user/services/user';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreCourse } from '@features/course/services/course';
|
||||
|
||||
/**
|
||||
* Component that displays a forum entry page.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-mod-forum-index',
|
||||
templateUrl: 'index.html',
|
||||
styleUrls: ['index.scss'],
|
||||
})
|
||||
export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit, OnDestroy {
|
||||
|
||||
component = AddonModForumProvider.COMPONENT;
|
||||
moduleName = 'forum';
|
||||
|
||||
descriptionNote?: string;
|
||||
forum?: AddonModForumData;
|
||||
canLoadMore = false;
|
||||
loadMoreError = false;
|
||||
discussions: AddonModForumDiscussion[] = [];
|
||||
offlineDiscussions: AddonModForumOfflineDiscussion[] = [];
|
||||
selectedDiscussion = 0; // Disucssion ID or negative timecreated if it's an offline discussion.
|
||||
canAddDiscussion = false;
|
||||
addDiscussionText!: string;
|
||||
availabilityMessage: string | null = null;
|
||||
|
||||
sortingAvailable!: boolean;
|
||||
sortOrders: AddonModForumSortOrder[] = [];
|
||||
selectedSortOrder: AddonModForumSortOrder | null = null;
|
||||
sortOrderSelectorExpanded = false;
|
||||
|
||||
protected syncEventName = AddonModForumSyncProvider.AUTO_SYNCED;
|
||||
protected page = 0;
|
||||
protected trackPosts = false;
|
||||
protected usesGroups = false;
|
||||
protected canPin = false;
|
||||
protected syncManualObserver: any; // It will observe the sync manual event.
|
||||
protected replyObserver: any;
|
||||
protected newDiscObserver: any;
|
||||
protected viewDiscObserver: any;
|
||||
protected changeDiscObserver: any;
|
||||
|
||||
hasOfflineRatings?: boolean;
|
||||
protected ratingOfflineObserver: any;
|
||||
protected ratingSyncObserver: any;
|
||||
|
||||
constructor(
|
||||
@Optional() protected content?: IonContent,
|
||||
@Optional() courseContentsPage?: CoreCourseContentsPage,
|
||||
) {
|
||||
super('AddonModForumIndexComponent', content, courseContentsPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.addDiscussionText = Translate.instance.instant('addon.mod_forum.addanewdiscussion');
|
||||
this.sortingAvailable = AddonModForum.instance.isDiscussionListSortingAvailable();
|
||||
this.sortOrders = AddonModForum.instance.getAvailableSortOrders();
|
||||
|
||||
await super.ngOnInit();
|
||||
await this.loadContent(false, true);
|
||||
|
||||
if (!this.forum) {
|
||||
return;
|
||||
}
|
||||
|
||||
CoreUtils.instance.ignoreErrors(
|
||||
AddonModForum.instance
|
||||
.logView(this.forum.id, this.forum.name)
|
||||
.then(async () => {
|
||||
CoreCourse.instance.checkModuleCompletion(this.courseId!, this.module!.completiondata);
|
||||
|
||||
return;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
super.ngOnDestroy();
|
||||
|
||||
this.syncManualObserver && this.syncManualObserver.off();
|
||||
this.newDiscObserver && this.newDiscObserver.off();
|
||||
this.replyObserver && this.replyObserver.off();
|
||||
this.viewDiscObserver && this.viewDiscObserver.off();
|
||||
this.changeDiscObserver && this.changeDiscObserver.off();
|
||||
this.ratingOfflineObserver && this.ratingOfflineObserver.off();
|
||||
this.ratingSyncObserver && this.ratingSyncObserver.off();
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the component contents.
|
||||
*
|
||||
* @param refresh Whether we're refreshing data.
|
||||
* @param sync If the refresh needs syncing.
|
||||
* @param showErrors Wether to show errors to the user or hide them.
|
||||
*/
|
||||
protected async fetchContent(refresh: boolean = false, sync: boolean = false): Promise<void> {
|
||||
this.loadMoreError = false;
|
||||
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
promises.push(this.fetchForum());
|
||||
promises.push(this.fetchSortOrderPreference());
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
await Promise.all([
|
||||
this.fetchOfflineDiscussions(),
|
||||
this.fetchDiscussions(refresh),
|
||||
]);
|
||||
} catch (error) {
|
||||
if (refresh) {
|
||||
CoreDomUtils.instance.showErrorModalDefault(error, 'addon.mod_forum.errorgetforum', true);
|
||||
|
||||
this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading.
|
||||
} else {
|
||||
// Get forum failed, retry without using cache since it might be a new activity.
|
||||
await this.refreshContent(sync);
|
||||
}
|
||||
}
|
||||
|
||||
this.fillContextMenu(refresh);
|
||||
}
|
||||
|
||||
private async fetchForum(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> {
|
||||
if (!this.courseId || !this.module) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadMoreError = false;
|
||||
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
promises.push(
|
||||
AddonModForum.instance
|
||||
.getForum(this.courseId, this.module.id)
|
||||
.then(async (forum) => {
|
||||
this.forum = forum;
|
||||
|
||||
this.description = forum.intro || this.description;
|
||||
this.descriptionNote = Translate.instant('addon.mod_forum.numdiscussions', {
|
||||
numdiscussions: forum.numdiscussions,
|
||||
});
|
||||
if (typeof forum.istracked != 'undefined') {
|
||||
this.trackPosts = forum.istracked;
|
||||
}
|
||||
|
||||
this.availabilityMessage = AddonModForumHelper.instance.getAvailabilityMessage(forum);
|
||||
|
||||
this.dataRetrieved.emit(forum);
|
||||
|
||||
switch (forum.type) {
|
||||
case 'news':
|
||||
case 'blog':
|
||||
this.addDiscussionText = Translate.instant('addon.mod_forum.addanewtopic');
|
||||
break;
|
||||
case 'qanda':
|
||||
this.addDiscussionText = Translate.instant('addon.mod_forum.addanewquestion');
|
||||
break;
|
||||
default:
|
||||
this.addDiscussionText = Translate.instant('addon.mod_forum.addanewdiscussion');
|
||||
}
|
||||
|
||||
if (!sync) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to synchronize the forum.
|
||||
const updated = await this.syncActivity(showErrors);
|
||||
|
||||
if (!updated) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sync successful, send event.
|
||||
CoreEvents.trigger(AddonModForumSyncProvider.MANUAL_SYNCED, {
|
||||
forumId: forum.id,
|
||||
userId: CoreSites.instance.getCurrentSiteUserId(),
|
||||
source: 'index',
|
||||
}, CoreSites.instance.getCurrentSiteId());
|
||||
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
// Check if the activity uses groups.
|
||||
promises.push(
|
||||
// eslint-disable-next-line promise/no-nesting
|
||||
CoreGroups.instance
|
||||
.getActivityGroupMode(this.forum.cmid)
|
||||
.then(async mode => {
|
||||
this.usesGroups = mode === CoreGroupsProvider.SEPARATEGROUPS
|
||||
|| mode === CoreGroupsProvider.VISIBLEGROUPS;
|
||||
|
||||
return;
|
||||
}),
|
||||
);
|
||||
|
||||
promises.push(
|
||||
// eslint-disable-next-line promise/no-nesting
|
||||
AddonModForum.instance
|
||||
.getAccessInformation(this.forum.id, { cmId: this.module!.id })
|
||||
.then(async accessInfo => {
|
||||
// Disallow adding discussions if cut-off date is reached and the user has not the
|
||||
// capability to override it.
|
||||
// Just in case the forum was fetched from WS when the cut-off date was not reached but it is now.
|
||||
const cutoffDateReached = AddonModForumHelper.instance.isCutoffDateReached(this.forum!)
|
||||
&& !accessInfo.cancanoverridecutoff;
|
||||
this.canAddDiscussion = !!this.forum?.cancreatediscussions && !cutoffDateReached;
|
||||
|
||||
return;
|
||||
}),
|
||||
);
|
||||
|
||||
if (AddonModForum.instance.isSetPinStateAvailableForSite()) {
|
||||
// Use the canAddDiscussion WS to check if the user can pin discussions.
|
||||
promises.push(
|
||||
// eslint-disable-next-line promise/no-nesting
|
||||
AddonModForum.instance
|
||||
.canAddDiscussionToAll(this.forum.id, { cmId: this.module!.id })
|
||||
.then(async response => {
|
||||
this.canPin = !!response.canpindiscussions;
|
||||
|
||||
return;
|
||||
})
|
||||
.catch(async () => {
|
||||
this.canPin = false;
|
||||
|
||||
return;
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
this.canPin = false;
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
return;
|
||||
}),
|
||||
);
|
||||
|
||||
promises.push(this.fetchSortOrderPreference());
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
await Promise.all([
|
||||
this.fetchOfflineDiscussions(),
|
||||
this.fetchDiscussions(refresh),
|
||||
]);
|
||||
} catch (message) {
|
||||
if (!refresh) {
|
||||
// Get forum failed, retry without using cache since it might be a new activity.
|
||||
return this.refreshContent(sync);
|
||||
}
|
||||
|
||||
CoreDomUtils.instance.showErrorModalDefault(message, 'addon.mod_forum.errorgetforum', true);
|
||||
|
||||
this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading.
|
||||
}
|
||||
|
||||
this.fillContextMenu(refresh);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to fetch offline discussions.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async fetchOfflineDiscussions(): Promise<void> {
|
||||
const forum = this.forum!;
|
||||
let offlineDiscussions = await AddonModForumOffline.instance.getNewDiscussions(forum.id);
|
||||
this.hasOffline = !!offlineDiscussions.length;
|
||||
|
||||
if (!this.hasOffline) {
|
||||
this.offlineDiscussions = [];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.usesGroups) {
|
||||
offlineDiscussions = await AddonModForum.instance.formatDiscussionsGroups(forum.cmid, offlineDiscussions);
|
||||
}
|
||||
|
||||
// Fill user data for Offline discussions (should be already cached).
|
||||
const promises = offlineDiscussions.map(async (discussion: any) => {
|
||||
if (discussion.parent === 0 || forum.type === 'single') {
|
||||
// Do not show author for first post and type single.
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await CoreUser.instance.getProfile(discussion.userid, this.courseId, true);
|
||||
|
||||
discussion.userfullname = user.fullname;
|
||||
discussion.userpictureurl = user.profileimageurl;
|
||||
} catch (error) {
|
||||
// Ignore errors.
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
// Sort discussion by time (newer first).
|
||||
offlineDiscussions.sort((a, b) => b.timecreated - a.timecreated);
|
||||
|
||||
this.offlineDiscussions = offlineDiscussions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to get forum discussions.
|
||||
*
|
||||
* @param refresh Whether we're refreshing data.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async fetchDiscussions(refresh: boolean): Promise<void> {
|
||||
const forum = this.forum!;
|
||||
this.loadMoreError = false;
|
||||
|
||||
if (refresh) {
|
||||
this.page = 0;
|
||||
}
|
||||
|
||||
const response = await AddonModForum.instance.getDiscussions(forum.id, {
|
||||
cmId: forum.cmid,
|
||||
sortOrder: this.selectedSortOrder!.value,
|
||||
page: this.page,
|
||||
});
|
||||
let discussions = response.discussions;
|
||||
|
||||
if (this.usesGroups) {
|
||||
discussions = await AddonModForum.instance.formatDiscussionsGroups(forum.cmid, discussions);
|
||||
}
|
||||
|
||||
// Hide author for first post and type single.
|
||||
if (forum.type === 'single') {
|
||||
for (const discussion of discussions) {
|
||||
if (discussion.userfullname && discussion.parent === 0) {
|
||||
(discussion as any).userfullname = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If any discussion has unread posts, the whole forum is being tracked.
|
||||
if (typeof forum.istracked === 'undefined' && !this.trackPosts) {
|
||||
for (const discussion of discussions) {
|
||||
if (discussion.numunread > 0) {
|
||||
this.trackPosts = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.discussions = this.page === 0
|
||||
? discussions
|
||||
: this.discussions.concat(discussions);
|
||||
|
||||
this.canLoadMore = response.canLoadMore;
|
||||
this.page++;
|
||||
|
||||
// Check if there are replies for discussions stored in offline.
|
||||
const hasOffline = await AddonModForumOffline.instance.hasForumReplies(forum.id);
|
||||
|
||||
this.hasOffline = this.hasOffline || hasOffline;
|
||||
|
||||
if (hasOffline) {
|
||||
// Only update new fetched discussions.
|
||||
const promises = discussions.map(async (discussion: any) => {
|
||||
// Get offline discussions.
|
||||
const replies = await AddonModForumOffline.instance.getDiscussionReplies(discussion.discussion);
|
||||
|
||||
discussion.numreplies = Number(discussion.numreplies) + replies.length;
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to load more forum discussions.
|
||||
*
|
||||
* @param infiniteComplete Infinite scroll complete function. Only used from core-infinite-loading.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
fetchMoreDiscussions(infiniteComplete?: any): Promise<any> {
|
||||
return this.fetchDiscussions(false).catch((message) => {
|
||||
CoreDomUtils.instance.showErrorModalDefault(message, 'addon.mod_forum.errorgetforum', true);
|
||||
|
||||
this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading.
|
||||
}).finally(() => {
|
||||
infiniteComplete && infiniteComplete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to fetch the sort order preference.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async fetchSortOrderPreference(): Promise<void> {
|
||||
const getSortOrder = async () => {
|
||||
if (!this.sortingAvailable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = await CoreUtils.instance.ignoreErrors(
|
||||
CoreUser.instance.getUserPreference(AddonModForumProvider.PREFERENCE_SORTORDER),
|
||||
);
|
||||
|
||||
return value ? parseInt(value, 10) : null;
|
||||
};
|
||||
|
||||
const value = await getSortOrder();
|
||||
|
||||
this.selectedSortOrder = this.sortOrders.find(sortOrder => sortOrder.value === value) || this.sortOrders[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the invalidate content function.
|
||||
*
|
||||
* @return Resolved when done.
|
||||
*/
|
||||
protected invalidateContent(): Promise<any> {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
promises.push(AddonModForum.instance.invalidateForumData(this.courseId!));
|
||||
|
||||
if (this.forum) {
|
||||
promises.push(AddonModForum.instance.invalidateDiscussionsList(this.forum.id));
|
||||
promises.push(CoreGroups.instance.invalidateActivityGroupMode(this.forum.cmid));
|
||||
promises.push(AddonModForum.instance.invalidateAccessInformation(this.forum.id));
|
||||
}
|
||||
|
||||
if (this.sortingAvailable) {
|
||||
promises.push(CoreUser.instance.invalidateUserPreference(AddonModForumProvider.PREFERENCE_SORTORDER));
|
||||
}
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if sync has succeed from result sync data.
|
||||
*
|
||||
* @param result Data returned on the sync function.
|
||||
* @return Whether it succeed or not.
|
||||
*/
|
||||
protected hasSyncSucceed(result: any): boolean {
|
||||
return result.updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a discussion.
|
||||
*
|
||||
* @param discussion Discussion object.
|
||||
*/
|
||||
openDiscussion(discussion: AddonModForumDiscussion): void {
|
||||
alert(`Open discussion ${discussion.id}: Not implemented!`);
|
||||
|
||||
// @todo
|
||||
// const params = {
|
||||
// courseId: this.courseId,
|
||||
// cmId: this.module.id,
|
||||
// forumId: this.forum.id,
|
||||
// discussion: discussion,
|
||||
// trackPosts: this.trackPosts,
|
||||
// };
|
||||
// this.splitviewCtrl.push('AddonModForumDiscussionPage', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the new discussion form.
|
||||
*
|
||||
* @param timeCreated Creation time of the offline discussion.
|
||||
*/
|
||||
openNewDiscussion(timeCreated: number = 0): void {
|
||||
alert(`Open new discussion at ${timeCreated} not implemented!`);
|
||||
|
||||
// @todo
|
||||
// const params = {
|
||||
// courseId: this.courseId,
|
||||
// cmId: this.module.id,
|
||||
// forumId: this.forum.id,
|
||||
// timeCreated: timeCreated,
|
||||
// };
|
||||
// this.splitviewCtrl.push('AddonModForumNewDiscussionPage', params);
|
||||
|
||||
this.selectedDiscussion = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the sort order selector modal.
|
||||
*
|
||||
* @param event Event.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
showSortOrderSelector(event: MouseEvent): void {
|
||||
if (!this.sortingAvailable) {
|
||||
return;
|
||||
}
|
||||
|
||||
alert('Show sort order selector not implemented');
|
||||
|
||||
// @todo
|
||||
// const params = { sortOrders: this.sortOrders, selected: this.selectedSortOrder.value };
|
||||
// const modal = this.modalCtrl.create('AddonModForumSortOrderSelectorPage', params);
|
||||
// modal.onDidDismiss((sortOrder) => {
|
||||
// this.sortOrderSelectorExpanded = false;
|
||||
|
||||
// if (sortOrder && sortOrder.value != this.selectedSortOrder.value) {
|
||||
// this.selectedSortOrder = sortOrder;
|
||||
// this.page = 0;
|
||||
// this.userProvider.setUserPreference(AddonModForumProvider.PREFERENCE_SORTORDER, sortOrder.value.toFixed(0))
|
||||
// .then(() => {
|
||||
// this.showLoadingAndFetch();
|
||||
// }).catch((error) => {
|
||||
// this.domUtils.showErrorModalDefault(error, 'Error updating preference.');
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
|
||||
// modal.present({ ev: event });
|
||||
// this.sortOrderSelectorExpanded = true;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
// (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 { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { CoreSharedModule } from '@/core/shared.module';
|
||||
|
||||
import { AddonModForumComponentsModule } from './components/components.module';
|
||||
import { AddonModForumIndexPage } from './pages/index';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: ':courseId/:cmId',
|
||||
component: AddonModForumIndexPage,
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild(routes),
|
||||
CoreSharedModule,
|
||||
AddonModForumComponentsModule,
|
||||
],
|
||||
declarations: [
|
||||
AddonModForumIndexPage,
|
||||
],
|
||||
})
|
||||
export class AddonModForumLazyModule {}
|
|
@ -0,0 +1,51 @@
|
|||
// (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 { APP_INITIALIZER, NgModule } from '@angular/core';
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
import { CORE_SITE_SCHEMAS } from '@services/sites';
|
||||
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
|
||||
import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module';
|
||||
|
||||
import { AddonModForumComponentsModule } from './components/components.module';
|
||||
import { AddonModForumModuleHandler, AddonModForumModuleHandlerService } from './services/handlers/module';
|
||||
import { SITE_SCHEMA } from './services/offline-db';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: AddonModForumModuleHandlerService.PAGE_NAME,
|
||||
loadChildren: () => import('./forum-lazy.module').then(m => m.AddonModForumLazyModule),
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CoreMainMenuTabRoutingModule.forChild(routes),
|
||||
AddonModForumComponentsModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: CORE_SITE_SCHEMAS,
|
||||
useValue: [SITE_SCHEMA],
|
||||
multi: true,
|
||||
},
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
multi: true,
|
||||
useValue: () => CoreCourseModuleDelegate.instance.registerHandler(AddonModForumModuleHandler.instance),
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AddonModForumModule {}
|
|
@ -0,0 +1,66 @@
|
|||
{
|
||||
"addanewdiscussion": "Add a new discussion topic",
|
||||
"addanewquestion": "Add a new question",
|
||||
"addanewtopic": "Add a new topic",
|
||||
"addtofavourites": "Star this discussion",
|
||||
"advanced": "Advanced",
|
||||
"cannotadddiscussion": "Adding discussions to this forum requires group membership.",
|
||||
"cannotadddiscussionall": "You do not have permission to add a new discussion topic for all participants.",
|
||||
"cannotcreatediscussion": "Could not create new discussion",
|
||||
"couldnotadd": "Could not add your post due to an unknown error",
|
||||
"couldnotupdate": "Could not update your post due to an unknown error",
|
||||
"cutoffdatereached": "The cut-off date for posting to this forum is reached so you can no longer post to it.",
|
||||
"delete": "Delete",
|
||||
"deletedpost": "The post has been deleted",
|
||||
"deletesure": "Are you sure you want to delete this post?",
|
||||
"discussion": "Discussion",
|
||||
"discussionlistsortbycreatedasc": "Sort by creation date in ascending order",
|
||||
"discussionlistsortbycreateddesc": "Sort by creation date in descending order",
|
||||
"discussionlistsortbylastpostasc": "Sort by last post creation date in ascending order",
|
||||
"discussionlistsortbylastpostdesc": "Sort by last post creation date in descending order",
|
||||
"discussionlistsortbyrepliesasc": "Sort by number of replies in ascending order",
|
||||
"discussionlistsortbyrepliesdesc": "Sort by number of replies in descending order",
|
||||
"discussionlocked": "This discussion has been locked so you can no longer reply to it.",
|
||||
"discussionpinned": "Pinned",
|
||||
"discussionsubscription": "Discussion subscription",
|
||||
"edit": "Edit",
|
||||
"erroremptymessage": "Post message cannot be empty",
|
||||
"erroremptysubject": "Post subject cannot be empty.",
|
||||
"errorgetforum": "Error getting forum data.",
|
||||
"errorgetgroups": "Error getting group settings.",
|
||||
"errorposttoallgroups": "Could not create new discussion in all groups.",
|
||||
"favouriteupdated": "Your star option has been updated.",
|
||||
"forumnodiscussionsyet": "There are no discussions yet in this forum.",
|
||||
"group": "Group",
|
||||
"lastpost": "Last post",
|
||||
"lockdiscussion": "Lock this discussion",
|
||||
"lockupdated": "The lock option has been updated.",
|
||||
"message": "Message",
|
||||
"modeflatnewestfirst": "Display replies flat, with newest first",
|
||||
"modeflatoldestfirst": "Display replies flat, with oldest first",
|
||||
"modenested": "Display replies in nested form",
|
||||
"modulenameplural": "Forums",
|
||||
"numdiscussions": "{{numdiscussions}} discussions",
|
||||
"numreplies": "{{numreplies}} replies",
|
||||
"pindiscussion": "Pin this discussion",
|
||||
"pinupdated": "The pin option has been updated.",
|
||||
"postisprivatereply": "This is a private reply. It is not visible to other participants.",
|
||||
"posttoforum": "Post to forum",
|
||||
"posttomygroups": "Post a copy to all groups",
|
||||
"privatereply": "Reply privately",
|
||||
"re": "Re:",
|
||||
"refreshdiscussions": "Refresh discussions",
|
||||
"refreshposts": "Refresh posts",
|
||||
"removefromfavourites": "Unstar this discussion",
|
||||
"reply": "Reply",
|
||||
"replyplaceholder": "Write your reply...",
|
||||
"subject": "Subject",
|
||||
"tagarea_forum_posts": "Forum posts",
|
||||
"thisforumhasduedate": "The due date for posting to this forum is {{$a}}.",
|
||||
"thisforumisdue": "The due date for posting to this forum was {{$a}}.",
|
||||
"unlockdiscussion": "Unlock this discussion",
|
||||
"unpindiscussion": "Unpin this discussion",
|
||||
"unread": "Unread",
|
||||
"unreadpostsnumber": "{{$a}} unread posts",
|
||||
"yourreply": "Your reply"
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>
|
||||
<core-format-text [text]="title" contextLevel="module" [contextInstanceId]="module?.id" [courseId]="courseId">
|
||||
</core-format-text>
|
||||
</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<!-- The buttons defined by the component will be added in here. -->
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<addon-mod-forum-index [module]="module" [courseId]="courseId" (dataRetrieved)="updateData($event)"></addon-mod-forum-index>
|
||||
</ion-content>
|
|
@ -0,0 +1,49 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
import { AddonModForumData } from '@addons/mod/forum/services/forum.service';
|
||||
import { CoreCourseAnyModuleData } from '@features/course/services/course';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
|
||||
@Component({
|
||||
selector: 'page-addon-mod-forum-index',
|
||||
templateUrl: 'index.html',
|
||||
})
|
||||
export class AddonModForumIndexPage implements OnInit {
|
||||
|
||||
title!: string;
|
||||
module!: CoreCourseAnyModuleData;
|
||||
courseId!: number;
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.module = CoreNavigator.instance.getRouteParam<CoreCourseAnyModuleData>('module')!;
|
||||
this.courseId = CoreNavigator.instance.getRouteNumberParam('courseId')!;
|
||||
this.title = this.module?.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update some data based on the forum instance.
|
||||
*
|
||||
* @param forum Forum instance.
|
||||
*/
|
||||
updateData(forum: AddonModForumData): void {
|
||||
this.title = forum.name || this.title;
|
||||
}
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,132 +0,0 @@
|
|||
// (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 { Injectable } from '@angular/core';
|
||||
import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
|
||||
import { CoreSitesCommonWSOptions, CoreSites } from '@services/sites';
|
||||
import { CoreWSExternalFile } from '@services/ws';
|
||||
import { makeSingleton } from '@singletons';
|
||||
|
||||
const ROOT_CACHE_KEY = 'mmaModForum:';
|
||||
|
||||
/**
|
||||
* Service that provides some features for forums.
|
||||
*
|
||||
* @todo Add all content.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonModForumProvider {
|
||||
|
||||
static readonly COMPONENT = 'mmaModForum';
|
||||
|
||||
/**
|
||||
* Get cache key for forum data WS calls.
|
||||
*
|
||||
* @param courseId Course ID.
|
||||
* @return Cache key.
|
||||
*/
|
||||
protected getForumDataCacheKey(courseId: number): string {
|
||||
return ROOT_CACHE_KEY + 'forum:' + courseId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all course forums.
|
||||
*
|
||||
* @param courseId Course ID.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when the forums are retrieved.
|
||||
*/
|
||||
async getCourseForums(courseId: number, options: CoreSitesCommonWSOptions = {}): Promise<AddonModForumData[]> {
|
||||
const site = await CoreSites.instance.getSite(options.siteId);
|
||||
|
||||
const params: AddonModForumGetForumsByCoursesWSParams = {
|
||||
courseids: [courseId],
|
||||
};
|
||||
const preSets: CoreSiteWSPreSets = {
|
||||
cacheKey: this.getForumDataCacheKey(courseId),
|
||||
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
||||
component: AddonModForumProvider.COMPONENT,
|
||||
...CoreSites.instance.getReadingStrategyPreSets(options.readingStrategy),
|
||||
};
|
||||
|
||||
return site.read('mod_forum_get_forums_by_courses', params, preSets);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates forum data.
|
||||
*
|
||||
* @param courseId Course ID.
|
||||
* @return Promise resolved when the data is invalidated.
|
||||
*/
|
||||
async invalidateForumData(courseId: number): Promise<void> {
|
||||
await CoreSites.instance.getCurrentSite()?.invalidateWsCacheForKey(this.getForumDataCacheKey(courseId));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class AddonModForum extends makeSingleton(AddonModForumProvider) {}
|
||||
|
||||
/**
|
||||
* Params of mod_forum_get_forums_by_courses WS.
|
||||
*/
|
||||
type AddonModForumGetForumsByCoursesWSParams = {
|
||||
courseids?: number[]; // Array of Course IDs.
|
||||
};
|
||||
|
||||
/**
|
||||
* General forum activity data.
|
||||
*/
|
||||
export type AddonModForumData = {
|
||||
id: number; // Forum id.
|
||||
course: number; // Course id.
|
||||
type: string; // The forum type.
|
||||
name: string; // Forum name.
|
||||
intro: string; // The forum intro.
|
||||
introformat: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
|
||||
introfiles?: CoreWSExternalFile[];
|
||||
duedate?: number; // Duedate for the user.
|
||||
cutoffdate?: number; // Cutoffdate for the user.
|
||||
assessed: number; // Aggregate type.
|
||||
assesstimestart: number; // Assess start time.
|
||||
assesstimefinish: number; // Assess finish time.
|
||||
scale: number; // Scale.
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
grade_forum: number; // Whole forum grade.
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
grade_forum_notify: number; // Whether to send notifications to students upon grading by default.
|
||||
maxbytes: number; // Maximum attachment size.
|
||||
maxattachments: number; // Maximum number of attachments.
|
||||
forcesubscribe: number; // Force users to subscribe.
|
||||
trackingtype: number; // Subscription mode.
|
||||
rsstype: number; // RSS feed for this activity.
|
||||
rssarticles: number; // Number of RSS recent articles.
|
||||
timemodified: number; // Time modified.
|
||||
warnafter: number; // Post threshold for warning.
|
||||
blockafter: number; // Post threshold for blocking.
|
||||
blockperiod: number; // Time period for blocking.
|
||||
completiondiscussions: number; // Student must create discussions.
|
||||
completionreplies: number; // Student must post replies.
|
||||
completionposts: number; // Student must post discussions or replies.
|
||||
cmid: number; // Course module id.
|
||||
numdiscussions?: number; // Number of discussions in the forum.
|
||||
cancreatediscussions?: boolean; // If the user can create discussions.
|
||||
lockdiscussionafter?: number; // After what period a discussion is locked.
|
||||
istracked?: boolean; // If the user is tracking the forum.
|
||||
unreadpostscount?: number; // The number of unread posts for tracked forums.
|
||||
};
|
||||
|
||||
/**
|
||||
* Data returned by mod_forum_get_forums_by_courses WS.
|
||||
*/
|
||||
export type AddonModForumGetForumsByCoursesWSResponse = AddonModForumData[];
|
|
@ -0,0 +1,173 @@
|
|||
// (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 { Injectable, Type } from '@angular/core';
|
||||
import { AddonModForum, AddonModForumProvider } from '../forum.service';
|
||||
import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/course';
|
||||
import { CoreCourseModule } from '@features/course/services/course-helper';
|
||||
import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
|
||||
import { makeSingleton, Translate } from '@singletons';
|
||||
import { CoreEvents } from '@singletons/events';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate';
|
||||
import { CoreConstants } from '@/core/constants';
|
||||
import { AddonModForumIndexComponent } from '../../components/index';
|
||||
|
||||
/**
|
||||
* Handler to support forum modules.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonModForumModuleHandlerService implements CoreCourseModuleHandler {
|
||||
|
||||
static readonly PAGE_NAME = 'mod_forum';
|
||||
|
||||
name = 'AddonModForum';
|
||||
modName = 'forum';
|
||||
|
||||
supportedFeatures = {
|
||||
[CoreConstants.FEATURE_GROUPS]: true,
|
||||
[CoreConstants.FEATURE_GROUPINGS]: true,
|
||||
[CoreConstants.FEATURE_MOD_INTRO]: true,
|
||||
[CoreConstants.FEATURE_COMPLETION_TRACKS_VIEWS]: true,
|
||||
[CoreConstants.FEATURE_COMPLETION_HAS_RULES]: true,
|
||||
[CoreConstants.FEATURE_GRADE_HAS_GRADE]: true,
|
||||
[CoreConstants.FEATURE_GRADE_OUTCOMES]: true,
|
||||
[CoreConstants.FEATURE_BACKUP_MOODLE2]: true,
|
||||
[CoreConstants.FEATURE_SHOW_DESCRIPTION]: true,
|
||||
[CoreConstants.FEATURE_RATE]: true,
|
||||
[CoreConstants.FEATURE_PLAGIARISM]: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the handler is enabled on a site level.
|
||||
*
|
||||
* @return Whether or not the handler is enabled on a site level.
|
||||
*/
|
||||
isEnabled(): Promise<boolean> {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data required to display the module in the course contents view.
|
||||
*
|
||||
* @param module The module object.
|
||||
* @param courseId The course ID.
|
||||
* @param sectionId The section ID.
|
||||
* @return Data to render the module.
|
||||
*/
|
||||
getData(module: CoreCourseAnyModuleData, courseId: number): CoreCourseModuleHandlerData {
|
||||
const data: CoreCourseModuleHandlerData = {
|
||||
icon: CoreCourse.instance.getModuleIconSrc(this.modName, 'modicon' in module ? module.modicon : undefined),
|
||||
title: module.name,
|
||||
class: 'addon-mod_forum-handler',
|
||||
showDownloadButton: true,
|
||||
action(_: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): void {
|
||||
options = options || {};
|
||||
options.params = options.params || {};
|
||||
Object.assign(options.params, { module });
|
||||
|
||||
CoreNavigator.instance.navigateToSitePath(
|
||||
`${AddonModForumModuleHandlerService.PAGE_NAME}/${courseId}/${module.id}`,
|
||||
options,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
if ('afterlink' in module && !!module.afterlink) {
|
||||
data.extraBadgeColor = '';
|
||||
const match = />(\d+)[^<]+/.exec(module.afterlink);
|
||||
data.extraBadge = match ? Translate.instance.instant('addon.mod_forum.unreadpostsnumber', { $a : match[1] }) : '';
|
||||
} else {
|
||||
this.updateExtraBadge(data, courseId, module.id);
|
||||
}
|
||||
|
||||
const event = CoreEvents.on(
|
||||
AddonModForumProvider.MARK_READ_EVENT,
|
||||
(eventData: { courseId?: number; moduleId?: number; siteId?: string }) => {
|
||||
if (eventData.courseId !== courseId || eventData.moduleId !== module.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateExtraBadge(data, eventData.courseId, eventData.moduleId, eventData.siteId);
|
||||
},
|
||||
CoreSites.instance.getCurrentSiteId(),
|
||||
);
|
||||
|
||||
data.onDestroy = () => event.off();
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the component to render the module. This is needed to support singleactivity course format.
|
||||
* The component returned must implement CoreCourseModuleMainComponent.
|
||||
*
|
||||
* @return The component to use, undefined if not found.
|
||||
*/
|
||||
async getMainComponent(): Promise<Type<unknown> | undefined> {
|
||||
return AddonModForumIndexComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to display the course refresher in single activity course format. If it returns false, a refresher must be
|
||||
* included in the template that calls the doRefresh method of the component. Defaults to true.
|
||||
*
|
||||
* @return Whether the refresher should be displayed.
|
||||
*/
|
||||
displayRefresherInSingleActivity(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers an update for the extra badge text.
|
||||
*
|
||||
* @param data Course Module Handler data.
|
||||
* @param courseId Course ID.
|
||||
* @param moduleId Course module ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
*/
|
||||
async updateExtraBadge(data: CoreCourseModuleHandlerData, courseId: number, moduleId: number, siteId?: string): Promise<void> {
|
||||
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||||
|
||||
if (!siteId) {
|
||||
return;
|
||||
}
|
||||
|
||||
data.extraBadge = Translate.instance.instant('core.loading');
|
||||
data.extraBadgeColor = 'light';
|
||||
|
||||
await CoreUtils.instance.ignoreErrors(AddonModForum.instance.invalidateForumData(courseId));
|
||||
|
||||
try {
|
||||
// Handle unread posts.
|
||||
const forum = await AddonModForum.instance.getForum(courseId, moduleId, { siteId });
|
||||
|
||||
data.extraBadgeColor = '';
|
||||
data.extraBadge = forum.unreadpostscount
|
||||
? Translate.instance.instant(
|
||||
'addon.mod_forum.unreadpostsnumber',
|
||||
{ $a : forum.unreadpostscount },
|
||||
)
|
||||
: '';
|
||||
} catch (error) {
|
||||
// Ignore errors.
|
||||
data.extraBadgeColor = '';
|
||||
data.extraBadge = '';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class AddonModForumModuleHandler extends makeSingleton(AddonModForumModuleHandlerService) {}
|
|
@ -0,0 +1,507 @@
|
|||
// (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 { Injectable } from '@angular/core';
|
||||
import { CoreFileEntry, CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
|
||||
import { CoreUser } from '@features/user/services/user';
|
||||
import { CoreApp } from '@services/app';
|
||||
import { CoreFile } from '@services/file';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreTimeUtils } from '@services/utils/time';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { makeSingleton, Translate } from '@singletons';
|
||||
import { AddonModForum, AddonModForumData, AddonModForumProvider } from './forum.service';
|
||||
import { AddonModForumOffline } from './offline.service';
|
||||
|
||||
/**
|
||||
* Service that provides some features for forums.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonModForumHelperProvider {
|
||||
|
||||
/**
|
||||
* Add a new discussion.
|
||||
*
|
||||
* @param forumId Forum ID.
|
||||
* @param name Forum name.
|
||||
* @param courseId Course ID the forum belongs to.
|
||||
* @param subject New discussion's subject.
|
||||
* @param message New discussion's message.
|
||||
* @param attachments New discussion's attachments.
|
||||
* @param options Options (subscribe, pin, ...).
|
||||
* @param groupIds Groups this discussion belongs to.
|
||||
* @param timeCreated The time the discussion was created. Only used when editing discussion.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with ids of the created discussions or null if stored offline
|
||||
*/
|
||||
async addNewDiscussion(
|
||||
forumId: number,
|
||||
name: string,
|
||||
courseId: number,
|
||||
subject: string,
|
||||
message: string,
|
||||
attachments?: any[],
|
||||
options?: any,
|
||||
groupIds?: number[],
|
||||
timeCreated?: number,
|
||||
siteId?: string,
|
||||
): Promise<number[] | null> {
|
||||
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||||
groupIds = (groupIds && groupIds.length > 0) ? groupIds : [0];
|
||||
|
||||
let saveOffline = false;
|
||||
const attachmentsIds: number[] = [];
|
||||
let offlineAttachments: any;
|
||||
|
||||
// Convenience function to store a message to be synchronized later.
|
||||
const storeOffline = async (): Promise<void> => {
|
||||
// Multiple groups, the discussion is being posted to all groups.
|
||||
const groupId = groupIds!.length > 1 ? AddonModForumProvider.ALL_GROUPS : groupIds![0];
|
||||
|
||||
if (offlineAttachments) {
|
||||
options.attachmentsid = offlineAttachments;
|
||||
}
|
||||
|
||||
await AddonModForumOffline.instance.addNewDiscussion(
|
||||
forumId,
|
||||
name,
|
||||
courseId,
|
||||
subject,
|
||||
message,
|
||||
options,
|
||||
groupId,
|
||||
timeCreated,
|
||||
siteId,
|
||||
);
|
||||
};
|
||||
|
||||
// First try to upload attachments, once per group.
|
||||
if (attachments && attachments.length > 0) {
|
||||
const promises = groupIds.map(
|
||||
() => this
|
||||
.uploadOrStoreNewDiscussionFiles(forumId, timeCreated || 0, attachments, false)
|
||||
.then(attach => attachmentsIds.push(attach)),
|
||||
);
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
} catch (error) {
|
||||
// Cannot upload them in online, save them in offline.
|
||||
saveOffline = true;
|
||||
|
||||
const attach = await this.uploadOrStoreNewDiscussionFiles(forumId, timeCreated || 0, attachments, true);
|
||||
|
||||
offlineAttachments = attach;
|
||||
}
|
||||
}
|
||||
|
||||
// If we are editing an offline discussion, discard previous first.
|
||||
if (timeCreated) {
|
||||
await AddonModForumOffline.instance.deleteNewDiscussion(forumId, timeCreated, siteId);
|
||||
}
|
||||
|
||||
if (saveOffline || !CoreApp.instance.isOnline()) {
|
||||
await storeOffline();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const errors: Error[] = [];
|
||||
const discussionIds: number[] = [];
|
||||
const promises = groupIds.map(async (groupId, index) => {
|
||||
const groupOptions = CoreUtils.instance.clone(options);
|
||||
|
||||
if (attachmentsIds[index]) {
|
||||
groupOptions.attachmentsid = attachmentsIds[index];
|
||||
}
|
||||
|
||||
try {
|
||||
const discussionId = await AddonModForum.instance.addNewDiscussionOnline(
|
||||
forumId,
|
||||
subject,
|
||||
message,
|
||||
groupOptions,
|
||||
groupId,
|
||||
siteId,
|
||||
);
|
||||
|
||||
discussionIds.push(discussionId);
|
||||
} catch (error) {
|
||||
errors.push(error);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
if (errors.length == groupIds.length) {
|
||||
// All requests have failed.
|
||||
for (let i = 0; i < errors.length; i++) {
|
||||
if (CoreUtils.instance.isWebServiceError(errors[i]) || (attachments && attachments.length > 0)) {
|
||||
// The WebService has thrown an error or offline not supported, reject.
|
||||
throw errors[i];
|
||||
}
|
||||
}
|
||||
|
||||
// Couldn't connect to server, store offline.
|
||||
await storeOffline();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return discussionIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert offline reply to online format in order to be compatible with them.
|
||||
*
|
||||
* @param offlineReply Offline version of the reply.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with the object converted to Online.
|
||||
*/
|
||||
convertOfflineReplyToOnline(offlineReply: any, siteId?: string): Promise<any> {
|
||||
const reply: any = {
|
||||
id: -offlineReply.timecreated,
|
||||
discussionid: offlineReply.discussionid,
|
||||
parentid: offlineReply.postid,
|
||||
hasparent: !!offlineReply.postid,
|
||||
author: {
|
||||
id: offlineReply.userid,
|
||||
},
|
||||
timecreated: false,
|
||||
subject: offlineReply.subject,
|
||||
message: offlineReply.message,
|
||||
attachments: [],
|
||||
capabilities: {
|
||||
reply: false,
|
||||
},
|
||||
unread: false,
|
||||
isprivatereply: offlineReply.options && offlineReply.options.private,
|
||||
tags: null,
|
||||
};
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
// Treat attachments if any.
|
||||
if (offlineReply.options && offlineReply.options.attachmentsid) {
|
||||
reply.attachments = offlineReply.options.attachmentsid.online || [];
|
||||
|
||||
if (offlineReply.options.attachmentsid.offline) {
|
||||
promises.push(
|
||||
this
|
||||
.getReplyStoredFiles(offlineReply.forumid, reply.parentid, siteId, offlineReply.userid)
|
||||
.then(files => reply.attachments = reply.attachments.concat(files)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Get user data.
|
||||
promises.push(
|
||||
CoreUtils.instance.ignoreErrors(
|
||||
CoreUser.instance
|
||||
.getProfile(offlineReply.userid, offlineReply.courseid, true)
|
||||
.then(user => {
|
||||
reply.author.fullname = user.fullname;
|
||||
reply.author.urls = { profileimage: user.profileimageurl };
|
||||
|
||||
return;
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
reply.attachment = reply.attachments.length > 0 ? 1 : 0;
|
||||
|
||||
return reply;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete stored attachment files for a new discussion.
|
||||
*
|
||||
* @param forumId Forum ID.
|
||||
* @param timecreated The time the discussion was created.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved when deleted.
|
||||
*/
|
||||
async deleteNewDiscussionStoredFiles(forumId: number, timecreated: number, siteId?: string): Promise<void> {
|
||||
const folderPath = await AddonModForumOffline.instance.getNewDiscussionFolder(forumId, timecreated, siteId);
|
||||
|
||||
// Ignore any errors, CoreFileProvider.removeDir fails if folder doesn't exist.
|
||||
await CoreUtils.instance.ignoreErrors(CoreFile.instance.removeDir(folderPath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete stored attachment files for a reply.
|
||||
*
|
||||
* @param forumId Forum ID.
|
||||
* @param postId ID of the post being replied.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param userId User the reply belongs to. If not defined, current user in site.
|
||||
* @return Promise resolved when deleted.
|
||||
*/
|
||||
async deleteReplyStoredFiles(forumId: number, postId: number, siteId?: string, userId?: number): Promise<void> {
|
||||
const folderPath = await AddonModForumOffline.instance.getReplyFolder(forumId, postId, siteId, userId);
|
||||
|
||||
// Ignore any errors, CoreFileProvider.removeDir fails if folder doesn't exist.
|
||||
await CoreUtils.instance.ignoreErrors(CoreFile.instance.removeDir(folderPath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the availability message of the given forum.
|
||||
*
|
||||
* @param forum Forum instance.
|
||||
* @return Message or null if the forum has no cut-off or due date.
|
||||
*/
|
||||
getAvailabilityMessage(forum: AddonModForumData): string | null {
|
||||
if (this.isCutoffDateReached(forum)) {
|
||||
return Translate.instance.instant('addon.mod_forum.cutoffdatereached');
|
||||
}
|
||||
|
||||
if (this.isDueDateReached(forum)) {
|
||||
const dueDate = CoreTimeUtils.instance.userDate(forum.duedate * 1000);
|
||||
|
||||
return Translate.instance.instant('addon.mod_forum.thisforumisdue', { $a: dueDate });
|
||||
}
|
||||
|
||||
if ((forum.duedate ?? 0) > 0) {
|
||||
const dueDate = CoreTimeUtils.instance.userDate(forum.duedate! * 1000);
|
||||
|
||||
return Translate.instance.instant('addon.mod_forum.thisforumhasduedate', { $a: dueDate });
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a forum discussion by id.
|
||||
*
|
||||
* This function is inefficient because it needs to fetch all discussion pages in the worst case.
|
||||
*
|
||||
* @param forumId Forum ID.
|
||||
* @param cmId Forum cmid
|
||||
* @param discussionId Discussion ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with the discussion data.
|
||||
*/
|
||||
getDiscussionById(forumId: number, cmId: number, discussionId: number, siteId?: string): Promise<any> {
|
||||
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||||
|
||||
const findDiscussion = async (page: number): Promise<any> => {
|
||||
const response = await AddonModForum.instance.getDiscussions(forumId, {
|
||||
cmId,
|
||||
page,
|
||||
siteId,
|
||||
});
|
||||
|
||||
if (response.discussions && response.discussions.length > 0) {
|
||||
// Note that discussion.id is the main post ID but discussion ID is discussion.discussion.
|
||||
const discussion = response.discussions.find((discussion) => discussion.discussion == discussionId);
|
||||
|
||||
if (discussion) {
|
||||
return discussion;
|
||||
}
|
||||
|
||||
if (response.canLoadMore) {
|
||||
return findDiscussion(page + 1);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Discussion not found');
|
||||
};
|
||||
|
||||
return findDiscussion(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of stored attachment files for a new discussion. See AddonModForumHelper#storeNewDiscussionFiles.
|
||||
*
|
||||
* @param forumId Forum ID.
|
||||
* @param timecreated The time the discussion was created.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with the files.
|
||||
*/
|
||||
async getNewDiscussionStoredFiles(forumId: number, timecreated: number, siteId?: string): Promise<any[]> {
|
||||
const folderPath = await AddonModForumOffline.instance.getNewDiscussionFolder(forumId, timecreated, siteId);
|
||||
|
||||
return CoreFileUploader.instance.getStoredFiles(folderPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of stored attachment files for a reply. See AddonModForumHelper#storeReplyFiles.
|
||||
*
|
||||
* @param forumId Forum ID.
|
||||
* @param postId ID of the post being replied.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param userId User the reply belongs to. If not defined, current user in site.
|
||||
* @return Promise resolved with the files.
|
||||
*/
|
||||
async getReplyStoredFiles(forumId: number, postId: number, siteId?: string, userId?: number): Promise<any[]> {
|
||||
const folderPath = await AddonModForumOffline.instance.getReplyFolder(forumId, postId, siteId, userId);
|
||||
|
||||
return CoreFileUploader.instance.getStoredFiles(folderPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the data of a post/discussion has changed.
|
||||
*
|
||||
* @param post Current data.
|
||||
* @param original Original ata.
|
||||
* @return True if data has changed, false otherwise.
|
||||
*/
|
||||
hasPostDataChanged(post: any, original?: any): boolean {
|
||||
if (!original || original.subject == null) {
|
||||
// There is no original data, assume it hasn't changed.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (post.subject != original.subject || post.message != original.message) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (post.isprivatereply != original.isprivatereply) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return CoreFileUploader.instance.areFileListDifferent(post.files, original.files);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the cutoff date for the forum reached?
|
||||
*
|
||||
* @param forum Forum instance.
|
||||
*/
|
||||
isCutoffDateReached(forum: any): boolean {
|
||||
const now = Date.now() / 1000;
|
||||
|
||||
return forum.cutoffdate > 0 && forum.cutoffdate < now;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the due date for the forum reached?
|
||||
*
|
||||
* @param forum Forum instance.
|
||||
*/
|
||||
isDueDateReached(forum: AddonModForumData): forum is AddonModForumData & { duedate: number } {
|
||||
const now = Date.now() / 1000;
|
||||
const duedate = forum.duedate ?? 0;
|
||||
|
||||
return duedate > 0 && duedate < now;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of files (either online files or local files), store the local files in a local folder
|
||||
* to be submitted later.
|
||||
*
|
||||
* @param forumId Forum ID.
|
||||
* @param timecreated The time the discussion was created.
|
||||
* @param files List of files.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved if success, rejected otherwise.
|
||||
*/
|
||||
async storeNewDiscussionFiles(
|
||||
forumId: number,
|
||||
timecreated: number,
|
||||
files: CoreFileEntry[],
|
||||
siteId?: string,
|
||||
): Promise<CoreFileUploaderStoreFilesResult> {
|
||||
// Get the folder where to store the files.
|
||||
const folderPath = await AddonModForumOffline.instance.getNewDiscussionFolder(forumId, timecreated, siteId);
|
||||
|
||||
return CoreFileUploader.instance.storeFilesToUpload(folderPath, files);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of files (either online files or local files), store the local files in a local folder
|
||||
* to be submitted later.
|
||||
*
|
||||
* @param forumId Forum ID.
|
||||
* @param postId ID of the post being replied.
|
||||
* @param files List of files.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param userId User the reply belongs to. If not defined, current user in site.
|
||||
* @return Promise resolved if success, rejected otherwise.
|
||||
*/
|
||||
async storeReplyFiles(forumId: number, postId: number, files: any[], siteId?: string, userId?: number): Promise<void> {
|
||||
// Get the folder where to store the files.
|
||||
const folderPath = await AddonModForumOffline.instance.getReplyFolder(forumId, postId, siteId, userId);
|
||||
|
||||
await CoreFileUploader.instance.storeFilesToUpload(folderPath, files);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload or store some files for a new discussion, depending if the user is offline or not.
|
||||
*
|
||||
* @param forumId Forum ID.
|
||||
* @param timecreated The time the discussion was created.
|
||||
* @param files List of files.
|
||||
* @param offline True if files sould be stored for offline, false to upload them.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved if success.
|
||||
*/
|
||||
uploadOrStoreNewDiscussionFiles(
|
||||
forumId: number,
|
||||
timecreated: number,
|
||||
files: CoreFileEntry[],
|
||||
offline: true,
|
||||
siteId?: string,
|
||||
): Promise<CoreFileUploaderStoreFilesResult>;
|
||||
uploadOrStoreNewDiscussionFiles(
|
||||
forumId: number,
|
||||
timecreated: number,
|
||||
files: CoreFileEntry[],
|
||||
offline: false,
|
||||
siteId?: string,
|
||||
): Promise<number>;
|
||||
uploadOrStoreNewDiscussionFiles(
|
||||
forumId: number,
|
||||
timecreated: number,
|
||||
files: CoreFileEntry[],
|
||||
offline: boolean,
|
||||
siteId?: string,
|
||||
): Promise<CoreFileUploaderStoreFilesResult | number> {
|
||||
if (offline) {
|
||||
return this.storeNewDiscussionFiles(forumId, timecreated, files, siteId);
|
||||
} else {
|
||||
return CoreFileUploader.instance.uploadOrReuploadFiles(files, AddonModForumProvider.COMPONENT, forumId, siteId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload or store some files for a reply, depending if the user is offline or not.
|
||||
*
|
||||
* @param forumId Forum ID.
|
||||
* @param postId ID of the post being replied.
|
||||
* @param files List of files.
|
||||
* @param offline True if files sould be stored for offline, false to upload them.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param userId User the reply belongs to. If not defined, current user in site.
|
||||
* @return Promise resolved if success.
|
||||
*/
|
||||
uploadOrStoreReplyFiles(
|
||||
forumId: number,
|
||||
postId: number,
|
||||
files: any[],
|
||||
offline: boolean,
|
||||
siteId?: string,
|
||||
userId?: number,
|
||||
): Promise<any> {
|
||||
if (offline) {
|
||||
return this.storeReplyFiles(forumId, postId, files, siteId, userId);
|
||||
} else {
|
||||
return CoreFileUploader.instance.uploadOrReuploadFiles(files, AddonModForumProvider.COMPONENT, forumId, siteId);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class AddonModForumHelper extends makeSingleton(AddonModForumHelperProvider) {}
|
|
@ -0,0 +1,115 @@
|
|||
// (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 { CoreSiteSchema } from '@services/sites';
|
||||
|
||||
/**
|
||||
* Database variables for AddonModForum service.
|
||||
*/
|
||||
export const DISCUSSIONS_TABLE = 'addon_mod_forum_discussions';
|
||||
export const REPLIES_TABLE = 'addon_mod_forum_replies';
|
||||
export const SITE_SCHEMA: CoreSiteSchema = {
|
||||
name: 'AddonModForumOfflineProvider',
|
||||
version: 1,
|
||||
tables: [
|
||||
{
|
||||
name: DISCUSSIONS_TABLE,
|
||||
columns: [
|
||||
{
|
||||
name: 'forumid',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'TEXT',
|
||||
},
|
||||
{
|
||||
name: 'courseid',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'subject',
|
||||
type: 'TEXT',
|
||||
},
|
||||
{
|
||||
name: 'message',
|
||||
type: 'TEXT',
|
||||
},
|
||||
{
|
||||
name: 'options',
|
||||
type: 'TEXT',
|
||||
},
|
||||
{
|
||||
name: 'groupid',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'userid',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'timecreated',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
],
|
||||
primaryKeys: ['forumid', 'userid', 'timecreated'],
|
||||
},
|
||||
{
|
||||
name: REPLIES_TABLE,
|
||||
columns: [
|
||||
{
|
||||
name: 'postid',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'discussionid',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'forumid',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'TEXT',
|
||||
},
|
||||
{
|
||||
name: 'courseid',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'subject',
|
||||
type: 'TEXT',
|
||||
},
|
||||
{
|
||||
name: 'message',
|
||||
type: 'TEXT',
|
||||
},
|
||||
{
|
||||
name: 'options',
|
||||
type: 'TEXT',
|
||||
},
|
||||
{
|
||||
name: 'userid',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'timecreated',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
],
|
||||
primaryKeys: ['postid', 'userid'],
|
||||
},
|
||||
],
|
||||
};
|
|
@ -0,0 +1,436 @@
|
|||
// (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 { Injectable } from '@angular/core';
|
||||
import { CoreFile } from '@services/file';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { AddonModForumProvider } from './forum.service';
|
||||
import { DISCUSSIONS_TABLE, REPLIES_TABLE } from './offline-db';
|
||||
|
||||
/**
|
||||
* Service to handle offline forum.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonModForumOfflineProvider {
|
||||
|
||||
/**
|
||||
* Delete a forum offline discussion.
|
||||
*
|
||||
* @param forumId Forum ID.
|
||||
* @param timeCreated The time the discussion was created.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param userId User the discussion belongs to. If not defined, current user in site.
|
||||
* @return Promise resolved if stored, rejected if failure.
|
||||
*/
|
||||
async deleteNewDiscussion(forumId: number, timeCreated: number, siteId?: string, userId?: number): Promise<void> {
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
const conditions = {
|
||||
forumid: forumId,
|
||||
userid: userId || site.getUserId(),
|
||||
timecreated: timeCreated,
|
||||
};
|
||||
|
||||
await site.getDb().deleteRecords(DISCUSSIONS_TABLE, conditions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a forum offline discussion.
|
||||
*
|
||||
* @param forumId Forum ID.
|
||||
* @param timeCreated The time the discussion was created.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param userId User the discussion belongs to. If not defined, current user in site.
|
||||
* @return Promise resolved if stored, rejected if failure.
|
||||
*/
|
||||
async getNewDiscussion(
|
||||
forumId: number,
|
||||
timeCreated: number,
|
||||
siteId?: string,
|
||||
userId?: number,
|
||||
): Promise<AddonModForumOfflineDiscussion> {
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
const conditions = {
|
||||
forumid: forumId,
|
||||
userid: userId || site.getUserId(),
|
||||
timecreated: timeCreated,
|
||||
};
|
||||
|
||||
const record = await site.getDb().getRecord<AddonModForumOfflineDiscussionRecord>(DISCUSSIONS_TABLE, conditions);
|
||||
|
||||
return this.parseRecordOptions(record);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all offline new discussions.
|
||||
*
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with discussions.
|
||||
*/
|
||||
async getAllNewDiscussions(siteId?: string): Promise<AddonModForumOfflineDiscussion[]> {
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
const records = await site.getDb().getRecords<AddonModForumOfflineDiscussionRecord>(DISCUSSIONS_TABLE);
|
||||
|
||||
return this.parseRecordsOptions(records);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there are offline new discussions to send.
|
||||
*
|
||||
* @param forumId Forum ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param userId User the discussions belong to. If not defined, current user in site.
|
||||
* @return Promise resolved with boolean: true if has offline answers, false otherwise.
|
||||
*/
|
||||
async hasNewDiscussions(forumId: number, siteId?: string, userId?: number): Promise<boolean> {
|
||||
try {
|
||||
const discussions = await this.getNewDiscussions(forumId, siteId, userId);
|
||||
|
||||
return !!discussions.length;
|
||||
} catch (error) {
|
||||
// No offline data found, return false.
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get new discussions to be synced.
|
||||
*
|
||||
* @param forumId Forum ID to get.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param userId User the discussions belong to. If not defined, current user in site.
|
||||
* @return Promise resolved with the object to be synced.
|
||||
*/
|
||||
async getNewDiscussions(forumId: number, siteId?: string, userId?: number): Promise<AddonModForumOfflineDiscussion[]> {
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
const conditions = {
|
||||
forumid: forumId,
|
||||
userid: userId || site.getUserId(),
|
||||
};
|
||||
|
||||
const records = await site.getDb().getRecords<AddonModForumOfflineDiscussionRecord>(DISCUSSIONS_TABLE, conditions);
|
||||
|
||||
return this.parseRecordsOptions(records);
|
||||
}
|
||||
|
||||
/**
|
||||
* Offline version for adding a new discussion to a forum.
|
||||
*
|
||||
* @param forumId Forum ID.
|
||||
* @param name Forum name.
|
||||
* @param courseId Course ID the forum belongs to.
|
||||
* @param subject New discussion's subject.
|
||||
* @param message New discussion's message.
|
||||
* @param options Options (subscribe, pin, ...).
|
||||
* @param groupId Group this discussion belongs to.
|
||||
* @param timeCreated The time the discussion was created. If not defined, current time.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param userId User the discussion belong to. If not defined, current user in site.
|
||||
* @return Promise resolved when new discussion is successfully saved.
|
||||
*/
|
||||
async addNewDiscussion(
|
||||
forumId: number,
|
||||
name: string,
|
||||
courseId: number,
|
||||
subject: string,
|
||||
message: string,
|
||||
options?: AddonModForumDiscussionOptions,
|
||||
groupId?: number,
|
||||
timeCreated?: number,
|
||||
siteId?: string,
|
||||
userId?: number,
|
||||
): Promise<void> {
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
const data: AddonModForumOfflineDiscussionRecord = {
|
||||
forumid: forumId,
|
||||
name: name,
|
||||
courseid: courseId,
|
||||
subject: subject,
|
||||
message: message,
|
||||
options: JSON.stringify(options || {}),
|
||||
groupid: groupId || AddonModForumProvider.ALL_PARTICIPANTS,
|
||||
userid: userId || site.getUserId(),
|
||||
timecreated: timeCreated || new Date().getTime(),
|
||||
};
|
||||
|
||||
await site.getDb().insertRecord(DISCUSSIONS_TABLE, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete forum offline replies.
|
||||
*
|
||||
* @param postId ID of the post being replied.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param userId User the reply belongs to. If not defined, current user in site.
|
||||
* @return Promise resolved if stored, rejected if failure.
|
||||
*/
|
||||
async deleteReply(postId: number, siteId?: string, userId?: number): Promise<void> {
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
const conditions = {
|
||||
postid: postId,
|
||||
userid: userId || site.getUserId(),
|
||||
};
|
||||
|
||||
await site.getDb().deleteRecords(REPLIES_TABLE, conditions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all offline replies.
|
||||
*
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with replies.
|
||||
*/
|
||||
async getAllReplies(siteId?: string): Promise<AddonModForumOfflineReply[]> {
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
const records = await site.getDb().getRecords<AddonModForumOfflineReplyRecord>(REPLIES_TABLE);
|
||||
|
||||
return this.parseRecordsOptions(records);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there is an offline reply for a forum to be synced.
|
||||
*
|
||||
* @param forumId ID of the forum being replied.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param userId User the replies belong to. If not defined, current user in site.
|
||||
* @return Promise resolved with boolean: true if has offline answers, false otherwise.
|
||||
*/
|
||||
async hasForumReplies(forumId: number, siteId?: string, userId?: number): Promise<boolean> {
|
||||
try {
|
||||
const replies = await this.getForumReplies(forumId, siteId, userId);
|
||||
|
||||
return !!replies.length;
|
||||
} catch (error) {
|
||||
// No offline data found, return false.
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the replies of a forum to be synced.
|
||||
*
|
||||
* @param forumId ID of the forum being replied.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param userId User the replies belong to. If not defined, current user in site.
|
||||
* @return Promise resolved with replies.
|
||||
*/
|
||||
async getForumReplies(forumId: number, siteId?: string, userId?: number): Promise<AddonModForumOfflineReply[]> {
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
const conditions = {
|
||||
forumid: forumId,
|
||||
userid: userId || site.getUserId(),
|
||||
};
|
||||
|
||||
const records = await site.getDb().getRecords<AddonModForumOfflineReplyRecord>(REPLIES_TABLE, conditions);
|
||||
|
||||
return this.parseRecordsOptions(records);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there is an offline reply to be synced.
|
||||
*
|
||||
* @param discussionId ID of the discussion the user is replying to.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param userId User the replies belong to. If not defined, current user in site.
|
||||
* @return Promise resolved with boolean: true if has offline answers, false otherwise.
|
||||
*/
|
||||
async hasDiscussionReplies(discussionId: number, siteId?: string, userId?: number): Promise<boolean> {
|
||||
try {
|
||||
const replies = await this.getDiscussionReplies(discussionId, siteId, userId);
|
||||
|
||||
return !!replies.length;
|
||||
} catch (error) {
|
||||
// No offline data found, return false.
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the replies of a discussion to be synced.
|
||||
*
|
||||
* @param discussionId ID of the discussion the user is replying to.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param userId User the replies belong to. If not defined, current user in site.
|
||||
* @return Promise resolved with discussions.
|
||||
*/
|
||||
async getDiscussionReplies(discussionId: number, siteId?: string, userId?: number): Promise<AddonModForumOfflineReply[]> {
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
const conditions = {
|
||||
discussionid: discussionId,
|
||||
userid: userId || site.getUserId(),
|
||||
};
|
||||
|
||||
const records = await site.getDb().getRecords<AddonModForumOfflineReplyRecord>(REPLIES_TABLE, conditions);
|
||||
|
||||
return this.parseRecordsOptions(records);
|
||||
}
|
||||
|
||||
/**
|
||||
* Offline version for replying to a certain post.
|
||||
*
|
||||
* @param postId ID of the post being replied.
|
||||
* @param discussionId ID of the discussion the user is replying to.
|
||||
* @param forumId ID of the forum the user is replying to.
|
||||
* @param name Forum name.
|
||||
* @param courseId Course ID the forum belongs to.
|
||||
* @param subject New post's subject.
|
||||
* @param message New post's message.
|
||||
* @param options Options (subscribe, attachments, ...).
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param userId User the post belong to. If not defined, current user in site.
|
||||
* @return Promise resolved when the post is created.
|
||||
*/
|
||||
async replyPost(
|
||||
postId: number,
|
||||
discussionId: number,
|
||||
forumId: number,
|
||||
name: string,
|
||||
courseId: number,
|
||||
subject: string,
|
||||
message: string,
|
||||
options?: AddonModForumReplyOptions,
|
||||
siteId?: string,
|
||||
userId?: number,
|
||||
): Promise<void> {
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
const data: AddonModForumOfflineReplyRecord = {
|
||||
postid: postId,
|
||||
discussionid: discussionId,
|
||||
forumid: forumId,
|
||||
name: name,
|
||||
courseid: courseId,
|
||||
subject: subject,
|
||||
message: message,
|
||||
options: JSON.stringify(options || {}),
|
||||
userid: userId || site.getUserId(),
|
||||
timecreated: new Date().getTime(),
|
||||
};
|
||||
|
||||
await site.getDb().insertRecord(REPLIES_TABLE, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the folder where to store files for offline attachments in a forum.
|
||||
*
|
||||
* @param forumId Forum ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with the path.
|
||||
*/
|
||||
async getForumFolder(forumId: number, siteId?: string): Promise<string> {
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
const siteFolderPath = CoreFile.instance.getSiteFolder(site.getId());
|
||||
|
||||
return CoreTextUtils.instance.concatenatePaths(siteFolderPath, 'offlineforum/' + forumId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the folder where to store files for a new offline discussion.
|
||||
*
|
||||
* @param forumId Forum ID.
|
||||
* @param timeCreated The time the discussion was created.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with the path.
|
||||
*/
|
||||
async getNewDiscussionFolder(forumId: number, timeCreated: number, siteId?: string): Promise<string> {
|
||||
const folderPath = await this.getForumFolder(forumId, siteId);
|
||||
|
||||
return CoreTextUtils.instance.concatenatePaths(folderPath, 'newdisc_' + timeCreated);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the folder where to store files for a new offline reply.
|
||||
*
|
||||
* @param forumId Forum ID.
|
||||
* @param postId ID of the post being replied.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param userId User the replies belong to. If not defined, current user in site.
|
||||
* @return Promise resolved with the path.
|
||||
*/
|
||||
async getReplyFolder(forumId: number, postId: number, siteId?: string, userId?: number): Promise<string> {
|
||||
const folderPath = await this.getForumFolder(forumId, siteId);
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
userId = userId || site.getUserId();
|
||||
|
||||
return CoreTextUtils.instance.concatenatePaths(folderPath, 'reply_' + postId + '_' + userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse "options" column of fetched record.
|
||||
*
|
||||
* @param records List of records.
|
||||
* @return List of records with options parsed.
|
||||
*/
|
||||
protected parseRecordsOptions<
|
||||
R extends { options: string },
|
||||
O extends Record<string, unknown> = Record<string, unknown>
|
||||
>(records: R[]): (Omit<R, 'options'> & { options: O })[] {
|
||||
return records.map(record => this.parseRecordOptions(record));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse "options" column of fetched record.
|
||||
*
|
||||
* @param record Record.
|
||||
* @return Record with options parsed.
|
||||
*/
|
||||
protected parseRecordOptions<
|
||||
R extends { options: string },
|
||||
O extends Record<string, unknown> = Record<string, unknown>
|
||||
>(record: R): Omit<R, 'options'> & { options: O } {
|
||||
record.options = CoreTextUtils.instance.parseJSON(record.options);
|
||||
|
||||
return record as unknown as Omit<R, 'options'> & { options: O };
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class AddonModForumOffline extends makeSingleton(AddonModForumOfflineProvider) {}
|
||||
|
||||
export type AddonModForumDiscussionOptions = Record<string, unknown>;
|
||||
export type AddonModForumReplyOptions = Record<string, unknown>;
|
||||
|
||||
export type AddonModForumOfflineDiscussion = {
|
||||
forumid: number;
|
||||
name: string;
|
||||
courseid: number;
|
||||
subject: string;
|
||||
message: string;
|
||||
options: AddonModForumDiscussionOptions;
|
||||
groupid: number;
|
||||
userid: number;
|
||||
timecreated: number;
|
||||
};
|
||||
export type AddonModForumOfflineReply = {
|
||||
postid: number;
|
||||
discussionid: number;
|
||||
forumid: number;
|
||||
name: string;
|
||||
courseid: number;
|
||||
subject: string;
|
||||
message: string;
|
||||
options: AddonModForumReplyOptions;
|
||||
userid: number;
|
||||
timecreated: number;
|
||||
};
|
||||
|
||||
export type AddonModForumOfflineDiscussionRecord = Omit<AddonModForumOfflineDiscussion, 'options'> & {
|
||||
options: string;
|
||||
};
|
||||
export type AddonModForumOfflineReplyRecord = Omit<AddonModForumOfflineReply, 'options'> & {
|
||||
options: string;
|
||||
};
|
|
@ -0,0 +1,600 @@
|
|||
// (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 { Injectable } from '@angular/core';
|
||||
import { CoreSyncBaseProvider } from '@classes/base-sync';
|
||||
import { CoreCourse } from '@features/course/services/course';
|
||||
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
|
||||
import { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
|
||||
import { CoreApp } from '@services/app';
|
||||
import { CoreGroups } from '@services/groups';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreSync } from '@services/sync';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { Translate } from '@singletons';
|
||||
import { CoreArray } from '@singletons/array';
|
||||
import { CoreEvents } from '@singletons/events';
|
||||
import { AddonModForum, AddonModForumProvider } from './forum.service';
|
||||
import { AddonModForumHelper } from './helper.service';
|
||||
import { AddonModForumOffline, AddonModForumOfflineDiscussion, AddonModForumOfflineReply } from './offline.service';
|
||||
|
||||
/**
|
||||
* Service to sync forums.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonModForumSyncProvider extends CoreSyncBaseProvider<AddonModForumSyncResult> {
|
||||
|
||||
static readonly AUTO_SYNCED = 'addon_mod_forum_autom_synced';
|
||||
static readonly MANUAL_SYNCED = 'addon_mod_forum_manual_synced';
|
||||
|
||||
private _componentTranslate?: string;
|
||||
|
||||
constructor() {
|
||||
super('AddonModForumSyncProvider');
|
||||
}
|
||||
|
||||
protected get componentTranslate(): string {
|
||||
if (!this._componentTranslate) {
|
||||
this._componentTranslate = CoreCourse.instance.translateModuleName('forum');
|
||||
}
|
||||
|
||||
return this._componentTranslate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to synchronize all the forums in a certain site or in all sites.
|
||||
*
|
||||
* @param siteId Site ID to sync. If not defined, sync all sites.
|
||||
* @param force Wether to force sync not depending on last execution.
|
||||
* @return Promise resolved if sync is successful, rejected if sync fails.
|
||||
*/
|
||||
async syncAllForums(siteId?: string, force?: boolean): Promise<void> {
|
||||
await this.syncOnSites('all forums', this.syncAllForumsFunc.bind(this, !!force), siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync all forums on a site.
|
||||
*
|
||||
* @param force Wether to force sync not depending on last execution.
|
||||
* @param siteId Site ID to sync.
|
||||
* @return Promise resolved if sync is successful, rejected if sync fails.
|
||||
*/
|
||||
protected async syncAllForumsFunc(force: boolean, siteId: string): Promise<void> {
|
||||
const sitePromises: Promise<void>[] = [];
|
||||
|
||||
// Sync all new discussions.
|
||||
const syncDiscussions = async (discussions: AddonModForumOfflineDiscussion[]) => {
|
||||
// Do not sync same forum twice.
|
||||
const syncedForumIds: number[] = [];
|
||||
const promises = discussions.map(async discussion => {
|
||||
if (CoreArray.contains(syncedForumIds, discussion.forumid)) {
|
||||
return;
|
||||
}
|
||||
|
||||
syncedForumIds.push(discussion.forumid);
|
||||
const result = force
|
||||
? await this.syncForumDiscussions(discussion.forumid, discussion.userid, siteId)
|
||||
: await this.syncForumDiscussionsIfNeeded(discussion.forumid, discussion.userid, siteId);
|
||||
|
||||
if (result && result.updated) {
|
||||
// Sync successful, send event.
|
||||
CoreEvents.trigger(AddonModForumSyncProvider.AUTO_SYNCED, {
|
||||
forumId: discussion.forumid,
|
||||
userId: discussion.userid,
|
||||
warnings: result.warnings,
|
||||
}, siteId);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(Object.values(promises));
|
||||
};
|
||||
|
||||
sitePromises.push(
|
||||
AddonModForumOffline.instance
|
||||
.getAllNewDiscussions(siteId)
|
||||
.then(discussions => syncDiscussions(discussions)),
|
||||
);
|
||||
|
||||
// Sync all discussion replies.
|
||||
const syncReplies = async (replies: AddonModForumOfflineReply[]) => {
|
||||
// Do not sync same discussion twice.
|
||||
const syncedDiscussionIds: number[] = [];
|
||||
const promises = replies.map(async reply => {
|
||||
if (CoreArray.contains(syncedDiscussionIds, reply.discussionid)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = force
|
||||
? await this.syncDiscussionReplies(reply.discussionid, reply.userid, siteId)
|
||||
: await this.syncDiscussionRepliesIfNeeded(reply.discussionid, reply.userid, siteId);
|
||||
|
||||
if (result && result.updated) {
|
||||
// Sync successful, send event.
|
||||
CoreEvents.trigger(AddonModForumSyncProvider.AUTO_SYNCED, {
|
||||
forumId: reply.forumid,
|
||||
discussionId: reply.discussionid,
|
||||
userId: reply.userid,
|
||||
warnings: result.warnings,
|
||||
}, siteId);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
};
|
||||
|
||||
sitePromises.push(
|
||||
AddonModForumOffline.instance
|
||||
.getAllReplies(siteId)
|
||||
.then(replies => syncReplies(replies)),
|
||||
);
|
||||
|
||||
// Sync ratings.
|
||||
sitePromises.push(this.syncRatings(undefined, undefined, force, siteId));
|
||||
|
||||
await Promise.all(sitePromises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync a forum only if a certain time has passed since the last time.
|
||||
*
|
||||
* @param forumId Forum ID.
|
||||
* @param userId User the discussion belong to.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved when the forum is synced or if it doesn't need to be synced.
|
||||
*/
|
||||
async syncForumDiscussionsIfNeeded(
|
||||
forumId: number,
|
||||
userId: number,
|
||||
siteId?: string,
|
||||
): Promise<AddonModForumSyncResult | void> {
|
||||
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||||
|
||||
const syncId = this.getForumSyncId(forumId, userId);
|
||||
|
||||
const needed = await this.isSyncNeeded(syncId, siteId);
|
||||
|
||||
if (needed) {
|
||||
return this.syncForumDiscussions(forumId, userId, siteId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize all offline discussions of a forum.
|
||||
*
|
||||
* @param forumId Forum ID to be synced.
|
||||
* @param userId User the discussions belong to.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved if sync is successful, rejected otherwise.
|
||||
*/
|
||||
async syncForumDiscussions(
|
||||
forumId: number,
|
||||
userId?: number,
|
||||
siteId?: string,
|
||||
): Promise<AddonModForumSyncResult> {
|
||||
userId = userId || CoreSites.instance.getCurrentSiteUserId();
|
||||
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||||
|
||||
const syncId = this.getForumSyncId(forumId, userId);
|
||||
|
||||
if (this.isSyncing(syncId, siteId)) {
|
||||
// There's already a sync ongoing for this discussion, return the promise.
|
||||
return this.getOngoingSync(syncId, siteId)!;
|
||||
}
|
||||
|
||||
// Verify that forum isn't blocked.
|
||||
if (CoreSync.instance.isBlocked(AddonModForumProvider.COMPONENT, syncId, siteId)) {
|
||||
this.logger.debug('Cannot sync forum ' + forumId + ' because it is blocked.');
|
||||
|
||||
return Promise.reject(Translate.instant('core.errorsyncblocked', { $a: this.componentTranslate }));
|
||||
}
|
||||
|
||||
this.logger.debug('Try to sync forum ' + forumId + ' for user ' + userId);
|
||||
|
||||
const result: AddonModForumSyncResult = {
|
||||
warnings: [],
|
||||
updated: false,
|
||||
};
|
||||
|
||||
// Sync offline logs.
|
||||
const syncDiscussions = async (): Promise<{ warnings: string[]; updated: boolean }> => {
|
||||
await CoreUtils.instance.ignoreErrors(
|
||||
CoreCourseLogHelper.instance.syncActivity(AddonModForumProvider.COMPONENT, forumId, siteId),
|
||||
);
|
||||
|
||||
// Get offline responses to be sent.
|
||||
const discussions = await CoreUtils.instance.ignoreErrors(
|
||||
AddonModForumOffline.instance.getNewDiscussions(forumId, siteId, userId),
|
||||
[] as AddonModForumOfflineDiscussion[],
|
||||
);
|
||||
|
||||
if (discussions.length !== 0 && !CoreApp.instance.isOnline()) {
|
||||
throw new Error('cannot sync in offline');
|
||||
}
|
||||
|
||||
const promises = discussions.map(async discussion => {
|
||||
const errors: Error[] = [];
|
||||
const groupIds = discussion.groupid === AddonModForumProvider.ALL_GROUPS
|
||||
? await AddonModForum.instance
|
||||
.getForumById(discussion.courseid, discussion.forumid, { siteId })
|
||||
.then(forum => CoreGroups.instance.getActivityAllowedGroups(forum.cmid))
|
||||
.then(result => result.groups.map((group) => group.id))
|
||||
: [discussion.groupid];
|
||||
|
||||
await Promise.all(groupIds.map(async groupId => {
|
||||
try {
|
||||
// First of all upload the attachments (if any).
|
||||
const itemId = await this.uploadAttachments(forumId, discussion, true, siteId, userId);
|
||||
|
||||
// Now try to add the discussion.
|
||||
const options = CoreUtils.instance.clone(discussion.options || {});
|
||||
options.attachmentsid = itemId;
|
||||
|
||||
await AddonModForum.instance.addNewDiscussionOnline(
|
||||
forumId,
|
||||
discussion.subject,
|
||||
discussion.message,
|
||||
options,
|
||||
groupId,
|
||||
siteId,
|
||||
);
|
||||
} catch (error) {
|
||||
errors.push(error);
|
||||
}
|
||||
}));
|
||||
|
||||
if (errors.length === groupIds.length) {
|
||||
// All requests have failed, reject if errors were not returned by WS.
|
||||
for (const error of errors) {
|
||||
if (!CoreUtils.instance.isWebServiceError(error)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// All requests succeeded, some failed or all failed with a WS error.
|
||||
result.updated = true;
|
||||
|
||||
await this.deleteNewDiscussion(forumId, discussion.timecreated, siteId, userId);
|
||||
|
||||
if (errors.length === groupIds.length) {
|
||||
// All requests failed with WS error.
|
||||
result.warnings.push(Translate.instant('core.warningofflinedatadeleted', {
|
||||
component: this.componentTranslate,
|
||||
name: discussion.name,
|
||||
error: CoreTextUtils.instance.getErrorMessageFromError(errors[0]),
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
if (result.updated) {
|
||||
// Data has been sent to server. Now invalidate the WS calls.
|
||||
const promises = [
|
||||
AddonModForum.instance.invalidateDiscussionsList(forumId, siteId),
|
||||
AddonModForum.instance.invalidateCanAddDiscussion(forumId, siteId),
|
||||
];
|
||||
|
||||
await CoreUtils.instance.ignoreErrors(Promise.all(promises));
|
||||
}
|
||||
|
||||
// Sync finished, set sync time.
|
||||
await CoreUtils.instance.ignoreErrors(this.setSyncTime(syncId, siteId));
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
return this.addOngoingSync(syncId, syncDiscussions(), siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize forum offline ratings.
|
||||
*
|
||||
* @param cmId Course module to be synced. If not defined, sync all forums.
|
||||
* @param discussionId Discussion id to be synced. If not defined, sync all discussions.
|
||||
* @param force Wether to force sync not depending on last execution.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved if sync is successful, rejected otherwise.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async syncRatings(cmId?: number, discussionId?: number, force?: boolean, siteId?: string): Promise<void> {
|
||||
// @todo
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize all offline discussion replies of a forum.
|
||||
*
|
||||
* @param forumId Forum ID to be synced.
|
||||
* @param userId User the discussions belong to.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved if sync is successful, rejected otherwise.
|
||||
*/
|
||||
async syncForumReplies(forumId: number, userId?: number, siteId?: string): Promise<AddonModForumSyncResult> {
|
||||
// Get offline forum replies to be sent.
|
||||
const replies = await CoreUtils.instance.ignoreErrors(
|
||||
AddonModForumOffline.instance.getForumReplies(forumId, siteId, userId),
|
||||
[] as AddonModForumOfflineReply[],
|
||||
);
|
||||
|
||||
if (!replies.length) {
|
||||
// Nothing to sync.
|
||||
return { warnings: [], updated: false };
|
||||
} else if (!CoreApp.instance.isOnline()) {
|
||||
// Cannot sync in offline.
|
||||
return Promise.reject(null);
|
||||
}
|
||||
|
||||
const promises: Record<string, Promise<AddonModForumSyncResult>> = {};
|
||||
|
||||
// Do not sync same discussion twice.
|
||||
replies.forEach((reply) => {
|
||||
if (typeof promises[reply.discussionid] != 'undefined') {
|
||||
return;
|
||||
}
|
||||
promises[reply.discussionid] = this.syncDiscussionReplies(reply.discussionid, userId, siteId);
|
||||
});
|
||||
|
||||
const results = await Promise.all(Object.values(promises));
|
||||
|
||||
return results.reduce((a, b) => ({
|
||||
warnings: a.warnings.concat(b.warnings),
|
||||
updated: a.updated || b.updated,
|
||||
}), { warnings: [], updated: false } as AddonModForumSyncResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync a forum discussion replies only if a certain time has passed since the last time.
|
||||
*
|
||||
* @param discussionId Discussion ID to be synced.
|
||||
* @param userId User the posts belong to.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved when the forum discussion is synced or if it doesn't need to be synced.
|
||||
*/
|
||||
async syncDiscussionRepliesIfNeeded(
|
||||
discussionId: number,
|
||||
userId?: number,
|
||||
siteId?: string,
|
||||
): Promise<AddonModForumSyncResult | void> {
|
||||
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||||
|
||||
const syncId = this.getDiscussionSyncId(discussionId, userId);
|
||||
|
||||
const needed = await this.isSyncNeeded(syncId, siteId);
|
||||
|
||||
if (needed) {
|
||||
return this.syncDiscussionReplies(discussionId, userId, siteId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize all offline replies from a discussion.
|
||||
*
|
||||
* @param discussionId Discussion ID to be synced.
|
||||
* @param userId User the posts belong to.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved if sync is successful, rejected otherwise.
|
||||
*/
|
||||
async syncDiscussionReplies(discussionId: number, userId?: number, siteId?: string): Promise<AddonModForumSyncResult> {
|
||||
userId = userId || CoreSites.instance.getCurrentSiteUserId();
|
||||
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||||
|
||||
const syncId = this.getDiscussionSyncId(discussionId, userId);
|
||||
|
||||
if (this.isSyncing(syncId, siteId)) {
|
||||
// There's already a sync ongoing for this discussion, return the promise.
|
||||
return this.getOngoingSync(syncId, siteId)!;
|
||||
}
|
||||
|
||||
// Verify that forum isn't blocked.
|
||||
if (CoreSync.instance.isBlocked(AddonModForumProvider.COMPONENT, syncId, siteId)) {
|
||||
this.logger.debug('Cannot sync forum discussion ' + discussionId + ' because it is blocked.');
|
||||
|
||||
throw new Error(Translate.instant('core.errorsyncblocked', { $a: this.componentTranslate }));
|
||||
}
|
||||
|
||||
this.logger.debug('Try to sync forum discussion ' + discussionId + ' for user ' + userId);
|
||||
|
||||
let forumId;
|
||||
const result: AddonModForumSyncResult = {
|
||||
warnings: [],
|
||||
updated: false,
|
||||
};
|
||||
|
||||
// Get offline responses to be sent.
|
||||
const syncReplies = async () => {
|
||||
const replies = await CoreUtils.instance.ignoreErrors(
|
||||
AddonModForumOffline.instance.getDiscussionReplies(discussionId, siteId, userId),
|
||||
[] as AddonModForumOfflineReply[],
|
||||
);
|
||||
|
||||
if (replies.length !== 0 && !CoreApp.instance.isOnline()) {
|
||||
throw new Error('Cannot sync in offline');
|
||||
}
|
||||
|
||||
const promises = replies.map(async reply => {
|
||||
forumId = reply.forumid;
|
||||
reply.options = reply.options || {};
|
||||
|
||||
try {
|
||||
// First of all upload the attachments (if any).
|
||||
await this.uploadAttachments(forumId, reply, false, siteId, userId).then((itemId) => {
|
||||
// Now try to send the reply.
|
||||
reply.options.attachmentsid = itemId;
|
||||
|
||||
return AddonModForum.instance.replyPostOnline(
|
||||
reply.postid,
|
||||
reply.subject,
|
||||
reply.message,
|
||||
reply.options,
|
||||
siteId,
|
||||
);
|
||||
});
|
||||
|
||||
result.updated = true;
|
||||
|
||||
await this.deleteReply(forumId, reply.postid, siteId, userId);
|
||||
} catch (error) {
|
||||
if (!CoreUtils.instance.isWebServiceError(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// The WebService has thrown an error, this means that responses cannot be submitted. Delete them.
|
||||
result.updated = true;
|
||||
|
||||
await this.deleteReply(forumId, reply.postid, siteId, userId);
|
||||
|
||||
// Responses deleted, add a warning.
|
||||
result.warnings.push(Translate.instant('core.warningofflinedatadeleted', {
|
||||
component: this.componentTranslate,
|
||||
name: reply.name,
|
||||
error: CoreTextUtils.instance.getErrorMessageFromError(error),
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
// Data has been sent to server. Now invalidate the WS calls.
|
||||
const invalidationPromises: Promise<void>[] = [];
|
||||
|
||||
if (forumId) {
|
||||
invalidationPromises.push(AddonModForum.instance.invalidateDiscussionsList(forumId, siteId));
|
||||
}
|
||||
|
||||
invalidationPromises.push(AddonModForum.instance.invalidateDiscussionPosts(discussionId, forumId, siteId));
|
||||
|
||||
await CoreUtils.instance.ignoreErrors(CoreUtils.instance.allPromises(invalidationPromises));
|
||||
|
||||
// Sync finished, set sync time.
|
||||
await CoreUtils.instance.ignoreErrors(this.setSyncTime(syncId, siteId));
|
||||
|
||||
// All done, return the warnings.
|
||||
return result;
|
||||
};
|
||||
|
||||
return this.addOngoingSync(syncId, syncReplies(), siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a new discussion.
|
||||
*
|
||||
* @param forumId Forum ID the discussion belongs to.
|
||||
* @param timecreated The timecreated of the discussion.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param userId User the discussion belongs to. If not defined, current user in site.
|
||||
* @return Promise resolved when deleted.
|
||||
*/
|
||||
protected async deleteNewDiscussion(forumId: number, timecreated: number, siteId?: string, userId?: number): Promise<void> {
|
||||
await Promise.all([
|
||||
AddonModForumOffline.instance.deleteNewDiscussion(forumId, timecreated, siteId, userId),
|
||||
CoreUtils.instance.ignoreErrors(
|
||||
AddonModForumHelper.instance.deleteNewDiscussionStoredFiles(forumId, timecreated, siteId),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a new discussion.
|
||||
*
|
||||
* @param forumId Forum ID the discussion belongs to.
|
||||
* @param postId ID of the post being replied.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param userId User the discussion belongs to. If not defined, current user in site.
|
||||
* @return Promise resolved when deleted.
|
||||
*/
|
||||
protected async deleteReply(forumId: number, postId: number, siteId?: string, userId?: number): Promise<void> {
|
||||
await Promise.all([
|
||||
AddonModForumOffline.instance.deleteReply(postId, siteId, userId),
|
||||
CoreUtils.instance.ignoreErrors(AddonModForumHelper.instance.deleteReplyStoredFiles(forumId, postId, siteId, userId)),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload attachments of an offline post/discussion.
|
||||
*
|
||||
* @param forumId Forum ID the post belongs to.
|
||||
* @param post Offline post or discussion.
|
||||
* @param isDisc True if it's a new discussion, false if it's a reply.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param userId User the reply belongs to. If not defined, current user in site.
|
||||
* @return Promise resolved with draftid if uploaded, resolved with undefined if nothing to upload.
|
||||
*/
|
||||
protected async uploadAttachments(
|
||||
forumId: number,
|
||||
post: any,
|
||||
isDisc: boolean,
|
||||
siteId?: string,
|
||||
userId?: number,
|
||||
): Promise<void> {
|
||||
const attachments = post && post.options && post.options.attachmentsid;
|
||||
|
||||
if (!attachments) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Has some attachments to sync.
|
||||
let files = attachments.online || [];
|
||||
|
||||
if (attachments.offline) {
|
||||
// Has offline files.
|
||||
try {
|
||||
const atts = isDisc
|
||||
? await AddonModForumHelper.instance.getNewDiscussionStoredFiles(forumId, post.timecreated, siteId)
|
||||
: await AddonModForumHelper.instance.getReplyStoredFiles(forumId, post.postid, siteId, userId);
|
||||
|
||||
files = files.concat(atts);
|
||||
} catch (error) {
|
||||
// Folder not found, no files to add.
|
||||
}
|
||||
}
|
||||
|
||||
await CoreFileUploader.instance.uploadOrReuploadFiles(files, AddonModForumProvider.COMPONENT, forumId, siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID of a forum sync.
|
||||
*
|
||||
* @param forumId Forum ID.
|
||||
* @param userId User the responses belong to.. If not defined, current user.
|
||||
* @return Sync ID.
|
||||
*/
|
||||
getForumSyncId(forumId: number, userId?: number): string {
|
||||
userId = userId || CoreSites.instance.getCurrentSiteUserId();
|
||||
|
||||
return 'forum#' + forumId + '#' + userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID of a discussion sync.
|
||||
*
|
||||
* @param discussionId Discussion ID.
|
||||
* @param userId User the responses belong to.. If not defined, current user.
|
||||
* @return Sync ID.
|
||||
*/
|
||||
getDiscussionSyncId(discussionId: number, userId?: number): string {
|
||||
userId = userId || CoreSites.instance.getCurrentSiteUserId();
|
||||
|
||||
return 'discussion#' + discussionId + '#' + userId;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of forum sync.
|
||||
*/
|
||||
export type AddonModForumSyncResult = {
|
||||
updated: boolean;
|
||||
warnings: string[];
|
||||
};
|
|
@ -3110,7 +3110,7 @@ export class AddonModLessonProvider {
|
|||
const params: AddonModLessonProcessPageWSParams = {
|
||||
lessonid: lessonId,
|
||||
pageid: pageId,
|
||||
data: <ProcessPageData[]> CoreUtils.instance.objectToArrayOfObjects(data, 'name', 'value', true),
|
||||
data: CoreUtils.instance.objectToArrayOfObjects<ProcessPageData>(data, 'name', 'value', true),
|
||||
review: !!options.review,
|
||||
};
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ import { NgModule } from '@angular/core';
|
|||
|
||||
import { AddonModAssignModule } from './assign/assign.module';
|
||||
import { AddonModBookModule } from './book/book.module';
|
||||
import { AddonModForumModule } from './forum/forum.module';
|
||||
import { AddonModLessonModule } from './lesson/lesson.module';
|
||||
import { AddonModPageModule } from './page/page.module';
|
||||
import { AddonModQuizModule } from './quiz/quiz.module';
|
||||
|
@ -25,6 +26,7 @@ import { AddonModQuizModule } from './quiz/quiz.module';
|
|||
imports: [
|
||||
AddonModAssignModule,
|
||||
AddonModBookModule,
|
||||
AddonModForumModule,
|
||||
AddonModLessonModule,
|
||||
AddonModPageModule,
|
||||
AddonModQuizModule,
|
||||
|
|
|
@ -68,7 +68,7 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR
|
|||
* Component being initialized.
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
super.ngOnInit();
|
||||
await super.ngOnInit();
|
||||
|
||||
this.hasOffline = false;
|
||||
this.syncIcon = CoreConstants.ICON_LOADING;
|
||||
|
|
|
@ -779,3 +779,5 @@ export type CoreFileUploaderTypeListInfoEntry = {
|
|||
name?: string;
|
||||
extlist: string;
|
||||
};
|
||||
|
||||
export type CoreFileEntry = CoreWSExternalFile | FileEntry;
|
||||
|
|
|
@ -19,7 +19,7 @@ import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
|
|||
import { makeSingleton } from '@singletons';
|
||||
import { CoreCourse } from '../../course/services/course';
|
||||
import { CoreCourses } from '../../courses/services/courses';
|
||||
import { AddonModForum, AddonModForumData } from '@/addons/mod/forum/services/forum';
|
||||
import { AddonModForum, AddonModForumData } from '@addons/mod/forum/services/forum.service';
|
||||
|
||||
/**
|
||||
* Items with index 1 and 3 were removed on 2.5 and not being supported in the app.
|
||||
|
|
|
@ -219,7 +219,7 @@ export class CoreUtilsProvider {
|
|||
try {
|
||||
const response = await this.timeoutPromise(window.fetch(url, initOptions), CoreWS.instance.getRequestTimeout());
|
||||
|
||||
return response.redirected;
|
||||
return !!response && response.redirected;
|
||||
} catch (error) {
|
||||
if (error.timeout && controller) {
|
||||
// Timeout, abort the request.
|
||||
|
@ -1072,13 +1072,16 @@ export class CoreUtilsProvider {
|
|||
* @param sortByValue True to sort values alphabetically, false otherwise.
|
||||
* @return Array of objects with the name & value of each property.
|
||||
*/
|
||||
objectToArrayOfObjects<T = Record<string, unknown>>(
|
||||
obj: Record<string, unknown>,
|
||||
objectToArrayOfObjects<
|
||||
A extends Record<string,unknown> = Record<string, unknown>,
|
||||
O extends Record<string, unknown> = Record<string, unknown>
|
||||
>(
|
||||
obj: O,
|
||||
keyName: string,
|
||||
valueName: string,
|
||||
sortByKey?: boolean,
|
||||
sortByValue?: boolean,
|
||||
): T[] {
|
||||
): A[] {
|
||||
// Get the entries from an object or primitive value.
|
||||
const getEntries = (elKey: string, value: unknown): Record<string, unknown>[] | unknown => {
|
||||
if (typeof value == 'undefined' || value == null) {
|
||||
|
@ -1114,7 +1117,7 @@ export class CoreUtilsProvider {
|
|||
}
|
||||
|
||||
// "obj" will always be an object, so "entries" will always be an array.
|
||||
const entries = getEntries('', obj) as T[];
|
||||
const entries = getEntries('', obj) as A[];
|
||||
if (sortByKey || sortByValue) {
|
||||
return entries.sort((a, b) => {
|
||||
if (sortByKey) {
|
||||
|
@ -1223,7 +1226,7 @@ export class CoreUtilsProvider {
|
|||
promiseDefer<T>(): PromiseDefer<T> {
|
||||
const deferred: Partial<PromiseDefer<T>> = {};
|
||||
deferred.promise = new Promise((resolve, reject): void => {
|
||||
deferred.resolve = resolve;
|
||||
deferred.resolve = resolve as (value?: T | undefined) => void;
|
||||
deferred.reject = reject;
|
||||
});
|
||||
|
||||
|
@ -1371,7 +1374,7 @@ export class CoreUtilsProvider {
|
|||
* @param time Number of milliseconds of the timeout.
|
||||
* @return Promise with the timeout.
|
||||
*/
|
||||
timeoutPromise<T>(promise: Promise<T>, time: number): Promise<T> {
|
||||
timeoutPromise<T>(promise: Promise<T>, time: number): Promise<T | void> {
|
||||
return new Promise((resolve, reject): void => {
|
||||
let timedOut = false;
|
||||
const resolveBeforeTimeout = () => {
|
||||
|
|
|
@ -158,4 +158,10 @@ export class NavController extends makeSingleton(NavControllerService) {}
|
|||
export class Router extends makeSingleton(RouterService) {}
|
||||
|
||||
// Convert external libraries injectables.
|
||||
export class Translate extends makeSingleton(TranslateService) {}
|
||||
export class Translate extends makeSingleton(TranslateService) {
|
||||
|
||||
static instant(key: string | Array<string>, interpolateParams?: Record<string, unknown>): string | any {
|
||||
return this.instance.instant(key, interpolateParams);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -183,6 +183,8 @@
|
|||
--addon-messages-discussion-badge: var(--custom-messages-discussion-badge, var(--core-color));
|
||||
--addon-messages-discussion-badge-text: var(--custom-messages-discussion-badge-text, var(--white));
|
||||
|
||||
--addon-forum-avatar-size: var(--custom-forum-avatar-size, 28px);
|
||||
|
||||
--drop-shadow: var(--custom-drop-shadow, 0, 0, 0, 0.2);
|
||||
|
||||
--core-menu-box-shadow-end: var(--custom-menu-box-shadow-end, -4px 0px 16px rgba(0, 0, 0, 0.18));
|
||||
|
|
Loading…
Reference in New Issue