MOBILE-2336 chat: Migrate Chat

main
Albert Gasset 2018-04-11 13:01:56 +02:00
parent 11e3cca5c1
commit c0b43405b6
20 changed files with 1142 additions and 2 deletions

View File

@ -0,0 +1,36 @@
// (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 { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate';
import { AddonModChatComponentsModule } from './components/components.module';
import { AddonModChatModuleHandler } from './providers/module-handler';
import { AddonModChatProvider } from './providers/chat';
@NgModule({
declarations: [
],
imports: [
AddonModChatComponentsModule
],
providers: [
AddonModChatProvider,
AddonModChatModuleHandler,
]
})
export class AddonModChatModule {
constructor(moduleDelegate: CoreCourseModuleDelegate, moduleHandler: AddonModChatModuleHandler) {
moduleDelegate.registerHandler(moduleHandler);
}
}

View File

@ -0,0 +1,45 @@
// (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 { CommonModule } from '@angular/common';
import { IonicModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CoreCourseComponentsModule } from '@core/course/components/components.module';
import { AddonModChatIndexComponent } from './index/index';
@NgModule({
declarations: [
AddonModChatIndexComponent
],
imports: [
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule,
CoreCourseComponentsModule
],
providers: [
],
exports: [
AddonModChatIndexComponent
],
entryComponents: [
AddonModChatIndexComponent
]
})
export class AddonModChatComponentsModule {}

View File

@ -0,0 +1,22 @@
<!-- Buttons to add to the header. -->
<core-navbar-buttons end>
<core-context-menu>
<core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" [href]="externalUrl" [iconAction]="'open'"></core-context-menu-item>
<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="loaded && !hasOffline && isOnline" [priority]="700" [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"></core-context-menu-item>
</core-context-menu>
</core-navbar-buttons>
<!-- Content. -->
<core-loading [hideUntil]="loaded" class="core-loading-center">
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId"></core-course-module-description>
<ion-card class="core-info-card" *ngIf="chatInfo">
<ion-icon item-start name="time"></ion-icon> {{ 'addon.mod_chat.sessionstart' | translate:{$a: chatInfo} }}
</ion-card>
<div padding-horizontal>
<a ion-button block color="primary" (click)="enterChat()">{{ 'addon.mod_chat.enterchat' | translate }}</a>
</div>
</core-loading>

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, Injector } from '@angular/core';
import { NavController } from 'ionic-angular';
import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component';
import { CoreTimeUtilsProvider } from '@providers/utils/time';
import { AddonModChatProvider } from '../../providers/chat';
import * as moment from 'moment';
/**
* Component that displays a chat.
*/
@Component({
selector: 'addon-mod-chat-index',
templateUrl: 'index.html',
})
export class AddonModChatIndexComponent extends CoreCourseModuleMainActivityComponent {
component = AddonModChatProvider.COMPONENT;
moduleName = 'chat';
chat: any;
chatInfo: any;
protected title: string;
constructor(injector: Injector, private chatProvider: AddonModChatProvider, private timeUtils: CoreTimeUtilsProvider,
private navCtrl: NavController) {
super(injector);
}
/**
* Component being initialized.
*/
ngOnInit(): void {
super.ngOnInit();
this.loadContent().then(() => {
this.chatProvider.logView(this.chat.id).then(() => {
this.courseProvider.checkModuleCompletion(this.courseId, this.module.completionstatus);
});
});
}
/**
* Download chat.
*
* @param {boolean} [refresh=false] If it's refreshing content.
* @param {boolean} [sync=false] If the refresh is needs syncing.
* @param {boolean} [showErrors=false] If show errors to the user of hide them.
* @return {Promise<any>} Promise resolved when done.
*/
protected fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<any> {
return this.chatProvider.getChat(this.courseId, this.module.id).then((chat) => {
this.chat = chat;
this.description = chat.intro || chat.description;
const now = this.timeUtils.timestamp();
const span = chat.chattime - now;
if (chat.chattime && chat.schedule > 0 && span > 0) {
this.chatInfo = {
date: moment(chat.chattime * 1000).format('LLL'),
fromnow: this.timeUtils.formatTime(span)
};
} else {
this.chatInfo = false;
}
this.dataRetrieved.emit(chat);
// All data obtained, now fill the context menu.
this.fillContextMenu(refresh);
});
}
/**
* Enter the chat.
*/
enterChat(): void {
const title = this.chat.name || this.moduleName;
this.navCtrl.push('AddonModChatChatPage', {chatId: this.chat.id, courseId: this.courseId, title: title });
}
}

