input 메시지를 서버로 보내기 위해서 백엔드에서 먼저 구현해줍니다.
//backend src/graphql/typeDefs
import gql from "graphql-tag";
const typeDefs = gql`
type Message {
id: String
sender: User
body: String
createdAt: Date
}
type Query {
messages(conversationId: String): [Message]
}
type Mutation {
sendMessage(
id: String
senderId: String
conversationId: String
body: String
): Boolean
}
type Subscription {
messageSent(conversationId: String): Message
}
`;
export default typeDefs;
Query는 메시지를 가져오는거고 Mutation은 위 메시지 보내기 부분입니다
subscription은 메시지 구독 정의한 부분을 실시간 통신하기 위해 적용합니다
resolvers로 가겠습니다.
//backend src/graphql/resolvers/message.ts
import { conversationPopulated } from "./conversation";
import { Prisma } from "@prisma/client";
import { GraphQLError } from "graphql";
import { withFilter } from "graphql-subscriptions";
import {
GraphQLContext,
MessagePopulated,
SendMessageArgments,
MessageSentSubscriptionPayload,
} from "../../util/types";
import { userIsConversationPraticipant } from "../../util/functions";
const resolvers = {
Query: {
messages: async function (
_: any,
args: { conversationId: string },
context: GraphQLContext
): Promise<Array<MessagePopulated>> {
const { session, prisma } = context;
const { conversationId } = args;
if (!session?.user) {
throw new GraphQLError("Not authorized");
}
const {
user: { id: userId },
} = session;
//대화가 존재하는지 확인하고 사용자가 참가인지 확인
const conversation = await prisma.conversation.findUnique({
where: {
id: conversationId,
},
include: conversationPopulated,
});
if (!conversation) {
throw new GraphQLError("Conversation Not Found");
}
const allowedToView = userIsConversationPraticipant(
conversation.participants,
userId
);
//대화 참가자에 내가 있는지 확인
if (!allowedToView) {
throw new GraphQLError("Not Authorized");
}
try {
const messages = await prisma.message.findMany({
where: {
conversationId,
},
include: messagePopulated,
orderBy: {
createdAt: "desc",
},
});
return messages;
return [
{ body: "hey dude this is a super mesaage" } as MessagePopulated,
];
} catch (err: any) {
console.log("messages erro", err);
throw new GraphQLError(err.message);
}
},
},
Mutation: {
sendMessage: async function (
_: any,
args: SendMessageArgments,
context: GraphQLContext
): Promise<boolean> {
const { session, prisma, pubsub } = context;
if (!session?.user) {
throw new GraphQLError("Not authorized");
}
const { id: userId } = session.user;
const { id: messageId, senderId, conversationId, body } = args;
if (userId !== senderId) {
throw new GraphQLError("Not authorized");
}
try {
// create new message
const newMessage = await prisma.message.create({
data: {
id: messageId,
senderId,
conversationId,
body,
},
include: messagePopulated,
});
//find ConversationParticipant
const participant = await prisma.conversationParticipant.findFirst({
where: {
userId,
conversationId,
},
});
if (!participant) {
throw new GraphQLError("참여자가 존재하지 않습니다.");
}
//update conversation
const conversation = await prisma.conversation.update({
where: {
id: conversationId,
},
data: {
latestMessageId: newMessage.id,
participants: {
update: {
where: {
id: participant.id,
},
data: {
hasSeenLatestMessage: true,
},
},
updateMany: {
where: {
NOT: {
userId,
},
},
data: {
hasSeenLatestMessage: false,
},
},
},
},
include: conversationPopulated,
});
//오더가 생성 됐어요
pubsub.publish("MESSAGE_SENT", { messageSent: newMessage });
// pubsub.publish("CONVERSATION_UPDATED", {
// conversationUpdated: {
// conversation,
// },
// });
} catch (err) {
console.log("sendMessage err", err);
throw new GraphQLError("ERR sending message");
}
return true;
},
},
Subscription: {
messageSent: {
subscribe: withFilter(
(_: any, __: any, context: GraphQLContext) => {
const { pubsub } = context;
return pubsub.asyncIterator(["MESSAGE_SENT"]); //구독하고 있는 클라이언트에게 오더데이터 알려줄게요
},
(
payload: MessageSentSubscriptionPayload,
args: { conversationId: string },
context: GraphQLContext
) => {
return payload.messageSent.conversationId === args.conversationId;
}
),
},
},
};
export const messagePopulated = Prisma.validator<Prisma.MessageInclude>()({
sender: {
select: {
id: true,
username: true,
},
},
});
export default resolvers;
query부분에서 conversationId로 클릭한 대화 메시지(대화리스트)를 가져오기를 위해 만들어졌습니다.
mutation에서 publish이벤트의 발생을 알리고 Subscribe에 asyncIterator로 이벤트를 감지하요 이를 구독하고 있는 클라이언트에게 데이터를 전송할 수 있는 api를 제공합니다
하지만 예를 들어 제주도대화에 있는 사용자가 메시지를 호출 했을 때 서울대화에 있는 메시지에게 요청이 가면 안 되겠죠? 이를 해결하기 위해 저희는 Subscription withFilter 을 활용했습니다. 말 그대로 특정 이벤트를 구독하고 있는 모든 사용자에게 알림을 주는 것이 아닌 특정 조건을 만족하는 사용자에게만 알림을 주는 것이죠.
Subscribe블로그 참조-> (https://happy8131.tistory.com/121)
다시 프론트로 넘어옵니다
//frontend src/graphql/operations/messages.ts
import { gql } from "@apollo/client";
export const MessageFields = `
id
sender {
id
username
}
body
createdAt
`;
export default {
Query: {
messages: gql`
query Messages($conversationId:String!){
messages(conversationId:$conversationId){
${MessageFields}
}
}
`,
},
Mutation: {
sendMessage: gql`
mutation SendMessage(
$id: String!
$senderId: String!
$conversationId: String!
$body: String!
) {
sendMessage(
id: $id
senderId: $senderId
conversationId: $conversationId
body: $body
)
}
`,
},
Subscription: {
messageSent: gql`
subscription MessageSent($conversationId: String!) {
messageSent(conversationId: $conversationId){
${MessageFields}
}
}
`,
},
};
onSendMessage를 만들어줍니다.
//frontend src/component/Chat/Feed/Messages/Input.tsx
import { useMutation } from "@apollo/client";
import { Box, Input } from "@chakra-ui/react";
import { Session } from "next-auth";
import React, { useState } from "react";
import toast from "react-hot-toast";
import { SendMessageArgments } from "../../../../../../backend/src/util/types";
import MessagesOperations from "../../../../graphql/operations/messages";
import { ObjectID } from "bson";
interface MessageInputProps {
session: Session;
conversationId: string;
}
const MessageInput = ({ session, conversationId }: MessageInputProps) => {
const [messageBody, setMessageBody] = useState("");
const [sendMessage] = useMutation<SendMessageArgments>(
MessagesOperations.Mutation.sendMessage
);
const onSendMessage = async (e: React.FormEvent) => {
e.preventDefault();
try {
const { id: senderId } = session.user;
const newId = new ObjectID().toString();
const newMessage: SendMessageArgments = {
id: newId,
senderId,
conversationId,
body: messageBody,
};
const { data, errors } = await sendMessage({
variables: {
...newMessage,
},
});
if (!data?.sendMessage || errors) {
throw new Error("메세지 보내기 실패");
}
} catch (err: any) {
console.log("onSendMessage Err", err);
toast.error(err.message);
}
};
return (
<Box px={4} py={6} width="100%`">
<form onSubmit={onSendMessage}>
<Input
value={messageBody}
onChange={(e) => setMessageBody(e.target.value)}
placeholder="메시지를 입력해주세요."
size="md"
resize="none"
_focus={{
boxShadow: "none",
border: "1px solid",
borderColor: "whiteAlpha.300",
}}
/>
</form>
</Box>
);
};
export default MessageInput;
useMutation가져와서 sendMessage를 가져옵니다
인자로 id를 가져오는데 moongo와 함께 작업라려면 bson을 가져옵니다 npm i bson 설치해줍니다
해당 메시지 아이디가 되고 senderId는 사용자로부터 오는 id가 됩니다 conversationId는 대화 id를 가져옵니다
body에 iuput에 메시지를 가져옵니다 그리고 이 데이터들을 mutation에 sendMessage로 전달합니다.
//frontend src//components/Chat/Feed/Messages.tsx
import SkeletonLoader from "@/components/common/SkeletonLoader";
import { MessagesData, MessageSubscriptionData } from "@/util/types";
import { useQuery } from "@apollo/client";
import { Flex, Stack, Text } from "@chakra-ui/react";
import { useEffect } from "react";
import toast from "react-hot-toast";
import MessagesOperations from "../../../../graphql/operations/messages";
interface MessagesProps {
userId: string;
conversationId: string;
}
const Messages = ({ userId, conversationId }: MessagesProps) => {
const { data, loading, error, subscribeToMore } = useQuery(
MessagesOperations.Query.messages,
{
variables: {
conversationId,
},
onError: ({ message }) => {
toast.error(message);
},
}
);
if (error) {
return null;
}
const subscribeToMoreMessages = (conversationId: string) => {
subscribeToMore({
document: MessagesOperations.Subscription.messageSent,
variables: {
conversationId,
},
updateQuery: (prev, { subscriptionData }: MessageSubscriptionData) => {
if (!subscriptionData) return prev;
const newMessage = subscriptionData.data.messageSent;
return Object.assign({}, prev, {
messages: [newMessage, ...prev.messages],
});
},
});
};
useEffect(() => {
subscribeToMoreMessages(conversationId);
}, [conversationId]);
console.log("HERE IS MESSAGES DATA", data);
return (
<Flex direction="column" justify="flex-end" overflow="hidden">
{loading && (
<Stack spacing={4} px={4}>
<SkeletonLoader count={4} height="60px" width="320px" />
<span>Loading Messages</span>
</Stack>
)}
{data?.messages && (
<Flex direction="column-reverse" overflowY="scroll" height="100%">
{data.messages.map((message: any) => (
<Text key={message.body}>{message.body}</Text>
))}
</Flex>
)}
</Flex>
);
};
export default Messages;
subscribeToMoreMessages 함수에서 어떤 구독인지 conversationId를 전달 해줘야 합니다.
그러면
실시간 채팅이됩니다.
fireBox웹 브라우저에 안녕하세요라고 엔터쳐보겠습니다
이렇게 바로 실시간으로 업데이트가 됩니다.!
'Project' 카테고리의 다른 글
GraphQL 메시지 대화 삭제 (0) | 2023.09.20 |
---|---|
GraphQL 메시지 읽음으로 표시 (0) | 2023.09.18 |
GraphQL 대화생성 Subscription (0) | 2023.08.29 |
WebSocket/ Subscription (0) | 2023.08.29 |
GraphQL 대화 생성 (0) | 2023.08.28 |