first commit

This commit is contained in:
Inshal
2024-05-29 22:34:28 +05:00
commit e63fc41a20
1470 changed files with 174828 additions and 0 deletions

View File

@@ -0,0 +1,191 @@
<script setup>
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
import { useChat } from './useChat'
import { useChatStore } from '@/views/apps/chat/useChatStore'
const emit = defineEmits(['close'])
const store = useChatStore()
const { resolveAvatarBadgeVariant } = useChat()
</script>
<template>
<template v-if="store.activeChat">
<!-- Close Button -->
<div
class="pt-2 me-2"
:class="$vuetify.locale.isRtl ? 'text-left' : 'text-right'"
>
<IconBtn @click="$emit('close')">
<VIcon
icon="ri-close-line"
class="text-medium-emphasis"
/>
</IconBtn>
</div>
<!-- User Avatar + Name + Role -->
<div class="text-center px-6">
<VBadge
location="bottom right"
offset-x="7"
offset-y="4"
bordered
:color="resolveAvatarBadgeVariant(store.activeChat.contact.status)"
class="chat-user-profile-badge mb-4"
>
<VAvatar
size="84"
:variant="!store.activeChat.contact.avatar ? 'tonal' : undefined"
:color="!store.activeChat.contact.avatar ? resolveAvatarBadgeVariant(store.activeChat.contact.status) : undefined"
>
<VImg
v-if="store.activeChat.contact.avatar"
:src="store.activeChat.contact.avatar"
/>
<span
v-else
class="text-3xl"
>{{ avatarText(store.activeChat.contact.fullName) }}</span>
</VAvatar>
</VBadge>
<h5 class="text-h5">
{{ store.activeChat.contact.fullName }}
</h5>
<p class="text-body-1 mb-0">
{{ store.activeChat.contact.role }}
</p>
</div>
<!-- User Data -->
<PerfectScrollbar
class="ps-chat-user-profile-sidebar-content text-medium-emphasis pb-5 px-5"
:options="{ wheelPropagation: false }"
>
<!-- About -->
<div class="my-6">
<p
for="textarea-user-about"
class="text-base text-disabled mb-1"
>
ABOUT
</p>
<p class="mb-0">
{{ store.activeChat.contact.about }}
</p>
</div>
<!-- Personal Information -->
<div class="mb-6">
<p class="text-base text-disabled mb-1">
PERSONAL INFORMATION
</p>
<div class="d-flex align-center pa-2">
<VIcon
class="me-2"
size="22"
color="high-emphasis"
icon="ri-mail-line"
/>
<h6 class="text-h6 font-weight-regular">
lucifer@email.com
</h6>
</div>
<div class="d-flex align-center pa-2">
<VIcon
class="me-2"
size="22"
color="high-emphasis"
icon="ri-phone-line"
/>
<h6 class="text-h6 font-weight-regular">
+1(123) 456 - 7890
</h6>
</div>
<div class="d-flex align-center pa-2">
<VIcon
class="me-2"
size="22"
icon="ri-time-line"
color="high-emphasis"
/>
<h6 class="text-h6 font-weight-regular">
Mon - Fri 10AM - 8PM
</h6>
</div>
</div>
<!-- Options -->
<div>
<p class="text-base text-disabled mb-1">
OPTIONS
</p>
<div class="d-flex align-center pa-2">
<VIcon
class="me-2"
size="22"
color="high-emphasis"
icon="ri-bookmark-line"
/>
<h6 class="text-h6 font-weight-regular">
Add Tag
</h6>
</div>
<div class="d-flex align-center pa-2">
<VIcon
class="me-2"
size="22"
color="high-emphasis"
icon="ri-star-line"
/>
<h6 class="text-h6 font-weight-regular">
Important Contact
</h6>
</div>
<div class="d-flex align-center pa-2">
<VIcon
class="me-2"
size="22"
color="high-emphasis"
icon="ri-file-image-line"
/>
<h6 class="text-h6 font-weight-regular">
Shared Media
</h6>
</div>
<div class="d-flex align-center pa-2">
<VIcon
icon="ri-delete-bin-line"
size="22"
color="high-emphasis"
class="me-2"
/>
<h6 class="text-h6 font-weight-regular">
Delete Contact
</h6>
</div>
<div class="d-flex align-center pa-2">
<VIcon
size="22"
color="high-emphasis"
icon="ri-forbid-line"
class="me-2"
/>
<h6 class="text-h6 font-weight-regular">
Block Contact
</h6>
</div>
</div>
<VBtn
block
color="error"
append-icon="ri-delete-bin-7-line"
class="mt-12"
>
Delete Contact
</VBtn>
</PerfectScrollbar>
</template>
</template>

