diff --git a/scripts/langindex.json b/scripts/langindex.json index 6e9b7a740..c9486501c 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -346,6 +346,7 @@ "addon.mod_book.errorchapter": "book", "addon.mod_book.modulenameplural": "book", "addon.mod_chat.beep": "chat", + "addon.mod_chat.chatreport": "chat", "addon.mod_chat.currentusers": "chat", "addon.mod_chat.enterchat": "chat", "addon.mod_chat.entermessage": "chat", @@ -357,12 +358,16 @@ "addon.mod_chat.messagebeepsyou": "chat", "addon.mod_chat.messageenter": "chat", "addon.mod_chat.messageexit": "chat", + "addon.mod_chat.messages": "chat", "addon.mod_chat.modulenameplural": "chat", "addon.mod_chat.mustbeonlinetosendmessages": "local_moodlemobileapp", "addon.mod_chat.nomessages": "chat", + "addon.mod_chat.nosessionsfound": "local_moodlemobileapp", "addon.mod_chat.send": "chat", "addon.mod_chat.sessionstart": "chat", + "addon.mod_chat.showincompletesessions": "local_moodlemobileapp", "addon.mod_chat.talk": "chat", + "addon.mod_chat.viewreport": "chat", "addon.mod_choice.cannotsubmit": "choice", "addon.mod_choice.choiceoptions": "choice", "addon.mod_choice.errorgetchoice": "local_moodlemobileapp", @@ -1295,6 +1300,7 @@ "core.defaultvalue": "tool_usertours", "core.delete": "moodle", "core.deletedoffline": "local_moodlemobileapp", + "core.deleteduser": "bulkusers", "core.deleting": "local_moodlemobileapp", "core.description": "moodle", "core.dfdaymonthyear": "local_moodlemobileapp", @@ -1545,6 +1551,7 @@ "core.noresults": "moodle", "core.notapplicable": "local_moodlemobileapp", "core.notice": "moodle", + "core.notingroup": "moodle", "core.notsent": "local_moodlemobileapp", "core.now": "moodle", "core.numwords": "moodle", diff --git a/src/addon/mod/chat/chat.module.ts b/src/addon/mod/chat/chat.module.ts index 62ce5d80b..7d90566e0 100644 --- a/src/addon/mod/chat/chat.module.ts +++ b/src/addon/mod/chat/chat.module.ts @@ -15,11 +15,13 @@ import { NgModule } from '@angular/core'; import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate'; import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate'; +import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; import { AddonModChatComponentsModule } from './components/components.module'; import { AddonModChatProvider } from './providers/chat'; import { AddonModChatLinkHandler } from './providers/link-handler'; import { AddonModChatListLinkHandler } from './providers/list-link-handler'; import { AddonModChatModuleHandler } from './providers/module-handler'; +import { AddonModChatPrefetchHandler } from './providers/prefetch-handler'; // List of providers (without handlers). export const ADDON_MOD_CHAT_PROVIDERS: any[] = [ @@ -37,15 +39,18 @@ export const ADDON_MOD_CHAT_PROVIDERS: any[] = [ AddonModChatLinkHandler, AddonModChatListLinkHandler, AddonModChatModuleHandler, + AddonModChatPrefetchHandler ] }) export class AddonModChatModule { constructor(moduleDelegate: CoreCourseModuleDelegate, moduleHandler: AddonModChatModuleHandler, contentLinksDelegate: CoreContentLinksDelegate, linkHandler: AddonModChatLinkHandler, + prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModChatPrefetchHandler, listLinkHandler: AddonModChatListLinkHandler) { moduleDelegate.registerHandler(moduleHandler); contentLinksDelegate.registerHandler(linkHandler); contentLinksDelegate.registerHandler(listLinkHandler); + prefetchDelegate.registerHandler(prefetchHandler); } } diff --git a/src/addon/mod/chat/components/index/addon-mod-chat-index.html b/src/addon/mod/chat/components/index/addon-mod-chat-index.html index b2f0d53c3..f76b8f8f8 100644 --- a/src/addon/mod/chat/components/index/addon-mod-chat-index.html +++ b/src/addon/mod/chat/components/index/addon-mod-chat-index.html @@ -5,6 +5,7 @@ + @@ -17,7 +18,8 @@ {{ 'addon.mod_chat.sessionstart' | translate:{$a: chatInfo} }} -
+
{{ 'addon.mod_chat.enterchat' | translate }} + {{ 'addon.mod_chat.viewreport' | translate }}
diff --git a/src/addon/mod/chat/components/index/index.ts b/src/addon/mod/chat/components/index/index.ts index 55d60961f..a04cf11d0 100644 --- a/src/addon/mod/chat/components/index/index.ts +++ b/src/addon/mod/chat/components/index/index.ts @@ -33,6 +33,7 @@ export class AddonModChatIndexComponent extends CoreCourseModuleMainActivityComp chatInfo: any; protected title: string; + protected sessionsAvailable = false; constructor(injector: Injector, private chatProvider: AddonModChatProvider, private timeUtils: CoreTimeUtilsProvider, protected navCtrl: NavController) { @@ -83,6 +84,10 @@ export class AddonModChatIndexComponent extends CoreCourseModuleMainActivityComp // All data obtained, now fill the context menu. this.fillContextMenu(refresh); + + return this.chatProvider.areSessionsAvailable().then((available) => { + this.sessionsAvailable = available; + }); }); } @@ -93,4 +98,11 @@ export class AddonModChatIndexComponent extends CoreCourseModuleMainActivityComp const title = this.chat.name || this.moduleName; this.navCtrl.push('AddonModChatChatPage', {chatId: this.chat.id, courseId: this.courseId, title: title }); } + + /** + * View past sessions. + */ + viewSessions(): void { + this.navCtrl.push('AddonModChatSessionsPage', {courseId: this.courseId, chatId: this.chat.id, cmId: this.module.id}); + } } diff --git a/src/addon/mod/chat/lang/en.json b/src/addon/mod/chat/lang/en.json index 30f9613ca..4348b5d63 100644 --- a/src/addon/mod/chat/lang/en.json +++ b/src/addon/mod/chat/lang/en.json @@ -1,5 +1,6 @@ { "beep": "Beep", + "chatreport": "Chat sessions", "currentusers": "Current users", "enterchat": "Click here to enter the chat now", "entermessage": "Enter your message", @@ -11,10 +12,14 @@ "messagebeepsyou": "{{$a}} has just beeped you!", "messageenter": "{{$a}} has just entered this chat", "messageexit": "{{$a}} has left this chat", + "messages": "Messages", "modulenameplural": "Chats", "mustbeonlinetosendmessages": "You must be online to send messages.", "nomessages": "No messages yet", + "nosessionsfound": "No sessions found", "send": "Send", "sessionstart": "The next chat session will start on {{$a.date}}, ({{$a.fromnow}} from now)", - "talk": "Talk" + "showincompletesessions": "Show incomplete sessions", + "talk": "Talk", + "viewreport": "View past chat sessions" } \ No newline at end of file diff --git a/src/addon/mod/chat/pages/session-messages/session-messages.html b/src/addon/mod/chat/pages/session-messages/session-messages.html new file mode 100644 index 000000000..a315ebecf --- /dev/null +++ b/src/addon/mod/chat/pages/session-messages/session-messages.html @@ -0,0 +1,40 @@ + + + {{ 'addon.mod_chat.messages' | translate }} + + + + + + + +
+
+ + {{ message.timestamp * 1000 | coreFormatDate:"strftimedayshort" }} + +
+ +
+ + {{ message.timestamp * 1000 | coreFormatDate:"strftimetime" }} {{ 'addon.mod_chat.messageenter' | translate:{$a: message.userfullname} }} + +
+ +
+ + {{ message.timestamp * 1000 | coreFormatDate:"strftimetime" }} {{ 'addon.mod_chat.messageexit' | translate:{$a: message.userfullname} }} + +
+ + + +

+

{{ message.timestamp * 1000 | coreFormatDate:"strftimetime" }}

+ +

+ +
+
+
+
diff --git a/src/addon/mod/chat/pages/session-messages/session-messages.module.ts b/src/addon/mod/chat/pages/session-messages/session-messages.module.ts new file mode 100644 index 000000000..816b70999 --- /dev/null +++ b/src/addon/mod/chat/pages/session-messages/session-messages.module.ts @@ -0,0 +1,37 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { IonicPageModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CorePipesModule } from '@pipes/pipes.module'; +import { AddonModChatComponentsModule } from '../../components/components.module'; +import { AddonModChatSessionMessagesPage } from './session-messages'; + +@NgModule({ + declarations: [ + AddonModChatSessionMessagesPage, + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + CorePipesModule, + AddonModChatComponentsModule, + IonicPageModule.forChild(AddonModChatSessionMessagesPage), + TranslateModule.forChild() + ], +}) +export class AddonModChatSessionMessagesPageModule {} diff --git a/src/addon/mod/chat/pages/session-messages/session-messages.scss b/src/addon/mod/chat/pages/session-messages/session-messages.scss new file mode 100644 index 000000000..a8d1e96e8 --- /dev/null +++ b/src/addon/mod/chat/pages/session-messages/session-messages.scss @@ -0,0 +1,9 @@ +ion-app.app-root page-addon-mod-chat-session-messages { + .addon-mod-chat-notice { + margin-top: 10px; + margin-bottom: 10px; + } + .addon-mod-chat-message { + align-items: flex-start; + } +} diff --git a/src/addon/mod/chat/pages/session-messages/session-messages.ts b/src/addon/mod/chat/pages/session-messages/session-messages.ts new file mode 100644 index 000000000..91fe8874b --- /dev/null +++ b/src/addon/mod/chat/pages/session-messages/session-messages.ts @@ -0,0 +1,95 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 } from '@angular/core'; +import { IonicPage, NavParams } from 'ionic-angular'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { AddonModChatProvider } from '../../providers/chat'; +import * as moment from 'moment'; + +/** + * Page that displays list of chat session messages. + */ +@IonicPage({ segment: 'addon-mod-chat-session-messages' }) +@Component({ + selector: 'page-addon-mod-chat-session-messages', + templateUrl: 'session-messages.html', +}) +export class AddonModChatSessionMessagesPage { + + protected courseId: number; + protected chatId: number; + protected sessionStart: number; + protected sessionEnd: number; + protected groupId: number; + protected loaded = false; + protected messages = []; + + constructor(navParams: NavParams, private domUtils: CoreDomUtilsProvider, private chatProvider: AddonModChatProvider) { + this.courseId = navParams.get('courseId'); + this.chatId = navParams.get('chatId'); + this.groupId = navParams.get('groupId'); + this.sessionStart = navParams.get('sessionStart'); + this.sessionEnd = navParams.get('sessionEnd'); + + this.fetchMessages(); + } + + /** + * Fetch session messages. + * + * @return {Promise} Promise resolved when done. + */ + protected fetchMessages(): Promise { + return this.chatProvider.getSessionMessages(this.chatId, this.sessionStart, this.sessionEnd, this.groupId) + .then((messages) => { + return this.chatProvider.getMessagesUserData(messages, this.courseId).then((messages) => { + this.messages = messages; + }); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'core.errorloadingcontent', true); + }).finally(() => { + this.loaded = true; + }); + } + + /** + * Refresh session messages. + * + * @param {any} refresher Refresher. + */ + refreshMessages(refresher: any): void { + this.chatProvider.invalidateSessionMessages(this.chatId, this.sessionStart, this.groupId).finally(() => { + this.fetchMessages().finally(() => { + refresher.complete(); + }); + }); + } + + /** + * Check if the date should be displayed between messages (when the day changes at midnight for example). + * + * @param {any} message New message object. + * @param {any} prevMessage Previous message object. + * @return {boolean} True if messages are from diferent days, false othetwise. + */ + showDate(message: any, prevMessage: any): boolean { + if (!prevMessage) { + return true; + } + + // Check if day has changed. + return !moment(message.timestamp * 1000).isSame(prevMessage.timestamp * 1000, 'day'); + } +} diff --git a/src/addon/mod/chat/pages/sessions/sessions.html b/src/addon/mod/chat/pages/sessions/sessions.html new file mode 100644 index 000000000..b78df54bc --- /dev/null +++ b/src/addon/mod/chat/pages/sessions/sessions.html @@ -0,0 +1,45 @@ + + + {{ 'addon.mod_chat.chatreport' | translate }} + + + + + + + + + + {{ 'core.groupsseparate' | translate }} + {{ 'core.groupsvisible' | translate }} + + {{groupOpt.name}} + + + + {{ 'addon.mod_chat.showincompletesessions' | translate }} + + + + +

{{ session.sessionstart * 1000 | coreFormatDate }}

+

{{ session.duration | coreDuration }}

+
+ +

+ {{ user.userfullname }} ({{ user.messagecount }}) +

+
+
+ +
+
+ + +
+
+
diff --git a/src/addon/mod/chat/pages/sessions/sessions.module.ts b/src/addon/mod/chat/pages/sessions/sessions.module.ts new file mode 100644 index 000000000..23e5cdc1e --- /dev/null +++ b/src/addon/mod/chat/pages/sessions/sessions.module.ts @@ -0,0 +1,37 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { IonicPageModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CorePipesModule } from '@pipes/pipes.module'; +import { AddonModChatComponentsModule } from '../../components/components.module'; +import { AddonModChatSessionsPage } from './sessions'; + +@NgModule({ + declarations: [ + AddonModChatSessionsPage, + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + CorePipesModule, + AddonModChatComponentsModule, + IonicPageModule.forChild(AddonModChatSessionsPage), + TranslateModule.forChild() + ], +}) +export class AddonModChatSessionsPageModule {} diff --git a/src/addon/mod/chat/pages/sessions/sessions.scss b/src/addon/mod/chat/pages/sessions/sessions.scss new file mode 100644 index 000000000..066605cdc --- /dev/null +++ b/src/addon/mod/chat/pages/sessions/sessions.scss @@ -0,0 +1,8 @@ +ion-app.app-root page-addon-mod-chat-sessions { + .addon-mod-chat-session-show-more .card-content{ + padding-bottom: 0; + } + .addon-mod-chat-session-selected { + border-top: 5px solid $core-splitview-selected; + } +} diff --git a/src/addon/mod/chat/pages/sessions/sessions.ts b/src/addon/mod/chat/pages/sessions/sessions.ts new file mode 100644 index 000000000..35f26cb33 --- /dev/null +++ b/src/addon/mod/chat/pages/sessions/sessions.ts @@ -0,0 +1,165 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 } from '@angular/core'; +import { IonicPage, NavParams } from 'ionic-angular'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreSplitViewComponent } from '@components/split-view/split-view'; +import { CoreUserProvider } from '@core/user/providers/user'; +import { CoreGroupsProvider, CoreGroupInfo } from '@providers/groups'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { AddonModChatProvider } from '../../providers/chat'; + +/** + * Page that displays list of chat sessions. + */ +@IonicPage({ segment: 'addon-mod-chat-sessions' }) +@Component({ + selector: 'page-addon-mod-chat-sessions', + templateUrl: 'sessions.html', +}) +export class AddonModChatSessionsPage { + + @ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent; + + protected courseId: number; + protected cmId: number; + protected chatId: number; + protected loaded = false; + protected showAll = false; + protected groupId = 0; + protected groupInfo: CoreGroupInfo; + protected sessions = []; + protected selectedSessionStart: number; + protected selectedSessionGroupId: number; + + constructor(navParams: NavParams, private chatProvider: AddonModChatProvider, private domUtils: CoreDomUtilsProvider, + private userProvider: CoreUserProvider, private groupsProvider: CoreGroupsProvider, + private translate: TranslateService, private utils: CoreUtilsProvider) { + this.courseId = navParams.get('courseId'); + this.cmId = navParams.get('cmId'); + this.chatId = navParams.get('chatId'); + + this.fetchSessions().then(() => { + if (this.splitviewCtrl.isOn() && this.sessions.length > 0) { + this.openSession(this.sessions[0]); + } + }); + } + + /** + * Fetch chat sessions. + * + * @param {number} [showLoading] Display a loading modal. + * @return {Promise} Promise resolved when done. + */ + fetchSessions(showLoading?: boolean): Promise { + const modal = showLoading ? this.domUtils.showModalLoading() : null; + + return this.groupsProvider.getActivityGroupInfo(this.cmId, false).then((groupInfo) => { + this.groupInfo = groupInfo; + + if (groupInfo.groups && groupInfo.groups.length > 0) { + if (!groupInfo.groups.find((group) => group.id === this.groupId)) { + this.groupId = groupInfo.groups[0].id; + } + } else { + this.groupId = 0; + } + + return this.chatProvider.getSessions(this.chatId, this.groupId, this.showAll); + }).then((sessions) => { + // Fetch user profiles. + const promises = []; + + sessions.forEach((session) => { + session.duration = session.sessionend - session.sessionstart; + session.sessionusers.forEach((sessionUser) => { + if (!sessionUser.userfullname) { + // The WS does not return the user name, fetch user profile. + promises.push(this.userProvider.getProfile(sessionUser.userid, this.courseId, true).then((user) => { + sessionUser.userfullname = user.fullname; + }).catch(() => { + // Error getting profile, most probably the user is deleted. + sessionUser.userfullname = this.translate.instant('core.deleteduser') + ' ' + sessionUser.userid; + })); + } + }); + + // If session has more than 4 users we display a "Show more" link. + session.allsessionusers = session.sessionusers; + if (session.sessionusers.length > 4) { + session.sessionusers = session.allsessionusers.slice(0, 3); + } + }); + + return Promise.all(promises).then(() => { + this.sessions = sessions; + }); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'core.errorloadingcontent', true); + }).finally(() => { + this.loaded = true; + modal && modal.dismiss(); + }); + } + + /** + * Refresh chat sessions. + * + * @param {any} refresher Refresher. + */ + refreshSessions(refresher: any): void { + const promises = [ + this.groupsProvider.invalidateActivityGroupInfo(this.cmId), + this.chatProvider.invalidateSessions(this.chatId, this.groupId, this.showAll) + ]; + + this.utils.allPromises(promises).finally(() => { + this.fetchSessions().finally(() => { + refresher.complete(); + }); + }); + } + + /** + * Navigate to a session. + * + * @param {any} session Chat session. + */ + openSession(session: any): void { + this.selectedSessionStart = session.sessionstart; + this.selectedSessionGroupId = this.groupId; + const params = { + courseId: this.courseId, + chatId: this.chatId, + groupId: this.groupId, + sessionStart: session.sessionstart, + sessionEnd: session.sessionend + }; + this.splitviewCtrl.push('AddonModChatSessionMessagesPage', params); + } + + /** + * Show more session users. + * + * @param {any} session Chat session. + * @param {Event} $event The event. + */ + showMoreUsers(session: any, $event: Event): void { + session.sessionusers = session.allsessionusers; + $event.stopPropagation(); + } +} diff --git a/src/addon/mod/chat/providers/chat.ts b/src/addon/mod/chat/providers/chat.ts index 1138502da..f12107dc4 100644 --- a/src/addon/mod/chat/providers/chat.ts +++ b/src/addon/mod/chat/providers/chat.ts @@ -13,9 +13,12 @@ // limitations under the License. import { Injectable } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; import { CoreSitesProvider } from '@providers/sites'; import { CoreUserProvider } from '@core/user/providers/user'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreSiteWSPreSets } from '@classes/site'; /** * Service that provides some features for chats. @@ -25,34 +28,38 @@ export class AddonModChatProvider { static COMPONENT = 'mmaModChat'; static POLL_INTERVAL = 4000; + protected ROOT_CACHE_KEY = 'AddonModChat:'; + constructor(private sitesProvider: CoreSitesProvider, private userProvider: CoreUserProvider, - private logHelper: CoreCourseLogHelperProvider) {} + private logHelper: CoreCourseLogHelperProvider, protected utils: CoreUtilsProvider, private translate: TranslateService) {} /** * Get a chat. * - * @param {number} courseId Course ID. - * @param {number} cmId Course module ID. - * @param {boolean} [refresh=false] True when we should not get the value from the cache. + * @param {number} courseId Course ID. + * @param {number} cmId Course module ID. + * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when the chat is retrieved. */ - getChat(courseId: number, cmId: number, refresh: boolean = false): Promise { - const params = { - courseids: [courseId] - }; - const preSets = { - getFromCache: refresh ? false : undefined, - }; + getChat(courseId: number, cmId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + courseids: [courseId] + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getChatsCacheKey(courseId) + }; - return this.sitesProvider.getCurrentSite().read('mod_chat_get_chats_by_courses', params, preSets).then((response) => { - if (response.chats) { - const chat = response.chats.find((chat) => chat.coursemodule == cmId); - if (chat) { - return chat; + return site.read('mod_chat_get_chats_by_courses', params, preSets).then((response) => { + if (response.chats) { + const chat = response.chats.find((chat) => chat.coursemodule == cmId); + if (chat) { + return chat; + } } - } - return Promise.reject(null); + return Promise.reject(null); + }); }); } @@ -146,8 +153,8 @@ export class AddonModChatProvider { message.userfullname = user.fullname; message.userprofileimageurl = user.profileimageurl; }).catch(() => { - // Error getting profile. Set default data. - message.userfullname = message.userid; + // Error getting profile, most probably the user is deleted. + message.userfullname = this.translate.instant('core.deleteduser') + ' ' + message.userid; }); }); @@ -172,4 +179,210 @@ export class AddonModChatProvider { return this.sitesProvider.getCurrentSite().read('mod_chat_get_chat_users', params, preSets); } + + /** + * Return whether WS for passed sessions are available. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with a boolean. + */ + areSessionsAvailable(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.wsAvailable('mod_chat_get_sessions') && site.wsAvailable('mod_chat_get_session_messages'); + }); + } + + /** + * Get chat sessions. + * + * @param {number} chatId Chat ID. + * @param {number} [groupId=0] Group ID, 0 means that the function will determine the user group. + * @param {boolean} [showAll=false] Whether to include incomplete sessions or not. + * @param {boolean} [ignoreCache=false] True if it should ignore cached data (it will always fail in offline or server down). + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the list of sessions. + * @since 3.5 + */ + getSessions(chatId: number, groupId: number = 0, showAll: boolean = false, ignoreCache: boolean = false, siteId?: string): + Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + chatid: chatId, + groupid: groupId, + showall: showAll ? 1 : 0 + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getSessionsCacheKey(chatId, groupId, showAll), + }; + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + + return site.read('mod_chat_get_sessions', params, preSets).then((response) => { + if (!response || !response.sessions) { + return Promise.reject(null); + } + + return response.sessions; + }); + }); + } + + /** + * Get chat session messages. + * + * @param {number} chatId Chat ID. + * @param {number} sessionStart Session start time. + * @param {number} sessionEnd Session end time. + * @param {number} [groupId=0] Group ID, 0 means that the function will determine the user group. + * @param {boolean} [ignoreCache=false] True if it should ignore cached data (it will always fail in offline or server down). + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the list of messages. + * @since 3.5 + */ + getSessionMessages(chatId: number, sessionStart: number, sessionEnd: number, groupId: number = 0, ignoreCache: boolean = false, + siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + chatid: chatId, + sessionstart: sessionStart, + sessionend: sessionEnd, + groupid: groupId + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getSessionMessagesCacheKey(chatId, sessionStart, groupId) + }; + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + + return site.read('mod_chat_get_session_messages', params, preSets).then((response) => { + if (!response || !response.messages) { + return Promise.reject(null); + } + + return response.messages; + }); + }); + } + + /** + * Invalidate chats. + * + * @param {number} courseId Course ID. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateChats(courseId: number): Promise { + const site = this.sitesProvider.getCurrentSite(); + + return site.invalidateWsCacheForKey(this.getChatsCacheKey(courseId)); + } + + /** + * Invalidate chat sessions. + * + * @param {number} chatId Chat ID. + * @param {number} [groupId=0] Group ID, 0 means that the function will determine the user group. + * @param {boolean} [showAll=false] Whether to include incomplete sessions or not. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateSessions(chatId: number, groupId: number = 0, showAll: boolean = false): Promise { + const site = this.sitesProvider.getCurrentSite(); + + return site.invalidateWsCacheForKey(this.getSessionsCacheKey(chatId, groupId, showAll)); + } + + /** + * Invalidate all chat sessions. + * + * @param {number} chatId Chat ID. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateAllSessions(chatId: number): Promise { + const site = this.sitesProvider.getCurrentSite(); + + return site.invalidateWsCacheForKeyStartingWith(this.getSessionsCacheKeyPrefix(chatId)); + } + + /** + * Invalidate chat session messages. + * + * @param {number} chatId Chat ID. + * @param {number} sessionStart Session start time. + * @param {number} [groupId=0] Group ID, 0 means that the function will determine the user group. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateSessionMessages(chatId: number, sessionStart: number, groupId: number = 0): Promise { + const site = this.sitesProvider.getCurrentSite(); + + return site.invalidateWsCacheForKey(this.getSessionMessagesCacheKey(chatId, sessionStart, groupId)); + } + + /** + * Invalidate all chat session messages. + * + * @param {number} chatId Chat ID. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateAllSessionMessages(chatId: number): Promise { + const site = this.sitesProvider.getCurrentSite(); + + return site.invalidateWsCacheForKeyStartingWith(this.getSessionMessagesCacheKeyPrefix(chatId)); + } + + /** + * Get cache key for chats WS call. + * + * @param {number} courseId Course ID. + * @return {string} Cache key. + */ + protected getChatsCacheKey(courseId: number): string { + return this.ROOT_CACHE_KEY + 'chats:' + courseId; + } + + /** + * Get cache key for sessions WS call. + * + * @param {number} chatId Chat ID. + * @param {number} groupId Goup ID, 0 means that the function will determine the user group. + * @param {boolean} showAll Whether to include incomplete sessions or not. + * @return {string} 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 {number} chatId Chat ID. + * @return {string} Cache key prefix. + */ + protected getSessionsCacheKeyPrefix(chatId: number): string { + return this.ROOT_CACHE_KEY + 'sessions:' + chatId + ':'; + } + + /** + * Get cache key for session messages WS call. + * + * @param {number} chatId Chat ID. + * @param {number} sessionStart Session start time. + * @param {number} groupId Group ID, 0 means that the function will determine the user group. + * @return {string} 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 {number} chatId Chat ID. + * @return {string} Cache key prefix. + */ + protected getSessionMessagesCacheKeyPrefix(chatId: number): string { + return this.ROOT_CACHE_KEY + 'sessionsMessages:' + chatId + ':'; + } } diff --git a/src/addon/mod/chat/providers/module-handler.ts b/src/addon/mod/chat/providers/module-handler.ts index 4ef57c7b3..f85c2be5e 100644 --- a/src/addon/mod/chat/providers/module-handler.ts +++ b/src/addon/mod/chat/providers/module-handler.ts @@ -18,6 +18,7 @@ import { AddonModChatIndexComponent } from '../components/index/index'; import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@core/course/providers/module-delegate'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreConstants } from '@core/constants'; +import { AddonModChatProvider } from './chat'; /** * Handler to support chat modules. @@ -38,7 +39,7 @@ export class AddonModChatModuleHandler implements CoreCourseModuleHandler { [CoreConstants.FEATURE_SHOW_DESCRIPTION]: true }; - constructor(private courseProvider: CoreCourseProvider) { } + constructor(private courseProvider: CoreCourseProvider, private chatProvider: AddonModChatProvider) { } /** * Check if the handler is enabled on a site level. @@ -58,7 +59,7 @@ export class AddonModChatModuleHandler implements CoreCourseModuleHandler { * @return {CoreCourseModuleHandlerData} Data to render the module. */ getData(module: any, courseId: number, sectionId: number): CoreCourseModuleHandlerData { - return { + const data: CoreCourseModuleHandlerData = { icon: this.courseProvider.getModuleIconSrc(this.modName, module.modicon), title: module.name, class: 'addon-mod_chat-handler', @@ -70,6 +71,12 @@ export class AddonModChatModuleHandler implements CoreCourseModuleHandler { navCtrl.push('AddonModChatIndexPage', pageParams, options); } }; + + this.chatProvider.areSessionsAvailable().then((available) => { + data.showDownloadButton = available; + }); + + return data; } /** diff --git a/src/addon/mod/chat/providers/prefetch-handler.ts b/src/addon/mod/chat/providers/prefetch-handler.ts new file mode 100644 index 000000000..c9f54a62a --- /dev/null +++ b/src/addon/mod/chat/providers/prefetch-handler.ts @@ -0,0 +1,185 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { TranslateService } from '@ngx-translate/core'; +import { CoreAppProvider } from '@providers/app'; +import { CoreFilepoolProvider } from '@providers/filepool'; +import { CoreGroupsProvider, CoreGroupInfo } from '@providers/groups'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreCourseActivityPrefetchHandlerBase } from '@core/course/classes/activity-prefetch-handler'; +import { CoreUserProvider } from '@core/user/providers/user'; +import { AddonModChatProvider } from './chat'; + +/** + * Handler to prefetch chats. + */ +@Injectable() +export class AddonModChatPrefetchHandler extends CoreCourseActivityPrefetchHandlerBase { + name = 'AddonModChat'; + modName = 'chat'; + component = AddonModChatProvider.COMPONENT; + + constructor(translate: TranslateService, + appProvider: CoreAppProvider, + utils: CoreUtilsProvider, + courseProvider: CoreCourseProvider, + filepoolProvider: CoreFilepoolProvider, + sitesProvider: CoreSitesProvider, + domUtils: CoreDomUtilsProvider, + private groupsProvider: CoreGroupsProvider, + private userProvider: CoreUserProvider, + private chatProvider: AddonModChatProvider) { + + super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils); + } + + /** + * Whether or not the handler is enabled on a site level. + * + * @return {boolean|Promise} A boolean, or a promise resolved with a boolean, indicating if the handler is enabled. + */ + isEnabled(): boolean | Promise { + return this.chatProvider.areSessionsAvailable(); + } + + /** + * Invalidate the prefetched content. + * + * @param {number} moduleId The module ID. + * @param {number} courseId The course ID the module belongs to. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateContent(moduleId: number, courseId: number): Promise { + return this.chatProvider.getChat(courseId, moduleId).then((chat) => { + const promises = [ + this.chatProvider.invalidateAllSessions(chat.id), + this.chatProvider.invalidateAllSessionMessages(chat.id) + ]; + + return this.utils.allPromises(promises); + }); + } + + /** + * Invalidate WS calls needed to determine module status (usually, to check if module is downloadable). + * It doesn't need to invalidate check updates. It should NOT invalidate files nor all the prefetched data. + * + * @param {any} module Module. + * @param {number} courseId Course ID the module belongs to. + * @return {Promise} Promise resolved when invalidated. + */ + invalidateModule(module: any, courseId: number): Promise { + const promises = [ + this.chatProvider.invalidateChats(courseId), + this.courseProvider.invalidateModule(module.id) + ]; + + return this.utils.allPromises(promises); + } + + /** + * Prefetch a module. + * + * @param {any} module Module. + * @param {number} courseId Course ID the module belongs to. + * @param {boolean} [single] True if we're downloading a single module, false if we're downloading a whole section. + * @param {string} [dirPath] Path of the directory where to store all the content files. + * @return {Promise} Promise resolved when done. + */ + prefetch(module: any, courseId?: number, single?: boolean, dirPath?: string): Promise { + return this.prefetchPackage(module, courseId, single, this.prefetchChat.bind(this)); + } + + /** + * Prefetch a chat. + * + * @param {any} module The module object returned by WS. + * @param {number} courseId Course ID the module belongs to. + * @param {boolean} single True if we're downloading a single module, false if we're downloading a whole section. + * @param {string} siteId Site ID. + * @return {Promise} Promise resolved when done. + */ + protected prefetchChat(module: any, courseId: number, single: boolean, siteId: string): Promise { + // Prefetch chat and group info. + const promises = [ + this.chatProvider.getChat(courseId, module.id, siteId), + this.groupsProvider.getActivityGroupInfo(module.id, false, undefined, siteId) + ]; + + return Promise.all(promises).then(([chat, groupInfo]: [any, CoreGroupInfo]) => { + const promises = []; + + 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.chatProvider.getSessions(chat.id, groupId, false, true, siteId).catch((error) => { + // Ignore group error. + if (error.errorcode != 'notingroup') { + return Promise.reject(error); + } + })); + + // Prefetch all sessions. + promises.push(this.chatProvider.getSessions(chat.id, groupId, true, true, siteId).then((sessions) => { + const promises = sessions.map((session) => this.prefetchSession(chat.id, session, 0, courseId, siteId)); + + return Promise.all(promises); + }).catch((error) => { + // Ignore group error. + if (error.errorcode != 'notingroup') { + return Promise.reject(error); + } + })); + }); + + return Promise.all(promises); + }); + } + + /** + * Prefetch chat session messages and user profiles. + * + * @param {number} chatId Chat ID. + * @param {any} session Session object. + * @param {number} groupId Group ID. + * @param {number} courseId Course ID the module belongs to. + * @param {string} siteId Site ID. + * @return {Promise} Promise resolved when done. + */ + protected prefetchSession(chatId: number, session: any, groupId: number, courseId: number, siteId: string): Promise { + return this.chatProvider.getSessionMessages(chatId, session.sessionstart, session.sessionend, groupId, true, siteId) + .then((messages) => { + const users = {}; + session.sessionusers.forEach((user) => { + users[user.userid] = true; + }); + messages.forEach((message) => { + users[message.userid] = true; + }); + const userIds = Object.keys(users).map(Number); + + return this.userProvider.prefetchProfiles(userIds, courseId, siteId).catch(() => { + // Ignore errors, some users might not exist. + }); + }); + } +} diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index a82101823..0731749b0 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -345,6 +345,7 @@ "addon.mod_book.errorchapter": "Error reading chapter of book.", "addon.mod_book.modulenameplural": "Books", "addon.mod_chat.beep": "Beep", + "addon.mod_chat.chatreport": "Chat sessions", "addon.mod_chat.currentusers": "Current users", "addon.mod_chat.enterchat": "Click here to enter the chat now", "addon.mod_chat.entermessage": "Enter your message", @@ -356,12 +357,16 @@ "addon.mod_chat.messagebeepsyou": "{{$a}} has just beeped you!", "addon.mod_chat.messageenter": "{{$a}} has just entered this chat", "addon.mod_chat.messageexit": "{{$a}} has left this chat", + "addon.mod_chat.messages": "Messages", "addon.mod_chat.modulenameplural": "Chats", "addon.mod_chat.mustbeonlinetosendmessages": "You must be online to send messages.", "addon.mod_chat.nomessages": "No messages yet", + "addon.mod_chat.nosessionsfound": "No sessions found", "addon.mod_chat.send": "Send", "addon.mod_chat.sessionstart": "The next chat session will start on {{$a.date}}, ({{$a.fromnow}} from now)", + "addon.mod_chat.showincompletesessions": "Show incomplete sessions", "addon.mod_chat.talk": "Talk", + "addon.mod_chat.viewreport": "View past chat sessions", "addon.mod_choice.cannotsubmit": "Sorry, there was a problem submitting your choice. Please try again.", "addon.mod_choice.choiceoptions": "Choice options", "addon.mod_choice.errorgetchoice": "Error getting choice data.", @@ -1294,6 +1299,7 @@ "core.defaultvalue": "Default ({{$a}})", "core.delete": "Delete", "core.deletedoffline": "Deleted offline", + "core.deleteduser": "Deleted user", "core.deleting": "Deleting", "core.description": "Description", "core.dfdaymonthyear": "MM-DD-YYYY", @@ -1544,6 +1550,7 @@ "core.noresults": "No results", "core.notapplicable": "n/a", "core.notice": "Notice", + "core.notingroup": "Sorry, but you need to be part of a group to see this page.", "core.notsent": "Not sent", "core.now": "now", "core.numwords": "{{$a}} words", diff --git a/src/classes/site.ts b/src/classes/site.ts index 36fd3d598..9776bac47 100644 --- a/src/classes/site.ts +++ b/src/classes/site.ts @@ -632,10 +632,13 @@ export class CoreSite { error.message = this.translate.instant('core.unicodenotsupported'); return Promise.reject(error); - } else if (error.exception === 'required_capability_exception' || error.errorcode === 'nopermission') { + } else if (error.exception === 'required_capability_exception' || error.errorcode === 'nopermission' || + error.errorcode === 'notingroup') { + // Translate error messages with missing strings. if (error.message === 'error/nopermission') { - // This error message is returned by some web services but the string does not exist. error.message = this.translate.instant('core.nopermissionerror'); + } else if (error.message === 'error/notingroup') { + error.message = this.translate.instant('core.notingroup'); } // Save the error instead of deleting the cache entry so the same content is displayed in offline. diff --git a/src/lang/en.json b/src/lang/en.json index bc10378ca..65c423bd1 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -57,6 +57,7 @@ "defaultvalue": "Default ({{$a}})", "delete": "Delete", "deletedoffline": "Deleted offline", + "deleteduser": "Deleted user", "deleting": "Deleting", "description": "Description", "dfdaymonthyear": "MM-DD-YYYY", @@ -169,6 +170,7 @@ "noresults": "No results", "notapplicable": "n/a", "notice": "Notice", + "notingroup": "Sorry, but you need to be part of a group to see this page.", "notsent": "Not sent", "now": "now", "numwords": "{{$a}} words",