MOBILE-3658 comments: Comments module

main
Pau Ferrer Ocaña 2021-02-16 13:47:07 +01:00
parent 019e48a7ac
commit ed22889840
22 changed files with 2593 additions and 10 deletions

View File

@ -37,19 +37,13 @@ export class AddonBlockCommentsHandlerService extends CoreBlockBaseHandler {
* @return Data or promise resolved with the data.
*/
getDisplayData(block: CoreCourseBlock, contextLevel: string, instanceId: number): CoreBlockHandlerData {
// @todo
return {
title: 'addon.block_comments.pluginname',
class: 'addon-block-comments',
component: CoreBlockOnlyTitleComponent,
link: 'CoreCommentsViewerPage',
link: 'comments/' + contextLevel + '/' + instanceId + '/block_comments/0',
linkParams: {
contextLevel: contextLevel,
instanceId: instanceId,
componentName: 'block_comments',
area: 'page_comments',
itemId: 0,
},
};
}

View File

@ -0,0 +1,28 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
const routes: Routes = [
{
path: ':contextLevel/:instanceId/:componentName/:itemId',
loadChildren: () => import('./pages/viewer/viewer.module').then( m => m.CoreCommentsViewerPageModule),
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
})
export class CoreCommentsLazyModule {}

View File

@ -0,0 +1,54 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { Routes } from '@angular/router';
import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module';
import { CoreCronDelegate } from '@services/cron';
import { CORE_SITE_SCHEMAS } from '@services/sites';
import { CoreCommentsComponentsModule } from './components/components.module';
import { CoreComments } from './services/comments';
import { COMMENTS_OFFLINE_SITE_SCHEMA } from './services/database/comments';
import { CoreCommentsSyncCronHandler } from './services/handlers/sync-cron';
const routes: Routes = [
{
path: 'comments',
loadChildren: () => import('@features/comments/comments-lazy.module').then(m => m.CoreCommentsLazyModule),
},
];
@NgModule({
imports: [
CoreCommentsComponentsModule,
CoreMainMenuTabRoutingModule.forChild(routes),
],
providers: [
{
provide: CORE_SITE_SCHEMAS,
useValue: [COMMENTS_OFFLINE_SITE_SCHEMA],
multi: true,
},
{
provide: APP_INITIALIZER,
multi: true,
useValue: () => {
CoreCronDelegate.instance.register(CoreCommentsSyncCronHandler.instance);
CoreComments.instance.initialize();
},
},
],
})
export class CoreCommentsModule {}

View File

@ -0,0 +1,94 @@
// (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, ViewChild, ElementRef, Input } from '@angular/core';
import { CoreComments } from '@features/comments/services/comments';
import { CoreApp } from '@services/app';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { ModalController } from '@singletons';
/**
* Component that displays a text area for composing a comment.
*/
@Component({
selector: 'core-comments-add',
templateUrl: 'add.html',
})
export class CoreCommentsAddComponent {
@ViewChild('commentForm') formElement?: ElementRef;
@Input() protected contextLevel!: string;
@Input() protected instanceId!: number;
@Input() protected componentName!: string;
@Input() protected itemId!: number;
@Input() protected area = '';
@Input() content = '';
processing = false;
/**
* Send the comment or store it offline.
*
* @param e Event.
*/
async addComment(e: Event): Promise<void> {
e.preventDefault();
e.stopPropagation();
CoreApp.instance.closeKeyboard();
const loadingModal = await CoreDomUtils.instance.showModalLoading('core.sending', true);
// Freeze the add comment button.
this.processing = true;
try {
const commentsResponse = await CoreComments.instance.addComment(
this.content,
this.contextLevel,
this.instanceId,
this.componentName,
this.itemId,
this.area,
);
CoreDomUtils.instance.triggerFormSubmittedEvent(
this.formElement,
!!commentsResponse,
CoreSites.instance.getCurrentSiteId(),
);
ModalController.instance.dismiss({ comment: commentsResponse }).finally(() => {
CoreDomUtils.instance.showToast(
commentsResponse ? 'core.comments.eventcommentcreated' : 'core.datastoredoffline',
true,
3000,
);
});
} catch (error) {
CoreDomUtils.instance.showErrorModal(error);
this.processing = false;
} finally {
loadingModal.dismiss();
}
}
/**
* Close modal.
*/
closeModal(): void {
CoreDomUtils.instance.triggerFormCancelledEvent(this.formElement, CoreSites.instance.getCurrentSiteId());
ModalController.instance.dismiss();
}
}

View File

@ -0,0 +1,29 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title>{{ 'core.comments.addcomment' | translate }}</ion-title>
<ion-buttons slot="end">
<ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
<ion-icon name="fas-times" slot="icon-only"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<form name="itemEdit" (ngSubmit)="addComment($event)" #commentForm>
<ion-item>
<ion-label>
<ion-textarea placeholder="{{ 'core.comments.addcomment' | translate }}" rows="5" [(ngModel)]="content"
name="content" required="required">
</ion-textarea>
</ion-label>
</ion-item>
<div class="ion-padding">
<ion-button expand="block" type="submit" [disabled]="processing || content.length < 1">
{{ 'core.comments.savecomment' | translate }}
</ion-button>
</div>
</form>
</ion-content>

View File

@ -0,0 +1,4 @@
.core-comments-clickable {
pointer-events: auto;
cursor: pointer;
}

View File

@ -0,0 +1,220 @@
// (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, EventEmitter, Input, OnChanges, OnDestroy, Output, SimpleChange, OnInit } from '@angular/core';
import {
CoreComments,
CoreCommentsCountChangedEventData,
CoreCommentsProvider,
CoreCommentsRefreshCommentsEventData,
} from '../../services/comments';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreSites } from '@services/sites';
import { CoreNavigator } from '@services/navigator';
import { CoreUtils } from '@services/utils/utils';
import { ContextLevel } from '@/core/constants';
/**
* Component that displays the count of comments.
*/
@Component({
selector: 'core-comments',
templateUrl: 'core-comments.html',
styleUrls: ['comments.scss'],
})
export class CoreCommentsCommentsComponent implements OnInit, OnChanges, OnDestroy {
@Input() contextLevel!: ContextLevel;
@Input() instanceId!: number;
@Input() component!: string;
@Input() itemId!: number;
@Input() area = '';
@Input() title?: string;
@Input() displaySpinner = true; // Whether to display the loading spinner.
@Output() onLoading: EventEmitter<boolean>; // Eevent that indicates whether the component is loading data.
@Input() courseId?: number; // Course ID the comments belong to. It can be used to improve performance with filters.
commentsLoaded = false;
commentsCount = '';
countError = false;
disabled = false;
protected updateSiteObserver?: CoreEventObserver;
protected refreshCommentsObserver?: CoreEventObserver;
protected commentsCountObserver?: CoreEventObserver;
constructor() {
this.onLoading = new EventEmitter<boolean>();
this.disabled = CoreComments.instance.areCommentsDisabledInSite();
// Update visibility if current site info is updated.
this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
const wasDisabled = this.disabled;
this.disabled = CoreComments.instance.areCommentsDisabledInSite();
if (wasDisabled && !this.disabled) {
this.fetchData();
}
}, CoreSites.instance.getCurrentSiteId());
// Refresh comments if event received.
this.refreshCommentsObserver = CoreEvents.on<CoreCommentsRefreshCommentsEventData>(
CoreCommentsProvider.REFRESH_COMMENTS_EVENT,
(data) => {
// Verify these comments need to be updated.
if (this.undefinedOrEqual(data, 'contextLevel') && this.undefinedOrEqual(data, 'instanceId') &&
this.undefinedOrEqual(data, 'component') && this.undefinedOrEqual(data, 'itemId') &&
this.undefinedOrEqual(data, 'area')) {
CoreUtils.instance.ignoreErrors(this.doRefresh());
}
},
CoreSites.instance.getCurrentSiteId(),
);
// Refresh comments count if event received.
this.commentsCountObserver = CoreEvents.on<CoreCommentsCountChangedEventData>(
CoreCommentsProvider.COMMENTS_COUNT_CHANGED_EVENT,
(data) => {
// Verify these comments need to be updated.
if (!this.commentsCount.endsWith('+') && this.undefinedOrEqual(data, 'contextLevel') &&
this.undefinedOrEqual(data, 'instanceId') && this.undefinedOrEqual(data, 'component') &&
this.undefinedOrEqual(data, 'itemId') && this.undefinedOrEqual(data, 'area') && !this.countError) {
let newNumber = parseInt(this.commentsCount, 10) + data.countChange;
newNumber = newNumber >= 0 ? newNumber : 0;
// Parse and unparse string.
this.commentsCount = newNumber + '';
}
},
CoreSites.instance.getCurrentSiteId(),
);
}
/**
* View loaded.
*/
ngOnInit(): void {
this.fetchData();
}
/**
* Listen to changes.
*/
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
// If something change, update the fields.
if (changes && this.commentsLoaded) {
this.fetchData();
}
}
/**
* Fetch comments data.
*/
async fetchData(): Promise<void> {
if (this.disabled) {
return;
}
this.commentsLoaded = false;
this.onLoading.emit(true);
const commentsCount = await CoreComments.instance.getCommentsCount(
this.contextLevel,
this.instanceId,
this.component,
this.itemId,
this.area,
);
this.commentsCount = commentsCount;
this.countError = parseInt(this.commentsCount, 10) < 0;
this.commentsLoaded = true;
this.onLoading.emit(false);
}
/**
* Refresh comments.
*
* @return Promise resolved when done.
*/
async doRefresh(): Promise<void> {
await this.invalidateComments();
await this.fetchData();
}
/**
* Invalidate comments data.
*
* @return Promise resolved when done.
*/
async invalidateComments(): Promise<void> {
await CoreComments.instance.invalidateCommentsData(
this.contextLevel,
this.instanceId,
this.component,
this.itemId,
this.area,
);
}
/**
* Opens the comments page.
*/
openComments(e?: Event): void {
if (e) {
e.preventDefault();
e.stopPropagation();
}
if (this.disabled || this.countError) {
return;
}
CoreNavigator.instance.navigateToSitePath(
'comments/' + this.contextLevel + '/' + this.instanceId + '/' + this.component + '/' + this.itemId + '/',
{
params: {
area: this.area,
title: this.title,
courseId: this.courseId,
},
},
);
}
/**
* Component destroyed.
*/
ngOnDestroy(): void {
this.updateSiteObserver?.off();
this.refreshCommentsObserver?.off();
this.commentsCountObserver?.off();
}
/**
* Check if a certain value in data is undefined or equal to this instance value.
*
* @param data Data object.
* @param name Name of the property to check.
* @return Whether it's undefined or equal.
*/
protected undefinedOrEqual(data: Record<string, unknown>, name: string): boolean {
return typeof data[name] == 'undefined' || data[name] == this[name];
}
}

