MOBILE-3905 course: Swipe between participants

main
Noel De Martin 2021-11-09 13:08:56 +01:00
parent df92c97802
commit 429aecf3aa
21 changed files with 1205 additions and 124 deletions

14
package-lock.json generated
View File

@ -88,6 +88,7 @@
"cordova.plugins.diagnostic": "^5.0.2",
"core-js": "^3.9.1",
"es6-promise-plugin": "^4.2.2",
"hammerjs": "^2.0.8",
"jszip": "^3.5.0",
"mathjax": "2.7.7",
"moment": "^2.29.0",
@ -15988,6 +15989,14 @@
"node": ">= 0.10"
}
},
"node_modules/hammerjs": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz",
"integrity": "sha1-BO93hiz/K7edMPdpIJWTAiK/YPE=",
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/handle-thing": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz",
@ -43687,6 +43696,11 @@
"glogg": "^1.0.0"
}
},
"hammerjs": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz",
"integrity": "sha1-BO93hiz/K7edMPdpIJWTAiK/YPE="
},
"handle-thing": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz",

View File

@ -117,6 +117,7 @@
"cordova.plugins.diagnostic": "^5.0.2",
"core-js": "^3.9.1",
"es6-promise-plugin": "^4.2.2",
"hammerjs": "^2.0.8",
"jszip": "^3.5.0",
"mathjax": "2.7.7",
"moment": "^2.29.0",
@ -245,4 +246,4 @@
"optionalDependencies": {
"keytar": "^7.2.0"
}
}
}

View File

@ -0,0 +1,27 @@
// (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 { HammerGestureConfig } from '@angular/platform-browser';
import { DIRECTION_ALL } from 'hammerjs';
/**
* Application HammerJS config.
*/
export class CoreHammerGestureConfig extends HammerGestureConfig {
overrides = {
swipe: { direction: DIRECTION_ALL },
};
}

View File

@ -0,0 +1,169 @@
// (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.
/**
* Updates listener.
*/
export interface CoreItemsListSourceListener<Item> {
onItemsUpdated(items: Item[], hasMoreItems: boolean): void;
onReset(): void;
}
/**
* Items collection source data.
*/
export abstract class CoreItemsManagerSource<Item = unknown> {
/**
* Get a string to identify instances constructed with the given arguments as being reusable.
*
* @param args Constructor arguments.
* @returns Id.
*/
static getSourceId(...args: unknown[]): string {
return args.map(argument => String(argument)).join('-');
}
private items: Item[] | null = null;
private hasMoreItems = true;
private listeners: CoreItemsListSourceListener<Item>[] = [];
/**
* Check whether any page has been loaded.
*
* @returns Whether any page has been loaded.
*/
isLoaded(): boolean {
return this.items !== null;
}
/**
* Check whether there are more pages to be loaded.
*
* @return Whether there are more pages to be loaded.
*/
isCompleted(): boolean {
return !this.hasMoreItems;
}
/**
* Get collection items.
*
* @returns Items.
*/
getItems(): Item[] | null {
return this.items;
}
/**
* Get the count of pages that have been loaded.
*
* @returns Pages loaded.
*/
getPagesLoaded(): number {
if (this.items === null) {
return 0;
}
return Math.ceil(this.items.length / this.getPageLength());
}
/**
* Reset collection data.
*/
reset(): void {
this.items = null;
this.hasMoreItems = true;
this.listeners.forEach(listener => listener.onReset());
}
/**
* Register a listener.
*
* @param listener Listener.
* @returns Unsubscribe function.
*/
addListener(listener: CoreItemsListSourceListener<Item>): () => void {
this.listeners.push(listener);
return () => this.removeListener(listener);
}
/**
* Remove a listener.
*
* @param listener Listener.
*/
removeListener(listener: CoreItemsListSourceListener<Item>): void {
const index = this.listeners.indexOf(listener);
if (index === -1) {
return;
}
this.listeners.splice(index, 1);
}
/**
* Reload the collection, this resets the data to the first page.
*/
async reload(): Promise<void> {
const { items, hasMoreItems } = await this.loadPageItems(0);
this.setItems(items, hasMoreItems);
}
/**
* Load items for the next page, if any.
*/
async loadNextPage(): Promise<void> {
if (!this.hasMoreItems) {
return;
}
const { items, hasMoreItems } = await this.loadPageItems(this.getPagesLoaded());
this.setItems((this.items ?? []).concat(items), hasMoreItems);
}
/**
* Load page items.
*
* @param page Page number (starting at 0).
* @return Page items data.
*/
protected abstract loadPageItems(page: number): Promise<{ items: Item[]; hasMoreItems: boolean }>;
/**
* Get the length of each page in the collection.
*
* @return Page length.
*/
protected abstract getPageLength(): number;
/**
* Update the collection items.
*
* @param items Items.
* @param hasMoreItems Whether there are more pages to be loaded.
*/
protected setItems(items: Item[], hasMoreItems: boolean): void {
this.items = items;
this.hasMoreItems = hasMoreItems;
this.listeners.forEach(listener => listener.onItemsUpdated(items, hasMoreItems));
}
}

View File

