Open Mkurowski03 opened 1 month ago
To implement the peer-to-peer chat functionality, we need to update various parts of the application, including state management, GraphQL queries and mutations, and the user interface. The solution involves:
TradingHub
component and updating the chat message component to support additional features like timestamps and message status.store/store.ts
Add atoms to manage chat messages, the current chat interlocutor, and loading/error states.
import { TradingHubStateType } from '@/components/Market/TradingHub/TradingHub';
import { EnrichedMarketType } from '@/types/marketTypes';
import { atom } from 'jotai';
/* MARKETS ATOM */
export const marketsAtom = atom<EnrichedMarketType[]>([]);
/* CHOSEN MARKET ATOM */
export const chosenMarketAtom = atom<EnrichedMarketType | undefined>(undefined);
/* TRADING HUB */
export const tradingHubStateAtom = atom<TradingHubStateType>('chart');
export const tradingHubPositionsCountAtom = atom<number>(0);
export const tradingHubOrdersCountAtom = atom<number>(0);
/* CHAT */
/* Atom to store chat messages */
export const chatMessagesAtom = atom<{ [key: string]: string[] }>({});
/* Atom to manage the current chat interlocutor */
export const chosenInterlocutorAtom = atom<string | null>(null);
/* Atom to manage loading state for chat operations */
export const chatLoadingAtom = atom<boolean>(false);
/* Atom to manage error state for chat operations */
export const chatErrorAtom = atom<string | null>(null);
requests/queries.ts
Add queries to fetch chat messages and mutations to send messages.
import { gql } from '@apollo/client';
/* Query for fetching chat messages between two users */
export const GET_CHAT_MESSAGES_QUERY = gql`
query getChatMessages($userId1: String!, $userId2: String!) {
chatMessages(
where: {
OR: [
{ sender_eq: $userId1, receiver_eq: $userId2 },
{ sender_eq: $userId2, receiver_eq: $userId1 }
]
}
orderBy: timestamp_ASC
) {
id
sender
receiver
message
timestamp
}
}
`;
/* Mutation for sending a chat message */
export const SEND_CHAT_MESSAGE_MUTATION = gql`
mutation sendChatMessage($sender: String!, $receiver: String!, $message: String!) {
createChatMessage(data: { sender: $sender, receiver: $receiver, message: $message }) {
id
sender
receiver
message
timestamp
}
}
`;
requests/graphql.ts
Ensure the new queries and mutations are accessible through the Apollo Client.
import { ApolloClient, HttpLink, InMemoryCache, split, gql } from '@apollo/client';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { httpEndpoint, wsEndpoint } from './endpoints';
import { createClient } from 'graphql-ws';
import { getMainDefinition } from '@apollo/client/utilities';
// Define GraphQL queries and mutations
export const FETCH_CHAT_MESSAGES = gql`
query FetchChatMessages($userId1: ID!, $userId2: ID!) {
fetchChatMessages(userId1: $userId1, userId2: $userId2) {
id
senderId
receiverId
message
timestamp
}
}
`;
export const SEND_CHAT_MESSAGE = gql`
mutation SendChatMessage($senderId: ID!, $receiverId: ID!, $message: String!) {
sendChatMessage(senderId: $senderId, receiverId: $receiverId, message: $message) {
id
senderId
receiverId
message
timestamp
}
}
`;
const httpLink = new HttpLink({
uri: httpEndpoint,
});
const wsLink = new GraphQLWsLink(
createClient({
url: wsEndpoint,
})
);
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink
);
export const apolloClient = new ApolloClient({
link: splitLink,
cache: new InMemoryCache(),
connectToDevTools: true,
});
TradingHub.tsx
Ensure the chat interface is accessible to users who have position pairs.
import { useEffect, useState } from 'react';
import { useAtom } from 'jotai';
import { useAccount } from 'wagmi';
import { TradingHubTab } from './TradingHubTab';
import { TradingHubContentContainer } from './TradingHubContentContainer';
import { AggregatedPositionsCheckbox } from './TradingHubPositions/AggregatedPositionsCheckbox';
import { tradingHubStateAtom } from '@/store/store';
import { TradingHubFooter } from './TradingHubFooter';
import { selectedMarketIdAtom } from '../Market';
import { ChartFeedResponse } from '@/types/chartTypes';
import { useQuery } from '@apollo/client';
import { CHART_FEED_QUERY } from '@/requests/queries';
import { UTCTimestamp } from 'lightweight-charts';
import { ChatContainer } from './Chat/ChatContainer';
import { TradingHubChart } from './TradingHubChart/TradingHubChart';
const tabs = ['chart', 'positions', 'orders', 'history'] as const;
export type TradingHubStateType = (typeof tabs)[number];
export const TradingHub = () => {
const { address } = useAccount();
const [tradingHubState] = useAtom(tradingHubStateAtom);
const [isAggregated, setIsAggregated] = useState<boolean>(true);
const toggleIsAggregated = () => {
setIsAggregated(!isAggregated);
};
/* Chart logic */
const [selectedMarketId] = useAtom(selectedMarketIdAtom);
const {
data: chartRes,
error,
loading,
} = useQuery<ChartFeedResponse>(CHART_FEED_QUERY, {
pollInterval: 5000,
variables: { marketId: selectedMarketId },
});
const [chartData, setChartData] = useState<
{ time: UTCTimestamp; value: number }[]
>([]);
useEffect(() => {
if (chartRes?.positions) {
const formattedData = chartRes.positions.map((item) => ({
time: (new Date(item.timestamp).getTime() / 1000) as UTCTimestamp,
value: Number(item.createPrice),
}));
setChartData(formattedData);
}
}, [chartRes]);
/* */
return (
<div className='h-[calc(100vh-166px)] lg:flex-1 lg:h-full border-[#444650] border rounded-[10px] flex flex-col bg-[#191B24]'>
<div className='flex-grow'>
<div className='flex items-center justify-between px-2.5 pt-3 pb-2.5'>
<div className='flex gap-1'>
{tabs.map((tab, key) => (
<TradingHubTab key={key} value={tab} />
))}
</div>
{tradingHubState === 'positions' && (
<div className='hidden md:block'>
<AggregatedPositionsCheckbox
setIsAggregated={toggleIsAggregated}
isAggregated={isAggregated}
/>
</div>
)}
</div>
{address && <TradingHubContentContainer isAggregated={isAggregated} />}
{tradingHubState === 'chart' && (
<div className='w-full no-scrollbar'>
<TradingHubChart data={chartData} />
</div>
)}
</div>
{/* Integrate ChatContainer */}
{address && tradingHubState === 'positions' && (
<div className='chat-container'>
<ChatContainer />
</div>
)}
<TradingHubFooter />
</div>
);
};
ChatMessage.tsx
to Support Additional FeaturesEnhance the ChatMessage
component to include timestamps, message status, and user avatars.
import React from 'react';
export interface ChatMessageProps {
author: MessageAuthorType;
value: string;
timestamp: string; // New prop for timestamp
status?: MessageStatusType; // New optional prop for message status
avatarUrl?: string; // New optional prop for user avatar
}
export type MessageAuthorType = 'user' | 'interlocutor';
export type MessageStatusType = 'sent' | 'delivered' | 'read';
export const ChatMessage = ({ author, value, timestamp, status, avatarUrl }: ChatMessageProps) => {
return (
<div className={`flex w-full ${author === 'user' ? 'justify-end' : 'justify-start'} mb-[16px]`}>
{author === 'interlocutor' && avatarUrl && (
<img src={avatarUrl} alt="avatar" className="w-8 h-8 rounded-full mr-2" />
)}
<div className="flex flex-col max-w-[50%]">
<div
className={`break-normal px-[16px] py-[8px]
${author === 'user' ? 'rounded-bl-2xl rounded-tl-2xl rounded-tr-2xl bg-[#87DAA4]' : 'rounded-br-2xl rounded-tr-2xl rounded-tl-2xl bg-[#23252E]'}
`}
>
<p className={`break-words text-xs ${author === 'user' ? 'text-black' : 'text-tetriary'}`}>
{value}
</p>
</div>
<div className="flex justify-between items-center mt-1 text-xs text-gray-500">
<span>{timestamp}</span>
{status && author === 'user' && (
<span className="ml-2">
{status === 'sent' && '✓'}
{status === 'delivered' && '✓✓'}
{status === 'read' && '✓✓✓'}
</span>
)}
</div>
</div>
{author === 'user' && avatarUrl && (
<img src={avatarUrl} alt="avatar" className="w-8 h-8 rounded-full ml-2" />
)}
</div>
);
};
ChatContainer.tsx
to Handle Peer-to-Peer ChatFetch and display chat messages between users with position pairs and allow users to send new messages.
import React, { useEffect, useRef, useState } from 'react';
import { IoSend } from 'react-icons/io5';
import { ChatMessage, ChatMessageProps } from './ChatMessage';
import { useAccount } from 'wagmi';
import { truncateAddress } from '@/utils/truncateAddress';
import { FaSearch } from 'react-icons/fa';
import { useAtom } from 'jotai';
import { chosenInterlocutorAtom } from '@/store/store';
import { useQuery, useMutation } from '@apollo/client';
import { FETCH_MESSAGES, SEND_MESSAGE } from '@/requests/queries';
export const ChatContainer = () => {
const [messages, setMessages] = useState<ChatMessageProps[]>([]);
const [inputVal, setInputVal] = useState<string>('');
const [chosenInterlocutor, setChosenInterlocutor] = useAtom(chosenInterlocutorAtom);
const messagesEndRef = useRef<HTMLDivElement>(null);
const { address } = useAccount();
// Fetch messages
const { data, loading, error, refetch } = useQuery(FETCH_MESSAGES, {
variables: { user1: address, user2: chosenInterlocutor },
skip: !chosenInterlocutor,
});
useEffect(() => {
if (data && data.messages) {
setMessages(data.messages);
}
}, [data]);
// Send message mutation
const [sendMessage] = useMutation(SEND_MESSAGE);
const handleSendMessage = async () => {
if (inputVal.trim() !== '') {
const message: ChatMessageProps = { author: 'user', value: inputVal };
const newArr = [...messages, message];
setMessages(newArr);
setInputVal('');
try {
await sendMessage({
variables: {
from: address,
to: chosenInterlocutor,
message: inputVal,
},
});
refetch();
} catch (error) {
console.error('Error sending message:', error);
}
}
};
const handleKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
handleSendMessage();
}
};
return (
<div className='w-full h-full flex pt-4' style={{ height: 'calc(100vh - 229px)' }}>
<div className='w-[200px] h-full border-r border-[#444650] border-t'>
<div className='w-full px-2 my-4'>
<div className='w-full bg-[#23252E] h-[32px] rounded-[100px] flex justify-between items-center mb-3'>
<input
placeholder='Search'
type='text'
className='w-[85%] outline-none bg-[#23252E] h-full rounded-[100px] pl-3 text-xs text-tetriary'
/>
<div className='pr-3 text-tetriary text-sm'>
<FaSearch />
</div>
</div>
</div>
<div className='mb-3'>
<p className='text-[10px] text-tetriary px-2'>All messages</p>
</div>
{/* Add logic to list all chat interlocutors */}
</div>
<div className='flex-grow h-full border-t border-[#444650] flex flex-col justify-between'>
<div className='flex flex-col justify-between h-full'>
<div className='h-[56px] border-b border-[#444650] flex items-center justify-between px-3'>
<div className='flex items-center gap-1.5'>
<div className='h-4 w-4 rounded-full bg-white'></div>
<p className='text-tetriary text-sm'>
{truncateAddress(chosenInterlocutor)}
</p>
</div>
<div>
<p className='text-[10px] text-tetriary'>
PnL with Opponent:{' '}
<span className='font-bold text-[#87DAA4]'>
+400.50 $DOLLARS
</span>
</p>
</div>
</div>
<div className='flex-1 overflow-auto pb-[16px] no-scrollbar'>
<div className='flex flex-col p-3 first:mt-2'>
{messages.map((message, key) => (
<ChatMessage
author={message.author}
key={key}
value={message.value}
/>
))}
</div>
</div>
<div className='w-full px-3'>
<div className='w-full bg-[#23252E] h-[42px] rounded-[100px] flex justify-between items-center mb-3'>
<input
onChange={(e) => setInputVal(e.target.value)}
type='text'
value={inputVal}
onKeyPress={handleKeyPress}
placeholder='Your message here'
className='w-[85%] outline-none bg-[#23252E] h-full rounded-[100px] pl-3 text-xs text-tetriary'
/>
<button
className='pr-3 text-tetriary'
onClick={handleSendMessage}
>
<IoSend />
</button>
</div>
</div>
</div>
</div>
</div>
);
};
Click here to create a Pull Request with the proposed solution
Files used for this task:
{value}
All messages
{truncateAddress(chosenInterlocutor)}
PnL with Opponent:{' '} +400.50 $DOLLARS
Is your feature request related to a problem? Please describe. I want to add feature that will allow users to chat peer to peer whenever they have position pair.