DEV Community

Cover image for How to Build a Team Messenger Site With React (Slack Clone)
Gospel Darlington
Gospel Darlington

Posted on • Originally published at cometchat.com

2 2

How to Build a Team Messenger Site With React (Slack Clone)

What you’ll be building. Demo, Git Repo Here.

Slack Clone

Introduction

Are you inspired enough as a developer? Are you starting your journey as a web developer? Or do you seek to improve your skills to the next level? If you said yes to any of the above questions, then this tutorial is for you. As a developer, you need to get your hands dirty and implement the best set of apps available in the market to get the right people interested in you. In this tutorial, we will be combining the full power of React, Firebase, and CometChat to build a slack clone that will leave you mind-blown.

Prerequisites

To follow this tutorial, you must have a basic understanding of the rudimentary principles of React. This will help you to speedily digest this tutorial.

Installing The App Dependencies

First, you need to have NodeJs installed on your machine; you can go to their website to do that.

Second, you need to also have the React-CLI installed on your computer using the command below.

   npm install -g create-react-app
Enter fullscreen mode Exit fullscreen mode

Next, create a new project with the name slack-clone.

   npx create-react-app slack-clone
Enter fullscreen mode Exit fullscreen mode

Now, install these essential dependencies for our project using the command below.

    npm install react-router-dom
    npm install @material-ui/core
    npm install @material-ui/icons
    npm install firebase
    npm install moment react-moment
    npm install moment-timezone
Enter fullscreen mode Exit fullscreen mode

Now that we're done with the installations, let's move on to building our slack-clone solution.

Installing CometChat SDK

  1. Head to CometChat Pro and create an account.
  2. From the dashboard, add a new app called "slack-clone".
  3. Select this newly added app from the list.
  4. From the Quick Start, copy the APP_ID, REGION and AUTH_KEY, which will be used later.
  5. Also, copy the REST_API_KEY from the API & Auth Key tab.
  6. Navigate to the Users tab, and delete all the default users and groups leaving it clean (very important).
  7. Create a "app.config.js" in the src directory of the project.
  8. Enter your secret keys from CometChat and Firebase below on the next heading.
  9. Run the following command to install the CometChat SDK.
    npm install @cometchat-pro/chat@2.3.0 --save
Enter fullscreen mode Exit fullscreen mode

The App Config File

The setup below spells out the format for configuring the app.config.js files for this project.

    const firebaseConfig = {
        apiKey: 'xxx-xxx-xxx-xxx-xxx-xxx-xxx-xxx',
        authDomain: 'xxx-xxx-xxx-xxx-xxx-xxx-xxx',
        databaseURL: 'xxx-xxx-xxx-xxx-xxx-xxx-xxx-xxx-xxx',
        projectId: 'xxx-xxx-xxx',
        storageBucket: 'xxx-xxx-xxx-xxx-xxx',
        messagingSenderId: 'xxx-xxx-xxx',
        appId: 'xxx-xxx-xxx-xxx-xxx-xxx-xxx-xxx',
        measurementId: 'xxx-xxx-xxx',
    },

    const cometChat = {
      APP_ID: 'xxx-xxx-xxx',
      AUTH_KEY: 'xxx-xxx-xxx-xxx-xxx-xxx-xxx-xxx',
      REST_KEY: 'xxx-xxx-xxx-xxx-xxx-xxx-xxx-xxx',
      APP_REGION: 'xx',
    }

    export { firebaseConfig, cometChat }
Enter fullscreen mode Exit fullscreen mode

Setting Up Firebase Project

Head to Firebase create a new project and activate the email and password authentication service.

To begin using Firebase, you’ll need a Gmail account. Head over to Firebase and create a new project.

Firebase Console Create Project

Firebase provides support for authentication using different providers. For example, Social Auth, phone numbers, as well as the standard email and password method. Since we’ll be using the email and password authentication method in this tutorial, we need to enable this method for the project we created in Firebase, as it is by default disabled.

Under the authentication tab for your project, click the sign-in method and you should see a list of providers Firebase currently supports.

Firebase Authentication Options

Next, click the edit icon on the email/password provider and enable it.

Firebase Enabling Authentication

Now, you need to go and register your application under your Firebase project. On the project’s overview page, select the add app option and pick web as the platform.

Youtube-Live Clone Project Page

Once you’re done registering the application, you’ll be presented with a screen containing your application credentials. Take note of the second script tag as we’ll be using it shortly in our application.

Congratulations! Now that you're done with the installations, let's do some configurations.

Configuring CometChat SDK

Inside your project structure, open the index.js & index.css files and paste the codes below.
The above codes initialize CometChat in your app before it boots up. The index.js entry file uses your CometChat API Credentials. The app.config.js file also contains your Firebase Configurations variable file. Please do not share your secret keys on Github.

* {
margin: 0;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
view raw index.css hosted with ❤ by GitHub
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import { CometChat } from '@cometchat-pro/chat'
import { cometChat } from './app.config'
const appID = cometChat.APP_ID
const region = cometChat.APP_REGION
const appSetting = new CometChat.AppSettingsBuilder()
.subscribePresenceForAllUsers()
.setRegion(region)
.build()
CometChat.init(appID, appSetting)
.then(() => {
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
)
console.log('Initialization completed successfully')
})
.catch((error) => {
console.log('Initialization failed with error:', error)
})
view raw index.js hosted with ❤ by GitHub

Configuring The Firebase File

This file is responsible for interfacing with Firebase authentication and database services. Also, it makes ready our Google authentication service provider, enabling us to sign in with Google.

import firebase from 'firebase/app';
import 'firebase/firestore';
import 'firebase/auth';
import { firebaseConfig } from './app.config'
const firebaseApp = firebase.initializeApp(firebaseConfig)
const db = firebaseApp.firestore()
const auth = firebaseApp.auth()
const provider = new firebase.auth.GoogleAuthProvider()
export { auth, provider }
export default db
view raw firebase.js hosted with ❤ by GitHub

Project Structure

The image below reveals the project structure. Make sure you see the folder arrangement before proceeding.

Slack Clone Project Structure

Now let's make the rest of the project components as seen in the image above.

The App Component

The App component is responsible for dynamically rendering our components employing the services of the Auth-Guard. The Auth-Guard ensures that only authenticated users are permitted to access our resources, thereby providing security for our application.

body {
--slack-color: #3f0e40;
overflow: hidden;
}
.app__body {
display: flex;
height: 100vh;
}
.overlay {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.7);
transition: opacity 500ms;
visibility: hidden;
opacity: 0;
display: flex;
align-items: center;
justify-content: center;
}
.overlay:target {
visibility: visible;
opacity: 1;
}
.overlay__show {
visibility: visible;
opacity: 1;
}
.popup {
margin: 70px auto;
padding: 20px;
background: #fff;
border-radius: 5px;
width: 50%;
position: relative;
transition: all 5s ease-in-out;
}
.popup h2 {
margin-top: 0;
color: #333;
font-family: Tahoma, Arial, sans-serif;
}
.popup .close {
position: absolute;
top: 20px;
right: 30px;
transition: all 200ms;
font-size: 30px;
font-weight: bold;
text-decoration: none;
color: #333;
cursor: pointer;
}
.popup .close:hover {
color: var(--slack-color);
}
.popup .content {
max-height: 30%;
overflow: auto;
margin: 20px 0;
}
#add-channel-form {
display: flex;
flex-direction: column;
}
#add-channel-form > .form-control {
margin-bottom: 10px;
padding: 10px;
color: gray;
border-radius: 9px;
border: 1px solid gray;
}
#add-channel-form > .form-btn {
border: none;
padding: 10px;
color: gray;
border-radius: 9px;
cursor: pointer;
}
#add-channel-form > .form-btn:hover {
color: white;
background-color: var(--slack-color);
transition: all 0.3s ease-in-out;
}
#add-channel-form > .form-control,
#add-channel-form > .form-btn:focus {
outline: none;
}
view raw App.css hosted with ❤ by GitHub
import './App.css'
import Header from './components/header/Header'
import Sidebar from './components/sidebar/Sidebar'
import Channel from './screens/channel/Channel'
import Login from './screens/login/Login'
import User from './screens/user/User'
import Home from './screens/home/Home'
import Add from './screens/add/Add'
import { useState, useEffect } from 'react'
import {
BrowserRouter as Router,
Switch,
Route,
Redirect,
} from 'react-router-dom'
import { auth } from './firebase'
function App() {
const [isLoggedIn, setIsLoggedIn] = useState(false)
const [isLoaded, setIsLoaded] = useState(false)
const addStructure = (Component, props) => {
return (
<>
<Header />
<main className="app__body">
<Sidebar />
<Component {...props} />
</main>
</>
)
}
const GuardedRoute = ({ component: Component, auth, ...rest }) => (
<Route
{...rest}
render={(props) =>
auth ? (
addStructure(Component, props)
) : (
<Redirect
to={{ pathname: '/login', state: { from: props.location } }}
/>
)
}
/>
)
useEffect(() => {
const data = localStorage.getItem('user')
if (data) {
setIsLoggedIn(true)
} else {
auth.onAuthStateChanged((user) => {
if (user) {
setIsLoggedIn(true)
}
})
}
setIsLoaded(true)
}, [])
if (!isLoaded) return null
return (
<div className="app">
<Router>
<Switch>
<GuardedRoute
path="/channels/:id"
auth={isLoggedIn}
component={Channel}
/>
<GuardedRoute path="/users/:id" auth={isLoggedIn} component={User} />
<GuardedRoute path="/add/channel" auth={isLoggedIn} component={Add} />
<Route path="/login">
<Login />
</Route>
<GuardedRoute path="/" auth={isLoggedIn} component={Home} />
</Switch>
</Router>
</div>
)
}
export default App
view raw App.js hosted with ❤ by GitHub

