💡 There’s a newer version of this tutorial! Stream now offers a dedicated Angular Chat SDK, paired with a new official Angular Chat App Tutorial.
The new SDK drastically simplifies the development process described below. You can still skim this post for inspiration, but please refer to the new resources linked above for consistently up-to-date information on developing in-app messaging with Angular.
In this tutorial, I’ll take you through building a live chat application with Angular 9 and Stream Chat. I’ll demonstrate how to work with channels and how to send messages in real-time between users. In addition, you’ll see how to keep track of the number of channels that a user belongs to, and also how to retrieve the message history for a channel.
Here’s how the final messaging application will function:
Prerequisites
Before you continue on with this tutorial, make sure you have Node.js and npm installed. You also need to have installed the Angular CLI package. If you’ve already installed it, make sure you update to the latest version:
$ npm install -g @angular/cli
Signing Up for Stream
Create a free Stream account or sign in to your existing account. Once you’re logged in, create a new application and grab your app access keys, which we’ll be making use of shortly:
Bootstrapping the Angular App
Run the command below to create a new Angular app with Angular CLI. When prompted to add Angular routing, hit N, and choose CSS as the preferred stylesheet format.
$ ng new chatroom && cd chatroom
Next, install the additional dependencies we’ll be making use of:
$ npm install express cors dotenv body-parser stream-chat axios @types/node
The @types/node
package is necessary to provide "type" definitions for Node.js. Update your tsconfig.app.json
file to include the "node
" "types
" definitions as shown below:
{ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": ["node"] }, "files": ["src/main.ts", "src/polyfills.ts"], "include": ["src/**/*.ts"], "exclude": ["src/test.ts", "src/**/*.spec.ts"] }
At this point, you can run ng serve
to start the development server. Open http://localhost:4200 in your browser to view the running application.
Setting Up the Server
Create a new server.js
file in your project directory and populate it with the following code:
require('dotenv').config(); const express = require('express'); const cors = require('cors'); const bodyParser = require('body-parser'); const { StreamChat } = require('stream-chat'); const app = express(); app.use(cors()); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); // initialize Stream Chat SDK const serverSideClient = new StreamChat( process.env.STREAM_API_KEY, process.env.STREAM_APP_SECRET ); app.post('/join', async (req, res) => { const { username } = req.body; const token = serverSideClient.createToken(username); try { await serverSideClient.updateUser( { id: username, name: username, }, token ); } catch (err) { console.log(err); } const admin = { id: 'admin' }; const channel = serverSideClient.channel('team', 'talkshop', { name: 'Talk Shop', created_by: admin, }); try { await channel.create(); await channel.addMembers([username, 'admin']); } catch (err) { console.log(err); } return res .status(200) .json({ user: { username }, token, api_key: process.env.STREAM_API_KEY }); }); const server = app.listen(process.env.PORT || 5500, () => { const { port } = server.address(); console.log(`Server running on PORT ${port}`); });
Our server contains only one route (/join
), which expects a username
to be included in the request body. When this happens, an authentication token
is generated for the user who will be created on the chat instance (or updated if the user already exists). By default, user tokens are valid indefinitely; you can set an expiration on a token by passing the number of seconds till expiration as the second parameter.
After this, the user is added to a channel of the team
type whose ID is set to talkshop
and the generated token is sent back to the client to enable user authentication on the frontend. You can learn more about channel types here, including how to create your own, if the defaults don’t work for you.
We need to set up some environmental variables before we can start the server. Create a new .env
file in your project root and paste in your Stream credentials, as shown below:
PORT=5500 STREAM_API_KEY= STREAM_APP_SECRET=
Now, go ahead and start your server by running node server.js
to make it available on PORT 5500
.
Building the Chat Interface
Let’s create the HTML template and styles for the application. Open up src/app/app.component.html
in your text editor and change it to look like this:
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous" /> <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css" type="text/css" rel="stylesheet" /> <!-- Toolbar --> <div class="content" role="main"> <div *ngIf="!channel" class="login"> <h2 class="title">Login to Chat</h2> <form id="login-form" (ngSubmit)="joinChat()"> <div class="form-group"> <label for="username">Username</label> <input type="text" class="form-control" id="text" name="username" placeholder="Username" [(ngModel)]="username" /> </div> <button type="submit" class="btn btn-primary">Submit</button> </form> </div> <div *ngIf="channel" class="container"> <h3 class=" text-center">Stream Messaging</h3> <div class="messaging"> <div class="inbox_msg"> <div class="inbox_people"> <div class="headind_srch"> <div class="channel_heading"> <h4>Channels</h4> </div> </div> <div class="inbox_chat"> <div class="channels" *ngFor="let channel of channelList"> <div class="chat_list"> <div class="chat_people"> <div class="chat_ib"> <h5> {{ channel.data.name }} </h5> <p> {{ channel.state.messages[ channel.state.messages.length - 1 ].text }} </p> </div> </div> </div> </div> </div> </div> <div class="mesgs"> <div class="msg_history"> <li class="message" *ngFor="let message of messages"> <div *ngIf=" message.user.id !== currentUser.me.id; then incoming_msg; else outgoing_msg " ></div> <ng-template #incoming_msg> <div class="incoming_msg"> <div class="incoming_msg_img"> <img src="https://i.imgur.com/k2PZLZa.png" alt="User avatar" /> </div> <div class="received_msg"> <div class="received_withd_msg"> <p>{{ message.text }}</p> </div> </div> </div> </ng-template> <ng-template #outgoing_msg> <div class="outgoing_msg"> <div class="sent_msg"> <p>{{ message.text }}</p> </div> </div> </ng-template> </li> </div> <div class="type_msg"> <form class="input_msg_write" (ngSubmit)="sendMessage()"> <input type="text" class="write_msg" placeholder="Type a message" name="newMessage" [(ngModel)]="newMessage" /> <button class="msg_send_btn" type="button"> <i class="fa fa-paper-plane-o" aria-hidden="true"></i> </button> </form> </div> </div> </div> </div> </div> </div>
Next, add the styles for the app to src/app/app.component.css
:
.login { position: absolute; width: 100%; max-width: 600px; top: 50%; left: 50%; transform: translate(-50%, -50%); } .container { max-width: 1170px; margin: auto; } img { max-width: 100%; } .inbox_people { background: #f8f8f8 none repeat scroll 0 0; float: left; overflow: hidden; width: 40%; border-right: 1px solid #c4c4c4; } .inbox_msg { border: 1px solid #c4c4c4; clear: both; overflow: hidden; } .top_spac { margin: 20px 0 0; } .channel_heading { float: left; width: 40%; } .srch_bar { display: inline-block; text-align: right; width: 60%; } .headind_srch { padding: 10px 29px 10px 20px; overflow: hidden; border-bottom: 1px solid #c4c4c4; } .channel_heading h4 { color: #05728f; font-size: 21px; margin: auto; } .srch_bar input { border: 1px solid #cdcdcd; border-width: 0 0 1px 0; width: 80%; padding: 2px 0 4px 6px; background: none; } .srch_bar .input-group-addon button { background: rgba(0, 0, 0, 0) none repeat scroll 0 0; border: medium none; padding: 0; color: #707070; font-size: 18px; } .srch_bar .input-group-addon { margin: 0 0 0 -27px; } .chat_ib h5 { font-size: 15px; color: #464646; margin: 0 0 8px 0; } .chat_ib h5 span { font-size: 13px; float: right; } .chat_ib p { font-size: 14px; color: #989898; margin: auto } .chat_img { float: left; width: 11%; } .chat_ib { float: left; padding: 0 0 0 15px; width: 88%; } .chat_people { overflow: hidden; clear: both; } .chat_list { border-bottom: 1px solid #c4c4c4; margin: 0; padding: 18px 16px 10px; } .inbox_chat { height: 550px; overflow-y: scroll; } .active_chat { background: #ebebeb; } .incoming_msg_img { display: inline-block; width: 6%; } .received_msg { display: inline-block; padding: 0 0 0 10px; vertical-align: top; width: 92%; } .received_withd_msg p { background: #ebebeb none repeat scroll 0 0; border-radius: 3px; color: #646464; font-size: 14px; margin: 0; padding: 5px 10px 5px 12px; width: 100%; } .time_date { color: #747474; display: block; font-size: 12px; margin: 8px 0 0; } .received_withd_msg { width: 57%; } .mesgs { float: left; padding: 30px 15px 0 25px; width: 60%; } .sent_msg p { background: #05728f none repeat scroll 0 0; border-radius: 3px; font-size: 14px; margin: 0; color: #fff; padding: 5px 10px 5px 12px; width: 100%; } .outgoing_msg { overflow: hidden; margin: 26px 0 26px; } .sent_msg { float: right; width: 46%; } .input_msg_write input { background: rgba(0, 0, 0, 0) none repeat scroll 0 0; border: medium none; color: #4c4c4c; font-size: 15px; min-height: 48px; width: 100%; } .type_msg { border-top: 1px solid #c4c4c4; position: relative; } .msg_send_btn { background: #05728f none repeat scroll 0 0; border: medium none; border-radius: 50%; color: #fff; cursor: pointer; font-size: 17px; height: 33px; position: absolute; right: 0; top: 11px; width: 33px; } .messaging { padding: 0 0 50px 0; } .msg_history { height: 516px; overflow-y: auto; } .message { list-style-type: none; }
Now, we can go ahead and write the logic for the application. Locate app.module.ts
and import FormsModule
which exports the required providers and directives for template-driven forms and makes them available in our AppComponent
:
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; @NgModule({ declarations: [AppComponent], imports: [BrowserModule, FormsModule], providers: [], bootstrap: [AppComponent], }) export class AppModule {}
Finally, update your app.component.ts
file as shown below:
import { Component } from '@angular/core'; import { StreamChat, ChannelData, Message, User } from 'stream-chat'; import axios from 'axios'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'], }) export class AppComponent { title = 'angular-chat'; channel: ChannelData; username = ''; messages: Message[] = []; newMessage = ''; channelList: ChannelData[]; chatClient: any; currentUser: User; async joinChat() { const { username } = this; try { const response = await axios.post('http://localhost:5500/join', { username, }); const { token } = response.data; const apiKey = response.data.api_key; this.chatClient = new StreamChat(apiKey); this.currentUser = await this.chatClient.setUser( { id: username, name: username, }, token ); const channel = this.chatClient.channel('team', 'talkshop'); await channel.watch(); this.channel = channel; this.messages = channel.state.messages; this.channel.on('message.new', event => { this.messages = [...this.messages, event.message]; }); const filter = { type: 'team', members: { $in: [`${this.currentUser.me.id}`] }, }; const sort = { last_message_at: -1 }; this.channelList = await this.chatClient.queryChannels(filter, sort, { watch: true, state: true, }); } catch (err) { console.log(err); return; } } async sendMessage() { if (this.newMessage.trim() === '') { return; } try { await this.channel.sendMessage({ text: this.newMessage, }); this.newMessage = ''; } catch (err) { console.log(err); } } }
At this point, a login form should be rendered on the page. It only contains a "username" field, which is enough to demonstrate how the app works, although you’d have a proper authentication flow in a real application.
Once the form is submitted, the joinChat()
method is called, which submits the username to the /join
route that we set up earlier, and receives a token
from the server, which is subsequently used to set the current user.
After this, we connect to the talkshop
channel, and listen for new messages on the channel, thanks to Stream’s event capabilities. This allows us to update the UI whenever a new message is sent to the channel. We do this by appending the new message to the messages
array, so that the new message is displayed in the chat window.
To render the list of channels that a user belongs to on the sidebar, we use the queryChannels()
method, which is documented here. It accepts query filters, which you can use on any of the built-in channel fields or any custom ones you may have defined.
New messages are sent to the room by submitting the form at the bottom of the chat window according to the code in sendMessage()
. The text input is immediately cleared by setting this.newMessage
to an empty string.
To test the application, open it in separate tabs and login using different usernames. Send a few messages using each user:
Final thoughts
That concludes this tutorial on how to build a real-time chat application in Angular!
You can now build on the knowledge gained here to create a live chatting solution that solves a real-world messaging problem. You can check out other things Stream Chat can do by viewing its extensive documentation. The complete code for this tutorial can be found in the GitHub repository.
Thanks for reading, and happy coding!