diff --git a/src/addon/mod/chat/chat.module.ts b/src/addon/mod/chat/chat.module.ts new file mode 100644 index 000000000..05076c858 --- /dev/null +++ b/src/addon/mod/chat/chat.module.ts @@ -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); + } +} diff --git a/src/addon/mod/chat/components/components.module.ts b/src/addon/mod/chat/components/components.module.ts new file mode 100644 index 000000000..8caa6f720 --- /dev/null +++ b/src/addon/mod/chat/components/components.module.ts @@ -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 {} diff --git a/src/addon/mod/chat/components/index/index.html b/src/addon/mod/chat/components/index/index.html new file mode 100644 index 000000000..2feb0cfea --- /dev/null +++ b/src/addon/mod/chat/components/index/index.html @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + {{ 'addon.mod_chat.sessionstart' | translate:{$a: chatInfo} }} + + +
+ {{ 'addon.mod_chat.enterchat' | translate }} +
+
diff --git a/src/addon/mod/chat/components/index/index.ts b/src/addon/mod/chat/components/index/index.ts new file mode 100644 index 000000000..62ed9dd85 --- /dev/null +++ b/src/addon/mod/chat/components/index/index.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, 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} Promise resolved when done. + */ + protected fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise { + 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 }); + } +} diff --git a/src/addon/mod/chat/lang/en.json b/src/addon/mod/chat/lang/en.json new file mode 100644 index 000000000..7fc12c00c --- /dev/null +++ b/src/addon/mod/chat/lang/en.json @@ -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" +} \ No newline at end of file diff --git a/src/addon/mod/chat/pages/chat/chat.html b/src/addon/mod/chat/pages/chat/chat.html new file mode 100644 index 000000000..c2a185ab2 --- /dev/null +++ b/src/addon/mod/chat/pages/chat/chat.html @@ -0,0 +1,64 @@ + + + + + + + + + + +
+
+ +
+ + {{ message.timestamp * 1000 | coreFormatDate:"dfdayweekmonth" }} + +
+ +
+ + {{ message.timestamp * 1000 | coreFormatDate:"dftimedate" }} {{ 'addon.mod_chat.messageenter' | translate:{$a: message.userfullname} }} + +
+ +
+ + {{ message.timestamp * 1000 | coreFormatDate:"dftimedate" }} {{ 'addon.mod_chat.messageexit' | translate:{$a: message.userfullname} }} + +
+ +
+ + {{ 'addon.mod_chat.messagebeepsyou' | translate:{$a: message.userfullname} }} + +
+ + + + + +

+

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

+ +

+ +
+
+ +
+

{{ 'addon.mod_chat.nomessages' | translate}}

+
+
+
+
+ + +

{{ 'addon.mod_chat.mustbeonlinetosendmessages' | translate }}