Replace everything in the App.js and App.css files with the above codes. Great, let’s move on to the next thing.

The Sub-Components

We are about to look at the various mini-components that complement the bigger components within our project. We will use the image to identify the various sub-components and what they do.

Each of the above components renders the following parts of the app. Yes, it's a well-styled react-reusable component. Let’s go ahead a spit out the codes that sponsor their individual operations.

The Header Component

Observe the amazing amount of CSS beautification within this component.

.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 0;
color: white;
background-color: #340e36;
}
.header .MuiSvgIcon-root {
opacity: 0.6;
}
.header__left {
flex: 0.3;
display: flex;
align-items: center;
margin-left: 20px;
}
.header__left > .MuiSvgIcon-root {
margin-left: auto;
margin-right: 20px;
}
.header__middle {
flex: 0.4;
background-color: #421f44;
text-align: center;
display: flex;
padding: 0 50px;
color: gray;
border: 1px solid gray;
border-radius: 6px;
}
.header__middle > input {
background-color: transparent;
border: none;
color: white;
text-align: center;
min-width: 35vw;
}
.header__middle > input:focus {
outline: none;
}
.header__right {
flex: 0.3;
display: flex;
align-items: center;
justify-content: space-between;
margin: 0 20px;
}
.header__right .MuiAvatar-root {
border-radius: 20%;
width: 30px;
height: 30px;
cursor: pointer;
}
view raw Header.css hosted with ❤ by GitHub
import './Header.css'
import { Avatar } from '@material-ui/core'
import AccessTimeIcon from '@material-ui/icons/AccessTime'
import SearchIcon from '@material-ui/icons/Search'
import HelpOutlineIcon from '@material-ui/icons/HelpOutline'
import { useState, useEffect } from 'react'
import { useHistory } from 'react-router-dom'
function Header() {
const history = useHistory()
const [user, setUser] = useState(null)
const moveToAcc = () => {
const user = JSON.parse(localStorage.getItem('user'))
history.push(`/users/${user.uid}`)
}
useEffect(() => {
const data = localStorage.getItem('user')
setUser(JSON.parse(data))
}, [])
return (
<div className="header">
<div className="header__left">
<AccessTimeIcon />
</div>
<div className="header__middle">
<SearchIcon />
<input placeholder="Search tutorial-daltonic" />
</div>
<div className="header__right">
<HelpOutlineIcon />
<Avatar
className="header__avatar"
src={user?.photoURL}
alt={user?.displayName}
onClick={moveToAcc}
/>
</div>
</div>
)
}
export default Header
view raw Header.js hosted with ❤ by GitHub

The Sidebar Component

Observe the code carefully, you will definitely respect front-end development. Also, observe that this component employs the services of the getChannel and getDirectMessages methods on the initialization of this component. These records once retrieved are passed on to the sidebarOption component which then populates the sidebar view.

.sidebar {
color: white;
background-color: var(--slack-color);
border-top: 1px solid #49274b;
max-width: 260px;
flex: 0.3;
}
.sidebar__header {
display: flex;
border: 1px solid #49274b;
padding: 13px;
padding-bottom: 10px;
}
.sidebar__header > .MuiSvgIcon-root {
padding: 8px;
color: #49274b;
font-size: 18px;
background-color: white;
border-radius: 50%;
}
.sidebar__info {
flex: 1;
}
.sidebar__info > h2 {
font-size: 15px;
font-weight: 900;
margin-bottom: 5px;
cursor: pointer;
}
.sidebar__info > h2 > a {
text-decoration: none;
color: white;
}
.sidebar__info > h3 {
display: flex;
align-items: center;
font-size: 13px;
font-weight: 400;
text-transform: capitalize;
}
.sidebar__info > h3 > .MuiSvgIcon-root {
font-size: 14px;
margin-top: 1px;
margin-right: 2px;
color: green;
}
.sidebar__options {
overflow-y: auto;
height: calc(100vh - 170px);
min-height: calc(100vh - 150px);
}
.sidebar__options > hr {
border: 1px solid #49274b;
}
.sidebar__options::-webkit-scrollbar {
width: 0.8em;
}
.sidebar__options::-webkit-scrollbar-track {
box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
}
.sidebar__options::-webkit-scrollbar-thumb {
background-color: darkgrey;
outline: none;
border-radius: 9px;
}
.sidebar__logout {
width: 100%;
padding: 10px 0;
outline: none;
border: none;
background-color: #340e36;
color: white;
cursor: pointer;
}
view raw Sidebar.css hosted with ❤ by GitHub
import './Sidebar.css'
import { useState, useEffect } from 'react'
import { auth } from '../../firebase'
import SidebarOption from '../sidebarOption/SidebarOption'
import CreateIcon from '@material-ui/icons/Create'
import FiberManualRecordIcon from '@material-ui/icons/FiberManualRecord'
import InsertCommentIcon from '@material-ui/icons/InsertComment'
import AlternateEmailIcon from '@material-ui/icons/AlternateEmail'
import MoreVertIcon from '@material-ui/icons/MoreVert'
import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown'
import LockOutlinedIcon from '@material-ui/icons/LockOutlined'
import AddIcon from '@material-ui/icons/Add'
import { CometChat } from '@cometchat-pro/chat'
import { Link, useHistory } from 'react-router-dom'
function Sidebar() {
const [channels, setChannels] = useState([])
const [user, setUser] = useState(null)
const [dms, setDms] = useState([])
const history = useHistory()
const getDirectMessages = () => {
const limit = 10
const usersRequest = new CometChat.UsersRequestBuilder()
.setLimit(limit)
.friendsOnly(true)
.build()
usersRequest
.fetchNext()
.then((userList) => setDms(userList))
.catch((error) => {
console.log('User list fetching failed with error:', error)
})
}
const getChannels = () => {
const limit = 30
const groupsRequest = new CometChat.GroupsRequestBuilder()
.setLimit(limit)
.joinedOnly(true)
.build()
groupsRequest
.fetchNext()
.then((groupList) => setChannels(groupList))
.catch((error) => {
console.log('Groups list fetching failed with error', error)
})
}
const logOut = () => {
auth
.signOut()
.then(() => {
localStorage.removeItem('user')
history.push('/login')
})
.catch((error) => console.log(error.message))
}
useEffect(() => {
const data = localStorage.getItem('user')
setUser(JSON.parse(data))
getChannels()
getDirectMessages()
}, [])
return (
<div className="sidebar">
<div className="sidebar__header">
<div className="sidebar__info">
<h2>
<Link to="/">Cometchat (e)</Link>
</h2>
<h3>
<FiberManualRecordIcon />
{user?.displayName.split(' ')[0]}
</h3>
</div>
<CreateIcon />
</div>
<div className="sidebar__options">
<SidebarOption Icon={InsertCommentIcon} title="Thread" />
<SidebarOption Icon={AlternateEmailIcon} title="Mentions & Reactions" />
<SidebarOption Icon={MoreVertIcon} title="More" />
<hr />
<SidebarOption Icon={ArrowDropDownIcon} title="Channels" />
<hr />
{channels.map((channel) =>
channel.type === 'private' ? (
<SidebarOption
Icon={LockOutlinedIcon}
title={channel.name}
id={channel.guid}
key={channel.guid}
sub="sidebarOption__sub"
/>
) : (
<SidebarOption
title={channel.name}
id={channel.guid}
key={channel.guid}
sub="sidebarOption__sub"
/>
)
)}
<SidebarOption
Icon={AddIcon}
title="Add Channel"
sub="sidebarOption__sub"
addChannelOption
/>
<hr />
<SidebarOption Icon={ArrowDropDownIcon} title="Direct Messages" />
<hr />
{dms.map((dm) => (
<SidebarOption
Icon={FiberManualRecordIcon}
title={dm.name}
id={dm.uid}
key={dm.uid}
sub="sidebarOption__sub sidebarOption__color"
user
online={dm.status === 'online' ? 'isOnline' : ''}
/>
))}
</div>
<button className="sidebar__logout" onClick={logOut}>
Logout
</button>
</div>
)
}
export default Sidebar
view raw Sidebar.js hosted with ❤ by GitHub