@ -0,0 +1,141 @@
// (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 { CoreItemsManagerSource } from './items-manager-source';
type SourceConstructor<T extends CoreItemsManagerSource = CoreItemsManagerSource> = {
getSourceId(...args: unknown[]): string;
new (...args: unknown[]): T;
};
type InstanceTracking = { instance: CoreItemsManagerSource; references: unknown[] };
type Instances = Record<string, InstanceTracking>;
/**
* Tracks CoreItemsManagerSource instances to reuse between pages.
*/
export class CoreItemsManagerSourcesTracker {
private static instances: WeakMap<SourceConstructor, Instances> = new WeakMap();
private static instanceIds: WeakMap<CoreItemsManagerSource, string> = new WeakMap();
/**
* Create an instance of the given source or retrieve one if it's already in use.
*
* @param constructor Source constructor.
* @param constructorArguments Arguments to create a new instance, used to find out if an instance already exists.
* @returns Source.
*/
static getOrCreateSource<T extends CoreItemsManagerSource>(
constructor: SourceConstructor<T>,
constructorArguments: ConstructorParameters<SourceConstructor<T>>,
): T {
const id = constructor.getSourceId(...constructorArguments);
const constructorInstances = this.getConstructorInstances(constructor);
return constructorInstances[id]?.instance as T
?? this.createInstance(id, constructor, constructorArguments);
}
/**
* Track an object referencing a source.
*
* @param source Source.
* @param reference Object referncing this source.
*/
static addReference(source: CoreItemsManagerSource, reference: unknown): void {
const constructorInstances = this.getConstructorInstances(source.constructor as SourceConstructor);
const instanceId = this.instanceIds.get(source);
if (!instanceId) {
return;
}
if (!(instanceId in constructorInstances)) {
constructorInstances[instanceId] = {
instance: source,
references: [],
};
}
constructorInstances[instanceId].references.push(reference);
}
/**
* Remove a reference to an existing source, freeing it from memory if it's not referenced elsewhere.
*
* @param source Source.
* @param reference Object that was referncing this source.
*/
static removeReference(source: CoreItemsManagerSource, reference: unknown): void {
const constructorInstances = this.instances.get(source.constructor as SourceConstructor);
const instanceId = this.instanceIds.get(source);
const index = constructorInstances?.[instanceId ?? '']?.references.indexOf(reference) ?? -1;
if (!constructorInstances || !instanceId || index === -1) {
return;
}
constructorInstances[instanceId].references.splice(index, 1);
if (constructorInstances[instanceId].references.length === 0) {
delete constructorInstances[instanceId];
}
}
/**
* Get instances for a given constructor.
*
* @param constructor Source constructor.
* @returns Constructor instances.
*/
private static getConstructorInstances(constructor: SourceConstructor): Instances {
return this.instances.get(constructor)
?? this.initialiseConstructorInstances(constructor);
}
/**
* Initialise instances for a given constructor.
*
* @param constructor Source constructor.
* @returns Constructor instances.
*/
private static initialiseConstructorInstances(constructor: SourceConstructor): Instances {
const constructorInstances = {};
this.instances.set(constructor, constructorInstances);
return constructorInstances;
}
/**
* Create a new source instance.
*
* @param id Source id.
* @param constructor Source constructor.
* @param constructorArguments Source constructor arguments.
* @returns Source instance.
*/
private static createInstance<T extends CoreItemsManagerSource>(
id: string,
constructor: SourceConstructor<T>,
constructorArguments: ConstructorParameters<SourceConstructor<T>>,
): T {
const instance = new constructor(...constructorArguments);
this.instanceIds.set(instance, id);
return instance;
}
}

View File

@ -0,0 +1,192 @@
// (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 { ActivatedRoute, ActivatedRouteSnapshot, Params } from '@angular/router';
import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
import { CoreItemsManagerSource } from './items-manager-source';
import { CoreItemsManagerSourcesTracker } from './items-manager-sources-tracker';
/**
* Helper to manage a collection of items in a page.
*/
export abstract class CoreItemsManager<Item = unknown> {
protected source?: { instance: CoreItemsManagerSource<Item>; unsubscribe: () => void };
protected itemsMap: Record<string, Item> | null = null;
protected selectedItem: Item | null = null;
constructor(source: CoreItemsManagerSource<Item>) {
this.setSource(source);
}
/**
* Get source.
*
* @returns Source.
*/
getSource(): CoreItemsManagerSource<Item> {
if (!this.source) {
throw new Error('Source is missing from items manager');
}
return this.source.instance;
}
/**
* Set source.
*
* @param newSource New source.
*/
setSource(newSource: CoreItemsManagerSource<Item> | null): void {
if (this.source) {
CoreItemsManagerSourcesTracker.removeReference(this.source.instance, this);
this.source.unsubscribe();
delete this.source;
this.onSourceReset();
}
if (newSource) {
CoreItemsManagerSourcesTracker.addReference(newSource, this);
this.source = {
instance: newSource,
unsubscribe: newSource.addListener({
onItemsUpdated: items => this.onSourceItemsUpdated(items),
onReset: () => this.onSourceReset(),
}),
};
const items = newSource.getItems();
if (items) {
this.onSourceItemsUpdated(items);
}
}
}
/**
* Process page destroyed operations.
*/
destroy(): void {
this.setSource(null);
}
/**
* Get page route.
*
* @returns Current page route, if any.
*/
protected abstract getCurrentPageRoute(): ActivatedRoute | null;
/**
* Get the path to use when navigating to an item page.
*
* @param item Item.
* @return Path to use when navigating to the item page.
*/
protected abstract getItemPath(item: Item): string;
/**
* Get the path of the selected item given the current route.
*
* @param route Page route.
* @return Path of the selected item in the given route.
*/
protected abstract getSelectedItemPath(route?: ActivatedRouteSnapshot | null): string | null;
/**
* Get the query parameters to use when navigating to an item page.
*
* @param item Item.
* @return Query parameters to use when navigating to the item page.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected getItemQueryParams(item: Item): Params {
return {};
}
/**
* Update the selected item given the current route.
*
* @param route Current route.
*/
protected updateSelectedItem(route: ActivatedRouteSnapshot | null = null): void {
route = route ?? this.getCurrentPageRoute()?.snapshot ?? null;
const selectedItemPath = this.getSelectedItemPath(route);
this.selectedItem = selectedItemPath
? this.itemsMap?.[selectedItemPath] ?? null
: null;
}
/**
* Navigate to an item in the collection.
*
* @param item Item.
* @param options Navigation options.
*/
protected async navigateToItem(
item: Item,
options: Pick<CoreNavigationOptions, 'reset' | 'replace' | 'animationDirection'> = {},
): Promise<void> {
// Get current route in the page.
const route = this.getCurrentPageRoute();
if (route === null) {
return;
}
// If this item is already selected, do nothing.
const itemPath = this.getItemPath(item);
const selectedItemPath = this.getSelectedItemPath(route.snapshot);
if (selectedItemPath === itemPath) {
return;
}
// Navigate to item.
const params = this.getItemQueryParams(item);
const pathPrefix = selectedItemPath ? selectedItemPath.split('/').fill('../').join('') : '';
await CoreNavigator.navigate(pathPrefix + itemPath, { params, ...options });
}
/**
* Called when source items have been updated.
*
* @param items New items.
*/
protected onSourceItemsUpdated(items: Item[]): void {
this.itemsMap = items.reduce((map, item) => {
map[this.getItemPath(item)] = item;
return map;
}, {});
this.updateSelectedItem();
}
/**
* Called when source has been updated.
*/
protected onSourceReset(): void {
this.itemsMap = null;
this.selectedItem = null;
}
}

