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