IO-2208 added virtualization to conversation list

This commit is contained in:
swtmply
2023-05-18 23:04:10 +08:00
parent 4bc8ff26d2
commit d66fdfb2e0
3 changed files with 167 additions and 91 deletions

View File

@@ -7,6 +7,8 @@ import { selectSelectedConversation } from "../../redux/messaging/messaging.sele
import { TimeAgoFormatter } from "../../utils/DateFormatter"; import { TimeAgoFormatter } from "../../utils/DateFormatter";
import PhoneFormatter from "../../utils/PhoneFormatter"; import PhoneFormatter from "../../utils/PhoneFormatter";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import { List as VirtualizedList, AutoSizer } from "react-virtualized";
import "./chat-conversation-list.styles.scss"; import "./chat-conversation-list.styles.scss";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
@@ -18,73 +20,87 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(setSelectedConversation(conversationId)), dispatch(setSelectedConversation(conversationId)),
}); });
const ChatConversationListComponent = React.forwardRef( function ChatConversationListComponent({
function ChatConversationListComponent( conversationList,
{ selectedConversation,
conversationList, setSelectedConversation,
selectedConversation, subscribeToMoreConversations,
setSelectedConversation, loadMoreConversations,
subscribeToMoreConversations, }) {
}, useEffect(
ref () => subscribeToMoreConversations(),
) { // eslint-disable-next-line react-hooks/exhaustive-deps
useEffect( []
() => subscribeToMoreConversations(), );
// eslint-disable-next-line react-hooks/exhaustive-deps
[] const rowRenderer = ({ index, key, style }) => {
); const item = conversationList[index];
return ( return (
<div className="chat-list-container"> <List.Item
<List key={key}
bordered onClick={() => setSelectedConversation(item.id)}
dataSource={conversationList} className={`chat-list-item ${
renderItem={(item) => ( item.id === selectedConversation
<List.Item ? "chat-list-selected-conversation"
key={item.id} : null
onClick={() => setSelectedConversation(item.id)} }`}
className={`chat-list-item ${ style={style}
item.id === selectedConversation >
? "chat-list-selected-conversation" <div sryle={{ display: "inline-block" }}>
: null {item.label && <div className="chat-name">{item.label}</div>}
}`} {item.job_conversations.length > 0 ? (
> <div className="chat-name">
<div sryle={{ display: "inline-block" }}> {item.job_conversations.map((j, idx) => (
{item.label && <div className="chat-name">{item.label}</div>} <div key={idx}>
{item.job_conversations.length > 0 ? ( <OwnerNameDisplay ownerObject={j.job} />
<div className="chat-name">
{item.job_conversations.map((j, idx) => (
<div key={idx}>
<OwnerNameDisplay ownerObject={j.job} />
</div>
))}
</div>
) : (
<PhoneFormatter>{item.phone_num}</PhoneFormatter>
)}
</div>
<div sryle={{ display: "inline-block" }}>
<div>
{item.job_conversations.length > 0
? item.job_conversations.map((j, idx) => (
<Tag key={idx} className="ro-number-tag">
{j.job.ro_number}
</Tag>
))
: null}
</div> </div>
<TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter> ))}
</div> </div>
<Badge count={item.messages_aggregate.aggregate.count || 0} /> ) : (
</List.Item> <PhoneFormatter>{item.phone_num}</PhoneFormatter>
)} )}
footer={<span ref={ref}></span>} </div>
/> <div sryle={{ display: "inline-block" }}>
</div> <div>
{item.job_conversations.length > 0
? item.job_conversations.map((j, idx) => (
<Tag key={idx} className="ro-number-tag">
{j.job.ro_number}
</Tag>
))
: null}
</div>
<TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>
</div>
<Badge count={item.messages_aggregate.aggregate.count || 0} />
</List.Item>
); );
} };
);
export default connect(mapStateToProps, mapDispatchToProps, null, { return (
forwardRef: true, <div className="chat-list-container">
})(ChatConversationListComponent); <AutoSizer>
{({ height, width }) => (
<VirtualizedList
height={height}
width={width}
rowCount={conversationList.length}
rowHeight={60}
rowRenderer={rowRenderer}
onScroll={({ scrollTop, scrollHeight, clientHeight }) => {
if (scrollTop + clientHeight === scrollHeight) {
loadMoreConversations();
}
}}
/>
)}
</AutoSizer>
</div>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(ChatConversationListComponent);

View File

@@ -3,8 +3,9 @@
} }
.chat-list-container { .chat-list-container {
flex: 1; flex: 1;
overflow: auto; overflow: hidden;
height: 100%; height: 100%;
border: 1px solid gainsboro;
} }
.chat-list-item { .chat-list-item {
@@ -21,4 +22,6 @@
.ro-number-tag { .ro-number-tag {
align-self: baseline; align-self: baseline;
} }
padding: 12px 24px;
border-bottom: 1px solid gainsboro;
} }

View File