View File

@ -0,0 +1,237 @@
// (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 { ActivatedRoute, ActivatedRouteSnapshot, UrlSegment } from '@angular/router';
import { Subscription } from 'rxjs';
import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreNavigator } from '@services/navigator';
import { CoreScreen } from '@services/screen';
import { CoreUtils } from '@services/utils/utils';
import { CoreItemsManager } from './items-manager';
import { CoreItemsManagerSource } from './items-manager-source';
/**
* Helper class to manage the state and routing of a list of items in a page.
*/
export abstract class CoreListItemsManager<Item = unknown> extends CoreItemsManager<Item> {
protected pageRouteLocator?: unknown | ActivatedRoute;
protected splitView?: CoreSplitViewComponent;
protected splitViewOutletSubscription?: Subscription;
constructor(source: CoreItemsManagerSource<Item>, pageRouteLocator: unknown | ActivatedRoute) {
super(source);
this.pageRouteLocator = pageRouteLocator;
}
get items(): Item[] {
return this.getSource().getItems() || [];
}
get loaded(): boolean {
return this.itemsMap !== null;
}
get completed(): boolean {
return this.getSource().isCompleted();
}
get empty(): boolean {
const items = this.getSource().getItems();
return items === null || items.length === 0;
}
/**
* Process page started operations.
*
* @param splitView Split view component.
*/
async start(splitView: CoreSplitViewComponent): Promise<void> {
this.watchSplitViewOutlet(splitView);
// Calculate current selected item.
this.updateSelectedItem();
// Select default item if none is selected on a non-mobile layout.
if (!CoreScreen.isMobile && this.selectedItem === null && !splitView.isNested) {
const defaultItem = this.getDefaultItem();
if (defaultItem) {
this.select(defaultItem);
}
}
// Log activity.
await CoreUtils.ignoreErrors(this.logActivity());
}
/**
* Process page destroyed operations.
*/
destroy(): void {
super.destroy();
this.splitViewOutletSubscription?.unsubscribe();
}
/**
* Watch a split view outlet to keep track of the selected item.
*
* @param splitView Split view component.
*/
watchSplitViewOutlet(splitView: CoreSplitViewComponent): void {
this.splitView = splitView;
this.splitViewOutletSubscription = splitView.outletRouteObservable.subscribe(
route => this.updateSelectedItem(this.getPageRouteFromSplitViewOutlet(route)),
);
this.updateSelectedItem(this.getPageRouteFromSplitViewOutlet(splitView.outletRoute) ?? null);
}
/**
* Check whether the given item is selected or not.
*
* @param item Item.
* @return Whether the given item is selected.
*/
isSelected(item: Item): boolean {
return this.selectedItem === item;
}
/**
* Return the current aria value.
*
* @param item Item.
* @return Will return the current value of the item if selected, false otherwise.
*/
getItemAriaCurrent(item: Item): string {
return this.isSelected(item) ? 'page' : 'false';
}
/**
* Select an item.
*
* @param item Item.
*/
async select(item: Item): Promise<void> {
await this.navigateToItem(item, { reset: this.resetNavigation() });
}
/**
* Reset the list of items.
*/
reset(): void {
this.getSource().reset();
}
/**
* Reload the list of items.
*/
async reload(): Promise<void> {
await this.getSource().reload();
}
/**
* Load items for the next page, if any.
*/
async loadNextPage(): Promise<void> {
await this.getSource().loadNextPage();
}
/**
* Log activity when the page starts.
*/
protected async logActivity(): Promise<void> {
// Override to log activity.
}
/**
* Check whether to reset navigation when selecting an item.
*
* @returns boolean Whether navigation should be reset.
*/
protected resetNavigation(): boolean {
if (!CoreScreen.isTablet) {
return false;
}
return !!this.splitView && !this.splitView?.isNested;
}
/**
* Get the item that should be selected by default.
*/
protected getDefaultItem(): Item | null {
return this.items[0] || null;
}
/**
* @inheritdoc
*/
protected getCurrentPageRoute(): ActivatedRoute | null {
if (this.pageRouteLocator instanceof ActivatedRoute) {
return CoreNavigator.isRouteActive(this.pageRouteLocator) ? this.pageRouteLocator : null;
}
return CoreNavigator.getCurrentRoute({ pageComponent: this.pageRouteLocator });
}
/**
* @inheritdoc
*/
protected getSelectedItemPath(route?: ActivatedRouteSnapshot | null): string | null {
const segments: UrlSegment[] = [];
while ((route = route?.firstChild)) {
segments.push(...route.url);
}
return segments.map(segment => segment.path).join('/').replace(/\/+/, '/').trim() || null;
}
/**
* Get the page route given a child route on the splitview outlet.
*
* @param route Child route.
* @return Page route.
*/
private getPageRouteFromSplitViewOutlet(route: ActivatedRouteSnapshot | null): ActivatedRouteSnapshot | null {
const isPageRoute = this.buildRouteMatcher();
while (route && !isPageRoute(route)) {
route = route.parent;
}
return route;
}
/**
* Build a function to check whether the given snapshot belongs to the page.
*
* @returns Route matcher.
*/
private buildRouteMatcher(): (route: ActivatedRouteSnapshot) => boolean {
if (this.pageRouteLocator instanceof ActivatedRoute) {
const pageRoutePath = CoreNavigator.getRouteFullPath(this.pageRouteLocator.snapshot);
return route => CoreNavigator.getRouteFullPath(route) === pageRoutePath;
}
return route => route.component === this.pageRouteLocator;
}
}