View File

@@ -0,0 +1,110 @@
<script setup>
import { useChat } from '@/views/apps/chat/useChat'
import { useChatStore } from '@/views/apps/chat/useChatStore'
const props = defineProps({
isChatContact: {
type: Boolean,
required: false,
default: false,
},
user: {
type: null,
required: true,
},
})
const store = useChatStore()
const { resolveAvatarBadgeVariant } = useChat()
const isChatContactActive = computed(() => {
const isActive = store.activeChat?.contact.id === props.user.id
if (!props.isChatContact)
return !store.activeChat?.chat && isActive
return isActive
})
</script>
<template>
<li
:key="store.chatsContacts.length"
class="chat-contact cursor-pointer d-flex align-center"
:class="{ 'chat-contact-active': isChatContactActive }"
:data-x="store.chatsContacts.length"
>
<VBadge
dot
location="bottom right"
offset-x="3"
offset-y="3"
:color="resolveAvatarBadgeVariant(props.user.status)"
bordered
:model-value="props.isChatContact"
>
<VAvatar
size="40"
:variant="!props.user.avatar ? 'tonal' : undefined"
:color="!props.user.avatar ? resolveAvatarBadgeVariant(props.user.status) : undefined"
>
<VImg
v-if="props.user.avatar"
:src="props.user.avatar"
alt="John Doe"
/>
<span v-else>{{ avatarText(user.fullName) }}</span>
</VAvatar>
</VBadge>
<div class="flex-grow-1 ms-4 overflow-hidden">
<p class="text-base mb-0">
{{ props.user.fullName }}
</p>
<span class="d-block text-body-2 text-truncate">{{ props.isChatContact && 'chat' in props.user ? props.user.chat.lastMessage.message : props.user.about }}</span>
</div>
<div
v-if="props.isChatContact && 'chat' in props.user"
class="d-flex flex-column align-self-start"
>
<span class="d-block text-sm text-disabled whitespace-no-wrap">{{ formatDateToMonthShort(props.user.chat.lastMessage.time) }}</span>
<VBadge
v-if="props.user.chat.unseenMsgs"
color="error"
inline
:content="props.user.chat.unseenMsgs"
class="ms-auto"
/>
</div>
</li>
</template>
<style lang="scss">
@use "@styles/variables/vuetify.scss";
@use "@core-scss/base/mixins";
@use "vuetify/lib/styles/tools/states" as vuetifyStates;
.chat-contact {
border-radius: vuetify.$border-radius-root;
padding-block: 8px;
padding-inline: var(--chat-content-spacing-x);
@include mixins.before-pseudo;
@include vuetifyStates.states($active: false);
&.chat-contact-active {
@include mixins.elevation(2);
background: rgb(var(--v-theme-primary));
color: #fff;
--v-theme-on-background: #fff;
.v-avatar {
background: #fff;
}
}
.v-badge--bordered .v-badge__badge::after {
color: #fff;
}
}
</style>

View File

