Merge pull request #1778 from albertgasset/MOBILE-1928

Mobile 1928
main
Juan Leyva 2019-02-25 17:41:25 +01:00 committed by GitHub
commit ac574a76f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 910 additions and 26 deletions

View File

@ -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",

View File

@ -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);
}
}

View File

@ -5,6 +5,7 @@
<core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate" (action)="expandDescription()" [iconAction]="'arrow-forward'"></core-context-menu-item>
<core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}" [iconAction]="'fa-newspaper-o'" (action)="gotoBlog($event)"></core-context-menu-item>
<core-context-menu-item *ngIf="loaded && !hasOffline && isOnline" [priority]="700" [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"></core-context-menu-item>
<core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch()" [iconAction]="prefetchStatusIcon" [closeOnClick]="false"></core-context-menu-item>
</core-context-menu>
</core-navbar-buttons>
@ -17,7 +18,8 @@
<ion-icon name="time"></ion-icon> {{ 'addon.mod_chat.sessionstart' | translate:{$a: chatInfo} }}
</ion-card>
<div padding-horizontal>
<div padding>
<a ion-button block color="primary" (click)="enterChat()">{{ 'addon.mod_chat.enterchat' | translate }}</a>
<a ion-button block color="light" margin-top *ngIf="sessionsAvailable" (click)="viewSessions()">{{ 'addon.mod_chat.viewreport' | translate }}</a>
</div>
</core-loading>

View File

@ -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});
}
}

View File

@ -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"
}

View File

@ -0,0 +1,40 @@
<ion-header>
<ion-navbar core-back-button>
<ion-title>{{ 'addon.mod_chat.messages' | translate }}</ion-title>
</ion-navbar>
</ion-header>
<ion-content>
<ion-refresher [enabled]="loaded" (ionRefresh)="refreshMessages($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="loaded">
<div *ngFor="let message of messages; index as index; last as last">
<div text-center *ngIf="showDate(messages[index], messages[index - 1])" class="addon-mod-chat-notice">
<ion-badge text-wrap color="light">
<span>{{ message.timestamp * 1000 | coreFormatDate:"strftimedayshort" }}</span>
</ion-badge>
</div>
<div text-center *ngIf="message.issystem && message.message == 'enter'" class="addon-mod-chat-notice">
<ion-badge text-wrap color="light">
<span>{{ message.timestamp * 1000 | coreFormatDate:"strftimetime" }} {{ 'addon.mod_chat.messageenter' | translate:{$a: message.userfullname} }}</span>
</ion-badge>
</div>
<div text-center *ngIf="message.issystem && message.message == 'exit'" class="addon-mod-chat-notice">
<ion-badge text-wrap color="light">
<span>{{ message.timestamp * 1000 | coreFormatDate:"strftimetime" }} {{ 'addon.mod_chat.messageexit' | translate:{$a: message.userfullname} }}</span>
</ion-badge>
</div>
<ion-item text-wrap *ngIf="!message.issystem && message.message.substr(0, 4) != 'beep'" class="addon-mod-chat-message">
<ion-avatar core-user-avatar [user]="message" item-start></ion-avatar>
<h2>
<p float-end>{{ message.timestamp * 1000 | coreFormatDate:"strftimetime" }}</p>
<core-format-text [text]="message.userfullname"></core-format-text>
</h2>
<core-format-text [text]="message.message"></core-format-text>
</ion-item>
</div>
</core-loading>
</ion-content>

View File

@ -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 {}

View File

@ -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;
}
}

View File

@ -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<any>} Promise resolved when done.
*/
protected fetchMessages(): Promise<any> {
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');
}
}

View File

@ -0,0 +1,45 @@
<ion-header>
<ion-navbar core-back-button>
<ion-title>{{ 'addon.mod_chat.chatreport' | translate }}</ion-title>
</ion-navbar>
</ion-header>
<core-split-view>
<ion-content>
<ion-refresher [enabled]="loaded" (ionRefresh)="refreshSessions($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="loaded">
<ion-item text-wrap *ngIf="groupInfo && (groupInfo.separateGroups || groupInfo.visibleGroups)">
<ion-label id="addon-chat-groupslabel" *ngIf="groupInfo.separateGroups">{{ 'core.groupsseparate' | translate }}</ion-label>
<ion-label id="addon-chat-groupslabel" *ngIf="groupInfo.visibleGroups">{{ 'core.groupsvisible' | translate }}</ion-label>
<ion-select [(ngModel)]="groupId" (ionChange)="fetchSessions(true)" aria-labelledby="addon-chat-groupslabel" interface="action-sheet">
<ion-option *ngFor="let groupOpt of groupInfo.groups" [value]="groupOpt.id">{{groupOpt.name}}</ion-option>
</ion-select>
</ion-item>
<ion-item>
<ion-label id="addon-chat-showalllabel">{{ 'addon.mod_chat.showincompletesessions' | translate }}</ion-label>
<ion-toggle [(ngModel)]="showAll" (ionChange)="fetchSessions(true)" aria-labelledby="addon-chat-showalllabel"></ion-toggle>
</ion-item>
<ion-card *ngFor="let session of sessions" (click)="openSession(session)"
[class.addon-mod-chat-session-selected]="session.sessionstart == selectedSessionStart && groupId == selectedSessionGroupId"
[class.addon-mod-chat-session-show-more]="session.sessionusers.length < session.allsessionusers.length">
<ion-item text-wrap>
<h2>{{ session.sessionstart * 1000 | coreFormatDate }}</h2>
<p *ngIf="session.duration">{{ session.duration | coreDuration }}</p>
</ion-item>
<ion-card-content>
<p *ngFor="let user of session.sessionusers">
{{ user.userfullname }} <span *ngIf="user.messagecount">({{ user.messagecount }})</span>
</p>
</ion-card-content>
<div *ngIf="session.sessionusers.length < session.allsessionusers.length">
<button ion-button clear (click)="showMoreUsers(session, $event)">
{{ 'core.showmore' | translate }}
</button>
</div>
</ion-card>
<core-empty-box *ngIf="sessions.length == 0" icon="chatbubbles" [message]="'addon.mod_chat.nosessionsfound' | translate">
</core-empty-box>
</core-loading>
</ion-content>
</core-split-view>

View File

@ -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 {}

View File

@ -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;
}
}

View File

@ -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<any>} Promise resolved when done.
*/
fetchSessions(showLoading?: boolean): Promise<any> {
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();
}
}

View File

@ -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<any>} Promise resolved when the chat is retrieved.
*/
getChat(courseId: number, cmId: number, refresh: boolean = false): Promise<any> {
const params = {
courseids: [courseId]
};
const preSets = {
getFromCache: refresh ? false : undefined,
};
getChat(courseId: number, cmId: number, siteId?: string): Promise<any> {
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<boolean>} Promise resolved with a boolean.
*/
areSessionsAvailable(siteId?: string): Promise<boolean> {
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<any[]>} 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<any[]> {
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<any[]>} 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<any[]> {
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<any>} Promise resolved when the data is invalidated.
*/
invalidateChats(courseId: number): Promise<any> {
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<any>} Promise resolved when the data is invalidated.
*/
invalidateSessions(chatId: number, groupId: number = 0, showAll: boolean = false): Promise<any> {
const site = this.sitesProvider.getCurrentSite();
return site.invalidateWsCacheForKey(this.getSessionsCacheKey(chatId, groupId, showAll));
}
/**
* Invalidate all chat sessions.
*
* @param {number} chatId Chat ID.
* @return {Promise<any>} Promise resolved when the data is invalidated.
*/
invalidateAllSessions(chatId: number): Promise<any> {
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<any>} Promise resolved when the data is invalidated.
*/
invalidateSessionMessages(chatId: number, sessionStart: number, groupId: number = 0): Promise<any> {
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<any>} Promise resolved when the data is invalidated.
*/
invalidateAllSessionMessages(chatId: number): Promise<any> {
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 + ':';
}
}

View File

@ -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;
}
/**

View File

@ -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<boolean>} A boolean, or a promise resolved with a boolean, indicating if the handler is enabled.
*/
isEnabled(): boolean | Promise<boolean> {
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<any>} Promise resolved when the data is invalidated.
*/
invalidateContent(moduleId: number, courseId: number): Promise<any> {
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<any>} Promise resolved when invalidated.
*/
invalidateModule(module: any, courseId: number): Promise<any> {
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<any>} Promise resolved when done.
*/
prefetch(module: any, courseId?: number, single?: boolean, dirPath?: string): Promise<any> {
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<any>} Promise resolved when done.
*/
protected prefetchChat(module: any, courseId: number, single: boolean, siteId: string): Promise<any> {
// 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<any>} Promise resolved when done.
*/
protected prefetchSession(chatId: number, session: any, groupId: number, courseId: number, siteId: string): Promise<any> {
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.
});
});
}
}

View File

@ -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",

View File

@ -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.

View File

@ -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",