View File

@ -0,0 +1,97 @@
// (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 { ActivatedRoute } from '@angular/router';
import { CoreNavigator } from '@services/navigator';
import { CoreItemsManager } from './items-manager';
/**
* Helper class to manage the state and routing of a swipeable page.
*/
export abstract class CoreSwipeItemsManager<Item = unknown> extends CoreItemsManager<Item> {
/**
* Process page started operations.
*/
async start(): Promise<void> {
this.updateSelectedItem();
}
/**
* Navigate to the next item.
*/
async navigateToNextItem(): Promise<void> {
await this.navigateToItemBy(-1, 'back');
}
/**
* Navigate to the previous item.
*/
async navigateToPreviousItem(): Promise<void> {
await this.navigateToItemBy(1, 'forward');
}
/**
* @inheritdoc
*/
protected getCurrentPageRoute(): ActivatedRoute | null {
return CoreNavigator.getCurrentRoute();
}
/**
* Navigate to an item by an offset.
*
* @param delta Index offset.
* @param animationDirection Animation direction.
*/
protected async navigateToItemBy(delta: number, animationDirection: 'forward' | 'back'): Promise<void> {
const item = await this.getItemBy(delta);
if (!item) {
return;
}
await this.navigateToItem(item, { animationDirection, replace: true });
}
/**
* Get item by an offset.
*
* @param delta Index offset.
*/
protected async getItemBy(delta: number): Promise<Item | null> {
const items = this.getSource().getItems();
// Get current item.
const index = (this.selectedItem && items?.indexOf(this.selectedItem)) ?? -1;
if (index === -1) {
return null;
}
// Get item by delta.
const item = items?.[index + delta] ?? null;
if (!item && !this.getSource().isCompleted()) {
await this.getSource().loadNextPage();
return this.getItemBy(delta);
}
return item;
}
}

View File

