Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions client/src/components/chat-meta-panel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ const props = withDefaults(
},
)

const emit = defineEmits<{ left: []; deleted: [] }>()

const chatTypeLabels: Record<EChatType, string> = {
[EChatType.Private]: "Личный чат",
[EChatType.Public]: "Групповой чат",
Expand Down Expand Up @@ -115,6 +117,50 @@ 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 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 = {
Expand Down Expand Up @@ -286,6 +332,30 @@ onUnmounted(() => window.removeEventListener("keydown", handleEscape))
</span>
</div>

<div v-if="canLeave || canDelete" class="mt-auto border-t border-second/15 px-4 py-4 lg:px-6">
<button
v-if="canLeave"
type="button"
class="flex w-full cursor-pointer items-center justify-center gap-2 rounded-input border-[1.5px] border-red-500/30 px-4 py-2.5 text-sm font-medium text-red-600 transition-colors duration-150 hover:bg-red-500/10 disabled:cursor-not-allowed disabled:opacity-60 dark:text-red-400"
:disabled="leaving"
@click="leaveChat"
>
<NavIcon name="logout" :size="16" />
Выйти из чата
</button>

<button
v-if="canDelete"
type="button"
class="flex w-full cursor-pointer items-center justify-center gap-2 rounded-input border-[1.5px] border-red-500/30 px-4 py-2.5 text-sm font-medium text-red-600 transition-colors duration-150 hover:bg-red-500/10 disabled:cursor-not-allowed disabled:opacity-60 dark:text-red-400"
:disabled="deleting"
@click="deleteChat"
>
<NavIcon name="delete" :size="16" />
Удалить чат
</button>
</div>

<AddParticipantsDialog
:open="addParticipantsOpen"
:chat-id="chat.id"
Expand Down
10 changes: 10 additions & 0 deletions client/src/graphql/base-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ export type Mutation = {
createPrivateChat: ChatInvalidInputErrorObjectNotFoundErrorObjectAlreadyExistsError
createPublicChat: ChatInvalidInputErrorObjectNotFoundErrorObjectAlreadyExistsError
createSystemNotification: SystemNotification
deleteChat: ChatInvalidInputErrorObjectNotFoundError
leaveChat: ChatInvalidInputErrorObjectNotFoundError
login: JwTsObjectNotFoundError
logout?: Maybe<Scalars["Void"]["output"]>
markChatRead: ChatParticipantObjectNotFoundError
Expand Down Expand Up @@ -204,6 +206,14 @@ export type MutationCreateSystemNotificationArgs = {
input: SystemNotificationIn
}

export type MutationDeleteChatArgs = {
chatId: Scalars["Int"]["input"]
}

export type MutationLeaveChatArgs = {
chatId: Scalars["Int"]["input"]
}

export type MutationLoginArgs = {
input: UserCredentialsIn
}
Expand Down
72 changes: 72 additions & 0 deletions client/src/graphql/mutations/chats/delete-chat.generated.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/** Internal type. DO NOT USE DIRECTLY. */
type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] }
/** Internal type. DO NOT USE DIRECTLY. */
export type Incremental<T> =
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> = () => 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<DeleteChatMutation, DeleteChatMutationVariables>
| ReactiveFunction<
VueApolloComposable.UseMutationOptions<DeleteChatMutation, DeleteChatMutationVariables>
> = {},
) {
return VueApolloComposable.useMutation<DeleteChatMutation, DeleteChatMutationVariables>(
DeleteChatDocument,
options,
)
}
export type DeleteChatMutationCompositionFunctionResult = VueApolloComposable.UseMutationReturn<
DeleteChatMutation,
DeleteChatMutationVariables
>
14 changes: 14 additions & 0 deletions client/src/graphql/mutations/chats/delete-chat.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
mutation DeleteChat($chatId: Int!) {
deleteChat(chatId: $chatId) {
__typename
... on Chat {
id
}
... on InvalidInputError {
message
}
... on ObjectNotFoundError {
message
}
}
}
72 changes: 72 additions & 0 deletions client/src/graphql/mutations/chats/leave-chat.generated.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/** Internal type. DO NOT USE DIRECTLY. */
type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] }
/** Internal type. DO NOT USE DIRECTLY. */
export type Incremental<T> =
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> = () => 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<LeaveChatMutation, LeaveChatMutationVariables>
| ReactiveFunction<
VueApolloComposable.UseMutationOptions<LeaveChatMutation, LeaveChatMutationVariables>
> = {},
) {
return VueApolloComposable.useMutation<LeaveChatMutation, LeaveChatMutationVariables>(
LeaveChatDocument,
options,
)
}
export type LeaveChatMutationCompositionFunctionResult = VueApolloComposable.UseMutationReturn<
LeaveChatMutation,
LeaveChatMutationVariables
>
14 changes: 14 additions & 0 deletions client/src/graphql/mutations/chats/leave-chat.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
mutation LeaveChat($chatId: Int!) {
leaveChat(chatId: $chatId) {
__typename
... on Chat {
id
}
... on InvalidInputError {
message
}
... on ObjectNotFoundError {
message
}
}
}
48 changes: 48 additions & 0 deletions client/src/stores/chats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ 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 { 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"
Expand Down Expand Up @@ -174,6 +176,14 @@ export const useChatStore = defineStore("chats", () => {
patchParticipantIn(adminChats, chatId, participant)
}

function removeChatIn(list: Ref<ChatSummary[]>, total: Ref<number>, 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<ChatSummary[]>, chatId: number, userId: number) {
const chat = list.value.find((item) => item.id === chatId)
if (chat === undefined) {
Expand Down Expand Up @@ -241,6 +251,42 @@ 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 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,
Expand Down Expand Up @@ -310,6 +356,8 @@ export const useChatStore = defineStore("chats", () => {
updateChatTitle,
addParticipant,
removeParticipant,
leaveChat,
deleteChat,
setParticipantPermissions,
markChatRead,
handleIncomingMessage,
Expand Down
8 changes: 7 additions & 1 deletion client/src/views/admin-chats.vue
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,13 @@ function selectChat(chatId: number) {
<NavIcon name="arrow-right" :size="16" class="rotate-180" />
Назад к списку
</button>
<ChatMetaPanel :key="activeChat.id" :chat="activeChat" manage variant="page" />
<ChatMetaPanel
:key="activeChat.id"
:chat="activeChat"
manage
variant="page"
@deleted="router.push('/admin/chats')"
/>
</div>
</template>
<div
Expand Down
Loading