View File

@ -0,0 +1,19 @@
{
"beep": "Beep",
"currentusers": "Current users",
"enterchat": "Click here to enter the chat now",
"entermessage": "Enter your message",
"errorwhileconnecting": "Error while connecting to the chat.",
"errorwhilegettingchatdata": "Error while getting chat data.",
"errorwhilegettingchatusers": "Error while getting chat users.",
"errorwhileretrievingmessages": "Error while retrieving messages from the server.",
"errorwhilesendingmessage": "Error while sending the message.",
"messagebeepsyou": "{{$a}} has just beeped you!",
"messageenter": "{{$a}} has just entered this chat",
"messageexit": "{{$a}} has left this chat",
"mustbeonlinetosendmessages": "You must be online to send messages.",
"nomessages": "No messages yet",
"send": "Send",
"sessionstart": "The next chat session will start on {{$a.date}}, ({{$a.fromnow}} from now)",
"talk": "Talk"
}

View File

@ -0,0 +1,64 @@
<ion-header>
<ion-navbar>
<ion-title><core-format-text [text]="title"></core-format-text></ion-title>
<ion-buttons end>
<button *ngIf="loaded" ion-button icon-only (click)="showChatUsers()">
<ion-icon name="people"></ion-icon>
</button>
</ion-buttons>
</ion-navbar>
</ion-header>
<ion-content>
<core-loading [hideUntil]="loaded">
<div aria-live="polite">
<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:"dfdayweekmonth" }}</span>
</ion-badge>
</div>
<div text-center *ngIf="message.system && message.message == 'enter'" class="addon-mod-chat-notice">
<ion-badge text-wrap color="light">
<span>{{ message.timestamp * 1000 | coreFormatDate:"dftimedate" }} {{ 'addon.mod_chat.messageenter' | translate:{$a: message.userfullname} }}</span>
</ion-badge>
</div>
<div text-center *ngIf="message.system && message.message == 'exit'" class="addon-mod-chat-notice">
<ion-badge text-wrap color="light">
<span>{{ message.timestamp * 1000 | coreFormatDate:"dftimedate" }} {{ 'addon.mod_chat.messageexit' | translate:{$a: message.userfullname} }}</span>
</ion-badge>
</div>
<div text-center *ngIf="message.message == currentUserBeep" class="addon-mod-chat-notice">
<ion-badge text-wrap color="light">
<span>{{ 'addon.mod_chat.messagebeepsyou' | translate:{$a: message.userfullname} }}</span>
</ion-badge>
</div>
<ion-item text-wrap *ngIf="!message.system && message.message.substr(0, 4) != 'beep'" class="addon-mod-chat-message">
<ion-avatar item-start>
<img [src]="message.userprofileimageurl" onError="this.src='assets/img/user-avatar.png'" core-external-content [alt]="'core.pictureof' | translate:{$a: message.userfullname}" role="presentation">
</ion-avatar>
<h2>
<p float-right>{{ message.timestamp * 1000 | coreFormatDate:"dftimedate" }}</p>
<core-format-text [text]="message.userfullname"></core-format-text>
</h2>
<core-format-text [text]="message.message" (afterRender)="last && scrollToBottom()"></core-format-text>
</ion-item>
</div>
<div text-center margin *ngIf="!messages || messages.length <= 0">
<p>{{ 'addon.mod_chat.nomessages' | translate}}</p>
</div>
</div>
</core-loading>
</ion-content>
<ion-footer color="light" class="footer-adjustable">
<ion-toolbar color="light" position="bottom">
<p text-center *ngIf="!isOnline">{{ 'addon.mod_chat.mustbeonlinetosendmessages' | translate }}</p>
<core-send-message-form *ngIf="isOnline && polling && loaded" [message]="newMessage" (onSubmit)="sendMessage($event)" [showKeyboard]="showKeyboard" [placeholder]="'addon.messages.newmessage' | translate" (onResize)="resizeContent()"></core-send-message-form>
<button *ngIf="isOnline && !polling && loaded" (click)="reconnect()" ion-button block color="light">{{ 'core.login.reconnect' | translate }}</button>
</ion-toolbar>
</ion-footer>

