MOBILE-3926 forum: Discussions swipe navigation
parent
70ff7a375a
commit
651461b01e
|
@ -0,0 +1,265 @@
|
||||||
|
// (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 { Params } from '@angular/router';
|
||||||
|
import { CoreItemsManagerSource } from '@classes/items-management/items-manager-source';
|
||||||
|
import { CoreUser } from '@features/user/services/user';
|
||||||
|
import {
|
||||||
|
AddonModForum,
|
||||||
|
AddonModForumData,
|
||||||
|
AddonModForumDiscussion,
|
||||||
|
AddonModForumProvider,
|
||||||
|
AddonModForumSortOrder,
|
||||||
|
} from '../services/forum';
|
||||||
|
import { AddonModForumOffline, AddonModForumOfflineDiscussion } from '../services/forum-offline';
|
||||||
|
|
||||||
|
export class AddonModForumDiscussionsSource extends CoreItemsManagerSource<AddonModForumDiscussionItem> {
|
||||||
|
|
||||||
|
static readonly NEW_DISCUSSION: AddonModForumNewDiscussionForm = { newDiscussion: true };
|
||||||
|
|
||||||
|
readonly DISCUSSIONS_PATH_PREFIX: string;
|
||||||
|
readonly COURSE_ID: number;
|
||||||
|
readonly CM_ID: number;
|
||||||
|
|
||||||
|
forum?: AddonModForumData;
|
||||||
|
trackPosts = false;
|
||||||
|
usesGroups = false;
|
||||||
|
selectedSortOrder: AddonModForumSortOrder | null = null;
|
||||||
|
|
||||||
|
constructor(courseId: number, cmId: number, discussionsPathPrefix: string) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.DISCUSSIONS_PATH_PREFIX = discussionsPathPrefix;
|
||||||
|
this.COURSE_ID = courseId;
|
||||||
|
this.CM_ID = cmId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard to infer NewDiscussionForm objects.
|
||||||
|
*
|
||||||
|
* @param discussion Item to check.
|
||||||
|
* @return Whether the item is a new discussion form.
|
||||||
|
*/
|
||||||
|
isNewDiscussionForm(discussion: AddonModForumDiscussionItem): discussion is AddonModForumNewDiscussionForm {
|
||||||
|
return 'newDiscussion' in discussion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard to infer AddonModForumDiscussion objects.
|
||||||
|
*
|
||||||
|
* @param discussion Item to check.
|
||||||
|
* @return Whether the item is an online discussion.
|
||||||
|
*/
|
||||||
|
isOfflineDiscussion(discussion: AddonModForumDiscussionItem): discussion is AddonModForumOfflineDiscussion {
|
||||||
|
return !this.isNewDiscussionForm(discussion) && !this.isOnlineDiscussion(discussion);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard to infer AddonModForumDiscussion objects.
|
||||||
|
*
|
||||||
|
* @param discussion Item to check.
|
||||||
|
* @return Whether the item is an online discussion.
|
||||||
|
*/
|
||||||
|
isOnlineDiscussion(discussion: AddonModForumDiscussionItem): discussion is AddonModForumDiscussion {
|
||||||
|
return 'id' in discussion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
getItemPath(discussion: AddonModForumDiscussionItem): string {
|
||||||
|
if (this.isOnlineDiscussion(discussion)) {
|
||||||
|
return this.DISCUSSIONS_PATH_PREFIX + discussion.discussion;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isOfflineDiscussion(discussion)) {
|
||||||
|
return `${this.DISCUSSIONS_PATH_PREFIX}new/${discussion.timecreated}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${this.DISCUSSIONS_PATH_PREFIX}new/0`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
getItemQueryParams(discussion: AddonModForumDiscussionItem): Params {
|
||||||
|
return {
|
||||||
|
courseId: this.COURSE_ID,
|
||||||
|
cmId: this.CM_ID,
|
||||||
|
forumId: this.forum?.id,
|
||||||
|
...(this.isOnlineDiscussion(discussion) ? { discussion, trackPosts: this.trackPosts } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
getPagesLoaded(): number {
|
||||||
|
if (this.items === null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onlineEntries = this.items.filter(item => this.isOnlineDiscussion(item));
|
||||||
|
|
||||||
|
return Math.ceil(onlineEntries.length / this.getPageLength());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
getPageLength(): number {
|
||||||
|
return AddonModForumProvider.DISCUSSIONS_PER_PAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load forum.
|
||||||
|
*/
|
||||||
|
async loadForum(): Promise<void> {
|
||||||
|
this.forum = await AddonModForum.getForum(this.COURSE_ID, this.CM_ID);
|
||||||
|
|
||||||
|
if (typeof this.forum.istracked != 'undefined') {
|
||||||
|
this.trackPosts = this.forum.istracked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
protected async loadPageItems(page: number): Promise<{ items: AddonModForumDiscussionItem[]; hasMoreItems: boolean }> {
|
||||||
|
const discussions: AddonModForumDiscussionItem[] = [];
|
||||||
|
|
||||||
|
if (page === 0) {
|
||||||
|
const offlineDiscussions = await this.loadOfflineDiscussions();
|
||||||
|
|
||||||
|
discussions.push(AddonModForumDiscussionsSource.NEW_DISCUSSION);
|
||||||
|
discussions.push(...offlineDiscussions);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { discussions: onlineDiscussions, canLoadMore } = await this.loadOnlineDiscussions(page);
|
||||||
|
|
||||||
|
discussions.push(...onlineDiscussions);
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: discussions,
|
||||||
|
hasMoreItems: canLoadMore,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load online discussions for the given page.
|
||||||
|
*
|
||||||
|
* @param page Page.
|
||||||
|
* @returns Online discussions info.
|
||||||
|
*/
|
||||||
|
private async loadOnlineDiscussions(page: number): Promise<{
|
||||||
|
discussions: AddonModForumDiscussionItem[];
|
||||||
|
canLoadMore: boolean;
|
||||||
|
}> {
|
||||||
|
if (!this.forum || !this.selectedSortOrder) {
|
||||||
|
throw new Error('Can\'t load discussions without a forum or selected sort order');
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await AddonModForum.getDiscussions(this.forum.id, {
|
||||||
|
cmId: this.forum.cmid,
|
||||||
|
sortOrder: this.selectedSortOrder.value,
|
||||||
|
page,
|
||||||
|
});
|
||||||
|
let discussions = response.discussions;
|
||||||
|
|
||||||
|
if (this.usesGroups) {
|
||||||
|
discussions = await AddonModForum.formatDiscussionsGroups(this.forum.cmid, discussions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide author for first post and type single.
|
||||||
|
if (this.forum.type === 'single') {
|
||||||
|
for (const discussion of discussions) {
|
||||||
|
if (discussion.userfullname && discussion.parent === 0) {
|
||||||
|
discussion.userfullname = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If any discussion has unread posts, the whole forum is being tracked.
|
||||||
|
if (typeof this.forum.istracked === 'undefined' && !this.trackPosts) {
|
||||||
|
for (const discussion of discussions) {
|
||||||
|
if (discussion.numunread > 0) {
|
||||||
|
this.trackPosts = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { discussions, canLoadMore: response.canLoadMore };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load offline discussions.
|
||||||
|
*
|
||||||
|
* @returns Offline discussions.
|
||||||
|
*/
|
||||||
|
private async loadOfflineDiscussions(): Promise<AddonModForumOfflineDiscussion[]> {
|
||||||
|
if (!this.forum) {
|
||||||
|
throw new Error('Can\'t load discussions without a forum');
|
||||||
|
}
|
||||||
|
|
||||||
|
const forum = this.forum;
|
||||||
|
let offlineDiscussions = await AddonModForumOffline.getNewDiscussions(forum.id);
|
||||||
|
|
||||||
|
if (offlineDiscussions.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.usesGroups) {
|
||||||
|
offlineDiscussions = await AddonModForum.formatDiscussionsGroups(forum.cmid, offlineDiscussions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill user data for Offline discussions (should be already cached).
|
||||||
|
const promises = offlineDiscussions.map(async (offlineDiscussion) => {
|
||||||
|
const discussion = offlineDiscussion as unknown as AddonModForumDiscussion;
|
||||||
|
|
||||||
|
if (discussion.parent === 0 || forum.type === 'single') {
|
||||||
|
// Do not show author for first post and type single.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await CoreUser.getProfile(discussion.userid, this.COURSE_ID, 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);
|
||||||
|
|
||||||
|
return offlineDiscussions;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type to select the new discussion form.
|
||||||
|
*/
|
||||||
|
export type AddonModForumNewDiscussionForm = { newDiscussion: true };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of items that can be held by the discussions manager.
|
||||||
|
*/
|
||||||
|
export type AddonModForumDiscussionItem = AddonModForumDiscussion | AddonModForumOfflineDiscussion | AddonModForumNewDiscussionForm;
|
|
@ -0,0 +1,52 @@
|
||||||
|
// (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 { CoreSwipeItemsManager } from '@classes/items-management/swipe-items-manager';
|
||||||
|
import { AddonModForumDiscussionItem, AddonModForumDiscussionsSource } from './forum-discussions-source';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to manage swiping within a collection of discussions.
|
||||||
|
*/
|
||||||
|
export class AddonModForumDiscussionsSwipeManager
|
||||||
|
extends CoreSwipeItemsManager<AddonModForumDiscussionItem, AddonModForumDiscussionsSource> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async navigateToNextItem(): Promise<void> {
|
||||||
|
let delta = -1;
|
||||||
|
const item = await this.getItemBy(-1);
|
||||||
|
|
||||||
|
if (item && this.getSource().isNewDiscussionForm(item)) {
|
||||||
|
delta--;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.navigateToItemBy(delta, 'back');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async navigateToPreviousItem(): Promise<void> {
|
||||||
|
let delta = 1;
|
||||||
|
const item = await this.getItemBy(1);
|
||||||
|
|
||||||
|
if (item && this.getSource().isNewDiscussionForm(item)) {
|
||||||
|
delta++;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.navigateToItemBy(delta, 'forward');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -10,11 +10,11 @@
|
||||||
<core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}" iconAction="far-newspaper"
|
<core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}" iconAction="far-newspaper"
|
||||||
(action)="gotoBlog()">
|
(action)="gotoBlog()">
|
||||||
</core-context-menu-item>
|
</core-context-menu-item>
|
||||||
<core-context-menu-item *ngIf="discussions.loaded && !(hasOffline || hasOfflineRatings) && isOnline" [priority]="700"
|
<core-context-menu-item *ngIf="discussions && discussions.loaded && !(hasOffline || hasOfflineRatings) && isOnline" [priority]="700"
|
||||||
[content]="'addon.mod_forum.refreshdiscussions' | translate" [iconAction]="refreshIcon" [closeOnClick]="false"
|
[content]="'addon.mod_forum.refreshdiscussions' | translate" [iconAction]="refreshIcon" [closeOnClick]="false"
|
||||||
(action)="doRefresh(null, $event)">
|
(action)="doRefresh(null, $event)">
|
||||||
</core-context-menu-item>
|
</core-context-menu-item>
|
||||||
<core-context-menu-item *ngIf="discussions.loaded && (hasOffline || hasOfflineRatings) && isOnline" [priority]="600"
|
<core-context-menu-item *ngIf="discussions && discussions.loaded && (hasOffline || hasOfflineRatings) && isOnline" [priority]="600"
|
||||||
[content]="'core.settings.synchronizenow' | translate" [iconAction]="syncIcon" [closeOnClick]="false"
|
[content]="'core.settings.synchronizenow' | translate" [iconAction]="syncIcon" [closeOnClick]="false"
|
||||||
(action)="doRefresh(null, $event, true)">
|
(action)="doRefresh(null, $event, true)">
|
||||||
</core-context-menu-item>
|
</core-context-menu-item>
|
||||||
|
@ -32,11 +32,11 @@
|
||||||
|
|
||||||
<!-- Content. -->
|
<!-- Content. -->
|
||||||
<core-split-view>
|
<core-split-view>
|
||||||
<ion-refresher slot="fixed" [disabled]="!discussions.loaded" (ionRefresh)="doRefresh($event.target)">
|
<ion-refresher slot="fixed" [disabled]="discussions && !discussions.loaded" (ionRefresh)="doRefresh($event.target)">
|
||||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||||
</ion-refresher>
|
</ion-refresher>
|
||||||
|
|
||||||
<core-loading [hideUntil]="discussions.loaded">
|
<core-loading [hideUntil]="discussions && discussions.loaded">
|
||||||
<!-- Activity info. -->
|
<!-- Activity info. -->
|
||||||
<core-course-module-info [module]="module" (completionChanged)="onCompletionChange()"
|
<core-course-module-info [module]="module" (completionChanged)="onCompletionChange()"
|
||||||
[description]="forum && forum.type != 'single' && description" [component]="component" [componentId]="componentId"
|
[description]="forum && forum.type != 'single' && description" [component]="component" [componentId]="componentId"
|
||||||
|
@ -57,17 +57,18 @@
|
||||||
</ion-card>
|
</ion-card>
|
||||||
|
|
||||||
<ng-container *ngIf="forum">
|
<ng-container *ngIf="forum">
|
||||||
<core-empty-box *ngIf="discussions.empty" icon="far-comments" [message]="'addon.mod_forum.forumnodiscussionsyet' | translate">
|
<core-empty-box *ngIf="!discussions || discussions.empty" icon="far-comments"
|
||||||
|
[message]="'addon.mod_forum.forumnodiscussionsyet' | translate">
|
||||||
</core-empty-box>
|
</core-empty-box>
|
||||||
|
|
||||||
<div *ngIf="!discussions.empty && sortingAvailable && selectedSortOrder" class="ion-text-wrap">
|
<div *ngIf="discussions && !discussions.empty && sortingAvailable && selectedSortOrder" class="ion-text-wrap">
|
||||||
<core-combobox [modalOptions]="sortOrderSelectorModalOptions" listboxId="addon-mod-forum-sort-selector"
|
<core-combobox [modalOptions]="sortOrderSelectorModalOptions" listboxId="addon-mod-forum-sort-selector"
|
||||||
[label]="('core.sort' | translate)" (onChange)="setSortOrder($event)" [selection]="selectedSortOrder.label | translate"
|
[label]="('core.sort' | translate)" (onChange)="setSortOrder($event)" [selection]="selectedSortOrder.label | translate"
|
||||||
interface="modal">
|
interface="modal">
|
||||||
</core-combobox>
|
</core-combobox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ion-item *ngFor="let discussion of discussions.items" class="addon-mod-forum-discussion" detail="true"
|
<ion-item *ngFor="let discussion of discussionsItems" class="addon-mod-forum-discussion" detail="true"
|
||||||
[lines]="discussion.groupname && 'none'" [attr.aria-current]="discussions.getItemAriaCurrent(discussion)"
|
[lines]="discussion.groupname && 'none'" [attr.aria-current]="discussions.getItemAriaCurrent(discussion)"
|
||||||
(click)="discussions.select(discussion)" button>
|
(click)="discussions.select(discussion)" button>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
|
@ -96,17 +97,16 @@
|
||||||
<ion-icon name="fas-users" [attr.aria-label]="'addon.mod_forum.group' | translate">
|
<ion-icon name="fas-users" [attr.aria-label]="'addon.mod_forum.group' | translate">
|
||||||
</ion-icon> {{ discussion.groupname }}
|
</ion-icon> {{ discussion.groupname }}
|
||||||
</p>
|
</p>
|
||||||
<p *ngIf="discussions.isOnlineDiscussion(discussion)">
|
<p *ngIf="isOnlineDiscussion(discussion)">
|
||||||
{{discussion.created * 1000 | coreFormatDate: "strftimerecentfull"}}
|
{{discussion.created * 1000 | coreFormatDate: "strftimerecentfull"}}
|
||||||
</p>
|
</p>
|
||||||
<p *ngIf="discussions.isOfflineDiscussion(discussion)">
|
<p *ngIf="isOfflineDiscussion(discussion)">
|
||||||
<ion-icon name="fas-clock" aria-hidden="true"></ion-icon>
|
<ion-icon name="fas-clock" aria-hidden="true"></ion-icon>
|
||||||
{{ 'core.notsent' | translate }}
|
{{ 'core.notsent' | translate }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ion-row *ngIf="discussions.isOnlineDiscussion(discussion)"
|
<ion-row *ngIf="isOnlineDiscussion(discussion)" class="ion-text-center addon-mod-forum-discussion-more-info">
|
||||||
class="ion-text-center addon-mod-forum-discussion-more-info">
|
|
||||||
<ion-col class="ion-text-start">
|
<ion-col class="ion-text-start">
|
||||||
<ion-note>
|
<ion-note>
|
||||||
<ion-icon name="fas-clock" aria-hidden="true"></ion-icon> {{ 'addon.mod_forum.lastpost' | translate }}
|
<ion-icon name="fas-clock" aria-hidden="true"></ion-icon> {{ 'addon.mod_forum.lastpost' | translate }}
|
||||||
|
@ -134,7 +134,7 @@
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
<core-infinite-loading [enabled]="discussions.onlineLoaded && !discussions.completed" [error]="discussions.fetchFailed"
|
<core-infinite-loading [enabled]="discussions && discussions.loaded && !discussions.completed" [error]="fetchFailed"
|
||||||
(action)="fetchMoreDiscussions($event)">
|
(action)="fetchMoreDiscussions($event)">
|
||||||
</core-infinite-loading>
|
</core-infinite-loading>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component, Optional, OnInit, OnDestroy, ViewChild, AfterViewInit } from '@angular/core';
|
import { Component, Optional, OnInit, OnDestroy, ViewChild, AfterViewInit } from '@angular/core';
|
||||||
import { ActivatedRoute, Params } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { IonContent } from '@ionic/angular';
|
import { IonContent } from '@ionic/angular';
|
||||||
import { ModalOptions } from '@ionic/core';
|
import { ModalOptions } from '@ionic/core';
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ import {
|
||||||
AddonModForumNewDiscussionData,
|
AddonModForumNewDiscussionData,
|
||||||
AddonModForumReplyDiscussionData,
|
AddonModForumReplyDiscussionData,
|
||||||
} from '@addons/mod/forum/services/forum';
|
} from '@addons/mod/forum/services/forum';
|
||||||
import { AddonModForumOffline, AddonModForumOfflineDiscussion } from '@addons/mod/forum/services/forum-offline';
|
import { AddonModForumOffline } from '@addons/mod/forum/services/forum-offline';
|
||||||
import { Translate } from '@singletons';
|
import { Translate } from '@singletons';
|
||||||
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
|
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
|
||||||
import { AddonModForumHelper } from '@addons/mod/forum/services/forum-helper';
|
import { AddonModForumHelper } from '@addons/mod/forum/services/forum-helper';
|
||||||
|
@ -44,7 +44,6 @@ import { CoreUser } from '@features/user/services/user';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
import { CoreCourse } from '@features/course/services/course';
|
import { CoreCourse } from '@features/course/services/course';
|
||||||
import { CorePageItemsListManager } from '@classes/page-items-list-manager';
|
|
||||||
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||||
import { AddonModForumDiscussionOptionsMenuComponent } from '../discussion-options-menu/discussion-options-menu';
|
import { AddonModForumDiscussionOptionsMenuComponent } from '../discussion-options-menu/discussion-options-menu';
|
||||||
import { AddonModForumSortOrderSelectorComponent } from '../sort-order-selector/sort-order-selector';
|
import { AddonModForumSortOrderSelectorComponent } from '../sort-order-selector/sort-order-selector';
|
||||||
|
@ -56,6 +55,9 @@ import { CoreRatingProvider } from '@features/rating/services/rating';
|
||||||
import { CoreRatingSyncProvider } from '@features/rating/services/rating-sync';
|
import { CoreRatingSyncProvider } from '@features/rating/services/rating-sync';
|
||||||
import { CoreRatingOffline } from '@features/rating/services/rating-offline';
|
import { CoreRatingOffline } from '@features/rating/services/rating-offline';
|
||||||
import { ContextLevel } from '@/core/constants';
|
import { ContextLevel } from '@/core/constants';
|
||||||
|
import { AddonModForumDiscussionItem, AddonModForumDiscussionsSource } from '../../classes/forum-discussions-source';
|
||||||
|
import { CoreListItemsManager } from '@classes/items-management/list-items-manager';
|
||||||
|
import { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component that displays a forum entry page.
|
* Component that displays a forum entry page.
|
||||||
|
@ -72,24 +74,21 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
component = AddonModForumProvider.COMPONENT;
|
component = AddonModForumProvider.COMPONENT;
|
||||||
moduleName = 'forum';
|
moduleName = 'forum';
|
||||||
descriptionNote?: string;
|
descriptionNote?: string;
|
||||||
forum?: AddonModForumData;
|
discussions!: AddonModForumDiscussionsManager;
|
||||||
discussions: AddonModForumDiscussionsManager;
|
discussionsItems: AddonModForumDiscussionItem[] = [];
|
||||||
|
fetchFailed = false;
|
||||||
canAddDiscussion = false;
|
canAddDiscussion = false;
|
||||||
addDiscussionText!: string;
|
addDiscussionText!: string;
|
||||||
availabilityMessage: string | null = null;
|
availabilityMessage: string | null = null;
|
||||||
sortingAvailable!: boolean;
|
sortingAvailable!: boolean;
|
||||||
sortOrders: AddonModForumSortOrder[] = [];
|
sortOrders: AddonModForumSortOrder[] = [];
|
||||||
selectedSortOrder: AddonModForumSortOrder | null = null;
|
|
||||||
canPin = false;
|
canPin = false;
|
||||||
trackPosts = false;
|
|
||||||
hasOfflineRatings = false;
|
hasOfflineRatings = false;
|
||||||
sortOrderSelectorModalOptions: ModalOptions = {
|
sortOrderSelectorModalOptions: ModalOptions = {
|
||||||
component: AddonModForumSortOrderSelectorComponent,
|
component: AddonModForumSortOrderSelectorComponent,
|
||||||
};
|
};
|
||||||
|
|
||||||
protected syncEventName = AddonModForumSyncProvider.AUTO_SYNCED;
|
protected syncEventName = AddonModForumSyncProvider.AUTO_SYNCED;
|
||||||
protected page = 0;
|
|
||||||
protected usesGroups = false;
|
|
||||||
protected syncManualObserver?: CoreEventObserver; // It will observe the sync manual event.
|
protected syncManualObserver?: CoreEventObserver; // It will observe the sync manual event.
|
||||||
protected replyObserver?: CoreEventObserver;
|
protected replyObserver?: CoreEventObserver;
|
||||||
protected newDiscObserver?: CoreEventObserver;
|
protected newDiscObserver?: CoreEventObserver;
|
||||||
|
@ -97,19 +96,42 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
protected changeDiscObserver?: CoreEventObserver;
|
protected changeDiscObserver?: CoreEventObserver;
|
||||||
protected ratingOfflineObserver?: CoreEventObserver;
|
protected ratingOfflineObserver?: CoreEventObserver;
|
||||||
protected ratingSyncObserver?: CoreEventObserver;
|
protected ratingSyncObserver?: CoreEventObserver;
|
||||||
|
protected sourceUnsubscribe?: () => void;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
route: ActivatedRoute,
|
public route: ActivatedRoute,
|
||||||
@Optional() protected content?: IonContent,
|
@Optional() protected content?: IonContent,
|
||||||
@Optional() courseContentsPage?: CoreCourseContentsPage,
|
@Optional() courseContentsPage?: CoreCourseContentsPage,
|
||||||
) {
|
) {
|
||||||
super('AddonModForumIndexComponent', content, courseContentsPage);
|
super('AddonModForumIndexComponent', content, courseContentsPage);
|
||||||
|
}
|
||||||
|
|
||||||
this.discussions = new AddonModForumDiscussionsManager(
|
get forum(): AddonModForumData | undefined {
|
||||||
route.component,
|
return this.discussions?.getSource().forum;
|
||||||
this,
|
}
|
||||||
courseContentsPage ? `${AddonModForumModuleHandlerService.PAGE_NAME}/` : '',
|
|
||||||
);
|
get selectedSortOrder(): AddonModForumSortOrder | undefined {
|
||||||
|
return this.discussions?.getSource().selectedSortOrder ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a discussion is online.
|
||||||
|
*
|
||||||
|
* @param discussion Discussion
|
||||||
|
* @return Whether the discussion is online.
|
||||||
|
*/
|
||||||
|
isOnlineDiscussion(discussion: AddonModForumDiscussionItem): boolean {
|
||||||
|
return this.discussions && this.discussions.getSource().isOnlineDiscussion(discussion);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a discussion is offline.
|
||||||
|
*
|
||||||
|
* @param discussion Discussion
|
||||||
|
* @return Whether the discussion is offline.
|
||||||
|
*/
|
||||||
|
isOfflineDiscussion(discussion: AddonModForumDiscussionItem): boolean {
|
||||||
|
return this.discussions && this.discussions.getSource().isOfflineDiscussion(discussion);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -126,6 +148,48 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
|
|
||||||
await super.ngOnInit();
|
await super.ngOnInit();
|
||||||
|
|
||||||
|
// Initialize discussions manager.
|
||||||
|
const source = CoreItemsManagerSourcesTracker.getOrCreateSource(
|
||||||
|
AddonModForumDiscussionsSource,
|
||||||
|
[this.courseId, this.module.id, this.courseContentsPage ? `${AddonModForumModuleHandlerService.PAGE_NAME}/` : ''],
|
||||||
|
);
|
||||||
|
|
||||||
|
this.sourceUnsubscribe = source.addListener({
|
||||||
|
onItemsUpdated: async discussions => {
|
||||||
|
this.discussionsItems = discussions.filter(discussion => !source.isNewDiscussionForm(discussion));
|
||||||
|
|
||||||
|
if (!this.forum) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are replies for discussions stored in offline.
|
||||||
|
const hasOffline = await AddonModForumOffline.hasForumReplies(this.forum.id);
|
||||||
|
|
||||||
|
this.hasOffline = this.hasOffline || hasOffline;
|
||||||
|
|
||||||
|
if (hasOffline) {
|
||||||
|
// Only update new fetched discussions.
|
||||||
|
const promises = discussions.map(async (discussion) => {
|
||||||
|
if (!this.discussions.getSource().isOnlineDiscussion(discussion)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get offline discussions.
|
||||||
|
const replies = await AddonModForumOffline.getDiscussionReplies(discussion.discussion);
|
||||||
|
|
||||||
|
discussion.numreplies = Number(discussion.numreplies) + replies.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onReset: () => {
|
||||||
|
this.discussionsItems = [];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.discussions = new AddonModForumDiscussionsManager(source, this);
|
||||||
|
|
||||||
// Refresh data if this forum discussion is synchronized from discussions list.
|
// Refresh data if this forum discussion is synchronized from discussions list.
|
||||||
this.syncManualObserver = CoreEvents.on(AddonModForumSyncProvider.MANUAL_SYNCED, (data) => {
|
this.syncManualObserver = CoreEvents.on(AddonModForumSyncProvider.MANUAL_SYNCED, (data) => {
|
||||||
this.autoSyncEventReceived(data);
|
this.autoSyncEventReceived(data);
|
||||||
|
@ -141,12 +205,16 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
this.eventReceived.bind(this, false),
|
this.eventReceived.bind(this, false),
|
||||||
);
|
);
|
||||||
this.changeDiscObserver = CoreEvents.on(AddonModForumProvider.CHANGE_DISCUSSION_EVENT, data => {
|
this.changeDiscObserver = CoreEvents.on(AddonModForumProvider.CHANGE_DISCUSSION_EVENT, data => {
|
||||||
if ((this.forum && this.forum.id === data.forumId) || data.cmId === this.module.id) {
|
if (!this.forum) {
|
||||||
AddonModForum.invalidateDiscussionsList(this.forum!.id).finally(() => {
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.forum.id === data.forumId || data.cmId === this.module.id) {
|
||||||
|
AddonModForum.invalidateDiscussionsList(this.forum.id).finally(() => {
|
||||||
if (data.discussionId) {
|
if (data.discussionId) {
|
||||||
// Discussion changed, search it in the list of discussions.
|
// Discussion changed, search it in the list of discussions.
|
||||||
const discussion = this.discussions.items.find(
|
const discussion = this.discussions.items.find(
|
||||||
(disc) => this.discussions.isOnlineDiscussion(disc) && data.discussionId == disc.discussion,
|
(disc) => this.discussions.getSource().isOnlineDiscussion(disc) && data.discussionId == disc.discussion,
|
||||||
) as AddonModForumDiscussion;
|
) as AddonModForumDiscussion;
|
||||||
|
|
||||||
if (discussion) {
|
if (discussion) {
|
||||||
|
@ -196,20 +264,6 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
async ngAfterViewInit(): Promise<void> {
|
async ngAfterViewInit(): Promise<void> {
|
||||||
await this.loadContent(false, true);
|
await this.loadContent(false, true);
|
||||||
|
|
||||||
if (!this.forum) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
CoreUtils.ignoreErrors(
|
|
||||||
AddonModForum.instance
|
|
||||||
.logView(this.forum.id, this.forum.name)
|
|
||||||
.then(async () => {
|
|
||||||
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.discussions.start(this.splitView);
|
this.discussions.start(this.splitView);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -226,6 +280,8 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
this.changeDiscObserver && this.changeDiscObserver.off();
|
this.changeDiscObserver && this.changeDiscObserver.off();
|
||||||
this.ratingOfflineObserver && this.ratingOfflineObserver.off();
|
this.ratingOfflineObserver && this.ratingOfflineObserver.off();
|
||||||
this.ratingSyncObserver && this.ratingSyncObserver.off();
|
this.ratingSyncObserver && this.ratingSyncObserver.off();
|
||||||
|
this.sourceUnsubscribe && this.sourceUnsubscribe();
|
||||||
|
this.discussions.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -236,19 +292,21 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
* @param showErrors Wether to show errors to the user or hide them.
|
* @param showErrors Wether to show errors to the user or hide them.
|
||||||
*/
|
*/
|
||||||
protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> {
|
protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> {
|
||||||
this.discussions.fetchFailed = false;
|
this.fetchFailed = false;
|
||||||
|
|
||||||
const promises: Promise<void>[] = [];
|
|
||||||
|
|
||||||
promises.push(this.fetchForum(sync, showErrors));
|
|
||||||
promises.push(this.fetchSortOrderPreference());
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all(promises);
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.fetchOfflineDiscussions(),
|
this.fetchForum(sync, showErrors),
|
||||||
this.fetchDiscussions(refresh),
|
this.fetchSortOrderPreference(),
|
||||||
CoreRatingOffline.hasRatings('mod_forum', 'post', ContextLevel.MODULE, this.forum!.cmid).then((hasRatings) => {
|
]);
|
||||||
|
|
||||||
|
if (!this.forum) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
refresh ? this.discussions.reload() : this.discussions.load(),
|
||||||
|
CoreRatingOffline.hasRatings('mod_forum', 'post', ContextLevel.MODULE, this.forum.cmid).then((hasRatings) => {
|
||||||
this.hasOfflineRatings = hasRatings;
|
this.hasOfflineRatings = hasRatings;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
@ -258,7 +316,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
if (refresh) {
|
if (refresh) {
|
||||||
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_forum.errorgetforum', true);
|
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_forum.errorgetforum', true);
|
||||||
|
|
||||||
this.discussions.fetchFailed = true; // Set to prevent infinite calls with infinite-loading.
|
this.fetchFailed = true; // Set to prevent infinite calls with infinite-loading.
|
||||||
} else {
|
} else {
|
||||||
// Get forum failed, retry without using cache since it might be a new activity.
|
// Get forum failed, retry without using cache since it might be a new activity.
|
||||||
await this.refreshContent(sync);
|
await this.refreshContent(sync);
|
||||||
|
@ -273,19 +331,19 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const forum = await AddonModForum.getForum(this.courseId, this.module.id);
|
await this.discussions.getSource().loadForum();
|
||||||
|
|
||||||
this.forum = forum;
|
if (!this.forum) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const forum = this.forum;
|
||||||
this.description = forum.intro || this.description;
|
this.description = forum.intro || this.description;
|
||||||
this.availabilityMessage = AddonModForumHelper.getAvailabilityMessage(forum);
|
this.availabilityMessage = AddonModForumHelper.getAvailabilityMessage(forum);
|
||||||
this.descriptionNote = Translate.instant('addon.mod_forum.numdiscussions', {
|
this.descriptionNote = Translate.instant('addon.mod_forum.numdiscussions', {
|
||||||
numdiscussions: forum.numdiscussions,
|
numdiscussions: forum.numdiscussions,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (typeof forum.istracked != 'undefined') {
|
|
||||||
this.trackPosts = forum.istracked;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dataRetrieved.emit(forum);
|
this.dataRetrieved.emit(forum);
|
||||||
|
|
||||||
switch (forum.type) {
|
switch (forum.type) {
|
||||||
|
@ -319,10 +377,10 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
// Check if the activity uses groups.
|
// Check if the activity uses groups.
|
||||||
promises.push(
|
promises.push(
|
||||||
CoreGroups.instance
|
CoreGroups.instance
|
||||||
.getActivityGroupMode(this.forum.cmid)
|
.getActivityGroupMode(forum.cmid)
|
||||||
.then(async mode => {
|
.then(async mode => {
|
||||||
this.usesGroups = mode === CoreGroupsProvider.SEPARATEGROUPS
|
this.discussions.getSource().usesGroups =
|
||||||
|| mode === CoreGroupsProvider.VISIBLEGROUPS;
|
mode === CoreGroupsProvider.SEPARATEGROUPS || mode === CoreGroupsProvider.VISIBLEGROUPS;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}),
|
}),
|
||||||
|
@ -330,14 +388,14 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
|
|
||||||
promises.push(
|
promises.push(
|
||||||
AddonModForum.instance
|
AddonModForum.instance
|
||||||
.getAccessInformation(this.forum.id, { cmId: this.module.id })
|
.getAccessInformation(forum.id, { cmId: this.module.id })
|
||||||
.then(async accessInfo => {
|
.then(async accessInfo => {
|
||||||
// Disallow adding discussions if cut-off date is reached and the user has not the
|
// Disallow adding discussions if cut-off date is reached and the user has not the
|
||||||
// capability to override it.
|
// 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.
|
// Just in case the forum was fetched from WS when the cut-off date was not reached but it is now.
|
||||||
const cutoffDateReached = AddonModForumHelper.isCutoffDateReached(this.forum!)
|
const cutoffDateReached = AddonModForumHelper.isCutoffDateReached(forum)
|
||||||
&& !accessInfo.cancanoverridecutoff;
|
&& !accessInfo.cancanoverridecutoff;
|
||||||
this.canAddDiscussion = !!this.forum?.cancreatediscussions && !cutoffDateReached;
|
this.canAddDiscussion = !!forum.cancreatediscussions && !cutoffDateReached;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}),
|
}),
|
||||||
|
@ -347,7 +405,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
// Use the canAddDiscussion WS to check if the user can pin discussions.
|
// Use the canAddDiscussion WS to check if the user can pin discussions.
|
||||||
promises.push(
|
promises.push(
|
||||||
AddonModForum.instance
|
AddonModForum.instance
|
||||||
.canAddDiscussionToAll(this.forum.id, { cmId: this.module.id })
|
.canAddDiscussionToAll(forum.id, { cmId: this.module.id })
|
||||||
.then(async response => {
|
.then(async response => {
|
||||||
this.canPin = !!response.canpindiscussions;
|
this.canPin = !!response.canpindiscussions;
|
||||||
|
|
||||||
|
@ -366,124 +424,6 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Convenience function to fetch offline discussions.
|
|
||||||
*
|
|
||||||
* @return Promise resolved when done.
|
|
||||||
*/
|
|
||||||
protected async fetchOfflineDiscussions(): Promise<void> {
|
|
||||||
const forum = this.forum!;
|
|
||||||
let offlineDiscussions = await AddonModForumOffline.getNewDiscussions(forum.id);
|
|
||||||
this.hasOffline = !!offlineDiscussions.length;
|
|
||||||
|
|
||||||
if (!this.hasOffline) {
|
|
||||||
this.discussions.setOfflineDiscussions([]);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.usesGroups) {
|
|
||||||
offlineDiscussions = await AddonModForum.formatDiscussionsGroups(forum.cmid, offlineDiscussions);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fill user data for Offline discussions (should be already cached).
|
|
||||||
const promises = offlineDiscussions.map(async (offlineDiscussion) => {
|
|
||||||
const discussion = offlineDiscussion as unknown as AddonModForumDiscussion;
|
|
||||||
|
|
||||||
if (discussion.parent === 0 || forum.type === 'single') {
|
|
||||||
// Do not show author for first post and type single.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const user = await CoreUser.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.discussions.setOfflineDiscussions(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.discussions.fetchFailed = false;
|
|
||||||
|
|
||||||
if (refresh) {
|
|
||||||
this.page = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await AddonModForum.getDiscussions(forum.id, {
|
|
||||||
cmId: forum.cmid,
|
|
||||||
sortOrder: this.selectedSortOrder!.value,
|
|
||||||
page: this.page,
|
|
||||||
});
|
|
||||||
let discussions = response.discussions;
|
|
||||||
|
|
||||||
if (this.usesGroups) {
|
|
||||||
discussions = await AddonModForum.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.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.page === 0) {
|
|
||||||
this.discussions.setOnlineDiscussions(discussions, response.canLoadMore);
|
|
||||||
} else {
|
|
||||||
this.discussions.setItems(this.discussions.items.concat(discussions), response.canLoadMore);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.page++;
|
|
||||||
|
|
||||||
// Check if there are replies for discussions stored in offline.
|
|
||||||
const hasOffline = await AddonModForumOffline.hasForumReplies(forum.id);
|
|
||||||
|
|
||||||
this.hasOffline = this.hasOffline || hasOffline;
|
|
||||||
|
|
||||||
if (hasOffline) {
|
|
||||||
// Only update new fetched discussions.
|
|
||||||
const promises = discussions.map(async (discussion) => {
|
|
||||||
// Get offline discussions.
|
|
||||||
const replies = await AddonModForumOffline.getDiscussionReplies(discussion.discussion);
|
|
||||||
|
|
||||||
discussion.numreplies = Number(discussion.numreplies) + replies.length;
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all(promises);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convenience function to load more forum discussions.
|
* Convenience function to load more forum discussions.
|
||||||
*
|
*
|
||||||
|
@ -492,11 +432,13 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
*/
|
*/
|
||||||
async fetchMoreDiscussions(complete: () => void): Promise<void> {
|
async fetchMoreDiscussions(complete: () => void): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await this.fetchDiscussions(false);
|
this.fetchFailed = false;
|
||||||
|
|
||||||
|
await this.discussions.load();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_forum.errorgetforum', true);
|
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_forum.errorgetforum', true);
|
||||||
|
|
||||||
this.discussions.fetchFailed = true;
|
this.fetchFailed = true;
|
||||||
} finally {
|
} finally {
|
||||||
complete();
|
complete();
|
||||||
}
|
}
|
||||||
|
@ -521,9 +463,13 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
};
|
};
|
||||||
|
|
||||||
const value = await getSortOrder();
|
const value = await getSortOrder();
|
||||||
|
const selectedOrder = this.sortOrders.find(sortOrder => sortOrder.value === value) || this.sortOrders[0];
|
||||||
|
|
||||||
this.selectedSortOrder = this.sortOrders.find(sortOrder => sortOrder.value === value) || this.sortOrders[0];
|
this.discussions.getSource().selectedSortOrder = selectedOrder;
|
||||||
this.sortOrderSelectorModalOptions.componentProps!.selected = this.selectedSortOrder.value;
|
|
||||||
|
if (this.sortOrderSelectorModalOptions.componentProps) {
|
||||||
|
this.sortOrderSelectorModalOptions.componentProps.selected = selectedOrder.value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -597,11 +543,11 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
if (isNewDiscussion && CoreScreen.isTablet) {
|
if (isNewDiscussion && CoreScreen.isTablet) {
|
||||||
const newDiscussionData = data as AddonModForumNewDiscussionData;
|
const newDiscussionData = data as AddonModForumNewDiscussionData;
|
||||||
const discussion = this.discussions.items.find(disc => {
|
const discussion = this.discussions.items.find(disc => {
|
||||||
if (this.discussions.isOfflineDiscussion(disc)) {
|
if (this.discussions.getSource().isOfflineDiscussion(disc)) {
|
||||||
return disc.timecreated === newDiscussionData.discTimecreated;
|
return disc.timecreated === newDiscussionData.discTimecreated;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.discussions.isOnlineDiscussion(disc)) {
|
if (this.discussions.getSource().isOnlineDiscussion(disc)) {
|
||||||
return CoreArray.contains(newDiscussionData.discussionIds ?? [], disc.discussion);
|
return CoreArray.contains(newDiscussionData.discussionIds ?? [], disc.discussion);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -625,7 +571,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
* @param timeCreated Creation time of the offline discussion.
|
* @param timeCreated Creation time of the offline discussion.
|
||||||
*/
|
*/
|
||||||
openNewDiscussion(): void {
|
openNewDiscussion(): void {
|
||||||
this.discussions.select({ newDiscussion: true });
|
this.discussions.select(AddonModForumDiscussionsSource.NEW_DISCUSSION);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -634,10 +580,13 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
* @param sortOrder Sort order new data.
|
* @param sortOrder Sort order new data.
|
||||||
*/
|
*/
|
||||||
async setSortOrder(sortOrder: AddonModForumSortOrder): Promise<void> {
|
async setSortOrder(sortOrder: AddonModForumSortOrder): Promise<void> {
|
||||||
if (sortOrder.value != this.selectedSortOrder?.value) {
|
if (sortOrder.value != this.discussions.getSource().selectedSortOrder?.value) {
|
||||||
this.selectedSortOrder = sortOrder;
|
this.discussions.getSource().selectedSortOrder = sortOrder;
|
||||||
this.sortOrderSelectorModalOptions.componentProps!.selected = this.selectedSortOrder.value;
|
this.discussions.getSource().setDirty(true);
|
||||||
this.page = 0;
|
|
||||||
|
if (this.sortOrderSelectorModalOptions.componentProps) {
|
||||||
|
this.sortOrderSelectorModalOptions.componentProps.selected = sortOrder.value;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await CoreUser.setUserPreference(AddonModForumProvider.PREFERENCE_SORTORDER, sortOrder.value.toFixed(0));
|
await CoreUser.setUserPreference(AddonModForumProvider.PREFERENCE_SORTORDER, sortOrder.value.toFixed(0));
|
||||||
|
@ -666,6 +615,10 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
* @param discussion Discussion.
|
* @param discussion Discussion.
|
||||||
*/
|
*/
|
||||||
async showOptionsMenu(event: Event, discussion: AddonModForumDiscussion): Promise<void> {
|
async showOptionsMenu(event: Event, discussion: AddonModForumDiscussion): Promise<void> {
|
||||||
|
if (!this.forum) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
|
@ -673,7 +626,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
component: AddonModForumDiscussionOptionsMenuComponent,
|
component: AddonModForumDiscussionOptionsMenuComponent,
|
||||||
componentProps: {
|
componentProps: {
|
||||||
discussion,
|
discussion,
|
||||||
forumId: this.forum!.id,
|
forumId: this.forum.id,
|
||||||
cmId: this.module.id,
|
cmId: this.module.id,
|
||||||
},
|
},
|
||||||
event,
|
event,
|
||||||
|
@ -698,125 +651,47 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Type to select the new discussion form.
|
|
||||||
*/
|
|
||||||
type NewDiscussionForm = { newDiscussion: true };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type of items that can be held by the discussions manager.
|
|
||||||
*/
|
|
||||||
type DiscussionItem = AddonModForumDiscussion | AddonModForumOfflineDiscussion | NewDiscussionForm;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Discussions manager.
|
* Discussions manager.
|
||||||
*/
|
*/
|
||||||
class AddonModForumDiscussionsManager extends CorePageItemsListManager<DiscussionItem> {
|
class AddonModForumDiscussionsManager extends CoreListItemsManager<AddonModForumDiscussionItem, AddonModForumDiscussionsSource> {
|
||||||
|
|
||||||
onlineLoaded = false;
|
page: AddonModForumIndexComponent;
|
||||||
fetchFailed = false;
|
|
||||||
|
|
||||||
private discussionsPathPrefix: string;
|
constructor(source: AddonModForumDiscussionsSource, page: AddonModForumIndexComponent) {
|
||||||
private component: AddonModForumIndexComponent;
|
super(source, page.route.component);
|
||||||
|
|
||||||
constructor(pageComponent: unknown, component: AddonModForumIndexComponent, discussionsPathPrefix: string) {
|
this.page = page;
|
||||||
super(pageComponent);
|
|
||||||
|
|
||||||
this.component = component;
|
|
||||||
this.discussionsPathPrefix = discussionsPathPrefix;
|
|
||||||
}
|
|
||||||
|
|
||||||
get loaded(): boolean {
|
|
||||||
return super.loaded && (this.onlineLoaded || this.fetchFailed);
|
|
||||||
}
|
|
||||||
|
|
||||||
get onlineDiscussions(): AddonModForumDiscussion[] {
|
|
||||||
return this.items.filter(discussion => this.isOnlineDiscussion(discussion)) as AddonModForumDiscussion[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
getItemQueryParams(discussion: DiscussionItem): Params {
|
protected getDefaultItem(): AddonModForumDiscussionItem | null {
|
||||||
return {
|
const source = this.getSource();
|
||||||
courseId: this.component.courseId,
|
|
||||||
cmId: this.component.module.id,
|
|
||||||
forumId: this.component.forum!.id,
|
|
||||||
...(this.isOnlineDiscussion(discussion) ? { discussion, trackPosts: this.component.trackPosts } : {}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
return this.items.find(discussion => !source.isNewDiscussionForm(discussion)) || null;
|
||||||
* Type guard to infer NewDiscussionForm objects.
|
|
||||||
*
|
|
||||||
* @param discussion Item to check.
|
|
||||||
* @return Whether the item is a new discussion form.
|
|
||||||
*/
|
|
||||||
isNewDiscussionForm(discussion: DiscussionItem): discussion is NewDiscussionForm {
|
|
||||||
return 'newDiscussion' in discussion;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type guard to infer AddonModForumDiscussion objects.
|
|
||||||
*
|
|
||||||
* @param discussion Item to check.
|
|
||||||
* @return Whether the item is an online discussion.
|
|
||||||
*/
|
|
||||||
isOfflineDiscussion(discussion: DiscussionItem): discussion is AddonModForumOfflineDiscussion {
|
|
||||||
return !this.isNewDiscussionForm(discussion)
|
|
||||||
&& !this.isOnlineDiscussion(discussion);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type guard to infer AddonModForumDiscussion objects.
|
|
||||||
*
|
|
||||||
* @param discussion Item to check.
|
|
||||||
* @return Whether the item is an online discussion.
|
|
||||||
*/
|
|
||||||
isOnlineDiscussion(discussion: DiscussionItem): discussion is AddonModForumDiscussion {
|
|
||||||
return 'id' in discussion;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update online discussion items.
|
|
||||||
*
|
|
||||||
* @param onlineDiscussions Online discussions
|
|
||||||
*/
|
|
||||||
setOnlineDiscussions(onlineDiscussions: AddonModForumDiscussion[], hasMoreItems: boolean = false): void {
|
|
||||||
const otherDiscussions = this.items.filter(discussion => !this.isOnlineDiscussion(discussion));
|
|
||||||
|
|
||||||
this.setItems(otherDiscussions.concat(onlineDiscussions), hasMoreItems);
|
|
||||||
this.onlineLoaded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update offline discussion items.
|
|
||||||
*
|
|
||||||
* @param offlineDiscussions Offline discussions
|
|
||||||
*/
|
|
||||||
setOfflineDiscussions(offlineDiscussions: AddonModForumOfflineDiscussion[]): void {
|
|
||||||
const otherDiscussions = this.items.filter(discussion => !this.isOfflineDiscussion(discussion));
|
|
||||||
|
|
||||||
this.setItems((offlineDiscussions as DiscussionItem[]).concat(otherDiscussions), this.hasMoreItems);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
protected getItemPath(discussion: DiscussionItem): string {
|
protected async logActivity(): Promise<void> {
|
||||||
const getRelativePath = () => {
|
const forum = this.getSource().forum;
|
||||||
if (this.isOnlineDiscussion(discussion)) {
|
|
||||||
return discussion.discussion;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isOfflineDiscussion(discussion)) {
|
if (!forum) {
|
||||||
return `new/${discussion.timecreated}`;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'new/0';
|
CoreUtils.ignoreErrors(
|
||||||
};
|
AddonModForum.instance
|
||||||
|
.logView(forum.id, forum.name)
|
||||||
|
.then(async () => {
|
||||||
|
CoreCourse.checkModuleCompletion(this.page.courseId, this.page.module.completiondata);
|
||||||
|
|
||||||
return this.discussionsPathPrefix + getRelativePath();
|
return;
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,6 +55,7 @@ const mainMenuRoutes: Routes = [
|
||||||
{
|
{
|
||||||
path: `${AddonModForumModuleHandlerService.PAGE_NAME}/discussion/:discussionId`,
|
path: `${AddonModForumModuleHandlerService.PAGE_NAME}/discussion/:discussionId`,
|
||||||
loadChildren: () => import('./pages/discussion/discussion.module').then(m => m.AddonForumDiscussionPageModule),
|
loadChildren: () => import('./pages/discussion/discussion.module').then(m => m.AddonForumDiscussionPageModule),
|
||||||
|
data: { swipeEnabled: false },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: AddonModForumModuleHandlerService.PAGE_NAME,
|
path: AddonModForumModuleHandlerService.PAGE_NAME,
|
||||||
|
@ -66,10 +67,12 @@ const mainMenuRoutes: Routes = [
|
||||||
path: `${COURSE_CONTENTS_PATH}/${AddonModForumModuleHandlerService.PAGE_NAME}/new/:timeCreated`,
|
path: `${COURSE_CONTENTS_PATH}/${AddonModForumModuleHandlerService.PAGE_NAME}/new/:timeCreated`,
|
||||||
loadChildren: () => import('./pages/new-discussion/new-discussion.module')
|
loadChildren: () => import('./pages/new-discussion/new-discussion.module')
|
||||||
.then(m => m.AddonForumNewDiscussionPageModule),
|
.then(m => m.AddonForumNewDiscussionPageModule),
|
||||||
|
data: { discussionsPathPrefix: `${AddonModForumModuleHandlerService.PAGE_NAME}/` },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `${COURSE_CONTENTS_PATH}/${AddonModForumModuleHandlerService.PAGE_NAME}/:discussionId`,
|
path: `${COURSE_CONTENTS_PATH}/${AddonModForumModuleHandlerService.PAGE_NAME}/:discussionId`,
|
||||||
loadChildren: () => import('./pages/discussion/discussion.module').then(m => m.AddonForumDiscussionPageModule),
|
loadChildren: () => import('./pages/discussion/discussion.module').then(m => m.AddonForumDiscussionPageModule),
|
||||||
|
data: { discussionsPathPrefix: `${AddonModForumModuleHandlerService.PAGE_NAME}/` },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
() => CoreScreen.isMobile,
|
() => CoreScreen.isMobile,
|
||||||
|
@ -82,10 +85,12 @@ const courseContentsRoutes: Routes = conditionalRoutes(
|
||||||
path: `${AddonModForumModuleHandlerService.PAGE_NAME}/new/:timeCreated`,
|
path: `${AddonModForumModuleHandlerService.PAGE_NAME}/new/:timeCreated`,
|
||||||
loadChildren: () => import('./pages/new-discussion/new-discussion.module')
|
loadChildren: () => import('./pages/new-discussion/new-discussion.module')
|
||||||
.then(m => m.AddonForumNewDiscussionPageModule),
|
.then(m => m.AddonForumNewDiscussionPageModule),
|
||||||
|
data: { discussionsPathPrefix: `${AddonModForumModuleHandlerService.PAGE_NAME}/` },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `${AddonModForumModuleHandlerService.PAGE_NAME}/:discussionId`,
|
path: `${AddonModForumModuleHandlerService.PAGE_NAME}/:discussionId`,
|
||||||
loadChildren: () => import('./pages/discussion/discussion.module').then(m => m.AddonForumDiscussionPageModule),
|
loadChildren: () => import('./pages/discussion/discussion.module').then(m => m.AddonForumDiscussionPageModule),
|
||||||
|
data: { discussionsPathPrefix: `${AddonModForumModuleHandlerService.PAGE_NAME}/` },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
() => CoreScreen.isTablet,
|
() => CoreScreen.isTablet,
|
||||||
|
|
|
@ -56,72 +56,74 @@
|
||||||
</core-context-menu>
|
</core-context-menu>
|
||||||
</core-navbar-buttons>
|
</core-navbar-buttons>
|
||||||
<ion-content>
|
<ion-content>
|
||||||
<ion-refresher slot="fixed" [disabled]="!discussionLoaded" (ionRefresh)="doRefresh($event.target)">
|
<core-swipe-navigation [manager]="discussions">
|
||||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
<ion-refresher slot="fixed" [disabled]="!discussionLoaded" (ionRefresh)="doRefresh($event.target)">
|
||||||
</ion-refresher>
|
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||||
|
</ion-refresher>
|
||||||
|
|
||||||
<core-loading [hideUntil]="discussionLoaded">
|
<core-loading [hideUntil]="discussionLoaded">
|
||||||
<!-- Discussion replies found to be synchronized -->
|
<!-- Discussion replies found to be synchronized -->
|
||||||
<ion-card class="core-warning-card" *ngIf="postHasOffline || hasOfflineRatings">
|
<ion-card class="core-warning-card" *ngIf="postHasOffline || hasOfflineRatings">
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-icon name="fas-exclamation-triangle" slot="start" aria-hidden="true"></ion-icon>
|
<ion-icon name="fas-exclamation-triangle" slot="start" aria-hidden="true"></ion-icon>
|
||||||
<ion-label>{{ 'core.hasdatatosync' | translate:{$a: discussionStr} }}</ion-label>
|
<ion-label>{{ 'core.hasdatatosync' | translate:{$a: discussionStr} }}</ion-label>
|
||||||
</ion-item>
|
</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" aria-hidden="true"></ion-icon>
|
|
||||||
<ion-label>{{ availabilityMessage }}</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
</ion-card>
|
|
||||||
|
|
||||||
<ion-card class="core-info-card" *ngIf="discussion && discussion.locked">
|
|
||||||
<ion-item>
|
|
||||||
<ion-icon name="fas-lock" slot="start" aria-hidden="true"></ion-icon>
|
|
||||||
<ion-label>{{ 'addon.mod_forum.discussionlocked' | translate }}</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
</ion-card>
|
|
||||||
|
|
||||||
<div *ngIf="startingPost" class="ion-margin-bottom">
|
|
||||||
<addon-mod-forum-post [post]="startingPost" [discussion]="discussion" [courseId]="courseId" [highlight]="true"
|
|
||||||
[discussionId]="discussionId" [component]="component" [componentId]="cmId" [formData]="formData"
|
|
||||||
[originalData]="originalData" [forum]="forum" [accessInfo]="accessInfo" [trackPosts]="trackPosts" [ratingInfo]="ratingInfo"
|
|
||||||
[leavingPage]="leavingPage" (onPostChange)="postListChanged()">
|
|
||||||
</addon-mod-forum-post>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ion-card *ngIf="sort != 'nested'">
|
|
||||||
<ng-container *ngFor="let post of posts; first as first">
|
|
||||||
<core-spacer *ngIf="!first"></core-spacer>
|
|
||||||
<addon-mod-forum-post [post]="post" [courseId]="courseId" [discussionId]="discussionId" [component]="component"
|
|
||||||
[componentId]="cmId" [formData]="formData" [originalData]="originalData" [parentSubject]="postSubjects[post.parentid]"
|
|
||||||
[forum]="forum" [accessInfo]="accessInfo" [trackPosts]="trackPosts" [ratingInfo]="ratingInfo"
|
|
||||||
[leavingPage]="leavingPage" (onPostChange)="postListChanged()">
|
|
||||||
</addon-mod-forum-post>
|
|
||||||
</ng-container>
|
|
||||||
</ion-card>
|
|
||||||
|
|
||||||
<ng-container *ngIf="sort == 'nested'">
|
|
||||||
<ng-container *ngFor="let post of posts">
|
|
||||||
<ng-container *ngTemplateOutlet="nestedPosts; context: {post: post}"></ng-container>
|
|
||||||
</ng-container>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-template #nestedPosts let-post="post">
|
|
||||||
<ion-card>
|
|
||||||
<addon-mod-forum-post [post]="post" [courseId]="courseId" [discussionId]="discussionId" [component]="component"
|
|
||||||
[componentId]="cmId" [formData]="formData" [originalData]="originalData" [parentSubject]="postSubjects[post.parentid]"
|
|
||||||
[forum]="forum" [accessInfo]="accessInfo" [trackPosts]="trackPosts" [ratingInfo]="ratingInfo"
|
|
||||||
[leavingPage]="leavingPage" (onPostChange)="postListChanged()">
|
|
||||||
</addon-mod-forum-post>
|
|
||||||
</ion-card>
|
</ion-card>
|
||||||
<div class="ion-padding-start" *ngIf="post.children && post.children.length && post.children[0].subject">
|
|
||||||
<ng-container *ngFor="let child of post.children">
|
<!-- Cut-off date or due date message -->
|
||||||
<ng-container *ngTemplateOutlet="nestedPosts; context: {post: child}"></ng-container>
|
<ion-card class="core-info-card" *ngIf="availabilityMessage">
|
||||||
</ng-container>
|
<ion-item>
|
||||||
|
<ion-icon name="fas-info-circle" slot="start" aria-hidden="true"></ion-icon>
|
||||||
|
<ion-label>{{ availabilityMessage }}</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ion-card>
|
||||||
|
|
||||||
|
<ion-card class="core-info-card" *ngIf="discussion && discussion.locked">
|
||||||
|
<ion-item>
|
||||||
|
<ion-icon name="fas-lock" slot="start" aria-hidden="true"></ion-icon>
|
||||||
|
<ion-label>{{ 'addon.mod_forum.discussionlocked' | translate }}</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ion-card>
|
||||||
|
|
||||||
|
<div *ngIf="startingPost" class="ion-margin-bottom">
|
||||||
|
<addon-mod-forum-post [post]="startingPost" [discussion]="discussion" [courseId]="courseId" [highlight]="true"
|
||||||
|
[discussionId]="discussionId" [component]="component" [componentId]="cmId" [formData]="formData"
|
||||||
|
[originalData]="originalData" [forum]="forum" [accessInfo]="accessInfo" [trackPosts]="trackPosts"
|
||||||
|
[ratingInfo]="ratingInfo" [leavingPage]="leavingPage" (onPostChange)="postListChanged()">
|
||||||
|
</addon-mod-forum-post>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
|
||||||
</core-loading>
|
<ion-card *ngIf="sort != 'nested'">
|
||||||
|
<ng-container *ngFor="let post of posts; first as first">
|
||||||
|
<core-spacer *ngIf="!first"></core-spacer>
|
||||||
|
<addon-mod-forum-post [post]="post" [courseId]="courseId" [discussionId]="discussionId" [component]="component"
|
||||||
|
[componentId]="cmId" [formData]="formData" [originalData]="originalData"
|
||||||
|
[parentSubject]="postSubjects[post.parentid]" [forum]="forum" [accessInfo]="accessInfo" [trackPosts]="trackPosts"
|
||||||
|
[ratingInfo]="ratingInfo" [leavingPage]="leavingPage" (onPostChange)="postListChanged()">
|
||||||
|
</addon-mod-forum-post>
|
||||||
|
</ng-container>
|
||||||
|
</ion-card>
|
||||||
|
|
||||||
|
<ng-container *ngIf="sort == 'nested'">
|
||||||
|
<ng-container *ngFor="let post of posts">
|
||||||
|
<ng-container *ngTemplateOutlet="nestedPosts; context: {post: post}"></ng-container>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-template #nestedPosts let-post="post">
|
||||||
|
<ion-card>
|
||||||
|
<addon-mod-forum-post [post]="post" [courseId]="courseId" [discussionId]="discussionId" [component]="component"
|
||||||
|
[componentId]="cmId" [formData]="formData" [originalData]="originalData"
|
||||||
|
[parentSubject]="postSubjects[post.parentid]" [forum]="forum" [accessInfo]="accessInfo" [trackPosts]="trackPosts"
|
||||||
|
[ratingInfo]="ratingInfo" [leavingPage]="leavingPage" (onPostChange)="postListChanged()">
|
||||||
|
</addon-mod-forum-post>
|
||||||
|
</ion-card>
|
||||||
|
<div class="ion-padding-start" *ngIf="post.children && post.children.length && post.children[0].subject">
|
||||||
|
<ng-container *ngFor="let child of post.children">
|
||||||
|
<ng-container *ngTemplateOutlet="nestedPosts; context: {post: child}"></ng-container>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</core-loading>
|
||||||
|
</core-swipe-navigation>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|
|
@ -14,6 +14,8 @@
|
||||||
|
|
||||||
import { ContextLevel, CoreConstants } from '@/core/constants';
|
import { ContextLevel, CoreConstants } from '@/core/constants';
|
||||||
import { Component, OnDestroy, ViewChild, OnInit, AfterViewInit, ElementRef, Optional } from '@angular/core';
|
import { Component, OnDestroy, ViewChild, OnInit, AfterViewInit, ElementRef, Optional } from '@angular/core';
|
||||||
|
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
|
||||||
|
import { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker';
|
||||||
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||||
import { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
|
import { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
|
||||||
import { CoreRatingInfo, CoreRatingProvider } from '@features/rating/services/rating';
|
import { CoreRatingInfo, CoreRatingProvider } from '@features/rating/services/rating';
|
||||||
|
@ -32,6 +34,8 @@ import { Network, NgZone, Translate } from '@singletons';
|
||||||
import { CoreArray } from '@singletons/array';
|
import { CoreArray } from '@singletons/array';
|
||||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
|
import { AddonModForumDiscussionsSource } from '../../classes/forum-discussions-source';
|
||||||
|
import { AddonModForumDiscussionsSwipeManager } from '../../classes/forum-discussions-swipe-manager';
|
||||||
import {
|
import {
|
||||||
AddonModForum,
|
AddonModForum,
|
||||||
AddonModForumAccessInformation,
|
AddonModForumAccessInformation,
|
||||||
|
@ -68,6 +72,7 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
|
||||||
forum: Partial<AddonModForumData> = {};
|
forum: Partial<AddonModForumData> = {};
|
||||||
accessInfo: AddonModForumAccessInformation = {};
|
accessInfo: AddonModForumAccessInformation = {};
|
||||||
discussion?: AddonModForumDiscussion;
|
discussion?: AddonModForumDiscussion;
|
||||||
|
discussions?: AddonModForumDiscussionDiscussionsSwipeManager;
|
||||||
startingPost?: Post;
|
startingPost?: Post;
|
||||||
posts!: Post[];
|
posts!: Post[];
|
||||||
discussionLoaded = false;
|
discussionLoaded = false;
|
||||||
|
@ -117,14 +122,16 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
|
||||||
constructor(
|
constructor(
|
||||||
@Optional() protected splitView: CoreSplitViewComponent,
|
@Optional() protected splitView: CoreSplitViewComponent,
|
||||||
protected elementRef: ElementRef,
|
protected elementRef: ElementRef,
|
||||||
|
protected route: ActivatedRoute,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
get isMobile(): boolean {
|
get isMobile(): boolean {
|
||||||
return CoreScreen.isMobile;
|
return CoreScreen.isMobile;
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
async ngOnInit(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
const routeData = this.route.snapshot.data;
|
||||||
this.courseId = CoreNavigator.getRouteNumberParam('courseId');
|
this.courseId = CoreNavigator.getRouteNumberParam('courseId');
|
||||||
this.cmId = CoreNavigator.getRouteNumberParam('cmId');
|
this.cmId = CoreNavigator.getRouteNumberParam('cmId');
|
||||||
this.forumId = CoreNavigator.getRouteNumberParam('forumId');
|
this.forumId = CoreNavigator.getRouteNumberParam('forumId');
|
||||||
|
@ -136,6 +143,16 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
|
||||||
this.postId = CoreNavigator.getRouteNumberParam('postId');
|
this.postId = CoreNavigator.getRouteNumberParam('postId');
|
||||||
this.parent = CoreNavigator.getRouteNumberParam('parent');
|
this.parent = CoreNavigator.getRouteNumberParam('parent');
|
||||||
|
|
||||||
|
if (this.courseId && this.cmId && (routeData.swipeEnabled ?? true)) {
|
||||||
|
this.discussions = new AddonModForumDiscussionDiscussionsSwipeManager(
|
||||||
|
CoreItemsManagerSourcesTracker.getOrCreateSource(
|
||||||
|
AddonModForumDiscussionsSource,
|
||||||
|
[this.courseId, this.cmId, routeData.discussionsPathPrefix ?? ''],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.discussions.start();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CoreDomUtils.showErrorModal(error);
|
CoreDomUtils.showErrorModal(error);
|
||||||
|
|
||||||
|
@ -311,6 +328,7 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
|
||||||
*/
|
*/
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.onlineObserver && this.onlineObserver.unsubscribe();
|
this.onlineObserver && this.onlineObserver.unsubscribe();
|
||||||
|
this.discussions && this.discussions.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -839,3 +857,17 @@ export type AddonModForumSharedPostFormData = Omit<AddonModForumPostFormData, 'i
|
||||||
id?: number; // ID when editing an online reply.
|
id?: number; // ID when editing an online reply.
|
||||||
syncId?: string; // Sync ID if some post has blocked synchronization.
|
syncId?: string; // Sync ID if some post has blocked synchronization.
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to manage swiping within a collection of discussions.
|
||||||
|
*/
|
||||||
|
class AddonModForumDiscussionDiscussionsSwipeManager extends AddonModForumDiscussionsSwipeManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null {
|
||||||
|
return this.getSource().DISCUSSIONS_PATH_PREFIX + route.params.discussionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -12,76 +12,77 @@
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
<ion-content>
|
<ion-content>
|
||||||
<ion-refresher slot="fixed" [disabled]="!groupsLoaded" (ionRefresh)="refreshGroups($event.target)">
|
<core-swipe-navigation [manager]="discussions">
|
||||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
<ion-refresher slot="fixed" [disabled]="!groupsLoaded" (ionRefresh)="refreshGroups($event.target)">
|
||||||
</ion-refresher>
|
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||||
|
</ion-refresher>
|
||||||
<core-loading [hideUntil]="groupsLoaded">
|
<core-loading [hideUntil]="groupsLoaded">
|
||||||
<form *ngIf="showForm" #newDiscFormEl>
|
<form *ngIf="showForm" #newDiscFormEl>
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-label position="stacked">{{ 'addon.mod_forum.subject' | translate }}</ion-label>
|
<ion-label position="stacked">{{ 'addon.mod_forum.subject' | translate }}</ion-label>
|
||||||
<ion-input [(ngModel)]="newDiscussion.subject" type="text" [placeholder]="'addon.mod_forum.subject' | translate"
|
<ion-input [(ngModel)]="newDiscussion.subject" type="text" [placeholder]="'addon.mod_forum.subject' | translate"
|
||||||
name="subject">
|
name="subject">
|
||||||
</ion-input>
|
</ion-input>
|
||||||
</ion-item>
|
|
||||||
<ion-item>
|
|
||||||
<ion-label position="stacked">{{ 'addon.mod_forum.message' | translate }}</ion-label>
|
|
||||||
<core-rich-text-editor name="addon_mod_forum_new_discussion" contextLevel="module" elementId="message"
|
|
||||||
[control]="messageControl" [placeholder]="'addon.mod_forum.message' | translate" [component]="component"
|
|
||||||
[componentId]="forum.cmid" [autoSave]="true" [contextInstanceId]="forum.cmid"
|
|
||||||
(contentChanged)="onMessageChange($event)">
|
|
||||||
</core-rich-text-editor>
|
|
||||||
</ion-item>
|
|
||||||
<ion-item button class="divider ion-text-wrap" (click)="toggleAdvanced()" detail="false" [attr.aria-expanded]="advanced"
|
|
||||||
[attr.aria-label]="(advanced ? 'core.hideadvanced' : 'core.showadvanced') | translate" role="heading"
|
|
||||||
aria-controls="addon-mod-forum-new-discussion-advanced">
|
|
||||||
<ion-icon *ngIf="!advanced" name="fas-caret-right" flip-rtl slot="start" aria-hidden="true"></ion-icon>
|
|
||||||
<ion-icon *ngIf="advanced" name="fas-caret-down" slot="start" aria-hidden="true"></ion-icon>
|
|
||||||
<ion-label>
|
|
||||||
<h2>{{ 'addon.mod_forum.advanced' | translate }}</h2>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
<div *ngIf="advanced" id="addon-mod-forum-new-discussion-advanced">
|
|
||||||
<ion-item *ngIf="showGroups && groupIds.length > 1 && accessInfo.cancanposttomygroups">
|
|
||||||
<ion-label>{{ 'addon.mod_forum.posttomygroups' | translate }}</ion-label>
|
|
||||||
<ion-toggle [(ngModel)]="newDiscussion.postToAllGroups" name="postallgroups"></ion-toggle>
|
|
||||||
</ion-item>
|
|
||||||
<ion-item *ngIf="showGroups">
|
|
||||||
<ion-label id="addon-mod-forum-groupslabel">{{ 'addon.mod_forum.group' | translate }}</ion-label>
|
|
||||||
<ion-select [(ngModel)]="newDiscussion.groupId" [disabled]="newDiscussion.postToAllGroups"
|
|
||||||
aria-labelledby="addon-mod-forum-groupslabel" interface="action-sheet" name="groupid"
|
|
||||||
[interfaceOptions]="{header: 'addon.mod_forum.group' | translate}">
|
|
||||||
<ion-select-option *ngFor="let group of groups" [value]="group.id">{{ group.name }}</ion-select-option>
|
|
||||||
</ion-select>
|
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-label>{{ 'addon.mod_forum.discussionsubscription' | translate }}</ion-label>
|
<ion-label position="stacked">{{ 'addon.mod_forum.message' | translate }}</ion-label>
|
||||||
<ion-toggle [(ngModel)]="newDiscussion.subscribe" name="subscribe"></ion-toggle>
|
<core-rich-text-editor name="addon_mod_forum_new_discussion" contextLevel="module" elementId="message"
|
||||||
|
[control]="messageControl" [placeholder]="'addon.mod_forum.message' | translate" [component]="component"
|
||||||
|
[componentId]="forum.cmid" [autoSave]="true" [contextInstanceId]="forum.cmid"
|
||||||
|
(contentChanged)="onMessageChange($event)">
|
||||||
|
</core-rich-text-editor>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item *ngIf="canPin">
|
<ion-item button class="divider ion-text-wrap" (click)="toggleAdvanced()" detail="false" [attr.aria-expanded]="advanced"
|
||||||
<ion-label>{{ 'addon.mod_forum.discussionpinned' | translate }}</ion-label>
|
[attr.aria-label]="(advanced ? 'core.hideadvanced' : 'core.showadvanced') | translate" role="heading"
|
||||||
<ion-toggle [(ngModel)]="newDiscussion.pin" name="pin"></ion-toggle>
|
aria-controls="addon-mod-forum-new-discussion-advanced">
|
||||||
|
<ion-icon *ngIf="!advanced" name="fas-caret-right" flip-rtl slot="start" aria-hidden="true"></ion-icon>
|
||||||
|
<ion-icon *ngIf="advanced" name="fas-caret-down" slot="start" aria-hidden="true"></ion-icon>
|
||||||
|
<ion-label>
|
||||||
|
<h2>{{ 'addon.mod_forum.advanced' | translate }}</h2>
|
||||||
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<core-attachments *ngIf="canCreateAttachments && forum && forum.maxattachments > 0" [files]="newDiscussion.files"
|
<div *ngIf="advanced" id="addon-mod-forum-new-discussion-advanced">
|
||||||
[maxSize]="forum.maxbytes" [maxSubmissions]="forum.maxattachments" [component]="component" [componentId]="forum.cmid"
|
<ion-item *ngIf="showGroups && groupIds.length > 1 && accessInfo.cancanposttomygroups">
|
||||||
[allowOffline]="true" [courseId]="courseId">
|
<ion-label>{{ 'addon.mod_forum.posttomygroups' | translate }}</ion-label>
|
||||||
</core-attachments>
|
<ion-toggle [(ngModel)]="newDiscussion.postToAllGroups" name="postallgroups"></ion-toggle>
|
||||||
</div>
|
</ion-item>
|
||||||
<ion-item>
|
<ion-item *ngIf="showGroups">
|
||||||
<ion-label>
|
<ion-label id="addon-mod-forum-groupslabel">{{ 'addon.mod_forum.group' | translate }}</ion-label>
|
||||||
<ion-row>
|
<ion-select [(ngModel)]="newDiscussion.groupId" [disabled]="newDiscussion.postToAllGroups"
|
||||||
<ion-col>
|
aria-labelledby="addon-mod-forum-groupslabel" interface="action-sheet" name="groupid"
|
||||||
<ion-button expand="block" [disabled]="newDiscussion.subject == '' || newDiscussion.message == null"
|
[interfaceOptions]="{header: 'addon.mod_forum.group' | translate}">
|
||||||
(click)="add()">
|
<ion-select-option *ngFor="let group of groups" [value]="group.id">{{ group.name }}</ion-select-option>
|
||||||
{{ 'addon.mod_forum.posttoforum' | translate }}
|
</ion-select>
|
||||||
</ion-button>
|
</ion-item>
|
||||||
</ion-col>
|
<ion-item>
|
||||||
<ion-col *ngIf="hasOffline">
|
<ion-label>{{ 'addon.mod_forum.discussionsubscription' | translate }}</ion-label>
|
||||||
<ion-button expand="block" color="light" (click)="discard()">{{ 'core.discard' | translate }}</ion-button>
|
<ion-toggle [(ngModel)]="newDiscussion.subscribe" name="subscribe"></ion-toggle>
|
||||||
</ion-col>
|
</ion-item>
|
||||||
</ion-row>
|
<ion-item *ngIf="canPin">
|
||||||
</ion-label>
|
<ion-label>{{ 'addon.mod_forum.discussionpinned' | translate }}</ion-label>
|
||||||
</ion-item>
|
<ion-toggle [(ngModel)]="newDiscussion.pin" name="pin"></ion-toggle>
|
||||||
</form>
|
</ion-item>
|
||||||
</core-loading>
|
<core-attachments *ngIf="canCreateAttachments && forum && forum.maxattachments > 0" [files]="newDiscussion.files"
|
||||||
|
[maxSize]="forum.maxbytes" [maxSubmissions]="forum.maxattachments" [component]="component"
|
||||||
|
[componentId]="forum.cmid" [allowOffline]="true" [courseId]="courseId">
|
||||||
|
</core-attachments>
|
||||||
|
</div>
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>
|
||||||
|
<ion-row>
|
||||||
|
<ion-col>
|
||||||
|
<ion-button expand="block" [disabled]="newDiscussion.subject == '' || newDiscussion.message == null"
|
||||||
|
(click)="add()">
|
||||||
|
{{ 'addon.mod_forum.posttoforum' | translate }}
|
||||||
|
</ion-button>
|
||||||
|
</ion-col>
|
||||||
|
<ion-col *ngIf="hasOffline">
|
||||||
|
<ion-button expand="block" color="light" (click)="discard()">{{ 'core.discard' | translate }}</ion-button>
|
||||||
|
</ion-col>
|
||||||
|
</ion-row>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</form>
|
||||||
|
</core-loading>
|
||||||
|
</core-swipe-navigation>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|
|
@ -40,6 +40,10 @@ import { CoreTextUtils } from '@services/utils/text';
|
||||||
import { CanLeave } from '@guards/can-leave';
|
import { CanLeave } from '@guards/can-leave';
|
||||||
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||||
import { CoreForms } from '@singletons/form';
|
import { CoreForms } from '@singletons/form';
|
||||||
|
import { AddonModForumDiscussionsSwipeManager } from '../../classes/forum-discussions-swipe-manager';
|
||||||
|
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
|
||||||
|
import { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker';
|
||||||
|
import { AddonModForumDiscussionsSource } from '../../classes/forum-discussions-source';
|
||||||
|
|
||||||
type NewDiscussionData = {
|
type NewDiscussionData = {
|
||||||
subject: string;
|
subject: string;
|
||||||
|
@ -88,6 +92,8 @@ export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLea
|
||||||
accessInfo: AddonModForumAccessInformation = {};
|
accessInfo: AddonModForumAccessInformation = {};
|
||||||
courseId!: number;
|
courseId!: number;
|
||||||
|
|
||||||
|
discussions?: AddonModForumNewDiscussionDiscussionsSwipeManager;
|
||||||
|
|
||||||
protected cmId!: number;
|
protected cmId!: number;
|
||||||
protected forumId!: number;
|
protected forumId!: number;
|
||||||
protected timeCreated!: number;
|
protected timeCreated!: number;
|
||||||
|
@ -97,17 +103,29 @@ export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLea
|
||||||
protected originalData?: Partial<NewDiscussionData>;
|
protected originalData?: Partial<NewDiscussionData>;
|
||||||
protected forceLeave = false;
|
protected forceLeave = false;
|
||||||
|
|
||||||
constructor(@Optional() protected splitView: CoreSplitViewComponent) {}
|
constructor(protected route: ActivatedRoute, @Optional() protected splitView: CoreSplitViewComponent) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component being initialized.
|
* Component being initialized.
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
async ngOnInit(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
const routeData = this.route.snapshot.data;
|
||||||
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
||||||
this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
|
this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
|
||||||
this.forumId = CoreNavigator.getRequiredRouteNumberParam('forumId');
|
this.forumId = CoreNavigator.getRequiredRouteNumberParam('forumId');
|
||||||
this.timeCreated = CoreNavigator.getRequiredRouteNumberParam('timeCreated');
|
this.timeCreated = CoreNavigator.getRequiredRouteNumberParam('timeCreated');
|
||||||
|
|
||||||
|
if (this.timeCreated !== 0 && (routeData.swipeEnabled ?? true)) {
|
||||||
|
const source = CoreItemsManagerSourcesTracker.getOrCreateSource(
|
||||||
|
AddonModForumDiscussionsSource,
|
||||||
|
[this.courseId, this.cmId, routeData.discussionsPathPrefix ?? ''],
|
||||||
|
);
|
||||||
|
|
||||||
|
this.discussions = new AddonModForumNewDiscussionDiscussionsSwipeManager(source);
|
||||||
|
|
||||||
|
await this.discussions.start();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CoreDomUtils.showErrorModal(error);
|
CoreDomUtils.showErrorModal(error);
|
||||||
|
|
||||||
|
@ -625,3 +643,17 @@ export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLea
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to manage swiping within a collection of discussions.
|
||||||
|
*/
|
||||||
|
class AddonModForumNewDiscussionDiscussionsSwipeManager extends AddonModForumDiscussionsSwipeManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null {
|
||||||
|
return `${this.getSource().DISCUSSIONS_PATH_PREFIX}new/${route.params.timeCreated}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -198,7 +198,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
|
|
||||||
const [hasOfflineRatings] = await Promise.all([
|
const [hasOfflineRatings] = await Promise.all([
|
||||||
CoreRatingOffline.hasRatings('mod_glossary', 'entry', ContextLevel.MODULE, this.glossary.coursemodule),
|
CoreRatingOffline.hasRatings('mod_glossary', 'entry', ContextLevel.MODULE, this.glossary.coursemodule),
|
||||||
refresh ? this.entries.reload() : this.entries.loadNextPage(),
|
refresh ? this.entries.reload() : this.entries.load(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
this.hasOfflineRatings = hasOfflineRatings;
|
this.hasOfflineRatings = hasOfflineRatings;
|
||||||
|
@ -307,7 +307,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
try {
|
try {
|
||||||
this.loadMoreError = false;
|
this.loadMoreError = false;
|
||||||
|
|
||||||
await this.entries.loadNextPage();
|
await this.entries.load();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.loadMoreError = true;
|
this.loadMoreError = true;
|
||||||
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentries', true);
|
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentries', true);
|
||||||
|
|
|
@ -40,6 +40,7 @@ export abstract class CoreItemsManagerSource<Item = unknown> {
|
||||||
protected items: Item[] | null = null;
|
protected items: Item[] | null = null;
|
||||||
protected hasMoreItems = true;
|
protected hasMoreItems = true;
|
||||||
protected listeners: CoreItemsListSourceListener<Item>[] = [];
|
protected listeners: CoreItemsListSourceListener<Item>[] = [];
|
||||||
|
protected dirty = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether any page has been loaded.
|
* Check whether any page has been loaded.
|
||||||
|
@ -59,6 +60,17 @@ export abstract class CoreItemsManagerSource<Item = unknown> {
|
||||||
return !this.hasMoreItems;
|
return !this.hasMoreItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set whether the source as dirty.
|
||||||
|
*
|
||||||
|
* When a source is dirty, the next load request will reload items from the beginning.
|
||||||
|
*
|
||||||
|
* @param dirty Whether source should be marked as dirty or not.
|
||||||
|
*/
|
||||||
|
setDirty(dirty: boolean): void {
|
||||||
|
this.dirty = dirty;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get collection items.
|
* Get collection items.
|
||||||
*
|
*
|
||||||
|
@ -92,6 +104,7 @@ export abstract class CoreItemsManagerSource<Item = unknown> {
|
||||||
reset(): void {
|
reset(): void {
|
||||||
this.items = null;
|
this.items = null;
|
||||||
this.hasMoreItems = true;
|
this.hasMoreItems = true;
|
||||||
|
this.dirty = false;
|
||||||
|
|
||||||
this.listeners.forEach(listener => listener.onReset?.call(listener));
|
this.listeners.forEach(listener => listener.onReset?.call(listener));
|
||||||
}
|
}
|
||||||
|
@ -129,13 +142,23 @@ export abstract class CoreItemsManagerSource<Item = unknown> {
|
||||||
async reload(): Promise<void> {
|
async reload(): Promise<void> {
|
||||||
const { items, hasMoreItems } = await this.loadPageItems(0);
|
const { items, hasMoreItems } = await this.loadPageItems(0);
|
||||||
|
|
||||||
|
this.dirty = false;
|
||||||
this.setItems(items, hasMoreItems ?? false);
|
this.setItems(items, hasMoreItems ?? false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load items for the next page, if any.
|
* Load more items, if any.
|
||||||
*/
|
*/
|
||||||
async loadNextPage(): Promise<void> {
|
async load(): Promise<void> {
|
||||||
|
if (this.dirty) {
|
||||||
|
const { items, hasMoreItems } = await this.loadPageItems(0);
|
||||||
|
|
||||||
|
this.dirty = false;
|
||||||
|
this.setItems(items, hasMoreItems ?? false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.hasMoreItems) {
|
if (!this.hasMoreItems) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -140,10 +140,10 @@ export class CoreListItemsManager<
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load items for the next page, if any.
|
* Load more items, if any.
|
||||||
*/
|
*/
|
||||||
async loadNextPage(): Promise<void> {
|
async load(): Promise<void> {
|
||||||
await this.getSource().loadNextPage();
|
await this.getSource().load();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -110,7 +110,7 @@ export class CoreSwipeItemsManager<
|
||||||
const item = items?.[index + delta] ?? null;
|
const item = items?.[index + delta] ?? null;
|
||||||
|
|
||||||
if (!item && !this.getSource().isCompleted()) {
|
if (!item && !this.getSource().isCompleted()) {
|
||||||
await this.getSource().loadNextPage();
|
await this.getSource().load();
|
||||||
|
|
||||||
return this.getItemBy(delta);
|
return this.getItemBy(delta);
|
||||||
}
|
}
|
||||||
|
|
|
@ -185,7 +185,7 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro
|
||||||
private async fetchParticipants(reload: boolean): Promise<void> {
|
private async fetchParticipants(reload: boolean): Promise<void> {
|
||||||
reload
|
reload
|
||||||
? await this.participants.reload()
|
? await this.participants.reload()
|
||||||
: await this.participants.loadNextPage();
|
: await this.participants.load();
|
||||||
|
|
||||||
this.fetchMoreParticipantsFailed = false;
|
this.fetchMoreParticipantsFailed = false;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue