wip
This commit is contained in:
48
package-lock.json
generated
48
package-lock.json
generated
@@ -8,10 +8,13 @@
|
||||
"name": "chat-client",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@fontsource/roboto": "^5.2.9",
|
||||
"pinia": "^3.0.4",
|
||||
"vue": "^3.5.28"
|
||||
"vue": "^3.5.28",
|
||||
"vuetify": "^3.11.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mdi/font": "^7.4.47",
|
||||
"@tsconfig/node24": "^24.0.4",
|
||||
"@types/node": "^24.10.13",
|
||||
"@vitejs/plugin-vue": "^6.0.4",
|
||||
@@ -1140,6 +1143,15 @@
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource/roboto": {
|
||||
"version": "5.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.2.9.tgz",
|
||||
"integrity": "sha512-ZTkyHiPk74B/aj8BZWbsxD5Yu+Lq+nR64eV4wirlrac2qXR7jYk2h6JlLYuOuoruTkGQWNw2fMuKNavw7/rg0w==",
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||
@@ -1241,6 +1253,13 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@mdi/font": {
|
||||
"version": "7.4.47",
|
||||
"resolved": "https://registry.npmjs.org/@mdi/font/-/font-7.4.47.tgz",
|
||||
"integrity": "sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -5196,6 +5215,33 @@
|
||||
"typescript": ">=5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vuetify": {
|
||||
"version": "3.11.8",
|
||||
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.11.8.tgz",
|
||||
"integrity": "sha512-4iKnntOnLFFklygZjzlVfcHrtLO8+iK4HOhiia6HP2U8v82x+ngaSCgm+epvPrGyCMfCpfuEttqD2qElrr1axw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/johnleider"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.7",
|
||||
"vite-plugin-vuetify": ">=2.1.0",
|
||||
"vue": "^3.5.0",
|
||||
"webpack-plugin-vuetify": ">=3.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
},
|
||||
"vite-plugin-vuetify": {
|
||||
"optional": true
|
||||
},
|
||||
"webpack-plugin-vuetify": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
||||
@@ -15,10 +15,13 @@
|
||||
"format": "prettier --write --experimental-cli src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/roboto": "^5.2.9",
|
||||
"pinia": "^3.0.4",
|
||||
"vue": "^3.5.28"
|
||||
"vue": "^3.5.28",
|
||||
"vuetify": "^3.11.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mdi/font": "^7.4.47",
|
||||
"@tsconfig/node24": "^24.0.4",
|
||||
"@types/node": "^24.10.13",
|
||||
"@vitejs/plugin-vue": "^6.0.4",
|
||||
|
||||
129
src/App.vue
129
src/App.vue
@@ -1,8 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import WebSocketComponent from '@/components/WebSocketComponent.vue'
|
||||
import { ref } from 'vue'
|
||||
import ChatMessageText from '@/components/ChatMessageText.vue'
|
||||
import ChatMessage from '@/components/ChatMessage.vue'
|
||||
|
||||
const email = ref('vadim.olonin@gmail.com')
|
||||
const search = ref('foobar')
|
||||
|
||||
async function onSubmit() {
|
||||
console.log('onSubmit')
|
||||
@@ -28,20 +31,120 @@ async function onSubmit() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div>chat</div>
|
||||
<form @submit.prevent="onSubmit">
|
||||
<div>login</div>
|
||||
<div>
|
||||
<input v-model="email" placeholder="email" type="email" />
|
||||
<button>login</button>
|
||||
</div>
|
||||
</form>
|
||||
<hr />
|
||||
<div>
|
||||
<WebSocketComponent />
|
||||
<v-app>
|
||||
<v-main class="chat-fullscreen">
|
||||
<v-container fluid class="h-100 pa-0">
|
||||
<v-row no-gutters class="h-100">
|
||||
<v-col cols="12" md="3">
|
||||
<v-sheet class="pa-4 d-flex flex-column h-100" theme="dark">
|
||||
<div class="d-flex align-center ga-4">
|
||||
<v-btn icon="mdi-menu" variant="text"></v-btn>
|
||||
<v-text-field
|
||||
v-model="search"
|
||||
rounded="xl"
|
||||
density="compact"
|
||||
hide-details
|
||||
variant="outlined"
|
||||
bg-color="black"
|
||||
base-color="black"
|
||||
color="primary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<v-list theme="dark" density="compact" lines="two">
|
||||
<v-list-subheader>Чаты</v-list-subheader>
|
||||
|
||||
<v-list-item
|
||||
v-for="n in 5"
|
||||
:key="n"
|
||||
:title="'Контакт ' + n"
|
||||
:subtitle="'Последнее сообщение...'"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-avatar color="grey-lighten-1" text="V"> </v-avatar>
|
||||
</template>
|
||||
<template v-slot:append>
|
||||
<div class="d-flex flex-column align-end">
|
||||
<v-chip density="compact" size="small">2</v-chip>
|
||||
<div class="text-caption">22:22</div>
|
||||
</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-sheet>
|
||||
</v-col>
|
||||
|
||||
<style scoped></style>
|
||||
<v-col cols="9" class="d-flex flex-column h-100">
|
||||
<v-toolbar color="white" class="flex-grow-0 px-4">
|
||||
<v-avatar color="grey-lighten-1" text="V"></v-avatar>
|
||||
<v-toolbar-title class="">name</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
|
||||
<v-card
|
||||
rounded="0"
|
||||
variant="flat"
|
||||
color="grey-lighten-3"
|
||||
class="flex-grow-1 d-flex flex-column"
|
||||
style="overflow: hidden"
|
||||
>
|
||||
<v-card-text
|
||||
class="flex-grow-1 d-flex ga-4 flex-column flex-column-reverse"
|
||||
style="overflow-y: auto"
|
||||
>
|
||||
<!-- <div class="d-flex justify-center">-->
|
||||
<!-- <v-chip size="small">Сегодня</v-chip>-->
|
||||
<!-- </div>-->
|
||||
|
||||
<chat-message />
|
||||
<chat-message username="Vadim" text="Привет! Как дела? Что нового?" />
|
||||
<chat-message
|
||||
username="Vadim"
|
||||
text="Привет! Как дела? Что нового?"
|
||||
created-at="2020-02-04T05:45:00.000Z"
|
||||
/>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions class="flex-grow-0 bg-white pa-4">
|
||||
<v-row dense align="center">
|
||||
<v-col>
|
||||
<v-text-field
|
||||
rounded="xl"
|
||||
bg-color="white"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
label="message"
|
||||
>
|
||||
<template v-slot:prepend-inner>
|
||||
<v-btn icon variant="text" density="compact" color="grey-darken-1">
|
||||
<v-icon>mdi-emoticon-outline</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="auto">
|
||||
<v-btn icon variant="text" color="grey-darken-1">
|
||||
<v-icon>mdi-paperclip</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="auto">
|
||||
<v-btn icon variant="flat" color="primary">
|
||||
<v-icon>mdi-send</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-main>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.chat-fullscreen {
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
|
||||
41
src/components/ChatMessage.vue
Normal file
41
src/components/ChatMessage.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{ createdAt?: string; username?: string; side?: 'left' | 'right'; text?: string }>(),
|
||||
{
|
||||
text: 'foobar',
|
||||
side: 'left',
|
||||
username: 'robot',
|
||||
},
|
||||
)
|
||||
|
||||
const createdAt = computed(() => {
|
||||
return props.createdAt
|
||||
? new Date(props.createdAt).toLocaleTimeString('ru-RU', { timeStyle: 'short' })
|
||||
: new Date().toLocaleTimeString('ru-RU', { timeStyle: 'short' })
|
||||
})
|
||||
|
||||
const avatarLetter = computed(() => {
|
||||
return props.username.slice(0, 1).toUpperCase()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="d-flex ga-2">
|
||||
<v-avatar size="36" color="blue-lighten-4">
|
||||
<span class="text-blue-darken-2">{{ avatarLetter }}</span>
|
||||
</v-avatar>
|
||||
<div class="message-wrapper">
|
||||
<div class="d-flex align-center ga-2">
|
||||
<span class="text-subtitle-2 font-weight-medium">{{ props.username }}</span>
|
||||
<span class="text-caption text-disabled">{{ createdAt }}</span>
|
||||
</div>
|
||||
<v-card class="pa-3" rounded="lg" flat>
|
||||
<span>{{ props.text }}</span>
|
||||
</v-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
73
src/components/ChatMessageText.vue
Normal file
73
src/components/ChatMessageText.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="d-flex mb-4 ga-2">
|
||||
<v-avatar size="36" class="" color="blue-lighten-4">
|
||||
<span class="text-blue-darken-2">А</span>
|
||||
</v-avatar>
|
||||
<div class="message-wrapper">
|
||||
<div class="d-flex align-center ga-2">
|
||||
<span class="text-subtitle-2 font-weight-medium">Анна</span>
|
||||
<span class="text-caption text-disabled">10:30</span>
|
||||
</div>
|
||||
<v-card class="pa-4" rounded="lg" flat>
|
||||
<span>Привет! Как дела? Что нового?</span>
|
||||
</v-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex mb-4 ga-2">
|
||||
<v-avatar size="36" class="" color="deep-purple-lighten-4">
|
||||
<span class="text-deep-purple-darken-2">М</span>
|
||||
</v-avatar>
|
||||
<div class="message-wrapper">
|
||||
<div class="d-flex align-center ga-2">
|
||||
<span class="text-subtitle-2 font-weight-medium">Максим</span>
|
||||
<span class="text-caption text-disabled">10:30</span>
|
||||
</div>
|
||||
<v-card class="pa-4" rounded="lg" flat>
|
||||
<span>
|
||||
Всем привет! Смотрели новый фильм? Там такие спецэффекты, просто космос! Очень советую
|
||||
сходить в кино.
|
||||
</span>
|
||||
</v-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex mb-4 ga-2 flex-row-reverse">
|
||||
<v-avatar size="36" color="primary">
|
||||
<span class="text-white">Я</span>
|
||||
</v-avatar>
|
||||
<div class="message-wrapper" style="align-items: flex-end">
|
||||
<div class="d-flex align-center flex-row-reverse ga-2">
|
||||
<span class="text-caption text-disabled">10:35</span>
|
||||
<span class="text-subtitle-2 font-weight-medium">Вы</span>
|
||||
</div>
|
||||
<v-card class="message-bubble my-message pa-4" rounded="lg" flat color="primary">
|
||||
<span class="text-white">Отлично! Тоже хочу сходить.</span>
|
||||
</v-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex mb-4 ga-2">
|
||||
<v-avatar size="36" color="orange-lighten-4">
|
||||
<span class="text-orange-darken-2">К</span>
|
||||
</v-avatar>
|
||||
<div class="message-wrapper">
|
||||
<div class="d-flex align-center ga-2">
|
||||
<span class="text-subtitle-2 font-weight-medium">Катя</span>
|
||||
<span class="text-caption text-disabled">10:40</span>
|
||||
</div>
|
||||
<v-card class="message-bubble friend-message pa-4" rounded="lg" flat>
|
||||
<span>Смотрите какой милый котик</span>
|
||||
<v-img
|
||||
src="https://cdn.vuetifyjs.com/images/cards/kitchen.png"
|
||||
max-width="200"
|
||||
class="mt-2 rounded"
|
||||
></v-img>
|
||||
</v-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -3,6 +3,7 @@ import { ref } from 'vue'
|
||||
import { useSharedWebSocket, WsDataType } from '../composables/useSharedWebSocket'
|
||||
|
||||
const message = ref('')
|
||||
const selectedChat = ref('')
|
||||
|
||||
const { messages, chats, isConnected, error, send } = useSharedWebSocket({
|
||||
// onMessage: (data) => {
|
||||
@@ -10,11 +11,21 @@ const { messages, chats, isConnected, error, send } = useSharedWebSocket({
|
||||
// },
|
||||
})
|
||||
|
||||
const onChatClick = (id: string) => {
|
||||
console.log(id)
|
||||
selectedChat.value = id
|
||||
send({
|
||||
type: WsDataType.GET_MESSAGES,
|
||||
data: { chat_id: id },
|
||||
})
|
||||
}
|
||||
|
||||
const sendMessage = () => {
|
||||
if (message.value.trim()) {
|
||||
send({
|
||||
type: WsDataType.MESSAGE,
|
||||
type: WsDataType.CREATE_MESSAGE,
|
||||
data: {
|
||||
chat_id: selectedChat.value,
|
||||
text: message.value,
|
||||
},
|
||||
})
|
||||
@@ -24,37 +35,55 @@ const sendMessage = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<div class="status" :class="{ isConnected, disconnected: !isConnected }">
|
||||
status: {{ isConnected ? 'connected' : 'disconnected' }}
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="error">Error: {{ error }}</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div>selected chat {{ selectedChat }}</div>
|
||||
<div class="error">Error: {{ error }}</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<div>
|
||||
<div>chats</div>
|
||||
<div v-for="chat in chats" :key="chat.id">
|
||||
<div v-for="chat in chats" :key="chat.id" @click="onChatClick(chat.id)">
|
||||
<div>{{ chat.id }}</div>
|
||||
<div>{{ chat.users }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div>
|
||||
<div>messages:</div>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-sheet border class="pa-4 border-opacity-50">
|
||||
<div v-if="selectedChat">
|
||||
<v-sheet height="400px" class="overflow-y-auto">
|
||||
<div v-for="message in messages" :key="message.id">
|
||||
{{ message }}
|
||||
</div>
|
||||
<div>{{ message }}</div>
|
||||
</div>
|
||||
</v-sheet>
|
||||
|
||||
<div class="controls">
|
||||
<input v-model="message" @keyup.enter="sendMessage" placeholder="Type message..." />
|
||||
<button @click="sendMessage" :disabled="!isConnected">Send</button>
|
||||
<div class="d-flex align-center ga-1">
|
||||
<v-text-field
|
||||
rounded="lg"
|
||||
hide-details
|
||||
variant="outlined"
|
||||
v-model="message"
|
||||
@keyup.enter="sendMessage"
|
||||
placeholder="Type message..."
|
||||
>
|
||||
</v-text-field>
|
||||
<v-btn
|
||||
:disabled="!isConnected"
|
||||
variant="elevated"
|
||||
color="primary"
|
||||
icon="mdi-send"
|
||||
@click="sendMessage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</v-sheet>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -21,13 +21,17 @@ export interface User {
|
||||
export interface WsData {
|
||||
type: WsDataType
|
||||
// data: Chat | Message | User | User[] | Message[] | Chat[]
|
||||
data: {text: string}
|
||||
data: unknown
|
||||
}
|
||||
|
||||
export enum WsDataType {
|
||||
CHAT = 'CHAT',
|
||||
GET_CHATS = 'GET_CHATS',
|
||||
GET_USERS = 'GET_USERS',
|
||||
GET_MESSAGES = 'GET_MESSAGES',
|
||||
CREATE_MESSAGE = 'CREATE_MESSAGE',
|
||||
CHATS = 'CHATS',
|
||||
USER = 'USER',
|
||||
MESSAGE = 'MESSAGE',
|
||||
MESSAGES = 'MESSAGES',
|
||||
STATUS = 'STATUS',
|
||||
ERROR = 'ERROR',
|
||||
}
|
||||
@@ -69,16 +73,19 @@ export function useSharedWebSocket(options?: { url?: string; autoConnect?: true
|
||||
case WsDataType.USER:
|
||||
console.log('USER')
|
||||
break
|
||||
case WsDataType.CHAT:
|
||||
case WsDataType.CHATS:
|
||||
console.log('chat')
|
||||
chats.value = data
|
||||
break
|
||||
case WsDataType.MESSAGE:
|
||||
messages.value.push(data)
|
||||
case WsDataType.MESSAGES:
|
||||
messages.value = data
|
||||
// if (options.onMessage) {
|
||||
// options.onMessage(data)
|
||||
// }
|
||||
break
|
||||
case WsDataType.CREATE_MESSAGE:
|
||||
messages.value.push(data)
|
||||
break
|
||||
case WsDataType.STATUS:
|
||||
isConnected.value = connected
|
||||
break
|
||||
|
||||
19
src/main.ts
19
src/main.ts
@@ -1,9 +1,28 @@
|
||||
import '@fontsource/roboto/100.css'
|
||||
import '@fontsource/roboto/300.css'
|
||||
import '@fontsource/roboto/400.css'
|
||||
import '@mdi/font/css/materialdesignicons.css'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
|
||||
import 'vuetify/styles'
|
||||
import { createVuetify } from 'vuetify'
|
||||
import * as components from 'vuetify/components'
|
||||
import * as directives from 'vuetify/directives'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia())
|
||||
|
||||
const vuetify = createVuetify({
|
||||
components,
|
||||
directives,
|
||||
icons: {
|
||||
defaultSet: 'mdi',
|
||||
},
|
||||
})
|
||||
app.use(vuetify)
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
Reference in New Issue
Block a user