Let’s connect our Vue.js application with the server via WebSocket, we will use Laravel Echo for real-time updates and connection.
Course content
First, we need to install Laravel-echo and Pusher-js
npm i laravel-echo pusher-js
And make sure you have added the WebSocket key and server in .env
file.
VITE_WEBSOCKETS_KEY=local
VITE_WEBSOCKETS_SERVER=127.0.0.1
Let’s create a new file src\echo.js
for the configuration and connection of the WebSocket.
import { reactive } from "vue";
import Echo from "laravel-echo"
import Pusher from "pusher-js";
window.Pusher = Pusher;
window.Echo = new Echo({
broadcaster: 'pusher',
key: import.meta.env.VITE_WEBSOCKETS_KEY,
wsHost:import.meta.env.VITE_WEBSOCKETS_SERVER,
wsPort: 6001,
cluster: "mt1",
forceTLS: false,
disableStats: true,
authEndpoint :import.meta.env.VITE_BACKEND_URL+'/broadcasting/auth',
auth:{
headers: {
Authorization: 'Bearer '+localStorage.getItem('token'),
}
},
});
The user must be authenticated to connect.
Add the new file to src\main.js to start the connection when the app runs.
//
import "./echo"
//
createApp(App).use(head).use(router).use(store).mount('#app')
Let’s create a new page for the chat but first, we need to install the moment.js package for formatting the message sent date and create the style for the chat
npm install moment --save
.min-h-content{
min-height: calc(100vh - 82px - 148px);
}
.chat-card{
min-height: 70vh;
}
.chat-online {
color: #34ce57
}
.chat-offline {
color: #e4606d
}
.chat-messages {
display: flex;
flex-direction: column;
min-height: 400px;
max-height: 500px;
overflow-y: scroll
}
.messsage-container{
min-height: 125px;
}
.chat-message-left,
.chat-message-right {
display: flex;
flex-shrink: 0
}
.chat-message-left {
margin-right: auto
}
.chat-message-right {
flex-direction: row-reverse;
margin-left: auto
}
.chat-message-left .message-box{
background-color: rgb(143, 240, 240);
}
.chat-message-right .message-box{
background-color: #88f5a1;
}
.py-3 {
padding-top: 1rem!important;
padding-bottom: 1rem!important;
}
.px-4 {
padding-right: 1.5rem!important;
padding-left: 1.5rem!important;
}
.flex-grow-0 {
flex-grow: 0!important;
}
.border-top {
border-top: 1px solid #dee2e6!important;
}
Here is the chat page src\views\Chat.vue
<template>
<main class="container min-h-content mb-3 mt-5">
<h1 class="h3 mb-3"><i class="bi bi-chat-left-dots-fill"></i> Messages</h1>
<div class="card chat-card">
<div class="row g-0">
<ChatSidebar @renderChat="renderChat" />
<ChatBox v-if="startChat" :chat_id="chatId" />
</div>
</div>
</main>
</template>
<script>
import ChatBox from "../components/ChatBox.vue"
import ChatSidebar from "../components/ChatSideBar.vue"
export default {
components:{ChatBox,ChatSidebar},
data(){
return{
chatId : null,
startChat :false
}
},
methods: {
/*
* To open a new chat
*we pass this method to ChatSidebar component
*/
renderChat(chat_id) {
this.chatId = null
this.chatId = chat_id
this.startChat =true;
console.log("chatId",chat_id);
},
},
}
</script>
As you can see the page consists of 2 components the ChatSideba
r is for the open chats and the search box and the second component is for the chat box. src\components\ChatBox.vue
We have renderChat(chat_id)
function to open and re-render ChatBox
with a new chat id. we pass this method to ChatSidebar
component.
In the chat box component, we can detect who is online in the chat room so easily because we broadcast on a presence channel from the backend. So we will create a function startWebSocket()
to subscribe to the channel and listen to the events. In our component data, we will add all the online users to user
s and if we received a new message we will push it to messages
.
When we listen to ChatMessageSent
we change the message status to “delivered ” or “seen ” after receiving it via WebSocket for the receivers, not the sender.
data(){
return{
messages:[],
users :[]
}
},
async startWebSocket(){
console.log('startWebSocket',this.chat_id)
//to subscribe to the presence channel
window.Echo.join('chat.'+this.chat_id)
.here(users => {
//getting the online users
this.users = users
})
.joining(user => {
//if a new user joins
this.users.push(user)
})
.leaving(user => {
// remove the users who go offline
this.users = this.users.filter(u => (u.id !== user.id));
}).listen('ChatMessageSent', (e) => {
// for listening to ChatMessageSent event from the server
//adding any new message
this.messages.push(e.message)
//auto scroll to last message
this.scrollToLastMessage();
//changing the message status to delivered or seen after receiving it via websocket for the other users not the sender
if (this.$store.state.id != e.message.sender.id){
let url =import.meta.env.VITE_BACKEND_URL+'/chat/message-status/'+e.message.id
axios
.get(url,
{ headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + this.$store.state.token,
}},
)
}
}).listen('ChatMessageStatus', (e) => {
// listening to ChatMessageStatus event from the server
this.messages.find(o => o.id ==e.message.id ).data.status = e.message.data.status
});
}
Below is the chat box component with everything.
<template>
<div class="col-12 col-lg-7 col-xl-9">
<div class="py-2 px-4 border-bottom d-none d-lg-block">
<div v-for="participant in chat.participants" :key="participant.id">
<div v-if="$store.state.id != participant.id" class="d-flex align-items-center py-1">
<div class="position-relative me-1">
<img src="http://localhost:5173/img/avatar-7.png" class="rounded-circle mr-1" alt="Sharon Lessman" width="40" height="40">
</div>
<div class="flex-grow-1 pl-3">
<strong>
<span>{{ participant.first_name }} {{ participant.last_name }}</span>
</strong>
<div class="small"><i class="bi bi-circle-fill" :class="users.find(o =>o.id == participant.id)?' chat-online':' chat-offline'"></i>{{ users.find(o =>o.id == participant.id)? ' Online':' Offline' }}</div>
</div>
</div>
</div>
</div>
<div class="position-relative">
<div id="chatBox" class="chat-messages p-4">
<div v-for="message in messages" :class="$store.state.id == message.sender.id? 'chat-message-right' :'chat-message-left'" ref="messsageContainers" class="pb-4" :key="message.id">
<div>
<img src="http://localhost:5173/img/avatar-7.png" class="rounded-circle mr-1" alt="Chris Wood" width="40" height="40">
</div>
<div class="flex-shrink-1 message-box rounded py-2 px-3 mx-2">
<div class="font-weight-bold mb-1">{{ message.sender.first_name }}</div>
{{ message.message }}
<div class="text-muted small text-nowrap mt-2">{{ moment(message.created_at).format("DD-MM-yy, h:m a") }} - {{ message.data.status }}</div>
</div>
</div>
</div>
</div>
<div class="flex-grow-0 py-3 px-4 border-top">
<div class="input-group">
<input type="text" v-model="message" class="form-control" placeholder="Type your message">
<button class="btn btn-primary" :class="{isSendingForm:disabled}" @click="onSubmit"><i class="bi bi-send"></i></button>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
import moment from 'moment'
export default {
props:['chat_id'],
setup() {
},
created: function () {
this.moment = moment;
},
data(){
return{
messages:[],
chat:[],
message:'',
isSendingForm:false,
users :[]
}
},
methods:{
// to auto-scroll to the new received message
scrollToLastMessage(){
this.$nextTick(() =>{
let items = this.$refs.messsageContainers;
let last = items[items.length-1];
if(items.length > 0){
last.scrollIntoView({
block: "nearest",
inline: "center",
behavior: "smooth",
alignToTop: false
});
}
})
},
//to get the chat data when we open a chat
async getData(){
let url =import.meta.env.VITE_BACKEND_URL+'/chat/get-chat-by-id/'+this.chat_id
axios
.get(url,
{ headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + this.$store.state.token,
}},
)
.then((response) => {
console.log(response.data)
this.messages = response.data.messages.data
this.chat = response.data.chat
window.Echo.leave('chat.'+this.chat_id)
this.startWebSocket()
});
},
// to send new message
async onSubmit() {
this.isSendingForm = true;
axios.post(
this.$store.state.backendUrl+'/chat/send-text-message',{message:this.message,chat_id:this.chat_id},
{
headers: {
"Content-Type": "application/json",
'Authorization': 'Bearer ' + this.$store.state.token,
}
})
.then((response) => {
console.log(response);
this.isSendingForm = false;
this.message =''
})
.catch( (error) => {
console.log(error);
/* this.ShowError=true;
this.errorMgs = error.response.data.error;*/
this.isSendingForm = false;
});
},
//to subscribe to the chat websocket channel
async startWebSocket(){
console.log('startWebSocket',this.chat_id)
window.Echo.join('chat.'+this.chat_id)
.here(users => {
this.users = users
})
.joining(user => {
this.users.push(user)
})
.leaving(user => {
this.users = this.users.filter(u => (u.id !== user.id));
}).listen('ChatMessageSent', (e) => {
// for listening to ChatMessageSent event from the server
this.messages.push(e.message)
this.scrollToLastMessage();
if (this.$store.state.id != e.message.sender.id){
let url =import.meta.env.VITE_BACKEND_URL+'/chat/message-status/'+e.message.id
axios
.get(url,
{ headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + this.$store.state.token,
}},
)
}
}).listen('ChatMessageStatus', (e) => {
// listening to ChatMessageStatus event from the server
this.messages.find(o => o.id ==e.message.id ).data.status = e.message.data.status
});
}
},
watch: {
// call the method if the chat_id changes in chat.vue
'chat_id': {
handler: 'getData',
immediate: true // runs immediately with mount() instead of calling method on mount hook
},
},
}
</script>
The sidebar consisted of a search for the user with suggestions and open chats. src\components\ChatSidebar.vue
<template>
<div class="col-12 col-lg-5 col-xl-3 border-end">
<div class="px-4 d-none d-md-block">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<div class="input-group mt-3">
<input list="browsers" v-model="searchEmail" @input="searchUsers" type="text" class="form-control" placeholder="" aria-label="Example text with button addon" aria-describedby="button-addon1">
<datalist id="browsers">
<option v-for="user in users" :key="user.email">{{ user.email }}</option>
</datalist>
<button class="btn btn-outline-secondary" :class="{isSendingForm:disabled}" type="button" @click="onSubmit" id="button-addon1">start</button>
</div>
</div>
</div>
</div>
<a v-for="chat in chats" @click="OpentChat(chat.id)" :key="chat.id" class="list-group-item list-group-item-action border-0 ps-3 py-3" >
<div class="d-flex align-items-start">
<img src="http://localhost:5173/img/avatar-7.png" class="rounded-circle me-1" alt="Vanessa Tucker" width="40" height="40">
<div class="flex-grow-1 ml-3 fw-bold">
<span v-for="participant in chat.participants" :key="participant.id">
<span v-if="$store.state.id != participant.id" >{{ participant.first_name }} {{ participant.last_name }}</span>
</span>
</div>
</div>
</a>
</div>
</template>
<script>
import axios from 'axios'
export default {
// to user the renderChat message from the parent component chat.vue
emits: ["renderChat"],
data(){
return{
chat_id:null,
chats:[],
isSendingForm:false,
users:[],
searchEmail:'',
isSendingForm:false,
}
},
methods:{
// to start open a chat in ChatBox.vue
async OpentChat(chat_id){
// disconnect the current chat channel
await window.Echo.leave('chat.'+this.chat_id)
//open the new chat
this.chat_id = chat_id
this.$emit("renderChat", chat_id);
},
// to search users by email
searchUsers() {
this.isSendingForm = true;
axios.post(
this.$store.state.backendUrl+'/chat/search-user',{email:this.searchEmail},
{
headers: {
"Content-Type": "application/json",
'Authorization': 'Bearer ' + this.$store.state.token,
}
})
.then((response) => {
console.log(response);
this.isSendingForm = false;
this.users = response.data.users
})
.catch( (error) => {
console.log(error);
this.isSendingForm = false;
});
},
// to get the user's chats to be displayed on the sidbar
getData(){
let url =import.meta.env.VITE_BACKEND_URL+'/chat/get-chats'
axios
.get(url,
{ headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + this.$store.state.token,
}},
)
.then((response) => {
console.log(response.data.chats)
this.chats = response.data.chats
});
},
// to start a chat with user
async onSubmit() {
this.isSendingForm = true;
let user = this.users.find(o => o.email === this.searchEmail);
var data= new FormData();
data.append('users[]', user.id);
data.append('isPrivate', 1);
axios.post(
this.$store.state.backendUrl+'/chat/create-chat',data,
{
headers: {
"Content-Type": "application/json",
'Authorization': 'Bearer ' + this.$store.state.token,
}
})
.then((response) => {
// to start a chat with the user
this.isSendingForm = false;
this.OpentChat(response.data.chat.id)
})
.catch( (error) => {
console.log(error);
this.isSendingForm = false;
});
},
},
mounted(){
this.getData()
}
}
</script>
We used window.Echo.leave('chat.'+this.chat_id)
in OpentChat
function to make sure that we don’t open more than one connection with a WebSocket channel at a time. We must close the connection if we navigate away or open a new chat.
If you want to know if a user is online or offline on the app, you can create a presence channel like we did with the chat.
Conclusion
This is the end of this course. this chat app can be used by companies, schools, CRM, and small groups of people. I’m not recommending Laravel for a chat application like Facebook Messager instead use Node.js as they did in Facebook. I will create another course for a Node.js chat app so stay tuned
Here is the project on GitHub, I hope that was useful for you, thank you.