✨ feat(src/views/matches/MatchesView.vue): добавляет поддержку чатов с использованием сокетов
✨ feat(src/views/profile/MyProfileView.vue): добавляет раздел истории чатов в профиль пользователя ✨ feat(src/composables/useSocket.ts): создает хук для работы с сокетами 📝 chore(src/queries/useChatsQuery.ts): обновляет логику закрытия чата с учетом новых параметров 📝 chore(src/components/profile/MediaGallery.vue): исправляет путь к медиафайлам в галерее 🔧 fix(src/components/layout/SideNav.vue): комментирует неиспользуемый элемент навигации "Встречи"
This commit is contained in:
@@ -13,8 +13,8 @@
|
|||||||
"gen:api": "npx swagger-typescript-api@12.0.4 -p http://localhost:1337/api/docs-json -o ./src/api -n api.ts --axios --unwrap-response-data --extract-request-params --extract-request-body --single-http-client"
|
"gen:api": "npx swagger-typescript-api@12.0.4 -p http://localhost:1337/api/docs-json -o ./src/api -n api.ts --axios --unwrap-response-data --extract-request-params --extract-request-body --single-http-client"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/vue-query": "^5.101.0",
|
|
||||||
"@floating-ui/vue": "^1.1.5",
|
"@floating-ui/vue": "^1.1.5",
|
||||||
|
"@tanstack/vue-query": "^5.101.0",
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-dialog": "^2",
|
"@tauri-apps/plugin-dialog": "^2",
|
||||||
"@tauri-apps/plugin-shell": "^2",
|
"@tauri-apps/plugin-shell": "^2",
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
"esbuild": "^0.28.0",
|
"esbuild": "^0.28.0",
|
||||||
"gsap": "^3.12.5",
|
"gsap": "^3.12.5",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
|
"socket.io-client": "^4.8.3",
|
||||||
"vue": "^3.5.6",
|
"vue": "^3.5.6",
|
||||||
"vue-router": "^4.4.5"
|
"vue-router": "^4.4.5"
|
||||||
},
|
},
|
||||||
|
|||||||
75
pnpm-lock.yaml
generated
75
pnpm-lock.yaml
generated
@@ -46,6 +46,9 @@ importers:
|
|||||||
leaflet:
|
leaflet:
|
||||||
specifier: ^1.9.4
|
specifier: ^1.9.4
|
||||||
version: 1.9.4
|
version: 1.9.4
|
||||||
|
socket.io-client:
|
||||||
|
specifier: ^4.8.3
|
||||||
|
version: 4.8.3
|
||||||
vue:
|
vue:
|
||||||
specifier: ^3.5.6
|
specifier: ^3.5.6
|
||||||
version: 3.5.35(typescript@5.9.3)
|
version: 3.5.35(typescript@5.9.3)
|
||||||
@@ -989,6 +992,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-TeheYy0ILzBEI/CO55CP6zJCSdSWeRtGnHy8U8dWSUH4I68iqTsy7HkMktR4xakThc9jotkPQUXT4ITdbV7cHA==}
|
resolution: {integrity: sha512-TeheYy0ILzBEI/CO55CP6zJCSdSWeRtGnHy8U8dWSUH4I68iqTsy7HkMktR4xakThc9jotkPQUXT4ITdbV7cHA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
'@socket.io/component-emitter@3.1.2':
|
||||||
|
resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==}
|
||||||
|
|
||||||
'@stylistic/eslint-plugin@5.10.0':
|
'@stylistic/eslint-plugin@5.10.0':
|
||||||
resolution: {integrity: sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ==}
|
resolution: {integrity: sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
@@ -1587,6 +1593,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==}
|
resolution: {integrity: sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
engine.io-client@6.6.5:
|
||||||
|
resolution: {integrity: sha512-QCwxUDULPlXv8F6tqMMKx5dNkTe6OaBYRMPYeXKBlyOoKvAmE0ac6pW7fFhSscJ/5SI7666/U/B+MElbsrJlIg==}
|
||||||
|
|
||||||
|
engine.io-parser@5.2.3:
|
||||||
|
resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
|
||||||
enhanced-resolve@5.23.0:
|
enhanced-resolve@5.23.0:
|
||||||
resolution: {integrity: sha512-yJN/BOOLxcOW2aQgeif9mSnaUB8KtvmMMp56oA1kx1CRfBKbhZm2pJ+NBY+3eOboHxix8lfjWpHE0Ei5U8RbSA==}
|
resolution: {integrity: sha512-yJN/BOOLxcOW2aQgeif9mSnaUB8KtvmMMp56oA1kx1CRfBKbhZm2pJ+NBY+3eOboHxix8lfjWpHE0Ei5U8RbSA==}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
@@ -2516,6 +2529,14 @@ packages:
|
|||||||
sisteransi@1.0.5:
|
sisteransi@1.0.5:
|
||||||
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
|
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
|
||||||
|
|
||||||
|
socket.io-client@4.8.3:
|
||||||
|
resolution: {integrity: sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
|
||||||
|
socket.io-parser@4.2.6:
|
||||||
|
resolution: {integrity: sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
|
||||||
source-map-js@1.2.1:
|
source-map-js@1.2.1:
|
||||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -2708,10 +2729,26 @@ packages:
|
|||||||
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
ws@8.20.1:
|
||||||
|
resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
bufferutil: ^4.0.1
|
||||||
|
utf-8-validate: '>=5.0.2'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
bufferutil:
|
||||||
|
optional: true
|
||||||
|
utf-8-validate:
|
||||||
|
optional: true
|
||||||
|
|
||||||
xml-name-validator@4.0.0:
|
xml-name-validator@4.0.0:
|
||||||
resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==}
|
resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
xmlhttprequest-ssl@2.1.2:
|
||||||
|
resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==}
|
||||||
|
engines: {node: '>=0.4.0'}
|
||||||
|
|
||||||
yaml-eslint-parser@2.0.0:
|
yaml-eslint-parser@2.0.0:
|
||||||
resolution: {integrity: sha512-h0uDm97wvT2bokfwwTmY6kJ1hp6YDFL0nRHwNKz8s/VD1FH/vvZjAKoMUE+un0eaYBSG7/c6h+lJTP+31tjgTw==}
|
resolution: {integrity: sha512-h0uDm97wvT2bokfwwTmY6kJ1hp6YDFL0nRHwNKz8s/VD1FH/vvZjAKoMUE+un0eaYBSG7/c6h+lJTP+31tjgTw==}
|
||||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||||
@@ -3317,6 +3354,8 @@ snapshots:
|
|||||||
|
|
||||||
'@sindresorhus/base62@1.0.0': {}
|
'@sindresorhus/base62@1.0.0': {}
|
||||||
|
|
||||||
|
'@socket.io/component-emitter@3.1.2': {}
|
||||||
|
|
||||||
'@stylistic/eslint-plugin@5.10.0(eslint@10.4.1(jiti@2.7.0))':
|
'@stylistic/eslint-plugin@5.10.0(eslint@10.4.1(jiti@2.7.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(jiti@2.7.0))
|
'@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(jiti@2.7.0))
|
||||||
@@ -3895,6 +3934,20 @@ snapshots:
|
|||||||
|
|
||||||
empathic@2.0.1: {}
|
empathic@2.0.1: {}
|
||||||
|
|
||||||
|
engine.io-client@6.6.5:
|
||||||
|
dependencies:
|
||||||
|
'@socket.io/component-emitter': 3.1.2
|
||||||
|
debug: 4.4.3
|
||||||
|
engine.io-parser: 5.2.3
|
||||||
|
ws: 8.20.1
|
||||||
|
xmlhttprequest-ssl: 2.1.2
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- supports-color
|
||||||
|
- utf-8-validate
|
||||||
|
|
||||||
|
engine.io-parser@5.2.3: {}
|
||||||
|
|
||||||
enhanced-resolve@5.23.0:
|
enhanced-resolve@5.23.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
@@ -5095,6 +5148,24 @@ snapshots:
|
|||||||
|
|
||||||
sisteransi@1.0.5: {}
|
sisteransi@1.0.5: {}
|
||||||
|
|
||||||
|
socket.io-client@4.8.3:
|
||||||
|
dependencies:
|
||||||
|
'@socket.io/component-emitter': 3.1.2
|
||||||
|
debug: 4.4.3
|
||||||
|
engine.io-client: 6.6.5
|
||||||
|
socket.io-parser: 4.2.6
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- supports-color
|
||||||
|
- utf-8-validate
|
||||||
|
|
||||||
|
socket.io-parser@4.2.6:
|
||||||
|
dependencies:
|
||||||
|
'@socket.io/component-emitter': 3.1.2
|
||||||
|
debug: 4.4.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
source-map-js@1.2.1: {}
|
source-map-js@1.2.1: {}
|
||||||
|
|
||||||
spdx-exceptions@2.5.0: {}
|
spdx-exceptions@2.5.0: {}
|
||||||
@@ -5246,8 +5317,12 @@ snapshots:
|
|||||||
|
|
||||||
word-wrap@1.2.5: {}
|
word-wrap@1.2.5: {}
|
||||||
|
|
||||||
|
ws@8.20.1: {}
|
||||||
|
|
||||||
xml-name-validator@4.0.0: {}
|
xml-name-validator@4.0.0: {}
|
||||||
|
|
||||||
|
xmlhttprequest-ssl@2.1.2: {}
|
||||||
|
|
||||||
yaml-eslint-parser@2.0.0:
|
yaml-eslint-parser@2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
eslint-visitor-keys: 5.0.1
|
eslint-visitor-keys: 5.0.1
|
||||||
|
|||||||
@@ -3,6 +3,6 @@ shellEmulator: true
|
|||||||
trustPolicy: no-downgrade
|
trustPolicy: no-downgrade
|
||||||
|
|
||||||
allowBuilds:
|
allowBuilds:
|
||||||
'@parcel/watcher': set this to true or false
|
'@parcel/watcher': false
|
||||||
esbuild: set this to true or false
|
esbuild: false
|
||||||
vue-demi: set this to true or false
|
vue-demi: false
|
||||||
|
|||||||
@@ -169,9 +169,17 @@ export interface MatchDto {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ChatDto {
|
||||||
|
id: string;
|
||||||
|
profile1Id: string;
|
||||||
|
profile2Id: string;
|
||||||
|
status: "active" | "closed";
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreateLikeResponseDto {
|
export interface CreateLikeResponseDto {
|
||||||
like: LikeDto;
|
like: LikeDto;
|
||||||
match?: MatchDto | null;
|
match?: MatchDto | null;
|
||||||
|
chat?: ChatDto | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateChatDto {
|
export interface CreateChatDto {
|
||||||
@@ -181,13 +189,6 @@ export interface CreateChatDto {
|
|||||||
matchId: string;
|
matchId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChatDto {
|
|
||||||
id: string;
|
|
||||||
profile1Id: string;
|
|
||||||
profile2Id: string;
|
|
||||||
status: "active" | "closed";
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MessageDto {
|
export interface MessageDto {
|
||||||
id: string;
|
id: string;
|
||||||
chatId: string;
|
chatId: string;
|
||||||
@@ -204,6 +205,17 @@ export interface SendMessageDto {
|
|||||||
mediaType?: "photo" | "voice" | "video";
|
mediaType?: "photo" | "voice" | "video";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CloseChatDto {
|
||||||
|
/** cancel — отменить матч, report — пожаловаться */
|
||||||
|
type: "cancel" | "report";
|
||||||
|
/** Сообщение собеседнику при отмене матча */
|
||||||
|
cancelMessage?: string;
|
||||||
|
/** Причина жалобы (preset) */
|
||||||
|
reportReason?: string;
|
||||||
|
/** Дополнительное описание жалобы */
|
||||||
|
reportDescription?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreateDateDto {
|
export interface CreateDateDto {
|
||||||
/** Your profile ID */
|
/** Your profile ID */
|
||||||
profileId: string;
|
profileId: string;
|
||||||
@@ -978,16 +990,22 @@ export class Api<SecurityDataType extends unknown> {
|
|||||||
*
|
*
|
||||||
* @tags chat
|
* @tags chat
|
||||||
* @name ChatControllerCloseChat
|
* @name ChatControllerCloseChat
|
||||||
* @summary Close a chat
|
* @summary Close a chat (cancel match or report)
|
||||||
* @request DELETE:/api/v1/chats/{chatId}
|
* @request DELETE:/api/v1/chats/{chatId}
|
||||||
* @secure
|
* @secure
|
||||||
*/
|
*/
|
||||||
chatControllerCloseChat: ({ chatId, ...query }: ChatControllerCloseChatParams, params: RequestParams = {}) =>
|
chatControllerCloseChat: (
|
||||||
|
{ chatId, ...query }: ChatControllerCloseChatParams,
|
||||||
|
data: CloseChatDto,
|
||||||
|
params: RequestParams = {},
|
||||||
|
) =>
|
||||||
this.http.request<MessageResponseDto, any>({
|
this.http.request<MessageResponseDto, any>({
|
||||||
path: `/api/v1/chats/${chatId}`,
|
path: `/api/v1/chats/${chatId}`,
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
query: query,
|
query: query,
|
||||||
|
body: data,
|
||||||
secure: true,
|
secure: true,
|
||||||
|
type: ContentType.Json,
|
||||||
format: "json",
|
format: "json",
|
||||||
...params,
|
...params,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -45,12 +45,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { IconName } from '@/assets/icons/icon-names'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
|
import AppIcon from '@/components/common/AppIcon.vue'
|
||||||
import { useAuth } from '@/composables/useAuth'
|
import { useAuth } from '@/composables/useAuth'
|
||||||
import { useUi } from '@/composables/useUi'
|
import { useUi } from '@/composables/useUi'
|
||||||
import AppIcon from '@/components/common/AppIcon.vue'
|
|
||||||
import type { IconName } from '@/assets/icons/icon-names'
|
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const authStore = useAuth()
|
const authStore = useAuth()
|
||||||
@@ -68,7 +68,7 @@ const navItems: NavItem[] = [
|
|||||||
{ name: 'feed', path: '/feed', label: 'Лента', icon: 'grid' },
|
{ name: 'feed', path: '/feed', label: 'Лента', icon: 'grid' },
|
||||||
{ name: 'matches', path: '/matches', label: 'Совпадения', icon: 'heart' },
|
{ name: 'matches', path: '/matches', label: 'Совпадения', icon: 'heart' },
|
||||||
{ name: 'chats', path: '/chats', label: 'Чаты', icon: 'chat' },
|
{ name: 'chats', path: '/chats', label: 'Чаты', icon: 'chat' },
|
||||||
{ name: 'dates', path: '/dates', label: 'Встречи', icon: 'calendar' },
|
// { name: 'dates', path: '/dates', label: 'Встречи', icon: 'calendar' },
|
||||||
{ name: 'profile', path: '/profile/me', label: 'Профиль', icon: 'person' },
|
{ name: 'profile', path: '/profile/me', label: 'Профиль', icon: 'person' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -22,11 +22,11 @@
|
|||||||
class="gallery__item"
|
class="gallery__item"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
:src="item.url"
|
:src="item.path"
|
||||||
class="gallery__img"
|
class="gallery__img"
|
||||||
alt="Медиа"
|
alt="Медиа"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
@click="lightboxUrl = item.url"
|
@click="lightboxUrl = item.path"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
v-if="editable"
|
v-if="editable"
|
||||||
@@ -56,12 +56,12 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { apiClient } from '@/api/client'
|
import { apiClient, axiosInstance, BASE_URL } from '@/api/client'
|
||||||
import { useUi } from '@/composables/useUi'
|
import { useUi } from '@/composables/useUi'
|
||||||
|
|
||||||
interface MediaItem {
|
interface MediaItem {
|
||||||
id: string
|
id: string
|
||||||
url: string
|
path: string
|
||||||
type: string
|
type: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,11 +114,28 @@ async function onFileChange(e: Event) {
|
|||||||
fileInput.value.value = ''
|
fileInput.value.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
async function doUpload(_file: File | null, _tauriPath?: string) {
|
async function doUpload(file: File | null, tauriPath?: string) {
|
||||||
uploading.value = true
|
uploading.value = true
|
||||||
try {
|
try {
|
||||||
// API expects multipart/form-data with the file; we pass query type=photo
|
const formData = new FormData()
|
||||||
await apiClient.api.mediaControllerUpload({ profileId: props.profileId, type: 'photo' })
|
if (file) {
|
||||||
|
formData.append('file', file)
|
||||||
|
}
|
||||||
|
else if (tauriPath) {
|
||||||
|
const { convertFileSrc } = await import('@tauri-apps/api/core')
|
||||||
|
const assetUrl = convertFileSrc(tauriPath)
|
||||||
|
const res = await fetch(assetUrl)
|
||||||
|
const blob = await res.blob()
|
||||||
|
const name = tauriPath.split(/[\\/]/).pop() ?? 'photo.jpg'
|
||||||
|
formData.append('file', blob, name)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new Error('No file provided')
|
||||||
|
}
|
||||||
|
await axiosInstance.post(
|
||||||
|
`${BASE_URL}/api/v1/profiles/${props.profileId}/media/upload?type=photo`,
|
||||||
|
formData,
|
||||||
|
)
|
||||||
uiStore.addToast('Фото загружено', 'success')
|
uiStore.addToast('Фото загружено', 'success')
|
||||||
await loadMedia()
|
await loadMedia()
|
||||||
emit('updated')
|
emit('updated')
|
||||||
|
|||||||
65
src/composables/useSocket.ts
Normal file
65
src/composables/useSocket.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { io, Socket } from 'socket.io-client'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { BASE_URL, _getAccessToken } from '@/api/client'
|
||||||
|
|
||||||
|
let socket: Socket | null = null
|
||||||
|
const connected = ref(false)
|
||||||
|
|
||||||
|
export function useSocket() {
|
||||||
|
function connect(profileId: string) {
|
||||||
|
if (socket?.connected) return
|
||||||
|
if (socket) {
|
||||||
|
socket.disconnect()
|
||||||
|
socket = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = _getAccessToken()
|
||||||
|
if (!token) return
|
||||||
|
|
||||||
|
socket = io(`${BASE_URL}/chat`, {
|
||||||
|
path: '/socket.io',
|
||||||
|
transports: ['websocket'],
|
||||||
|
auth: { token, profileId },
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on('connect', () => {
|
||||||
|
connected.value = true
|
||||||
|
})
|
||||||
|
socket.on('disconnect', () => {
|
||||||
|
connected.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function disconnect() {
|
||||||
|
socket?.disconnect()
|
||||||
|
socket = null
|
||||||
|
connected.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function on<T = any>(event: string, handler: (data: T) => void) {
|
||||||
|
socket?.on(event, handler)
|
||||||
|
return () => socket?.off(event, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
function off(event: string, handler: (...args: any[]) => void) {
|
||||||
|
socket?.off(event, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
function emit(event: string, data: any) {
|
||||||
|
socket?.emit(event, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
function joinChat(chatId: string) {
|
||||||
|
socket?.emit('join_chat', { chatId })
|
||||||
|
}
|
||||||
|
|
||||||
|
function leaveChat(chatId: string) {
|
||||||
|
socket?.emit('leave_chat', { chatId })
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendTyping(chatId: string, isTyping: boolean) {
|
||||||
|
socket?.emit('typing', { chatId, isTyping })
|
||||||
|
}
|
||||||
|
|
||||||
|
return { connect, disconnect, on, off, emit, joinChat, leaveChat, sendTyping, connected }
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import type { ChatDto, MessageDto } from '@/api/api'
|
|
||||||
import type { MaybeRefOrGetter } from 'vue'
|
import type { MaybeRefOrGetter } from 'vue'
|
||||||
import { computed, toValue } from 'vue'
|
import type { ChatDto, MessageDto } from '@/api/api'
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||||
import { apiClient } from '@/api/client'
|
import { computed, toValue } from 'vue'
|
||||||
|
import { apiClient, axiosInstance } from '@/api/client'
|
||||||
|
import { useAuth } from '@/composables/useAuth'
|
||||||
import { queryKeys } from './keys'
|
import { queryKeys } from './keys'
|
||||||
|
|
||||||
export interface ChatPartner {
|
export interface ChatPartner {
|
||||||
@@ -11,15 +12,23 @@ export interface ChatPartner {
|
|||||||
avatarUrl?: string
|
avatarUrl?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// The API returns richer chat objects than ChatDto — these extra fields aren't in the generated types
|
|
||||||
export type Chat = ChatDto & {
|
export type Chat = ChatDto & {
|
||||||
partner?: ChatPartner
|
partner?: ChatPartner
|
||||||
lastMessage?: MessageDto
|
lastMessage?: MessageDto
|
||||||
unreadCount?: number
|
unreadCount?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CloseChatPayload {
|
||||||
|
chatId: string
|
||||||
|
type: 'cancel' | 'report'
|
||||||
|
cancelMessage?: string
|
||||||
|
reportReason?: string
|
||||||
|
reportDescription?: string
|
||||||
|
}
|
||||||
|
|
||||||
export function useChatsQuery(profileId: MaybeRefOrGetter<string | undefined>) {
|
export function useChatsQuery(profileId: MaybeRefOrGetter<string | undefined>) {
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
|
const authStore = useAuth()
|
||||||
const pid = () => toValue(profileId) ?? ''
|
const pid = () => toValue(profileId) ?? ''
|
||||||
|
|
||||||
const query = useQuery({
|
const query = useQuery({
|
||||||
@@ -35,9 +44,15 @@ export function useChatsQuery(profileId: MaybeRefOrGetter<string | undefined>) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const closeChat = useMutation({
|
const closeChat = useMutation({
|
||||||
mutationFn: ({ chatId }: { chatId: string }) =>
|
mutationFn: ({ chatId, type, cancelMessage, reportReason, reportDescription }: CloseChatPayload) =>
|
||||||
apiClient.api.chatControllerCloseChat({ chatId, profileId: pid() }),
|
axiosInstance.delete(`/api/v1/chats/${chatId}`, {
|
||||||
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.chats(pid()) }),
|
params: { profileId: pid() },
|
||||||
|
data: { type, cancelMessage, reportReason, reportDescription },
|
||||||
|
}),
|
||||||
|
onSuccess: async () => {
|
||||||
|
await authStore.fetchMe()
|
||||||
|
qc.invalidateQueries({ queryKey: queryKeys.chats(pid()) })
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return { query, openChat, closeChat }
|
return { query, openChat, closeChat }
|
||||||
|
|||||||
@@ -61,6 +61,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { EnrichedMatch } from '@/queries/useMatchesQuery'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import AppButton from '@/components/common/AppButton.vue'
|
import AppButton from '@/components/common/AppButton.vue'
|
||||||
@@ -70,7 +71,6 @@ import { useAuth } from '@/composables/useAuth'
|
|||||||
import { useUi } from '@/composables/useUi'
|
import { useUi } from '@/composables/useUi'
|
||||||
import { useChatsQuery } from '@/queries/useChatsQuery'
|
import { useChatsQuery } from '@/queries/useChatsQuery'
|
||||||
import { useMatchesQuery } from '@/queries/useMatchesQuery'
|
import { useMatchesQuery } from '@/queries/useMatchesQuery'
|
||||||
import type { EnrichedMatch } from '@/queries/useMatchesQuery'
|
|
||||||
|
|
||||||
const authStore = useAuth()
|
const authStore = useAuth()
|
||||||
const uiStore = useUi()
|
const uiStore = useUi()
|
||||||
@@ -79,7 +79,7 @@ const router = useRouter()
|
|||||||
const profileId = computed(() => authStore.activeProfile?.id)
|
const profileId = computed(() => authStore.activeProfile?.id)
|
||||||
|
|
||||||
const { data: matches, isLoading: loading } = useMatchesQuery(profileId)
|
const { data: matches, isLoading: loading } = useMatchesQuery(profileId)
|
||||||
const { openChat: openChatMutation } = useChatsQuery(profileId)
|
const { query: { data: chats }, openChat: openChatMutation } = useChatsQuery(profileId)
|
||||||
|
|
||||||
function calcAge(birthDate: string): number | undefined {
|
function calcAge(birthDate: string): number | undefined {
|
||||||
const birth = new Date(birthDate)
|
const birth = new Date(birthDate)
|
||||||
@@ -93,13 +93,23 @@ function calcAge(birthDate: string): number | undefined {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function openChat(match: EnrichedMatch) {
|
async function openChat(match: EnrichedMatch) {
|
||||||
|
// const existing = (chats.value ?? []).find(
|
||||||
|
// c => c.status === 'active'
|
||||||
|
// && (c.profile1Id === match.partnerProfile.id || c.profile2Id === match.partnerProfile.id),
|
||||||
|
// )
|
||||||
|
// if (existing) {
|
||||||
|
// router.push(`/chats/${existing.id}`)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
try {
|
try {
|
||||||
const chat = await openChatMutation.mutateAsync({ matchId: match.id })
|
const chat = await openChatMutation.mutateAsync({ matchId: match.id })
|
||||||
router.push(`/chats/${chat.id}`)
|
router.push(`/chats/${chat.id}`)
|
||||||
}
|
}
|
||||||
catch (err: unknown) {
|
catch (err: unknown) {
|
||||||
const msg = (err as { response?: { data?: { message?: string } } })
|
const msg = (err as { response?: { data?: { message?: string } } })
|
||||||
?.response?.data?.message ?? 'Не удалось открыть чат'
|
?.response
|
||||||
|
?.data
|
||||||
|
?.message ?? 'Не удалось открыть чат'
|
||||||
uiStore.addToast(msg, 'error')
|
uiStore.addToast(msg, 'error')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,27 @@
|
|||||||
<MediaGallery :profile-id="profile.id" :editable="true" />
|
<MediaGallery :profile-id="profile.id" :editable="true" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Chat history -->
|
||||||
|
<div class="my-profile__section">
|
||||||
|
<h3 class="my-profile__section-title">
|
||||||
|
Общение
|
||||||
|
</h3>
|
||||||
|
<RouterLink to="/chats" class="my-profile__chat-history-link">
|
||||||
|
<span class="my-profile__chat-history-icon">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="18" height="18">
|
||||||
|
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span class="my-profile__chat-history-text">
|
||||||
|
<span class="my-profile__chat-history-label">История чатов</span>
|
||||||
|
<span class="meta my-profile__chat-history-meta">Активные диалоги и прошлые переписки</span>
|
||||||
|
</span>
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="16" height="16" class="my-profile__chat-history-arrow">
|
||||||
|
<path d="M9 18l6-6-6-6" />
|
||||||
|
</svg>
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Danger zone -->
|
<!-- Danger zone -->
|
||||||
<div class="my-profile__section my-profile__danger">
|
<div class="my-profile__section my-profile__danger">
|
||||||
<AppButton variant="danger" size="sm" @click="confirmDelete = true">
|
<AppButton variant="danger" size="sm" @click="confirmDelete = true">
|
||||||
@@ -328,6 +349,52 @@ function calcAge(birthDate: string) {
|
|||||||
letter-spacing: 0.03em;
|
letter-spacing: 0.03em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__chat-history-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: rgba(240, 235, 224, 0.04);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-sm, 4px);
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--color-cream);
|
||||||
|
transition: background var(--transition-fast);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(240, 235, 224, 0.07);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__chat-history-icon {
|
||||||
|
color: var(--color-signal);
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__chat-history-text {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__chat-history-label {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__chat-history-meta {
|
||||||
|
color: var(--color-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__chat-history-arrow {
|
||||||
|
color: var(--color-muted);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
&__danger {
|
&__danger {
|
||||||
padding-top: 16px;
|
padding-top: 16px;
|
||||||
border-top: 1px solid rgba(196, 92, 58, 0.2);
|
border-top: 1px solid rgba(196, 92, 58, 0.2);
|
||||||
|
|||||||
@@ -54,9 +54,9 @@
|
|||||||
|
|
||||||
<!-- Actions (non-own) -->
|
<!-- Actions (non-own) -->
|
||||||
<div v-if="!isOwnProfile" class="profile-detail__actions">
|
<div v-if="!isOwnProfile" class="profile-detail__actions">
|
||||||
<AppButton size="sm" variant="primary" @click="dateOpen = true">
|
<!-- <AppButton size="sm" variant="primary" @click="dateOpen = true"> -->
|
||||||
Встреча
|
<!-- Встреча -->
|
||||||
</AppButton>
|
<!-- </AppButton> -->
|
||||||
<AppButton size="sm" variant="ghost" @click="reportOpen = true">
|
<AppButton size="sm" variant="ghost" @click="reportOpen = true">
|
||||||
Пожаловаться
|
Пожаловаться
|
||||||
</AppButton>
|
</AppButton>
|
||||||
|
|||||||
Reference in New Issue
Block a user