तो, हाल ही में मेरे पास एक परियोजना थी जहां मुझे एक चैट सुविधा की आवश्यकता थी. मेरा पहला विचार यह था कि क्या मैं केवल जिवो या लाइवचैट जैसे मौजूदा उपकरण को एकीकृत करना चाहता था, लेकिन मैं कुछ ऐसा करने के लिए तीसरे पक्ष के उत्पादों पर भरोसा नहीं करना चाहता था जो सीधे मेरे व्यवस्थापक पैनल में बनाया जा सकता था। इस पोस्ट में, मैं इसे कैसे बनाया गया: वास्तुकला, सॉकेट और राज्य के लिए संदर्भ, और यूआई घटक जो इसे एक साथ जोड़ते हैं। क्यों ? महाद्वीप महाद्वीप फ़ाइल-आधारित राउटिंग, होक्स, और लचीले घटक के साथ, यह आपको अवरुद्ध नहीं करता है - यह आपको अनुकूलित सुविधाओं को लागू करने के लिए जगह देता है। महाद्वीप चैट आर्किटेक्ट यहां मैं चीजों को कैसे संरचित करता हूं: Core components ChatPage – मुख्य चैट पृष्ठ ChatSidebar – पूर्वावलोकन के साथ वार्तालाप सूची ChatPanel – चुने हुए चैट को बनाता है MessageFeed – संदेशों का थ्रेड MessageInput – फ़ाइल upload के साथ इनपुट Context providers SocketContext – WebSocket कनेक्शन प्रबंधन ChatContext – संवाद और संदेश की स्थिति का प्रबंधन करता है मुख्य चैट पेज एडमिरल के मार्गदर्शन के साथ, एक नई पृष्ठ स्थापित करना सरल था। // pages/chat/index.tsx import ChatPage from '@/src/crud/chat' export default ChatPage यह पृष्ठ उपलब्ध कराने के लिए पर्याप्त था . /chat मुख्य कार्य में प्रवेश किया गया : src/crud/chat/index.tsx // src/crud/chat/index.tsx import React from 'react' import { Card } from '@devfamily/admiral' import { usePermissions, usePermissionsRedirect } from '@devfamily/admiral' import { SocketProvider } from './contexts/SocketContext' import { ChatProvider } from './contexts/ChatContext' import ChatSidebar from './components/ChatSidebar' import ChatPanel from './components/ChatPanel' import styles from './Chat.module.css' export default function ChatPage() { const { permissions, loaded, isAdmin } = usePermissions() const identityPermissions = permissions?.chat?.chat usePermissionsRedirect({ identityPermissions, isAdmin, loaded }) return ( <SocketProvider> <ChatProvider> <Card className={styles.page}> <PageTitle title="Corporate chat" /> <div className={styles.chat}> <ChatSidebar /> <ChatPanel /> </div> </Card> </ChatProvider> </SocketProvider> ) } यहाँ, मैंने पृष्ठ को कवर किया और , और अनुमतिओं और redirects के लिए Admiral के होक्स का उपयोग किया। SocketProvider ChatProvider SocketContext के साथ WebSocket कनेक्शन का प्रबंधन वास्तविक समय चैट के लिए, मैंने सेंटीफ्यूज चुना. मैं एक जगह में सभी कनेक्शन तर्क चाहते थे, इसलिए मैंने बनाया : SocketContext // src/crud/chat/SocketContext.tsx import React from 'react' import { Centrifuge } from 'centrifuge' import { createContext, ReactNode, useContext, useEffect, useRef, useState } from 'react' import { useGetIdentity } from '@devfamily/admiral' const SocketContext = createContext(null) export const SocketProvider = ({ children }: { children: ReactNode }) => { const { identity: user } = useGetIdentity() const [lastMessage, setLastMessage] = useState(null) const centrifugeRef = useRef(null) const subscribedRef = useRef(false) useEffect(() => { if (!user?.ws_token) return const WS_URL = import.meta.env.VITE_WS_URL if (!WS_URL) { console.error('❌ Missing VITE_WS_URL in env') return } const centrifuge = new Centrifuge(WS_URL, { token: user.ws_token, // Initializing the WebSocket connection with a token }) centrifugeRef.current = centrifuge centrifugeRef.current.connect() // Subscribing to the chat channel const sub = centrifugeRef.current.newSubscription(`admin_chat`) sub.on('publication', function (ctx: any) { setLastMessage(ctx.data); }).subscribe() // Cleaning up on component unmount return () => { subscribedRef.current = false centrifuge.disconnect() } }, [user?.ws_token]) return ( <SocketContext.Provider value={{ lastMessage, centrifuge: centrifugeRef.current }}> {children} </SocketContext.Provider> ) } export const useSocket = () => { const ctx = useContext(SocketContext) if (!ctx) throw new Error('useSocket must be used within SocketProvider') return ctx } इस संदर्भ में कनेक्शन सेटिंग, सदस्यता और सफाई को संबोधित किया गया था। . useSocket() चैट संदर्भ के साथ चैट राज्य का प्रबंधन इसके बाद, मुझे संवादों को उठाने, संदेशों को लोड करने, नए संदेश भेजने और WebSocket अद्यतनों पर प्रतिक्रिया करने की आवश्यकता थी। : ChatContext // src/crud/chat/ChatContext.tsx import React, { useRef } from "react"; import { createContext, useContext, useEffect, useState, useRef, useCallback, } from "react"; import { useSocket } from "./SocketContext"; import { useUrlState } from "@devfamily/admiral"; import api from "../api"; const ChatContext = createContext(null); export const ChatProvider = ({ children }) => { const { lastMessage } = useSocket(); const [dialogs, setDialogs] = useState([]); const [messages, setMessages] = useState([]); const [selectedDialog, setSelectedDialog] = useState(null); const [urlState] = useUrlState(); const { client_id } = urlState; const fetchDialogs = useCallback(async () => { const res = await api.dialogs(); setDialogs(res.data || []); }, []); const fetchMessages = useCallback(async (id) => { const res = await api.messages(id); setMessages(res.data || []); }, []); useEffect(() => { fetchMessages(client_id); }, [fetchMessages, client_id]); useEffect(() => { fetchDialogs(); }, [fetchDialogs]); useEffect(() => { if (!lastMessage) return; fetchDialogs(); setMessages((prev) => [...prev, lastMessage.data]); }, [lastMessage]); const sendMessage = useCallback( async (value, onSuccess, onError) => { try { const res = await api.send(value); if (res?.data) setMessages((prev) => [...prev, res.data]); fetchDialogs(); onSuccess(); } catch (err) { onError(err); } }, [messages] ); // Within this context, you can extend the logic to: // – Mark messages as read (api.read()) // – Group messages by date, and more. return ( <ChatContext.Provider value={{ dialogs, messages: groupMessagesByDate(messages), selectedDialog, setSelectedDialog, sendMessage, }} > {children} </ChatContext.Provider> ); }; export const useChat = () => { const ctx = useContext(ChatContext); if (!ctx) throw new Error("useChat must be used within ChatProvider"); return ctx; }; यह सब कुछ - ढूंढना, भंडारण, अद्यतन करना - एक जगह में रखता था। API ग्राहक उदाहरण मैंने अनुरोधों के लिए एक छोटा एपीआई क्लाइंट जोड़ा: // src/crud/chat/api.ts import _ from '../../config/request' import { apiUrl } from '@/src/config/api' const api = { dialogs: () => _.get(`${apiUrl}/chat/dialogs`)(), messages: (id) => _.get(`${apiUrl}/chat/messages/${id}`)(), send: (data) => _.postFD(`${apiUrl}/chat/send`)({ data }), read: (data) => _.post(`${apiUrl}/chat/read`)({ data }), } export default api यूआई घटक: Sidebar + Panel + Input इसके बाद मैं सीबीआई के पास चली गई। चैट // src/crud/chat/components/ChatSidebar.tsx import React from "react"; import styles from "./ChatSidebar.module.scss"; import ChatSidebarItem from "../ChatSidebarItem/ChatSidebarItem"; import { useChat } from "../../model/ChatContext"; function ChatSidebar({}) { const { dialogs } = useChat(); if (!dialogs.length) { return ( <div className={styles.empty}> <span>No active активных dialogs</span> </div> ); } return <div className={styles.list}> {dialogs.map((item) => ( <ChatSidebarItem key={item.id} data={item} /> ))} </div> } export default ChatSidebar; चैट // src/crud/chat/components/ChatSidebarItem.tsx import React from "react"; import { Badge } from '@devfamily/admiral' import dayjs from "dayjs"; import { BsCheck2, BsCheck2All } from "react-icons/bs"; import styles from "./ChatSidebarItem.module.scss"; function ChatSidebarItem({ data }) { const { client_name, client_id, last_message, last_message_ } = data; const [urlState, setUrlState] = useUrlState(); const { client_id } = urlState; const { setSelectedDialog } = useChat(); const onSelectDialog = useCallback(() => { setUrlState({ client_id: client.id }); setSelectedDialog(data); }, [order.id]); return ( <div className={`${styles.item} ${isSelected ? styles.active : ""}`} onClick={onSelectDialog} role="button" > <div className={styles.avatar}>{client_name.charAt(0).toUpperCase()}</div> <div className={styles.content}> <div className={styles.header}> <span className={styles.name}>{client_name}</span> <span className={styles.time}> {dayjs(last_message_).format("HH:mm")} {message.is_read ? ( <BsCheck2All size="16px" /> ) : ( <BsCheck2 size="16px" /> )} </span> </div> <span className={styles.preview}>{last_message.text}</span> {unread_count > 0 && ( <Badge>{unread_count}</Badge> )} </div> </div> ); } export default ChatSidebarItem; चैट // src/crud/chat/components/ChatPanel.tsx import React from "react"; import { Card } from '@devfamily/admiral'; import { useChat } from "../../contexts/ChatContext"; import MessageFeed from "../MessageFeed"; import MessageInput from "../MessageInput"; import styles from "./ChatPanel.module.scss"; function ChatPanel() { const { selectedDialog } = useChat(); if (!selectedDialog) { return ( <Card className={styles.emptyPanel}> <div className={styles.emptyState}> <h3>Choose the dialog</h3> <p>Choose the dialog from the list to start conversation</p> </div> </Card> ); } return ( <div className={styles.panel}> <MessageFeed /> <div className={styles.divider} /> <MessageInput /> </div> ); } export default ChatPanel; संदेश // src/crud/chat/components/MessageFeed.tsx import React, { useRef, useEffect } from "react"; import { BsCheck2, BsCheck2All } from "react-icons/bs"; import { useChat } from "../../contexts/ChatContext"; import MessageItem from "../MessageItem"; import styles from "./MessageFeed.module.scss"; function MessageFeed() { const { messages } = useChat(); const scrollRef = useRef(null); useEffect(() => { scrollRef.current?.scrollIntoView({ behavior: "auto" }); }, [messages]); return ( <div ref={scrollRef} className={styles.feed}> {messages.map((group) => ( <div key={group.date} className={styles.dateGroup}> <div className={styles.dateDivider}> <span>{group.date}</span> </div> {group.messages.map((msg) => ( <div className={styles.message}> {msg.text && <p>{msg.text}</p>} {msg.image && ( <img src={msg.image} alt="" style={{ maxWidth: "200px", borderRadius: 4 }} /> )} {msg.file && ( <a href={msg.file} target="_blank" rel="noopener noreferrer"> Скачать файл </a> )} <div style={{ fontSize: "0.8rem", opacity: 0.6 }}> {dayjs(msg.created_at).format("HH:mm")} {msg.is_read ? <BsCheck2All /> : <BsCheck2 />} </div> </div> ))} </div> ))} </div> ); } export default MessageFeed; संदेश // src/crud/chat/components/MessageInput.tsx import React from "react"; import { ChangeEventHandler, useCallback, useEffect, useRef, useState, } from "react"; import { FiPaperclip } from "react-icons/fi"; import { RxPaperPlane } from "react-icons/rx"; import { Form, Button, useUrlState, Textarea } from "@devfamily/admiral"; import { useChat } from "../../model/ChatContext"; import styles from "./MessageInput.module.scss"; function MessageInput() { const { sendMessage } = useChat(); const [urlState] = useUrlState(); const { client_id } = urlState; const [values, setValues] = useState({}); const textRef = useRef < HTMLTextAreaElement > null; useEffect(() => { setValues({}); setErrors(null); }, [client_id]); const onSubmit = useCallback( async (e?: React.FormEvent<HTMLFormElement>) => { e?.preventDefault(); const textIsEmpty = !values.text?.trim()?.length; sendMessage( { ...(values.image && { image: values.image }), ...(!textIsEmpty && { text: values.text }), client_id, }, () => { setValues({ text: "" }); }, (err: any) => { if (err.errors) { setErrors(err.errors); } } ); }, [values, sendMessage, client_id] ); const onUploadFile: ChangeEventHandler<HTMLInputElement> = useCallback( (e) => { const file = Array.from(e.target.files || [])[0]; setValues((prev: any) => ({ ...prev, image: file })); e.target.value = ""; }, [values] ); const onChange = useCallback((e) => { setValues((prev) => ({ ...prev, text: e.target.value })); }, []); const onKeyDown = useCallback((e: React.KeyboardEvent<HTMLTextAreaElement>) => { if ((e.code === "Enter" || e.code === "NumpadEnter") && !e.shiftKey) { onSubmit(); e.preventDefault(); } }, [onSubmit]); return ( <form className={styles.form} onSubmit={onSubmit}> <label className={styles.upload}> <input type="file" onChange={onUploadFile} className={styles.visuallyHidden} /> <FiPaperclip size="24px" /> </label> <Textarea value={values.text ?? ""} onChange={onChange} rows={1} onKeyDown={onKeyDown} placeholder="Написать сообщение..." ref={textRef} className={styles.textarea} /> <Button view="secondary" type="submit" disabled={!values.image && !values.text?.trim().length} className={styles.submitBtn} > <RxPaperPlane /> </Button> </form> ); } export default MessageInput; स्टाइल मैंने इसे एडमिरल के सीएसएस परिवर्तकों का उपयोग करके स्टाइलिश किया ताकि सब कुछ लगातार रह सके: .chat { border-radius: var(--radius-m); border: 2px solid var(--color-bg-border); background-color: var(--color-bg-default); } .message { padding: var(--space-m); border-radius: var(--radius-s); background-color: var(--color-bg-default); } सूचनाएं जोड़ें मैंने नए संदेशों के लिए नोटिस भी जोड़ा जब उपयोगकर्ता उस चैट को नहीं देख रहा था: import { useNotifications } from '@devfamily/admiral' const ChatContext = () => { const { showNotification } = useNotifications() useEffect(() => { if (!lastMessage) return if (selectedDialog?.client_id !== lastMessage.client_id) { showNotification({ title: 'New message', message: `${lastMessage.client_name}: ${lastMessage.text || 'Image'}`, type: 'info', duration: 5000 }) } }, [lastMessage, selectedDialog, showNotification]) } निष्कर्ष और इसी तरह, तीसरे पक्ष के उपकरणों का उपयोग करने के बजाय, मैंने इसे सीधे अपने एडमिरल-आधारित प्रशासनिक पैनल में बनाया। परिणाम एक पूरी तरह से कस्टम चैट था: वास्तविक समय संदेश, संवाद, फ़ाइल अपलोड, और सूचनाएं - सभी एकीकृत और मेरे नियंत्रण में। इसे देखें, और मुझे बताएं कि आप क्या सोचते हैं!