View File

@ -0,0 +1,35 @@
// (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 { AddonModChatChatPage } from './chat';
@NgModule({
declarations: [
AddonModChatChatPage,
],
imports: [
CoreComponentsModule,
CoreDirectivesModule,
CorePipesModule,
IonicPageModule.forChild(AddonModChatChatPage),
TranslateModule.forChild()
],
})
export class AddonModChatChatPageModule {}

View File

@ -0,0 +1,9 @@
page-addon-mod-chat-chat {
.addon-mod-chat-notice {
margin-top: 10px;
margin-bottom: 10px;
}
.addon-mod-chat-message {
align-items: flex-start;
}
}

View File

@ -0,0 +1,309 @@
// (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 { Content, IonicPage, ModalController, NavController, NavParams } from 'ionic-angular';
import { CoreAppProvider } from '@providers/app';
import { CoreLoggerProvider } from '@providers/logger';
import { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { AddonModChatProvider } from '../../providers/chat';
import { Network } from '@ionic-native/network';
import * as moment from 'moment';
/**
* Page that displays a chat session.
*/
@IonicPage({ segment: 'addon-mod-chat-chat' })
@Component({
selector: 'page-addon-mod-chat-chat',
templateUrl: 'chat.html',
})
export class AddonModChatChatPage {
@ViewChild(Content) content: Content;
loaded = false;
title: string;
messages = [];
newMessage: string;
polling: any;
isOnline: boolean;
currentUserBeep: string;
protected logger;
protected courseId: number;
protected chatId: number;
protected sessionId: number;
protected lastTime = 0;
protected oldContentHeight = 0;
protected onlineObserver: any;
protected viewDestroyed = false;
protected pollingRunning = false;
constructor(navParams: NavParams, logger: CoreLoggerProvider, network: Network, private navCtrl: NavController,
private chatProvider: AddonModChatProvider, private appProvider: CoreAppProvider, sitesProvider: CoreSitesProvider,
private modalCtrl: ModalController, private domUtils: CoreDomUtilsProvider, private textUtils: CoreTextUtilsProvider) {
this.chatId = navParams.get('chatId');
this.courseId = navParams.get('courseId');
this.title = navParams.get('title');
this.logger = logger.getInstance('AddonModChoiceChoicePage');
this.currentUserBeep = 'beep ' + sitesProvider.getCurrentSiteUserId();
this.isOnline = this.appProvider.isOnline();
this.onlineObserver = network.onchange().subscribe((online) => {
this.isOnline = this.appProvider.isOnline();
});
}
/**
* View loaded.
*/
ionViewDidLoad(): void {
this.loginUser().then(() => {
return this.fetchMessages().then(() => {
this.startPolling();
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'addon.mod_chat.errorwhileretrievingmessages', true);
this.navCtrl.pop();
});
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'addon.mod_chat.errorwhileconnecting', true);
this.navCtrl.pop();
}).finally(() => {
this.loaded = true;
});
}
/**
* Runs when the page has fully entered and is now the active page.
* This event will fire, whether it was the first load or a cached page.
*/
ionViewDidEnter(): void {
this.startPolling();
}
/**
* Runs when the page is about to leave and no longer be the active page.
*/
ionViewWillLeave(): void {
this.stopPolling();
}
/**
* Display the chat users modal.
*/
showChatUsers(): void {
const modal = this.modalCtrl.create('AddonModChatUsersPage', {sessionId: this.sessionId});
modal.onDidDismiss((data) => {
if (data && data.talkTo) {
this.newMessage = `To ${data.talkTo}: `;
}
if (data && data.beepTo) {
this.sendMessage('', data.beepTo);
}
});
modal.present();
}
/**
* Convenience function to login the user.
*
* @return {Promise<any>} Resolved when done.
*/
protected loginUser(): Promise<any> {
return this.chatProvider.loginUser(this.chatId).then((sessionId) => {
this.sessionId = sessionId;
});
}
/**
* Convenience function to fetch chat messages.
*
* @return {Promise<any>} Promise resolved when done.
*/
protected fetchMessages(): Promise<any> {
return this.chatProvider.getLatestMessages(this.sessionId, this.lastTime).then((messagesInfo) => {
this.lastTime = messagesInfo.chatnewlasttime || 0;
return this.chatProvider.getMessagesUserData(messagesInfo.messages, this.courseId).then((messages) => {
this.messages = this.messages.concat(messages);
});
});
}
/**
* Start the polling to get chat messages periodically.
*/
protected startPolling(): void {
// We already have the polling in place.
if (this.polling) {
return;
}
// Start polling.
this.polling = setInterval(() => {
this.fetchMessagesInterval().catch(() => {
// Ignore errors.
});
}, AddonModChatProvider.POLL_INTERVAL);
}
/**
* Stop polling for messages.
*/
protected stopPolling(): void {
if (this.polling) {
this.logger.debug('Cancelling polling for messages');
clearInterval(this.polling);
}
}
/**
* Convenience function to be called every certain time to fetch chat messages.
*
* @return {Promise<any>} Promised resolved when done.
*/
protected fetchMessagesInterval(): Promise<any> {
this.logger.debug('Polling for messages');
if (!this.isOnline || this.pollingRunning) {
// Obviously we cannot check for new messages when the app is offline.
return Promise.reject(null);
}
this.pollingRunning = true;
return this.fetchMessages().catch(() => {
// Try to login, it might have failed because the session expired.
return this.loginUser().then(() => {
return this.fetchMessages();
}).catch((error) => {
// Fail again. Stop polling if needed.
if (this.polling) {
clearInterval(this.polling);
this.polling = undefined;
}
this.domUtils.showErrorModalDefault(error, 'addon.mod_chat.errorwhileretrievingmessages', true);
return Promise.reject(null);
});
}).finally(() => {
this.pollingRunning = false;
});
}
/**
* 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');
}
/**
* Send a message to the chat.
*/
sendMessage(text: string, beep: number = 0): void {
if (!this.isOnline) {
// Silent error, the view should prevent this.
return;
} else if (beep === 0 && !text.trim()) {
// Silent error.
return;
}
text = this.textUtils.replaceNewLines(text, '<br>');
const modal = this.domUtils.showModalLoading('core.sending', true);
this.chatProvider.sendMessage(this.sessionId, text, beep).then(() => {
// Update messages to show the sent message.
this.fetchMessagesInterval().catch(() => {
// Ignore errors.
});
}).catch((error) => {
/* Only close the keyboard if an error happens, we want the user to be able to send multiple
messages without the keyboard being closed. */
this.appProvider.closeKeyboard();
this.domUtils.showErrorModalDefault(error, 'addon.mod_chat.errorwhilesendingmessage', true);
}).finally(() => {
modal.dismiss();
});
}
reconnect(): Promise<any> {
const modal = this.domUtils.showModalLoading();
// Call startPolling would take a while for the first execution, so we'll execute it manually to check if it works now.
return this.fetchMessagesInterval().then(() => {
// It works, start the polling again.
this.startPolling();
}).catch(() => {
// Ignore errors.
}).finally(() => {
modal.dismiss();
});
}
/**
* Scroll bottom when render has finished.
*/
scrollToBottom(): void {
// Need a timeout to leave time to the view to be rendered.
setTimeout(() => {
if (!this.viewDestroyed) {
this.content.scrollToBottom(0);
}
});
}
/**
* Content or scroll has been resized. For content, only call it if it's been added on top.
*/
resizeContent(): void {
let top = this.content.getContentDimensions().scrollTop;
this.content.resize();
// Wait for new content height to be calculated.
setTimeout(() => {
// Visible content size changed, maintain the bottom position.
if (!this.viewDestroyed && this.content && this.content.contentHeight != this.oldContentHeight) {
if (!top) {
top = this.content.getContentDimensions().scrollTop;
}
top += this.oldContentHeight - this.content.contentHeight;
this.oldContentHeight = this.content.contentHeight;
this.content.scrollTo(0, top, 0);
}
});
}
/**
* Page destroyed.
*/
ngOnDestroy(): void {
this.onlineObserver && this.onlineObserver.unsubscribe();
this.stopPolling();
this.viewDestroyed = true;
}
}