+ + +
+
diff --git a/src/addon/mod/chat/pages/chat/chat.module.ts b/src/addon/mod/chat/pages/chat/chat.module.ts new file mode 100644 index 000000000..24979c427 --- /dev/null +++ b/src/addon/mod/chat/pages/chat/chat.module.ts @@ -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 {} diff --git a/src/addon/mod/chat/pages/chat/chat.scss b/src/addon/mod/chat/pages/chat/chat.scss new file mode 100644 index 000000000..148f2fe33 --- /dev/null +++ b/src/addon/mod/chat/pages/chat/chat.scss @@ -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; + } +} diff --git a/src/addon/mod/chat/pages/chat/chat.ts b/src/addon/mod/chat/pages/chat/chat.ts new file mode 100644 index 000000000..59d4555ad --- /dev/null +++ b/src/addon/mod/chat/pages/chat/chat.ts @@ -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} Resolved when done. + */ + protected loginUser(): Promise { + return this.chatProvider.loginUser(this.chatId).then((sessionId) => { + this.sessionId = sessionId; + }); + } + + /** + * Convenience function to fetch chat messages. + * + * @return {Promise} Promise resolved when done. + */ + protected fetchMessages(): Promise { + 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} Promised resolved when done. + */ + protected fetchMessagesInterval(): Promise { + 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, '
'); + + 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 { + 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; + } +} diff --git a/src/addon/mod/chat/pages/index/index.html b/src/addon/mod/chat/pages/index/index.html new file mode 100644 index 000000000..ee27edc24 --- /dev/null +++ b/src/addon/mod/chat/pages/index/index.html @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/addon/mod/chat/pages/index/index.module.ts b/src/addon/mod/chat/pages/index/index.module.ts new file mode 100644 index 000000000..158d4bfa5 --- /dev/null +++ b/src/addon/mod/chat/pages/index/index.module.ts @@ -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 {} diff --git a/src/addon/mod/chat/pages/index/index.ts b/src/addon/mod/chat/pages/index/index.ts new file mode 100644 index 000000000..391b654fc --- /dev/null +++ b/src/addon/mod/chat/pages/index/index.ts @@ -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; + } +} diff --git a/src/addon/mod/chat/pages/users/users.html b/src/addon/mod/chat/pages/users/users.html new file mode 100644 index 000000000..ae1eaed43 --- /dev/null +++ b/src/addon/mod/chat/pages/users/users.html @@ -0,0 +1,30 @@ + + + {{ 'addon.mod_chat.currentusers' | translate }} + + + + + + + + + + + +

+ + + + +
+
+
diff --git a/src/addon/mod/chat/pages/users/users.module.ts b/src/addon/mod/chat/pages/users/users.module.ts new file mode 100644 index 000000000..169a368da --- /dev/null +++ b/src/addon/mod/chat/pages/users/users.module.ts @@ -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 {} diff --git a/src/addon/mod/chat/pages/users/users.scss b/src/addon/mod/chat/pages/users/users.scss new file mode 100644 index 000000000..20fdf6ae7 --- /dev/null +++ b/src/addon/mod/chat/pages/users/users.scss @@ -0,0 +1,5 @@ +page-addon-mod-chat-users { + .addon-mod-chat-user ion-label { + margin-bottom: 0; + } +} diff --git a/src/addon/mod/chat/pages/users/users.ts b/src/addon/mod/chat/pages/users/users.ts new file mode 100644 index 000000000..ee6a4f126 --- /dev/null +++ b/src/addon/mod/chat/pages/users/users.ts @@ -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(); + } +} diff --git a/src/addon/mod/chat/providers/chat.ts b/src/addon/mod/chat/providers/chat.ts new file mode 100644 index 000000000..d4f6e8f3e --- /dev/null +++ b/src/addon/mod/chat/providers/chat.ts @@ -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} 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, + }; + + 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} Promise resolved when the WS is executed. + */ + loginUser(chatId: number): Promise { + 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} Promise resolved when the WS call is executed. + */ + logView(chatId: number): Promise { + 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} Promise resolved when the WS is executed. + */ + sendMessage(sessionId: number, message: string, beepUserId: number): Promise { + 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} Promise resolved when the WS is executed. + */ + getLatestMessages(sessionId: number, lastTime: number): Promise { + 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} Promise always resolved with the formatted messages. + */ + getMessagesUserData(messages: any[], courseId: number): Promise { + 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} Promise resolved when the WS is executed. + */ + getChatUsers(sessionId: number): Promise { + const params = { + chatsid: sessionId + }; + const preSets = { + getFromCache: false + }; + + return this.sitesProvider.getCurrentSite().read('mod_chat_get_chat_users', params, preSets); + } +} diff --git a/src/addon/mod/chat/providers/module-handler.ts b/src/addon/mod/chat/providers/module-handler.ts new file mode 100644 index 000000000..848ed156e --- /dev/null +++ b/src/addon/mod/chat/providers/module-handler.ts @@ -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; + } +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index a1d70141e..c7bce1b87 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -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, diff --git a/src/components/send-message-form/send-message-form.ts b/src/components/send-message-form/send-message-form.ts index 2342d42e5..a7d0bbcad 100644 --- a/src/components/send-message-form/send-message-form.ts +++ b/src/components/send-message-form/send-message-form.ts @@ -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; // Send data when submitting the message form. @Output() onResize: EventEmitter; // Emit when resizing the textarea. - message: string; - constructor(private utils: CoreUtilsProvider, private textUtils: CoreTextUtilsProvider) { this.onSubmit = new EventEmitter(); this.onResize = new EventEmitter();