@@ -4,9 +4,9 @@ import {
ShrinkOutlined, ShrinkOutlined,
SyncOutlined, SyncOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { useQuery, useSubscription } from "@apollo/client"; import { useLazyQuery, useQuery, useSubscription } from "@apollo/client";
import { Badge, Card, Col, Row, Space, Tag, Tooltip, Typography } from "antd"; import { Badge, Card, Col, Row, Space, Tag, Tooltip, Typography } from "antd";
import React, { useEffect, useState } from "react"; import React, { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
@@ -25,7 +25,6 @@ import ChatConversationContainer from "../chat-conversation/chat-conversation.co
import ChatNewConversation from "../chat-new-conversation/chat-new-conversation.component"; import ChatNewConversation from "../chat-new-conversation/chat-new-conversation.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component"; import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import "./chat-popup.styles.scss"; import "./chat-popup.styles.scss";
import { useInView } from "react-intersection-observer";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
selectedConversation: selectSelectedConversation, selectedConversation: selectSelectedConversation,
@@ -42,7 +41,6 @@ export function ChatPopupComponent({
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [pollInterval, setpollInterval] = useState(0); const [pollInterval, setpollInterval] = useState(0);
const { ref, inView } = useInView({});
const { data: unreadData } = useSubscription( const { data: unreadData } = useSubscription(
UNREAD_CONVERSATION_COUNT_SUBSCRIPTION UNREAD_CONVERSATION_COUNT_SUBSCRIPTION
@@ -56,15 +54,14 @@ export function ChatPopupComponent({
// } // }
// ); // );
const { loading, data, called, refetch, fetchMore, subscribeToMore } = const [
useQuery(CONVERSATION_LIST_QUERY, { getConversations,
variables: { { loading, data, called, fetchMore, subscribeToMore },
offset: 0, ] = useLazyQuery(CONVERSATION_LIST_QUERY, {
}, fetchPolicy: "cache-and-network",
fetchPolicy: "cache-and-network", nextFetchPolicy: "cache-first",
nextFetchPolicy: "cache-first", skip: !chatVisible,
skip: !chatVisible, });
});
const fcmToken = sessionStorage.getItem("fcmtoken"); const fcmToken = sessionStorage.getItem("fcmtoken");
@@ -77,18 +74,60 @@ export function ChatPopupComponent({
}, [fcmToken]); }, [fcmToken]);
useEffect(() => { useEffect(() => {
if (called && chatVisible) refetch(); if (called && chatVisible)
}, [chatVisible, called, refetch]); getConversations({
variables: {
offset: 0,
},
});
}, [chatVisible, called, getConversations]);
useEffect(() => { const loadMoreConversations = useCallback(() => {
if (inView && data && data.conversations) { if (data)
fetchMore({ fetchMore({
variables: { variables: {
offset: data.conversations.length, offset: data.conversations.length,
}, },
}); });
} }, [data, fetchMore]);
}, [inView, data, fetchMore]);
const subscribeToMoreConversations = useCallback(
() =>
subscribeToMore({
document: CONVERSATION_LIST_SUBSCRIPTION,
variables: { offset: 0 },
updateQuery: (prev, { subscriptionData }) => {
console.log("Hello");
if (
!subscriptionData.data ||
subscriptionData.data.conversations.length === 0
)
return prev;
let conversations = prev.conversations;
const newConversations = subscriptionData.data.conversations;
for (const conversation of newConversations) {
const index = conversations.findIndex(
(prevConversation) => prevConversation.id === conversation.id
);
if (index !== -1) {
conversations.splice(index, 1);
conversations.unshift(conversation);
continue;
}
conversation.unshift(conversation);
}
return Object.assign({}, prev, {
conversations: conversations,
});
},
}),
[subscribeToMore]
);
const unreadCount = unreadData?.messages_aggregate.aggregate.count || 0; const unreadCount = unreadData?.messages_aggregate.aggregate.count || 0;
@@ -105,10 +144,10 @@ export function ChatPopupComponent({
<Tooltip title={t("messaging.labels.recentonly")}> <Tooltip title={t("messaging.labels.recentonly")}>
<InfoCircleOutlined /> <InfoCircleOutlined />
</Tooltip> </Tooltip>
<SyncOutlined {/* <SyncOutlined
style={{ cursor: "pointer" }} style={{ cursor: "pointer" }}
onClick={() => refetch()} onClick={() => refetch()}
/> /> */}
{pollInterval > 0 && ( {pollInterval > 0 && (
<Tag color="yellow">{t("messaging.labels.nopush")}</Tag> <Tag color="yellow">{t("messaging.labels.nopush")}</Tag>
)} )}
@@ -125,7 +164,8 @@ export function ChatPopupComponent({
) : ( ) : (
<ChatConversationListComponent <ChatConversationListComponent
conversationList={data ? data.conversations : []} conversationList={data ? data.conversations : []}
subscribeToMoreConversations={() => { loadMoreConversations={loadMoreConversations}
subscribeToMoreConversations={() =>
subscribeToMore({ subscribeToMore({
document: CONVERSATION_LIST_SUBSCRIPTION, document: CONVERSATION_LIST_SUBSCRIPTION,
variables: { offset: 0 }, variables: { offset: 0 },
@@ -135,16 +175,33 @@ export function ChatPopupComponent({
subscriptionData.data.conversations.length === 0 subscriptionData.data.conversations.length === 0
) )
return prev; return prev;
let conversations = [...prev.conversations];
const newConversations = const newConversations =
subscriptionData.data.conversations; subscriptionData.data.conversations;
for (const conversation of newConversations) {
const index = conversations.findIndex(
(prevConversation) =>
prevConversation.id === conversation.id
);
if (index !== -1) {
conversations.splice(index, 1);
conversations.unshift(conversation);
continue;
}
conversations.unshift(conversation);
}
return Object.assign({}, prev, { return Object.assign({}, prev, {
conversations: [...newConversations], conversations: conversations,
}); });
}, },
}); })
}} }
ref={ref}
/> />
)} )}
</Col> </Col>