The SidebarOption Component

This reusable component solely functions as a navigational agent in our app. It keeps track of the user’s channel and also the online presence of a user’s friends.

.sidebarOption {
display: flex;
align-items: center;
font-size: 12px;
padding-left: 2px;
cursor: pointer;
}
.sidebarOption:hover {
background-color: #340e36;
opacity: 0.9;
}
.sidebarOption__icon {
padding: 10px;
font-size: 15px !important;
opacity: 0.6;
}
.sidebarOption__channel {
padding: 10px 0;
}
.sidebarOption__hash {
padding: 10px;
padding-left: 12px;
}
.sidebarOption > h3 {
font-weight: 400;
opacity: 0.6;
}
.sidebarOption__sub {
margin-left: 10px;
}
.isOnline > .MuiSvgIcon-root {
color: green;
}
import './SidebarOption.css'
import { useHistory } from 'react-router-dom'
function SidebarOption({
Icon,
title,
sub,
id,
addChannelOption,
user,
online,
}) {
const history = useHistory()
const selectChannel = () => {
if (id) {
if (user) {
history.push(`/users/${id}`)
} else {
history.push(`/channels/${id}`)
}
} else {
history.push(title)
}
}
const addChannel = () => {
history.push('/add/channel')
}
return (
<div
className={`sidebarOption ${online} ${sub}`}
onClick={addChannelOption ? addChannel : selectChannel}
>
{Icon && <Icon className="sidebarOption__icon" />}
{Icon ? (
<h3>{title}</h3>
) : (
<h3 className="sidebarOption__channel">
<span className="sidebarOption__hash">#</span> {title}
</h3>
)}
</div>
)
}
export default SidebarOption

The Message Component

Lastly, the message component elegantly populates the view with a given list of messages either for a one-to-many or one-on-one chat.

.message {
display: flex;
align-items: center;
margin: 10px 0;
padding: 5px 20px;
}
.message:hover {
background-color: #f3f3f3;
}
.message__left .MuiAvatar-root {
border-radius: 20%;
width: 40px;
height: 40px;
cursor: pointer;
}
.message__left {
margin-right: 10px;
}
.message__details a {
margin-right: 5px;
color: black;
text-decoration: none;
font-weight: 700;
text-transform: capitalize;
}
.message__text {
font-size: 14px;
}
.message__data {
display: flex;
align-items: center;
flex: 0.7;
}
.message__actions {
visibility: hidden;
display: flex;
align-items: center;
justify-content: flex-end;
flex: 0.3;
}
.message__actions__show {
visibility: visible;
}
.message__actions .MuiSvgIcon-root {
margin-right: 10px;
cursor: pointer;
}
view raw Message.css hosted with ❤ by GitHub
import './Message.css'
import { useState } from 'react'
import { Link } from 'react-router-dom'
import { Avatar } from '@material-ui/core'
import Moment from 'react-moment'
import 'moment-timezone'
function Message({ uid, name, avatar, message, timestamp }) {
const [hovered, setHovered] = useState(false)
const toggleHover = () => setHovered(!hovered)
// Moment.globalTimezone = 'America/Los_Angeles'
return (
<div
className="message"
onMouseEnter={toggleHover}
onMouseLeave={toggleHover}
>
<div className="message__data">
<div className="message__left">
<Avatar
className="message__avatar"
src={avatar}
alt={`${name} ${uid} - Image`}
/>
</div>
<div className="message__right">
<div className="message__details">
<Link to={`/users/${uid}`}>{name}</Link>
<small>
<Moment unix date={timestamp} format="YYYY-MM-D hh:mm A" />
</small>
</div>
<p className="message__text">{message}</p>
</div>
</div>
</div>
)
}
export default Message
view raw Message.js hosted with ❤ by GitHub

At this point, we’re done with mentioning and explaining what the sub-components do. Let’s take a step further to the bigger components.

The Login Component

The Login Component

As elegant and simple as it looks, the login component features two major operations: sign up and sign in. Behind the scene, these two methods combine the power of Firebase auth-service and CometChat user authentications.

To illustrate this process, let’s consider a user called “James”. Now, James has to click on the green button that reads, “Sign in with Google”. If it's his first time in our system, it will register him on both Firebase and CometChat and also alerting him to sign in again.

Once the initial registration is achieved, the user can click on the green button once and be allowed to use our app with his google credentials. The codes below sponsor the operations of the login component.

.login {
height: 100vh;
background-color: #f8f8f8;
display: grid;
place-items: center;
}
.login__container {
padding: 100px;
text-align: center;
background-color: white;
border-radius: 10px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
}
.login__container > img {
object-fit: contain;
height: 100px;
margin-bottom: 40px;
}
.login__container > button {
margin-top: 50px;
text-transform: inherit !important;
background-color: #0a8d48;
color: white;
border: 1px solid #0a8d48;
}
.login__container > button:hover {
background-color: transparent;
color: #0a8d48;
}
#loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid rgb(255 255 255);
border-radius: 50%;
border-top-color: #0a8d48;
animation: spin 1s ease-in-out infinite;
-webkit-animation: spin 1s ease-in-out infinite;
}
#loading:hover {
border: 3px solid #0a8d48;
border-top-color: rgb(255 255 255);
}
@keyframes spin {
to { -webkit-transform: rotate(360deg); }
}
@-webkit-keyframes spin {
to { -webkit-transform: rotate(360deg); }
}
view raw Login.css hosted with ❤ by GitHub
import './Login.css'
import { Button } from '@material-ui/core'
import { auth, provider } from '../../firebase'
import { CometChat } from '@cometchat-pro/chat'
import { cometChat } from '../../app.config'
import { useState } from 'react'
function Login() {
const [loading, setLoading] = useState(false)
const signIn = () => {
setLoading(true)
auth
.signInWithPopup(provider)
.then((res) => loginCometChat(res.user))
.catch((error) => {
setLoading(false)
alert(error.message)
})
}
const loginCometChat = (data) => {
const authKey = cometChat.AUTH_KEY
CometChat.login(data.uid, authKey)
.then((u) => {
localStorage.setItem('user', JSON.stringify(data))
window.location.href = '/'
console.log(u)
setLoading(false)
})
.catch((error) => {
if (error.code === 'ERR_UID_NOT_FOUND') {
signUpWithCometChat(data)
} else {
console.log(error)
setLoading(false)
alert(error.message)
}
})
}
const signUpWithCometChat = (data) => {
const authKey = cometChat.AUTH_KEY
const user = new CometChat.User(data.uid)
user.setName(data.displayName)
user.setAvatar(data.photoURL)
CometChat.createUser(user, authKey)
.then(() => {
setLoading(false)
alert('You are now signed up, click the button again to login')
})
.catch((error) => {
console.log(error)
setLoading(false)
alert(error.message)
})
}
return (
<div className="login">
<div className="login__container">
<img src={'/logo.png'} alt="Slack Logo" />
<h4>Sign in to CometChat</h4>
<p>cometchat.slack.com</p>
<Button onClick={signIn}>
{!loading ? 'Sign In With Google' : <div id="loading"></div>}
</Button>
</div>
</div>
)
}
export default Login
view raw Login.js hosted with ❤ by GitHub

We’re done with the authentication procedure, let’s move on to the other pages of our application.

The Home Component

The Home Component

This component provides you with a warm welcome screen, giving you a first look at the beauty of the slack-clone. The codes are given below.

.home {
flex: 0.7;
flex-grow: 1;
height: 100vh;
display: grid;
place-items: center;
}
.home__container {
padding: 0 25%;
text-align: center;
}
.home__container > button {
margin-top: 30px;
text-transform: inherit !important;
background-color: #0a8d48;
color: white;
}
.home__container > button:hover {
background-color: transparent;
color: #0a8d48;
border: 1px solid #0a8d48;
}
.home__container > img {
object-fit: contain;
height: 100px;
margin-bottom: 40px;
}
.home__container > * {
margin-bottom: 1rem;
}
.home__container > h1 {
font-family: cursive;
}
.home__container > p {
font-size: 18px;
line-height: 1.5;
}
view raw Home.css hosted with ❤ by GitHub
import './Home.css'
import { Button } from '@material-ui/core'
import { useHistory } from 'react-router-dom'
function Home() {
const history = useHistory()
const addChannel = () => {
history.push('/add/channel')
}
return (
<div className="home">
<div className="home__container">
<img src="/logo.png" alt="Slack Logo" />
<h1>Welcome to Slack</h1>
<p>
Slack brings all your team communication into one place, makes it all
instantly searchable and available wherever you go.
</p>
<p>
Our aim is to make your working life simpler, more pleasant and more
productive.
</p>
<Button onClick={addChannel}>Create Channel</Button>
</div>
</div>
)
}
export default Home
view raw Home.js hosted with ❤ by GitHub

