From c416e570048f6b706a0a603f24a32806e8524e35 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 14 Apr 2021 08:14:06 +0200 Subject: [PATCH] MOBILE-3638 chat: Migrate services --- src/addons/mod/chat/chat.module.ts | 60 ++ src/addons/mod/chat/lang.json | 28 + src/addons/mod/chat/services/chat-helper.ts | 154 +++++ src/addons/mod/chat/services/chat.ts | 650 ++++++++++++++++++ .../mod/chat/services/handlers/index-link.ts | 33 + .../mod/chat/services/handlers/list-link.ts | 33 + .../mod/chat/services/handlers/module.ts | 95 +++ .../mod/chat/services/handlers/prefetch.ts | 186 +++++ src/addons/mod/mod.module.ts | 2 + src/core/features/compile/services/compile.ts | 4 +- 10 files changed, 1243 insertions(+), 2 deletions(-) create mode 100644 src/addons/mod/chat/chat.module.ts create mode 100644 src/addons/mod/chat/lang.json create mode 100644 src/addons/mod/chat/services/chat-helper.ts create mode 100644 src/addons/mod/chat/services/chat.ts create mode 100644 src/addons/mod/chat/services/handlers/index-link.ts create mode 100644 src/addons/mod/chat/services/handlers/list-link.ts create mode 100644 src/addons/mod/chat/services/handlers/module.ts create mode 100644 src/addons/mod/chat/services/handlers/prefetch.ts diff --git a/src/addons/mod/chat/chat.module.ts b/src/addons/mod/chat/chat.module.ts new file mode 100644 index 000000000..b62ca916c --- /dev/null +++ b/src/addons/mod/chat/chat.module.ts @@ -0,0 +1,60 @@ +// (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, Type } from '@angular/core'; +import { Routes } from '@angular/router'; +import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate'; +import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; +import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate'; +import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; +import { AddonModChatComponentsModule } from './components/components.module'; +import { AddonModChatProvider } from './services/chat'; +import { AddonModChatHelperProvider } from './services/chat-helper'; +import { AddonModChatIndexLinkHandler } from './services/handlers/index-link'; +import { AddonModChatListLinkHandler } from './services/handlers/list-link'; +import { AddonModChatModuleHandler, AddonModChatModuleHandlerService } from './services/handlers/module'; +import { AddonModChatPrefetchHandler } from './services/handlers/prefetch'; + +export const ADDON_MOD_CHAT_SERVICES: Type[] = [ + AddonModChatProvider, + AddonModChatHelperProvider, +]; + +const routes: Routes = [ + { + path: AddonModChatModuleHandlerService.PAGE_NAME, + loadChildren: () => import('./chat-lazy.module').then(m => m.AddonModChatLazyModule), + }, +]; + +@NgModule({ + imports: [ + CoreMainMenuTabRoutingModule.forChild(routes), + AddonModChatComponentsModule, + ], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + deps: [], + useFactory: () => () => { + CoreCourseModuleDelegate.registerHandler(AddonModChatModuleHandler.instance); + CoreContentLinksDelegate.registerHandler(AddonModChatIndexLinkHandler.instance); + CoreContentLinksDelegate.registerHandler(AddonModChatListLinkHandler.instance); + CoreCourseModulePrefetchDelegate.registerHandler(AddonModChatPrefetchHandler.instance); + }, + }, + ], +}) +export class AddonModChatModule {} diff --git a/src/addons/mod/chat/lang.json b/src/addons/mod/chat/lang.json new file mode 100644 index 000000000..ad8f86ce8 --- /dev/null +++ b/src/addons/mod/chat/lang.json @@ -0,0 +1,28 @@ +{ + "beep": "Beep", + "chatreport": "Chat sessions", + "currentusers": "Current users", + "enterchat": "Click here to enter the chat now", + "entermessage": "Enter your message", + "errorwhileconnecting": "Error while connecting to the chat.", + "errorwhilegettingchatdata": "Error while getting chat data.", + "errorwhilegettingchatusers": "Error while getting chat users.", + "errorwhileretrievingmessages": "Error while retrieving messages from the server.", + "errorwhilesendingmessage": "Error while sending the message.", + "messagebeepseveryone": "{{$a}} beeps everyone!", + "messagebeepsyou": "{{$a}} has just beeped you!", + "messageenter": "{{$a}} has just entered this chat", + "messageexit": "{{$a}} has left this chat", + "messages": "Messages", + "messageyoubeep": "You beeped {{$a}}", + "modulenameplural": "Chats", + "mustbeonlinetosendmessages": "You must be online to send messages.", + "nomessages": "No messages yet", + "nosessionsfound": "No sessions found", + "saidto": "said to", + "send": "Send", + "sessionstart": "The next chat session will start on {{$a.date}}, ({{$a.fromnow}} from now)", + "showincompletesessions": "Show incomplete sessions", + "talk": "Talk", + "viewreport": "View past chat sessions" +} \ No newline at end of file diff --git a/src/addons/mod/chat/services/chat-helper.ts b/src/addons/mod/chat/services/chat-helper.ts new file mode 100644 index 000000000..61d518e61 --- /dev/null +++ b/src/addons/mod/chat/services/chat-helper.ts @@ -0,0 +1,154 @@ +// (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 { makeSingleton, Translate } from '@singletons'; +import * as moment from 'moment'; +import { AddonModChatMessage, AddonModChatSessionMessage } from './chat'; + +const patternTo = new RegExp(/^To\s([^:]+):(.*)/); + +/** + * Helper service that provides some features for chat. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModChatHelperProvider { + + /** + * Give some format info about messages. + * + * @param currentUserId User Id. + * @param message Message. + * @param prevMessage Previous message (if any). + * @return Message with additional info. + */ + formatMessage( + currentUserId: number, + message: AddonModChatMessage, + prevMessage?: AddonModChatFormattedMessage, + ): AddonModChatFormattedMessage; + formatMessage( + currentUserId: number, + message: AddonModChatSessionMessage, + prevMessage?: AddonModChatFormattedSessionMessage, + ): AddonModChatFormattedSessionMessage; + formatMessage( + currentUserId: number, + message: AddonModChatMessage | AddonModChatSessionMessage, + prevMessage?: AddonModChatAnyFormattedMessage, + ): AddonModChatAnyFormattedMessage { + const formattedMessage: AddonModChatAnyFormattedMessage = message; + + formattedMessage.message = formattedMessage.message.trim(); + + formattedMessage.showDate = this.showDate(message, prevMessage); + formattedMessage.beep = (message.message.substr(0, 5) == 'beep ' && message.message.substr(5).trim()) || undefined; + + formattedMessage.special = !!formattedMessage.beep || ( message).issystem || + ( message).system; + + if (formattedMessage.message.substr(0, 4) == '/me ') { + formattedMessage.special = true; + formattedMessage.message = formattedMessage.message.substr(4).trim(); + } + + if (!formattedMessage.special && formattedMessage.message.match(patternTo)) { + const matches = formattedMessage.message.match(patternTo); + + formattedMessage.message = '' + Translate.instant('addon.mod_chat.saidto') + + ' ' + matches![1] + ': ' + matches![2]; + } + + formattedMessage.showUserData = this.showUserData(currentUserId, message, prevMessage); + if (prevMessage) { + prevMessage.showTail = this.showTail(prevMessage, message); + } + + return formattedMessage; + } + + /** + * Check if the user info should be displayed for the current message. + * User data is only displayed if the previous message was from another user. + * + * @param message Current message where to show the user info. + * @param prevMessage Previous message. + * @return Whether user data should be shown. + */ + protected showUserData( + currentUserId: number, + message: AddonModChatAnyFormattedMessage, + prevMessage?: AddonModChatAnyFormattedMessage, + ): boolean { + return message.userid != currentUserId && + (!prevMessage || prevMessage.userid != message.userid || !!message.showDate || !!prevMessage.special); + } + + /** + * Check if a css tail should be shown. + * + * @param message Current message where to show the user info. + * @param nextMessage Next message. + * @return Whether user data should be shown. + */ + protected showTail(message: AddonModChatAnyFormattedMessage, nextMessage?: AddonModChatAnyFormattedMessage): boolean { + return !nextMessage || nextMessage.userid != message.userid || !!nextMessage.showDate || !!nextMessage.special; + } + + /** + * Check if the date should be displayed between messages (when the day changes at midnight for example). + * + * @param message New message object. + * @param prevMessage Previous message object. + * @return True if messages are from diferent days, false othetwise. + */ + protected showDate(message: AddonModChatAnyFormattedMessage, prevMessage?: AddonModChatAnyFormattedMessage): boolean { + if (!prevMessage) { + return true; + } + + // Check if day has changed. + return !moment(message.timestamp * 1000).isSame(prevMessage.timestamp * 1000, 'day'); + } + +} + +export const AddonModChatHelper = makeSingleton(AddonModChatHelperProvider); + +/** + * Special info for view usage. + */ +type AddonModChatInfoForView = { + showDate?: boolean; // If date should be displayed before the message. + beep?: string; // User id of the beeped user or 'all'. + special?: boolean; // True if is an special message (system, beep or command). + showUserData?: boolean; // If user data should be displayed. + showTail?: boolean; // If tail should be displayed (decoration). + beepWho?: string; // Fullname of the beeped user. +}; + +/** + * Message with data for view usage. + */ +export type AddonModChatFormattedMessage = AddonModChatMessage & AddonModChatInfoForView; + +/** + * Session message with data for view usage. + */ +export type AddonModChatFormattedSessionMessage = AddonModChatSessionMessage & AddonModChatInfoForView; + +/** + * Any possivle formatted message. + */ +export type AddonModChatAnyFormattedMessage = AddonModChatFormattedMessage | AddonModChatFormattedSessionMessage; diff --git a/src/addons/mod/chat/services/chat.ts b/src/addons/mod/chat/services/chat.ts new file mode 100644 index 000000000..c4a361160 --- /dev/null +++ b/src/addons/mod/chat/services/chat.ts @@ -0,0 +1,650 @@ +// (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 { CoreCourseCommonModWSOptions } from '@features/course/services/course'; +import { CoreCourseLogHelper } from '@features/course/services/log-helper'; +import { CoreUser } from '@features/user/services/user'; +import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites'; +import { CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws'; +import { makeSingleton, Translate } from '@singletons'; + +const ROOT_CACHE_KEY = 'AddonModChat:'; + +/** + * Service that provides some features for chats. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModChatProvider { + + static readonly COMPONENT = 'mmaModChat'; + static readonly POLL_INTERVAL = 4000; + + /** + * Get a chat. + * + * @param courseId Course ID. + * @param cmId Course module ID. + * @param options Other options. + * @return Promise resolved when the chat is retrieved. + */ + async getChat(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + const site = await CoreSites.getSite(options.siteId); + + const params: AddonModChatGetChatsByCoursesWSParams = { + courseids: [courseId], + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getChatsCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModChatProvider.COMPONENT, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + const response = await site.read('mod_chat_get_chats_by_courses', params, preSets); + + const chat = response.chats.find((chat) => chat.coursemodule == cmId); + if (chat) { + return chat; + } + + throw new CoreError('Chat not found.'); + } + + /** + * Log the user into a chat room. + * + * @param chatId Chat instance ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the WS is executed. + */ + async loginUser(chatId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + const params: AddonModChatLoginUserWSParams = { + chatid: chatId, + }; + + const response = await site.write('mod_chat_login_user', params); + + return response.chatsid; + } + + /** + * Report a chat as being viewed. + * + * @param id Chat instance ID. + * @param name Name of the chat. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the WS call is successful. + */ + async logView(id: number, name?: string, siteId?: string): Promise { + const params: AddonModChatViewChatWSParams = { + chatid: id, + }; + + await CoreCourseLogHelper.logSingle( + 'mod_chat_view_chat', + params, + AddonModChatProvider.COMPONENT, + id, + name, + 'chat', + {}, + siteId, + ); + } + + /** + * Send a message to a chat. + * + * @param sessionId Chat sessiond ID. + * @param message Message text. + * @param beepUserId Beep user ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the WS is executed. + */ + async sendMessage(sessionId: string, message: string, beepUserId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + const params: AddonModChatSendChatMessageWSParams = { + chatsid: sessionId, + messagetext: message, + beepid: String(beepUserId), + }; + + const response = await site.write('mod_chat_send_chat_message', params); + + return response.messageid; + } + + /** + * Get the latest messages from a chat session. + * + * @param sessionId Chat sessiond ID. + * @param lastTime Last time when messages were retrieved. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the WS is executed. + */ + async getLatestMessages( + sessionId: string, + lastTime: number, + siteId?: string, + ): Promise { + const site = await CoreSites.getSite(siteId); + + const params: AddonModChatGetChatLatestMessagesWSParams = { + chatsid: sessionId, + chatlasttime: lastTime, + }; + + /* We use write to not use cache. It doesn't make sense to store the messages in cache + because we won't be able to retireve them if AddonModChatProvider.loginUser fails. */ + return site.write('mod_chat_get_chat_latest_messages', params); + } + + /** + * Get user data for messages since they only have userid. + * + * @param messages Messages to get the user data for. + * @param courseId ID of the course the messages belong to. + * @param siteId Site ID. If not defined, current site. + * @return Promise always resolved with the formatted messages. + */ + async getMessagesUserData(messages: AddonModChatWSMessage[], courseId: number, siteId?: string): Promise; + async getMessagesUserData( + messages: AddonModChatWSSessionMessage[], + courseId: number, + siteId?: string, + ): Promise; + async getMessagesUserData( + messages: (AddonModChatWSMessage | AddonModChatWSSessionMessage)[], + courseId: number, + siteId?: string, + ): Promise<(AddonModChatMessage | AddonModChatSessionMessage)[]> { + const formattedMessages: (AddonModChatMessage | AddonModChatSessionMessage)[] = messages; + + await Promise.all(formattedMessages.map(async (message) => { + try { + const user = await CoreUser.getProfile(message.userid, courseId, true, siteId); + + message.userfullname = user.fullname; + message.userprofileimageurl = user.profileimageurl; + } catch { + // Error getting profile, most probably the user is deleted. + message.userfullname = Translate.instant('core.deleteduser') + ' ' + message.userid; + } + })); + + return formattedMessages; + } + + /** + * Get the actives users of a current chat. + * + * @param sessionId Chat sessiond ID. + * @param options Other options. + * @return Promise resolved when the WS is executed. + */ + async getChatUsers(sessionId: string, options: CoreCourseCommonModWSOptions = {}): Promise { + // By default, always try to get the latest data. + options.readingStrategy = options.readingStrategy || CoreSitesReadingStrategy.PreferNetwork; + + const site = await CoreSites.getSite(options.siteId); + + const params: AddonModChatGetChatUsersWSParams = { + chatsid: sessionId, + }; + const preSets: CoreSiteWSPreSets = { + component: AddonModChatProvider.COMPONENT, + componentId: options.cmId, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + return site.read('mod_chat_get_chat_users', params, preSets); + } + + /** + * Return whether WS for passed sessions are available. + * + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with a boolean. + * @since 3.5 + */ + async areSessionsAvailable(siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + return site.wsAvailable('mod_chat_get_sessions') && site.wsAvailable('mod_chat_get_session_messages'); + } + + /** + * Get chat sessions. + * + * @param chatId Chat ID. + * @param groupId Group ID, 0 means that the function will determine the user group. + * @param showAll Whether to include incomplete sessions or not. + * @param options Other options. + * @return Promise resolved with the list of sessions. + * @since 3.5 + */ + async getSessions( + chatId: number, + groupId: number = 0, + showAll: boolean = false, + options: CoreCourseCommonModWSOptions = {}, + ): Promise { + const site = await CoreSites.getSite(options.siteId); + + const params: AddonModChatGetSessionsWSParams = { + chatid: chatId, + groupid: groupId, + showall: showAll, + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getSessionsCacheKey(chatId, groupId, showAll), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModChatProvider.COMPONENT, + componentId: options.cmId, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + const response = await site.read('mod_chat_get_sessions', params, preSets); + + return response.sessions; + } + + /** + * Get chat session messages. + * + * @param chatId Chat ID. + * @param sessionStart Session start time. + * @param sessionEnd Session end time. + * @param groupId Group ID, 0 means that the function will determine the user group. + * @param options Other options. + * @return Promise resolved with the list of messages. + * @since 3.5 + */ + async getSessionMessages( + chatId: number, + sessionStart: number, + sessionEnd: number, + groupId: number = 0, + options: CoreCourseCommonModWSOptions = {}, + ): Promise { + const site = await CoreSites.getSite(options.siteId); + + const params: AddonModChatGetSessionMessagesWSParams = { + chatid: chatId, + sessionstart: sessionStart, + sessionend: sessionEnd, + groupid: groupId, + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getSessionMessagesCacheKey(chatId, sessionStart, groupId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModChatProvider.COMPONENT, + componentId: options.cmId, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + const response = await site.read( + 'mod_chat_get_session_messages', + params, + preSets, + ); + + return response.messages; + } + + /** + * Invalidate chats. + * + * @param courseId Course ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateChats(courseId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getChatsCacheKey(courseId)); + } + + /** + * Invalidate chat sessions. + * + * @param chatId Chat ID. + * @param groupId Group ID, 0 means that the function will determine the user group. + * @param showAll Whether to include incomplete sessions or not. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateSessions(chatId: number, groupId: number = 0, showAll: boolean = false, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getSessionsCacheKey(chatId, groupId, showAll)); + } + + /** + * Invalidate all chat sessions. + * + * @param chatId Chat ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateAllSessions(chatId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + await site.invalidateWsCacheForKeyStartingWith(this.getSessionsCacheKeyPrefix(chatId)); + } + + /** + * Invalidate chat session messages. + * + * @param chatId Chat ID. + * @param sessionStart Session start time. + * @param groupId Group ID, 0 means that the function will determine the user group. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateSessionMessages(chatId: number, sessionStart: number, groupId: number = 0, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getSessionMessagesCacheKey(chatId, sessionStart, groupId)); + } + + /** + * Invalidate all chat session messages. + * + * @param chatId Chat ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateAllSessionMessages(chatId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + await site.invalidateWsCacheForKeyStartingWith(this.getSessionMessagesCacheKeyPrefix(chatId)); + } + + /** + * Get cache key for chats WS call. + * + * @param courseId Course ID. + * @return Cache key. + */ + protected getChatsCacheKey(courseId: number): string { + return ROOT_CACHE_KEY + 'chats:' + courseId; + } + + /** + * Get cache key for sessions WS call. + * + * @param chatId Chat ID. + * @param groupId Goup ID, 0 means that the function will determine the user group. + * @param showAll Whether to include incomplete sessions or not. + * @return Cache key. + */ + protected getSessionsCacheKey(chatId: number, groupId: number, showAll: boolean): string { + return this.getSessionsCacheKeyPrefix(chatId) + groupId + ':' + (showAll ? 1 : 0); + } + + /** + * Get cache key prefix for sessions WS call. + * + * @param chatId Chat ID. + * @return Cache key prefix. + */ + protected getSessionsCacheKeyPrefix(chatId: number): string { + return ROOT_CACHE_KEY + 'sessions:' + chatId + ':'; + } + + /** + * Get cache key for session messages WS call. + * + * @param chatId Chat ID. + * @param sessionStart Session start time. + * @param groupId Group ID, 0 means that the function will determine the user group. + * @return Cache key. + */ + protected getSessionMessagesCacheKey(chatId: number, sessionStart: number, groupId: number): string { + return this.getSessionMessagesCacheKeyPrefix(chatId) + sessionStart + ':' + groupId; + } + + /** + * Get cache key prefix for session messages WS call. + * + * @param chatId Chat ID. + * @return Cache key prefix. + */ + protected getSessionMessagesCacheKeyPrefix(chatId: number): string { + return ROOT_CACHE_KEY + 'sessionsMessages:' + chatId + ':'; + } + +} + +export const AddonModChat = makeSingleton(AddonModChatProvider); + +/** + * Params of mod_chat_get_chats_by_courses WS. + */ +export type AddonModChatGetChatsByCoursesWSParams = { + courseids?: number[]; // Array of course ids. +}; + +/** + * Data returned by mod_chat_get_chats_by_courses WS. + */ +export type AddonModChatGetChatsByCoursesWSResponse = { + chats: AddonModChatChat[]; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Chat returned by mod_chat_get_chats_by_courses. + */ +export type AddonModChatChat = { + id: number; // Chat id. + coursemodule: number; // Course module id. + course: number; // Course id. + name: string; // Chat name. + intro: string; // The Chat intro. + introformat: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + introfiles?: CoreWSExternalFile[]; // @since 3.2. + chatmethod?: string; // Chat method (sockets, ajax, header_js). + keepdays?: number; // Keep days. + studentlogs?: number; // Student logs visible to everyone. + chattime?: number; // Chat time. + schedule?: number; // Schedule type. + timemodified?: number; // Time of last modification. + section?: number; // Course section id. + visible?: boolean; // Visible. + groupmode?: number; // Group mode. + groupingid?: number; // Group id. +}; + +/** + * Params of mod_chat_login_user WS. + */ +export type AddonModChatLoginUserWSParams = { + chatid: number; // Chat instance id. + groupid?: number; // Group id, 0 means that the function will determine the user group. +}; + +/** + * Data returned by mod_chat_login_user WS. + */ +export type AddonModChatLoginUserWSResponse = { + chatsid: string; // Unique chat session id. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Params of mod_chat_view_chat WS. + */ +export type AddonModChatViewChatWSParams = { + chatid: number; // Chat instance id. +}; + +/** + * Params of mod_chat_send_chat_message WS. + */ +export type AddonModChatSendChatMessageWSParams = { + chatsid: string; // Chat session id (obtained via mod_chat_login_user). + messagetext: string; // The message text. + beepid?: string; // The beep id. +}; + +/** + * Data returned by mod_chat_send_chat_message WS. + */ +export type AddonModChatSendChatMessageWSResponse = { + messageid: number; // Message sent id. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Params of mod_chat_get_chat_latest_messages WS. + */ +export type AddonModChatGetChatLatestMessagesWSParams = { + chatsid: string; // Chat session id (obtained via mod_chat_login_user). + chatlasttime?: number; // Last time messages were retrieved (epoch time). +}; + +/** + * Data returned by mod_chat_get_chat_latest_messages WS. + */ +export type AddonModChatGetChatLatestMessagesWSResponse = { + messages: AddonModChatWSMessage[]; + chatnewlasttime: number; // New last time. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Params of mod_chat_get_chat_users WS. + */ +export type AddonModChatGetChatUsersWSParams = { + chatsid: string; // Chat session id (obtained via mod_chat_login_user). +}; + +/** + * Data returned by mod_chat_get_chat_users WS. + */ +export type AddonModChatGetChatUsersWSResponse = { + users: AddonModChatUser[]; // List of users. + warnings?: CoreWSExternalWarning[]; +}; +/** + * Chat user returned by mod_chat_get_chat_users. + */ +export type AddonModChatUser = { + id: number; // User id. + fullname: string; // User full name. + profileimageurl: string; // User picture URL. +}; + +/** + * Params of mod_chat_get_sessions WS. + */ +export type AddonModChatGetSessionsWSParams = { + chatid: number; // Chat instance id. + groupid?: number; // Get messages from users in this group. 0 means that the function will determine the user group. + showall?: boolean; // Whether to show completed sessions or not. +}; + +/** + * Data returned by mod_chat_get_sessions WS. + */ +export type AddonModChatGetSessionsWSResponse = { + sessions: AddonModChatSession[]; // List of sessions. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Chat session returned by mod_chat_get_sessions. + */ +export type AddonModChatSession = { + sessionstart: number; // Session start time. + sessionend: number; // Session end time. + sessionusers: AddonModChatSessionUser[]; // Session users. + iscomplete: boolean; // Whether the session is completed or not. +}; + +/** + * Chat user returned by mod_chat_get_sessions. + */ +export type AddonModChatSessionUser = { + userid: number; // User id. + messagecount: number; // Number of messages in the session. +}; + +/** + * Params of mod_chat_get_session_messages WS. + */ +export type AddonModChatGetSessionMessagesWSParams = { + chatid: number; // Chat instance id. + sessionstart: number; // The session start time (timestamp). + sessionend: number; // The session end time (timestamp). + groupid?: number; // Get messages from users in this group. 0 means that the function will determine the user group. +}; + +/** + * Data returned by mod_chat_get_session_messages WS. + */ +export type AddonModChatGetSessionMessagesWSResponse = { + messages: AddonModChatWSSessionMessage[]; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Meessage returned by mod_chat_get_chat_latest_messages. + */ +export type AddonModChatWSMessage = { + id: number; // Message id. + userid: number; // User id. + system: boolean; // True if is a system message (like user joined). + message: string; // Message text. + timestamp: number; // Timestamp for the message. +}; + +/** + * Message with user data. + */ +export type AddonModChatMessage = AddonModChatWSMessage & AddonModChatMessageUserData; + +/** + * Message returned by mod_chat_get_session_messages. + */ +export type AddonModChatWSSessionMessage = { + id: number; // The message record id. + chatid: number; // The chat id. + userid: number; // The user who wrote the message. + groupid: number; // The group this message belongs to. + issystem: boolean; // Whether is a system message or not. + message: string; // The message text. + timestamp: number; // The message timestamp (indicates when the message was sent). +}; + +/** + * Session message with user data. + */ +export type AddonModChatSessionMessage = AddonModChatWSSessionMessage & AddonModChatMessageUserData; + +/** + * User data added to messages. + */ +type AddonModChatMessageUserData = { + userfullname?: string; // Calculated in the app. Full name of the user who wrote the message. + userprofileimageurl?: string; // Calculated in the app. Full name of the user who wrote the message. +}; diff --git a/src/addons/mod/chat/services/handlers/index-link.ts b/src/addons/mod/chat/services/handlers/index-link.ts new file mode 100644 index 000000000..f128e3582 --- /dev/null +++ b/src/addons/mod/chat/services/handlers/index-link.ts @@ -0,0 +1,33 @@ +// (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 { CoreContentLinksModuleIndexHandler } from '@features/contentlinks/classes/module-index-handler'; +import { makeSingleton } from '@singletons'; + +/** + * Handler to treat links to chat. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModChatIndexLinkHandlerService extends CoreContentLinksModuleIndexHandler { + + name = 'AddonModChatIndexLinkHandlerService'; + + constructor() { + super('AddonModChat', 'chat', 'c'); + } + +} + +export const AddonModChatIndexLinkHandler = makeSingleton(AddonModChatIndexLinkHandlerService); diff --git a/src/addons/mod/chat/services/handlers/list-link.ts b/src/addons/mod/chat/services/handlers/list-link.ts new file mode 100644 index 000000000..2ec525297 --- /dev/null +++ b/src/addons/mod/chat/services/handlers/list-link.ts @@ -0,0 +1,33 @@ +// (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 { CoreContentLinksModuleListHandler } from '@features/contentlinks/classes/module-list-handler'; +import { makeSingleton } from '@singletons'; + +/** + * Handler to treat links to chat list page. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModChatListLinkHandlerService extends CoreContentLinksModuleListHandler { + + name = 'AddonModChatListLinkHandler'; + + constructor() { + super('AddonModChat', 'chat'); + } + +} + +export const AddonModChatListLinkHandler = makeSingleton(AddonModChatListLinkHandlerService); diff --git a/src/addons/mod/chat/services/handlers/module.ts b/src/addons/mod/chat/services/handlers/module.ts new file mode 100644 index 000000000..a3e15e097 --- /dev/null +++ b/src/addons/mod/chat/services/handlers/module.ts @@ -0,0 +1,95 @@ +// (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 { CoreConstants } from '@/core/constants'; +import { Injectable, Type } from '@angular/core'; +import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/course'; +import { CoreCourseModule } from '@features/course/services/course-helper'; +import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate'; +import { CoreNavigationOptions, CoreNavigator } from '@services/navigator'; +import { makeSingleton } from '@singletons'; +import { AddonModChatIndexComponent } from '../../components/index'; +import { AddonModChat } from '../chat'; + +/** + * Handler to support chat modules. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModChatModuleHandlerService implements CoreCourseModuleHandler { + + static readonly PAGE_NAME = 'mod_chat'; + + name = 'AddonModChat'; + modName = 'chat'; + + supportedFeatures = { + [CoreConstants.FEATURE_GROUPS]: true, + [CoreConstants.FEATURE_GROUPINGS]: true, + [CoreConstants.FEATURE_MOD_INTRO]: true, + [CoreConstants.FEATURE_COMPLETION_TRACKS_VIEWS]: true, + [CoreConstants.FEATURE_GRADE_HAS_GRADE]: false, + [CoreConstants.FEATURE_GRADE_OUTCOMES]: true, + [CoreConstants.FEATURE_BACKUP_MOODLE2]: true, + [CoreConstants.FEATURE_SHOW_DESCRIPTION]: true, + }; + + /** + * @inheritdoc + */ + async isEnabled(): Promise { + return true; + } + + /** + * @inheritdoc + */ + getData(module: CoreCourseAnyModuleData): CoreCourseModuleHandlerData { + const data: CoreCourseModuleHandlerData = { + icon: CoreCourse.getModuleIconSrc(this.modName, 'modicon' in module ? module.modicon : undefined), + title: module.name, + class: 'addon-mod_chat-handler', + action(event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): void { + options = options || {}; + options.params = options.params || {}; + Object.assign(options.params, { module }); + const routeParams = '/' + courseId + '/' + module.id; + + CoreNavigator.navigateToSitePath(AddonModChatModuleHandlerService.PAGE_NAME + routeParams, options); + }, + }; + + this.checkDownloadButton(data); + + return data; + } + + /** + * Check whether download button should be displayed. + * + * @param data Handler data. + */ + protected async checkDownloadButton(data: CoreCourseModuleHandlerData): Promise { + data.showDownloadButton = await AddonModChat.areSessionsAvailable(); + } + + /** + * @inheritdoc + */ + async getMainComponent(): Promise> { + return AddonModChatIndexComponent; + } + +} + +export const AddonModChatModuleHandler = makeSingleton(AddonModChatModuleHandlerService); diff --git a/src/addons/mod/chat/services/handlers/prefetch.ts b/src/addons/mod/chat/services/handlers/prefetch.ts new file mode 100644 index 000000000..a332b9382 --- /dev/null +++ b/src/addons/mod/chat/services/handlers/prefetch.ts @@ -0,0 +1,186 @@ +// (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 { CoreCourseActivityPrefetchHandlerBase } from '@features/course/classes/activity-prefetch-handler'; +import { CoreCourse, CoreCourseAnyModuleData, CoreCourseCommonModWSOptions } from '@features/course/services/course'; +import { CoreUser } from '@features/user/services/user'; +import { CoreGroups } from '@services/groups'; +import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; +import { CoreUtils } from '@services/utils/utils'; +import { makeSingleton } from '@singletons'; +import { AddonModChat, AddonModChatProvider, AddonModChatSession } from '../chat'; + +/** + * Handler to prefetch chats. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModChatPrefetchHandlerService extends CoreCourseActivityPrefetchHandlerBase { + + name = 'AddonModChat'; + modName = 'chat'; + component = AddonModChatProvider.COMPONENT; + + /** + * @inheritdoc + */ + async isEnabled(): Promise { + return AddonModChat.areSessionsAvailable(); + } + + /** + * @inheritdoc + */ + async invalidateContent(moduleId: number, courseId: number): Promise { + const chat = await AddonModChat.getChat(courseId, moduleId); + + await CoreUtils.allPromises([ + AddonModChat.invalidateAllSessions(chat.id), + AddonModChat.invalidateAllSessionMessages(chat.id), + ]); + } + + /** + * @inheritdoc + */ + async invalidateModule(module: CoreCourseAnyModuleData, courseId: number): Promise { + await CoreUtils.allPromises([ + AddonModChat.invalidateChats(courseId), + CoreCourse.invalidateModule(module.id), + ]); + } + + /** + * @inheritdoc + */ + prefetch(module: CoreCourseAnyModuleData, courseId?: number): Promise { + return this.prefetchPackage(module, courseId, this.prefetchChat.bind(this, module, courseId)); + } + + /** + * Prefetch a chat. + * + * @param module The module object returned by WS. + * @param courseId Course ID the module belongs to. + * @return Promise resolved when done. + */ + protected async prefetchChat(module: CoreCourseAnyModuleData, courseId: number): Promise { + const siteId = CoreSites.getCurrentSiteId(); + const options = { + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; + const modOptions = { + ...options, + cmId: module.id, + }; + + // Prefetch chat and group info. + const [chat, groupInfo] = await Promise.all([ + AddonModChat.getChat(courseId, module.id, options), + CoreGroups.getActivityGroupInfo(module.id, false, undefined, siteId), + ]); + + const promises: Promise[] = []; + + let groupIds = [0]; + if (groupInfo.groups && groupInfo.groups.length > 0) { + groupIds = groupInfo.groups.map((group) => group.id); + } + + groupIds.forEach((groupId) => { + // Prefetch complete sessions. + promises.push(this.prefetchSessions(chat.id, groupId, courseId, false, modOptions)); + + // Prefetch all sessions. + promises.push(this.prefetchSessions(chat.id, groupId, courseId, true, modOptions)); + }); + + await Promise.all(promises); + } + + /** + * Prefetch chat sessions. + * + * @param chatId Chat ID. + * @param groupId Group ID, 0 means that the function will determine the user group. + * @param courseId Course ID. + * @param showAll Whether to include incomplete sessions or not. + * @param modOptions Other options. + * @return Promise resolved with the list of sessions. + */ + protected async prefetchSessions( + chatId: number, + groupId: number, + courseId: number, + showAll: boolean, + modOptions: CoreCourseCommonModWSOptions, + ): Promise { + try { + const sessions = await AddonModChat.getSessions(chatId, groupId, showAll, modOptions); + + if (showAll) { + // Prefetch each session data too. + await Promise.all(sessions.map((session) => this.prefetchSession(chatId, session, groupId, courseId, modOptions))); + } + } catch (error) { + // Ignore group error. + if (error && error.errorcode == 'notingroup') { + return; + } + + throw error; + } + } + + /** + * Prefetch chat session messages and user profiles. + * + * @param chatId Chat ID. + * @param session Session object. + * @param groupId Group ID. + * @param courseId Course ID the module belongs to. + * @param modOptions Other options. + * @return Promise resolved when done. + */ + protected async prefetchSession( + chatId: number, + session: AddonModChatSession, + groupId: number, + courseId: number, + modOptions: CoreCourseCommonModWSOptions, + ): Promise { + const messages = await AddonModChat.getSessionMessages( + chatId, + session.sessionstart, + session.sessionend, + groupId, + modOptions, + ); + + const users: Record = {}; + session.sessionusers.forEach((user) => { + users[user.userid] = user.userid; + }); + messages.forEach((message) => { + users[message.userid] = message.userid; + }); + const userIds = Object.values(users); + + await CoreUser.prefetchProfiles(userIds, courseId, modOptions.siteId); + } + +} + +export const AddonModChatPrefetchHandler = makeSingleton(AddonModChatPrefetchHandlerService); diff --git a/src/addons/mod/mod.module.ts b/src/addons/mod/mod.module.ts index 4ce3025a6..8976cdff9 100644 --- a/src/addons/mod/mod.module.ts +++ b/src/addons/mod/mod.module.ts @@ -33,6 +33,7 @@ import { AddonModScormModule } from './scorm/scorm.module'; import { AddonModChoiceModule } from './choice/choice.module'; import { AddonModWikiModule } from './wiki/wiki.module'; import { AddonModGlossaryModule } from './glossary/glossary.module'; +import { AddonModChatModule } from './chat/chat.module'; @NgModule({ imports: [ @@ -55,6 +56,7 @@ import { AddonModGlossaryModule } from './glossary/glossary.module'; AddonModChoiceModule, AddonModWikiModule, AddonModGlossaryModule, + AddonModChatModule, ], }) export class AddonModModule { } diff --git a/src/core/features/compile/services/compile.ts b/src/core/features/compile/services/compile.ts index 80e733aca..86bc7dda4 100644 --- a/src/core/features/compile/services/compile.ts +++ b/src/core/features/compile/services/compile.ts @@ -124,7 +124,7 @@ import { ADDON_MESSAGEOUTPUT_SERVICES } from '@addons/messageoutput/messageoutpu import { ADDON_MESSAGES_SERVICES } from '@addons/messages/messages.module'; import { ADDON_MOD_ASSIGN_SERVICES } from '@addons/mod/assign/assign.module'; import { ADDON_MOD_BOOK_SERVICES } from '@addons/mod/book/book.module'; -// @todo import { ADDON_MOD_CHAT_SERVICES } from '@addons/mod/chat/chat.module'; +import { ADDON_MOD_CHAT_SERVICES } from '@addons/mod/chat/chat.module'; import { ADDON_MOD_CHOICE_SERVICES } from '@addons/mod/choice/choice.module'; import { ADDON_MOD_DATA_SERVICES } from '@addons/mod/data/data.module'; // @todo import { ADDON_MOD_FEEDBACK_SERVICES } from '@addons/mod/feedback/feedback.module'; @@ -290,7 +290,7 @@ export class CoreCompileProvider { ...ADDON_MESSAGES_SERVICES, ...ADDON_MOD_ASSIGN_SERVICES, ...ADDON_MOD_BOOK_SERVICES, - // @todo ...ADDON_MOD_CHAT_SERVICES, + ...ADDON_MOD_CHAT_SERVICES, ...ADDON_MOD_CHOICE_SERVICES, ...ADDON_MOD_DATA_SERVICES, // @todo ...ADDON_MOD_FEEDBACK_SERVICES,