View File

@ -0,0 +1,8 @@
<core-loading *ngIf="!disabled" [hideUntil]="commentsLoaded || !displaySpinner">
<div *ngIf="!countError" (click)="openComments($event)" [class.core-comments-clickable]="!disabled">
{{ 'core.comments.commentscount' | translate : {'$a': commentsCount} }}
</div>
<div *ngIf="countError">
{{ 'core.comments.commentsnotworking' | translate }}
</div>
</core-loading>

View File

@ -0,0 +1,36 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { CoreSharedModule } from '@/core/shared.module';
import { NgModule } from '@angular/core';
import { CoreCommentsAddComponent } from './add/add-modal';
import { CoreCommentsCommentsComponent } from './comments/comments';
@NgModule({
declarations: [
CoreCommentsCommentsComponent,
CoreCommentsAddComponent,
],
imports: [
CoreSharedModule,
],
exports: [
CoreCommentsCommentsComponent,
CoreCommentsAddComponent,
],
entryComponents: [
CoreCommentsCommentsComponent,
],
})
export class CoreCommentsComponentsModule {}

View File

@ -0,0 +1,12 @@
{
"addcomment": "Add a comment...",
"comments": "Comments",
"commentscount": "Comments ({{$a}})",
"commentsnotworking": "Comments cannot be retrieved",
"deletecommentbyon": "Delete comment posted by {{$a.user}} on {{$a.time}}",
"eventcommentcreated": "Comment created",
"eventcommentdeleted": "Comment deleted",
"nocomments": "No comments",
"savecomment": "Save comment",
"warningcommentsnotsent": "Couldn't sync comments. {{error}}"
}

View File

@ -0,0 +1,108 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title>
<core-format-text [text]="title" [contextLevel]="contextLevel" [contextInstanceId]="instanceId" [courseId]="courseId">
</core-format-text>
</ion-title>
<ion-buttons slot="end">
<ion-button *ngIf="canDeleteComments" slot="end" fill="clear" (click)="toggleDelete()"
[attr.aria-label]="'core.delete' | translate">
<ion-icon name="fas-pen" slot="icon-only"></ion-icon>
</ion-button>
<core-context-menu>
<core-context-menu-item [hidden]="!(commentsLoaded && !hasOffline)" [priority]="100"
[content]="'core.refresh' | translate" (action)="refreshComments(false)" [iconAction]="refreshIcon"
[closeOnClick]="true">
</core-context-menu-item>
<core-context-menu-item [hidden]="!(commentsLoaded && hasOffline)" [priority]="100"
[content]="'core.settings.synchronizenow' | translate" (action)="refreshComments(true)" [iconAction]="syncIcon"
[closeOnClick]="false">
</core-context-menu-item>
</core-context-menu>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!commentsLoaded" (ionRefresh)="refreshComments(false, $event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="commentsLoaded">
<core-empty-box *ngIf="!comments || !comments.length" icon="fas-comments"
[message]="'core.comments.nocomments' | translate">
</core-empty-box>
<ion-card class="core-warning-card" *ngIf="hasOffline">
<ion-item>
<ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon>
<ion-label>
{{ 'core.thereisdatatosync' | translate:{$a: 'core.comments.comments' | translate | lowercase } }}
</ion-label>
</ion-item>
</ion-card>
<ion-card *ngIf="offlineComment" (click)="addComment($event)">
<ion-item class="ion-text-wrap">
<core-user-avatar [user]="offlineComment" slot="start"></core-user-avatar>
<ion-label>
<h2>{{ offlineComment.fullname }}</h2>
<p>
<ion-icon name="far-clock"></ion-icon> {{ 'core.notsent' | translate }}
</p>
</ion-label>
<ion-button *ngIf="showDelete" slot="end" fill="clear" [@coreSlideInOut]="'fromRight'" color="danger"
(click)="deleteComment($event, offlineComment)" [attr.aria-label]="'core.delete' | translate">
<ion-icon name="fas-trash" slot="icon-only"></ion-icon>
</ion-button>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label>
<core-format-text clean="true" [text]="offlineComment.content" [contextLevel]="contextLevel"
[contextInstanceId]="instanceId" [courseId]="courseId">
</core-format-text>
</ion-label>
</ion-item>
</ion-card>
<ion-card *ngFor="let comment of comments">
<ion-item class="ion-text-wrap">
<core-user-avatar [user]="comment" slot="start"></core-user-avatar>
<ion-label>
<h2>{{ comment.fullname }}</h2>
<p *ngIf="!comment.deleted">{{ comment.timecreated * 1000 | coreFormatDate: 'strftimerecentfull' }}</p>
<p *ngIf="comment.deleted">
<ion-icon name="fas-trash"></ion-icon> <span class="ion-text-wrap">
{{ 'core.deletedoffline' | translate }}
</span>
</p>
</ion-label>
<ion-button *ngIf="showDelete && !comment.deleted && comment.delete" slot="end" fill="clear"
[@coreSlideInOut]="'fromRight'" color="danger" (click)="deleteComment($event, comment)"
[attr.aria-label]="'core.delete' | translate">
<ion-icon name="fas-trash" slot="icon-only"></ion-icon>
</ion-button>
<ion-button *ngIf="showDelete && comment.deleted" slot="end" fill="clear" color="danger"
(click)="undoDeleteComment($event, comment)" [attr.aria-label]="'core.restore' | translate">
<ion-icon name="fas-undo-alt" slot="icon-only"></ion-icon>
</ion-button>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label>
<core-format-text [text]="comment.content" [contextLevel]="contextLevel" [contextInstanceId]="instanceId"
[courseId]="courseId">
</core-format-text>
</ion-label>
</ion-item>
</ion-card>
<core-infinite-loading [enabled]="canLoadMore" (action)="loadMore($event)" [error]="loadMoreError"></core-infinite-loading>
</core-loading>
<ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="canAddComments">
<ion-fab-button (click)="addComment($event)" [attr.aria-label]="'core.comments.addcomment' | translate">
<ion-icon name="fas-plus"></ion-icon>
</ion-fab-button>
</ion-fab>
</ion-content>

View File

@ -0,0 +1,38 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { CoreSharedModule } from '@/core/shared.module';
import { CoreCommentsViewerPage } from './viewer.page';
const routes: Routes = [
{
path: '',
component: CoreCommentsViewerPage,
},
];
@NgModule({
imports: [
RouterModule.forChild(routes),
CoreSharedModule,
],
declarations: [
CoreCommentsViewerPage,
],
exports: [RouterModule],
})
export class CoreCommentsViewerPageModule {}

View File

@ -0,0 +1,527 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreAnimations } from '@components/animations';
import { ActivatedRoute, Params } from '@angular/router';
import { CoreSites } from '@services/sites';
import {
CoreComments,
CoreCommentsCommentBasicData,
CoreCommentsCountChangedEventData,
CoreCommentsData,
CoreCommentsProvider,
} from '@features/comments/services/comments';
import {
CoreCommentsSync,
CoreCommentsSyncAutoSyncData,
CoreCommentsSyncProvider,
} from '@features/comments/services/comments-sync';
import { IonContent, IonRefresher } from '@ionic/angular';
import { ContextLevel, CoreConstants } from '@/core/constants';
import { CoreNavigator } from '@services/navigator';
import { ModalController, Translate } from '@singletons';
import { CoreUtils } from '@services/utils/utils';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreUser, CoreUserProfile } from '@features/user/services/user';
import { CoreTextUtils } from '@services/utils/text';
import { CoreError } from '@classes/errors/error';
import { CoreCommentsOffline } from '@features/comments/services/comments-offline';
import { CoreCommentsDBRecord } from '@features/comments/services/database/comments';
import { CoreTimeUtils } from '@services/utils/time';
import { CoreCommentsAddComponent } from '@features/comments/components/add/add-modal';
/**
* Page that displays comments.
*/
@Component({
selector: 'page-core-comments-viewer',
templateUrl: 'viewer.html',
animations: [CoreAnimations.SLIDE_IN_OUT],
})
export class CoreCommentsViewerPage implements OnInit, OnDestroy {
@ViewChild(IonContent) content?: IonContent;
comments: CoreCommentsDataWithUser[] = [];
commentsLoaded = false;
contextLevel!: ContextLevel;
instanceId!: number;
componentName!: string;
itemId = 0;
area = '';
page = 0;
title = '';
courseId?: number;
canLoadMore = false;
loadMoreError = false;
canAddComments = false;
canDeleteComments = false;
showDelete = false;
hasOffline = false;
refreshIcon = CoreConstants.ICON_LOADING;
syncIcon = CoreConstants.ICON_LOADING;
offlineComment?: CoreCommentsOfflineWithUser;
currentUserId: number;
protected addDeleteCommentsAvailable = false;
protected syncObserver?: CoreEventObserver;
protected currentUser?: CoreUserProfile;
constructor(
protected route: ActivatedRoute,
) {
this.currentUserId = CoreSites.instance.getCurrentSiteUserId();
// Refresh data if comments are synchronized automatically.
this.syncObserver = CoreEvents.on<CoreCommentsSyncAutoSyncData>(CoreCommentsSyncProvider.AUTO_SYNCED, (data) => {
if (data.contextLevel == this.contextLevel && data.instanceId == this.instanceId &&
data.componentName == this.componentName && data.itemId == this.itemId && data.area == this.area) {
// Show the sync warnings.
this.showSyncWarnings(data.warnings);
// Refresh the data.
this.commentsLoaded = false;
this.refreshIcon = CoreConstants.ICON_LOADING;
this.syncIcon = CoreConstants.ICON_LOADING;
this.content?.scrollToTop();
this.page = 0;
this.comments = [];
this.fetchComments(false);
}
}, CoreSites.instance.getCurrentSiteId());
}
/**
* View loaded.
*/
async ngOnInit(): Promise<void> {
// Is implicit the user can delete if he can add.
this.addDeleteCommentsAvailable = await CoreComments.instance.isAddCommentsAvailable();
this.currentUserId = CoreSites.instance.getCurrentSiteUserId();
this.commentsLoaded = false;
this.contextLevel = CoreNavigator.instance.getRouteParam<ContextLevel>('contextLevel')!;
this.instanceId = CoreNavigator.instance.getRouteNumberParam('instanceId')!;
this.componentName = CoreNavigator.instance.getRouteParam<string>('componentName')!;
this.itemId = CoreNavigator.instance.getRouteNumberParam('itemId')!;
this.area = CoreNavigator.instance.getRouteParam('area') || '';
this.title = CoreNavigator.instance.getRouteNumberParam('title') ||
Translate.instance.instant('core.comments.comments');
this.courseId = CoreNavigator.instance.getRouteNumberParam('courseId');
await this.fetchComments(true);
}
/**
* Fetches the comments.
*
* @param sync When to resync comments.
* @param showErrors When to display errors or not.
* @return Resolved when done.
*/
protected async fetchComments(sync: boolean, showErrors = false): Promise<void> {
this.loadMoreError = false;
if (sync) {
await CoreUtils.instance.ignoreErrors(this.syncComments(showErrors));
}
try {
// Get comments data.
const commentsResponse = await CoreComments.instance.getComments(
this.contextLevel,
this.instanceId,
this.componentName,
this.itemId,
this.area,
this.page,
);
this.canAddComments = this.addDeleteCommentsAvailable && !!commentsResponse.canpost;
let comments = commentsResponse.comments.sort((a, b) => b.timecreated - a.timecreated);
if (typeof commentsResponse.count != 'undefined') {
this.canLoadMore = (this.comments.length + comments.length) > commentsResponse.count;
} else {
// Old style.
this.canLoadMore = commentsResponse.comments.length > 0 &&
commentsResponse.comments.length >= CoreCommentsProvider.pageSize;
}
comments = await Promise.all(comments.map((comment) => this.loadCommentProfile(comment)));
this.comments = this.comments.concat(comments);
this.canDeleteComments = this.addDeleteCommentsAvailable &&
(this.hasOffline || this.comments.some((comment) => !!comment.delete));
await this.loadOfflineData();
} catch (error) {
this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading.
if (error && this.componentName == 'assignsubmission_comments') {
CoreDomUtils.instance.showAlertTranslated('core.notice', 'core.comments.commentsnotworking');
} else {
CoreDomUtils.instance.showErrorModalDefault(error, Translate.instance.instant('core.error') + ': get_comments');
}
} finally {
this.commentsLoaded = true;
this.refreshIcon = CoreConstants.ICON_REFRESH;
this.syncIcon = CoreConstants.ICON_SYNC;
}
}
/**
* Function to load more commemts.
*
* @param infiniteComplete Infinite scroll complete function. Only used from core-infinite-loading.
* @return Resolved when done.
*/
loadMore(infiniteComplete?: () => void): Promise<void> {
this.page++;
this.canLoadMore = false;
return this.fetchComments(true).finally(() => {
infiniteComplete && infiniteComplete();
});
}
/**
* Refresh the comments.
*
* @param showErrors Whether to display errors or not.
* @param refresher Refresher.
* @return Resolved when done.
*/
async refreshComments(showErrors: boolean, refresher?: CustomEvent<IonRefresher>): Promise<void> {
this.commentsLoaded = false;
this.refreshIcon = CoreConstants.ICON_LOADING;
this.syncIcon = CoreConstants.ICON_LOADING;
try {
await this.invalidateComments();
} finally {
this.page = 0;
this.comments = [];
try {
await this.fetchComments(true, showErrors);
} finally {
refresher?.detail.complete();
}
}
}
/**
* Show sync warnings if any.
*
* @param warnings the warnings
*/
private showSyncWarnings(warnings: string[]): void {
const message = CoreTextUtils.instance.buildMessage(warnings);
if (message) {
CoreDomUtils.instance.showErrorModal(message);
}
}
/**
* Tries to synchronize comments.
*
* @param showErrors Whether to display errors or not.
* @return Promise resolved if sync is successful, rejected otherwise.
*/
private async syncComments(showErrors: boolean): Promise<void> {
try {
const result = await CoreCommentsSync.instance.syncComments(
this.contextLevel,
this.instanceId,
this.componentName,
this.itemId,
this.area,
);
this.showSyncWarnings(result?.warnings || []);
} catch (error) {
if (showErrors) {
CoreDomUtils.instance.showErrorModalDefault(error, 'core.errorsync', true);
}
throw new CoreError(error);
}
}
/**
* Add a new comment to the list.
*
* @param e Event.
*/
async addComment(e: Event): Promise<void> {
e.preventDefault();
e.stopPropagation();
const params: Params = {
contextLevel: this.contextLevel,
instanceId: this.instanceId,
componentName: this.componentName,
itemId: this.itemId,
area: this.area,
content: this.offlineComment ? this.offlineComment!.content : '',
};
const modal = await ModalController.instance.create({
component: CoreCommentsAddComponent,
componentProps: params,
});
await modal.present();
const result = await modal.onDidDismiss();
if (result?.data?.comment) {
this.invalidateComments();
const addedComments = await this.loadCommentProfile(result.data.comment);
// Add the comment to the top.
this.comments = [addedComments].concat(this.comments);
this.canDeleteComments = this.addDeleteCommentsAvailable;
CoreEvents.trigger<CoreCommentsCountChangedEventData>(CoreCommentsProvider.COMMENTS_COUNT_CHANGED_EVENT, {
contextLevel: this.contextLevel,
instanceId: this.instanceId,
component: this.componentName,
itemId: this.itemId,
area: this.area,
countChange: 1,
}, CoreSites.instance.getCurrentSiteId());
} else if (result?.data?.comment === false) {
// Comments added in offline mode.
return this.loadOfflineData();
}
}
/**
* Delete a comment.
*
* @param e Click event.
* @param comment Comment to delete.
*/
async deleteComment(e: Event, comment: CoreCommentsDataWithUser | CoreCommentsOfflineWithUser): Promise<void> {
e.preventDefault();
e.stopPropagation();
const modified = 'lastmodified' in comment
? comment.lastmodified
: comment.timecreated;
const time = CoreTimeUtils.instance.userDate(
modified * 1000,
'core.strftimerecentfull',
);
const deleteComment: CoreCommentsCommentBasicData = {
contextlevel: this.contextLevel,
instanceid: this.instanceId,
component: this.componentName,
itemid: this.itemId,
area: this.area,
content: comment.content,
id: 'id' in comment ? comment.id : undefined,
};
try {
await CoreDomUtils.instance.showDeleteConfirm('core.comments.deletecommentbyon', {
$a:
{ user: comment.fullname || '', time: time },
});
} catch {
// User cancelled, nothing to do.
return;
}
try {
const deletedOnline = await CoreComments.instance.deleteComment(deleteComment);
this.showDelete = false;
if (deletedOnline) {
const index = this.comments.findIndex((comment) => comment.id == comment.id);
if (index >= 0) {
this.comments.splice(index, 1);
CoreEvents.trigger<CoreCommentsCountChangedEventData>(CoreCommentsProvider.COMMENTS_COUNT_CHANGED_EVENT, {
contextLevel: this.contextLevel,
instanceId: this.instanceId,
component: this.componentName,
itemId: this.itemId,
area: this.area,
countChange: -1,
}, CoreSites.instance.getCurrentSiteId());
}
} else {
this.loadOfflineData();
}
this.invalidateComments();
CoreDomUtils.instance.showToast('core.comments.eventcommentdeleted', true, 3000);
} catch (error) {
CoreDomUtils.instance.showErrorModalDefault(error, 'Delete comment failed.');
}
}
/**
* Invalidate comments.
*
* @return Resolved when done.
*/
protected invalidateComments(): Promise<void> {
return CoreComments.instance.invalidateCommentsData(
this.contextLevel,
this.instanceId,
this.componentName,
this.itemId,
this.area,
);
}
/**
* Loads the profile info onto the comment object.
*
* @param comment Comment object.
* @return Promise resolved with modified comment when done.
*/
protected async loadCommentProfile(comment: CoreCommentsDataWithUser): Promise<CoreCommentsDataWithUser> {
// Get the user profile image.
try {
const user = await CoreUser.instance.getProfile(comment.userid!, undefined, true);
comment.profileimageurl = user.profileimageurl;
comment.fullname = user.fullname;
} catch {
// Ignore errors.
}
return comment;
}
/**
* Load offline comments.
*
* @return Promise resolved when done.
*/
protected async loadOfflineData(): Promise<void> {
const promises: Promise<void>[] = [];
let hasDeletedComments = false;
// Load the only offline comment allowed if any.
promises.push(CoreCommentsOffline.instance.getComment(
this.contextLevel,
this.instanceId,
this.componentName,
this.itemId,
this.area,
).then(async (offlineComment) => {
this.offlineComment = offlineComment;
if (!offlineComment) {
return;
}
if (!this.currentUser) {
this.currentUser = await CoreUser.instance.getProfile(this.currentUserId, undefined, true);
}
if (this.currentUser) {
this.offlineComment!.profileimageurl = this.currentUser.profileimageurl;
this.offlineComment!.fullname = this.currentUser.fullname;
}
this.offlineComment!.userid = this.currentUserId;
return;
}));
// Load deleted comments offline.
promises.push(CoreCommentsOffline.instance.getDeletedComments(
this.contextLevel,
this.instanceId,
this.componentName,
this.itemId,
this.area,
).then((deletedComments) => {
hasDeletedComments = deletedComments && deletedComments.length > 0;
if (hasDeletedComments) {
deletedComments.forEach((deletedComment) => {
const comment = this.comments.find((comment) => comment.id == deletedComment.commentid);
if (comment) {
comment.deleted = !!deletedComment.deleted;
}
});
}
return;
}));
await Promise.all(promises);
this.hasOffline = !!this.offlineComment || hasDeletedComments;
}
/**
* Restore a comment.
*
* @param e Click event.
* @param comment Comment to delete.
*/
async undoDeleteComment(e: Event, comment: CoreCommentsDataWithUser): Promise<void> {
e.preventDefault();
e.stopPropagation();
await CoreCommentsOffline.instance.undoDeleteComment(comment.id);
comment.deleted = false;
this.showDelete = false;
}
/**
* Toggle delete.
*/
toggleDelete(): void {
this.showDelete = !this.showDelete;
}
/**
* Page destroyed.
*/
ngOnDestroy(): void {
this.syncObserver && this.syncObserver.off();
}
}
export type CoreCommentsDataWithUser = CoreCommentsData & {
profileimageurl?: string;
fullname?: string;
deleted?: boolean;
};
export type CoreCommentsOfflineWithUser = CoreCommentsDBRecord & {
profileimageurl?: string;
fullname?: string;
userid?: number;
deleted?: boolean;
};

View File

@ -0,0 +1,304 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreSites } from '@services/sites';
import { CoreTimeUtils } from '@services/utils/time';
import { makeSingleton } from '@singletons';
import { COMMENTS_TABLE, COMMENTS_DELETED_TABLE, CoreCommentsDBRecord, CoreCommentsDeletedDBRecord } from './database/comments';
/**
* Service to handle offline comments.
*/
@Injectable( { providedIn: 'root' })
export class CoreCommentsOfflineProvider {
/**
* Get all offline comments.
*
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with comments.
*/
async getAllComments(siteId?: string): Promise<(CoreCommentsDBRecord | CoreCommentsDeletedDBRecord)[]> {
const site = await CoreSites.instance.getSite(siteId);
const results = await Promise.all([
site.getDb().getRecords(COMMENTS_TABLE),
site.getDb().getRecords(COMMENTS_DELETED_TABLE),
]);
return [].concat.apply([], results);
}
/**
* Get an offline comment.
*
* @param contextLevel Contextlevel system, course, user...
* @param instanceId The Instance id of item associated with the context level.
* @param component Component name.
* @param itemId Associated id.
* @param area String comment area. Default empty.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the comments.
*/
async getComment(
contextLevel: string,
instanceId: number,
component: string,
itemId: number,
area: string = '',
siteId?: string,
): Promise<CoreCommentsDBRecord | undefined> {
try {
const site = await CoreSites.instance.getSite(siteId);
return await site.getDb().getRecord(COMMENTS_TABLE, {
contextlevel: contextLevel,
instanceid: instanceId,
component: component,
itemid: itemId,
area: area,
});
} catch {
return;
}
}
/**
* Get all offline comments added or deleted of a special area.
*
* @param contextLevel Contextlevel system, course, user...
* @param instanceId The Instance id of item associated with the context level.
* @param component Component name.
* @param itemId Associated id.
* @param area String comment area. Default empty.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the comments.
*/
async getComments(
contextLevel: string,
instanceId: number,
component: string,
itemId: number,
area: string = '',
siteId?: string,
): Promise<(CoreCommentsDBRecord | CoreCommentsDeletedDBRecord)[]> {
let comments: (CoreCommentsDBRecord | CoreCommentsDeletedDBRecord)[] = [];
siteId = siteId || CoreSites.instance.getCurrentSiteId();
const comment = await this.getComment(contextLevel, instanceId, component, itemId, area, siteId);
comments = comment ? [comment] : [];
const deletedComments = await this.getDeletedComments(contextLevel, instanceId, component, itemId, area, siteId);
comments = comments.concat(deletedComments);
return comments;
}
/**
* Get all offline deleted comments.
*
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with comments.
*/
async getAllDeletedComments(siteId?: string): Promise<CoreCommentsDeletedDBRecord[]> {
const site = await CoreSites.instance.getSite(siteId);
return await site.getDb().getRecords(COMMENTS_DELETED_TABLE);
}
/**
* Get an offline comment.
*
* @param contextLevel Contextlevel system, course, user...
* @param instanceId The Instance id of item associated with the context level.
* @param component Component name.
* @param itemId Associated id.
* @param area String comment area. Default empty.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the comments.
*/
async getDeletedComments(
contextLevel: string,
instanceId: number,
component: string,
itemId: number,
area: string = '',
siteId?: string,
): Promise<CoreCommentsDeletedDBRecord[]> {
try {
const site = await CoreSites.instance.getSite(siteId);
return await site.getDb().getRecords(COMMENTS_DELETED_TABLE, {
contextlevel: contextLevel,
instanceid: instanceId,
component: component,
itemid: itemId,
area: area,
});
} catch {
return [];
}
}
/**
* Remove an offline comment.
*
* @param contextLevel Contextlevel system, course, user...
* @param instanceId The Instance id of item associated with the context level.
* @param component Component name.
* @param itemId Associated id.
* @param area String comment area. Default empty.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved if deleted, rejected if failure.
*/
async removeComment(
contextLevel: string,
instanceId: number,
component: string,
itemId: number,
area: string = '',
siteId?: string,
): Promise<void> {
const site = await CoreSites.instance.getSite(siteId);
await site.getDb().deleteRecords(COMMENTS_TABLE, {
contextlevel: contextLevel,
instanceid: instanceId,
component: component,
itemid: itemId,
area: area,
});
}
/**
* Remove an offline deleted comment.
*
* @param contextLevel Contextlevel system, course, user...
* @param instanceId The Instance id of item associated with the context level.
* @param component Component name.
* @param itemId Associated id.
* @param area String comment area. Default empty.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved if deleted, rejected if failure.
*/
async removeDeletedComments(
contextLevel: string,
instanceId: number,
component: string,
itemId: number,
area: string = '',
siteId?: string,
): Promise<void> {
const site = await CoreSites.instance.getSite(siteId);
await site.getDb().deleteRecords(COMMENTS_DELETED_TABLE, {
contextlevel: contextLevel,
instanceid: instanceId,
component: component,
itemid: itemId,
area: area,
});
}
/**
* Save a comment to be sent later.
*
* @param content Comment text.
* @param contextLevel Contextlevel system, course, user...
* @param instanceId The Instance id of item associated with the context level.
* @param component Component name.
* @param itemId Associated id.
* @param area String comment area. Default empty.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved if stored, rejected if failure.
*/
async saveComment(
content: string,
contextLevel: string,
instanceId: number,
component: string,
itemId: number,
area: string = '',
siteId?: string,
): Promise<CoreCommentsDBRecord> {
const site = await CoreSites.instance.getSite(siteId);
const now = CoreTimeUtils.instance.timestamp();
const data: CoreCommentsDBRecord = {
contextlevel: contextLevel,
instanceid: instanceId,
component: component,
itemid: itemId,
area: area,
content: content,
lastmodified: now,
};
await site.getDb().insertRecord(COMMENTS_TABLE, data);
return data;
}
/**
* Delete a comment offline to be sent later.
*
* @param commentId Comment ID.
* @param contextLevel Contextlevel system, course, user...
* @param instanceId The Instance id of item associated with the context level.
* @param component Component name.
* @param itemId Associated id.
* @param area String comment area. Default empty.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved if stored, rejected if failure.
*/
async deleteComment(
commentId: number,
contextLevel: string,
instanceId: number,
component: string,
itemId: number,
area: string = '',
siteId?: string,
): Promise<void> {
const site = await CoreSites.instance.getSite(siteId);
const now = CoreTimeUtils.instance.timestamp();
const data: CoreCommentsDeletedDBRecord = {
contextlevel: contextLevel,
instanceid: instanceId,
component: component,
itemid: itemId,
area: area,
commentid: commentId,
deleted: now,
};
await site.getDb().insertRecord(COMMENTS_DELETED_TABLE, data);
}
/**
* Undo delete a comment.
*
* @param commentId Comment ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved if deleted, rejected if failure.
*/
async undoDeleteComment(commentId: number, siteId?: string): Promise<void> {
const site = await CoreSites.instance.getSite(siteId);
await site.getDb().deleteRecords(COMMENTS_DELETED_TABLE, { commentid: commentId });
}
}
export const CoreCommentsOffline = makeSingleton(CoreCommentsOfflineProvider);

View File

@ -0,0 +1,336 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreSyncBaseProvider } from '@classes/base-sync';
import { CoreComments, CoreCommentsCountChangedEventData, CoreCommentsProvider } from './comments';
import { CoreEvents } from '@singletons/events';
import { makeSingleton, Translate } from '@singletons';
import { CoreCommentsOffline } from './comments-offline';
import { CoreSites } from '@services/sites';
import { CoreApp } from '@services/app';
import { CoreUtils } from '@services/utils/utils';
import { CoreNetworkError } from '@classes/errors/network-error';
import { CoreCommentsDBRecord, CoreCommentsDeletedDBRecord } from './database/comments';
/**
* Service to sync omments.
*/
@Injectable( { providedIn: 'root' })
export class CoreCommentsSyncProvider extends CoreSyncBaseProvider<CoreCommentsSyncResult> {
static readonly AUTO_SYNCED = 'core_comments_autom_synced';
constructor() {
super('CoreCommentsSync');
}
/**
* Try to synchronize all the comments in a certain site or in all sites.
*
* @param siteId Site ID to sync. If not defined, sync all sites.
* @param force Wether to force sync not depending on last execution.
* @return Promise resolved if sync is successful, rejected if sync fails.
*/
syncAllComments(siteId?: string, force?: boolean): Promise<void> {
return this.syncOnSites('all comments', this.syncAllCommentsFunc.bind(this, siteId, force), siteId);
}
/**
* Synchronize all the comments in a certain site
*
* @param siteId Site ID to sync.
* @param force Wether to force sync not depending on last execution.
* @return Promise resolved if sync is successful, rejected if sync fails.
*/
private async syncAllCommentsFunc(siteId: string, force: boolean): Promise<void> {
const comments = await CoreCommentsOffline.instance.getAllComments(siteId);
const commentsUnique: { [syncId: string]: (CoreCommentsDBRecord | CoreCommentsDeletedDBRecord) } = {};
// Get Unique array.
comments.forEach((comment) => {
const syncId = this.getSyncId(
comment.contextlevel,
comment.instanceid,
comment.component,
comment.itemid,
comment.area,
);
commentsUnique[syncId] = comment;
});
// Sync all courses.
const promises = Object.keys(commentsUnique).map(async (key) => {
const comment = commentsUnique[key];
const result = await (force
? this.syncComments(
comment.contextlevel,
comment.instanceid,
comment.component,
comment.itemid,
comment.area,
siteId,
)
: this.syncCommentsIfNeeded(
comment.contextlevel,
comment.instanceid,
comment.component,
comment.itemid,
comment.area,
siteId,
));
if (typeof result != 'undefined') {
// Sync successful, send event.
CoreEvents.trigger<CoreCommentsSyncAutoSyncData>(CoreCommentsSyncProvider.AUTO_SYNCED, {
contextLevel: comment.contextlevel,
instanceId: comment.instanceid,
componentName: comment.component,
itemId: comment.itemid,
area: comment.area,
warnings: result.warnings,
}, siteId);
}
});
await Promise.all(promises);
}
/**
* Sync course comments only if a certain time has passed since the last time.
*
* @param contextLevel Contextlevel system, course, user...
* @param instanceId The Instance id of item associated with the context level.
* @param component Component name.
* @param itemId Associated id.
* @param area String comment area. Default empty.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the comments are synced or if they don't need to be synced.
*/
private async syncCommentsIfNeeded(
contextLevel: string,
instanceId: number,
component: string,
itemId: number,
area: string = '',
siteId?: string,
): Promise<CoreCommentsSyncResult | undefined> {
const syncId = this.getSyncId(contextLevel, instanceId, component, itemId, area);
const needed = await this.isSyncNeeded(syncId, siteId);
if (needed) {
return this.syncComments(contextLevel, instanceId, component, itemId, area, siteId);
}
}
/**
* Synchronize comments in a particular area.
*
* @param contextLevel Contextlevel system, course, user...
* @param instanceId The Instance id of item associated with the context level.
* @param component Component name.
* @param itemId Associated id.
* @param area String comment area. Default empty.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved if sync is successful, rejected otherwise.
*/
syncComments(
contextLevel: string,
instanceId: number,
component: string,
itemId: number,
area: string = '',
siteId?: string,
): Promise<CoreCommentsSyncResult> {
siteId = siteId || CoreSites.instance.getCurrentSiteId();
const syncId = this.getSyncId(contextLevel, instanceId, component, itemId, area);
if (this.isSyncing(syncId, siteId)) {
// There's already a sync ongoing for comments, return the promise.
return this.getOngoingSync(syncId, siteId)!;
}
this.logger.debug('Try to sync comments ' + syncId + ' in site ' + siteId);
const syncPromise = this.performSyncComments(contextLevel, instanceId, component, itemId, area, siteId);
return this.addOngoingSync(syncId, syncPromise, siteId);
}
/**
* Performs the syncronization of comments in a particular area.
*
* @param contextLevel Contextlevel system, course, user...
* @param instanceId The Instance id of item associated with the context level.
* @param component Component name.
* @param itemId Associated id.
* @param area String comment area. Default empty.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved if sync is successful, rejected otherwise.
*/
private async performSyncComments(
contextLevel: string,
instanceId: number,
component: string,
itemId: number,
area: string = '',
siteId: string,
): Promise<CoreCommentsSyncResult> {
const result: CoreCommentsSyncResult = {
warnings: [],
updated: false,
};
// Get offline comments to be sent.
const comments = await CoreCommentsOffline.instance.getComments(contextLevel, instanceId, component, itemId, area, siteId);
if (!comments.length) {
// Nothing to sync.
return result;
}
if (!CoreApp.instance.isOnline()) {
// Cannot sync in offline.
throw new CoreNetworkError();
}
const errors: string[] = [];
const promises: Promise<void>[] = [];
const deleteCommentIds: number[] = [];
let countChange = 0;
comments.forEach((comment) => {
if ('deleted' in comment) {
deleteCommentIds.push(comment.commentid);
} else {
promises.push(CoreComments.instance.addCommentOnline(
comment.content,
contextLevel,
instanceId,
component,
itemId,
area,
siteId,
).then(() => {
countChange++;
return CoreCommentsOffline.instance.removeComment(contextLevel, instanceId, component, itemId, area, siteId);
}));
}
});
if (deleteCommentIds.length > 0) {
promises.push(CoreComments.instance.deleteCommentsOnline(
deleteCommentIds,
contextLevel,
instanceId,
component,
itemId,
area,
siteId,
).then(() => {
countChange--;
return CoreCommentsOffline.instance.removeDeletedComments(
contextLevel,
instanceId,
component,
itemId,
area,
siteId,
);
}));
}
// Send the comments.
try {
await Promise.all(promises);
result.updated = true;
CoreEvents.trigger<CoreCommentsCountChangedEventData>(CoreCommentsProvider.COMMENTS_COUNT_CHANGED_EVENT, {
contextLevel: contextLevel,
instanceId: instanceId,
component,
itemId: itemId,
area: area,
countChange: countChange,
}, CoreSites.instance.getCurrentSiteId());
// Fetch the comments from server to be sure they're up to date.
await CoreUtils.instance.ignoreErrors(
CoreComments.instance.invalidateCommentsData(contextLevel, instanceId, component, itemId, area, siteId),
);
await CoreUtils.instance.ignoreErrors(
CoreComments.instance.getComments(contextLevel, instanceId, component, itemId, area, 0, siteId),
);
} catch (error) {
if (CoreUtils.instance.isWebServiceError(error)) {
// It's a WebService error, this means the user cannot send comments.
errors.push(error.message);
} else {
// Not a WebService error, reject the synchronization to try again.
throw error;
}
}
if (errors && errors.length) {
errors.forEach((error) => {
result.warnings.push(Translate.instance.instant('core.comments.warningcommentsnotsent', {
error: error,
}));
});
}
// All done, return the warnings.
return result;
}
/**
* Get the ID of a comments sync.
*
* @param contextLevel Contextlevel system, course, user...
* @param instanceId The Instance id of item associated with the context level.
* @param component Component name.
* @param itemId Associated id.
* @param area String comment area. Default empty.
* @return Sync ID.
*/
protected getSyncId(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = ''): string {
return contextLevel + '#' + instanceId + '#' + component + '#' + itemId + '#' + area;
}
}
export const CoreCommentsSync = makeSingleton(CoreCommentsSyncProvider);
export type CoreCommentsSyncResult = {
warnings: string[]; // List of warnings.
updated: boolean; // Whether some data was sent to the server or offline data was updated.
};
/**
* Data passed to AUTO_SYNCED event.
*/
export type CoreCommentsSyncAutoSyncData = {
contextLevel: string;
instanceId: number;
componentName: string;
itemId: number;
area: string;
warnings: string[];
};

View File

@ -0,0 +1,625 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreError } from '@classes/errors/error';
import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
import { CoreApp } from '@services/app';
import { CoreSites } from '@services/sites';
import { CoreUtils } from '@services/utils/utils';
import { CoreWSExternalWarning } from '@services/ws';
import { makeSingleton } from '@singletons';
import { CoreEvents } from '@singletons/events';
import { CoreCommentsOffline } from './comments-offline';
const ROOT_CACHE_KEY = 'mmComments:';
/**
* Service that provides some features regarding comments.
*/
@Injectable( { providedIn: 'root' })
export class CoreCommentsProvider {
static readonly REFRESH_COMMENTS_EVENT = 'core_comments_refresh_comments';
static readonly COMMENTS_COUNT_CHANGED_EVENT = 'core_comments_count_changed';
static pageSize = 1; // At least it will be one.
static pageSizeOK = false; // If true, the pageSize is definitive. If not, it's a temporal value to reduce WS calls.
/**
* Initialize the module service.
*/
initialize(): void {
// Reset comments page size.
CoreEvents.on(CoreEvents.LOGIN, () => {
CoreCommentsProvider.pageSize = 1;
CoreCommentsProvider.pageSizeOK = false;
});
}
/**
* Add a comment.
*
* @param content Comment text.
* @param contextLevel Contextlevel system, course, user...
* @param instanceId The Instance id of item associated with the context level.
* @param component Component name.
* @param itemId Associated id.
* @param area String comment area. Default empty.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with boolean: true if comment was sent to server, false if stored in device.
*/
async addComment(
content: string,
contextLevel: string,
instanceId: number,
component: string,
itemId: number,
area: string = '',
siteId?: string,
): Promise<CoreCommentsData | boolean> {
siteId = siteId || CoreSites.instance.getCurrentSiteId();
// Convenience function to store a comment to be synchronized later.
const storeOffline = async (): Promise<boolean> => {
await CoreCommentsOffline.instance.saveComment(content, contextLevel, instanceId, component, itemId, area, siteId);
return false;
};
if (!CoreApp.instance.isOnline()) {
// App is offline, store the comment.
return storeOffline();
}
// Send comment to server.
try {
return await this.addCommentOnline(content, contextLevel, instanceId, component, itemId, area, siteId);
} catch (error) {
if (CoreUtils.instance.isWebServiceError(error)) {
// It's a WebService error, the user cannot send the message so don't store it.
throw error;
}
return storeOffline();
}
}
/**
* Add a comment. It will fail if offline or cannot connect.
*
* @param content Comment text.
* @param contextLevel Contextlevel system, course, user...
* @param instanceId The Instance id of item associated with the context level.
* @param component Component name.
* @param itemId Associated id.
* @param area String comment area. Default empty.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when added, rejected otherwise.
*/
async addCommentOnline(
content: string,
contextLevel: string,
instanceId: number,
component: string,
itemId: number,
area: string = '',
siteId?: string,
): Promise<CoreCommentsData> {
const comments: CoreCommentsCommentBasicData[] = [
{
contextlevel: contextLevel,
instanceid: instanceId,
component: component,
itemid: itemId,
area: area,
content: content,
},
];
const commentsResponse = await this.addCommentsOnline(comments, siteId);
// A comment was added, invalidate them.
await CoreUtils.instance.ignoreErrors(
this.invalidateCommentsData(contextLevel, instanceId, component, itemId, area, siteId),
);
return commentsResponse![0];
}
/**
* Add several comments. It will fail if offline or cannot connect.
*
* @param comments Comments to save.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when added, rejected otherwise. Promise resolved doesn't mean that comments
* have been added, the resolve param can contain errors for comments not sent.
*/
async addCommentsOnline(
comments: CoreCommentsCommentBasicData[],
siteId?: string,
): Promise<CoreCommentsAddCommentsWSResponse | undefined> {
if (!comments || !comments.length) {
return;
}
const site = await CoreSites.instance.getSite(siteId);
const data: CoreCommentsAddCommentsWSParams = {
comments: comments,
};
return await site.write('core_comment_add_comments', data);
}
/**
* Check if Calendar is disabled in a certain site.
*
* @param site Site. If not defined, use current site.
* @return Whether it's disabled.
*/
areCommentsDisabledInSite(site?: CoreSite): boolean {
site = site || CoreSites.instance.getCurrentSite();
return !!site?.isFeatureDisabled('NoDelegate_CoreComments');
}
/**
* Check if comments are disabled in a certain site.
*
* @param siteId Site Id. If not defined, use current site.
* @return Promise resolved with true if disabled, rejected or resolved with false otherwise.
*/
async areCommentsDisabled(siteId?: string): Promise<boolean> {
const site = await CoreSites.instance.getSite(siteId);
return this.areCommentsDisabledInSite(site);
}
/**
* Delete a comment.
*
* @param comment Comment object to delete.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when deleted (with true if deleted in online, false otherwise), rejected otherwise. Promise resolved
* doesn't mean that comments have been deleted, the resolve param can contain errors for comments not deleted.
*/
async deleteComment(comment: CoreCommentsCommentBasicData, siteId?: string): Promise<boolean> {
siteId = siteId || CoreSites.instance.getCurrentSiteId();
// Offline comment, just delete it.
if (!comment.id) {
await CoreCommentsOffline.instance.removeComment(
comment.contextlevel,
comment.instanceid,
comment.component,
comment.itemid,
comment.area,
siteId,
);
return false;
}
// Convenience function to store the action to be synchronized later.
const storeOffline = async (): Promise<boolean> => {
await CoreCommentsOffline.instance.deleteComment(
comment.id!,
comment.contextlevel,
comment.instanceid,
comment.component,
comment.itemid,
comment.area,
siteId,
);
return false;
};
if (!CoreApp.instance.isOnline()) {
// App is offline, store the comment.
return storeOffline();
}
// Send comment to server.
try {
await this.deleteCommentsOnline(
[comment.id],
comment.contextlevel,
comment.instanceid,
comment.component,
comment.itemid,
comment.area,
siteId,
);
return true;
} catch (error) {
if (CoreUtils.instance.isWebServiceError(error)) {
// It's a WebService error, the user cannot send the comment so don't store it.
throw error;
}
return storeOffline();
}
}
/**
* Delete a comment. It will fail if offline or cannot connect.
*
* @param commentIds Comment IDs to delete.
* @param contextLevel Contextlevel system, course, user...
* @param instanceId The Instance id of item associated with the context level.
* @param component Component name.
* @param itemId Associated id.
* @param area String comment area. Default empty.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when deleted, rejected otherwise. Promise resolved doesn't mean that comments
* have been deleted, the resolve param can contain errors for comments not deleted.
*/
async deleteCommentsOnline(
commentIds: number[],
contextLevel: string,
instanceId: number,
component: string,
itemId: number,
area: string = '',
siteId?: string,
): Promise<void> {
const site = await CoreSites.instance.getSite(siteId);
const data: CoreCommentsDeleteCommentsWSParams = {
comments: commentIds,
};
await site.write('core_comment_delete_comments', data);
await CoreUtils.instance.ignoreErrors(
this.invalidateCommentsData(contextLevel, instanceId, component, itemId, area, siteId),
);
}
/**
* Returns whether WS to add/delete comments are available in site.
*
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with true if available, resolved with false or rejected otherwise.
* @since 3.8
*/
async isAddCommentsAvailable(siteId?: string): Promise<boolean> {
const site = await CoreSites.instance.getSite(siteId);
// First check if it's disabled.
if (this.areCommentsDisabledInSite(site)) {
return false;
}
return site.wsAvailable('core_comment_add_comments');
}
/**
* Get cache key for get comments data WS calls.
*
* @param contextLevel Contextlevel system, course, user...
* @param instanceId The Instance id of item associated with the context level.
* @param component Component name.
* @param itemId Associated id.
* @param area String comment area. Default empty.
* @return Cache key.
*/
protected getCommentsCacheKey(
contextLevel: string,
instanceId: number,
component: string,
itemId: number,
area: string = '',
): string {
return this.getCommentsPrefixCacheKey(contextLevel, instanceId) + ':' + component + ':' + itemId + ':' + area;
}
/**
* Get cache key for get comments instance data WS calls.
*
* @param contextLevel Contextlevel system, course, user...
* @param instanceId The Instance id of item associated with the context level.
* @return Cache key.
*/
protected getCommentsPrefixCacheKey(contextLevel: string, instanceId: number): string {
return ROOT_CACHE_KEY + 'comments:' + contextLevel + ':' + instanceId;
}
/**
* Retrieve a list of comments.
*
* @param contextLevel Contextlevel system, course, user...
* @param instanceId The Instance id of item associated with the context level.
* @param component Component name.
* @param itemId Associated id.
* @param area String comment area. Default empty.
* @param page Page number (0 based). Default 0.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the comments.
*/
async getComments(
contextLevel: string,
instanceId: number,
component: string,
itemId: number,
area: string = '',
page: number = 0,
siteId?: string,
): Promise<CoreCommentsGetCommentsWSResponse> {
const site = await CoreSites.instance.getSite(siteId);
const params: CoreCommentsGetCommentsWSParams = {
contextlevel: contextLevel,
instanceid: instanceId,
component: component,
itemid: itemId,
area: area,
page: page,
};
const preSets: CoreSiteWSPreSets = {
cacheKey: this.getCommentsCacheKey(contextLevel, instanceId, component, itemId, area),
updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
};
const response = await site.read<CoreCommentsGetCommentsWSResponse>('core_comment_get_comments', params, preSets);
if (response.comments) {
// Update pageSize with the greatest count at the moment.
if (typeof response.count == 'undefined' && response.comments.length > CoreCommentsProvider.pageSize) {
CoreCommentsProvider.pageSize = response.comments.length;
}
return response;
}
throw new CoreError('No comments returned');
}
/**
* Get comments count number to show on the comments component.
*
* @param contextLevel Contextlevel system, course, user...
* @param instanceId The Instance id of item associated with the context level.
* @param component Component name.
* @param itemId Associated id.
* @param area String comment area. Default empty.
* @param siteId Site ID. If not defined, current site.
* @return Comments count with plus sign if needed.
*/
async getCommentsCount(
contextLevel: string,
instanceId: number,
component: string,
itemId: number,
area: string = '',
siteId?: string,
): Promise<string> {
siteId = siteId ? siteId : CoreSites.instance.getCurrentSiteId();
let trueCount = false;
// Convenience function to get comments number on a page.
const getCommentsPageCount = async (page: number): Promise<number> => {
try {
const response = await this.getComments(contextLevel, instanceId, component, itemId, area, page, siteId);
// Count is only available in 3.8 onwards.
if (typeof response.count != 'undefined') {
trueCount = true;
return response.count;
}
if (response.comments) {
return response.comments.length || 0;
}
return -1;
} catch {
return -1;
}
};
const count = await getCommentsPageCount(0);
if (trueCount || count < CoreCommentsProvider.pageSize) {
return count + '';
} else if (CoreCommentsProvider.pageSizeOK && count >= CoreCommentsProvider.pageSize) {
// Page Size is ok, show + in case it reached the limit.
return (CoreCommentsProvider.pageSize - 1) + '+';
}
const countMore = await getCommentsPageCount(1);
// Page limit was reached on the previous call.
if (countMore > 0) {
CoreCommentsProvider.pageSizeOK = true;
return (CoreCommentsProvider.pageSize - 1) + '+';
}
return count + '';
}
/**
* Invalidates comments data.
*
* @param contextLevel Contextlevel system, course, user...
* @param instanceId The Instance id of item associated with the context level.
* @param component Component name.
* @param itemId Associated id.
* @param area String comment area. Default empty.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the data is invalidated.
*/
async invalidateCommentsData(
contextLevel: string,
instanceId: number,
component: string,
itemId: number,
area: string = '',
siteId?: string,
): Promise<void> {
const site = await CoreSites.instance.getSite(siteId);
await CoreUtils.instance.allPromises([
// This is done with starting with to avoid conflicts with previous keys that were including page.
site.invalidateWsCacheForKeyStartingWith(this.getCommentsCacheKey(
contextLevel,
instanceId,
component,
itemId,
area,
) + ':'),
site.invalidateWsCacheForKey(this.getCommentsCacheKey(contextLevel, instanceId, component, itemId, area)),
]);
}
/**
* Invalidates all comments data for an instance.
*
* @param contextLevel Contextlevel system, course, user...
* @param instanceId The Instance id of item associated with the context level.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the data is invalidated.
*/
async invalidateCommentsByInstance(contextLevel: string, instanceId: number, siteId?: string): Promise<void> {
const site = await CoreSites.instance.getSite(siteId);
await site.invalidateWsCacheForKeyStartingWith(this.getCommentsPrefixCacheKey(contextLevel, instanceId));
}
}
export const CoreComments = makeSingleton(CoreCommentsProvider);
/**
* Data returned by comment_area_exporter.
*/
export type CoreCommentsArea = {
component: string; // Component.
commentarea: string; // Commentarea.
itemid: number; // Itemid.
courseid: number; // Courseid.
contextid: number; // Contextid.
cid: string; // Cid.
autostart: boolean; // Autostart.
canpost: boolean; // Canpost.
canview: boolean; // Canview.
count: number; // Count.
collapsediconkey: string; // @since 3.3. Collapsediconkey.
displaytotalcount: boolean; // Displaytotalcount.
displaycancel: boolean; // Displaycancel.
fullwidth: boolean; // Fullwidth.
linktext: string; // Linktext.
notoggle: boolean; // Notoggle.
template: string; // Template.
canpostorhascomments: boolean; // Canpostorhascomments.
};
/**
* Params of core_comment_add_comments WS.
*/
type CoreCommentsAddCommentsWSParams = {
comments: CoreCommentsCommentBasicData[];
};
export type CoreCommentsCommentBasicData = {
id?: number; // Comment ID.
contextlevel: string; // Contextlevel system, course, user...
instanceid: number; // The id of item associated with the contextlevel.
component: string; // Component.
content: string; // Component.
itemid: number; // Associated id.
area?: string; // String comment area.
};
/**
* Comments Data returned by WS.
*/
export type CoreCommentsData = {
id: number; // Comment ID.
content: string; // The content text formatted.
format: number; // Content format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
timecreated: number; // Time created (timestamp).
strftimeformat: string; // Time format.
profileurl: string; // URL profile.
fullname: string; // Fullname.
time: string; // Time in human format.
avatar: string; // HTML user picture.
userid: number; // User ID.
delete?: boolean; // Permission to delete=true/false.
};
/**
* Data returned by core_comment_add_comments WS.
*/
export type CoreCommentsAddCommentsWSResponse = CoreCommentsData[];
/**
* Params of core_comment_delete_comments WS.
*/
type CoreCommentsDeleteCommentsWSParams = {
comments: number[];
};
/**
* Params of core_comment_get_comments WS.
*/
type CoreCommentsGetCommentsWSParams = {
contextlevel: string; // Contextlevel system, course, user...
instanceid: number; // The Instance id of item associated with the context level.
component: string; // Component.
itemid: number; // Associated id.
area?: string; // String comment area.
page?: number; // Page number (0 based).
sortdirection?: string; // Sort direction: ASC or DESC.
};
/**
* Data returned by core_comment_get_comments WS.
*/
export type CoreCommentsGetCommentsWSResponse = {
comments: CoreCommentsData[]; // List of comments.
count?: number; // @since 3.8. Total number of comments.
perpage?: number; // @since 3.8. Number of comments per page.
canpost?: boolean; // Whether the user can post in this comment area.
warnings?: CoreWSExternalWarning[];
};
/**
* Data sent by COMMENTS_COUNT_CHANGED_EVENT event.
*/
export type CoreCommentsCountChangedEventData = {
contextLevel: string;
instanceId: number;
component: string;
itemId: number;
area: string;
countChange: number;
};
/**
* Data sent by REFRESH_COMMENTS_EVENT event.
*/
export type CoreCommentsRefreshCommentsEventData = {
contextLevel?: string;
instanceId?: number;
component?: string;
itemId?: number;
area?: string;
};

View File

@ -0,0 +1,115 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { CoreSiteSchema } from '@services/sites';
/**
* Database variables for CoreCommentsOfflineProvider.
*/
export const COMMENTS_TABLE = 'core_comments_offline_comments';
export const COMMENTS_DELETED_TABLE = 'core_comments_deleted_offline_comments';
export const COMMENTS_OFFLINE_SITE_SCHEMA: CoreSiteSchema = {
name: 'CoreCommentsOfflineProvider',
version: 1,
tables: [
{
name: COMMENTS_TABLE,
columns: [
{
name: 'contextlevel',
type: 'TEXT',
},
{
name: 'instanceid',
type: 'INTEGER',
},
{
name: 'component',
type: 'TEXT',
},
{
name: 'itemid',
type: 'INTEGER',
},
{
name: 'area',
type: 'TEXT',
},
{
name: 'content',
type: 'TEXT',
},
{
name: 'lastmodified',
type: 'INTEGER',
},
],
primaryKeys: ['contextlevel', 'instanceid', 'component', 'itemid', 'area'],
},
{
name: COMMENTS_DELETED_TABLE,
columns: [
{
name: 'commentid',
type: 'INTEGER',
primaryKey: true,
},
{
name: 'contextlevel',
type: 'TEXT',
},
{
name: 'instanceid',
type: 'INTEGER',
},
{
name: 'component',
type: 'TEXT',
},
{
name: 'itemid',
type: 'INTEGER',
},
{
name: 'area',
type: 'TEXT',
},
{
name: 'deleted',
type: 'INTEGER',
},
],
},
],
};
export type CoreCommentsDBRecord = {
contextlevel: string; // Primary key.
instanceid: number; // Primary key.
component: string; // Primary key.
itemid: number; // Primary key.
area: string; // Primary key.
content: string;
lastmodified: number;
};
export type CoreCommentsDeletedDBRecord = {
commentid: number; // Primary key.
contextlevel: string;
instanceid: number;
component: string;
itemid: number;
area: string;
deleted: number;
};

View File

@ -0,0 +1,49 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreCronHandler } from '@services/cron';
import { makeSingleton } from '@singletons';
import { CoreCommentsSync } from '../comments-sync';
/**
* Synchronization cron handler.
*/
@Injectable( { providedIn: 'root' })
export class CoreCommentsSyncCronHandlerService implements CoreCronHandler {
name = 'CoreCommentsSyncCronHandler';
/**
* Execute the process.
* Receives the ID of the site affected, undefined for all sites.
*
* @param siteId ID of the site affected, undefined for all sites.
* @param force Wether the execution is forced (manual sync).
* @return Promise resolved when done, rejected if failure.
*/
execute(siteId?: string, force?: boolean): Promise<void> {
return CoreCommentsSync.instance.syncAllComments(siteId, force);
}
/**
* Get the time between consecutive executions.
*
* @return Time between consecutive executions (in ms).
*/
getInterval(): number {
return 300000; // 5 minutes.
}
}
export const CoreCommentsSyncCronHandler = makeSingleton(CoreCommentsSyncCronHandlerService);

View File

@ -30,6 +30,7 @@ import { CorePushNotificationsModule } from './pushnotifications/pushnotificatio
import { CoreXAPIModule } from './xapi/xapi.module';
import { CoreViewerModule } from './viewer/viewer.module';
import { CoreSearchModule } from './search/search.module';
import { CoreCommentsModule } from './comments/comments.module';
@NgModule({
imports: [
@ -49,6 +50,7 @@ import { CoreSearchModule } from './search/search.module';
CoreXAPIModule,
CoreH5PModule,
CoreViewerModule,
CoreCommentsModule,
],
})
export class CoreFeaturesModule {}

View File

@ -50,7 +50,7 @@
<ion-item button *ngIf="enteredSiteUrl" (click)="connect($event, enteredSiteUrl.url)"
[attr.aria-label]="'core.login.connect' | translate" detail-push class="core-login-entered-site">
<ion-thumbnail slot="start">
<ion-icon name="fas-pencil-alt"></ion-icon>
<ion-icon name="fas-pen"></ion-icon>
</ion-thumbnail>
<ion-label>
<h2 text-wrap>{{ 'core.login.yourenteredsite' | translate }}</h2>

View File

@ -8,7 +8,7 @@
<ion-buttons slot="end">
<ion-button *ngIf="sites && sites.length > 0" (click)="toggleDelete()" [attr.aria-label]="'core.delete' | translate">
<ion-icon slot="icon-only" name="fas-pencil-alt"></ion-icon>
<ion-icon slot="icon-only" name="fas-pen"></ion-icon>
</ion-button>
<ion-button router-direction="forward" routerLink="/settings"
[attr.aria-label]="'core.settings.appsettings' | translate">

View File

@ -1286,7 +1286,7 @@ export class CoreDomUtilsProvider {
*/
showDeleteConfirm(
translateMessage: string = 'core.areyousure',
translateArgs: Record<string, string> = {},
translateArgs: Record<string, unknown> = {},
options?: AlertOptions,
): Promise<void> {
return this.showConfirm(