The Add Channel Component

The Add Channel Component

This component features a simple ability to create a new channel. A user is given the option to make the channel private or public which will determine how it is represented on the sidebar.

.add {
flex: 0.7;
flex-grow: 1;
height: 100vh;
display: grid;
place-items: center;
}
.add__container {
padding: 0 25%;
text-align: center;
}
.add__container > button {
margin-top: 20px;
text-transform: inherit !important;
background-color: #0a8d48;
color: white;
border: 1px solid #0a8d48;
}
.add__container > button:hover {
background-color: transparent;
color: #0a8d48;
}
.add__container > img {
object-fit: contain;
height: 100px;
margin-bottom: 40px;
}
.add__form > input {
width: 90%;
}
.add__form > input:focus {
outline: none;
}
.add__form > select {
width: 98%;
}
.add__form {
margin: 10px 0;
}
.add__form > * {
margin-bottom: 10px;
padding: 10px;
color: gray;
border-radius: 9px;
border: 1px solid gray;
}
#loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid rgb(255 255 255);
border-radius: 50%;
border-top-color: #0a8d48;
animation: spin 1s ease-in-out infinite;
-webkit-animation: spin 1s ease-in-out infinite;
}
#loading:hover {
border: 3px solid #0a8d48;
border-top-color: rgb(255 255 255);
}
@keyframes spin {
to {
-webkit-transform: rotate(360deg);
}
}
@-webkit-keyframes spin {
to {
-webkit-transform: rotate(360deg);
}
}
view raw Add.css hosted with ❤ by GitHub
import './Add.css'
import { Button } from '@material-ui/core'
import { useState } from 'react'
import { CometChat } from '@cometchat-pro/chat'
function Add() {
const [channel, setChannel] = useState('')
const [privacy, setPrivacy] = useState('')
const [loading, setLoading] = useState(false)
const addChannel = () => {
setLoading(true)
if (channel === '' || privacy === '') {
setLoading(false)
alert('Please fill the form completely')
return null
}
cometChatCreateGroup({
channel,
privacy,
guid: generateGUID(),
})
}
const cometChatCreateGroup = (data) => {
const GUID = data.guid
const groupName = data.channel
const groupType = data.privacy
? CometChat.GROUP_TYPE.PUBLIC
: CometChat.GROUP_TYPE.PRIVATE
const password = ''
const group = new CometChat.Group(GUID, groupName, groupType, password)
CometChat.createGroup(group)
.then((group) => {
console.log('Group created successfully:', group)
resetForm()
window.location.href = `/channels/${data.guid}`
setLoading(false)
})
.catch((error) => {
console.log('Group creation failed with exception:', error)
setLoading(false)
})
}
const generateGUID = (length = 20) => {
var result = []
var characters =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
var charactersLength = characters.length
for (var i = 0; i < length; i++) {
result.push(
characters.charAt(Math.floor(Math.random() * charactersLength))
)
}
return result.join('')
}
const resetForm = () => {
setChannel('')
setChannel('')
}
return (
<div className="add">
<form className="add__container">
<img src="/logo.png" alt="Slack Logo" />
<h1>Add New Channel</h1>
<div className="add__form">
<input
name="channel"
value={channel}
placeholder="Channel Name"
onChange={(e) => setChannel(e.target.value)}
required
/>
</div>
<div className="add__form">
<select
name="privacy"
value={privacy}
onChange={(e) => setPrivacy(e.target.value === true)}
required
>
<option value={''}>Select privacy</option>
<option value={false}>Public</option>
<option value={true}>Private</option>
</select>
</div>
<Button onClick={addChannel}>
{!loading ? 'Create Channel' : <div id="loading"></div>}
</Button>
</form>
</div>
)
}
export default Add
view raw Add.js hosted with ❤ by GitHub

The Channel Component

The Channel Component

The channel component is responsible for a lot of things, including obtaining the channel details, getting the channel messages, listing the members, adding new members, and so on.

This component uses the Message sub-component to render messages on its view. It also features the ability to send new messages and view for incoming messages from other users concurrently using that channel with you. One more thing this component does is to allow users to call each other by means of a video call.

Calling functionality

It's a lot easier to disclose the codes responsible for all the actions associated with the channel component.