@ -22,6 +22,8 @@ import { CoreUtils } from '@services/utils/utils';
/**
* Helper class to manage the state and routing of a list of items in a page, for example on pages using a split view.
*
* @deprecated use CoreListItemsManager instead.
*/
export abstract class CorePageItemsListManager<Item> {

View File

@ -50,6 +50,7 @@ import { CoreShowPasswordComponent } from './show-password/show-password';
import { CoreSitePickerComponent } from './site-picker/site-picker';
import { CoreSplitViewComponent } from './split-view/split-view';
import { CoreStyleComponent } from './style/style';
import { CoreSwipeNavigationComponent } from './swipe-navigation/swipe-navigation';
import { CoreTabComponent } from './tabs/tab';
import { CoreTabsComponent } from './tabs/tabs';
import { CoreTabsOutletComponent } from './tabs-outlet/tabs-outlet';
@ -92,6 +93,7 @@ import { CoreButtonWithSpinnerComponent } from './button-with-spinner/button-wit
CoreSitePickerComponent,
CoreSplitViewComponent,
CoreStyleComponent,
CoreSwipeNavigationComponent,
CoreTabComponent,
CoreTabsComponent,
CoreTabsOutletComponent,
@ -140,6 +142,7 @@ import { CoreButtonWithSpinnerComponent } from './button-with-spinner/button-wit
CoreSitePickerComponent,
CoreSplitViewComponent,
CoreStyleComponent,
CoreSwipeNavigationComponent,
CoreTabComponent,
CoreTabsComponent,
CoreTabsOutletComponent,

View File

@ -0,0 +1,5 @@
<ion-slides [options]="{ allowTouchMove: !!manager }" (swipeleft)="swipeLeft()" (swiperight)="swipeRight()">
<ion-slide>
<ng-content></ng-content>
</ion-slide>
</ion-slides>

View File

@ -0,0 +1,7 @@
ion-slides {
min-height: 100%;
}
ion-slide {
align-items: start;
}

View File

@ -0,0 +1,41 @@
// (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, Input } from '@angular/core';
import { CoreSwipeItemsManager } from '@classes/items-management/swipe-items-manager';
@Component({
selector: 'core-swipe-navigation',
templateUrl: 'swipe-navigation.html',
styleUrls: ['swipe-navigation.scss'],
})
export class CoreSwipeNavigationComponent {
@Input() manager?: CoreSwipeItemsManager;
/**
* Swipe to previous item.
*/
swipeLeft(): void {
this.manager?.navigateToPreviousItem();
}
/**
* Swipe to next item.
*/
swipeRight(): void {
this.manager?.navigateToNextItem();
}
}

View File

@ -14,9 +14,11 @@
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { ApplicationInitStatus, Injector, NgModule, Type } from '@angular/core';
import { HammerModule, HAMMER_GESTURE_CONFIG } from '@angular/platform-browser';
import { CoreApplicationInitStatus } from './classes/application-init-status';
import { CoreFeaturesModule } from './features/features.module';
import { CoreHammerGestureConfig } from './classes/hammer-gesture-config';
import { CoreInterceptor } from './classes/interceptor';
import { getDatabaseProviders } from './services/database';
import { getInitializerProviders } from './initializers';
@ -84,9 +86,11 @@ export const CORE_SERVICES: Type<unknown>[] = [
@NgModule({
imports: [
CoreFeaturesModule,
HammerModule,
],
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: CoreInterceptor, multi: true },
{ provide: HAMMER_GESTURE_CONFIG, useClass: CoreHammerGestureConfig },
{ provide: ApplicationInitStatus, useClass: CoreApplicationInitStatus, deps: [Injector] },
...getDatabaseProviders(),
...getInitializerProviders(),

View File

@ -0,0 +1,81 @@
// (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 { CoreItemsManagerSource } from '@classes/items-management/items-manager-source';
import { CoreUser, CoreUserData, CoreUserParticipant, CoreUserProvider } from '../services/user';
/**
* Provides a collection of course participants.
*/
export class CoreUserParticipantsSource extends CoreItemsManagerSource<CoreUserParticipant | CoreUserData> {
/**
* @inheritdoc
*/
static getSourceId(courseId: number, searchQuery: string | null = null): string {
searchQuery = searchQuery ?? '__empty__';
return `participants-${courseId}-${searchQuery}`;
}
readonly COURSE_ID: number;
readonly SEARCH_QUERY: string | null;
constructor(courseId: number, searchQuery: string | null = null) {
super();
this.COURSE_ID = courseId;
this.SEARCH_QUERY = searchQuery;
}
/**
* @inheritdoc
*/
protected async loadPageItems(page: number): Promise<{ items: (CoreUserParticipant | CoreUserData)[]; hasMoreItems: boolean }> {
if (this.SEARCH_QUERY) {
const { participants, canLoadMore } = await CoreUser.searchParticipants(
this.COURSE_ID,
this.SEARCH_QUERY,
true,
page,
CoreUserProvider.PARTICIPANTS_LIST_LIMIT,
);
return {
items: participants,
hasMoreItems: canLoadMore,
};
}
const { participants, canLoadMore } = await CoreUser.getParticipants(
this.COURSE_ID,
page * CoreUserProvider.PARTICIPANTS_LIST_LIMIT,
CoreUserProvider.PARTICIPANTS_LIST_LIMIT,
);
return {
items: participants,
hasMoreItems: canLoadMore,
};
}
/**
* @inheritdoc
*/
protected getPageLength(): number {
return CoreUserProvider.PARTICIPANTS_LIST_LIMIT;
}
}

View File

@ -13,15 +13,18 @@
// limitations under the License.
import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Params } from '@angular/router';
import { IonRefresher } from '@ionic/angular';
import { CoreApp } from '@services/app';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreNavigator } from '@services/navigator';
import { CorePageItemsListManager } from '@classes/page-items-list-manager';
import { CoreListItemsManager } from '@classes/items-management/list-items-manager';
import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreUser, CoreUserProvider, CoreUserParticipant, CoreUserData } from '@features/user/services/user';
import { CoreUser, CoreUserParticipant, CoreUserData } from '@features/user/services/user';
import { CoreUtils } from '@services/utils/utils';
import { CoreUserParticipantsSource } from '@features/user/classes/participants-source';
import { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker';
/**
* Page that displays the list of course participants.
@ -32,6 +35,7 @@ import { CoreUtils } from '@services/utils/utils';
})
export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestroy {
courseId!: number;
participants!: CoreUserParticipantsManager;
searchQuery: string | null = null;
searchInProgress = false;
@ -42,10 +46,12 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro
@ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
constructor() {
let courseId: number;
try {
courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
this.participants = new CoreUserParticipantsManager(
CoreItemsManagerSourcesTracker.getOrCreateSource(CoreUserParticipantsSource, [this.courseId]),
this,
);
} catch (error) {
CoreDomUtils.showErrorModal(error);
@ -54,7 +60,6 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro
return;
}
this.participants = new CoreUserParticipantsManager(CoreUserParticipantsPage, courseId);
}
/**
@ -103,9 +108,11 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro
return;
}
const newSource = CoreItemsManagerSourcesTracker.getOrCreateSource(CoreUserParticipantsSource, [this.courseId]);
this.searchQuery = null;
this.searchInProgress = false;
this.participants.resetItems();
this.participants.setSource(newSource);
await this.fetchInitialParticipants();
}
@ -118,9 +125,11 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro
async search(query: string): Promise<void> {
CoreApp.closeKeyboard();
const newSource = CoreItemsManagerSourcesTracker.getOrCreateSource(CoreUserParticipantsSource, [this.courseId, query]);
this.searchInProgress = true;
this.searchQuery = query;
this.participants.resetItems();
this.participants.setSource(newSource);
await this.fetchInitialParticipants();
@ -133,8 +142,8 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro
* @param refresher Refresher.
*/
async refreshParticipants(refresher: IonRefresher): Promise<void> {
await CoreUtils.ignoreErrors(CoreUser.invalidateParticipantsList(this.participants.courseId));
await CoreUtils.ignoreErrors(this.fetchParticipants());
await CoreUtils.ignoreErrors(CoreUser.invalidateParticipantsList(this.courseId));
await CoreUtils.ignoreErrors(this.fetchParticipants(true));
refresher?.complete();
}
@ -146,7 +155,7 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro
*/
async fetchMoreParticipants(complete: () => void): Promise<void> {
try {
await this.fetchParticipants(this.participants.items);
await this.fetchParticipants(false);
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'Error loading more participants');
@ -161,38 +170,23 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro
*/
private async fetchInitialParticipants(): Promise<void> {
try {
await this.fetchParticipants();
await this.fetchParticipants(true);
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'Error loading participants');
this.participants.setItems([]);
this.participants.reset();
}
}
/**
* Update the list of participants.
*
* @param loadedParticipants Participants list to continue loading from.
* @param reload Whether to reload the list or load the next page.
*/
private async fetchParticipants(loadedParticipants: CoreUserParticipant[] | CoreUserData[] = []): Promise<void> {
if (this.searchQuery) {
const { participants, canLoadMore } = await CoreUser.searchParticipants(
this.participants.courseId,
this.searchQuery,
true,
Math.ceil(loadedParticipants.length / CoreUserProvider.PARTICIPANTS_LIST_LIMIT),
CoreUserProvider.PARTICIPANTS_LIST_LIMIT,
);
this.participants.setItems((loadedParticipants as CoreUserData[]).concat(participants), canLoadMore);
} else {
const { participants, canLoadMore } = await CoreUser.getParticipants(
this.participants.courseId,
loadedParticipants.length,
);
this.participants.setItems((loadedParticipants as CoreUserParticipant[]).concat(participants), canLoadMore);
}
private async fetchParticipants(reload: boolean): Promise<void> {
reload
? await this.participants.reload()
: await this.participants.loadNextPage();
this.fetchMoreParticipantsFailed = false;
}
@ -202,14 +196,14 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro
/**
* Helper to manage the list of participants.
*/
class CoreUserParticipantsManager extends CorePageItemsListManager<CoreUserParticipant | CoreUserData> {
class CoreUserParticipantsManager extends CoreListItemsManager<CoreUserParticipant | CoreUserData> {
courseId: number;
page: CoreUserParticipantsPage;
constructor(pageComponent: unknown, courseId: number) {
super(pageComponent);
constructor(source: CoreUserParticipantsSource, page: CoreUserParticipantsPage) {
super(source, CoreUserParticipantsPage);
this.courseId = courseId;
this.page = page;
}
/**
@ -219,11 +213,18 @@ class CoreUserParticipantsManager extends CorePageItemsListManager<CoreUserParti
return participant.id.toString();
}
/**
* @inheritdoc
*/
protected getItemQueryParams(): Params {
return { search: this.page.searchQuery };
}
/**
* @inheritdoc
*/
protected async logActivity(): Promise<void> {
await CoreUser.logParticipantsView(this.courseId);
await CoreUser.logParticipantsView(this.page.courseId);
}
}

View File

@ -7,90 +7,87 @@
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!userLoaded" (ionRefresh)="refreshUser($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="userLoaded">
<ion-list *ngIf="user && !isDeleted && isEnrolled">
<ion-item class="ion-text-center core-user-profile-maininfo">
<core-user-avatar [user]="user" [userId]="user.id" [linkProfile]="false" [checkOnline]="true">
</core-user-avatar>
<ion-label>
<h2>{{ user.fullname }}</h2>
<p *ngIf="user.address">
<ion-icon name="fas-map-marker-alt" [attr.aria-hidden]="true"></ion-icon> {{ user.address }}
</p>
<p *ngIf="rolesFormatted" class="ion-text-wrap">
<strong>{{ 'core.user.roles' | translate}}</strong>{{'core.labelsep' | translate}}
{{ rolesFormatted }}
</p>
</ion-label>
</ion-item>
<div class="core-user-communication-handlers"
*ngIf="(communicationHandlers && communicationHandlers.length) || isLoadingHandlers">
<ion-item *ngIf="communicationHandlers && communicationHandlers.length">
<core-swipe-navigation [manager]="users">
<ion-refresher slot="fixed" [disabled]="!userLoaded" (ionRefresh)="refreshUser($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="userLoaded">
<ion-list *ngIf="user && !isDeleted && isEnrolled">
<ion-item class="ion-text-center core-user-profile-maininfo">
<core-user-avatar [user]="user" [userId]="user.id" [linkProfile]="false" [checkOnline]="true">
</core-user-avatar>
<ion-label>
<ion-button *ngFor="let handler of communicationHandlers" expand="block" size="default"
<h2>{{ user.fullname }}</h2>
<p *ngIf="user.address">
<ion-icon name="fas-map-marker-alt" [attr.aria-hidden]="true"></ion-icon> {{ user.address }}
</p>
<p *ngIf="rolesFormatted" class="ion-text-wrap">
<strong>{{ 'core.user.roles' | translate}}</strong>{{'core.labelsep' | translate}}
{{ rolesFormatted }}
</p>
</ion-label>
</ion-item>
<div class="core-user-communication-handlers"
*ngIf="(communicationHandlers && communicationHandlers.length) || isLoadingHandlers">
<ion-item *ngIf="communicationHandlers && communicationHandlers.length">
<ion-label>
<ion-button *ngFor="let handler of communicationHandlers" expand="block" size="default"
[ngClass]="['core-user-profile-handler', handler.class || '']" (click)="handlerClicked($event, handler)"
[hidden]="handler.hidden" [attr.aria-label]="handler.title | translate" [disabled]="handler.spinner">
<ion-icon *ngIf="handler.icon" [name]="handler.icon" slot="start" aria-hidden="true"></ion-icon>
{{ handler.title | translate }}
</ion-button>
</ion-label>
</ion-item>
<div *ngIf="isLoadingHandlers" class="ion-text-center core-loading-handlers">
<ion-spinner [attr.aria-label]="'core.loading' | translate"></ion-spinner>
</div>
</div>
<ion-item button class="ion-text-wrap core-user-profile-handler" (click)="openUserDetails()"
[attr.aria-label]="'core.user.details' | translate" detail="true">
<ion-icon name="fas-user" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<p class="item-heading">{{ 'core.user.details' | translate }}</p>
</ion-label>
</ion-item>
<ion-item class="ion-text-center core-loading-handlers" *ngIf="isLoadingHandlers">
<ion-label>
<ion-spinner [attr.aria-label]="'core.loading' | translate"></ion-spinner>
</ion-label>
</ion-item>
<ion-item button *ngFor="let handler of newPageHandlers" class="ion-text-wrap" (click)="handlerClicked($event, handler)"
[ngClass]="['core-user-profile-handler', handler.class || '']" [hidden]="handler.hidden"
[attr.aria-label]="handler.title | translate" detail="true">
<ion-icon *ngIf="handler.icon" [name]="handler.icon" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<p class="item-heading">{{ handler.title | translate }}</p>
</ion-label>
<ion-badge slot="end" *ngIf="handler.showBadge" [hidden]="handler.loading || !handler.badge" aria-hidden="true">
{{handler.badge}}
</ion-badge>
<span *ngIf="handler.showBadge && handler.badge && handler.badgeA11yText" class="sr-only">
{{ handler.badgeA11yText | translate: {$a : handler.badge } }}
</span>
<ion-spinner slot="end" *ngIf="handler.showBadge && handler.loading" [attr.aria-label]="'core.loading' | translate">
</ion-spinner>
</ion-item>
<ion-item *ngIf="actionHandlers && actionHandlers.length">
<ion-label>
<ion-button *ngFor="let handler of actionHandlers" expand="block" fill="outline" size="default"
[ngClass]="['core-user-profile-handler', handler.class || '']" (click)="handlerClicked($event, handler)"
[hidden]="handler.hidden" [attr.aria-label]="handler.title | translate" [disabled]="handler.spinner">
<ion-icon *ngIf="handler.icon" [name]="handler.icon" slot="start" aria-hidden="true"></ion-icon>
{{ handler.title | translate }}
<ion-spinner *ngIf="handler.spinner" slot="end" [attr.aria-label]="'core.loading' | translate"></ion-spinner>
</ion-button>
</ion-label>
</ion-item>
<div *ngIf="isLoadingHandlers" class="ion-text-center core-loading-handlers">
<ion-spinner [attr.aria-label]="'core.loading' | translate"></ion-spinner>
</div>
</div>
<ion-item button class="ion-text-wrap core-user-profile-handler" (click)="openUserDetails()"
[attr.aria-label]="'core.user.details' | translate" detail="true">
<ion-icon name="fas-user" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<p class="item-heading">{{ 'core.user.details' | translate }}</p>
</ion-label>
</ion-item>
<ion-item class="ion-text-center core-loading-handlers" *ngIf="isLoadingHandlers">
<ion-label>
<ion-spinner [attr.aria-label]="'core.loading' | translate"></ion-spinner>
</ion-label>
</ion-item>
<ion-item button *ngFor="let handler of newPageHandlers" class="ion-text-wrap" (click)="handlerClicked($event, handler)"
[ngClass]="['core-user-profile-handler', handler.class || '']" [hidden]="handler.hidden"
[attr.aria-label]="handler.title | translate" detail="true">
<ion-icon *ngIf="handler.icon" [name]="handler.icon" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<p class="item-heading">{{ handler.title | translate }}</p>
</ion-label>
<ion-badge slot="end" *ngIf="handler.showBadge" [hidden]="handler.loading || !handler.badge" aria-hidden="true">
{{handler.badge}}
</ion-badge>
<span *ngIf="handler.showBadge && handler.badge && handler.badgeA11yText" class="sr-only">
{{ handler.badgeA11yText | translate: {$a : handler.badge } }}
</span>
<ion-spinner slot="end" *ngIf="handler.showBadge && handler.loading" [attr.aria-label]="'core.loading' | translate">
</ion-spinner>
</ion-item>
<ion-item *ngIf="actionHandlers && actionHandlers.length">
<ion-label>
<ion-button *ngFor="let handler of actionHandlers" expand="block" fill="outline" size="default"
[ngClass]="['core-user-profile-handler', handler.class || '']" (click)="handlerClicked($event, handler)"
[hidden]="handler.hidden" [attr.aria-label]="handler.title | translate" [disabled]="handler.spinner">
<ion-icon *ngIf="handler.icon" [name]="handler.icon" slot="start" aria-hidden="true"></ion-icon>
{{ handler.title | translate }}
<ion-spinner *ngIf="handler.spinner" slot="end" [attr.aria-label]="'core.loading' | translate"></ion-spinner>
</ion-button>
</ion-label>
</ion-item>
</ion-list>
<core-empty-box *ngIf="!user && !isDeleted && isEnrolled" icon="far-user" [message]=" 'core.user.detailsnotavailable' | translate">
</core-empty-box>
<core-empty-box *ngIf="isDeleted" icon="far-user" [message]="'core.userdeleted' | translate"></core-empty-box>
<core-empty-box *ngIf="!isEnrolled" icon="far-user" [message]="'core.notenrolledprofile' | translate"></core-empty-box>
</core-loading>
</ion-list>
<core-empty-box *ngIf="!user && !isDeleted && isEnrolled" icon="far-user" [message]=" 'core.user.detailsnotavailable' | translate">
</core-empty-box>
<core-empty-box *ngIf="isDeleted" icon="far-user" [message]="'core.userdeleted' | translate"></core-empty-box>
<core-empty-box *ngIf="!isEnrolled" icon="far-user" [message]="'core.notenrolledprofile' | translate"></core-empty-box>
</core-loading>
</core-swipe-navigation>
</ion-content>

View File

@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { ActivatedRoute, ActivatedRouteSnapshot, Params } from '@angular/router';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { IonRefresher } from '@ionic/angular';
import { Subscription } from 'rxjs';
@ -20,16 +21,16 @@ import { CoreSite } from '@classes/site';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
import {
CoreUser,
CoreUserProfile,
CoreUserProvider,
} from '@features/user/services/user';
import { CoreUser, CoreUserBasicData, CoreUserProfile, CoreUserProvider } from '@features/user/services/user';
import { CoreUserHelper } from '@features/user/services/user-helper';
import { CoreUserDelegate, CoreUserDelegateService, CoreUserProfileHandlerData } from '@features/user/services/user-delegate';
import { CoreUtils } from '@services/utils/utils';
import { CoreNavigator } from '@services/navigator';
import { CoreCourses } from '@features/courses/services/courses';
import { CoreSwipeItemsManager } from '@classes/items-management/swipe-items-manager';
import { CoreUserParticipantsSource } from '@features/user/classes/participants-source';
import { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker';
import { CoreItemsManagerSource } from '@classes/items-management/items-manager-source';
@Component({
selector: 'page-core-user-profile',
@ -55,7 +56,10 @@ export class CoreUserProfilePage implements OnInit, OnDestroy {
newPageHandlers: CoreUserProfileHandlerData[] = [];
communicationHandlers: CoreUserProfileHandlerData[] = [];
constructor() {
users?: CoreUserSwipeItemsManager;
usersQueryParams: Params = {};
constructor(private route: ActivatedRoute) {
this.obsProfileRefreshed = CoreEvents.on(CoreUserProvider.PROFILE_REFRESHED, (data) => {
if (!this.user || !data.user) {
return;
@ -86,6 +90,15 @@ export class CoreUserProfilePage implements OnInit, OnDestroy {
this.courseId = undefined;
}
if (this.courseId && this.route.snapshot.data.swipeManagerSource === 'participants') {
const search = CoreNavigator.getRouteParam('search');
const source = CoreItemsManagerSourcesTracker.getOrCreateSource(CoreUserParticipantsSource, [this.courseId, search]);
this.users = new CoreUserSwipeItemsManager(source, this);
this.usersQueryParams.search = search;
this.users.start();
}
try {
await this.fetchUser();
@ -204,8 +217,49 @@ export class CoreUserProfilePage implements OnInit, OnDestroy {
* @inheritdoc
*/
ngOnDestroy(): void {
this.users?.destroy();
this.subscription?.unsubscribe();
this.obsProfileRefreshed.off();
}
}
/**
* Helper to manage swiping within a collection of users.
*/
class CoreUserSwipeItemsManager extends CoreSwipeItemsManager<CoreUserBasicData> {
page: CoreUserProfilePage;
constructor(source: CoreItemsManagerSource<CoreUserBasicData>, page: CoreUserProfilePage) {
super(source);
this.page = page;
}
/**
* @inheritdoc
*/
protected getItemPath(item: CoreUserBasicData): string {
return String(item.id);
}
/**
* @inheritdoc
*/
protected getItemQueryParams(): Params {
return this.page.usersQueryParams;
}
/**
* @inheritdoc
*/
protected getSelectedItemPath(route?: ActivatedRouteSnapshot | null): string | null {
if (!route) {
return null;
}
return route.params.userId;
}
}

View File

@ -19,6 +19,9 @@
right: calc(50% - 12px - var(--core-avatar-size) / 2) !important;
}
}
core-loading .core-loading-content {
width: 100%;
}
}
}

View File

@ -60,6 +60,9 @@ const routes: Routes = [
{
path: `${COURSE_PAGE_NAME}/${COURSE_INDEX_PATH}/${PARTICIPANTS_PAGE_NAME}/:userId`,
loadChildren: () => import('@features/user/pages/profile/profile.module').then(m => m.CoreUserProfilePageModule),
data: {
swipeManagerSource: 'participants',
},
},
], () => CoreScreen.isMobile),
];

View File

@ -48,6 +48,7 @@ export type CoreRedirectPayload = {
export type CoreNavigationOptions = Pick<NavigationOptions, 'animated'|'animation'|'animationDirection'> & {
params?: Params;
reset?: boolean;
replace?: boolean;
preferCurrentTab?: boolean; // Default true.
nextNavigation?: {
path: string;
@ -137,6 +138,7 @@ export class CoreNavigatorService {
animationDirection: options.animationDirection,
queryParams: CoreObject.isEmpty(options.params ?? {}) ? null : CoreObject.withoutEmpty(options.params),
relativeTo: path.startsWith('/') ? null : this.getCurrentRoute(),
replaceUrl: options.replace,
});
// Remove objects from queryParams and replace them with an ID.