626 lines
21 KiB
TypeScript
Raw Normal View History

2021-02-16 13:47:07 +01:00
// (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;
};