.channel {
flex: 0.7;
flex-grow: 1;
display: flex;
}
.channel__chat {
flex: 1;
}
.channel__details {
flex: 0.4;
border-left: 1px solid lightgray;
}
.hide__details {
display: none;
}
.channel__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px;
border-bottom: 1px solid lightgray;
}
.channel__messages {
overflow-y: auto;
height: calc(100vh - 35%);
}
.channel__channelName {
display: flex;
align-items: center;
text-transform: lowercase;
}
.channel__channelName > .MuiSvgIcon-root {
margin-left: 10px;
font-size: 16px;
}
.channel__channelName > strong {
display: flex;
align-items: center;
}
.channel__channelName > strong > .MuiSvgIcon-root {
font-size: 14px;
}
.channel__headerRight {
display: flex;
align-items: center;
}
.channel__headerRight > .MuiSvgIcon-root {
margin-left: 15px;
font-size: 16px;
cursor: pointer;
}
.channel__headerRight > .MuiSvgIcon-root:hover {
transform: scale(1.3);
transition: all 0.3s ease-in-out;
}
.channel__chatInput {
border-radius: 20px;
}
.channel__chatInput > form {
display: flex;
justify-content: center;
}
.channel__chatInput > form > input {
width: 90%;
border: 1px solid gray;
border-radius: 3px;
padding: 20px;
margin: 10px;
}
.channel__chatInput > form > input:focus {
outline: none;
}
.channel__chatInput > form > button {
display: none !important;
}
.channel__messages::-webkit-scrollbar,
.channel__detailsBody::-webkit-scrollbar {
width: 0.8em;
}
.channel__messages::-webkit-scrollbar-thumb,
.channel__detailsBody::-webkit-scrollbar-thumb {
background-color: darkgrey;
outline: none;
border-radius: 9px;
}
.channel__detailsActions {
padding: 30px;
display: flex;
justify-content: space-between;
align-items: center;
}
.channel__detailsActions > span {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.channel__detailsActions .MuiSvgIcon-root {
background-color: whitesmoke;
padding: 10px;
border-radius: 50px;
cursor: pointer;
margin-bottom: 10px;
}
.channel__detailsActions > .MuiSvgIcon-root:hover {
background-color: lightgray;
}
.channel__detailsBody {
overflow-y: auto;
height: calc(100vh - 112px);
}
.channel__detailsBody > hr {
border: none;
border-bottom: 1px solid lightgray;
}
.channel__detailsMembers {
margin: 30px;
}
.channel__detailsMembers > .available__member {
display: flex;
align-items: center;
text-decoration: none;
font-size: 15px;
font-weight: 600;
color: black;
margin: 10px 0;
}
.channel__detailsMembers > .available__member > a {
text-decoration: none;
color: black;
}
.channel__detailsMembers > .available__member > .MuiAvatar-root {
border-radius: 9px;
width: 25px;
height: 25px;
margin-right: 10px;
}
.channel__detailsMembers > .available__member > .MuiSvgIcon-root {
font-size: 14px;
margin-left: 10px;
margin-top: 3px;
cursor: pointer;
}
.channel__detailsMembers > .available__member > .MuiSvgIcon-root:last-child {
color: black;
}
.channel__detailsMembers
> .available__member
> .MuiSvgIcon-root:last-child:hover {
transform: scale(1.3);
transition: all 0.3s ease-in-out;
}
.channel__detailsMembers > h4 {
font-weight: 500;
margin-bottom: 20px;
}
.isOnline > .MuiSvgIcon-root {
color: green;
}
.channel__detailsForm {
padding: 30px;
display: flex;
justify-content: space-between;
align-items: center;
}
.channel__detailsForm > button {
text-transform: inherit !important;
background-color: #0a8d48;
color: white;
border: 1px solid #0a8d48;
line-height: 1.13;
border-radius: 9px;
}
.channel__detailsForm > button:hover {
background-color: transparent;
color: #0a8d48;
}
.channel__detailsForm > input {
padding: 5px;
border: 1px solid lightgray;
border-radius: 9px;
}
#loading {
display: inline-block;
width: 8px !important;
height: 8px !important;
border: 3px solid rgb(255 255 255);
border-radius: 50%;
border-top-color: #0a8d48;
animation: spin 1s ease-in-out infinite;
-webkit-animation: spin 1s ease-in-out infinite;
}
#loading:hover {
border: 3px solid #0a8d48;
border-top-color: rgb(255 255 255);
}
@keyframes spin {
to {
-webkit-transform: rotate(360deg);
}
}
@-webkit-keyframes spin {
to {
-webkit-transform: rotate(360deg);
}
}
.MuiButton-root.deleteBtn {
color: red;
text-transform: capitalize;
padding: 0 !important;
}
view raw Channel.css hosted with ❤ by GitHub
import './Channel.css'
import { useState, useEffect } from 'react'
import { Link, useParams, useHistory } from 'react-router-dom'
import StarBorderOutlinedIcon from '@material-ui/icons/StarBorderOutlined'
import InfoOutlinedIcon from '@material-ui/icons/InfoOutlined'
import PersonAddOutlinedIcon from '@material-ui/icons/PersonAddOutlined'
import PersonAddDisabledIcon from '@material-ui/icons/PersonAddDisabled'
import CallIcon from '@material-ui/icons/Call'
import CallEndIcon from '@material-ui/icons/CallEnd'
import FiberManualRecordIcon from '@material-ui/icons/FiberManualRecord'
import SearchIcon from '@material-ui/icons/Search'
import MoreHorizIcon from '@material-ui/icons/MoreHoriz'
import CloseIcon from '@material-ui/icons/Close'
import LockIcon from '@material-ui/icons/Lock'
import Message from '../../components/message/Message'
import { CometChat } from '@cometchat-pro/chat'
import { Avatar, Button } from '@material-ui/core'
function Channel() {
const { id } = useParams()
const history = useHistory()
const [channel, setChannel] = useState(null)
const [messages, setMessages] = useState([])
const [members, setMembers] = useState([])
const [users, setUsers] = useState([])
const [keyword, setKeyword] = useState(null)
const [currentUser, setCurrentUser] = useState(null)
const [message, setMessage] = useState('')
const [searching, setSearching] = useState(false)
const [toggle, setToggle] = useState(false)
const [toggleAdd, setToggleAdd] = useState(false)
const [calling, setCalling] = useState(false)
const [sessionID, setSessionID] = useState('')
const [isIncomingCall, setIsIncomingCall] = useState(false)
const [isOutgoingCall, setIsOutgoingCall] = useState(false)
const [isLive, setIsLive] = useState(false)
const togglerDetail = () => {
setToggle(!toggle)
}
const togglerAdd = () => {
setToggleAdd(!toggleAdd)
}
const findUser = (e) => {
e.preventDefault()
searchTerm(keyword)
}
const searchTerm = (keyword) => {
setSearching(true)
const limit = 30
const usersRequest = new CometChat.UsersRequestBuilder()
.setLimit(limit)
.setSearchKeyword(keyword)
.build()
usersRequest
.fetchNext()
.then((userList) => {
setUsers(userList)
setSearching(false)
})
.catch((error) => {
console.log('User list fetching failed with error:', error)
setSearching(false)
})
}
const getMembers = (guid) => {
const GUID = guid
const limit = 30
const groupMemberRequest = new CometChat.GroupMembersRequestBuilder(GUID)
.setLimit(limit)
.build()
groupMemberRequest
.fetchNext()
.then((groupMembers) => setMembers(groupMembers))
.catch((error) => {
console.log('Group Member list fetching failed with exception:', error)
})
}
const getChannel = (guid) => {
CometChat.getGroup(guid)
.then((group) => setChannel(group))
.catch((error) => {
if (error.code === 'ERR_GUID_NOT_FOUND') history.push('/')
console.log('Group details fetching failed with exception:', error)
})
}
const listenForMessage = (listenerID) => {
CometChat.addMessageListener(
listenerID,
new CometChat.MessageListener({
onTextMessageReceived: (message) => {
setMessages((prevState) => [...prevState, message])
scrollToEnd()
},
})
)
}
const getMessages = (guid) => {
const limit = 50
const messagesRequest = new CometChat.MessagesRequestBuilder()
.setLimit(limit)
.setGUID(guid)
.build()
messagesRequest
.fetchPrevious()
.then((msgs) => {
setMessages(msgs.filter((m) => m.type === 'text'))
scrollToEnd()
})
.catch((error) =>
console.log('Message fetching failed with error:', error)
)
}
const scrollToEnd = () => {
const elmnt = document.getElementById('messages-container')
elmnt.scrollTop = elmnt.scrollHeight
}
const onSubmit = (e) => {
e.preventDefault()
sendMessage(id, message)
}
const sendMessage = (guid, message) => {
const receiverID = guid
const messageText = message
const receiverType = CometChat.RECEIVER_TYPE.GROUP
const textMessage = new CometChat.TextMessage(
receiverID,
messageText,
receiverType
)
CometChat.sendMessage(textMessage)
.then((message) => {
setMessages((prevState) => [...prevState, message])
setMessage('')
scrollToEnd()
})
.catch((error) =>
console.log('Message sending failed with error:', error)
)
}
const addMember = (guid, uid) => {
let GUID = guid
let membersList = [
new CometChat.GroupMember(uid, CometChat.GROUP_MEMBER_SCOPE.PARTICIPANT),
]
CometChat.addMembersToGroup(GUID, membersList, [])
.then((member) => {
setMembers((prevState) => [...prevState, member])
alert('Member added successfully')
})
.catch((error) => {
console.log('Something went wrong', error)
alert(error.message)
})
}
const remMember = (GUID, UID) => {
if (channel.scope !== 'owner') return null
CometChat.kickGroupMember(GUID, UID).then(
(response) => {
const index = members.findIndex((member) => member.uid === UID)
members.splice(index, 1)
console.log('Group member kicked successfully', response)
alert('Member removed successfully')
},
(error) => {
console.log('Group member kicking failed with error', error)
}
)
}
const listenForCall = (listnerID) => {
CometChat.addCallListener(
listnerID,
new CometChat.CallListener({
onIncomingCallReceived(call) {
console.log('Incoming call:', call)
// Handle incoming call
setSessionID(call.sessionId)
setIsIncomingCall(true)
setCalling(true)
},
onOutgoingCallAccepted(call) {
console.log('Outgoing call accepted:', call)
// Outgoing Call Accepted
startCall(call)
},
onOutgoingCallRejected(call) {
console.log('Outgoing call rejected:', call)
// Outgoing Call Rejected
setIsIncomingCall(false)
setIsOutgoingCall(false)
setCalling(false)
},
onIncomingCallCancelled(call) {
console.log('Incoming call calcelled:', call)
setIsIncomingCall(false)
setIsIncomingCall(false)
setCalling(false)
},
})
)
}
const startCall = (call) => {
const sessionId = call.sessionId
const callType = call.type
const callSettings = new CometChat.CallSettingsBuilder()
.setSessionID(sessionId)
.enableDefaultLayout(true)
.setIsAudioOnlyCall(callType === 'audio' ? true : false)
.build()
setSessionID(sessionId)
setIsOutgoingCall(false)
setIsIncomingCall(false)
setCalling(false)
setIsLive(true)
CometChat.startCall(
callSettings,
document.getElementById('callScreen'),
new CometChat.OngoingCallListener({
onUserJoined: (user) => {
/* Notification received here if another user joins the call. */
console.log('User joined call:', user)
/* this method can be use to display message or perform any actions if someone joining the call */
},
onUserLeft: (user) => {
/* Notification received here if another user left the call. */
console.log('User left call:', user)
/* this method can be use to display message or perform any actions if someone leaving the call */
},
onUserListUpdated: (userList) => {
console.log('user list:', userList)
},
onCallEnded: (call) => {
/* Notification received here if current ongoing call is ended. */
console.log('Call ended:', call)
/* hiding/closing the call screen can be done here. */
setIsIncomingCall(false)
setIsOutgoingCall(false)
setCalling(false)
setIsLive(false)
},
onError: (error) => {
console.log('Error :', error)
/* hiding/closing the call screen can be done here. */
},
onMediaDeviceListUpdated: (deviceList) => {
console.log('Device List:', deviceList)
},
})
)
}
const initiateCall = () => {
const receiverID = id //The uid of the user to be called
const callType = CometChat.CALL_TYPE.VIDEO
const receiverType = CometChat.RECEIVER_TYPE.GROUP
const call = new CometChat.Call(receiverID, callType, receiverType)
CometChat.initiateCall(call)
.then((outGoingCall) => {
console.log('Call initiated successfully:', outGoingCall)
// perform action on success. Like show your calling screen.
setSessionID(outGoingCall.sessionId)
setIsOutgoingCall(true)
setCalling(true)
})
.catch((error) => {
console.log('Call initialization failed with exception:', error)
})
}
const acceptCall = (sessionID) => {
CometChat.acceptCall(sessionID)
.then((call) => {
console.log('Call accepted successfully:', call)
// start the call using the startCall() method
startCall(call)
})
.catch((error) => {
console.log('Call acceptance failed with error', error)
// handle exception
})
}
const rejectCall = (sessionID) => {
const status = CometChat.CALL_STATUS.REJECTED
CometChat.rejectCall(sessionID, status)
.then((call) => {
console.log('Call rejected successfully', call)
setCalling(false)
setIsIncomingCall(false)
setIsOutgoingCall(false)
setIsLive(false)
})
.catch((error) => {
console.log('Call rejection failed with error:', error)
})
}
const endCall = (sessionID) => {
CometChat.endCall(sessionID)
.then((call) => {
console.log('call ended', call)
setCalling(false)
setIsIncomingCall(false)
setIsIncomingCall(false)
})
.catch((error) => {
console.log('error', error)
})
}
const deleteChannel = (GUID) => {
if (window.confirm('Are you sure?')) {
CometChat.deleteGroup(GUID).then(
(response) => {
console.log('Channel deleted successfully:', response)
window.location.href = '/'
},
(error) => {
console.log('Channel delete failed with exception:', error)
}
)
}
}
useEffect(() => {
getChannel(id)
getMessages(id)
getMembers(id)
listenForMessage(id)
listenForCall(id)
setCurrentUser(JSON.parse(localStorage.getItem('user')))
}, [id])
return (
<div className="channel">
{calling ? (
<div className="callScreen">
<div className="callScreen__container">
<div className="call-animation">
<img
className="img-circle"
src={channel?.avatar}
alt=""
width="135"
/>
</div>
{isOutgoingCall ? (
<h4>Calling {channel?.name}</h4>
) : (
<h4>{channel?.name} Calling</h4>
)}
{isIncomingCall ? (
<div className="callScreen__btns">
<Button onClick={() => acceptCall(sessionID)}>
<CallIcon />
</Button>
<Button onClick={() => rejectCall(sessionID)}>
<CallEndIcon />
</Button>
</div>
) : (
<div className="callScreen__btns">
<Button onClick={() => endCall(sessionID)}>
<CallEndIcon />
</Button>
</div>
)}
</div>
</div>
) : (
''
)}
<div className="channel__chat">
<div className="channel__header">
<div className="channel__headerLeft">
<h4 className="channel__channelName">
<strong>
{channel?.type === 'private' ? <LockIcon /> : '#'}
{channel?.name}
</strong>
<StarBorderOutlinedIcon />
</h4>
</div>
<div className="channel__headerRight">
<PersonAddOutlinedIcon onClick={togglerAdd} />
<InfoOutlinedIcon onClick={togglerDetail} />
</div>
</div>
<div id="messages-container" className="channel__messages">
{messages.map((message) => (
<Message
uid={message?.sender.uid}
name={message.sender?.name}
avatar={message.sender?.avatar}
message={message?.text}
timestamp={message?.sentAt}
key={message?.sentAt}
/>
))}
</div>
<div className="channel__chatInput">
<form>
<input
placeholder={`Message ${channel?.name.toLowerCase()}`}
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
<button type="submit" onClick={(e) => onSubmit(e)}>
SEND
</button>
</form>
</div>
</div>
<div className={`channel__details ${!toggle ? 'hide__details' : ''}`}>
<div className="channel__header">
<div className="channel__headerLeft">
<h4 className="channel__channelName">
<strong>Details</strong>
</h4>
</div>
<div className="channel__headerRight">
<CloseIcon onClick={togglerDetail} />
</div>
</div>
<div className="channel__detailsBody">
<div className="channel__detailsActions">
<span>
<PersonAddOutlinedIcon onClick={togglerAdd} />
Add
</span>
<span>
<SearchIcon onClick={togglerAdd} />
Find
</span>
<span>
<CallIcon onClick={initiateCall} />
Call
</span>
<span>
<MoreHorizIcon />
More
</span>
</div>
<hr />
<div className="channel__detailsMembers">
<h4>Members({members.length})</h4>
{members.map((member) => (
<div
key={member?.uid}
className={`available__member ${
member?.status === 'online' ? 'isOnline' : ''
}`}
>
<Avatar src={member?.avatar} alt={member?.name} />
<Link to={`/users/${member?.uid}`}>{member?.name}</Link>
<FiberManualRecordIcon />
{member?.scope !== 'admin' ? (
channel?.scope === 'admin' ? (
<PersonAddDisabledIcon
onClick={() => remMember(id, member?.uid)}
title={member?.scope}
/>
) : (
''
)
) : (
<LockIcon title={member?.scope} />
)}
</div>
))}
</div>
{channel?.scope === 'owner' ? (
<>
<hr />
<div className="channel__detailsMembers">
<Button className="deleteBtn" onClick={() => deleteChannel(id)}>
Delete Channel
</Button>
</div>
</>
) : (
''
)}
</div>
</div>
<div className={`channel__details ${!toggleAdd ? 'hide__details' : ''}`}>
<div className="channel__header">
<div className="channel__headerLeft">
<h4 className="channel__channelName">
<strong>Add Member</strong>
</h4>
</div>
<div className="channel__headerRight">
<CloseIcon onClick={togglerAdd} />
</div>
</div>
<div className="channel__detailsBody">
<form onSubmit={(e) => findUser(e)} className="channel__detailsForm">
<input
placeholder="Search for a user"
onChange={(e) => setKeyword(e.target.value)}
required
/>
<Button onClick={(e) => findUser(e)}>
{!searching ? 'Find' : <div id="loading"></div>}
</Button>
</form>
<hr />
<div className="channel__detailsMembers">
<h4>Search Result({users.length})</h4>
{users.map((user) => (
<div
key={user?.uid}
className={`available__member ${
user?.status === 'online' ? 'isOnline' : ''
}`}
>
<Avatar src={user?.avatar} alt={user?.name} />
<Link to={`/users/${user?.uid}`}>{user?.name}</Link>
<FiberManualRecordIcon />
{currentUser.uid !== user?.uid ? (
channel?.scope === 'admin' ? (
<PersonAddOutlinedIcon
onClick={() => addMember(id, user?.uid)}
/>
) : (
''
)
) : (
''
)}
</div>
))}
</div>
</div>
</div>
{isLive ? <div id="callScreen"></div> : ''}
</div>
)
}
export default Channel
view raw Channel.js hosted with ❤ by GitHub