View File

@ -0,0 +1,16 @@
<ion-header>
<ion-navbar>
<ion-title><core-format-text [text]="title"></core-format-text></ion-title>
<ion-buttons end>
<!-- The buttons defined by the component will be added in here. -->
</ion-buttons>
</ion-navbar>
</ion-header>
<ion-content>
<ion-refresher [enabled]="chatComponent.loaded" (ionRefresh)="chatComponent.doRefresh($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<addon-mod-chat-index [module]="module" [courseId]="courseId" (dataRetrieved)="updateData($event)"></addon-mod-chat-index>
</ion-content>

View File

@ -0,0 +1,33 @@
// (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 { CoreDirectivesModule } from '@directives/directives.module';
import { AddonModChatComponentsModule } from '../../components/components.module';
import { AddonModChatIndexPage } from './index';
@NgModule({
declarations: [
AddonModChatIndexPage,
],
imports: [
CoreDirectivesModule,
AddonModChatComponentsModule,
IonicPageModule.forChild(AddonModChatIndexPage),
TranslateModule.forChild()
],
})
export class AddonModChatIndexPageModule {}

View File

@ -0,0 +1,48 @@
// (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 { AddonModChatIndexComponent } from '../../components/index/index';
/**
* Page that displays a chat.
*/
@IonicPage({ segment: 'addon-mod-chat-index' })
@Component({
selector: 'page-addon-mod-chat-index',
templateUrl: 'index.html',
})
export class AddonModChatIndexPage {
@ViewChild(AddonModChatIndexComponent) chatComponent: AddonModChatIndexComponent;
title: string;
module: any;
courseId: number;
constructor(navParams: NavParams) {
this.module = navParams.get('module') || {};
this.courseId = navParams.get('courseId');
this.title = this.module.name;
}
/**
* Update some data based on the chat instance.
*
* @param {any} chat Chat instance.
*/
updateData(chat: any): void {
this.title = chat.name || this.title;
}
}