@@ -0,0 +1,125 @@
<script setup>
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
import { useChat } from './useChat'
import ChatContact from '@/views/apps/chat/ChatContact.vue'
import { useChatStore } from '@/views/apps/chat/useChatStore'
const props = defineProps({
search: {
type: String,
required: true,
},
isDrawerOpen: {
type: Boolean,
required: true,
},
})
const emit = defineEmits([
'openChatOfContact',
'showUserProfile',
'close',
'update:search',
])
const { resolveAvatarBadgeVariant } = useChat()
const search = useVModel(props, 'search', emit)
const store = useChatStore()
</script>
<template>
<!-- 👉 Chat list header -->
<div
v-if="store.profileUser"
class="chat-list-header gap-4"
>
<VBadge
dot
location="bottom right"
offset-x="3"
offset-y="3"
:color="resolveAvatarBadgeVariant(store.profileUser.status)"
bordered
>
<VAvatar
class="cursor-pointer"
@click="$emit('showUserProfile')"
>
<VImg
:src="store.profileUser.avatar"
alt="John Doe"
/>
</VAvatar>
</VBadge>
<VTextField
v-model="search"
placeholder="Search..."
prepend-inner-icon="ri-search-line"
density="compact"
class="chat-list-search"
/>
<IconBtn
v-if="$vuetify.display.smAndDown"
@click="$emit('close')"
>
<VIcon
icon="ri-close-line"
class="text-medium-emphasis"
/>
</IconBtn>
</div>
<VDivider />
<PerfectScrollbar
tag="ul"
class="chat-contacts-list px-3 d-flex flex-column gap-1"
:options="{ wheelPropagation: false }"
>
<li class="list-none">
<span class="chat-contact-header d-block text-primary text-lg font-weight-medium">Chats</span>
</li>
<ChatContact
v-for="contact in store.chatsContacts"
:key="`chat-${contact.id}`"
:user="contact"
is-chat-contact
@click="$emit('openChatOfContact', contact.id)"
/>
<span
v-show="!store.chatsContacts.length"
class="no-chat-items-text text-disabled"
>No chats found</span>
<li class="list-none">
<span class="chat-contact-header d-block text-primary text-lg font-weight-medium">Contacts</span>
</li>
<ChatContact
v-for="contact in store.contacts"
:key="`chat-${contact.id}`"
:user="contact"
@click="$emit('openChatOfContact', contact.id)"
/>
<span
v-show="!store.contacts.length"
class="no-chat-items-text text-disabled"
>No contacts found</span>
</PerfectScrollbar>
</template>
<style lang="scss">
.chat-contacts-list {
--chat-content-spacing-x: 12px;
padding-block-end: 0.75rem;
.chat-contact-header {
margin-block: 1rem 4px;
margin-inline: 1rem;
}
.no-chat-items-text {
margin-inline: var(--chat-content-spacing-x);
}
}
</style>

View File

@@ -0,0 +1,149 @@
<script setup>
import { useChatStore } from '@/views/apps/chat/useChatStore'
const store = useChatStore()
const contact = computed(() => ({
id: store.activeChat?.contact.id,
avatar: store.activeChat?.contact.avatar,
}))
const resolveFeedbackIcon = feedback => {
if (feedback.isSeen)
return {
icon: 'ri-check-double-line',
color: 'success',
}
else if (feedback.isDelivered)
return {
icon: 'ri-check-double-line',
color: undefined,
}
else
return {
icon: 'ri-check-line',
color: undefined,
}
}
const msgGroups = computed(() => {
let messages = []
const _msgGroups = []
if (store.activeChat.chat) {
messages = store.activeChat.chat.messages
let msgSenderId = messages[0].senderId
let msgGroup = {
senderId: msgSenderId,
messages: [],
}
messages.forEach((msg, index) => {
if (msgSenderId === msg.senderId) {
msgGroup.messages.push({
message: msg.message,
time: msg.time,
feedback: msg.feedback,
})
} else {
msgSenderId = msg.senderId
_msgGroups.push(msgGroup)
msgGroup = {
senderId: msg.senderId,
messages: [{
message: msg.message,
time: msg.time,
feedback: msg.feedback,
}],
}
}
if (index === messages.length - 1)
_msgGroups.push(msgGroup)
})
}
return _msgGroups
})
</script>
<template>
<div class="chat-log pa-5">
<div
v-for="(msgGrp, index) in msgGroups"
:key="msgGrp.senderId + String(index)"
class="chat-group d-flex align-start"
:class="[{
'flex-row-reverse': msgGrp.senderId !== contact.id,
'mb-8': msgGroups.length - 1 !== index,
}]"
>
<div
class="chat-avatar"
:class="msgGrp.senderId !== contact.id ? 'ms-4' : 'me-4'"
>
<VAvatar size="32">
<VImg :src="msgGrp.senderId === contact.id ? contact.avatar : store.profileUser?.avatar" />
</VAvatar>
</div>
<div
class="chat-body d-inline-flex flex-column"
:class="msgGrp.senderId !== contact.id ? 'align-end' : 'align-start'"
>
<div
v-for="(msgData, msgIndex) in msgGrp.messages"
:key="msgData.time"
class="chat-content text-body-1 py-2 px-4 elevation-2"
:class="[
msgGrp.senderId === contact.id ? 'bg-surface chat-left' : 'bg-primary text-white chat-right',
msgGrp.messages.length - 1 !== msgIndex ? 'mb-2' : 'mb-1',
]"
>
<p class="mb-0">
{{ msgData.message }}
</p>
</div>
<div
:class="{ 'text-right': msgGrp.senderId !== contact.id }"
class="d-flex align-center gap-2"
>
<VIcon
v-if="msgGrp.senderId !== contact.id"
size="16"
:color="resolveFeedbackIcon(msgGrp.messages[msgGrp.messages.length - 1].feedback).color"
>
{{ resolveFeedbackIcon(msgGrp.messages[msgGrp.messages.length - 1].feedback).icon }}
</VIcon>
<p
class="text-sm text-disabled mb-0"
style="letter-spacing: 0.4px;"
>
{{ formatDate(msgGrp.messages[msgGrp.messages.length - 1].time, { hour: 'numeric', minute: 'numeric' }) }}
</p>
</div>
</div>
</div>
</div>
</template>
<style lang=scss>
.chat-log {
.chat-content {
border-end-end-radius: 6px;
border-end-start-radius: 6px;
p {
overflow-wrap: anywhere;
}
&.bg-surface{
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)) !important;
}
&.chat-left {
border-start-end-radius: 6px;
}
&.chat-right {
border-start-start-radius: 6px;
}
}
}
</style>

View File

@@ -0,0 +1,196 @@
<script setup>
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
import { useChat } from './useChat'
import { useChatStore } from '@/views/apps/chat/useChatStore'
const emit = defineEmits(['close'])
// composables
const store = useChatStore()
const { resolveAvatarBadgeVariant } = useChat()
const userStatusRadioOptions = [
{
title: 'Online',
value: 'online',
color: 'success',
},
{
title: 'Away',
value: 'away',
color: 'warning',
},
{
title: 'Do not disturb',
value: 'busy',
color: 'error',
},
{
title: 'Offline',
value: 'offline',
color: 'secondary',
},
]
const isTwoStepVerified = ref(true)
const isNotificationEnabled = ref(false)
</script>
<template>
<template v-if="store.profileUser">
<!-- Close Button -->
<div class="pt-2 me-2 text-end">
<IconBtn @click="$emit('close')">
<VIcon
class="text-medium-emphasis"
icon="ri-close-line"
/>
</IconBtn>
</div>
<!-- User Avatar + Name + Role -->
<div class="text-center px-6">
<VBadge
location="bottom right"
offset-x="7"
offset-y="4"
bordered
:color="resolveAvatarBadgeVariant(store.profileUser.status)"
class="chat-user-profile-badge mb-4"
>
<VAvatar
size="84"
:variant="!store.profileUser.avatar ? 'tonal' : undefined"
:color="!store.profileUser.avatar ? resolveAvatarBadgeVariant(store.profileUser.status) : undefined"
>
<VImg
v-if="store.profileUser.avatar"
:src="store.profileUser.avatar"
/>
<span
v-else
class="text-3xl"
>{{ avatarText(store.profileUser.fullName) }}</span>
</VAvatar>
</VBadge>
<h5 class="text-h5">
{{ store.profileUser.fullName }}
</h5>
<p class="text-body-1 text-capitalize mb-0">
{{ store.profileUser.role }}
</p>
</div>
<!-- User Data -->
<PerfectScrollbar
class="ps-chat-user-profile-sidebar-content pb-5 px-5"
:options="{ wheelPropagation: false }"
>
<!-- About -->
<div class="my-6 text-medium-emphasis">
<p
for="textarea-user-about"
class="text-base text-disabled mb-0"
>
ABOUT
</p>
<VTextarea
id="textarea-user-about"
v-model="store.profileUser.about"
auto-grow
class="mt-1"
rows="3"
/>
</div>
<!-- Status -->
<div class="mb-6">
<p class="text-base text-disabled mb-0">
STATUS
</p>
<VRadioGroup
v-model="store.profileUser.status"
class="ms-2 mt-1"
>
<VRadio
v-for="radioOption in userStatusRadioOptions"
:key="radioOption.title"
:label="radioOption.title"
:value="radioOption.value"
:color="radioOption.color"
/>
</VRadioGroup>
</div>
<!-- Settings -->
<div class="text-medium-emphasis">
<p class="text-base text-disabled mb-0">
SETTINGS
</p>
<div class="d-flex align-center pa-2">
<VIcon
class="me-2"
icon="ri-lock-password-line"
size="22"
color="high-emphasis"
/>
<h6 class="text-h6 font-weight-regular">
Two-step Verification
</h6>
<VSpacer />
<VSwitch v-model="isTwoStepVerified" />
</div>
<div class="d-flex align-center pa-2">
<VIcon
class="me-2"
icon="ri-notification-line"
size="22"
color="high-emphasis"
/>
<h6 class="text-h6 font-weight-regular">
Notification
</h6>
<VSpacer />
<VSwitch v-model="isNotificationEnabled" />
</div>
<div class="d-flex align-center pa-2">
<VIcon
class="me-2"
icon="ri-user-add-line"
size="22"
color="high-emphasis"
/>
<h6 class="text-h6 font-weight-regular">
Invite Friends
</h6>
</div>
<div class="d-flex align-center pa-2">
<VIcon
class="me-2"
icon="ri-delete-bin-7-line"
size="22"
color="high-emphasis"
/>
<h6 class="text-h6 font-weight-regular">
Delete Account
</h6>
</div>
</div>
<!-- Logout Button -->
<VBtn
block
color="primary"
class="mt-11"
append-icon="ri-logout-box-r-line"
>
Logout
</VBtn>
</PerfectScrollbar>
</template>
</template>

View File

@@ -0,0 +1,16 @@
export const useChat = () => {
const resolveAvatarBadgeVariant = status => {
if (status === 'online')
return 'success'
if (status === 'busy')
return 'error'
if (status === 'away')
return 'warning'
return 'secondary'
}
return {
resolveAvatarBadgeVariant,
}
}

View File

@@ -0,0 +1,80 @@
export const useChatStore = defineStore('chat', {
// arrow function recommended for full type inference
state: () => ({
contacts: [],
chatsContacts: [],
profileUser: undefined,
activeChat: null,
}),
actions: {
async fetchChatsAndContacts(q) {
const { data, error } = await useApi(createUrl('/apps/chat/chats-and-contacts', {
query: {
q,
},
}))
if (error.value) {
console.log(error.value)
}
else {
const { chatsContacts, contacts, profileUser } = data.value
this.chatsContacts = chatsContacts
this.contacts = contacts
this.profileUser = profileUser
}
},
async getChat(userId) {
const res = await $api(`/apps/chat/chats/${userId}`)
this.activeChat = res
},
async sendMsg(message) {
const senderId = this.profileUser?.id
const response = await $api(`apps/chat/chats/${this.activeChat?.contact.id}`, {
method: 'POST',
body: { message, senderId },
})
const { msg, chat } = response
// ? If it's not undefined => New chat is created (Contact is not in list of chats)
if (chat !== undefined) {
const activeChat = this.activeChat
this.chatsContacts.push({
...activeChat.contact,
chat: {
id: chat.id,
lastMessage: [],
unseenMsgs: 0,
messages: [msg],
},
})
if (this.activeChat) {
this.activeChat.chat = {
id: chat.id,
messages: [msg],
unseenMsgs: 0,
userId: this.activeChat?.contact.id,
}
}
}
else {
this.activeChat?.chat?.messages.push(msg)
}
// Set Last Message for active contact
const contact = this.chatsContacts.find(c => {
if (this.activeChat)
return c.id === this.activeChat.contact.id
return false
})
contact.chat.lastMessage = msg
},
},
})