first commit
This commit is contained in:
429
resources/js/pages/apps/chat.vue
Normal file
429
resources/js/pages/apps/chat.vue
Normal file
@@ -0,0 +1,429 @@
|
||||
<script setup>
|
||||
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
|
||||
import {
|
||||
useDisplay,
|
||||
useTheme,
|
||||
} from 'vuetify'
|
||||
import { themes } from '@/plugins/vuetify/theme'
|
||||
import ChatActiveChatUserProfileSidebarContent from '@/views/apps/chat/ChatActiveChatUserProfileSidebarContent.vue'
|
||||
import ChatLeftSidebarContent from '@/views/apps/chat/ChatLeftSidebarContent.vue'
|
||||
import ChatLog from '@/views/apps/chat/ChatLog.vue'
|
||||
import ChatUserProfileSidebarContent from '@/views/apps/chat/ChatUserProfileSidebarContent.vue'
|
||||
import { useChat } from '@/views/apps/chat/useChat'
|
||||
import { useChatStore } from '@/views/apps/chat/useChatStore'
|
||||
|
||||
definePage({ meta: { layoutWrapperClasses: 'layout-content-height-fixed' } })
|
||||
|
||||
// composables
|
||||
const vuetifyDisplays = useDisplay()
|
||||
const store = useChatStore()
|
||||
const { isLeftSidebarOpen } = useResponsiveLeftSidebar(vuetifyDisplays.smAndDown)
|
||||
const { resolveAvatarBadgeVariant } = useChat()
|
||||
|
||||
// Perfect scrollbar
|
||||
const chatLogPS = ref()
|
||||
|
||||
const scrollToBottomInChatLog = () => {
|
||||
const scrollEl = chatLogPS.value.$el || chatLogPS.value
|
||||
|
||||
scrollEl.scrollTop = scrollEl.scrollHeight
|
||||
}
|
||||
|
||||
// Search query
|
||||
const q = ref('')
|
||||
|
||||
watch(q, val => store.fetchChatsAndContacts(val), { immediate: true })
|
||||
|
||||
// Open Sidebar in smAndDown when "start conversation" is clicked
|
||||
const startConversation = () => {
|
||||
if (vuetifyDisplays.mdAndUp.value)
|
||||
return
|
||||
isLeftSidebarOpen.value = true
|
||||
}
|
||||
|
||||
// Chat message
|
||||
const msg = ref('')
|
||||
|
||||
const sendMessage = async () => {
|
||||
if (!msg.value)
|
||||
return
|
||||
await store.sendMsg(msg.value)
|
||||
|
||||
// Reset message input
|
||||
msg.value = ''
|
||||
|
||||
// Scroll to bottom
|
||||
nextTick(() => {
|
||||
scrollToBottomInChatLog()
|
||||
})
|
||||
}
|
||||
|
||||
const openChatOfContact = async userId => {
|
||||
await store.getChat(userId)
|
||||
|
||||
// Reset message input
|
||||
msg.value = ''
|
||||
|
||||
// Set unseenMsgs to 0
|
||||
const contact = store.chatsContacts.find(c => c.id === userId)
|
||||
if (contact)
|
||||
contact.chat.unseenMsgs = 0
|
||||
|
||||
// if smAndDown => Close Chat & Contacts left sidebar
|
||||
if (vuetifyDisplays.smAndDown.value)
|
||||
isLeftSidebarOpen.value = false
|
||||
|
||||
// Scroll to bottom
|
||||
nextTick(() => {
|
||||
scrollToBottomInChatLog()
|
||||
})
|
||||
}
|
||||
|
||||
// User profile sidebar
|
||||
const isUserProfileSidebarOpen = ref(false)
|
||||
|
||||
// Active chat user profile sidebar
|
||||
const isActiveChatUserProfileSidebarOpen = ref(false)
|
||||
|
||||
// file input
|
||||
const refInputEl = ref()
|
||||
|
||||
const moreList = [
|
||||
{
|
||||
title: 'View Contact',
|
||||
value: 'View Contact',
|
||||
},
|
||||
{
|
||||
title: 'Mute Notifications',
|
||||
value: 'Mute Notifications',
|
||||
},
|
||||
{
|
||||
title: 'Block Contact',
|
||||
value: 'Block Contact',
|
||||
},
|
||||
{
|
||||
title: 'Clear Chat',
|
||||
value: 'Clear Chat',
|
||||
},
|
||||
{
|
||||
title: 'Report',
|
||||
value: 'Report',
|
||||
},
|
||||
]
|
||||
|
||||
const { name } = useTheme()
|
||||
|
||||
const chatContentContainerBg = computed(() => {
|
||||
let color = 'transparent'
|
||||
if (themes)
|
||||
color = themes?.[name.value].colors?.['chat-bg']
|
||||
|
||||
return color
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VLayout class="chat-app-layout bg-surface">
|
||||
<!-- 👉 user profile sidebar -->
|
||||
<VNavigationDrawer
|
||||
v-model="isUserProfileSidebarOpen"
|
||||
temporary
|
||||
touchless
|
||||
absolute
|
||||
class="user-profile-sidebar"
|
||||
location="start"
|
||||
width="370"
|
||||
>
|
||||
<ChatUserProfileSidebarContent @close="isUserProfileSidebarOpen = false" />
|
||||
</VNavigationDrawer>
|
||||
|
||||
<!-- 👉 Active Chat sidebar -->
|
||||
<VNavigationDrawer
|
||||
v-model="isActiveChatUserProfileSidebarOpen"
|
||||
width="374"
|
||||
absolute
|
||||
temporary
|
||||
location="end"
|
||||
touchless
|
||||
class="active-chat-user-profile-sidebar"
|
||||
>
|
||||
<ChatActiveChatUserProfileSidebarContent @close="isActiveChatUserProfileSidebarOpen = false" />
|
||||
</VNavigationDrawer>
|
||||
|
||||
<!-- 👉 Left sidebar -->
|
||||
<VNavigationDrawer
|
||||
v-model="isLeftSidebarOpen"
|
||||
absolute
|
||||
touchless
|
||||
location="start"
|
||||
width="370"
|
||||
:temporary="$vuetify.display.smAndDown"
|
||||
class="chat-list-sidebar"
|
||||
:permanent="$vuetify.display.mdAndUp"
|
||||
>
|
||||
<ChatLeftSidebarContent
|
||||
v-model:isDrawerOpen="isLeftSidebarOpen"
|
||||
v-model:search="q"
|
||||
@open-chat-of-contact="openChatOfContact"
|
||||
@show-user-profile="isUserProfileSidebarOpen = true"
|
||||
@close="isLeftSidebarOpen = false"
|
||||
/>
|
||||
</VNavigationDrawer>
|
||||
|
||||
<!-- 👉 Chat content -->
|
||||
<VMain class="chat-content-container">
|
||||
<!-- 👉 Right content: Active Chat -->
|
||||
<div
|
||||
v-if="store.activeChat"
|
||||
class="d-flex flex-column h-100"
|
||||
>
|
||||
<!-- 👉 Active chat header -->
|
||||
<div class="active-chat-header d-flex align-center text-medium-emphasis">
|
||||
<!-- Sidebar toggler -->
|
||||
<IconBtn
|
||||
class="d-md-none me-4"
|
||||
@click="isLeftSidebarOpen = true"
|
||||
>
|
||||
<VIcon icon="ri-menu-line" />
|
||||
</IconBtn>
|
||||
|
||||
<!-- avatar -->
|
||||
<div
|
||||
class="d-flex align-center cursor-pointer"
|
||||
@click="isActiveChatUserProfileSidebarOpen = true"
|
||||
>
|
||||
<VBadge
|
||||
dot
|
||||
location="bottom right"
|
||||
offset-x="3"
|
||||
offset-y="3"
|
||||
:color="resolveAvatarBadgeVariant(store.activeChat.contact.status)"
|
||||
bordered
|
||||
class="me-4"
|
||||
>
|
||||
<VAvatar
|
||||
size="40"
|
||||
:variant="!store.activeChat.contact.avatar ? 'tonal' : undefined"
|
||||
:color="!store.activeChat.contact.avatar ? resolveAvatarBadgeVariant(store.activeChat.contact.status) : undefined"
|
||||
class="cursor-pointer"
|
||||
>
|
||||
<VImg
|
||||
v-if="store.activeChat.contact.avatar"
|
||||
:src="store.activeChat.contact.avatar"
|
||||
:alt="store.activeChat.contact.fullName"
|
||||
/>
|
||||
<span v-else>{{ avatarText(store.activeChat.contact.fullName) }}</span>
|
||||
</VAvatar>
|
||||
</VBadge>
|
||||
|
||||
<div class="flex-grow-1 overflow-hidden">
|
||||
<h6 class="text-h6 font-weight-regular">
|
||||
{{ store.activeChat.contact.fullName }}
|
||||
</h6>
|
||||
<p class="text-body-2 text-truncate mb-0">
|
||||
{{ store.activeChat.contact.role }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VSpacer />
|
||||
|
||||
<!-- Header right content -->
|
||||
<div class="d-sm-flex align-center d-none">
|
||||
<IconBtn>
|
||||
<VIcon icon="ri-phone-line" />
|
||||
</IconBtn>
|
||||
<IconBtn>
|
||||
<VIcon icon="ri-vidicon-line" />
|
||||
</IconBtn>
|
||||
<IconBtn>
|
||||
<VIcon icon="ri-search-line" />
|
||||
</IconBtn>
|
||||
</div>
|
||||
|
||||
<MoreBtn :menu-list="moreList" />
|
||||
</div>
|
||||
|
||||
<VDivider />
|
||||
|
||||
<!-- Chat log -->
|
||||
<PerfectScrollbar
|
||||
ref="chatLogPS"
|
||||
tag="ul"
|
||||
:options="{ wheelPropagation: false }"
|
||||
class="flex-grow-1"
|
||||
>
|
||||
<ChatLog />
|
||||
</PerfectScrollbar>
|
||||
|
||||
<!-- Message form -->
|
||||
<VForm
|
||||
class="chat-log-message-form mb-5 mx-5"
|
||||
@submit.prevent="sendMessage"
|
||||
>
|
||||
<VTextField
|
||||
:key="store.activeChat?.contact.id"
|
||||
v-model="msg"
|
||||
variant="solo"
|
||||
density="default"
|
||||
class="chat-message-input"
|
||||
placeholder="Type your message..."
|
||||
autofocus
|
||||
>
|
||||
<template #append-inner>
|
||||
<IconBtn>
|
||||
<VIcon icon="ri-mic-line" />
|
||||
</IconBtn>
|
||||
|
||||
<IconBtn
|
||||
class="me-4"
|
||||
@click="refInputEl?.click()"
|
||||
>
|
||||
<VIcon icon="ri-attachment-2" />
|
||||
</IconBtn>
|
||||
|
||||
<VBtn
|
||||
append-icon="ri-send-plane-line"
|
||||
@click="sendMessage"
|
||||
>
|
||||
Send
|
||||
</VBtn>
|
||||
</template>
|
||||
</VTextField>
|
||||
|
||||
<input
|
||||
ref="refInputEl"
|
||||
type="file"
|
||||
name="file"
|
||||
accept=".jpeg,.png,.jpg,GIF"
|
||||
hidden
|
||||
>
|
||||
</VForm>
|
||||
</div>
|
||||
|
||||
<!-- 👉 Start conversation -->
|
||||
<div
|
||||
v-else
|
||||
class="d-flex h-100 align-center justify-center flex-column"
|
||||
>
|
||||
<VAvatar
|
||||
size="98"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
class="mb-5"
|
||||
>
|
||||
<VIcon
|
||||
size="50"
|
||||
icon="ri-wechat-line"
|
||||
/>
|
||||
</VAvatar>
|
||||
<p
|
||||
class="mb-0 px-4 py-2 font-weight-medium elevation-2 rounded-xl bg-primary"
|
||||
:class="[{ 'cursor-pointer': $vuetify.display.smAndDown }]"
|
||||
@click="startConversation"
|
||||
>
|
||||
Start Conversation
|
||||
</p>
|
||||
</div>
|
||||
</VMain>
|
||||
</VLayout>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@styles/variables/vuetify.scss";
|
||||
@use "@core-scss/base/mixins.scss";
|
||||
@use "@layouts/styles/mixins" as layoutsMixins;
|
||||
|
||||
// Variables
|
||||
$chat-app-header-height: 76px;
|
||||
|
||||
// Placeholders
|
||||
%chat-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-block-size: $chat-app-header-height;
|
||||
padding-inline: 1.25rem;
|
||||
}
|
||||
|
||||
.chat-app-layout {
|
||||
border-radius: vuetify.$card-border-radius;
|
||||
|
||||
@include mixins.elevation(vuetify.$card-elevation);
|
||||
|
||||
$sel-chat-app-layout: &;
|
||||
|
||||
@at-root {
|
||||
.skin--bordered {
|
||||
@include mixins.bordered-skin($sel-chat-app-layout);
|
||||
}
|
||||
}
|
||||
|
||||
.active-chat-user-profile-sidebar,
|
||||
.user-profile-sidebar {
|
||||
.v-navigation-drawer__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-list-header,
|
||||
.active-chat-header {
|
||||
@extend %chat-header;
|
||||
}
|
||||
|
||||
.chat-list-search {
|
||||
.v-field__outline__start {
|
||||
flex-basis: 20px !important;
|
||||
border-radius: 28px 0 0 28px !important;
|
||||
}
|
||||
|
||||
.v-field__outline__end {
|
||||
border-radius: 0 28px 28px 0 !important;
|
||||
}
|
||||
|
||||
@include layoutsMixins.rtl {
|
||||
.v-field__outline__start {
|
||||
flex-basis: 20px !important;
|
||||
border-radius: 0 28px 28px 0 !important;
|
||||
}
|
||||
|
||||
.v-field__outline__end {
|
||||
border-radius: 28px 0 0 28px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-list-sidebar {
|
||||
.v-navigation-drawer__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-content-container {
|
||||
/* stylelint-disable-next-line value-keyword-case */
|
||||
background-color: v-bind(chatContentContainerBg);
|
||||
|
||||
// Adjust the padding so text field height stays 48px
|
||||
.chat-message-input {
|
||||
.v-field__append-inner {
|
||||
align-items: center;
|
||||
padding-block-start: 0;
|
||||
}
|
||||
|
||||
.v-field--appended {
|
||||
padding-inline-end: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-user-profile-badge {
|
||||
.v-badge__badge {
|
||||
/* stylelint-disable liberty/use-logical-spec */
|
||||
min-width: 12px !important;
|
||||
height: 0.75rem;
|
||||
/* stylelint-enable liberty/use-logical-spec */
|
||||
}
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user