From 9ca2dd3ec9e425b6a8203bd7f03a0ffec7b26f89 Mon Sep 17 00:00:00 2001 From: Nickolashaa Date: Tue, 1 Sep 2026 01:10:15 +0300 Subject: [PATCH 01/10] server:add mutation leave_chat --- .../server/graphql/schema/chats/mutations.py | 21 +++++++++++ .../services/chats/participants/service.py | 35 +++++++++++++++++++ server/src/server/services/chats/service.py | 10 +++++- 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/server/src/server/graphql/schema/chats/mutations.py b/server/src/server/graphql/schema/chats/mutations.py index ab4cc7e..76d7aa2 100644 --- a/server/src/server/graphql/schema/chats/mutations.py +++ b/server/src/server/graphql/schema/chats/mutations.py @@ -154,6 +154,27 @@ async def remove_participant_from_chat( ) await info.context.session.commit() + @strawberry.mutation(permission_classes=[IsAuthenticated, IsChatParticipant]) + async def leave_chat( + self, + info: AuthorizedAppInfo, + chat_id: int, + ) -> Chat | InvalidInputError | ObjectNotFoundError: + try: + instance = await info.context.services.chat_service.get(chat_id) + await info.context.services.chat_participant_service.leave( + chat_id=chat_id, + user_id=info.context.current_user.id, + ) + await info.context.session.commit() + return Chat.from_schema(instance) + except InvalidInput as e: + await info.context.session.rollback() + return InvalidInputError.from_service_exception(e) + except ObjectNotFound as e: + await info.context.session.rollback() + return ObjectNotFoundError.from_service_exception(e) + @strawberry.mutation(permission_classes=[IsAuthenticated, IsChatAdmin]) async def update_chat_participant_permissions( self, diff --git a/server/src/server/services/chats/participants/service.py b/server/src/server/services/chats/participants/service.py index 2c5c092..896489a 100644 --- a/server/src/server/services/chats/participants/service.py +++ b/server/src/server/services/chats/participants/service.py @@ -64,6 +64,41 @@ async def delete( ) await self._session.execute(stmt) + async def leave( + self, + chat_id: int, + user_id: int, + ) -> None: + chat = await self._chat_service.get(chat_id) + if chat.type == ChatType.PRIVATE: + raise InvalidInput("You cannot leave a private chat") + + link = await self.get_or_none(chat_id=chat_id, user_id=user_id) + if link is None: + raise ObjectNotFound( + f"User with id {user_id} not in chat with id {chat_id}" + ) + + await self.delete(chat_id=chat_id, user_id=user_id) + + remaining = await self.get_list(chat_id=chat_id) + if not remaining: + await self._chat_service.delete(chat_id) + return + + if not link.is_admin: + return + + admins_count = await self.count(chat_id=chat_id, is_admin=True) + if admins_count > 0: + return + + await self.update( + chat_id=chat_id, + user_id=min(remaining, key=lambda item: item.id).user_id, + is_admin=True, + ) + @staticmethod def _apply_filters( stmt: Select[tuple[ChatParticipant]], diff --git a/server/src/server/services/chats/service.py b/server/src/server/services/chats/service.py index 5903995..e78e87c 100644 --- a/server/src/server/services/chats/service.py +++ b/server/src/server/services/chats/service.py @@ -1,6 +1,6 @@ from typing import Unpack -from sqlalchemy import Select, func, insert, select, update +from sqlalchemy import Select, delete, func, insert, select, update from sqlalchemy.exc import IntegrityError from ...database.models.chats import Chat @@ -54,6 +54,14 @@ async def update( return ChatResponse.model_validate(res.scalar_one()) + async def delete( + self, + id: int, + ) -> None: + stmt = delete(Chat).where(Chat.id == id) + + await self._session.execute(stmt) + @staticmethod def _apply_filters( stmt: Select[tuple[Chat]], From e1d1f8fd5f066ccaca7c46dfacc98714d65432b0 Mon Sep 17 00:00:00 2001 From: Nickolashaa Date: Tue, 1 Sep 2026 01:10:22 +0300 Subject: [PATCH 02/10] server:schemagen --- server/schema.graphql | 1 + 1 file changed, 1 insertion(+) diff --git a/server/schema.graphql b/server/schema.graphql index 992e645..831ec07 100644 --- a/server/schema.graphql +++ b/server/schema.graphql @@ -148,6 +148,7 @@ type Mutation { updateChat(chatId: Int!, input: ChatUpdateIn!): ChatInvalidInputErrorObjectNotFoundError! addParticipantToChat(input: ChatParticipantIn!): ChatParticipantObjectNotFoundErrorObjectAlreadyExistsErrorInvalidInputError! removeParticipantFromChat(chatId: Int!, userId: Int!): Void + leaveChat(chatId: Int!): ChatInvalidInputErrorObjectNotFoundError! updateChatParticipantPermissions(input: ChatParticipantIn!): ChatParticipantObjectNotFoundError! markChatRead(chatId: Int!): ChatParticipantObjectNotFoundError! createMessage(input: MessageIn!): MessageObjectNotFoundError! From 11a9f845416dd7bc139f7f6f1b4aebd3f9a2c3c5 Mon Sep 17 00:00:00 2001 From: Nickolashaa Date: Tue, 1 Sep 2026 01:10:32 +0300 Subject: [PATCH 03/10] client:codegen --- client/src/graphql/base-types.ts | 5 ++ .../mutations/chats/leave-chat.generated.ts | 72 +++++++++++++++++++ .../graphql/mutations/chats/leave-chat.gql | 14 ++++ 3 files changed, 91 insertions(+) create mode 100644 client/src/graphql/mutations/chats/leave-chat.generated.ts create mode 100644 client/src/graphql/mutations/chats/leave-chat.gql diff --git a/client/src/graphql/base-types.ts b/client/src/graphql/base-types.ts index a12300c..4e02007 100644 --- a/client/src/graphql/base-types.ts +++ b/client/src/graphql/base-types.ts @@ -168,6 +168,7 @@ export type Mutation = { createPrivateChat: ChatInvalidInputErrorObjectNotFoundErrorObjectAlreadyExistsError createPublicChat: ChatInvalidInputErrorObjectNotFoundErrorObjectAlreadyExistsError createSystemNotification: SystemNotification + leaveChat: ChatInvalidInputErrorObjectNotFoundError login: JwTsObjectNotFoundError logout?: Maybe markChatRead: ChatParticipantObjectNotFoundError @@ -204,6 +205,10 @@ export type MutationCreateSystemNotificationArgs = { input: SystemNotificationIn } +export type MutationLeaveChatArgs = { + chatId: Scalars["Int"]["input"] +} + export type MutationLoginArgs = { input: UserCredentialsIn } diff --git a/client/src/graphql/mutations/chats/leave-chat.generated.ts b/client/src/graphql/mutations/chats/leave-chat.generated.ts new file mode 100644 index 0000000..04f45b0 --- /dev/null +++ b/client/src/graphql/mutations/chats/leave-chat.generated.ts @@ -0,0 +1,72 @@ +/** Internal type. DO NOT USE DIRECTLY. */ +type Exact = { [K in keyof T]: T[K] } +/** Internal type. DO NOT USE DIRECTLY. */ +export type Incremental = + T | { [P in keyof T]?: P extends " $fragmentName" | "__typename" ? T[P] : never } +import * as Types from "../../base-types" + +import gql from "graphql-tag" +import * as VueApolloComposable from "@vue/apollo-composable" +import * as VueCompositionApi from "vue" +export type ReactiveFunction = () => TParam +export type LeaveChatMutationVariables = Exact<{ + chatId: number +}> + +export type LeaveChatMutation = { + leaveChat: + | { __typename: "Chat"; id: number } + | { __typename: "InvalidInputError"; message: string } + | { __typename: "ObjectNotFoundError"; message: string } +} + +export const LeaveChatDocument = gql` + mutation LeaveChat($chatId: Int!) { + leaveChat(chatId: $chatId) { + __typename + ... on Chat { + id + } + ... on InvalidInputError { + message + } + ... on ObjectNotFoundError { + message + } + } + } +` + +/** + * __useLeaveChatMutation__ + * + * To run a mutation, you first call `useLeaveChatMutation` within a Vue component and pass it any options that fit your needs. + * When your component renders, `useLeaveChatMutation` returns an object that includes: + * - A mutate function that you can call at any time to execute the mutation + * - Several other properties: https://v4.apollo.vuejs.org/api/use-mutation.html#return + * + * @param options that will be passed into the mutation, supported options are listed on: https://v4.apollo.vuejs.org/guide-composable/mutation.html#options; + * + * @example + * const { mutate, loading, error, onDone } = useLeaveChatMutation({ + * variables: { + * chatId: // value for 'chatId' + * }, + * }); + */ +export function useLeaveChatMutation( + options: + | VueApolloComposable.UseMutationOptions + | ReactiveFunction< + VueApolloComposable.UseMutationOptions + > = {}, +) { + return VueApolloComposable.useMutation( + LeaveChatDocument, + options, + ) +} +export type LeaveChatMutationCompositionFunctionResult = VueApolloComposable.UseMutationReturn< + LeaveChatMutation, + LeaveChatMutationVariables +> diff --git a/client/src/graphql/mutations/chats/leave-chat.gql b/client/src/graphql/mutations/chats/leave-chat.gql new file mode 100644 index 0000000..4465bef --- /dev/null +++ b/client/src/graphql/mutations/chats/leave-chat.gql @@ -0,0 +1,14 @@ +mutation LeaveChat($chatId: Int!) { + leaveChat(chatId: $chatId) { + __typename + ... on Chat { + id + } + ... on InvalidInputError { + message + } + ... on ObjectNotFoundError { + message + } + } +} From 5216442251a4ebc40d49ea75ea234ebc0ecc3fb3 Mon Sep 17 00:00:00 2001 From: Nickolashaa Date: Tue, 1 Sep 2026 01:10:38 +0300 Subject: [PATCH 04/10] client:update store --- client/src/stores/chats.ts | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/client/src/stores/chats.ts b/client/src/stores/chats.ts index 2e97dba..4557310 100644 --- a/client/src/stores/chats.ts +++ b/client/src/stores/chats.ts @@ -7,6 +7,7 @@ import { CreatePublicChatDocument } from "@/graphql/mutations/chats/create-publi import { UpdateChatDocument } from "@/graphql/mutations/chats/update-chat.generated" import { AddParticipantToChatDocument } from "@/graphql/mutations/chats/add-participant-to-chat.generated" import { RemoveParticipantFromChatDocument } from "@/graphql/mutations/chats/remove-participant-from-chat.generated" +import { LeaveChatDocument } from "@/graphql/mutations/chats/leave-chat.generated" import { UpdateChatParticipantPermissionsDocument } from "@/graphql/mutations/chats/update-chat-participant-permissions.generated" import { MarkChatReadDocument } from "@/graphql/mutations/chats/mark-chat-read.generated" import { MeChatsDocument } from "@/graphql/queries/chats/me-chats.generated" @@ -174,6 +175,14 @@ export const useChatStore = defineStore("chats", () => { patchParticipantIn(adminChats, chatId, participant) } + function removeChatIn(list: Ref, total: Ref, chatId: number) { + if (!list.value.some((chat) => chat.id === chatId)) { + return + } + list.value = list.value.filter((chat) => chat.id !== chatId) + total.value = Math.max(0, total.value - 1) + } + function removeParticipantIn(list: Ref, chatId: number, userId: number) { const chat = list.value.find((item) => item.id === chatId) if (chat === undefined) { @@ -241,6 +250,26 @@ export const useChatStore = defineStore("chats", () => { removeParticipantIn(adminChats, chatId, userId) } + async function leaveChat(chatId: number) { + const { data } = await apolloClient.mutate({ + mutation: LeaveChatDocument, + variables: { chatId }, + }) + + const result = data?.leaveChat + + if (result?.__typename !== "Chat") { + throw new Error(result?.message ?? "Failed to leave chat") + } + + removeChatIn(chats, totalCount, chatId) + + const currentUser = authStore.user + if (currentUser !== undefined) { + removeParticipantIn(adminChats, chatId, currentUser.id) + } + } + async function setParticipantPermissions( chatId: number, userId: number, @@ -310,6 +339,7 @@ export const useChatStore = defineStore("chats", () => { updateChatTitle, addParticipant, removeParticipant, + leaveChat, setParticipantPermissions, markChatRead, handleIncomingMessage, From da702a1bc24fcbb49851c019ef87e3ccff85cafc Mon Sep 17 00:00:00 2001 From: Nickolashaa Date: Tue, 1 Sep 2026 01:10:44 +0300 Subject: [PATCH 05/10] client:update ui --- client/src/components/chat-meta-panel.vue | 37 +++++++++++++++++++++++ client/src/views/chats.vue | 9 ++++-- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/client/src/components/chat-meta-panel.vue b/client/src/components/chat-meta-panel.vue index e2b7627..4ec7089 100644 --- a/client/src/components/chat-meta-panel.vue +++ b/client/src/components/chat-meta-panel.vue @@ -26,6 +26,8 @@ const props = withDefaults( }, ) +const emit = defineEmits<{ left: [] }>() + const chatTypeLabels: Record = { [EChatType.Private]: "Личный чат", [EChatType.Public]: "Групповой чат", @@ -115,6 +117,29 @@ async function removeParticipant(participant: ChatParticipantItem) { } } +const canLeave = computed( + () => !props.manage && isGroupChat.value && currentParticipant.value !== null, +) + +const leaving = ref(false) + +async function leaveChat() { + if (!window.confirm(`Выйти из чата «${props.chat.title}»?`)) { + return + } + + leaving.value = true + try { + await chatStore.leaveChat(props.chat.id) + notify.success("Вы вышли из чата") + emit("left") + } catch { + notify.error("Не удалось выйти из чата") + } finally { + leaving.value = false + } +} + const addParticipantsOpen = ref(false) type ParticipantContextMenu = { @@ -286,6 +311,18 @@ onUnmounted(() => window.removeEventListener("keydown", handleEscape)) +
+ +
+ { mobileInfoOpen.value = false }) @@ -52,7 +57,7 @@ watch(activeChatId, () => { @open-info="mobileInfoOpen = true" />
{ > - +
From 3d8ce4a6b7f71b047bef091f7ebc15b30027893a Mon Sep 17 00:00:00 2001 From: Nickolashaa Date: Tue, 1 Sep 2026 01:12:07 +0300 Subject: [PATCH 06/10] server:add mutation delete_chat --- .../server/graphql/schema/chats/mutations.py | 20 ++++++++++++++++++- server/src/server/services/chats/service.py | 13 +++++++----- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/server/src/server/graphql/schema/chats/mutations.py b/server/src/server/graphql/schema/chats/mutations.py index 76d7aa2..fc0c7d4 100644 --- a/server/src/server/graphql/schema/chats/mutations.py +++ b/server/src/server/graphql/schema/chats/mutations.py @@ -4,7 +4,7 @@ from ....services.exceptions import InvalidInput, ObjectAlreadyExists, ObjectNotFound from ...context import AuthorizedAppInfo -from ...permissions.auth import IsAuthenticated +from ...permissions.auth import IsAdmin, IsAuthenticated from ...permissions.chats import IsChatAdmin, IsChatParticipant from ...types.chats import ( Chat, @@ -113,6 +113,24 @@ async def update_chat( await info.context.session.rollback() return ObjectNotFoundError.from_service_exception(e) + @strawberry.mutation(permission_classes=[IsAdmin]) + async def delete_chat( + self, + info: AuthorizedAppInfo, + chat_id: int, + ) -> Chat | InvalidInputError | ObjectNotFoundError: + try: + instance = await info.context.services.chat_service.get(chat_id) + await info.context.services.chat_service.delete(chat_id) + await info.context.session.commit() + return Chat.from_schema(instance) + except InvalidInput as e: + await info.context.session.rollback() + return InvalidInputError.from_service_exception(e) + except ObjectNotFound as e: + await info.context.session.rollback() + return ObjectNotFoundError.from_service_exception(e) + @strawberry.mutation(permission_classes=[IsAuthenticated, IsChatAdmin]) async def add_participant_to_chat( self, diff --git a/server/src/server/services/chats/service.py b/server/src/server/services/chats/service.py index e78e87c..0dd8966 100644 --- a/server/src/server/services/chats/service.py +++ b/server/src/server/services/chats/service.py @@ -1,7 +1,6 @@ from typing import Unpack from sqlalchemy import Select, delete, func, insert, select, update -from sqlalchemy.exc import IntegrityError from ...database.models.chats import Chat from ...database.models.messages import Message @@ -32,12 +31,12 @@ async def get( ) -> ChatResponse: stmt = select(Chat).where(Chat.id == id) - try: - res = await self._session.execute(stmt) - except IntegrityError: + res = await self._session.execute(stmt) + instance = res.scalar_one_or_none() + if instance is None: raise ObjectNotFound(f"Chat with id {id} not found") - return ChatResponse.model_validate(res.scalar_one()) + return ChatResponse.model_validate(instance) async def update( self, @@ -58,6 +57,10 @@ async def delete( self, id: int, ) -> None: + chat = await self.get(id) + if chat.type == ChatType.PRIVATE: + raise InvalidInput("Private chat cannot be deleted") + stmt = delete(Chat).where(Chat.id == id) await self._session.execute(stmt) From be2ad2a2b9e8ec0bec3f2a499905357a83e4588a Mon Sep 17 00:00:00 2001 From: Nickolashaa Date: Tue, 1 Sep 2026 01:12:12 +0300 Subject: [PATCH 07/10] server:schemagen --- server/schema.graphql | 1 + 1 file changed, 1 insertion(+) diff --git a/server/schema.graphql b/server/schema.graphql index 831ec07..ecf3fea 100644 --- a/server/schema.graphql +++ b/server/schema.graphql @@ -146,6 +146,7 @@ type Mutation { createPrivateChat(input: PrivateChatIn!): ChatInvalidInputErrorObjectNotFoundErrorObjectAlreadyExistsError! createPublicChat(input: PublicChatIn!): ChatInvalidInputErrorObjectNotFoundErrorObjectAlreadyExistsError! updateChat(chatId: Int!, input: ChatUpdateIn!): ChatInvalidInputErrorObjectNotFoundError! + deleteChat(chatId: Int!): ChatInvalidInputErrorObjectNotFoundError! addParticipantToChat(input: ChatParticipantIn!): ChatParticipantObjectNotFoundErrorObjectAlreadyExistsErrorInvalidInputError! removeParticipantFromChat(chatId: Int!, userId: Int!): Void leaveChat(chatId: Int!): ChatInvalidInputErrorObjectNotFoundError! From 9cf69d04f350ba36adf8883d37ce074a65f0b1eb Mon Sep 17 00:00:00 2001 From: Nickolashaa Date: Tue, 1 Sep 2026 01:12:24 +0300 Subject: [PATCH 08/10] client:codegen --- client/src/graphql/base-types.ts | 5 ++ .../mutations/chats/delete-chat.generated.ts | 72 +++++++++++++++++++ .../graphql/mutations/chats/delete-chat.gql | 14 ++++ 3 files changed, 91 insertions(+) create mode 100644 client/src/graphql/mutations/chats/delete-chat.generated.ts create mode 100644 client/src/graphql/mutations/chats/delete-chat.gql diff --git a/client/src/graphql/base-types.ts b/client/src/graphql/base-types.ts index 4e02007..33314b8 100644 --- a/client/src/graphql/base-types.ts +++ b/client/src/graphql/base-types.ts @@ -168,6 +168,7 @@ export type Mutation = { createPrivateChat: ChatInvalidInputErrorObjectNotFoundErrorObjectAlreadyExistsError createPublicChat: ChatInvalidInputErrorObjectNotFoundErrorObjectAlreadyExistsError createSystemNotification: SystemNotification + deleteChat: ChatInvalidInputErrorObjectNotFoundError leaveChat: ChatInvalidInputErrorObjectNotFoundError login: JwTsObjectNotFoundError logout?: Maybe @@ -205,6 +206,10 @@ export type MutationCreateSystemNotificationArgs = { input: SystemNotificationIn } +export type MutationDeleteChatArgs = { + chatId: Scalars["Int"]["input"] +} + export type MutationLeaveChatArgs = { chatId: Scalars["Int"]["input"] } diff --git a/client/src/graphql/mutations/chats/delete-chat.generated.ts b/client/src/graphql/mutations/chats/delete-chat.generated.ts new file mode 100644 index 0000000..669fdbc --- /dev/null +++ b/client/src/graphql/mutations/chats/delete-chat.generated.ts @@ -0,0 +1,72 @@ +/** Internal type. DO NOT USE DIRECTLY. */ +type Exact = { [K in keyof T]: T[K] } +/** Internal type. DO NOT USE DIRECTLY. */ +export type Incremental = + T | { [P in keyof T]?: P extends " $fragmentName" | "__typename" ? T[P] : never } +import * as Types from "../../base-types" + +import gql from "graphql-tag" +import * as VueApolloComposable from "@vue/apollo-composable" +import * as VueCompositionApi from "vue" +export type ReactiveFunction = () => TParam +export type DeleteChatMutationVariables = Exact<{ + chatId: number +}> + +export type DeleteChatMutation = { + deleteChat: + | { __typename: "Chat"; id: number } + | { __typename: "InvalidInputError"; message: string } + | { __typename: "ObjectNotFoundError"; message: string } +} + +export const DeleteChatDocument = gql` + mutation DeleteChat($chatId: Int!) { + deleteChat(chatId: $chatId) { + __typename + ... on Chat { + id + } + ... on InvalidInputError { + message + } + ... on ObjectNotFoundError { + message + } + } + } +` + +/** + * __useDeleteChatMutation__ + * + * To run a mutation, you first call `useDeleteChatMutation` within a Vue component and pass it any options that fit your needs. + * When your component renders, `useDeleteChatMutation` returns an object that includes: + * - A mutate function that you can call at any time to execute the mutation + * - Several other properties: https://v4.apollo.vuejs.org/api/use-mutation.html#return + * + * @param options that will be passed into the mutation, supported options are listed on: https://v4.apollo.vuejs.org/guide-composable/mutation.html#options; + * + * @example + * const { mutate, loading, error, onDone } = useDeleteChatMutation({ + * variables: { + * chatId: // value for 'chatId' + * }, + * }); + */ +export function useDeleteChatMutation( + options: + | VueApolloComposable.UseMutationOptions + | ReactiveFunction< + VueApolloComposable.UseMutationOptions + > = {}, +) { + return VueApolloComposable.useMutation( + DeleteChatDocument, + options, + ) +} +export type DeleteChatMutationCompositionFunctionResult = VueApolloComposable.UseMutationReturn< + DeleteChatMutation, + DeleteChatMutationVariables +> diff --git a/client/src/graphql/mutations/chats/delete-chat.gql b/client/src/graphql/mutations/chats/delete-chat.gql new file mode 100644 index 0000000..2ecbc54 --- /dev/null +++ b/client/src/graphql/mutations/chats/delete-chat.gql @@ -0,0 +1,14 @@ +mutation DeleteChat($chatId: Int!) { + deleteChat(chatId: $chatId) { + __typename + ... on Chat { + id + } + ... on InvalidInputError { + message + } + ... on ObjectNotFoundError { + message + } + } +} From 71420af12903a5d96bd915bd068139567859dfb6 Mon Sep 17 00:00:00 2001 From: Nickolashaa Date: Tue, 1 Sep 2026 01:12:42 +0300 Subject: [PATCH 09/10] client:add delete_chat in store --- client/src/stores/chats.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/client/src/stores/chats.ts b/client/src/stores/chats.ts index 4557310..3ef97e8 100644 --- a/client/src/stores/chats.ts +++ b/client/src/stores/chats.ts @@ -8,6 +8,7 @@ import { UpdateChatDocument } from "@/graphql/mutations/chats/update-chat.genera import { AddParticipantToChatDocument } from "@/graphql/mutations/chats/add-participant-to-chat.generated" import { RemoveParticipantFromChatDocument } from "@/graphql/mutations/chats/remove-participant-from-chat.generated" import { LeaveChatDocument } from "@/graphql/mutations/chats/leave-chat.generated" +import { DeleteChatDocument } from "@/graphql/mutations/chats/delete-chat.generated" import { UpdateChatParticipantPermissionsDocument } from "@/graphql/mutations/chats/update-chat-participant-permissions.generated" import { MarkChatReadDocument } from "@/graphql/mutations/chats/mark-chat-read.generated" import { MeChatsDocument } from "@/graphql/queries/chats/me-chats.generated" @@ -270,6 +271,22 @@ export const useChatStore = defineStore("chats", () => { } } + async function deleteChat(chatId: number) { + const { data } = await apolloClient.mutate({ + mutation: DeleteChatDocument, + variables: { chatId }, + }) + + const result = data?.deleteChat + + if (result?.__typename !== "Chat") { + throw new Error(result?.message ?? "Failed to delete chat") + } + + removeChatIn(chats, totalCount, chatId) + removeChatIn(adminChats, adminTotalCount, chatId) + } + async function setParticipantPermissions( chatId: number, userId: number, @@ -340,6 +357,7 @@ export const useChatStore = defineStore("chats", () => { addParticipant, removeParticipant, leaveChat, + deleteChat, setParticipantPermissions, markChatRead, handleIncomingMessage, From 7085b02054f0b84ab51c57c40d359bf3c264fd1e Mon Sep 17 00:00:00 2001 From: Nickolashaa Date: Tue, 1 Sep 2026 01:12:51 +0300 Subject: [PATCH 10/10] client:update ui --- client/src/components/chat-meta-panel.vue | 37 +++++++++++++++++++++-- client/src/views/admin-chats.vue | 8 ++++- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/client/src/components/chat-meta-panel.vue b/client/src/components/chat-meta-panel.vue index 4ec7089..f83830e 100644 --- a/client/src/components/chat-meta-panel.vue +++ b/client/src/components/chat-meta-panel.vue @@ -26,7 +26,7 @@ const props = withDefaults( }, ) -const emit = defineEmits<{ left: [] }>() +const emit = defineEmits<{ left: []; deleted: [] }>() const chatTypeLabels: Record = { [EChatType.Private]: "Личный чат", @@ -140,6 +140,27 @@ async function leaveChat() { } } +const canDelete = computed(() => props.manage && isGroupChat.value) + +const deleting = ref(false) + +async function deleteChat() { + if (!window.confirm(`Удалить чат «${props.chat.title}» вместе со всеми сообщениями?`)) { + return + } + + deleting.value = true + try { + await chatStore.deleteChat(props.chat.id) + notify.success("Чат удалён") + emit("deleted") + } catch { + notify.error("Не удалось удалить чат") + } finally { + deleting.value = false + } +} + const addParticipantsOpen = ref(false) type ParticipantContextMenu = { @@ -311,8 +332,9 @@ onUnmounted(() => window.removeEventListener("keydown", handleEscape)) -
+
+ +
Назад к списку - +