The User Component

The User Component

Still, the user component behaves the same way as the channel component but with some variations in features. With this component, you can search for friends and also have direct messages with them as given below.

.user {
flex: 0.7;
flex-grow: 1;
display: flex;
}
.user__chat {
flex: 1;
}
.user__details {
flex: 0.4;
border-left: 1px solid lightgray;
}
.hide__details {
display: none;
}
.user__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px;
border-bottom: 1px solid lightgray;
}
.user__messages {
overflow-y: auto;
height: calc(100vh - 35%);
}
.user__userName {
display: flex;
align-items: center;
text-transform: lowercase;
}
.user__userName > .MuiSvgIcon-root {
margin-left: 10px;
font-size: 16px;
}
.user__userName > strong {
display: flex;
align-items: center;
}
.user__userName > strong > .MuiSvgIcon-root {
font-size: 14px;
}
.user__headerRight {
display: flex;
align-items: center;
}
.user__headerRight > .MuiSvgIcon-root {
margin-left: 15px;
font-size: 16px;
cursor: pointer;
}
.user__headerRight > .MuiSvgIcon-root:hover {
transform: scale(1.3);
transition: all 0.3s ease-in-out;
}
.user__chatInput {
border-radius: 20px;
}
.user__chatInput > form {
display: flex;
justify-content: center;
}
.user__chatInput > form > input {
width: 90%;
border: 1px solid gray;
border-radius: 3px;
padding: 20px;
margin: 10px;
}
.user__chatInput > form > input:focus {
outline: none;
}
.user__chatInput > form > button {
display: none !important;
}
.user__messages::-webkit-scrollbar,
.user__detailsBody::-webkit-scrollbar {
width: 0.8em;
}
.user__messages::-webkit-scrollbar-thumb,
.user__detailsBody::-webkit-scrollbar-thumb {
background-color: darkgrey;
outline: none;
border-radius: 9px;
}
.user__detailsActions {
padding: 30px;
display: flex;
justify-content: space-between;
align-items: center;
}
.user__detailsActions > span {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.user__detailsActions .MuiSvgIcon-root {
background-color: whitesmoke;
padding: 10px;
border-radius: 50px;
cursor: pointer;
margin-bottom: 10px;
}
.user__detailsActions > .MuiSvgIcon-root:hover {
background-color: lightgray;
}
.user__detailsBody {
overflow-y: auto;
height: calc(100vh - 112px);
}
.user__detailsBody > hr {
border: none;
border-bottom: 1px solid lightgray;
}
.user__detailsMembers {
margin: 30px;
}
.available__member {
display: flex;
align-items: center;
text-decoration: none;
font-size: 15px;
font-weight: 600;
color: black;
margin: 10px 0;
}
.available__member > a {
text-decoration: none;
color: black;
}
.available__member > .MuiAvatar-root {
border-radius: 9px;
width: 25px;
height: 25px;
margin-right: 10px;
}
.available__member > .MuiSvgIcon-root {
font-size: 14px;
margin-left: 10px;
margin-top: 3px;
cursor: pointer;
}
.available__member > .MuiSvgIcon-root:last-child {
color: black;
}
.available__member > .MuiSvgIcon-root:last-child:hover {
transform: scale(1.3);
transition: all 0.3s ease-in-out;
}
.user__detailsMembers > h4 {
font-weight: 500;
margin-bottom: 20px;
}
.isOnline > .MuiSvgIcon-root {
color: green;
}
.user__detailsForm {
padding: 30px;
display: flex;
justify-content: space-between;
align-items: center;
}
.user__detailsForm > button {
text-transform: inherit !important;
background-color: #0a8d48;
color: white;
border: 1px solid #0a8d48;
line-height: 1.13;
border-radius: 9px;
}
.user__detailsForm > button:hover {
background-color: transparent;
color: #0a8d48;
}
.user__detailsForm > input {
padding: 5px;
border: 1px solid lightgray;
border-radius: 9px;
}
.user__detailsIdentity {
text-align: center;
padding: 20px;
}
.user__detailsIdentity > img {
border-radius: 9px;
}
.user__detailsIdentity > h4 {
font-size: 15px;
font-weight: 500;
text-transform: capitalize;
margin-top: 10px;
}
.user__detailsIdentity > h4 > .MuiSvgIcon-root {
font-size: 12px;
margin-left: 5px;
}
#loading {
display: inline-block;
width: 8px !important;
height: 8px !important;
border: 3px solid rgb(255 255 255);
border-radius: 50%;
border-top-color: #0a8d48;
animation: spin 1s ease-in-out infinite;
-webkit-animation: spin 1s ease-in-out infinite;
}
#loading:hover {
border: 3px solid #0a8d48;
border-top-color: rgb(255 255 255);
}
@keyframes spin {
to {
-webkit-transform: rotate(360deg);
}
}
@-webkit-keyframes spin {
to {
-webkit-transform: rotate(360deg);
}
}
#callScreen {
position: absolute;
width: 100vw;
height: 100vh;
/* visibility: hidden; */
}
.showCaller {
visibility: visible;
}
.callScreen {
background-color: #000000c4;
width: 100vw;
height: 100vh;
position: absolute;
z-index: 99999;
}
.callScreen__container {
display: grid;
place-items: center;
margin-top: 25vh;
width: 80%;
}
.callScreen__container > h4 {
margin: 30px 0;
color: white;
}
.callScreen__btns {
display: flex;
justify-content: space-evenly;
align-items: center;
width: 35%;
margin-top: 30px;
}
.callScreen__btns > button {
border-radius: 50%;
min-height: 40px;
min-width: 40px;
}
.callScreen__btns > button:first-child {
background-color: green;
}
.callScreen__btns > button:last-child {
background-color: red;
}
.callScreen__btns > button .MuiSvgIcon-root {
color: white;
}
.call-animation {
background: #fff;
width: 135px;
height: 135px;
position: relative;
margin: 0 auto;
border-radius: 100%;
border: solid 5px #fff;
overflow: hidden;
animation: play 2s ease infinite;
-webkit-backface-visibility: hidden;
-moz-backface-visibility: hidden;
-ms-backface-visibility: hidden;
backface-visibility: hidden;
}
.callScreen__container > img {
width: 135px;
height: 135px;
border-radius: 100%;
position: absolute;
left: 0px;
top: 0px;
}
@keyframes play {
0% {
transform: scale(1);
}
15% {
box-shadow: 0 0 0 5px rgba(255, 255, 255, 0.4);
}
25% {
box-shadow: 0 0 0 10px rgba(255, 255, 255, 0.4),
0 0 0 20px rgba(255, 255, 255, 0.2);
}
25% {
box-shadow: 0 0 0 15px rgba(255, 255, 255, 0.4),
0 0 0 30px rgba(255, 255, 255, 0.2);
}
}
view raw User.css hosted with ❤ by GitHub
import './User.css'
import { useState, useEffect } from 'react'
import { Link, useParams } from 'react-router-dom'
import StarBorderOutlinedIcon from '@material-ui/icons/StarBorderOutlined'
import InfoOutlinedIcon from '@material-ui/icons/InfoOutlined'
import CallIcon from '@material-ui/icons/Call'
import CallEndIcon from '@material-ui/icons/CallEnd'
import PersonAddOutlinedIcon from '@material-ui/icons/PersonAddOutlined'
import PersonAddDisabledIcon from '@material-ui/icons/PersonAddDisabled'
import FiberManualRecordIcon from '@material-ui/icons/FiberManualRecord'
import SearchIcon from '@material-ui/icons/Search'
import MoreHorizIcon from '@material-ui/icons/MoreHoriz'
import CloseIcon from '@material-ui/icons/Close'
import Message from '../../components/message/Message'
import { CometChat } from '@cometchat-pro/chat'
import { cometChat } from '../../app.config'
import { Avatar, Button } from '@material-ui/core'
function User() {
const { id } = useParams()
const [user, setUser] = useState(null)
const [messages, setMessages] = useState([])
const [users, setUsers] = useState([])
const [currentUser, setCurrentUser] = useState(null)
const [keyword, setKeyword] = useState(null)
const [message, setMessage] = useState('')
const [searching, setSearching] = useState(false)
const [toggle, setToggle] = useState(false)
const [calling, setCalling] = useState(false)
const [sessionID, setSessionID] = useState('')
const [isIncomingCall, setIsIncomingCall] = useState(false)
const [isOutgoingCall, setIsOutgoingCall] = useState(false)
const [isLive, setIsLive] = useState(false)
const togglerDetail = () => {
setToggle(!toggle)
}
const findUser = (e) => {
e.preventDefault()
searchTerm(keyword)
}
const searchTerm = (keyword) => {
setSearching(true)
const limit = 30
const usersRequest = new CometChat.UsersRequestBuilder()
.setLimit(limit)
.setSearchKeyword(keyword)
.build()
usersRequest
.fetchNext()
.then((userList) => {
setUsers(userList)
setSearching(false)
})
.catch((error) => {
console.log('User list fetching failed with error:', error)
setSearching(false)
})
}
const getUser = (UID) => {
CometChat.getUser(UID)
.then((user) => setUser(user))
.catch((error) => {
console.log('User details fetching failed with error:', error)
})
}
const getMessages = (uid) => {
const limit = 50
const messagesRequest = new CometChat.MessagesRequestBuilder()
.setLimit(limit)
.setUID(uid)
.build()
messagesRequest
.fetchPrevious()
.then((msgs) => {
setMessages(msgs.filter((m) => m.type === 'text'))
scrollToEnd()
})
.catch((error) =>
console.log('Message fetching failed with error:', error)
)
}
const listenForMessage = (listenerID) => {
CometChat.addMessageListener(
listenerID,
new CometChat.MessageListener({
onTextMessageReceived: (message) => {
setMessages((prevState) => [...prevState, message])
scrollToEnd()
},
})
)
}
const listenForCall = (listnerID) => {
CometChat.addCallListener(
listnerID,
new CometChat.CallListener({
onIncomingCallReceived(call) {
console.log('Incoming call:', call)
// Handle incoming call
setSessionID(call.sessionId)
setIsIncomingCall(true)
setCalling(true)
},
onOutgoingCallAccepted(call) {
console.log('Outgoing call accepted:', call)
// Outgoing Call Accepted
startCall(call)
},
onOutgoingCallRejected(call) {
console.log('Outgoing call rejected:', call)
// Outgoing Call Rejected
setIsIncomingCall(false)
setIsOutgoingCall(false)
setCalling(false)
},
onIncomingCallCancelled(call) {
console.log('Incoming call calcelled:', call)
setIsIncomingCall(false)
setIsIncomingCall(false)
setCalling(false)
},
})
)
}
const listFriends = () => {
const limit = 10
const usersRequest = new CometChat.UsersRequestBuilder()
.setLimit(limit)
.friendsOnly(true)
.build()
usersRequest
.fetchNext()
.then((userList) => setUsers(userList))
.catch((error) => {
console.log('User list fetching failed with error:', error)
})
}
const remFriend = (uid, fid) => {
if (window.confirm('Are you sure?')) {
const url = `https://api-us.cometchat.io/v2.0/users/${uid}/friends`
const options = {
method: 'DELETE',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
appId: cometChat.APP_ID,
apiKey: cometChat.REST_KEY,
},
body: JSON.stringify({ friends: [fid] }),
}
fetch(url, options)
.then(() => {
const index = users.findIndex(user => user.uid === fid)
users.splice(index, 1)
alert('Friend Removed successfully!')
})
.catch((err) => console.error('error:' + err))
}
}
const addFriend = (uid) => {
const user = JSON.parse(localStorage.getItem('user'))
const url = `https://api-us.cometchat.io/v2.0/users/${user.uid}/friends`
const options = {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
appId: cometChat.APP_ID,
apiKey: cometChat.REST_KEY,
},
body: JSON.stringify({ accepted: [uid] }),
}
fetch(url, options)
.then(() => {
setToggle(false)
alert('Added as friend successfully')
})
.catch((err) => console.error('error:' + err))
}
const scrollToEnd = () => {
const elmnt = document.getElementById('messages-container')
elmnt.scrollTop = elmnt.scrollHeight
}
const onSubmit = (e) => {
e.preventDefault()
sendMessage(id, message)
}
const sendMessage = (uid, message) => {
const receiverID = uid
const messageText = message
const receiverType = CometChat.RECEIVER_TYPE.USER
const textMessage = new CometChat.TextMessage(
receiverID,
messageText,
receiverType
)
CometChat.sendMessage(textMessage)
.then((message) => {
setMessages((prevState) => [...prevState, message])
setMessage('')
scrollToEnd()
})
.catch((error) =>
console.log('Message sending failed with error:', error)
)
}
const initiateCall = () => {
const receiverID = id //The uid of the user to be called
const callType = CometChat.CALL_TYPE.VIDEO
const receiverType = CometChat.RECEIVER_TYPE.USER
const call = new CometChat.Call(receiverID, callType, receiverType)
CometChat.initiateCall(call)
.then((outGoingCall) => {
console.log('Call initiated successfully:', outGoingCall)
// perform action on success. Like show your calling screen.
setSessionID(outGoingCall.sessionId)
setIsOutgoingCall(true)
setCalling(true)
})
.catch((error) => {
console.log('Call initialization failed with exception:', error)
})
}
const startCall = (call) => {
const sessionId = call.sessionId
const callType = call.type
const callSettings = new CometChat.CallSettingsBuilder()
.setSessionID(sessionId)
.enableDefaultLayout(true)
.setIsAudioOnlyCall(callType === 'audio' ? true : false)
.build()
setSessionID(sessionId)
setIsOutgoingCall(false)
setIsIncomingCall(false)
setCalling(false)
setIsLive(true)
CometChat.startCall(
callSettings,
document.getElementById('callScreen'),
new CometChat.OngoingCallListener({
onUserJoined: (user) => {
/* Notification received here if another user joins the call. */
console.log('User joined call:', user)
/* this method can be use to display message or perform any actions if someone joining the call */
},
onUserLeft: (user) => {
/* Notification received here if another user left the call. */
console.log('User left call:', user)
/* this method can be use to display message or perform any actions if someone leaving the call */
},
onUserListUpdated: (userList) => {
console.log('user list:', userList)
},
onCallEnded: (call) => {
/* Notification received here if current ongoing call is ended. */
console.log('Call ended:', call)
/* hiding/closing the call screen can be done here. */
setIsIncomingCall(false)
setIsOutgoingCall(false)
setCalling(false)
setIsLive(false)
},
onError: (error) => {
console.log('Error :', error)
/* hiding/closing the call screen can be done here. */
},
onMediaDeviceListUpdated: (deviceList) => {
console.log('Device List:', deviceList)
},
})
)
}
const acceptCall = (sessionID) => {
CometChat.acceptCall(sessionID)
.then((call) => {
console.log('Call accepted successfully:', call)
// start the call using the startCall() method
startCall(call)
})
.catch((error) => {
console.log('Call acceptance failed with error', error)
// handle exception
})
}
const rejectCall = (sessionID) => {
const status = CometChat.CALL_STATUS.REJECTED
CometChat.rejectCall(sessionID, status)
.then((call) => {
console.log('Call rejected successfully', call)
setCalling(false)
setIsIncomingCall(false)
setIsOutgoingCall(false)
setIsLive(false)
})
.catch((error) => {
console.log('Call rejection failed with error:', error)
})
}
const endCall = (sessionID) => {
CometChat.endCall(sessionID)
.then((call) => {
console.log('call ended', call)
setCalling(false)
setIsIncomingCall(false)
setIsIncomingCall(false)
})
.catch((error) => {
console.log('error', error)
})
}
useEffect(() => {
getUser(id)
getMessages(id)
listenForMessage(id)
listenForCall(id)
listFriends(id)
setCurrentUser(JSON.parse(localStorage.getItem('user')))
}, [id])
return (
<div className="user">
{calling ? (
<div className="callScreen">
<div className="callScreen__container">
<div className="call-animation">
<img
className="img-circle"
src={user?.avatar}
alt=""
width="135"
/>
</div>
{isOutgoingCall ? (
<h4>Calling {user?.name}</h4>
) : (
<h4>{user?.name} Calling</h4>
)}
{isIncomingCall ? (
<div className="callScreen__btns">
<Button onClick={() => acceptCall(sessionID)}>
<CallIcon />
</Button>
<Button onClick={() => rejectCall(sessionID)}>
<CallEndIcon />
</Button>
</div>
) : (
<div className="callScreen__btns">
<Button onClick={() => endCall(sessionID)}>
<CallEndIcon />
</Button>
</div>
)}
</div>
</div>
) : (
''
)}
<div className="user__chat">
<div className="user__header">
<div className="user__headerLeft">
<h4 className="user__userName">
<strong className={user?.status === 'online' ? 'isOnline' : ''}>
<FiberManualRecordIcon />
{user?.name}
</strong>
<StarBorderOutlinedIcon />
</h4>
</div>
<div className="user__headerRight">
<CallIcon onClick={initiateCall} />
<InfoOutlinedIcon onClick={togglerDetail} />
</div>
</div>
<div id="messages-container" className="user__messages">
{messages.map((message) => (
<Message
uid={message?.sender.uid}
name={message.sender?.name}
avatar={message.sender?.avatar}
message={message?.text}
timestamp={message?.sentAt}
key={message?.sentAt}
/>
))}
</div>
<div className="user__chatInput">
<form>
<input
placeholder={`Message ${user?.name.toLowerCase()}`}
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
<button type="submit" onClick={(e) => onSubmit(e)}>
SEND
</button>
</form>
</div>
</div>
<div className={`user__details ${!toggle ? 'hide__details' : ''}`}>
<div className="user__header">
<div className="user__headerLeft">
<h4 className="user__userName">
<strong>Details</strong>
</h4>
</div>
<div className="user__headerRight">
<CloseIcon onClick={togglerDetail} />
</div>
</div>
<div className="user__detailsBody">
<div className="user__detailsIdentity">
<img src={user?.avatar} alt={user?.name} />
<h4 className={user?.status === 'online' ? 'isOnline' : ''}>
{user?.name}
<FiberManualRecordIcon />
</h4>
</div>
<div className="user__detailsActions">
<span>
<PersonAddOutlinedIcon onClick={() => addFriend(user?.uid)} />
Add
</span>
<span>
<SearchIcon />
Find
</span>
<span>
<CallIcon onClick={initiateCall} />
Call
</span>
<span>
<MoreHorizIcon />
More
</span>
</div>
<form onSubmit={(e) => findUser(e)} className="channel__detailsForm">
<input
placeholder="Search for a user"
onChange={(e) => setKeyword(e.target.value)}
required
/>
<Button onClick={(e) => findUser(e)}>
{!searching ? 'Find' : <div id="loading"></div>}
</Button>
</form>
<hr />
<div className="channel__detailsMembers">
<h4>Friends</h4>
{users.map((user) => (
<div
key={user?.uid}
className={`available__member ${
user?.status === 'online' ? 'isOnline' : ''
}`}
>
<Avatar src={user?.avatar} alt={user?.name} />
<Link to={`/users/${user?.uid}`}>{user?.name}</Link>
<FiberManualRecordIcon />
{currentUser?.uid.toLowerCase() === id.toLowerCase() ? (
<PersonAddDisabledIcon
onClick={() => remFriend(id, user?.uid)}
/>
) : (
''
)}
</div>
))}
</div>
</div>
</div>
{isLive ? <div id="callScreen"></div> : ''}
</div>
)
}
export default User
view raw User.js hosted with ❤ by GitHub

Congratulations on completing the slack-clone, now we have to spin up our application with the command below using our terminal.

npm start
Enter fullscreen mode Exit fullscreen mode

Conclusion

In conclusion, we have done an epic job in the realm of software development. You’ve been introduced to the inner workings of slack and its messaging abilities using the CometChat SDK and Firebase.

You have seen firsthand how to integrate most of the CometChat functionalities such as texting and video chatting. It's time to rise and start crushing other kinds of applications with the values you have gotten from this tutorial.

About Author

Gospel Darlington is a remote full-stack web developer, prolific in Frontend and API development. He takes a huge interest in the development of high-grade and responsive web applications. He is currently exploring new techniques for improving progressive web applications (PWA). Gospel Darlington currently works as a freelancer and spends his free time coaching young people on how to become successful in life. His hobbies include inventing new recipes, book writing, songwriting, and singing. You can reach me on LinkedIn, Twitter, or Facebook.

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs