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} }}
-
+
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",