MOBILE-3643 forum: Migrate index page
parent
549735bf98
commit
b318b0e4a5
|
@ -29,7 +29,7 @@
|
||||||
<ion-item class="ion-text-wrap addon-messages-conversation-item"
|
<ion-item class="ion-text-wrap addon-messages-conversation-item"
|
||||||
*ngFor="let contact of confirmedContacts" [title]="contact.fullname" detail
|
*ngFor="let contact of confirmedContacts" [title]="contact.fullname" detail
|
||||||
(click)="selectUser(contact.id)" [class.core-selected-item]="contact.id == selectedUserId">
|
(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">
|
[checkOnline]="contact.showonlinestatus" [linkProfile]="false">
|
||||||
</core-user-avatar>
|
</core-user-avatar>
|
||||||
<ion-label>
|
<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 = {
|
const params: AddonModLessonProcessPageWSParams = {
|
||||||
lessonid: lessonId,
|
lessonid: lessonId,
|
||||||
pageid: pageId,
|
pageid: pageId,
|
||||||
data: <ProcessPageData[]> CoreUtils.instance.objectToArrayOfObjects(data, 'name', 'value', true),
|
data: CoreUtils.instance.objectToArrayOfObjects<ProcessPageData>(data, 'name', 'value', true),
|
||||||
review: !!options.review,
|
review: !!options.review,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { NgModule } from '@angular/core';
|
||||||
|
|
||||||
import { AddonModAssignModule } from './assign/assign.module';
|
import { AddonModAssignModule } from './assign/assign.module';
|
||||||
import { AddonModBookModule } from './book/book.module';
|
import { AddonModBookModule } from './book/book.module';
|
||||||
|
import { AddonModForumModule } from './forum/forum.module';
|
||||||
import { AddonModLessonModule } from './lesson/lesson.module';
|
import { AddonModLessonModule } from './lesson/lesson.module';
|
||||||
import { AddonModPageModule } from './page/page.module';
|
import { AddonModPageModule } from './page/page.module';
|
||||||
import { AddonModQuizModule } from './quiz/quiz.module';
|
import { AddonModQuizModule } from './quiz/quiz.module';
|
||||||
|
@ -25,6 +26,7 @@ import { AddonModQuizModule } from './quiz/quiz.module';
|
||||||
imports: [
|
imports: [
|
||||||
AddonModAssignModule,
|
AddonModAssignModule,
|
||||||
AddonModBookModule,
|
AddonModBookModule,
|
||||||
|
AddonModForumModule,
|
||||||
AddonModLessonModule,
|
AddonModLessonModule,
|
||||||
AddonModPageModule,
|
AddonModPageModule,
|
||||||
AddonModQuizModule,
|
AddonModQuizModule,
|
||||||
|
|
|
@ -68,7 +68,7 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR
|
||||||
* Component being initialized.
|
* Component being initialized.
|
||||||
*/
|
*/
|
||||||
async ngOnInit(): Promise<void> {
|
async ngOnInit(): Promise<void> {
|
||||||
super.ngOnInit();
|
await super.ngOnInit();
|
||||||
|
|
||||||
this.hasOffline = false;
|
this.hasOffline = false;
|
||||||
this.syncIcon = CoreConstants.ICON_LOADING;
|
this.syncIcon = CoreConstants.ICON_LOADING;
|
||||||
|
|
|
@ -779,3 +779,5 @@ export type CoreFileUploaderTypeListInfoEntry = {
|
||||||
name?: string;
|
name?: string;
|
||||||
extlist: string;
|
extlist: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CoreFileEntry = CoreWSExternalFile | FileEntry;
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
|
||||||
import { makeSingleton } from '@singletons';
|
import { makeSingleton } from '@singletons';
|
||||||
import { CoreCourse } from '../../course/services/course';
|
import { CoreCourse } from '../../course/services/course';
|
||||||
import { CoreCourses } from '../../courses/services/courses';
|
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.
|
* 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 {
|
try {
|
||||||
const response = await this.timeoutPromise(window.fetch(url, initOptions), CoreWS.instance.getRequestTimeout());
|
const response = await this.timeoutPromise(window.fetch(url, initOptions), CoreWS.instance.getRequestTimeout());
|
||||||
|
|
||||||
return response.redirected;
|
return !!response && response.redirected;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.timeout && controller) {
|
if (error.timeout && controller) {
|
||||||
// Timeout, abort the request.
|
// Timeout, abort the request.
|
||||||
|
@ -1072,13 +1072,16 @@ export class CoreUtilsProvider {
|
||||||
* @param sortByValue True to sort values alphabetically, false otherwise.
|
* @param sortByValue True to sort values alphabetically, false otherwise.
|
||||||
* @return Array of objects with the name & value of each property.
|
* @return Array of objects with the name & value of each property.
|
||||||
*/
|
*/
|
||||||
objectToArrayOfObjects<T = Record<string, unknown>>(
|
objectToArrayOfObjects<
|
||||||
obj: Record<string, unknown>,
|
A extends Record<string,unknown> = Record<string, unknown>,
|
||||||
|
O extends Record<string, unknown> = Record<string, unknown>
|
||||||
|
>(
|
||||||
|
obj: O,
|
||||||
keyName: string,
|
keyName: string,
|
||||||
valueName: string,
|
valueName: string,
|
||||||
sortByKey?: boolean,
|
sortByKey?: boolean,
|
||||||
sortByValue?: boolean,
|
sortByValue?: boolean,
|
||||||
): T[] {
|
): A[] {
|
||||||
// Get the entries from an object or primitive value.
|
// Get the entries from an object or primitive value.
|
||||||
const getEntries = (elKey: string, value: unknown): Record<string, unknown>[] | unknown => {
|
const getEntries = (elKey: string, value: unknown): Record<string, unknown>[] | unknown => {
|
||||||
if (typeof value == 'undefined' || value == null) {
|
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.
|
// "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) {
|
if (sortByKey || sortByValue) {
|
||||||
return entries.sort((a, b) => {
|
return entries.sort((a, b) => {
|
||||||
if (sortByKey) {
|
if (sortByKey) {
|
||||||
|
@ -1223,7 +1226,7 @@ export class CoreUtilsProvider {
|
||||||
promiseDefer<T>(): PromiseDefer<T> {
|
promiseDefer<T>(): PromiseDefer<T> {
|
||||||
const deferred: Partial<PromiseDefer<T>> = {};
|
const deferred: Partial<PromiseDefer<T>> = {};
|
||||||
deferred.promise = new Promise((resolve, reject): void => {
|
deferred.promise = new Promise((resolve, reject): void => {
|
||||||
deferred.resolve = resolve;
|
deferred.resolve = resolve as (value?: T | undefined) => void;
|
||||||
deferred.reject = reject;
|
deferred.reject = reject;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1371,7 +1374,7 @@ export class CoreUtilsProvider {
|
||||||
* @param time Number of milliseconds of the timeout.
|
* @param time Number of milliseconds of the timeout.
|
||||||
* @return Promise with 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 => {
|
return new Promise((resolve, reject): void => {
|
||||||
let timedOut = false;
|
let timedOut = false;
|
||||||
const resolveBeforeTimeout = () => {
|
const resolveBeforeTimeout = () => {
|
||||||
|
|
|
@ -158,4 +158,10 @@ export class NavController extends makeSingleton(NavControllerService) {}
|
||||||
export class Router extends makeSingleton(RouterService) {}
|
export class Router extends makeSingleton(RouterService) {}
|
||||||
|
|
||||||
// Convert external libraries injectables.
|
// 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: var(--custom-messages-discussion-badge, var(--core-color));
|
||||||
--addon-messages-discussion-badge-text: var(--custom-messages-discussion-badge-text, var(--white));
|
--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);
|
--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));
|
--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