What you’ll be building. Demo, Git Repo Here.
Introduction
As a software developer in this era, you can be sure that you'll face situations where you need to add a communication feature into an app. This tutorial will help you develop a web-based communication solution for chatting and calling using Comet Chat. I will be guiding you step-by-step with no step missed, so get ready for a smooth ride.
Prerequisites
To follow this tutorial, you must have understood the fundamental principles of VueJs. This will speed up your comprehension of 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 Vue-CLI installed on your computer using the command below.
npm install -g @vue/cli
Next, create a new project with the name tinder-clone and select the default preset.
vue create tinder-clone
Last, install these essential dependencies for our project using the command below.
npm install vue-router vue-swing vue-material-design-icons firebase
Now that we're done with the installations, let's move on to building our tinder clone solution.
Installing Comet Chat SDK
- Head to CometChat Pro and create an account.
- From the dashboard, create a new app called "Chat Apps".
- One created, click Explore.
- Go to the API Keys tab and click Create API Key.
- Create an API key called "Tinder Clone" with Full Access.
- Click on the newly created API, navigate to the Users tab, and delete all the default users leaving it blank (very important).
- Get the VueJs CLI installed on your machine by entering this command on your terminal.
npm install -g @vue/cli
- Create a ".env" file in the root directory of the project.
- Enter your secret keys from comet Chat and Firebase in this manner.
- Duplicate the ".env" file and rename it to ".env".
- Exclude ".env" and “.env.production” in the ".gitignore" file from getting exposed on Github.
- Run the following command to install the comet chat SDK.
The Environment Variables
The setup below spells out the format for configuring the .env files for this project.
VUE_APP_ID="xxx-xxx-xxx"
VUE_APP_KEY="xxx-xxx-xxx-xxx-xxx-xxx-xxx-xxx"
VUE_APP_REGION="xx"
VUE_APP_BASE_APIKEY="xxx-xxx-xxx-xxx-xxx-xxx-xxx-xxx"
VUE_APP_BASE_AUTHDOMAIN="xxx-xxx-xxx-xxx-xxx-xxx"
VUE_APP_BASE_PROJECTID="xxx-xxx-xxx"
VUE_APP_BASE_STORAGEBUCKET="xxx-xxx-xxx-xxx-xx"
VUE_APP_BASE_MESSAGINGSENDERID="xxx-xxx-xxx"
VUE_APP_BASE_APPID="xxx-xxx-xxx-xxx-xxx-xxx"
VUE_APP_BASE_MEASUREMENTID="xxx-xxx-xxx"
Setting Up Firebase Project
Head to Firebase create a new project and activate the email and password authentication service. This is how you do it.
To begin using Firebase, you’ll need a Gmail account. Head over to Firebase and create a new 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.
Next, click the edit icon on the email/password provider and enable it.
Next, 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.
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 Vue application.
Congratulations, now that you're done with the installations, let's do some configurations.
Installing The Comet Chat VueJs UI Kit
- Copy the folder to your source folder.
- Copy all the dependencies from package.json of cometchat-pro-vue-ui-kit into your project's package.json and install them.
Configuring Comet Chat SDK
Inside your project structure, open the main.js and paste these codes.
import Vue from 'vue' | |
import App from './App.vue' | |
import router from './router' | |
import { CometChat } from '@cometchat-pro/chat' | |
import { auth } from './firebase' | |
Vue.config.productionTip = false | |
router.beforeEach((to, from, next) => { | |
const requiresAuth = to.matched.some((record) => record.meta.requiresAuth) | |
const requiresProfile = to.matched.some((record) => record.meta.requiresProfile) | |
if (requiresAuth && !auth.currentUser) { | |
console.log('You are not authorized to access this area.') | |
next('login') | |
} else { | |
next() | |
} | |
if (requiresProfile && ((auth.currentUser?.photoURL || null) == null)) { | |
console.log('You must first complete your profile.') | |
next('profile') | |
} else { | |
next() | |
} | |
}) | |
const appID = process.env.VUE_APP_ID | |
const region = process.env.VUE_APP_REGION | |
const appSetting = new CometChat.AppSettingsBuilder() | |
.subscribePresenceForAllUsers() | |
.setRegion(region) | |
.build() | |
CometChat.init(appID, appSetting).then( | |
() => { | |
console.log('Initialization completed successfully') | |
// You can now call login function. | |
let app | |
auth.onAuthStateChanged(() => { | |
if (!app) { | |
new Vue({ | |
router, | |
render: (h) => h(App), | |
}).$mount('#app') | |
} | |
}) | |
}, | |
(error) => { | |
console.log('Initialization failed with error:', error) | |
// Check the reason for error and take appropriate action. | |
} | |
) |
The above codes initialize comet chat in your app and set it up. The route guard will filter out unauthenticated users. The main.js entry file uses your comet chat API Credentials. This file also contains the Firebase Configurations stored in the .env file. This .env file will not be public on your git repo as specified in the .gitignore file.
Setting Up The Router
The router.js file has all the routes available in our app along with their security clearance.
import Vue from 'vue' | |
import Router from 'vue-router' | |
Vue.use(Router) | |
const routes = [ | |
{ | |
path: '/', | |
component: () => import(/* webpackChunkName: "home" */ './views/Home.vue'), | |
name: 'home', | |
meta: { requiresAuth: true, requiresProfile: true } | |
}, | |
{ | |
path: '/profile', | |
component: () => import(/* webpackChunkName: "profile" */ './views/Profile.vue'), | |
name: 'profile', | |
meta: { requiresAuth: true } | |
}, | |
{ | |
path: '/friends', | |
component: () => import(/* webpackChunkName: "friends" */ './views/Friends.vue'), | |
name: 'friends', | |
meta: { requiresAuth: true, requiresProfile: true } | |
}, | |
{ | |
path: '/chats/:uid', | |
component: () => import(/* webpackChunkName: "chats" */ './views/Chats.vue'), | |
name: 'chat', | |
props: true, | |
meta: { requiresAuth: true, requiresProfile: true } | |
}, | |
{ | |
path: '/login', | |
component: () => import(/* webpackChunkName: "login" */ './views/Login.vue'), | |
name: 'login' | |
}, | |
{ | |
path: '/register', | |
component: () => import(/* webpackChunkName: "register" */ './views/Register.vue'), | |
name: 'register' | |
}, | |
{ | |
path: '/forget', | |
component: () => import(/* webpackChunkName: "forget" */ './views/Forget.vue'), | |
name: 'forget' | |
}, | |
] | |
const router = new Router({ | |
base: process.env.BASE_URL, | |
mode: 'history', | |
routes | |
}) | |
export default router; |
Setting Up The Firebase SDK
The firebase.js file has all the codes to interact with the auth firebase service. It will also make our code redundant-free.
import firebase from 'firebase/app' | |
import 'firebase/auth' | |
const firebaseConfig = { | |
apiKey: process.env.VUE_APP_BASE_APIKEY, | |
authDomain: process.env.VUE_APP_BASE_AUTHDOMAIN, | |
projectId: process.env.VUE_APP_BASE_PROJECTID, | |
storageBucket: process.env.VUE_APP_BASE_STORAGEBUCKET, | |
messagingSenderId: process.env.VUE_APP_BASE_MESSAGINGSENDERID, | |
appId: process.env.VUE_APP_BASE_APPID, | |
measurementId: process.env.VUE_APP_BASE_MEASUREMENTID, | |
} | |
firebase.initializeApp(firebaseConfig) | |
const auth = firebase.auth() | |
export { auth } |
Project Structure
The image below reveals the project structure. Make sure you see the folder arrangement before proceeding.
Now let's replicate the rest of the project components as seen in the image above.
The App Component
The following code wraps around our app within the vue-router enabling smooth navigation. For each route, this component generates a new id to improve the expected behavior of our app.
<template> | |
<div id="app"> | |
<router-view :key="$route.path"/> | |
</div> | |
</template> | |
<script> | |
export default { | |
name: "app", | |
}; | |
</script> | |
<style> | |
* { | |
margin: 0; | |
} | |
#app { | |
font-family: Avenir, Helvetica, Arial, sans-serif; | |
-webkit-font-smoothing: antialiased; | |
-moz-osx-font-smoothing: grayscale; | |
overflow-x: hidden; | |
} | |
/* width */ | |
::-webkit-scrollbar { | |
width: 10px; | |
} | |
/* Track */ | |
::-webkit-scrollbar-track { | |
background: #f1f1f1; | |
} | |
/* Handle */ | |
::-webkit-scrollbar-thumb { | |
background: #888; | |
} | |
/* Handle on hover */ | |
::-webkit-scrollbar-thumb:hover { | |
background: #555; | |
} | |
</style> |
The Sidebar Component
The sidebar component showcases matched users. Other than its elegant design, it gives users the ability to chat with other matched users. Other things that it does aside from the above mentioned is to provide searching and logout abilities.
The Messages Sub-Component
The Sidebar component contains a child component called "Messages". This child component lists the matched users associated with the currently logged-in user. Here is the code for it.
<template> | |
<div class="sidebar__messages"> | |
<h4 class="message__title">{{ title }}</h4> | |
<router-link | |
:to="'/chats/' + user.uid" | |
v-for="user in users" | |
:key="user.uid" | |
:class="[uid == user.uid ? 'active' : '', 'sidebar__message']" | |
> | |
<div class="message__left"> | |
<CometChatAvatar :image="user.avatar" /> | |
<CometChatUserPresence :status="user.status" /> | |
</div> | |
<div class="message__right"> | |
<h4 class="message__name">{{ user.name }}</h4> | |
<p class="message__content"> | |
{{ user.metadata.rawMetadata || "Hello I'm using tinder!" }} | |
</p> | |
</div> | |
</router-link> | |
</div> | |
</template> | |
<script> | |
import { | |
CometChatAvatar, | |
CometChatUserPresence, | |
} from "../cometchat-pro-vue-ui-kit"; | |
export default { | |
name: "messages", | |
props: { | |
title: { type: String, default: "Messages" }, | |
users: { | |
type: [Object, Array], | |
default: function () { | |
return [ | |
{ | |
uid: "1", | |
name: "Fullname", | |
avatar: | |
"https://hips.hearstapps.com/hmg-prod.s3.amazonaws.com/images/newborn-baby-boy-sleeping-peacefully-wearing-knit-royalty-free-image-1589459736.jpg?crop=0.669xw:1.00xh;0.228xw,0&resize=640:*", | |
metadata: { age: "21", rawMetadata: "Some Text Here!" }, | |
}, | |
]; | |
}, | |
}, | |
}, | |
components: { | |
CometChatAvatar, | |
CometChatUserPresence, | |
}, | |
computed: { | |
uid() { | |
return this.$route.params.uid; | |
}, | |
}, | |
}; | |
</script> | |
<style scoped> | |
.sidebar__messages { | |
padding: 20px 10px; | |
height: calc(100vh - 300px); | |
overflow-y: scroll; | |
} | |
.sidebar__messages a { | |
color: black; | |
text-decoration: none; | |
} | |
.sidebar__messages a:hover { | |
background: #f9f9ff; | |
transition: 0.5s; | |
} | |
.active { | |
background: #ececff; | |
transition: 0.5s; | |
} | |
.message__title { | |
color: #fd5068; | |
margin-bottom: 10px; | |
} | |
.sidebar__message { | |
display: flex; | |
align-items: center; | |
margin: 20px 0; | |
} | |
.message__left { | |
width: 70px; | |
height: 70px; | |
} | |
.message__left img { | |
object-fit: cover; | |
} | |
.message__right { | |
margin-left: 10px; | |
} | |
.message__name { | |
margin-bottom: 10px; | |
} | |
</style> |
The Authentication Components
The authentication components include the registration, login, and forget password components. Let make each of those files within the "views" folder and the instruction is as follow.
Make a new folder called “views” and create the following components inside of it. They should all end with the ".vue" extension of course. The Login, Register, and Forget components and must also contain the following codes.
The Register Component
We want a situation where a user can click on the "register button" and send his record to Firebase. After firebase registration, the Comet Chat SDK will also register the new user. This registration will be under the API key you created earlier on.
For instance, a user named Lucy wants to register on our platform. So she enters her credentials within the registration form provided and clicks the register button. Now, firebase sees her details and registers her if her email is unique. After the registration with Firebase, comet chat takes over and also registers her. Comet Chat uses your API key and places her under your account and her ID is set to her firebase ID.
The script below describes the authentication process in detail. Here is the full code for the registration component.
<template> | |
<div class="register__box"> | |
<h2>Register</h2> | |
<form @submit.prevent="onSubmit"> | |
<div class="user__box"> | |
<input type="text" required minlength="5" v-model="form.fullname" /> | |
<label>Fullname</label> | |
</div> | |
<div class="user__box"> | |
<input type="email" required minlength="10" v-model="form.email" /> | |
<label>Email</label> | |
</div> | |
<div class="user__box"> | |
<input type="password" minlength="6" required v-model="form.password" /> | |
<label>Password</label> | |
</div> | |
<div class="user__box"> | |
<router-link class="forget__link" to="/forget" | |
>Forget Password</router-link | |
> | |
</div> | |
<button type="submit"> | |
<span></span> | |
<span></span> | |
<span></span> | |
<span></span> | |
{{ requesting ? "Log..." : "Register" }} | |
</button> | |
<router-link class="links" to="/login">Login</router-link> | |
</form> | |
</div> | |
</template> | |
<script> | |
import { auth } from "../firebase"; | |
import { CometChat } from "@cometchat-pro/chat"; | |
export default { | |
data: () => ({ | |
valid: true, | |
requesting: false, | |
form: { | |
email: "", | |
password: "", | |
fullname: "", | |
} | |
}), | |
methods: { | |
onSubmit() { | |
this.requesting = true; | |
auth | |
.createUserWithEmailAndPassword(this.form.email, this.form.password) | |
.then((res) => { | |
res.user | |
.updateProfile({ | |
displayName: this.form.fullname, | |
}) | |
.then(() => this.signUpWithCometChat(res.user.uid)); | |
}) | |
.catch((error) => console.log(error)) | |
.finally(() => (this.requesting = false)); | |
}, | |
signUpWithCometChat(uid) { | |
const apiKey = process.env.VUE_APP_KEY; | |
const user = new CometChat.User(uid); | |
user.setName(this.form.fullname); | |
CometChat.createUser(user, apiKey) | |
.then(() => this.$router.push({ name: "login" })) | |
.catch((error) => console.log("error", error)) | |
.finally(() => (this.requesting = false)); | |
}, | |
}, | |
}; | |
</script> | |
<style> | |
html { | |
background-color: #e90d77; | |
} | |
.content { | |
background: none; | |
} | |
.wrapper { | |
position: unset; | |
} | |
.register__box { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
width: 300px; | |
padding: 40px; | |
transform: translate(-50%, -50%); | |
background: rgba(0, 0, 0, 0.5); | |
box-sizing: border-box; | |
box-shadow: 0 15px 25px rgba(0, 0, 0, 0.6); | |
border-radius: 10px; | |
} | |
.register__box h2 { | |
margin: 0 0 30px; | |
padding: 0; | |
color: #fff; | |
text-align: center; | |
} | |
.register__box .user__box { | |
position: relative; | |
} | |
.register__box .user__box input { | |
width: 100%; | |
padding: 10px 0; | |
font-size: 16px; | |
color: #fff; | |
margin-bottom: 30px; | |
border: none; | |
border-bottom: 1px solid #fff; | |
outline: none; | |
background: transparent; | |
} | |
.register__box .user__box label { | |
position: absolute; | |
top: 0; | |
left: 0; | |
padding: 10px 0; | |
font-size: 16px; | |
color: #fff; | |
pointer-events: none; | |
transition: 0.5s; | |
} | |
.register__box .user__box input:focus ~ label, | |
.register__box .user__box input:valid ~ label { | |
top: -20px; | |
left: 0; | |
color: #e90d77; | |
font-size: 12px; | |
} | |
.register__box form button { | |
position: relative; | |
display: inline-block; | |
padding: 10px 20px; | |
color: #e90d77; | |
font-size: 16px; | |
text-decoration: none; | |
text-transform: uppercase; | |
overflow: hidden; | |
transition: 0.5s; | |
margin-top: 40px; | |
letter-spacing: 4px; | |
background: none; | |
border: none; | |
} | |
.register__box button:hover { | |
background: #e90d77; | |
color: #fff; | |
border-radius: 5px; | |
box-shadow: 0 0 5px #e90d77, 0 0 25px #e90d77, 0 0 50px #e90d77, | |
0 0 100px #e90d77; | |
} | |
.register__box button span { | |
position: absolute; | |
display: block; | |
} | |
.register__box button span:nth-child(1) { | |
top: 0; | |
left: -100%; | |
width: 100%; | |
height: 2px; | |
background: linear-gradient(90deg, transparent, #e90d77); | |
animation: btn-anim1 1s linear infinite; | |
} | |
.register__box form .forget__link { | |
color: white; | |
text-decoration: none; | |
} | |
.register__box form .forget__link:hover { | |
color: #e90d77; | |
} | |
.register__box form .links { | |
color: white; | |
text-decoration: none; | |
margin-left: 5px; | |
padding: 10px; | |
} | |
.register__box form .links:hover { | |
background: #e90d77; | |
color: #fff; | |
border-radius: 5px; | |
box-shadow: 0 0 5px #e90d77, 0 0 25px #e90d77, 0 0 50px #e90d77, | |
0 0 100px #e90d77; | |
} | |
@keyframes btn-anim1 { | |
0% { | |
left: -100%; | |
} | |
50%, | |
100% { | |
left: 100%; | |
} | |
} | |
.register__box button span:nth-child(2) { | |
top: -100%; | |
right: 0; | |
width: 2px; | |
height: 100%; | |
background: linear-gradient(180deg, transparent, #e90d77); | |
animation: btn-anim2 1s linear infinite; | |
animation-delay: 0.25s; | |
} | |
@keyframes btn-anim2 { | |
0% { | |
top: -100%; | |
} | |
50%, | |
100% { | |
top: 100%; | |
} | |
} | |
.register__box button span:nth-child(3) { | |
bottom: 0; | |
right: -100%; | |
width: 100%; | |
height: 2px; | |
background: linear-gradient(270deg, transparent, #e90d77); | |
animation: btn-anim3 1s linear infinite; | |
animation-delay: 0.5s; | |
} | |
@keyframes btn-anim3 { | |
0% { | |
right: -100%; | |
} | |
50%, | |
100% { | |
right: 100%; | |
} | |
} | |
.register__box a span:nth-child(4) { | |
bottom: -100%; | |
left: 0; | |
width: 2px; | |
height: 100%; | |
background: linear-gradient(360deg, transparent, #e90d77); | |
animation: btn-anim4 1s linear infinite; | |
animation-delay: 0.75s; | |
} | |
@keyframes btn-anim4 { | |
0% { | |
bottom: -100%; | |
} | |
50%, | |
100% { | |
bottom: 100%; | |
} | |
} | |
</style> |
The Login Component
Once a user clicks on the Login Button with his detail entered in the Login form, firebase commences validation. If firebase validation is successful, comet chat signs the user in with his firebase ID. This follows the same principle as the registration process.
Below is the full code for the Login component.
<template> | |
<div class="register__box"> | |
<h2>Register</h2> | |
<form @submit.prevent="onSubmit"> | |
<div class="user__box"> | |
<input type="text" required minlength="5" v-model="form.fullname" /> | |
<label>Fullname</label> | |
</div> | |
<div class="user__box"> | |
<input type="email" required minlength="10" v-model="form.email" /> | |
<label>Email</label> | |
</div> | |
<div class="user__box"> | |
<input type="password" minlength="6" required v-model="form.password" /> | |
<label>Password</label> | |
</div> | |
<div class="user__box"> | |
<router-link class="forget__link" to="/forget" | |
>Forget Password</router-link | |
> | |
</div> | |
<button type="submit"> | |
<span></span> | |
<span></span> | |
<span></span> | |
<span></span> | |
{{ requesting ? "Log..." : "Register" }} | |
</button> | |
<router-link class="links" to="/login">Login</router-link> | |
</form> | |
</div> | |
</template> | |
<script> | |
import { auth } from "../firebase"; | |
import { CometChat } from "@cometchat-pro/chat"; | |
export default { | |
data: () => ({ | |
valid: true, | |
requesting: false, | |
form: { | |
email: "", | |
password: "", | |
fullname: "", | |
} | |
}), | |
methods: { | |
onSubmit() { | |
this.requesting = true; | |
auth | |
.createUserWithEmailAndPassword(this.form.email, this.form.password) | |
.then((res) => { | |
res.user | |
.updateProfile({ | |
displayName: this.form.fullname, | |
}) | |
.then(() => this.signUpWithCometChat(res.user.uid)); | |
}) | |
.catch((error) => console.log(error)) | |
.finally(() => (this.requesting = false)); | |
}, | |
signUpWithCometChat(uid) { | |
const apiKey = process.env.VUE_APP_KEY; | |
const user = new CometChat.User(uid); | |
user.setName(this.form.fullname); | |
CometChat.createUser(user, apiKey) | |
.then(() => this.$router.push({ name: "login" })) | |
.catch((error) => console.log("error", error)) | |
.finally(() => (this.requesting = false)); | |
}, | |
}, | |
}; | |
</script> | |
<style> | |
html { | |
background-color: #e90d77; | |
} | |
.content { | |
background: none; | |
} | |
.wrapper { | |
position: unset; | |
} | |
.register__box { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
width: 300px; | |
padding: 40px; | |
transform: translate(-50%, -50%); | |
background: rgba(0, 0, 0, 0.5); | |
box-sizing: border-box; | |
box-shadow: 0 15px 25px rgba(0, 0, 0, 0.6); | |
border-radius: 10px; | |
} | |
.register__box h2 { | |
margin: 0 0 30px; | |
padding: 0; | |
color: #fff; | |
text-align: center; | |
} | |
.register__box .user__box { | |
position: relative; | |
} | |
.register__box .user__box input { | |
width: 100%; | |
padding: 10px 0; | |
font-size: 16px; | |
color: #fff; | |
margin-bottom: 30px; | |
border: none; | |
border-bottom: 1px solid #fff; | |
outline: none; | |
background: transparent; | |
} | |
.register__box .user__box label { | |
position: absolute; | |
top: 0; | |
left: 0; | |
padding: 10px 0; | |
font-size: 16px; | |
color: #fff; | |
pointer-events: none; | |
transition: 0.5s; | |
} | |
.register__box .user__box input:focus ~ label, | |
.register__box .user__box input:valid ~ label { | |
top: -20px; | |
left: 0; | |
color: #e90d77; | |
font-size: 12px; | |
} | |
.register__box form button { | |
position: relative; | |
display: inline-block; | |
padding: 10px 20px; | |
color: #e90d77; | |
font-size: 16px; | |
text-decoration: none; | |
text-transform: uppercase; | |
overflow: hidden; | |
transition: 0.5s; | |
margin-top: 40px; | |
letter-spacing: 4px; | |
background: none; | |
border: none; | |
} | |
.register__box button:hover { | |
background: #e90d77; | |
color: #fff; | |
border-radius: 5px; | |
box-shadow: 0 0 5px #e90d77, 0 0 25px #e90d77, 0 0 50px #e90d77, | |
0 0 100px #e90d77; | |
} | |
.register__box button span { | |
position: absolute; | |
display: block; | |
} | |
.register__box button span:nth-child(1) { | |
top: 0; | |
left: -100%; | |
width: 100%; | |
height: 2px; | |
background: linear-gradient(90deg, transparent, #e90d77); | |
animation: btn-anim1 1s linear infinite; | |
} | |
.register__box form .forget__link { | |
color: white; | |
text-decoration: none; | |
} | |
.register__box form .forget__link:hover { | |
color: #e90d77; | |
} | |
.register__box form .links { | |
color: white; | |
text-decoration: none; | |
margin-left: 5px; | |
padding: 10px; | |
} | |
.register__box form .links:hover { | |
background: #e90d77; | |
color: #fff; | |
border-radius: 5px; | |
box-shadow: 0 0 5px #e90d77, 0 0 25px #e90d77, 0 0 50px #e90d77, | |
0 0 100px #e90d77; | |
} | |
@keyframes btn-anim1 { | |
0% { | |
left: -100%; | |
} | |
50%, | |
100% { | |
left: 100%; | |
} | |
} | |
.register__box button span:nth-child(2) { | |
top: -100%; | |
right: 0; | |
width: 2px; | |
height: 100%; | |
background: linear-gradient(180deg, transparent, #e90d77); | |
animation: btn-anim2 1s linear infinite; | |
animation-delay: 0.25s; | |
} | |
@keyframes btn-anim2 { | |
0% { | |
top: -100%; | |
} | |
50%, | |
100% { | |
top: 100%; | |
} | |
} | |
.register__box button span:nth-child(3) { | |
bottom: 0; | |
right: -100%; | |
width: 100%; | |
height: 2px; | |
background: linear-gradient(270deg, transparent, #e90d77); | |
animation: btn-anim3 1s linear infinite; | |
animation-delay: 0.5s; | |
} | |
@keyframes btn-anim3 { | |
0% { | |
right: -100%; | |
} | |
50%, | |
100% { | |
right: 100%; | |
} | |
} | |
.register__box a span:nth-child(4) { | |
bottom: -100%; | |
left: 0; | |
width: 2px; | |
height: 100%; | |
background: linear-gradient(360deg, transparent, #e90d77); | |
animation: btn-anim4 1s linear infinite; | |
animation-delay: 0.75s; | |
} | |
@keyframes btn-anim4 { | |
0% { | |
bottom: -100%; | |
} | |
50%, | |
100% { | |
bottom: 100%; | |
} | |
} | |
</style> |
The Forget Component
The forget password component is important for recovering passwords. The Firebase SDK provides that functionality. Also, to make our App complete we have to include it.
The code in this component allows you to recover lost passwords using a valid email address.
<template> | |
<div class="forget__box"> | |
<h2>Forget</h2> | |
<form @submit.prevent="onSubmit"> | |
<div class="user__box"> | |
<input type="email" required v-model="form.email" /> | |
<label>Email</label> | |
</div> | |
<button type="submit"> | |
<span></span> | |
<span></span> | |
<span></span> | |
<span></span> | |
{{ requesting ? "Log..." : "Send" }} | |
</button> | |
<router-link to="/login">Login</router-link> | |
</form> | |
</div> | |
</template> | |
<script> | |
import { auth } from "../firebase"; | |
export default { | |
data: () => ({ | |
valid: true, | |
requesting: false, | |
form: { | |
email: "", | |
}, | |
}), | |
methods: { | |
onSubmit() { | |
this.requesting = true; | |
auth | |
.sendPasswordResetEmail(this.form.email) | |
.then(() => this.$router.push({ name: "login" })) | |
.catch((error) => console.log(error)) | |
.finally(() => (this.requesting = false)); | |
}, | |
}, | |
}; | |
</script> | |
<style> | |
html { | |
background-color: #e90d77; | |
} | |
.content { | |
background: none; | |
} | |
.wrapper { | |
position: unset; | |
} | |
.forget__box { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
width: 300px; | |
padding: 40px; | |
transform: translate(-50%, -50%); | |
background: rgba(0, 0, 0, 0.5); | |
box-sizing: border-box; | |
box-shadow: 0 15px 25px rgba(0, 0, 0, 0.6); | |
border-radius: 10px; | |
} | |
.forget__box h2 { | |
margin: 0 0 30px; | |
padding: 0; | |
color: #fff; | |
text-align: center; | |
} | |
.forget__box .user__box { | |
position: relative; | |
} | |
.forget__box .user__box input { | |
width: 100%; | |
padding: 10px 0; | |
font-size: 16px; | |
color: #fff; | |
margin-bottom: 30px; | |
border: none; | |
border-bottom: 1px solid #fff; | |
outline: none; | |
background: transparent; | |
} | |
.forget__box .user__box label { | |
position: absolute; | |
top: 0; | |
left: 0; | |
padding: 10px 0; | |
font-size: 16px; | |
color: #fff; | |
pointer-events: none; | |
transition: 0.5s; | |
} | |
.forget__box .user__box input:focus ~ label, | |
.forget__box .user__box input:valid ~ label { | |
top: -20px; | |
left: 0; | |
color: #e90d77; | |
font-size: 12px; | |
} | |
.forget__box form button { | |
position: relative; | |
display: inline-block; | |
padding: 10px 20px; | |
color: #e90d77; | |
font-size: 16px; | |
text-decoration: none; | |
text-transform: uppercase; | |
overflow: hidden; | |
transition: 0.5s; | |
margin-top: 40px; | |
letter-spacing: 4px; | |
background: none; | |
border: none; | |
} | |
.forget__box button:hover { | |
background: #e90d77; | |
color: #fff; | |
border-radius: 5px; | |
box-shadow: 0 0 5px #e90d77, 0 0 25px #e90d77, 0 0 50px #e90d77, | |
0 0 100px #e90d77; | |
} | |
.forget__box button span { | |
position: absolute; | |
display: block; | |
} | |
.forget__box button span:nth-child(1) { | |
top: 0; | |
left: -100%; | |
width: 100%; | |
height: 2px; | |
background: linear-gradient(90deg, transparent, #e90d77); | |
animation: btn-anim1 1s linear infinite; | |
} | |
.forget__box form a { | |
color: white; | |
text-decoration: none; | |
margin-left: 5px; | |
padding: 10px; | |
} | |
.forget__box form a:hover { | |
background: #e90d77; | |
color: #fff; | |
border-radius: 5px; | |
box-shadow: 0 0 5px #e90d77, 0 0 25px #e90d77, 0 0 50px #e90d77, | |
0 0 100px #e90d77; | |
} | |
@keyframes btn-anim1 { | |
0% { | |
left: -100%; | |
} | |
50%, | |
100% { | |
left: 100%; | |
} | |
} | |
.forget__box button span:nth-child(2) { | |
top: -100%; | |
right: 0; | |
width: 2px; | |
height: 100%; | |
background: linear-gradient(180deg, transparent, #e90d77); | |
animation: btn-anim2 1s linear infinite; | |
animation-delay: 0.25s; | |
} | |
@keyframes btn-anim2 { | |
0% { | |
top: -100%; | |
} | |
50%, | |
100% { | |
top: 100%; | |
} | |
} | |
.forget__box button span:nth-child(3) { | |
bottom: 0; | |
right: -100%; | |
width: 100%; | |
height: 2px; | |
background: linear-gradient(270deg, transparent, #e90d77); | |
animation: btn-anim3 1s linear infinite; | |
animation-delay: 0.5s; | |
} | |
@keyframes btn-anim3 { | |
0% { | |
right: -100%; | |
} | |
50%, | |
100% { | |
right: 100%; | |
} | |
} | |
.forget__box a span:nth-child(4) { | |
bottom: -100%; | |
left: 0; | |
width: 2px; | |
height: 100%; | |
background: linear-gradient(360deg, transparent, #e90d77); | |
animation: btn-anim4 1s linear infinite; | |
animation-delay: 0.75s; | |
} | |
@keyframes btn-anim4 { | |
0% { | |
bottom: -100%; | |
} | |
50%, | |
100% { | |
bottom: 100%; | |
} | |
} | |
</style> |
The Profile Component
The profile component is responsible for capturing and updating the rest of a user’s details. Once a user registers in our app, he will be directed to the profile page to complete his details. These details are important to the proper functioning of our application. A user will not be allowed to continue with the system until he completes his profile. The information the profile component requires is as follows.
- User’s Fullname
- User’s Avatar
- User’s Age
- User’s Description
Below are the codes responsible for this operation.
<template> | |
<div class="login__box"> | |
<h2>Profile</h2> | |
<form @submit.prevent="onSubmit"> | |
<div class="user__box"> | |
<input type="text" required minlength="6" v-model="form.fullname" /> | |
<label>Fullname</label> | |
</div> | |
<div class="user__box"> | |
<input type="text" required minlength="10" v-model="form.avatar" /> | |
<label>Image URL</label> | |
</div> | |
<div class="user__box"> | |
<input | |
type="number" | |
required | |
v-model="form.age" | |
min="18" | |
max="60" | |
minlength="2" | |
maxlength="3" | |
/> | |
<label>Age</label> | |
</div> | |
<div class="user__box"> | |
<input | |
type="text" | |
required | |
minlength="10" | |
maxlength="50" | |
v-model="form.metadata" | |
/> | |
<label>Description</label> | |
</div> | |
<button type="submit"> | |
<span></span> | |
<span></span> | |
<span></span> | |
<span></span> | |
{{ requesting ? "Log..." : "Update" }} | |
</button> | |
<router-link class="links" to="/">Back</router-link> | |
</form> | |
</div> | |
</template> | |
<script> | |
import { CometChat } from "@cometchat-pro/chat"; | |
import { auth } from "../firebase"; | |
export default { | |
name: "profile-update", | |
data() { | |
return { | |
requesting: false, | |
form: { | |
fullname: "", | |
avatar: "", | |
age: "", | |
metadata: "", | |
}, | |
}; | |
}, | |
created() { | |
this.getUser(); | |
}, | |
methods: { | |
onSubmit() { | |
this.requesting = true; | |
auth.currentUser | |
.updateProfile({ | |
photoURL: this.form.avatar, | |
displayName: this.form.fullname, | |
}) | |
.then(() => this.setUser()) | |
.catch((error) => console.log("Error updating user:", error)) | |
}, | |
getUser() { | |
const uid = auth.currentUser.uid; | |
CometChat.getUser(uid) | |
.then((user) => { | |
this.form = { | |
...user, | |
fullname: user.name, | |
avatar: user.avatar || "", | |
metadata: user.metadata?.rawMetadata || "", | |
age: user.metadata?.age || "", | |
}; | |
}) | |
.catch((error) => console.log(error)); | |
}, | |
setUser() { | |
const apiKey = process.env.VUE_APP_KEY; | |
const uid = auth.currentUser.uid; | |
var user = new CometChat.User(uid); | |
user.setName(this.form.fullname); | |
user.setAvatar(this.form.avatar); | |
user.setMetadata({ rawMetadata: this.form.metadata, age: this.form.age }); | |
CometChat.updateUser(user, apiKey) | |
.then(() => this.$router.push({ name: "home" })) | |
.catch((error) => console.log(error)) | |
.finally(() => this.requesting = false); | |
}, | |
}, | |
}; | |
</script> | |
<style> | |
html { | |
background-color: #e90d77; | |
} | |
.content { | |
background: none; | |
} | |
.wrapper { | |
position: unset; | |
} | |
.login__box { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
width: 300px; | |
padding: 40px; | |
transform: translate(-50%, -50%); | |
background: rgba(0, 0, 0, 0.5); | |
box-sizing: border-box; | |
box-shadow: 0 15px 25px rgba(0, 0, 0, 0.6); | |
border-radius: 10px; | |
} | |
.login__box h2 { | |
margin: 0 0 30px; | |
padding: 0; | |
color: #fff; | |
text-align: center; | |
} | |
.login__box .user__box { | |
position: relative; | |
} | |
.login__box .user__box input { | |
width: 100%; | |
padding: 10px 0; | |
font-size: 16px; | |
color: #fff; | |
margin-bottom: 30px; | |
border: none; | |
border-bottom: 1px solid #fff; | |
outline: none; | |
background: transparent; | |
} | |
.login__box .user__box label { | |
position: absolute; | |
top: 0; | |
left: 0; | |
padding: 10px 0; | |
font-size: 16px; | |
color: #fff; | |
pointer-events: none; | |
transition: 0.5s; | |
} | |
.login__box .user__box input:focus ~ label, | |
.login__box .user__box input:valid ~ label { | |
top: -20px; | |
left: 0; | |
color: #e90d77; | |
font-size: 12px; | |
} | |
.login__box form button { | |
position: relative; | |
display: inline-block; | |
padding: 10px 20px; | |
color: #e90d77; | |
font-size: 16px; | |
text-decoration: none; | |
text-transform: uppercase; | |
overflow: hidden; | |
transition: 0.5s; | |
margin-top: 40px; | |
letter-spacing: 4px; | |
background: none; | |
border: none; | |
} | |
.login__box button:hover { | |
background: #e90d77; | |
color: #fff; | |
border-radius: 5px; | |
box-shadow: 0 0 5px #e90d77, 0 0 25px #e90d77, 0 0 50px #e90d77, | |
0 0 100px #e90d77; | |
} | |
.login__box button span { | |
position: absolute; | |
display: block; | |
} | |
.login__box button span:nth-child(1) { | |
top: 0; | |
left: -100%; | |
width: 100%; | |
height: 2px; | |
background: linear-gradient(90deg, transparent, #e90d77); | |
animation: btn-anim1 1s linear infinite; | |
} | |
.login__box form .forget__link { | |
color: white; | |
text-decoration: none; | |
} | |
.login__box form .forget__link:hover { | |
color: #e90d77; | |
} | |
.login__box form .links { | |
color: white; | |
text-decoration: none; | |
margin-left: 5px; | |
padding: 10px; | |
} | |
.login__box form .links:hover { | |
background: #e90d77; | |
color: #fff; | |
border-radius: 5px; | |
box-shadow: 0 0 5px #e90d77, 0 0 25px #e90d77, 0 0 50px #e90d77, | |
0 0 100px #e90d77; | |
} | |
@keyframes btn-anim1 { | |
0% { | |
left: -100%; | |
} | |
50%, | |
100% { | |
left: 100%; | |
} | |
} | |
.login__box button span:nth-child(2) { | |
top: -100%; | |
right: 0; | |
width: 2px; | |
height: 100%; | |
background: linear-gradient(180deg, transparent, #e90d77); | |
animation: btn-anim2 1s linear infinite; | |
animation-delay: 0.25s; | |
} | |
@keyframes btn-anim2 { | |
0% { | |
top: -100%; | |
} | |
50%, | |
100% { | |
top: 100%; | |
} | |
} | |
.login__box button span:nth-child(3) { | |
bottom: 0; | |
right: -100%; | |
width: 100%; | |
height: 2px; | |
background: linear-gradient(270deg, transparent, #e90d77); | |
animation: btn-anim3 1s linear infinite; | |
animation-delay: 0.5s; | |
} | |
@keyframes btn-anim3 { | |
0% { | |
right: -100%; | |
} | |
50%, | |
100% { | |
right: 100%; | |
} | |
} | |
.login__box a span:nth-child(4) { | |
bottom: -100%; | |
left: 0; | |
width: 2px; | |
height: 100%; | |
background: linear-gradient(360deg, transparent, #e90d77); | |
animation: btn-anim4 1s linear infinite; | |
animation-delay: 0.75s; | |
} | |
@keyframes btn-anim4 { | |
0% { | |
bottom: -100%; | |
} | |
50%, | |
100% { | |
bottom: 100%; | |
} | |
} | |
</style> |
The Home Component
The Home component carries two child components, the MainHeader and TinderCards components. Other than its beautiful design it also interacts with the comet chat SDK. Here is how it functions.
On create, the Home component retrieves the list of users within our comet chat account. After retrieval, it serves them to the TinderCards child component. The code below illustrates my point better.
Here is the full code of the Home component.
<template> | |
<div id="home"> | |
<div class="content-wrapper"> | |
<div class="content"> | |
<MainHeader /> | |
<TinderCards :uid="uid" :users="swipables" /> | |
</div> | |
</div> | |
<SideBar /> | |
</div> | |
</template> | |
<script> | |
import { auth } from "../firebase"; | |
import { CometChat } from "@cometchat-pro/chat"; | |
import MainHeader from "../shared/MainHeader"; | |
import TinderCards from "../components/TinderCards"; | |
import SideBar from "../shared/SideBar"; | |
export default { | |
name: "home", | |
data() { | |
return { | |
swipables: [], | |
uid: "", | |
}; | |
}, | |
created() { | |
this.uid = auth.currentUser?.uid || "99999"; | |
this.getUsers(); | |
}, | |
methods: { | |
getUsers() { | |
CometChat.getUser(this.uid) | |
.then(() => { | |
let usersRequest = new CometChat.UsersRequestBuilder() | |
.setLimit(30) | |
.build(); | |
usersRequest | |
.fetchNext() | |
.then((users) => { | |
this.swipables = users.filter( | |
(u) => (u.id = Date.now() + (Math.random() * 100000).toFixed()) | |
); | |
}) | |
.catch((error) => console.log(error)); | |
}) | |
.catch((error) => console.log(error)); | |
}, | |
}, | |
components: { | |
MainHeader, | |
TinderCards, | |
SideBar, | |
}, | |
}; | |
</script> | |
<style scoped> | |
html { | |
background-color: #f9f9f9; | |
} | |
.wrapper { | |
position: relative; | |
} | |
.content-wrapper { | |
float: right; | |
width: 100%; | |
} | |
.content { | |
margin-left: 320px; | |
clear: both; | |
overflow: auto; | |
background: #f9f9ff; | |
min-height: 100vh; | |
} | |
@media only screen and (max-width: 768px) { | |
.content { | |
margin-left: 0; | |
} | |
} | |
</style> |
The Child Components
While the MainHeader child component displays the navigation buttons. The TinderCards child component showcases the cards along with the well-styled buttons. Here are their respective codes.
<template> | |
<div id="main-header"> | |
<router-link class="header__icon mobile--only" to="/friends"> | |
<AccountGroupIcon fillColor="#989898" :size="30" /> | |
</router-link> | |
<router-link class="header__icon" to="/"> | |
<FireIcon fillColor="#fd5068" :size="40" /> | |
</router-link> | |
<router-link class="header__icon mobile--only" to="/profile"> | |
<AccountIcon fillColor="#989898" :size="30" /> | |
</router-link> | |
</div> | |
</template> | |
<script> | |
import AccountGroupIcon from "vue-material-design-icons/AccountGroup.vue"; | |
import FireIcon from "vue-material-design-icons/Fire.vue"; | |
import AccountIcon from "vue-material-design-icons/Account.vue"; | |
export default { | |
name: "main-header", | |
components: { | |
AccountGroupIcon, | |
FireIcon, | |
AccountIcon, | |
}, | |
}; | |
</script> | |
<style scoped> | |
#main-header { | |
display: flex; | |
align-items: center; | |
justify-content: space-around; | |
z-index: 100; | |
border-bottom: 1px solid #f5f0f0c2; | |
padding: 2px; | |
} | |
.header__icon { | |
cursor: pointer; | |
padding: 20px; | |
border-radius: 15px; | |
background: transparent; | |
} | |
.header__icon:hover { | |
transform: scale(1.06); | |
transition: all 0.2s ease-in-out; | |
} | |
@media only screen and (min-width: 768px) { | |
.mobile--only { | |
display: none; | |
} | |
} | |
</style> |
<template> | |
<div class="tinder__card"> | |
<div class="tinder__card__container"> | |
<vue-swing | |
v-for="user in users" | |
:key="user.id" | |
@throwout="swipped(user)" | |
:config="config" | |
class="swipe" | |
> | |
<div | |
class="card" | |
:style="{ 'background-image': `url(${user.avatar})` }" | |
> | |
<div class="card__content"> | |
<h3>{{ user.name }}</h3> | |
</div> | |
</div> | |
</vue-swing> | |
<div class="swipe__icons"> | |
<CloseIcon | |
:size="30" | |
fillColor="#ec5e6f" | |
class="swipe__icon swipe__icon__close" | |
@click="reject()" | |
/> | |
<StarIcon | |
:size="30" | |
fillColor="#62b4f9" | |
class="swipe__icon swipe__icon__star" | |
@click="onFavorite()" | |
v-if="favored" | |
/> | |
<StarOutlineIcon | |
:size="30" | |
fillColor="#62b4f9" | |
class="swipe__icon swipe__icon__star" | |
@click="onFavorite()" | |
v-else | |
/> | |
<HeartIcon | |
:size="30" | |
fillColor="#76e2b3" | |
class="swipe__icon swipe__icon__heart" | |
@click="onRequest()" | |
v-if="requested" | |
/> | |
<HeartOutlineIcon | |
:size="30" | |
fillColor="#76e2b3" | |
class="swipe__icon swipe__icon__heart" | |
@click="onRequest()" | |
v-else | |
/> | |
<PowerIcon | |
:size="30" | |
fillColor="#ec5e6f" | |
class="swipe__icon swipe__icon__power" | |
@click="logOut()" | |
/> | |
</div> | |
</div> | |
</div> | |
</template> | |
<script> | |
import { auth } from "../firebase"; | |
import { CometChat } from "@cometchat-pro/chat"; | |
import VueSwing from "vue-swing"; | |
import CloseIcon from "vue-material-design-icons/Close.vue"; | |
import PowerIcon from "vue-material-design-icons/Power.vue"; | |
import StarIcon from "vue-material-design-icons/Star.vue"; | |
import StarOutlineIcon from "vue-material-design-icons/StarOutline.vue"; | |
import HeartIcon from "vue-material-design-icons/Heart.vue"; | |
import HeartOutlineIcon from "vue-material-design-icons/HeartOutline.vue"; | |
export default { | |
name: "tinder-cards", | |
props: { | |
users: { | |
type: [Object, Array], | |
default: function () { | |
return [ | |
{ | |
uid: "1", | |
name: "Fullname", | |
avatar: | |
"https://hips.hearstapps.com/hmg-prod.s3.amazonaws.com/images/newborn-baby-boy-sleeping-peacefully-wearing-knit-royalty-free-image-1589459736.jpg?crop=0.669xw:1.00xh;0.228xw,0&resize=640:*", | |
metadata: { age: "21", rawMetadata: "Some Text Here!" }, | |
}, | |
]; | |
}, | |
}, | |
uid: { | |
type: String | |
} | |
}, | |
data() { | |
return { | |
config: { | |
allowedDirections: [VueSwing.Direction.LEFT, VueSwing.Direction.RIGHT], | |
minThrowOutDistance: 250, | |
maxThrowOutDistance: 300, | |
}, | |
favorites: [], | |
requests: [], | |
}; | |
}, | |
created() { | |
this.getUser(); | |
}, | |
components: { | |
VueSwing, | |
CloseIcon, | |
PowerIcon, | |
StarIcon, | |
StarOutlineIcon, | |
HeartIcon, | |
HeartOutlineIcon, | |
}, | |
methods: { | |
logOut() { | |
auth | |
.signOut() | |
.catch((error) => console.log(error.message)) | |
.finally(() => this.$router.push({ name: "login" })); | |
}, | |
reject() { | |
this.swipped(this.currentCard); | |
}, | |
swipped(user) { | |
const index = this.users.findIndex((u) => u.uid == user.uid); | |
this.users.splice(index, 1); | |
user.id = Date.now() + (Math.random() * 100000).toFixed(); | |
this.users.unshift({ ...user }); | |
}, | |
getUser() { | |
CometChat.getUser(this.uid) | |
.then((user) => { | |
this.favorites = user.metadata?.favorites || []; | |
this.requests = user.metadata?.requests || []; | |
}) | |
.catch((error) => console.log(error)); | |
}, | |
onRequest() { | |
const data = { ...this.currentCard }; | |
const apiKey = process.env.VUE_APP_KEY; | |
if (!this.requests.includes(data.uid)) { | |
this.requests.push(data.uid); | |
} else { | |
const index = this.requests.findIndex((f) => f == data.uid); | |
this.requests.splice(index, 1); | |
} | |
const user = new CometChat.User(this.uid); | |
user.setMetadata({ | |
...data.metadata, | |
favorites: this.favorites, | |
requests: this.requests, | |
}); | |
CometChat.updateUser(user, apiKey) | |
.then(() => console.log(user)) | |
.catch((error) => console.log(error)); | |
}, | |
onFavorite() { | |
const data = { ...this.currentCard }; | |
const apiKey = process.env.VUE_APP_KEY; | |
if (!this.favorites.includes(data.uid)) { | |
this.favorites.push(data.uid); | |
} else { | |
const index = this.favorites.findIndex((f) => f == data.uid); | |
this.favorites.splice(index, 1); | |
} | |
const user = new CometChat.User(this.uid); | |
user.setMetadata({ | |
...data.metadata, | |
favorites: this.favorites, | |
requests: this.requests, | |
}); | |
CometChat.updateUser(user, apiKey) | |
.then(() => console.log(user)) | |
.catch((error) => console.log(error)); | |
}, | |
}, | |
computed: { | |
currentCard() { | |
return this.users[this.users.length - 1]; | |
}, | |
favored() { | |
return this.favorites.includes(this.currentCard?.uid || 99999); | |
}, | |
requested() { | |
return this.requests.includes(this.currentCard?.uid || 99999); | |
}, | |
}, | |
}; | |
</script> | |
<style scoped> | |
.card { | |
position: relative; | |
background-color: #fff; | |
width: 250px; | |
padding: 20px; | |
max-width: 85vw; | |
height: 50vh; | |
border-radius: 9px; | |
background-size: cover; | |
background-position: center; | |
} | |
.tinder__card__container { | |
display: flex; | |
justify-content: center; | |
margin-top: 10vh; | |
} | |
.swipe { | |
position: absolute; | |
} | |
.card__content { | |
width: 100%; | |
height: 100%; | |
} | |
.card h3 { | |
position: absolute; | |
bottom: 0; | |
margin: 10px; | |
color: #fff; | |
background: linear-gradient( | |
109deg, | |
rgba(0, 0, 0, 0.5) 0%, | |
rgba(0, 0, 0, 0) 100% | |
); | |
border-radius: 9px; | |
padding: 5px; | |
} | |
.swipe__icons__container { | |
display: flex; | |
justify-content: center; | |
} | |
.swipe__icons { | |
position: fixed; | |
bottom: 5vh; | |
display: flex; | |
width: 250px; | |
justify-content: space-evenly; | |
} | |
.swipe__icon__close:hover, | |
.swipe__icon__star:hover, | |
.swipe__icon__heart:hover { | |
transform: scale(1.06); | |
transition: all 0.2s ease-in-out; | |
} | |
.swipe__icons .material-design-icon__svg { | |
background-color: white; | |
box-shadow: 0px 5px 10px 0px rgba(0, 0, 0, 0.2) !important; | |
border-radius: 30px; | |
padding: 10px; | |
} | |
@media only screen and (min-width: 768px) { | |
.swipe__icon__power { | |
display: none; | |
} | |
} | |
</style> |
The Chat Component
The Chat component lunches a warm and gorgeous chat UI that gives "Tinder.com" a run for its money (smiles). It gives a user the ability to engage in text conversations. Let's look at the code responsible for this functionality.
<template> | |
<div id="chats"> | |
<div class="content-wrapper"> | |
<div class="content"> | |
<section class="msger"> | |
<header class="msger-header"> | |
<div class="msger-header-title"> | |
<div class="msger__avatar"> | |
<CometChatAvatar :image="user.avatar" /> | |
</div> | |
<div class="msger__avatar__details"> | |
<p>{{ user.name }}</p> | |
<small>{{ user.status }}</small> | |
</div> | |
</div> | |
</header> | |
<main ref="container" class="msger-chat"> | |
<div | |
class="msg-list" | |
v-for="message in messages" | |
:key="message.sentAt" | |
> | |
<div v-if="typeof message.text != 'undefined'"> | |
<div | |
v-if="message.receiverId !== user.uid" | |
class="msg left-msg" | |
> | |
<div class="msg--wrapper"> | |
<div class="bubble--wrapper"> | |
<CometChatAvatar | |
:image="user.avatar" | |
class="msg-img" | |
style="width: 50px; height: 50px" | |
/> | |
<div class="msg-bubble"> | |
<div class="msg-info"> | |
<div class="msg-info-name">{{ user.name }}</div> | |
</div> | |
<div class="msg-text">{{ message.text }}</div> | |
</div> | |
</div> | |
<div class="msg-info-time"> | |
{{ toReadableString(message.sentAt) }} | |
</div> | |
</div> | |
</div> | |
<div v-else class="msg right-msg"> | |
<div class="msg--wrapper"> | |
<div class="bubble--wrapper"> | |
<CometChatAvatar | |
:image="message.sender.avatar" | |
class="msg-img" | |
style="width: 50px; height: 50px" | |
/> | |
<div class="msg-bubble"> | |
<div class="msg-info"> | |
<div class="msg-info-name"> | |
{{ message.sender.name }} | |
</div> | |
</div> | |
<div class="msg-text">{{ message.text }}</div> | |
</div> | |
</div> | |
<div class="msg-info-time"> | |
<img | |
v-if=" | |
message.sentAt && | |
!message.deliveredAt && | |
!message.readAt | |
" | |
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAFo9M/3AAAAAXNSR0IArs4c6QAAARdJREFUOBFjYEAGIiIiUQg+kBeB4EFZYmJiVnDB////M8I5WBmSkpImIAkmmCxQy0MYmwFogxGcQxYDaEIaTCPcKSA3/vv3T56JiYn11atXi2AKBopGdiQLuiNERUU9WVlZD8HEwSElLi4eBZTgcXBwYAGGmOyzZ89uwBTAvQk19p+AgMDiO3fu/IQpQKGBptigCNCFw0ysLSAPfvv2zZ+bm1vo69evj2H64OkFJoCNVlFR4bty5UoCUI6Xj4/vLDY1YDEFBQUOkE3ICiQkJBRAIQhMLAHY0jE8mEGagPFh8ffvXz0g8xMjI+MWFhYWjd+/fxsB2ddev359BNlgGBvFAJgg0DZrYNLUBvHZ2NgOIUcsTM3goQFXzEj1uzh+EwAAAABJRU5ErkJggg==" | |
alt="time" | |
class="message__timestamp__img" | |
/> | |
<img | |
v-else-if=" | |
message.sentAt && | |
message.deliveredAt && | |
!message.readAt | |
" | |
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAFo9M/3AAAAAXNSR0IArs4c6QAAAWNJREFUOBFjYEAGEhISosh8BgaQCCNMSFJS0gTGZhARETECYbgAMgOuRUxMzAokwQhUGgGkmYD435s3b1aABBn+///PCMJgDskE0Mg0kCYYDTYGZOG/f//kmZiYWIFGr2NkZAx69erVIpJNp5IGmONgNIpfRUVFPVlZWe//+fNHnoWF5eGzZ89uMImLi0cBJXgcHBxYgD6Qtba2vg3yEUgS5CiwCVDj/gkICCz++PFjND8//9I7d+78RHE10BQbkACMRpGkKYeZkOkgz3379s2fm5tb6OvXr4/R+SjBhG6YiooK34cPH8KAcfyLj49vJVCeHRgIocC4/g3iwwNCQUGBA2QysgHAFK0ACj1gQgkApT0pKSk5ZD5MLdgFwLiw+Pv3rx5Q8BPQ9C3ASNL4/fu3EZB97fXr10eAIWsINMQUxodpBtEoXgDaZg2MRG2QBBsb2yFQZAINdwEargTjg+QGFwAAd+eJSUtUxzMAAAAASUVORK5CYII=" | |
alt="time" | |
class="message__timestamp__img" | |
/> | |
<img | |
v-else | |
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAFo9M/3AAAAAXNSR0IArs4c6QAAAWdJREFUOBHNUrtKA1EQPXeNRUDxgVqLQj7AjWjAwh8QKyvbuIIgVhYWNhYWdhFBkugPpLOwt7CwSEQLrTQI1roB44Os7nhm4YZdCyWg4MDsmTOzM3dm7gXiMlUWN84B9RjrcouyBQOJOMmmqg0mMJ5S0IjhrzfELmpQWzYZdUJEjGpEOv6wZNSKxagMScEYzDHU63RjIgxwzvOGO67+Swm2OYupeN1sSY650DzxSFFjxi3JXcpgxunDQ8tHc2wA6bqPV06h24NT88zoe4h7Bv10D4bqDTwqajAh2aLsqcNiIvin5McbzVUkHTRwwms642BrX/m3BbjLcbZ/RX3i4JnWC/pDwTWLNZWfLhrf0flyBzKoldW2ki3LPG19jBdVDyNvz5j+CFFn8qVyTdZ/ow64uR2+sXXy25SDWa7do62Pfp9tr0yWZIMnb1tObEtiBN7wLk9Y1ahxkK8umUOOUSFdsLyd+W+MTyDvnh60pF/jAAAAAElFTkSuQmCC" | |
alt="time" | |
class="message__timestamp__img" | |
/> | |
{{ toReadableString(message.sentAt) }} | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="spacer"></div> | |
</main> | |
<form @submit.prevent="sendMessage()" class="msger-inputarea"> | |
<input | |
type="text" | |
class="msger-input" | |
placeholder="Enter your message..." | |
v-model.trim="message" | |
/> | |
<button type="submit" class="msger-send-btn">Send</button> | |
</form> | |
</section> | |
</div> | |
</div> | |
<SideBar /> | |
</div> | |
</template> | |
<script> | |
import { CometChat } from "@cometchat-pro/chat"; | |
import { CometChatAvatar } from "../cometchat-pro-vue-ui-kit"; | |
import SideBar from "../shared/SideBar"; | |
export default { | |
name: "chats", | |
props: { | |
uid: { | |
type: String, | |
required: true, | |
}, | |
}, | |
data() { | |
return { | |
messages: [], | |
message: "", | |
user: {}, | |
}; | |
}, | |
components: { | |
SideBar, | |
CometChatAvatar, | |
}, | |
created() { | |
this.getMessages(); | |
this.getUser(); | |
this.listenForMessage(); | |
}, | |
destroyed() { | |
CometChat.removeMessageListener(this.uid) | |
}, | |
updated() { | |
this.scrollToEnd() | |
}, | |
methods: { | |
getUser() { | |
const uid = this.uid; | |
CometChat.getUser(uid) | |
.then((user) => (this.user = user)) | |
.catch((error) => console.log(error)); | |
}, | |
getMessages() { | |
const limit = 50; | |
const messagesRequest = new CometChat.MessagesRequestBuilder() | |
.setLimit(limit) | |
.setUID(this.uid) | |
.build(); | |
messagesRequest | |
.fetchPrevious() | |
.then((messages) => { | |
messages.map((message, i) => { | |
if (!message.readAt) { | |
const messageId = message.id; | |
const receiverId = message.sender.uid; | |
const receiverType = "user"; | |
console.log({ i: i + 1, l: messages.length, r: receiverId, u: this.uid }); | |
if (i + 1 === messages.length && receiverId === this.uid) | |
CometChat.markAsRead(messageId, receiverId, receiverType); | |
} | |
}); | |
this.messages = messages; | |
}) | |
.catch((error) => console.log("Message fetching failed with error:", error)); | |
}, | |
sendMessage() { | |
const receiverID = this.uid; | |
const messageText = this.message; | |
const receiverType = CometChat.RECEIVER_TYPE.USER; | |
const textMessage = new CometChat.TextMessage( | |
receiverID, | |
messageText, | |
receiverType | |
); | |
CometChat.sendMessage(textMessage) | |
.then((message) => { | |
this.message = ""; | |
this.messages.push(message); | |
}) | |
.catch((error) => console.log("Message sending failed with error:", error)); | |
}, | |
listenForMessage() { | |
const listenerID = this.uid; | |
CometChat.addMessageListener( | |
listenerID, | |
new CometChat.MessageListener({ | |
onTextMessageReceived: (messageReceipt) => { | |
if (this.uid === messageReceipt.sender.uid) { | |
this.messages.push(messageReceipt); | |
const messageId = messageReceipt.id; | |
const receiverId = messageReceipt.sender.uid; | |
const receiverType = "user"; | |
CometChat.markAsRead(messageId, receiverId, receiverType); | |
} | |
}, | |
onMessagesDelivered: (messageReceipt) => { | |
this.messages.filter((msg) => (msg.deliveredAt = messageReceipt.deliveredAt)); | |
}, | |
onMessagesRead: (messageReceipt) => { | |
this.messages.filter((msg) => (msg.readAt = messageReceipt.readAt)); | |
}, | |
}) | |
); | |
}, | |
scrollToEnd () { | |
const elmnt = this.$refs.container; | |
elmnt.scrollTop = elmnt.scrollHeight; | |
}, | |
toReadableString(time) { | |
if (time < 0) time = 0; | |
let hrs = ~~((time / 3600) % 24), | |
mins = ~~((time % 3600) / 60), | |
timeType = hrs > 11 ? "PM" : "AM"; | |
if (hrs > 12) hrs = hrs - 12; | |
if (hrs == 0) hrs = 12; | |
if (mins < 10) mins = "0" + mins; | |
return hrs + ":" + mins + timeType; | |
}, | |
}, | |
watch: { | |
messages() { | |
this.scrollToEnd() | |
} | |
} | |
}; | |
</script> | |
<style scoped> | |
html { | |
background-color: #f9f9f9; | |
} | |
.wrapper { | |
position: relative; | |
} | |
.content-wrapper { | |
float: right; | |
width: 100%; | |
} | |
.content { | |
margin-left: 320px; | |
clear: both; | |
background: #f9f9ff; | |
overflow-y: hidden; | |
height: 100vh; | |
} | |
html { | |
box-sizing: border-box; | |
} | |
*, | |
*:before, | |
*:after { | |
margin: 0; | |
padding: 0; | |
box-sizing: inherit; | |
} | |
body { | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
height: 100vh; | |
background-image: var(--body-bg); | |
font-family: Helvetica, sans-serif; | |
} | |
.msger { | |
height: calc(100% - 50px); | |
border: var(--border); | |
border-radius: 5px; | |
background: var(--msger-bg); | |
box-shadow: 0 15px 15px -5px rgba(0, 0, 0, 0.2); | |
} | |
.msger-header { | |
display: flex; | |
justify-content: space-between; | |
padding: 10px; | |
border-bottom: var(--border); | |
background: #ececff; | |
color: #666; | |
} | |
.msger-header-title { | |
display: flex; | |
align-items: center; | |
} | |
.msger-chat { | |
flex: 1; | |
overflow-y: auto; | |
padding: 10px; | |
height: 100vh; | |
} | |
.msger-chat::-webkit-scrollbar { | |
width: 6px; | |
} | |
.msger-chat::-webkit-scrollbar-track { | |
background: #ddd; | |
} | |
.msger-chat::-webkit-scrollbar-thumb { | |
background: #bdbdbd; | |
} | |
.msg { | |
display: flex; | |
align-items: flex-end; | |
} | |
.msg-list { | |
margin: 10px 0; | |
} | |
.msg-img { | |
width: 50px; | |
height: 50px; | |
margin-right: 10px; | |
background: #ddd; | |
background-repeat: no-repeat; | |
background-position: center; | |
background-size: cover; | |
border-radius: 50%; | |
} | |
.msg-bubble { | |
max-width: 450px; | |
padding: 15px; | |
border-radius: 15px; | |
background: var(--left-msg-bg); | |
} | |
.msg-info { | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
margin-bottom: 10px; | |
} | |
.msg-info-name { | |
margin-right: 10px; | |
font-weight: bold; | |
} | |
.msg-info-time { | |
font-size: 0.85em; | |
display: flex; | |
margin-top: 10px; | |
} | |
.left-msg .msg-bubble { | |
border-bottom-left-radius: 0; | |
} | |
.right-msg { | |
flex-direction: row-reverse; | |
} | |
.right-msg .msg-bubble { | |
background: rgb(233, 12, 119); | |
background: linear-gradient( | |
90deg, | |
rgba(233, 12, 119, 1) 0%, | |
rgba(241, 96, 89, 1) 100% | |
); | |
color: #fff; | |
border-bottom-right-radius: 0; | |
} | |
.right-msg .msg-img { | |
margin: 0 0 0 10px; | |
} | |
.left-msg .msg-bubble { | |
display: inline-block; | |
border-radius: 12px; | |
background-color: rgb(246, 246, 246); | |
padding: 8px 12px; | |
align-self: flex-start; | |
width: auto; | |
} | |
.msger-inputarea { | |
padding: 10px; | |
border-top: var(--border); | |
background: #eee; | |
position: fixed; | |
bottom: 0; | |
width: 100vw; | |
} | |
.msger-inputarea * { | |
padding: 10px; | |
border: none; | |
border-radius: 3px; | |
font-size: 1em; | |
} | |
.msger-input { | |
flex: 1; | |
width: calc(100% - 425px); | |
background: #ddd; | |
} | |
.msger-send-btn { | |
margin-left: 10px; | |
background: rgb(233, 12, 119); | |
color: #fff; | |
font-weight: bold; | |
cursor: pointer; | |
transition: background 0.23s; | |
} | |
.msger-send-btn:hover { | |
background: rgb(241, 96, 89); | |
} | |
.msger-chat { | |
background-color: #fcfcfe; | |
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='260' height='260' viewBox='0 0 260 260'%3E%3Cg fill-rule='evenodd'%3E%3Cg fill='%23dddddd' fill-opacity='0.4'%3E%3Cpath d='M24.37 16c.2.65.39 1.32.54 2H21.17l1.17 2.34.45.9-.24.11V28a5 5 0 0 1-2.23 8.94l-.02.06a8 8 0 0 1-7.75 6h-20a8 8 0 0 1-7.74-6l-.02-.06A5 5 0 0 1-17.45 28v-6.76l-.79-1.58-.44-.9.9-.44.63-.32H-20a23.01 23.01 0 0 1 44.37-2zm-36.82 2a1 1 0 0 0-.44.1l-3.1 1.56.89 1.79 1.31-.66a3 3 0 0 1 2.69 0l2.2 1.1a1 1 0 0 0 .9 0l2.21-1.1a3 3 0 0 1 2.69 0l2.2 1.1a1 1 0 0 0 .9 0l2.21-1.1a3 3 0 0 1 2.69 0l2.2 1.1a1 1 0 0 0 .86.02l2.88-1.27a3 3 0 0 1 2.43 0l2.88 1.27a1 1 0 0 0 .85-.02l3.1-1.55-.89-1.79-1.42.71a3 3 0 0 1-2.56.06l-2.77-1.23a1 1 0 0 0-.4-.09h-.01a1 1 0 0 0-.4.09l-2.78 1.23a3 3 0 0 1-2.56-.06l-2.3-1.15a1 1 0 0 0-.45-.11h-.01a1 1 0 0 0-.44.1L.9 19.22a3 3 0 0 1-2.69 0l-2.2-1.1a1 1 0 0 0-.45-.11h-.01a1 1 0 0 0-.44.1l-2.21 1.11a3 3 0 0 1-2.69 0l-2.2-1.1a1 1 0 0 0-.45-.11h-.01zm0-2h-4.9a21.01 21.01 0 0 1 39.61 0h-2.09l-.06-.13-.26.13h-32.31zm30.35 7.68l1.36-.68h1.3v2h-36v-1.15l.34-.17 1.36-.68h2.59l1.36.68a3 3 0 0 0 2.69 0l1.36-.68h2.59l1.36.68a3 3 0 0 0 2.69 0L2.26 23h2.59l1.36.68a3 3 0 0 0 2.56.06l1.67-.74h3.23l1.67.74a3 3 0 0 0 2.56-.06zM-13.82 27l16.37 4.91L18.93 27h-32.75zm-.63 2h.34l16.66 5 16.67-5h.33a3 3 0 1 1 0 6h-34a3 3 0 1 1 0-6zm1.35 8a6 6 0 0 0 5.65 4h20a6 6 0 0 0 5.66-4H-13.1z'/%3E%3Cpath id='path6_fill-copy' d='M284.37 16c.2.65.39 1.32.54 2H281.17l1.17 2.34.45.9-.24.11V28a5 5 0 0 1-2.23 8.94l-.02.06a8 8 0 0 1-7.75 6h-20a8 8 0 0 1-7.74-6l-.02-.06a5 5 0 0 1-2.24-8.94v-6.76l-.79-1.58-.44-.9.9-.44.63-.32H240a23.01 23.01 0 0 1 44.37-2zm-36.82 2a1 1 0 0 0-.44.1l-3.1 1.56.89 1.79 1.31-.66a3 3 0 0 1 2.69 0l2.2 1.1a1 1 0 0 0 .9 0l2.21-1.1a3 3 0 0 1 2.69 0l2.2 1.1a1 1 0 0 0 .9 0l2.21-1.1a3 3 0 0 1 2.69 0l2.2 1.1a1 1 0 0 0 .86.02l2.88-1.27a3 3 0 0 1 2.43 0l2.88 1.27a1 1 0 0 0 .85-.02l3.1-1.55-.89-1.79-1.42.71a3 3 0 0 1-2.56.06l-2.77-1.23a1 1 0 0 0-.4-.09h-.01a1 1 0 0 0-.4.09l-2.78 1.23a3 3 0 0 1-2.56-.06l-2.3-1.15a1 1 0 0 0-.45-.11h-.01a1 1 0 0 0-.44.1l-2.21 1.11a3 3 0 0 1-2.69 0l-2.2-1.1a1 1 0 0 0-.45-.11h-.01a1 1 0 0 0-.44.1l-2.21 1.11a3 3 0 0 1-2.69 0l-2.2-1.1a1 1 0 0 0-.45-.11h-.01zm0-2h-4.9a21.01 21.01 0 0 1 39.61 0h-2.09l-.06-.13-.26.13h-32.31zm30.35 7.68l1.36-.68h1.3v2h-36v-1.15l.34-.17 1.36-.68h2.59l1.36.68a3 3 0 0 0 2.69 0l1.36-.68h2.59l1.36.68a3 3 0 0 0 2.69 0l1.36-.68h2.59l1.36.68a3 3 0 0 0 2.56.06l1.67-.74h3.23l1.67.74a3 3 0 0 0 2.56-.06zM246.18 27l16.37 4.91L278.93 27h-32.75zm-.63 2h.34l16.66 5 16.67-5h.33a3 3 0 1 1 0 6h-34a3 3 0 1 1 0-6zm1.35 8a6 6 0 0 0 5.65 4h20a6 6 0 0 0 5.66-4H246.9z'/%3E%3Cpath d='M159.5 21.02A9 9 0 0 0 151 15h-42a9 9 0 0 0-8.5 6.02 6 6 0 0 0 .02 11.96A8.99 8.99 0 0 0 109 45h42a9 9 0 0 0 8.48-12.02 6 6 0 0 0 .02-11.96zM151 17h-42a7 7 0 0 0-6.33 4h54.66a7 7 0 0 0-6.33-4zm-9.34 26a8.98 8.98 0 0 0 3.34-7h-2a7 7 0 0 1-7 7h-4.34a8.98 8.98 0 0 0 3.34-7h-2a7 7 0 0 1-7 7h-4.34a8.98 8.98 0 0 0 3.34-7h-2a7 7 0 0 1-7 7h-7a7 7 0 1 1 0-14h42a7 7 0 1 1 0 14h-9.34zM109 27a9 9 0 0 0-7.48 4H101a4 4 0 1 1 0-8h58a4 4 0 0 1 0 8h-.52a9 9 0 0 0-7.48-4h-42z'/%3E%3Cpath d='M39 115a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm6-8a6 6 0 1 1-12 0 6 6 0 0 1 12 0zm-3-29v-2h8v-6H40a4 4 0 0 0-4 4v10H22l-1.33 4-.67 2h2.19L26 130h26l3.81-40H58l-.67-2L56 84H42v-6zm-4-4v10h2V74h8v-2h-8a2 2 0 0 0-2 2zm2 12h14.56l.67 2H22.77l.67-2H40zm13.8 4H24.2l3.62 38h22.36l3.62-38z'/%3E%3Cpath d='M129 92h-6v4h-6v4h-6v14h-3l.24 2 3.76 32h36l3.76-32 .24-2h-3v-14h-6v-4h-6v-4h-8zm18 22v-12h-4v4h3v8h1zm-3 0v-6h-4v6h4zm-6 6v-16h-4v19.17c1.6-.7 2.97-1.8 4-3.17zm-6 3.8V100h-4v23.8a10.04 10.04 0 0 0 4 0zm-6-.63V104h-4v16a10.04 10.04 0 0 0 4 3.17zm-6-9.17v-6h-4v6h4zm-6 0v-8h3v-4h-4v12h1zm27-12v-4h-4v4h3v4h1v-4zm-6 0v-8h-4v4h3v4h1zm-6-4v-4h-4v8h1v-4h3zm-6 4v-4h-4v8h1v-4h3zm7 24a12 12 0 0 0 11.83-10h7.92l-3.53 30h-32.44l-3.53-30h7.92A12 12 0 0 0 130 126z'/%3E%3Cpath d='M212 86v2h-4v-2h4zm4 0h-2v2h2v-2zm-20 0v.1a5 5 0 0 0-.56 9.65l.06.25 1.12 4.48a2 2 0 0 0 1.94 1.52h.01l7.02 24.55a2 2 0 0 0 1.92 1.45h4.98a2 2 0 0 0 1.92-1.45l7.02-24.55a2 2 0 0 0 1.95-1.52L224.5 96l.06-.25a5 5 0 0 0-.56-9.65V86a14 14 0 0 0-28 0zm4 0h6v2h-9a3 3 0 1 0 0 6H223a3 3 0 1 0 0-6H220v-2h2a12 12 0 1 0-24 0h2zm-1.44 14l-1-4h24.88l-1 4h-22.88zm8.95 26l-6.86-24h18.7l-6.86 24h-4.98zM150 242a22 22 0 1 0 0-44 22 22 0 0 0 0 44zm24-22a24 24 0 1 1-48 0 24 24 0 0 1 48 0zm-28.38 17.73l2.04-.87a6 6 0 0 1 4.68 0l2.04.87a2 2 0 0 0 2.5-.82l1.14-1.9a6 6 0 0 1 3.79-2.75l2.15-.5a2 2 0 0 0 1.54-2.12l-.19-2.2a6 6 0 0 1 1.45-4.46l1.45-1.67a2 2 0 0 0 0-2.62l-1.45-1.67a6 6 0 0 1-1.45-4.46l.2-2.2a2 2 0 0 0-1.55-2.13l-2.15-.5a6 6 0 0 1-3.8-2.75l-1.13-1.9a2 2 0 0 0-2.5-.8l-2.04.86a6 6 0 0 1-4.68 0l-2.04-.87a2 2 0 0 0-2.5.82l-1.14 1.9a6 6 0 0 1-3.79 2.75l-2.15.5a2 2 0 0 0-1.54 2.12l.19 2.2a6 6 0 0 1-1.45 4.46l-1.45 1.67a2 2 0 0 0 0 2.62l1.45 1.67a6 6 0 0 1 1.45 4.46l-.2 2.2a2 2 0 0 0 1.55 2.13l2.15.5a6 6 0 0 1 3.8 2.75l1.13 1.9a2 2 0 0 0 2.5.8zm2.82.97a4 4 0 0 1 3.12 0l2.04.87a4 4 0 0 0 4.99-1.62l1.14-1.9a4 4 0 0 1 2.53-1.84l2.15-.5a4 4 0 0 0 3.09-4.24l-.2-2.2a4 4 0 0 1 .97-2.98l1.45-1.67a4 4 0 0 0 0-5.24l-1.45-1.67a4 4 0 0 1-.97-2.97l.2-2.2a4 4 0 0 0-3.09-4.25l-2.15-.5a4 4 0 0 1-2.53-1.84l-1.14-1.9a4 4 0 0 0-5-1.62l-2.03.87a4 4 0 0 1-3.12 0l-2.04-.87a4 4 0 0 0-4.99 1.62l-1.14 1.9a4 4 0 0 1-2.53 1.84l-2.15.5a4 4 0 0 0-3.09 4.24l.2 2.2a4 4 0 0 1-.97 2.98l-1.45 1.67a4 4 0 0 0 0 5.24l1.45 1.67a4 4 0 0 1 .97 2.97l-.2 2.2a4 4 0 0 0 3.09 4.25l2.15.5a4 4 0 0 1 2.53 1.84l1.14 1.9a4 4 0 0 0 5 1.62l2.03-.87zM152 207a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm6 2a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm-11 1a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm-6 0a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm3-5a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm-8 8a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm3 6a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm0 6a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm4 7a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm5-2a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm5 4a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm4-6a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm6-4a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm-4-3a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm4-3a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm-5-4a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm-24 6a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm16 5a5 5 0 1 0 0-10 5 5 0 0 0 0 10zm7-5a7 7 0 1 1-14 0 7 7 0 0 1 14 0zm86-29a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm19 9a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm-14 5a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm-25 1a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm5 4a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm9 0a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm15 1a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm12-2a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm-11-14a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm-19 0a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm6 5a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm-25 15c0-.47.01-.94.03-1.4a5 5 0 0 1-1.7-8 3.99 3.99 0 0 1 1.88-5.18 5 5 0 0 1 3.4-6.22 3 3 0 0 1 1.46-1.05 5 5 0 0 1 7.76-3.27A30.86 30.86 0 0 1 246 184c6.79 0 13.06 2.18 18.17 5.88a5 5 0 0 1 7.76 3.27 3 3 0 0 1 1.47 1.05 5 5 0 0 1 3.4 6.22 4 4 0 0 1 1.87 5.18 4.98 4.98 0 0 1-1.7 8c.02.46.03.93.03 1.4v1h-62v-1zm.83-7.17a30.9 30.9 0 0 0-.62 3.57 3 3 0 0 1-.61-4.2c.37.28.78.49 1.23.63zm1.49-4.61c-.36.87-.68 1.76-.96 2.68a2 2 0 0 1-.21-3.71c.33.4.73.75 1.17 1.03zm2.32-4.54c-.54.86-1.03 1.76-1.49 2.68a3 3 0 0 1-.07-4.67 3 3 0 0 0 1.56 1.99zm1.14-1.7c.35-.5.72-.98 1.1-1.46a1 1 0 1 0-1.1 1.45zm5.34-5.77c-1.03.86-2 1.79-2.9 2.77a3 3 0 0 0-1.11-.77 3 3 0 0 1 4-2zm42.66 2.77c-.9-.98-1.87-1.9-2.9-2.77a3 3 0 0 1 4.01 2 3 3 0 0 0-1.1.77zm1.34 1.54c.38.48.75.96 1.1 1.45a1 1 0 1 0-1.1-1.45zm3.73 5.84c-.46-.92-.95-1.82-1.5-2.68a3 3 0 0 0 1.57-1.99 3 3 0 0 1-.07 4.67zm1.8 4.53c-.29-.9-.6-1.8-.97-2.67.44-.28.84-.63 1.17-1.03a2 2 0 0 1-.2 3.7zm1.14 5.51c-.14-1.21-.35-2.4-.62-3.57.45-.14.86-.35 1.23-.63a2.99 2.99 0 0 1-.6 4.2zM275 214a29 29 0 0 0-57.97 0h57.96zM72.33 198.12c-.21-.32-.34-.7-.34-1.12v-12h-2v12a4.01 4.01 0 0 0 7.09 2.54c.57-.69.91-1.57.91-2.54v-12h-2v12a1.99 1.99 0 0 1-2 2 2 2 0 0 1-1.66-.88zM75 176c.38 0 .74-.04 1.1-.12a4 4 0 0 0 6.19 2.4A13.94 13.94 0 0 1 84 185v24a6 6 0 0 1-6 6h-3v9a5 5 0 1 1-10 0v-9h-3a6 6 0 0 1-6-6v-24a14 14 0 0 1 14-14 5 5 0 0 0 5 5zm-17 15v12a1.99 1.99 0 0 0 1.22 1.84 2 2 0 0 0 2.44-.72c.21-.32.34-.7.34-1.12v-12h2v12a3.98 3.98 0 0 1-5.35 3.77 3.98 3.98 0 0 1-.65-.3V209a4 4 0 0 0 4 4h16a4 4 0 0 0 4-4v-24c.01-1.53-.23-2.88-.72-4.17-.43.1-.87.16-1.28.17a6 6 0 0 1-5.2-3 7 7 0 0 1-6.47-4.88A12 12 0 0 0 58 185v6zm9 24v9a3 3 0 1 0 6 0v-9h-6z'/%3E%3Cpath d='M-17 191a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm19 9a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2H3a1 1 0 0 1-1-1zm-14 5a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm-25 1a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm5 4a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm9 0a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm15 1a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm12-2a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2H4zm-11-14a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm-19 0a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm6 5a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm-25 15c0-.47.01-.94.03-1.4a5 5 0 0 1-1.7-8 3.99 3.99 0 0 1 1.88-5.18 5 5 0 0 1 3.4-6.22 3 3 0 0 1 1.46-1.05 5 5 0 0 1 7.76-3.27A30.86 30.86 0 0 1-14 184c6.79 0 13.06 2.18 18.17 5.88a5 5 0 0 1 7.76 3.27 3 3 0 0 1 1.47 1.05 5 5 0 0 1 3.4 6.22 4 4 0 0 1 1.87 5.18 4.98 4.98 0 0 1-1.7 8c.02.46.03.93.03 1.4v1h-62v-1zm.83-7.17a30.9 30.9 0 0 0-.62 3.57 3 3 0 0 1-.61-4.2c.37.28.78.49 1.23.63zm1.49-4.61c-.36.87-.68 1.76-.96 2.68a2 2 0 0 1-.21-3.71c.33.4.73.75 1.17 1.03zm2.32-4.54c-.54.86-1.03 1.76-1.49 2.68a3 3 0 0 1-.07-4.67 3 3 0 0 0 1.56 1.99zm1.14-1.7c.35-.5.72-.98 1.1-1.46a1 1 0 1 0-1.1 1.45zm5.34-5.77c-1.03.86-2 1.79-2.9 2.77a3 3 0 0 0-1.11-.77 3 3 0 0 1 4-2zm42.66 2.77c-.9-.98-1.87-1.9-2.9-2.77a3 3 0 0 1 4.01 2 3 3 0 0 0-1.1.77zm1.34 1.54c.38.48.75.96 1.1 1.45a1 1 0 1 0-1.1-1.45zm3.73 5.84c-.46-.92-.95-1.82-1.5-2.68a3 3 0 0 0 1.57-1.99 3 3 0 0 1-.07 4.67zm1.8 4.53c-.29-.9-.6-1.8-.97-2.67.44-.28.84-.63 1.17-1.03a2 2 0 0 1-.2 3.7zm1.14 5.51c-.14-1.21-.35-2.4-.62-3.57.45-.14.86-.35 1.23-.63a2.99 2.99 0 0 1-.6 4.2zM15 214a29 29 0 0 0-57.97 0h57.96z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E"); | |
} | |
.spacer { | |
margin-bottom: 150px; | |
} | |
.msger__avatar { | |
width: 40px; | |
height: 40px; | |
margin-right: 10px; | |
} | |
.msger__avatar img, | |
.msg-img { | |
object-fit: cover; | |
} | |
.msger__options span { | |
margin: 0 5px; | |
cursor: pointer; | |
} | |
.right-msg .bubble--wrapper { | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
flex-direction: row-reverse; | |
} | |
.left-msg .bubble--wrapper { | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
} | |
@media only screen and (max-width: 768px) { | |
.content { | |
margin-left: 0; | |
} | |
.msger-input { | |
width: calc(100% - 105px); | |
} | |
} | |
:root { | |
--body-bg: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); | |
--msger-bg: #fff; | |
--border: 2px solid #ddd; | |
--left-msg-bg: #ececec; | |
--right-msg-bg: #579ffb; | |
} | |
</style> |
Let me explain further, there are three methods you should pay close attention to. They include getUser(), getMessages(), sendMessage(), and listenForMessage()
.
The getUser() method as intuitive as its name sounds retrieves a user from your comet chat account. After the retrieval, it saves the details in a user property for other usages.
getUser() {
const uid = this.uid;
CometChat.getUser(uid)
.then((user) => (this.user = user))
.catch((error) => console.log(error));
}
The getMessages() method collects all the conversations between you and another user. Afterward, it stores it up in a messages array for further use.
getMessages() {
const limit = 50;
const messagesRequest = new CometChat.MessagesRequestBuilder()
.setLimit(limit)
.setUID(this.uid)
.build();
messagesRequest
.fetchPrevious()
.then((messages) => {
messages.map((message, i) => {
if (!message.readAt) {
const messageId = message.id;
const receiverId = message.sender.uid;
const receiverType = "user";
console.log({ i: i + 1, l: messages.length, r: receiverId, u: this.uid });
if (i + 1 === messages.length && receiverId === this.uid)
CometChat.markAsRead(messageId, receiverId, receiverType);
}
});
this.messages = messages;
})
.catch((error) => console.log("Message fetching failed with error:", error));
}
The listenForMessage() method invokes a listener between two users engaged in a chat. It updates the view with the new messages sent by either user.
listenForMessage() {
const listenerID = this.uid;
CometChat.addMessageListener(
listenerID,
new CometChat.MessageListener({
onTextMessageReceived: (messageReceipt) => {
if (this.uid === messageReceipt.sender.uid) {
this.messages.push(messageReceipt);
const messageId = messageReceipt.id;
const receiverId = messageReceipt.sender.uid;
const receiverType = "user";
CometChat.markAsRead(messageId, receiverId, receiverType);
}
},
onMessagesDelivered: (messageReceipt) => {
this.messages.filter((msg) => (msg.deliveredAt = messageReceipt.deliveredAt));
},
onMessagesRead: (messageReceipt) => {
this.messages.filter((msg) => (msg.readAt = messageReceipt.readAt));
},
})
);
}
Lastly, the sendMessage() method sends a text from the one typing the message to the one receiving.
sendMessage() {
const receiverID = this.uid;
const messageText = this.message;
const receiverType = CometChat.RECEIVER_TYPE.USER;
const textMessage = new CometChat.TextMessage(
receiverID,
messageText,
receiverType
);
CometChat.sendMessage(textMessage)
.then((message) => {
this.message = "";
this.messages.push(message);
})
.catch((error) => console.log("Message sending failed with error:", error));
}
I bet you have got a better understanding of how that process works now, let's move on to the Friends component.
The Friends Component
The Friends component is yet another important component as it carries the full power of comet chat. It contains all the functionality of comet chat. Yes, you heard me right. These functionalities include the comet chat Vue UI Kit, chatting, audio, video calling, and more. Let's see its simple code below.
<template> | |
<CometChatUserListWithMessages /> | |
</template> | |
<script> | |
import { CometChatUserListWithMessages } from "../cometchat-pro-vue-ui-kit"; | |
export default { | |
name: "friends", | |
components: { | |
CometChatUserListWithMessages, | |
}, | |
}; | |
</script> |
The Vue UI Kit Customization
Access the following files within the Vue UI Kit component and change them to the following codes.
' .../src/cometchat-pro-vue-ui-kit/src/components/Users/CometChatUserListWithMessages/style.js '
export const userScreenStyle = (theme) => { | |
return { | |
display: "flex", | |
height: "100vh", | |
width: "100%", | |
boxSizing: "border-box", | |
fontFamily: `${theme.fontFamily}`, | |
"--cometchat-font-family": `${theme.fontFamily}`, | |
backgroundColor: "#f9f9ff" | |
}; | |
}; | |
export const userScreenSidebarStyle = (theme, sidebarView) => { | |
const sidebar = sidebarView | |
? { | |
"--cometchat-contacts-sidebar-left": "0", | |
"--cometchat-contacts-sidebar-box-shadow": | |
"rgba(0, 0, 0, .4) -30px 0 30px 30px", | |
} | |
: {}; | |
return { | |
width: "280px", | |
borderRight: `1px solid ${theme.borderColor.primary}`, | |
height: "100%", | |
position: "relative", | |
display: "flex", | |
flexDirection: "column", | |
"--cometchat-contacts-sidebar-left": "-100%", | |
"--cometchat-contacts-sidebar-bg": `${theme.backgroundColor.white}`, | |
backgroundColor: "white", | |
...sidebar, | |
}; | |
}; | |
export const userScreenMainStyle = (threadMessageView, detailView) => { | |
const secondaryView = threadMessageView | |
? { | |
width: "calc(100% - 680px)", | |
} | |
: detailView | |
? { | |
width: "calc(100% - 580px)", | |
} | |
: {}; | |
return { | |
width: "calc(100% - 280px)", | |
order: "2", | |
...secondaryView, | |
}; | |
}; | |
export const userScreenSecondaryStyle = (theme, threadMessageView) => { | |
return { | |
float: "right", | |
borderLeft: `1px solid ${theme.borderColor.primary}`, | |
height: "100%", | |
width: threadMessageView ? "400px" : "300px", | |
display: "flex", | |
flexDirection: "column", | |
order: "3", | |
"--contacts-secondary-bg-color": `${theme.backgroundColor.white}`, | |
}; | |
}; |
' .../src/cometchat-pro-vue-ui-kit/src/components/Users/CometChatUserList/style.js'
export const contactWrapperStyle = () => { | |
return { | |
display: "flex", | |
flexDirection: "column", | |
height: "100%", | |
boxSizing: "border-box", | |
}; | |
}; | |
export const contactHeaderStyle = ({ color }) => { | |
return { | |
padding: "19px 16px", | |
position: "relative", | |
display: "flex", | |
alignItems: "center", | |
borderBottom: `1px solid ${color.darkSecondary}`, | |
background: `linear-gradient(90deg, rgba(233, 12, 119, 1) 0%, rgba(241, 96, 89, 1) 100% )`, | |
}; | |
}; | |
export const contactHeaderCloseStyle = (img) => { | |
return { | |
cursor: "pointer", | |
display: "none", | |
background: `url(${img}) left center no-repeat`, | |
height: "24px", | |
width: "33%", | |
}; | |
}; | |
export const contactHeaderTitleStyle = (enableCloseMenu) => { | |
const alignment = | |
enableCloseMenu.length > 0 | |
? { | |
width: "33%", | |
textAlign: "center", | |
} | |
: {}; | |
return { | |
margin: "0", | |
fontWeight: "700", | |
display: "inline-block", | |
width: "66%", | |
textAlign: "left", | |
fontSize: "20px", | |
color: "white", | |
textDecoration: "none", | |
...alignment, | |
}; | |
}; | |
export const contactSearchStyle = () => { | |
return { | |
padding: "16px 16px", | |
position: "relative", | |
}; | |
}; | |
export const contactSearchInputStyle = (theme, img) => { | |
return { | |
display: "block", | |
width: "100%", | |
border: "0", | |
boxShadow: "rgba(20, 20, 20, 0.04) 0 0 0 1px inset", | |
borderRadius: "8px", | |
padding: "6px 8px 6px 35px", | |
fontSize: "15px", | |
outline: "none", | |
color: `${theme.color.primary}`, | |
background: `url(${img}) 10px center no-repeat ${theme.backgroundColor.grey}`, | |
}; | |
}; | |
export const contactMsgStyle = () => { | |
return { | |
overflow: "hidden", | |
width: "100%", | |
display: "flex", | |
justifyContent: "center", | |
alignItems: "center", | |
position: "absolute", | |
top: "50%", | |
}; | |
}; | |
export const contactMsgTxtStyle = (theme) => { | |
return { | |
margin: "0", | |
height: "30px", | |
color: `${theme.color.secondary}`, | |
fontSize: "24px!important", | |
fontWeight: "600", | |
}; | |
}; | |
export const contactListStyle = () => { | |
return { | |
height: "calc(100% - 125px)", | |
overflowY: "auto", | |
margin: "0", | |
padding: "0", | |
}; | |
}; | |
export const contactAlphabetStyle = () => { | |
return { | |
padding: "0 15px", | |
margin: "5px 0", | |
width: "100%", | |
fontSize: "12px", | |
fontWeight: "500", | |
opacity: "0.5", | |
}; | |
}; |
' .../src/cometchat-pro-vue-ui-kit/src/components/Users/CometChatUserList/CometChatUserList.vue'
<template> | |
<div :style="styles.wrapper" class="contacts__wrapper"> | |
<div :style="styles.header"> | |
<div | |
class="header__close" | |
v-if="enableCloseMenu" | |
:style="styles.headerClose" | |
@click="emitAction('closeMenuClicked')" | |
></div> | |
<router-link to="/"> | |
<h4 :style="styles.headerTitle">Back</h4> | |
</router-link> | |
</div> | |
<div :style="styles.search"> | |
<input | |
type="text" | |
autocomplete="off" | |
:style="styles.searchInput" | |
:placeholder="STRINGS.SEARCH" | |
@input="userSearchHandler" | |
/> | |
</div> | |
<div v-if="userList.length === 0" :style="styles.msg"> | |
<p :style="styles.msgText"> | |
{{ decoratorMessage }} | |
</p> | |
</div> | |
<div | |
ref="userListRef" | |
:style="styles.list" | |
v-else-if="userList.length != 0" | |
@scroll="userScrollHandler($event)" | |
> | |
<div v-for="(user, i) in userList" :key="i"> | |
<div :style="styles.alphabet"> | |
{{ getAlphabet(user) }} | |
</div> | |
<comet-chat-user-list-item | |
:user="user" | |
:theme="themeValue" | |
:selected-user="selectedUser" | |
@click="userClickHandler" | |
/> | |
</div> | |
</div> | |
</div> | |
</template> | |
<script> | |
import { | |
COMETCHAT_CONSTANTS, | |
DEFAULT_OBJECT_PROP, | |
DEFAULT_BOOLEAN_PROP, | |
} from "../../../resources/constants"; | |
import { CometChatManager } from "../../../util/controller"; | |
import { SvgAvatar } from "../../../util/svgavatar"; | |
import { UserListManager } from "./controller"; | |
import { theme } from "../../../resources/theme"; | |
import { propertyCheck, cometChatCommon } from "../../../mixins/"; | |
import searchIcon from "./resources/search-grey-icon.svg"; | |
import navigateIcon from "./resources/navigate.png"; | |
import CometChatUserListItem from "../CometChatUserListItem/CometChatUserListItem"; | |
import * as style from "./style"; | |
let timeout; | |
let friendsOnly; | |
let userListManager; | |
let currentLetter = ""; | |
/** | |
* Displays list of users. | |
* | |
* @displayName CometChatUserList | |
*/ | |
export default { | |
name: "CometChatUserList", | |
mixins: [propertyCheck, cometChatCommon], | |
components: { | |
CometChatUserListItem, | |
}, | |
props: { | |
/** | |
* The selected chat item object. | |
*/ | |
item: { ...DEFAULT_OBJECT_PROP }, | |
/** | |
* Theme of the UI. | |
*/ | |
theme: { ...DEFAULT_OBJECT_PROP }, | |
/** | |
* Flag for friends only filter. | |
*/ | |
friendsOnly: { ...DEFAULT_BOOLEAN_PROP }, | |
/** | |
* Shows/hides the close menu button. | |
*/ | |
enableCloseMenu: { ...DEFAULT_BOOLEAN_PROP }, | |
}, | |
data() { | |
return { | |
userList: [], | |
usersRequest: "", | |
selectedUser: {}, | |
decoratorMessage: COMETCHAT_CONSTANTS.LOADING_MESSSAGE, | |
}; | |
}, | |
computed: { | |
/** | |
* Theme computed using default theme and theme coming from prop. | |
*/ | |
themeValue() { | |
return Object.assign({}, theme, this.theme || {}); | |
}, | |
/** | |
* Local string constants. | |
*/ | |
STRINGS() { | |
return COMETCHAT_CONSTANTS; | |
}, | |
/** | |
* Computed styles for the component. | |
*/ | |
styles() { | |
return { | |
msg: style.contactMsgStyle(), | |
list: style.contactListStyle(), | |
search: style.contactSearchStyle(), | |
wrapper: style.contactWrapperStyle(), | |
alphabet: style.contactAlphabetStyle(), | |
header: style.contactHeaderStyle(this.themeValue), | |
msgText: style.contactMsgTxtStyle(this.themeValue), | |
headerClose: style.contactHeaderCloseStyle(navigateIcon), | |
headerTitle: style.contactHeaderTitleStyle(this.enableCloseMenu), | |
searchInput: style.contactSearchInputStyle(this.themeValue, searchIcon), | |
}; | |
}, | |
}, | |
watch: { | |
/** | |
* Updates local state on change of item. | |
*/ | |
item: { | |
handler(newItem, oldItem) { | |
const previousItem = JSON.stringify(oldItem); | |
const currentItem = JSON.stringify(newItem); | |
if (previousItem !== currentItem) { | |
if (Object.keys(newItem).length === 0) { | |
this.$nextTick(() => { | |
if (this.$refs.userListRef) { | |
this.$refs.userListRef.scrollTop = 0; | |
} | |
}); | |
this.selectedUser = {}; | |
} else { | |
let userList = [...this.userList]; | |
let userKey = userList.findIndex((u) => u.uid === newItem.uid); | |
if (userKey > -1) { | |
let userObj = { ...userList[userKey] }; | |
this.selectedUser = userObj; | |
} | |
} | |
} | |
if ( | |
oldItem && | |
Object.keys(oldItem).length && | |
oldItem.uid === newItem.uid && | |
oldItem.blockedByMe !== newItem.blockedByMe | |
) { | |
let userList = [...this.userList]; | |
let userKey = userList.findIndex((u) => u.uid === newItem.uid); | |
if (userKey > -1) { | |
let userObj = { ...userList[userKey] }; | |
let newUserObj = Object.assign({}, userObj, { | |
blockedByMe: newItem.blockedByMe, | |
}); | |
userList.splice(userKey, 1, newUserObj); | |
this.userList = userList; | |
} | |
} | |
}, | |
deep: true, | |
}, | |
}, | |
methods: { | |
/** | |
* Handles user search | |
*/ | |
userSearchHandler(e) { | |
if (timeout) { | |
clearTimeout(timeout); | |
} | |
let val = e.target.value; | |
timeout = setTimeout(() => { | |
userListManager = new UserListManager(friendsOnly, val); | |
this.getUsers(true); | |
}, 500); | |
}, | |
/** | |
* Handles click on user | |
*/ | |
userClickHandler(user) { | |
this.emitAction("item-click", { item: user, type: "user" }); | |
}, | |
/** | |
* Parses and return alphabet for list | |
*/ | |
getAlphabet(user) { | |
const chr = user.name[0].toUpperCase(); | |
if (chr !== currentLetter) { | |
currentLetter = chr; | |
return chr; | |
} else { | |
return null; | |
} | |
}, | |
/** | |
* Handles user list scroll | |
*/ | |
userScrollHandler(elem) { | |
if ( | |
elem.target.offsetHeight + elem.target.scrollTop >= | |
elem.target.scrollHeight - 20 | |
) { | |
this.getUsers(); | |
} | |
}, | |
/** | |
* Gets list of users | |
*/ | |
async getUsers(clear = false) { | |
const cometChatManager = new CometChatManager(); | |
try { | |
await cometChatManager.getLoggedInUser(); | |
if (!userListManager) { | |
userListManager = new UserListManager(friendsOnly); | |
} | |
const users = await userListManager.fetchNextUsers(); | |
if (users.length === 0) { | |
this.decoratorMessage = COMETCHAT_CONSTANTS.ERROR_NO_USERS_FOUND; | |
} | |
users.forEach((user) => (user = this.setAvatar(user))); | |
if (clear) { | |
this.userList = users; | |
} else { | |
this.userList = [...this.userList, ...users]; | |
} | |
} catch (error) { | |
this.decoratorMessage = COMETCHAT_CONSTANTS.ERROR_LOADING_USERS; | |
console.error("[CometChatUserList] getUsers fetchNext error", error); | |
} | |
}, | |
/** | |
* Sets SVG avatar | |
*/ | |
setAvatar(user) { | |
if (!user.getAvatar()) { | |
const uid = user.getUid(); | |
const char = user.getName().charAt(0).toUpperCase(); | |
user.setAvatar(SvgAvatar.getAvatar(uid, char)); | |
} | |
}, | |
/** | |
* Handles listener events | |
*/ | |
usersUpdateHandler(user) { | |
this.logInfo("CometChatUserList :usersUpdateHandler", { user }); | |
let users = [...this.userlist]; | |
let userKey = users.findIndex((u) => u.uid === user.uid); | |
if (userKey > -1) { | |
let userObj = { ...users[userKey] }; | |
let newUserObj = { ...userObj, ...user }; | |
users.splice(userKey, 1, newUserObj); | |
this.userlist = users; | |
} | |
}, | |
}, | |
beforeMount() { | |
friendsOnly = this.friendsOnly; | |
userListManager = new UserListManager(friendsOnly); | |
this.getUsers(); | |
userListManager.attachListeners(this.usersUpdateHandler); | |
}, | |
beforeDestroy() { | |
if (userListManager) { | |
userListManager.removeListeners(); | |
userListManager = null; | |
} | |
}, | |
}; | |
</script> | |
<style scoped> | |
.contacts__wrapper * { | |
box-sizing: border-box; | |
} | |
@media (min-width: 320px) and (max-width: 767px) { | |
.header__close { | |
display: block !important; | |
} | |
} | |
</style> |
' .../src/cometchat-pro-vue-ui-kit/src/components/Messages/CometChatSenderTextMessageBubble/style.js'
export const messageContainerStyle = () => { | |
return { | |
alignSelf: "flex-end", | |
marginBottom: "16px", | |
paddingLeft: "16px", | |
paddingRight: "16px", | |
maxWidth: "65%", | |
clear: "both", | |
position: "relative", | |
display: "flex", | |
flexDirection: "column", | |
flexShrink: "0", | |
}; | |
}; | |
export const messageWrapperStyle = () => { | |
return { | |
width: "auto", | |
flex: "1 1", | |
alignSelf: "flex-end", | |
display: "flex", | |
}; | |
}; | |
export const messageTextWrapperStyle = (theme) => { | |
return { | |
display: "inline-block", | |
borderRadius: "12px", | |
background: `linear-gradient(90deg, rgba(233, 12, 119, 1) 0%, rgba(241, 96, 89, 1) 100% )`, | |
color: `${theme.color.white}`, | |
padding: "8px 12px", | |
alignSelf: "flex-end", | |
width: "auto", | |
}; | |
}; | |
export const messagePreviewContainerStyle = (theme) => { | |
return { | |
display: "inline-block", | |
borderRadius: "12px", | |
overflow: "hidden", | |
backgroundColor: `${theme.backgroundColor.white}`, | |
boxShadow: "0px 1px 2px 1px rgba(0,0,0,0.18)", | |
alignSelf: "flex-start", | |
width: "auto", | |
maxWidth: "400px", | |
}; | |
}; | |
export const messagePreviewWrapperStyle = () => { | |
return { | |
display: "flex", | |
flexDirection: "column", | |
img: { | |
minHeight: "150px", | |
}, | |
}; | |
}; | |
export const previewDataStyle = (theme) => { | |
return { | |
borderTop: `1px solid ${theme.borderColor.primary}`, | |
borderBottom: `1px solid ${theme.borderColor.primary}`, | |
padding: "12px", | |
}; | |
}; | |
export const previewTitleStyle = (theme) => { | |
return { | |
whiteSpace: "pre-wrap", | |
wordBreak: "break-word", | |
textAlign: "left", | |
width: "auto", | |
color: `${theme.color.helpText}`, | |
fontWeight: "700", | |
marginBottom: "8px", | |
}; | |
}; | |
export const previewDescStyle = (theme) => { | |
return { | |
whiteSpace: "pre-wrap", | |
wordBreak: "break-word", | |
textAlign: "left", | |
width: "auto", | |
color: `${theme.color.helpText}`, | |
fontStyle: "italic", | |
fontSize: "13px", | |
}; | |
}; | |
export const previewTextStyle = (theme) => { | |
return { | |
whiteSpace: "pre-wrap", | |
wordBreak: "break-word", | |
textAlign: "left", | |
width: "auto", | |
textWrapper: { | |
backgroundColor: "transparent", | |
color: `${theme.color.helpText}`, | |
fontStyle: "normal", | |
padding: "8px 0", | |
}, | |
}; | |
}; | |
export const previewLinkStyle = (theme) => { | |
return { | |
display: "flex", | |
alignItems: "center", | |
justifyContent: "center", | |
padding: "12px", | |
a: { | |
display: "inline-block", | |
color: `${theme.color.blue}`, | |
fontWeight: "700", | |
}, | |
}; | |
}; | |
export const messageTextStyle = ( | |
theme, | |
parsedMessage, | |
emojiMessage, | |
showVariation | |
) => { | |
let emojiAlignmentProp = { | |
" > img": { | |
width: "24px", | |
height: "24px", | |
display: "inline-block", | |
verticalAlign: "top", | |
zoom: "1", | |
margin: "0 2px", | |
}, | |
}; | |
let emojiProp = {}; | |
if ( | |
parsedMessage.length === emojiMessage.length && | |
emojiMessage.length === 1 | |
) { | |
emojiProp = { | |
"> img": { | |
width: "48px", | |
height: "48px", | |
}, | |
}; | |
} else if ( | |
parsedMessage.length === emojiMessage.length && | |
emojiMessage.length === 2 | |
) { | |
emojiProp = { | |
"> img": { | |
width: "36px", | |
height: "36px", | |
}, | |
}; | |
} else if ( | |
parsedMessage.length === emojiMessage.length && | |
emojiMessage.length > 2 | |
) { | |
emojiProp = { | |
"> img": { | |
width: "24px", | |
height: "24px", | |
}, | |
}; | |
} | |
if (showVariation === false) { | |
emojiProp = { | |
"> img": { | |
width: "24px", | |
height: "24px", | |
}, | |
}; | |
} | |
return { | |
margin: "0", | |
fontSize: "13px", | |
whiteSpace: "pre-wrap", | |
wordBreak: "break-word", | |
textAlign: "left", | |
width: "auto", | |
" a": { | |
color: "#0432FF", | |
"&:hover": { | |
color: "#04009D", | |
}, | |
}, | |
" a[href^='mailto:']": { | |
color: "#F38C00", | |
"&:hover": { | |
color: "#F36800", | |
}, | |
}, | |
" a[href^='tel:']": { | |
color: "#3802DA", | |
"&:hover": { | |
color: "#2D038F", | |
}, | |
}, | |
...emojiAlignmentProp, | |
...emojiProp, | |
}; | |
}; | |
export const messageInfoWrapperStyle = () => { | |
return { | |
alignSelf: "flex-end", | |
}; | |
}; | |
export const messageActionWrapperStyle = (parentMessageId) => { | |
const hoverStyle = parentMessageId | |
? { | |
"--sender-message-bubble-hover-display": "hidden", | |
} | |
: {}; | |
return { | |
width: "auto", | |
flex: "1 1", | |
alignSelf: "flex-end", | |
display: "flex", | |
...hoverStyle, | |
}; | |
}; | |
export const messageReactionsWrapperStyle = () => { | |
return { | |
display: "inline-flex", | |
alignSelf: "flex-end", | |
width: "100%", | |
flexWrap: "wrap", | |
justifyContent: "flex-end", | |
}; | |
}; |
'.../src/cometchat-pro-vue-ui-kit/src/components/Messages/CometChatMessageList/style.js'
export const chatListStyle = (theme) => { | |
return { | |
backgroundColor: `${theme.backgroundColor.white}`, | |
zIndex: "1", | |
width: "100%", | |
flex: "1 1 0", | |
order: "2", | |
position: "relative", | |
}; | |
}; | |
export const listWrapperStyle = () => { | |
return { | |
boxSizing: "border-box", | |
display: "flex", | |
flexDirection: "column", | |
height: "100%", | |
overflowX: "hidden", | |
overflowY: "scroll", | |
position: "absolute", | |
top: "0", | |
transition: "background .3s ease-out .1s", | |
width: "100%", | |
zIndex: "100", | |
paddingTop: "14px", | |
backgroundColor: '#fcfcfe', | |
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='260' height='260' viewBox='0 0 260 260'%3E%3Cg fill-rule='evenodd'%3E%3Cg fill='%23dddddd' fill-opacity='0.4'%3E%3Cpath d='M24.37 16c.2.65.39 1.32.54 2H21.17l1.17 2.34.45.9-.24.11V28a5 5 0 0 1-2.23 8.94l-.02.06a8 8 0 0 1-7.75 6h-20a8 8 0 0 1-7.74-6l-.02-.06A5 5 0 0 1-17.45 28v-6.76l-.79-1.58-.44-.9.9-.44.63-.32H-20a23.01 23.01 0 0 1 44.37-2zm-36.82 2a1 1 0 0 0-.44.1l-3.1 1.56.89 1.79 1.31-.66a3 3 0 0 1 2.69 0l2.2 1.1a1 1 0 0 0 .9 0l2.21-1.1a3 3 0 0 1 2.69 0l2.2 1.1a1 1 0 0 0 .9 0l2.21-1.1a3 3 0 0 1 2.69 0l2.2 1.1a1 1 0 0 0 .86.02l2.88-1.27a3 3 0 0 1 2.43 0l2.88 1.27a1 1 0 0 0 .85-.02l3.1-1.55-.89-1.79-1.42.71a3 3 0 0 1-2.56.06l-2.77-1.23a1 1 0 0 0-.4-.09h-.01a1 1 0 0 0-.4.09l-2.78 1.23a3 3 0 0 1-2.56-.06l-2.3-1.15a1 1 0 0 0-.45-.11h-.01a1 1 0 0 0-.44.1L.9 19.22a3 3 0 0 1-2.69 0l-2.2-1.1a1 1 0 0 0-.45-.11h-.01a1 1 0 0 0-.44.1l-2.21 1.11a3 3 0 0 1-2.69 0l-2.2-1.1a1 1 0 0 0-.45-.11h-.01zm0-2h-4.9a21.01 21.01 0 0 1 39.61 0h-2.09l-.06-.13-.26.13h-32.31zm30.35 7.68l1.36-.68h1.3v2h-36v-1.15l.34-.17 1.36-.68h2.59l1.36.68a3 3 0 0 0 2.69 0l1.36-.68h2.59l1.36.68a3 3 0 0 0 2.69 0L2.26 23h2.59l1.36.68a3 3 0 0 0 2.56.06l1.67-.74h3.23l1.67.74a3 3 0 0 0 2.56-.06zM-13.82 27l16.37 4.91L18.93 27h-32.75zm-.63 2h.34l16.66 5 16.67-5h.33a3 3 0 1 1 0 6h-34a3 3 0 1 1 0-6zm1.35 8a6 6 0 0 0 5.65 4h20a6 6 0 0 0 5.66-4H-13.1z'/%3E%3Cpath id='path6_fill-copy' d='M284.37 16c.2.65.39 1.32.54 2H281.17l1.17 2.34.45.9-.24.11V28a5 5 0 0 1-2.23 8.94l-.02.06a8 8 0 0 1-7.75 6h-20a8 8 0 0 1-7.74-6l-.02-.06a5 5 0 0 1-2.24-8.94v-6.76l-.79-1.58-.44-.9.9-.44.63-.32H240a23.01 23.01 0 0 1 44.37-2zm-36.82 2a1 1 0 0 0-.44.1l-3.1 1.56.89 1.79 1.31-.66a3 3 0 0 1 2.69 0l2.2 1.1a1 1 0 0 0 .9 0l2.21-1.1a3 3 0 0 1 2.69 0l2.2 1.1a1 1 0 0 0 .9 0l2.21-1.1a3 3 0 0 1 2.69 0l2.2 1.1a1 1 0 0 0 .86.02l2.88-1.27a3 3 0 0 1 2.43 0l2.88 1.27a1 1 0 0 0 .85-.02l3.1-1.55-.89-1.79-1.42.71a3 3 0 0 1-2.56.06l-2.77-1.23a1 1 0 0 0-.4-.09h-.01a1 1 0 0 0-.4.09l-2.78 1.23a3 3 0 0 1-2.56-.06l-2.3-1.15a1 1 0 0 0-.45-.11h-.01a1 1 0 0 0-.44.1l-2.21 1.11a3 3 0 0 1-2.69 0l-2.2-1.1a1 1 0 0 0-.45-.11h-.01a1 1 0 0 0-.44.1l-2.21 1.11a3 3 0 0 1-2.69 0l-2.2-1.1a1 1 0 0 0-.45-.11h-.01zm0-2h-4.9a21.01 21.01 0 0 1 39.61 0h-2.09l-.06-.13-.26.13h-32.31zm30.35 7.68l1.36-.68h1.3v2h-36v-1.15l.34-.17 1.36-.68h2.59l1.36.68a3 3 0 0 0 2.69 0l1.36-.68h2.59l1.36.68a3 3 0 0 0 2.69 0l1.36-.68h2.59l1.36.68a3 3 0 0 0 2.56.06l1.67-.74h3.23l1.67.74a3 3 0 0 0 2.56-.06zM246.18 27l16.37 4.91L278.93 27h-32.75zm-.63 2h.34l16.66 5 16.67-5h.33a3 3 0 1 1 0 6h-34a3 3 0 1 1 0-6zm1.35 8a6 6 0 0 0 5.65 4h20a6 6 0 0 0 5.66-4H246.9z'/%3E%3Cpath d='M159.5 21.02A9 9 0 0 0 151 15h-42a9 9 0 0 0-8.5 6.02 6 6 0 0 0 .02 11.96A8.99 8.99 0 0 0 109 45h42a9 9 0 0 0 8.48-12.02 6 6 0 0 0 .02-11.96zM151 17h-42a7 7 0 0 0-6.33 4h54.66a7 7 0 0 0-6.33-4zm-9.34 26a8.98 8.98 0 0 0 3.34-7h-2a7 7 0 0 1-7 7h-4.34a8.98 8.98 0 0 0 3.34-7h-2a7 7 0 0 1-7 7h-4.34a8.98 8.98 0 0 0 3.34-7h-2a7 7 0 0 1-7 7h-7a7 7 0 1 1 0-14h42a7 7 0 1 1 0 14h-9.34zM109 27a9 9 0 0 0-7.48 4H101a4 4 0 1 1 0-8h58a4 4 0 0 1 0 8h-.52a9 9 0 0 0-7.48-4h-42z'/%3E%3Cpath d='M39 115a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm6-8a6 6 0 1 1-12 0 6 6 0 0 1 12 0zm-3-29v-2h8v-6H40a4 4 0 0 0-4 4v10H22l-1.33 4-.67 2h2.19L26 130h26l3.81-40H58l-.67-2L56 84H42v-6zm-4-4v10h2V74h8v-2h-8a2 2 0 0 0-2 2zm2 12h14.56l.67 2H22.77l.67-2H40zm13.8 4H24.2l3.62 38h22.36l3.62-38z'/%3E%3Cpath d='M129 92h-6v4h-6v4h-6v14h-3l.24 2 3.76 32h36l3.76-32 .24-2h-3v-14h-6v-4h-6v-4h-8zm18 22v-12h-4v4h3v8h1zm-3 0v-6h-4v6h4zm-6 6v-16h-4v19.17c1.6-.7 2.97-1.8 4-3.17zm-6 3.8V100h-4v23.8a10.04 10.04 0 0 0 4 0zm-6-.63V104h-4v16a10.04 10.04 0 0 0 4 3.17zm-6-9.17v-6h-4v6h4zm-6 0v-8h3v-4h-4v12h1zm27-12v-4h-4v4h3v4h1v-4zm-6 0v-8h-4v4h3v4h1zm-6-4v-4h-4v8h1v-4h3zm-6 4v-4h-4v8h1v-4h3zm7 24a12 12 0 0 0 11.83-10h7.92l-3.53 30h-32.44l-3.53-30h7.92A12 12 0 0 0 130 126z'/%3E%3Cpath d='M212 86v2h-4v-2h4zm4 0h-2v2h2v-2zm-20 0v.1a5 5 0 0 0-.56 9.65l.06.25 1.12 4.48a2 2 0 0 0 1.94 1.52h.01l7.02 24.55a2 2 0 0 0 1.92 1.45h4.98a2 2 0 0 0 1.92-1.45l7.02-24.55a2 2 0 0 0 1.95-1.52L224.5 96l.06-.25a5 5 0 0 0-.56-9.65V86a14 14 0 0 0-28 0zm4 0h6v2h-9a3 3 0 1 0 0 6H223a3 3 0 1 0 0-6H220v-2h2a12 12 0 1 0-24 0h2zm-1.44 14l-1-4h24.88l-1 4h-22.88zm8.95 26l-6.86-24h18.7l-6.86 24h-4.98zM150 242a22 22 0 1 0 0-44 22 22 0 0 0 0 44zm24-22a24 24 0 1 1-48 0 24 24 0 0 1 48 0zm-28.38 17.73l2.04-.87a6 6 0 0 1 4.68 0l2.04.87a2 2 0 0 0 2.5-.82l1.14-1.9a6 6 0 0 1 3.79-2.75l2.15-.5a2 2 0 0 0 1.54-2.12l-.19-2.2a6 6 0 0 1 1.45-4.46l1.45-1.67a2 2 0 0 0 0-2.62l-1.45-1.67a6 6 0 0 1-1.45-4.46l.2-2.2a2 2 0 0 0-1.55-2.13l-2.15-.5a6 6 0 0 1-3.8-2.75l-1.13-1.9a2 2 0 0 0-2.5-.8l-2.04.86a6 6 0 0 1-4.68 0l-2.04-.87a2 2 0 0 0-2.5.82l-1.14 1.9a6 6 0 0 1-3.79 2.75l-2.15.5a2 2 0 0 0-1.54 2.12l.19 2.2a6 6 0 0 1-1.45 4.46l-1.45 1.67a2 2 0 0 0 0 2.62l1.45 1.67a6 6 0 0 1 1.45 4.46l-.2 2.2a2 2 0 0 0 1.55 2.13l2.15.5a6 6 0 0 1 3.8 2.75l1.13 1.9a2 2 0 0 0 2.5.8zm2.82.97a4 4 0 0 1 3.12 0l2.04.87a4 4 0 0 0 4.99-1.62l1.14-1.9a4 4 0 0 1 2.53-1.84l2.15-.5a4 4 0 0 0 3.09-4.24l-.2-2.2a4 4 0 0 1 .97-2.98l1.45-1.67a4 4 0 0 0 0-5.24l-1.45-1.67a4 4 0 0 1-.97-2.97l.2-2.2a4 4 0 0 0-3.09-4.25l-2.15-.5a4 4 0 0 1-2.53-1.84l-1.14-1.9a4 4 0 0 0-5-1.62l-2.03.87a4 4 0 0 1-3.12 0l-2.04-.87a4 4 0 0 0-4.99 1.62l-1.14 1.9a4 4 0 0 1-2.53 1.84l-2.15.5a4 4 0 0 0-3.09 4.24l.2 2.2a4 4 0 0 1-.97 2.98l-1.45 1.67a4 4 0 0 0 0 5.24l1.45 1.67a4 4 0 0 1 .97 2.97l-.2 2.2a4 4 0 0 0 3.09 4.25l2.15.5a4 4 0 0 1 2.53 1.84l1.14 1.9a4 4 0 0 0 5 1.62l2.03-.87zM152 207a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm6 2a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm-11 1a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm-6 0a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm3-5a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm-8 8a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm3 6a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm0 6a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm4 7a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm5-2a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm5 4a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm4-6a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm6-4a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm-4-3a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm4-3a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm-5-4a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm-24 6a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm16 5a5 5 0 1 0 0-10 5 5 0 0 0 0 10zm7-5a7 7 0 1 1-14 0 7 7 0 0 1 14 0zm86-29a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm19 9a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm-14 5a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm-25 1a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm5 4a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm9 0a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm15 1a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm12-2a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm-11-14a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm-19 0a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm6 5a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm-25 15c0-.47.01-.94.03-1.4a5 5 0 0 1-1.7-8 3.99 3.99 0 0 1 1.88-5.18 5 5 0 0 1 3.4-6.22 3 3 0 0 1 1.46-1.05 5 5 0 0 1 7.76-3.27A30.86 30.86 0 0 1 246 184c6.79 0 13.06 2.18 18.17 5.88a5 5 0 0 1 7.76 3.27 3 3 0 0 1 1.47 1.05 5 5 0 0 1 3.4 6.22 4 4 0 0 1 1.87 5.18 4.98 4.98 0 0 1-1.7 8c.02.46.03.93.03 1.4v1h-62v-1zm.83-7.17a30.9 30.9 0 0 0-.62 3.57 3 3 0 0 1-.61-4.2c.37.28.78.49 1.23.63zm1.49-4.61c-.36.87-.68 1.76-.96 2.68a2 2 0 0 1-.21-3.71c.33.4.73.75 1.17 1.03zm2.32-4.54c-.54.86-1.03 1.76-1.49 2.68a3 3 0 0 1-.07-4.67 3 3 0 0 0 1.56 1.99zm1.14-1.7c.35-.5.72-.98 1.1-1.46a1 1 0 1 0-1.1 1.45zm5.34-5.77c-1.03.86-2 1.79-2.9 2.77a3 3 0 0 0-1.11-.77 3 3 0 0 1 4-2zm42.66 2.77c-.9-.98-1.87-1.9-2.9-2.77a3 3 0 0 1 4.01 2 3 3 0 0 0-1.1.77zm1.34 1.54c.38.48.75.96 1.1 1.45a1 1 0 1 0-1.1-1.45zm3.73 5.84c-.46-.92-.95-1.82-1.5-2.68a3 3 0 0 0 1.57-1.99 3 3 0 0 1-.07 4.67zm1.8 4.53c-.29-.9-.6-1.8-.97-2.67.44-.28.84-.63 1.17-1.03a2 2 0 0 1-.2 3.7zm1.14 5.51c-.14-1.21-.35-2.4-.62-3.57.45-.14.86-.35 1.23-.63a2.99 2.99 0 0 1-.6 4.2zM275 214a29 29 0 0 0-57.97 0h57.96zM72.33 198.12c-.21-.32-.34-.7-.34-1.12v-12h-2v12a4.01 4.01 0 0 0 7.09 2.54c.57-.69.91-1.57.91-2.54v-12h-2v12a1.99 1.99 0 0 1-2 2 2 2 0 0 1-1.66-.88zM75 176c.38 0 .74-.04 1.1-.12a4 4 0 0 0 6.19 2.4A13.94 13.94 0 0 1 84 185v24a6 6 0 0 1-6 6h-3v9a5 5 0 1 1-10 0v-9h-3a6 6 0 0 1-6-6v-24a14 14 0 0 1 14-14 5 5 0 0 0 5 5zm-17 15v12a1.99 1.99 0 0 0 1.22 1.84 2 2 0 0 0 2.44-.72c.21-.32.34-.7.34-1.12v-12h2v12a3.98 3.98 0 0 1-5.35 3.77 3.98 3.98 0 0 1-.65-.3V209a4 4 0 0 0 4 4h16a4 4 0 0 0 4-4v-24c.01-1.53-.23-2.88-.72-4.17-.43.1-.87.16-1.28.17a6 6 0 0 1-5.2-3 7 7 0 0 1-6.47-4.88A12 12 0 0 0 58 185v6zm9 24v9a3 3 0 1 0 6 0v-9h-6z'/%3E%3Cpath d='M-17 191a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm19 9a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2H3a1 1 0 0 1-1-1zm-14 5a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm-25 1a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm5 4a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm9 0a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm15 1a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm12-2a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2H4zm-11-14a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm-19 0a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm6 5a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm-25 15c0-.47.01-.94.03-1.4a5 5 0 0 1-1.7-8 3.99 3.99 0 0 1 1.88-5.18 5 5 0 0 1 3.4-6.22 3 3 0 0 1 1.46-1.05 5 5 0 0 1 7.76-3.27A30.86 30.86 0 0 1-14 184c6.79 0 13.06 2.18 18.17 5.88a5 5 0 0 1 7.76 3.27 3 3 0 0 1 1.47 1.05 5 5 0 0 1 3.4 6.22 4 4 0 0 1 1.87 5.18 4.98 4.98 0 0 1-1.7 8c.02.46.03.93.03 1.4v1h-62v-1zm.83-7.17a30.9 30.9 0 0 0-.62 3.57 3 3 0 0 1-.61-4.2c.37.28.78.49 1.23.63zm1.49-4.61c-.36.87-.68 1.76-.96 2.68a2 2 0 0 1-.21-3.71c.33.4.73.75 1.17 1.03zm2.32-4.54c-.54.86-1.03 1.76-1.49 2.68a3 3 0 0 1-.07-4.67 3 3 0 0 0 1.56 1.99zm1.14-1.7c.35-.5.72-.98 1.1-1.46a1 1 0 1 0-1.1 1.45zm5.34-5.77c-1.03.86-2 1.79-2.9 2.77a3 3 0 0 0-1.11-.77 3 3 0 0 1 4-2zm42.66 2.77c-.9-.98-1.87-1.9-2.9-2.77a3 3 0 0 1 4.01 2 3 3 0 0 0-1.1.77zm1.34 1.54c.38.48.75.96 1.1 1.45a1 1 0 1 0-1.1-1.45zm3.73 5.84c-.46-.92-.95-1.82-1.5-2.68a3 3 0 0 0 1.57-1.99 3 3 0 0 1-.07 4.67zm1.8 4.53c-.29-.9-.6-1.8-.97-2.67.44-.28.84-.63 1.17-1.03a2 2 0 0 1-.2 3.7zm1.14 5.51c-.14-1.21-.35-2.4-.62-3.57.45-.14.86-.35 1.23-.63a2.99 2.99 0 0 1-.6 4.2zM15 214a29 29 0 0 0-57.97 0h57.96z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")` | |
}; | |
}; | |
export const actionMessageStyle = () => { | |
return { | |
padding: "8px 12px", | |
marginBottom: "16px", | |
textAlign: "center", | |
}; | |
}; | |
export const actionMessageTextStyle = () => { | |
return { | |
fontSize: "13.5px", | |
margin: "0", | |
lineHeight: "20px", | |
}; | |
}; | |
export const messageDateContainerStyle = () => { | |
return { | |
textAlign: "center", | |
marginBottom: "16px", | |
}; | |
}; | |
export const messageDateStyle = (theme) => { | |
return { | |
padding: "8px 12px", | |
backgroundColor: `${theme.backgroundColor.secondary}`, | |
color: `${theme.color.primary}`, | |
borderRadius: "10px", | |
}; | |
}; | |
export const decoratorMessageStyle = () => { | |
return { | |
overflow: "hidden", | |
width: "100%", | |
display: "flex", | |
justifyContent: "center", | |
alignItems: "center", | |
position: "absolute", | |
top: "50%", | |
}; | |
}; | |
export const decoratorMessageTextStyle = (theme) => { | |
return { | |
margin: "0", | |
height: "30px", | |
color: `${theme.color.secondary}`, | |
fontSize: "24px!important", | |
fontWeight: "600", | |
}; | |
}; | |
export const loadingMessageTextStyle = (theme) => { | |
return { | |
margin: "0", | |
padding: "4px 24px 24px", | |
textAlign: "center", | |
color: `${theme.color.secondary}`, | |
fontSize: "18px!important", | |
fontWeight: "600", | |
}; | |
}; |
'.../src/cometchat-pro-vue-ui-kit/src/components/Messages/CometChatMessageHeader/CometChatMessageHeader.vue'
<template> | |
<div :style="styles.root"> | |
<div :style="styles.detail"> | |
<div | |
class="cometchat_sidebar_btn" | |
:style="styles.sidebarBtn" | |
@click="emitAction('menuClicked')" | |
></div> | |
<div :style="styles.thumbnail"> | |
<comet-chat-avatar | |
border-width="1px" | |
corner-radius="18px" | |
:image="avatarImage" | |
:border-color="theme.borderColor.primary" | |
/> | |
<comet-chat-user-presence | |
border-width="1px" | |
corner-radius="50%" | |
:status="presence" | |
v-if="type === 'user'" | |
:border-color="theme.borderColor.primary" | |
/> | |
</div> | |
<div :style="styles.user" class="cometchat__message__header__user"> | |
<h6 | |
:style="styles.name" | |
@mouseenter="toggleTooltip($event, true)" | |
@mouseleave="toggleTooltip($event, false)" | |
> | |
{{ item.name }} | |
</h6> | |
<span | |
:style="styles.status" | |
@mouseenter="toggleTooltip($event, true)" | |
@mouseleave="toggleTooltip($event, false)" | |
> | |
{{ status }} | |
</span> | |
</div> | |
</div> | |
<div :style="styles.optionWrapper"> | |
<span | |
v-if="canShowAudioCall" | |
class="cometchat_chat_option" | |
:style="styles.audioCallOption" | |
@click="emitAction('audioCall')" | |
></span> | |
<span | |
v-if="canShowVideoCall" | |
class="cometchat_chat_option" | |
:style="styles.videoCallOption" | |
@click="emitAction('videoCall')" | |
></span> | |
<!-- <span | |
class="cometchat_chat_option" | |
:style="styles.detailPaneOption" | |
@click="emitAction('viewDetail')" | |
></span> --> | |
</div> | |
</div> | |
</template> | |
<script> | |
import dateFormat from "dateformat"; | |
import { | |
COMETCHAT_CONSTANTS, | |
DEFAULT_STRING_PROP, | |
DEFAULT_OBJECT_PROP, | |
DEFAULT_BOOLEAN_PROP, | |
} from "../../../resources/constants"; | |
import { cometChatCommon, propertyCheck, tooltip } from "../../../mixins/"; | |
import { MessageHeaderManager } from "./controller"; | |
import { SvgAvatar } from "../../../util/svgavatar"; | |
import { CometChatAvatar, CometChatUserPresence } from "../../Shared"; | |
import * as enums from "../../../util/enums.js"; | |
import menuIcon from "./resources/menuicon.png"; | |
import audioCallIcon from "./resources/audio.png"; | |
import videoCallIcon from "./resources/video.png"; | |
import detailPaneIcon from "./resources/detailpane.png"; | |
import * as style from "./style"; | |
let messageHeaderManager; | |
/** | |
* Displays message info in header. | |
* | |
* @displayName CometChatMessageHeader | |
*/ | |
export default { | |
name: "CometChatMessageHeader", | |
mixins: [tooltip, propertyCheck, cometChatCommon], | |
components: { | |
CometChatAvatar, | |
CometChatUserPresence, | |
}, | |
props: { | |
/** | |
* The selected chat item object. | |
*/ | |
item: { ...DEFAULT_OBJECT_PROP }, | |
/** | |
* Type of chat item. | |
*/ | |
type: { ...DEFAULT_STRING_PROP }, | |
/** | |
* Theme of the UI. | |
*/ | |
theme: { ...DEFAULT_OBJECT_PROP }, | |
/** | |
* Whether to show sidebar. | |
*/ | |
sidebar: { ...DEFAULT_BOOLEAN_PROP }, | |
/** | |
* Current logged in user. | |
*/ | |
loggedInUser: { ...DEFAULT_OBJECT_PROP }, | |
}, | |
data() { | |
return { | |
status: "", | |
presence: "offline", | |
}; | |
}, | |
watch: { | |
item: { | |
/** | |
* Watches item to update status message for group. | |
*/ | |
handler(newValue, oldValue) { | |
if (messageHeaderManager) { | |
messageHeaderManager.removeListeners(); | |
} | |
messageHeaderManager = new MessageHeaderManager(); | |
messageHeaderManager.attachListeners(this.updateHeaderHandler); | |
if (this.type === "user" && oldValue.uid !== newValue.uid) { | |
this.setStatusForUser(); | |
} else if ( | |
this.type === "group" && | |
(newValue.guid !== oldValue.guid || | |
(newValue.guid === oldValue.guid && | |
newValue.membersCount !== oldValue.membersCount)) | |
) { | |
this.setStatusForGroup(); | |
} | |
}, | |
deep: true, | |
}, | |
}, | |
computed: { | |
/** | |
* Icons computed from file images. | |
*/ | |
styles() { | |
return { | |
name: style.chatNameStyle(), | |
user: style.chatUserStyle(), | |
detail: style.chatDetailStyle(), | |
thumbnail: style.chatThumbnailStyle(), | |
root: style.chatHeaderStyle(this.theme), | |
optionWrapper: style.chatOptionWrapStyle(), | |
sidebarBtn: style.chatSideBarBtnStyle(menuIcon, this.sidebar), | |
audioCallOption: style.chatOptionStyle(audioCallIcon, "audio"), | |
videoCallOption: style.chatOptionStyle(videoCallIcon, "video"), | |
detailPaneOption: style.chatOptionStyle(detailPaneIcon, "detail"), | |
status: style.chatStatusStyle( | |
this.theme, | |
this.presence, | |
this.status, | |
this.type | |
), | |
}; | |
}, | |
/** | |
* Computed avatar image | |
*/ | |
avatarImage() { | |
const isUser = this.type === "user"; | |
let avatar = isUser ? this.item.avatar : this.item.icon; | |
if (!avatar) { | |
if (!isUser) { | |
avatar = SvgAvatar.getAvatar( | |
this.item.uid, | |
this.item.name.charAt(0).toUpperCase() | |
); | |
} else { | |
avatar = SvgAvatar.getAvatar( | |
this.item.guid, | |
this.item.name.charAt(0).toUpperCase() | |
); | |
} | |
} | |
return avatar; | |
}, | |
/** | |
* Returns if item is blocked by current user. | |
*/ | |
isBlockedByMe() { | |
return this.item.blockedByMe; | |
}, | |
/** | |
* Returns if audio call icon can be shown. | |
*/ | |
canShowAudioCall() { | |
return this.isBlockedByMe ? false : true; | |
}, | |
/** | |
* Returns if video call icon can be shown. | |
*/ | |
canShowVideoCall() { | |
return this.isBlockedByMe ? false : true; | |
}, | |
}, | |
methods: { | |
/** | |
* Sets status message for user | |
*/ | |
setStatusForUser() { | |
let status = this.item.status; | |
const presence = this.item.status === "online" ? "online" : "offline"; | |
if (this.item.status === "offline" && this.item.lastActiveAt) { | |
status = | |
COMETCHAT_CONSTANTS.LAST_ACTIVE_AT + | |
dateFormat(this.item.lastActiveAt * 1000, "d mmmm yyyy, h:MM TT"); | |
} else if (this.item.status === "offline") { | |
status = "offline"; | |
} | |
this.status = status; | |
this.presence = presence; | |
}, | |
/** | |
* Sets status message for group | |
*/ | |
setStatusForGroup() { | |
this.status = `${this.item.membersCount} members`; | |
}, | |
/** | |
* Handles listener events | |
*/ | |
updateHeaderHandler(key, item, groupUser) { | |
this.logInfo("CometChatMessageHeader: updateHeaderHandler", { | |
key, | |
item, | |
groupUser, | |
}); | |
switch (key) { | |
case enums.USER_ONLINE: | |
case enums.USER_OFFLINE: { | |
if (this.type === "user" && this.item.uid === item.uid) { | |
this.status = item.status; | |
this.presence = item.status; | |
} | |
break; | |
} | |
case enums.GROUP_MEMBER_KICKED: | |
case enums.GROUP_MEMBER_BANNED: | |
case enums.GROUP_MEMBER_LEFT: | |
if ( | |
this.type === "group" && | |
this.item.guid === item.guid && | |
this.loggedInUser.uid !== groupUser.uid | |
) { | |
let membersCount = parseInt(item.membersCount); | |
const status = `${membersCount} members`; | |
this.status = status; | |
} | |
break; | |
case enums.GROUP_MEMBER_JOINED: | |
if (this.type === "group" && this.item.guid === item.guid) { | |
let membersCount = parseInt(item.membersCount); | |
const status = `${membersCount} members`; | |
this.status = status; | |
} | |
break; | |
case enums.GROUP_MEMBER_ADDED: | |
if (this.type === "group" && this.item.guid === item.guid) { | |
let membersCount = parseInt(item.membersCount); | |
const status = `${membersCount} members`; | |
this.status = status; | |
} | |
break; | |
case enums.TYPING_STARTED: { | |
if ( | |
this.type === "group" && | |
this.type === item.receiverType && | |
this.item.guid === item.receiverId | |
) { | |
this.status = `${item.sender.name} ${COMETCHAT_CONSTANTS.IS_TYPING}`; | |
} else if ( | |
this.type === "user" && | |
this.type === item.receiverType && | |
this.item.uid === item.sender.uid | |
) { | |
this.status = COMETCHAT_CONSTANTS.TYPING; | |
} | |
this.emitAction("showReaction", { reaction: item }); | |
break; | |
} | |
case enums.TYPING_ENDED: { | |
if ( | |
this.type === "group" && | |
this.type === item.receiverType && | |
this.item.guid === item.receiverId | |
) { | |
this.setStatusForGroup(); | |
} else if ( | |
this.type === "user" && | |
this.type === item.receiverType && | |
this.item.uid === item.sender.uid | |
) { | |
if (this.presence === "online") { | |
this.status = "online"; | |
this.presence = "online"; | |
} else { | |
this.setStatusForUser(); | |
} | |
} | |
this.emitAction("stopReaction", { reaction: item }); | |
break; | |
} | |
default: | |
break; | |
} | |
}, | |
}, | |
beforeMount() { | |
messageHeaderManager = new MessageHeaderManager(); | |
messageHeaderManager.attachListeners(this.updateHeaderHandler); | |
if (this.type === "user") { | |
this.setStatusForUser(); | |
} else { | |
this.setStatusForGroup(); | |
} | |
}, | |
beforeDestroy() { | |
if (this.messageHeaderManager) { | |
this.messageHeaderManager.removeListeners(); | |
this.messageHeaderManager = null; | |
} | |
}, | |
}; | |
</script> | |
<style scoped> | |
.cometchat_chat_option:first-of-type { | |
margin-left: 0 !important; | |
} | |
.cometchat_chat_option:last-of-type { | |
margin-right: 0 !important; | |
} | |
@media (min-width: 320px) and (max-width: 767px) { | |
.cometchat_sidebar_btn { | |
display: block !important; | |
} | |
.cometchat__message__header__user { | |
width: calc(100% - 80px) !important; | |
} | |
} | |
</style> |
Conclusion
To conclude, we have covered a step-by-step process on how to build a dating site using tinder as a case study. We've learned how to integrate the comet chat SDK in solving communication problems on the web. We've explored the various functions within the comet chat SDK to send and receive text, audio, and video calls. Now that you've seen how easy it is to use the comet chat SDK and UI Kit, it's time you get your hands on deck and create something else with it.
Top comments (1)
First of all thanks for your sharing, I'm developing a clone of Tinder app too!
I saw that you are writing pure css without css framework, but when I look at the css styling on the Tinder, they styles the css class like this "D(b) Pos(r) Expand Bdrs(50%)", and in the css file, they will be like: ".P\(r\){ position: relative; } or .Bdrs\(50\%\){ border-radius: 50%; }". Do you know about this style? If yes, could you tell me how they do that, because I think that style is really hard to write and maintain without any framework, so I think they might use a framework to render the css file or they are just write a big css file by hand???
Some comments may only be visible to logged-in visitors. Sign in to view all comments.