MOBILE-3658 comments: Comments module
parent
019e48a7ac
commit
ed22889840
|
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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 {}
|
|
@ -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 {}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -0,0 +1,4 @@
|
|||
.core-comments-clickable {
|
||||
pointer-events: auto;
|
||||
cursor: pointer;
|
||||
}
|
|
@ -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];
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -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 {}
|
|
@ -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}}"
|
||||
}
|
|
@ -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>
|
|
@ -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 {}
|
|
@ -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;
|
||||
};
|
|
@ -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);
|
|
@ -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[];
|
||||
};
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
||||
};
|
|
@ -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);
|
|
@ -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 {}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in New Issue