@droppii-org/chat-sdk
Version:
Droppii React Chat SDK
124 lines (123 loc) • 11.1 kB
JavaScript
"use client";
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { SessionType, } from "@openim/wasm-client-sdk";
import { useChatContext } from "../../context/ChatContext";
import { useMessage } from "../../hooks/message/useMessage";
import { Avatar, Button, Input, Tooltip } from "antd";
import { Icon } from "../icon";
import { useCallback, useEffect, useRef, useState } from "react";
import clsx from "clsx";
import { useSendMessage, } from "../../hooks/message/useSendMessage";
import InfiniteLoader from "react-window-infinite-loader";
import { VariableSizeList as List, } from "react-window";
import AutoSizer from "react-virtualized-auto-sizer";
const MessageList = (props) => {
var _a, _b;
const { conversationId, conversationData, onClose } = props;
const { messageList, groupMessages, refetch } = useMessage(conversationId);
const scrollRef = useRef(null);
const shouldScrollToBottomRef = useRef(true);
const lastMessageCountRef = useRef((messageList === null || messageList === void 0 ? void 0 : messageList.length) || 0);
const { user } = useChatContext();
const { sendTextMessage } = useSendMessage({
recvID: (conversationData === null || conversationData === void 0 ? void 0 : conversationData.conversationType) !== SessionType.Single
? ""
: (conversationData === null || conversationData === void 0 ? void 0 : conversationData.userID) || "",
groupID: (conversationData === null || conversationData === void 0 ? void 0 : conversationData.conversationType) === SessionType.Single
? ""
: (conversationData === null || conversationData === void 0 ? void 0 : conversationData.groupID) || "",
});
const [textMessage, setTextMessage] = useState("");
const [composing, setComposing] = useState(false);
const [showScrollToBottomButton, setShowScrollToBottomButton] = useState(false);
const scrollToBottom = useCallback((force = false) => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
if (force) {
shouldScrollToBottomRef.current = true; // If forced, ensure auto-scroll is re-enabled
}
}
}, []);
const handleScroll = useCallback(() => {
if (!scrollRef.current)
return;
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
const SCROLL_UP_THRESHOLD = 200; // If user scrolls up more than 200px from bottom, disable auto-scroll
const SCROLL_DOWN_THRESHOLD = 5; // If user scrolls within 5px of bottom, re-enable auto-scroll
if (distanceFromBottom > SCROLL_UP_THRESHOLD) {
shouldScrollToBottomRef.current = false;
}
else if (distanceFromBottom <= SCROLL_DOWN_THRESHOLD) {
shouldScrollToBottomRef.current = true;
}
// Show button if not at bottom AND auto-scroll is disabled
setShowScrollToBottomButton(distanceFromBottom > SCROLL_DOWN_THRESHOLD &&
!shouldScrollToBottomRef.current);
}, []);
const onSendTextMessage = useCallback(async () => {
setTextMessage("");
const lastMessage = messageList === null || messageList === void 0 ? void 0 : messageList[(messageList === null || messageList === void 0 ? void 0 : messageList.length) - 1];
const res = await sendTextMessage(textMessage, lastMessage);
if (res) {
refetch();
}
}, [textMessage, sendTextMessage, refetch, messageList]);
const renderMessageItem = useCallback((listItemProps) => {
const { index } = listItemProps;
const groupMessage = groupMessages === null || groupMessages === void 0 ? void 0 : groupMessages[index];
const messagesInGroup = (groupMessage === null || groupMessage === void 0 ? void 0 : groupMessage.messages) || [];
return (_jsxs("div", { children: [_jsx("div", { children: groupMessage === null || groupMessage === void 0 ? void 0 : groupMessage.sendTime }), messagesInGroup === null || messagesInGroup === void 0 ? void 0 : messagesInGroup.map((message) => {
var _a, _b, _c;
const isMine = (message === null || message === void 0 ? void 0 : message.sendID) === (user === null || user === void 0 ? void 0 : user.userID);
return (_jsx("div", { className: clsx("flex", isMine ? "justify-end" : "justify-start"), children: _jsxs("div", { className: "flex items-end gap-2", children: [!isMine && (_jsx(Avatar, { children: ((_b = (_a = message === null || message === void 0 ? void 0 : message.senderNickname) === null || _a === void 0 ? void 0 : _a.charAt) === null || _b === void 0 ? void 0 : _b.call(_a, 0)) || "A" })), _jsxs("div", { className: "flex flex-col items-start", children: [!isMine && (_jsx("span", { className: "text-xs text-gray-500 mb-1 px-3", children: message === null || message === void 0 ? void 0 : message.senderNickname })), _jsx("div", { className: clsx("px-3 py-2 sm:px-4 sm:py-2 rounded-2xl max-w-full break-words", isMine
? "bg-blue-500 text-white"
: "bg-gray-100 text-gray-900"), children: _jsx("p", { className: "text-sm sm:text-base whitespace-pre-wrap", children: ((_c = message === null || message === void 0 ? void 0 : message.textElem) === null || _c === void 0 ? void 0 : _c.content) ||
"Tin nhắn không khả dụng" }) })] })] }) }, message === null || message === void 0 ? void 0 : message.clientMsgID));
})] }));
}, [user === null || user === void 0 ? void 0 : user.userID, groupMessages]);
useEffect(() => {
const currentMessageCount = (messageList === null || messageList === void 0 ? void 0 : messageList.length) || 0;
const previousMessageCount = lastMessageCountRef.current;
if (currentMessageCount > previousMessageCount) {
const newMessages = messageList === null || messageList === void 0 ? void 0 : messageList.slice(previousMessageCount);
const hasNewMessageFromCurrentUser = newMessages === null || newMessages === void 0 ? void 0 : newMessages.some((msg) => msg.sendID === (user === null || user === void 0 ? void 0 : user.userID));
// If current user sent a message, always scroll to bottom
// If another user sent a message, only scroll if shouldScrollToBottomRef is true (user is already at bottom)
if (hasNewMessageFromCurrentUser) {
setTimeout(() => scrollToBottom(true), 50); // Force scroll for own messages
}
else if (shouldScrollToBottomRef.current) {
setTimeout(() => scrollToBottom(), 50); // Scroll if auto-scroll is enabled for others' messages
}
}
lastMessageCountRef.current = currentMessageCount;
}, [messageList, user === null || user === void 0 ? void 0 : user.userID, scrollToBottom]);
return (_jsxs("div", { className: "flex flex-col flex-1 relative h-full bg-white", children: [_jsxs("div", { className: "px-4 py-3 flex items-center border-b gap-3", children: [_jsx(Avatar, { src: conversationData === null || conversationData === void 0 ? void 0 : conversationData.faceURL, children: ((_b = (_a = conversationData === null || conversationData === void 0 ? void 0 : conversationData.showName) === null || _a === void 0 ? void 0 : _a.charAt) === null || _b === void 0 ? void 0 : _b.call(_a, 0)) || "A" }), _jsxs("div", { className: "flex flex-col flex-1", children: [_jsx("p", { children: (conversationData === null || conversationData === void 0 ? void 0 : conversationData.showName) || "" }), _jsx("p", { className: "text-xs text-gray-500", children: "2 thành viên" })] }), _jsx(Button, { type: "text", shape: "circle", icon: _jsx(Icon, { icon: "align-justify-o", size: 24 }) }), !!onClose && (_jsx(Button, { type: "text", shape: "circle", icon: _jsx(Icon, { icon: "close-b", size: 24 }), onClick: onClose }))] }), _jsx("div", { className: "flex flex-col flex-1 min-h-0", children: _jsx("div", { className: "relative h-full", children: _jsx(AutoSizer, { children: ({ height, width }) => (_jsx(InfiniteLoader, { isItemLoaded: () => true, loadMoreItems: () => { }, itemCount: (groupMessages === null || groupMessages === void 0 ? void 0 : groupMessages.length) || 0, children: (_a) => {
var { onItemsRendered, ref } = _a, rest = __rest(_a, ["onItemsRendered", "ref"]);
return (_jsx(List, Object.assign({ ref: ref }, rest, { itemCount: (groupMessages === null || groupMessages === void 0 ? void 0 : groupMessages.length) || 0, onItemsRendered: onItemsRendered, height: height, width: width, itemSize: (index) => {
const groupMessage = groupMessages === null || groupMessages === void 0 ? void 0 : groupMessages[index];
const messagesInGroup = (groupMessage === null || groupMessage === void 0 ? void 0 : groupMessage.messages) || [];
return (messagesInGroup === null || messagesInGroup === void 0 ? void 0 : messagesInGroup.length) * 50;
}, children: renderMessageItem })));
} })) }) }) }), _jsx("div", { className: "border-t px-4 py-3", children: _jsx("div", { className: "border rounded-lg bg-gray-50", children: _jsxs("div", { className: "px-4 py-3 flex items-center gap-4", children: [_jsx(Input, { placeholder: "Nh\u1EADp tin nh\u1EAFn", size: "small", variant: "borderless", value: textMessage, onChange: (e) => setTextMessage(e.target.value), onKeyDown: (e) => {
if (composing) {
return;
}
if (e.key === "Enter") {
onSendTextMessage();
}
}, onCompositionStart: () => setComposing(true), onCompositionEnd: () => setComposing(false) }), _jsx(Tooltip, { title: "G\u1EEDi tin nh\u1EAFn", children: _jsx(Button, { type: "primary", shape: "circle", size: "middle", icon: _jsx(Icon, { icon: "send-b", color: "white", size: 16 }), disabled: textMessage.length === 0, onClick: onSendTextMessage }) })] }) }) })] }));
};
export default MessageList;