View File

@ -0,0 +1,30 @@
<ion-header>
<ion-navbar>
<ion-title>{{ 'addon.mod_chat.currentusers' | translate }}</ion-title>
<ion-buttons end>
<button ion-button icon-only (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
<ion-icon name="close"></ion-icon>
</button>
</ion-buttons>
</ion-navbar>
</ion-header>
<ion-content>
<core-loading [hideUntil]="usersLoaded">
<ion-item text-wrap *ngFor="let user of users" [class.addon-mod-chat-user]="currentUserId != user.id && isOnline">
<ion-avatar item-start>
<img [src]="user.profileimageurl" onError="this.src='assets/img/user-avatar.png'" core-external-content [alt]="'core.pictureof' | translate:{$a: user.fullname}" role="presentation">
</ion-avatar>
<h2><core-format-text [text]="user.fullname"></core-format-text></h2>
<ng-container *ngIf="currentUserId != user.id && isOnline">
<button ion-button clear icon-left (click)="talkTo(user)">
<ion-icon name="chatboxes"></ion-icon>
{{ 'addon.mod_chat.talk' | translate }}
</button>
<button ion-button clear icon-left (click)="beepTo(user)">
<ion-icon name="notifications"></ion-icon>
{{ 'addon.mod_chat.beep' | translate }}
</button>
</ng-container>
</ion-item>
</core-loading>
</ion-content>

View File

@ -0,0 +1,35 @@
// (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 { AddonModChatUsersPage } from './users';
@NgModule({
declarations: [
AddonModChatUsersPage,
],
imports: [
CoreComponentsModule,
CoreDirectivesModule,
CorePipesModule,
IonicPageModule.forChild(AddonModChatUsersPage),
TranslateModule.forChild()
],
})
export class AddonModChatUsersPageModule {}

View File

@ -0,0 +1,5 @@
page-addon-mod-chat-users {
.addon-mod-chat-user ion-label {
margin-bottom: 0;
}
}

View File

@ -0,0 +1,96 @@
// (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, ViewController } from 'ionic-angular';
import { CoreAppProvider } from '@providers/app';
import { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { AddonModChatProvider } from '../../providers/chat';
import { Network } from '@ionic-native/network';
/**
* Page that displays the chat session users.
*/
@IonicPage({ segment: 'addon-mod-chat-users' })
@Component({
selector: 'page-addon-mod-chat-users',
templateUrl: 'users.html',
})
export class AddonModChatUsersPage {
users = [];
usersLoaded = false;
currentUserId: number;
isOnline: boolean;
protected sessionId: number;
protected onlineObserver: any;
constructor(navParams: NavParams, network: Network, private appProvider: CoreAppProvider,
private sitesProvider: CoreSitesProvider, private viewCtrl: ViewController,
private domUtils: CoreDomUtilsProvider, private chatProvider: AddonModChatProvider) {
this.sessionId = navParams.get('sessionId');
this.isOnline = this.appProvider.isOnline();
this.currentUserId = this.sitesProvider.getCurrentSiteUserId();
this.onlineObserver = network.onchange().subscribe((online) => {
this.isOnline = this.appProvider.isOnline();
});
}
/**
* View loaded.
*/
ionViewDidLoad(): void {
this.chatProvider.getChatUsers(this.sessionId).then((data) => {
this.users = data.users;
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'addon.mod_chat.errorwhilegettingchatusers', true);
}).finally(() => {
this.usersLoaded = true;
});
}
/**
* Close the chat users modal.
*/
closeModal(): void {
this.viewCtrl.dismiss();
}
/**
* Add "To user:".
*
* @param {any} user User object.
*/
talkTo(user: any): void {
this.viewCtrl.dismiss({talkTo: user.fullname});
}
/**
* Beep a user.
*
* @param {any} user User object.
*/
beepTo(user: any): void {
this.viewCtrl.dismiss({beepTo: user.id});
}
/**
* Page destroyed.
*/
ngOnDestroy(): void {
this.onlineObserver && this.onlineObserver.unsubscribe();
}
}

View File

@ -0,0 +1,172 @@
// (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 { CoreSitesProvider } from '@providers/sites';
import { CoreUserProvider } from '@core/user/providers/user';
/**
* Service that provides some features for chats.
*/
@Injectable()
export class AddonModChatProvider {
static COMPONENT = 'mmaModChat';
static POLL_INTERVAL = 4000;
constructor(private sitesProvider: CoreSitesProvider, private userProvider: CoreUserProvider) {}
/**
* 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.
* @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,
};
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 Promise.reject(null);
});
}
/**
* Log the user into a chat room.
*
* @param {number} chatId Chat instance ID.
* @return {Promise<any>} Promise resolved when the WS is executed.
*/
loginUser(chatId: number): Promise<any> {
const params = {
chatid: chatId
};
return this.sitesProvider.getCurrentSite().write('mod_chat_login_user', params).then((response) => {
if (response.chatsid) {
return response.chatsid;
}
return Promise.reject(null);
});
}
/**
* Report a chat as being viewed.
*
* @param {number} chatId Chat instance ID.
* @return {Promise<any>} Promise resolved when the WS call is executed.
*/
logView(chatId: number): Promise<any> {
const params = {
chatid: chatId
};
return this.sitesProvider.getCurrentSite().write('mod_chat_view_chat', params);
}
/**
* Send a message to a chat.
*
* @param {number} sessionId Chat sessiond ID.
* @param {string} message Message text.
* @param {number} beepUserId Beep user ID.
* @return {Promise<any>} Promise resolved when the WS is executed.
*/
sendMessage(sessionId: number, message: string, beepUserId: number): Promise<any> {
const params = {
chatsid: sessionId,
messagetext: message,
beepid: beepUserId
};
return this.sitesProvider.getCurrentSite().write('mod_chat_send_chat_message', params).then((response) => {
if (response.messageid) {
return response.messageid;
}
return Promise.reject(null);
});
}
/**
* Get the latest messages from a chat session.
*
* @param {number} sessionId Chat sessiond ID.
* @param {number} lastTime Last time when messages were retrieved.
* @return {Promise<any>} Promise resolved when the WS is executed.
*/
getLatestMessages(sessionId: number, lastTime: number): Promise<any> {
const params = {
chatsid: sessionId,
chatlasttime: lastTime
};
/* We use write to not use cache. It doesn't make sense to store the messages in cache
because we won't be able to retireve them if AddonModChatProvider.loginUser fails. */
return this.sitesProvider.getCurrentSite().write('mod_chat_get_chat_latest_messages', params);
}
/**
* Get user data for messages since they only have userid.
*
* @param {any[]} messages Messages to get the user data for.
* @param {number} courseId ID of the course the messages belong to.
* @return {Promise<any>} Promise always resolved with the formatted messages.
*/
getMessagesUserData(messages: any[], courseId: number): Promise<any> {
const promises = messages.map((message) => {
return this.userProvider.getProfile(message.userid, courseId, true).then((user) => {
message.userfullname = user.fullname;
message.userprofileimageurl = user.profileimageurl;
}).catch(() => {
// Error getting profile. Set default data.
message.userfullname = message.userid;
});
});
return Promise.all(promises).then(() => {
return messages;
});
}
/**
* Get the actives users of a current chat.
*
* @param {number} sessionId Chat sessiond ID.
* @return {Promise<any>} Promise resolved when the WS is executed.
*/
getChatUsers(sessionId: number): Promise<any> {
const params = {
chatsid: sessionId
};
const preSets = {
getFromCache: false
};
return this.sitesProvider.getCurrentSite().read('mod_chat_get_chat_users', params, preSets);
}
}

View File

@ -0,0 +1,70 @@
// (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 { NavController, NavOptions } from 'ionic-angular';
import { AddonModChatIndexComponent } from '../components/index/index';
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@core/course/providers/module-delegate';
import { CoreCourseProvider } from '@core/course/providers/course';
/**
* Handler to support chat modules.
*/
@Injectable()
export class AddonModChatModuleHandler implements CoreCourseModuleHandler {
name = 'AddonModChat';
modName = 'chat';
constructor(private courseProvider: CoreCourseProvider) { }
/**
* Check if the handler is enabled on a site level.
*
* @return {boolean} Whether or not the handler is enabled on a site level.
*/
isEnabled(): boolean {
return true;
}
/**
* Get the data required to display the module in the course contents view.
*
* @param {any} module The module object.
* @param {number} courseId The course ID.
* @param {number} sectionId The section ID.
* @return {CoreCourseModuleHandlerData} Data to render the module.
*/
getData(module: any, courseId: number, sectionId: number): CoreCourseModuleHandlerData {
return {
icon: this.courseProvider.getModuleIconSrc('chat'),
title: module.name,
class: 'addon-mod_chat-handler',
action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void {
navCtrl.push('AddonModChatIndexPage', {module: module, courseId: courseId}, options);
}
};
}
/**
* Get the component to render the module. This is needed to support singleactivity course format.
* The component returned must implement CoreCourseModuleMainComponent.
*
* @param {any} course The course object.
* @param {any} module The module object.
* @return {any} The component to use, undefined if not found.
*/
getMainComponent(course: any, module: any): any {
return AddonModChatIndexComponent;
}
}

View File

@ -77,6 +77,7 @@ import { AddonCompetencyModule } from '@addon/competency/competency.module';
import { AddonUserProfileFieldModule } from '@addon/userprofilefield/userprofilefield.module';
import { AddonFilesModule } from '@addon/files/files.module';
import { AddonModBookModule } from '@addon/mod/book/book.module';
import { AddonModChatModule } from '@addon/mod/chat/chat.module';
import { AddonModLabelModule } from '@addon/mod/label/label.module';
import { AddonModResourceModule } from '@addon/mod/resource/resource.module';
import { AddonModFolderModule } from '@addon/mod/folder/folder.module';
@ -170,6 +171,7 @@ export const CORE_PROVIDERS: any[] = [
AddonUserProfileFieldModule,
AddonFilesModule,
AddonModBookModule,
AddonModChatModule,
AddonModLabelModule,
AddonModResourceModule,
AddonModFolderModule,

View File

@ -31,13 +31,12 @@ import { CoreTextUtilsProvider } from '@providers/utils/text';
templateUrl: 'send-message-form.html'
})
export class CoreSendMessageFormComponent implements OnInit {
@Input() message: string; // Input text.
@Input() placeholder = ''; // Placeholder for the input area.
@Input() showKeyboard = false; // If keyboard is shown or not.
@Output() onSubmit: EventEmitter<string>; // Send data when submitting the message form.
@Output() onResize: EventEmitter<void>; // Emit when resizing the textarea.
message: string;
constructor(private utils: CoreUtilsProvider, private textUtils: CoreTextUtilsProvider) {
this.onSubmit = new EventEmitter();
this.onResize = new EventEmitter();