IO-2208 Added pagination and subscription to chat

This commit is contained in:
swtmply
2023-05-13 00:32:51 +08:00
parent 051ee347a9
commit 4bc8ff26d2
6 changed files with 189 additions and 110 deletions

View File

@@ -51,6 +51,7 @@
"react-i18next": "^12.2.0", "react-i18next": "^12.2.0",
"react-icons": "^4.7.1", "react-icons": "^4.7.1",
"react-image-lightbox": "^5.1.4", "react-image-lightbox": "^5.1.4",
"react-intersection-observer": "^9.4.3",
"react-number-format": "^5.1.3", "react-number-format": "^5.1.3",
"react-redux": "^8.0.5", "react-redux": "^8.0.5",
"react-resizable": "^3.0.4", "react-resizable": "^3.0.4",

View File

@@ -1,5 +1,5 @@
import { Badge, List, Tag } from "antd"; import { Badge, List, Tag } from "antd";
import React from "react"; import React, { useEffect } from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { setSelectedConversation } from "../../redux/messaging/messaging.actions"; import { setSelectedConversation } from "../../redux/messaging/messaging.actions";
@@ -18,60 +18,73 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(setSelectedConversation(conversationId)), dispatch(setSelectedConversation(conversationId)),
}); });
export function ChatConversationListComponent({ const ChatConversationListComponent = React.forwardRef(
conversationList, function ChatConversationListComponent(
selectedConversation, {
setSelectedConversation, conversationList,
}) { selectedConversation,
return ( setSelectedConversation,
<div className="chat-list-container"> subscribeToMoreConversations,
<List },
bordered ref
dataSource={conversationList} ) {
renderItem={(item) => ( useEffect(
<List.Item () => subscribeToMoreConversations(),
key={item.id} // eslint-disable-next-line react-hooks/exhaustive-deps
onClick={() => setSelectedConversation(item.id)} []
className={`chat-list-item ${ );
item.id === selectedConversation
? "chat-list-selected-conversation" return (
: null <div className="chat-list-container">
}`} <List
> bordered
<div sryle={{ display: "inline-block" }}> dataSource={conversationList}
{item.label && <div className="chat-name">{item.label}</div>} renderItem={(item) => (
{item.job_conversations.length > 0 ? ( <List.Item
<div className="chat-name"> key={item.id}
{item.job_conversations.map((j, idx) => ( onClick={() => setSelectedConversation(item.id)}
<div key={idx}> className={`chat-list-item ${
<OwnerNameDisplay ownerObject={j.job} /> item.id === selectedConversation
</div> ? "chat-list-selected-conversation"
))} : null
</div> }`}
) : ( >
<PhoneFormatter>{item.phone_num}</PhoneFormatter> <div sryle={{ display: "inline-block" }}>
)} {item.label && <div className="chat-name">{item.label}</div>}
</div> {item.job_conversations.length > 0 ? (
<div sryle={{ display: "inline-block" }}> <div className="chat-name">
<div> {item.job_conversations.map((j, idx) => (
{item.job_conversations.length > 0 <div key={idx}>
? item.job_conversations.map((j, idx) => ( <OwnerNameDisplay ownerObject={j.job} />
<Tag key={idx} className="ro-number-tag"> </div>
{j.job.ro_number} ))}
</Tag> </div>
)) ) : (
: null} <PhoneFormatter>{item.phone_num}</PhoneFormatter>
)}
</div> </div>
<TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter> <div sryle={{ display: "inline-block" }}>
</div> <div>
<Badge count={item.messages_aggregate.aggregate.count || 0} /> {item.job_conversations.length > 0
</List.Item> ? item.job_conversations.map((j, idx) => (
)} <Tag key={idx} className="ro-number-tag">
/> {j.job.ro_number}
</div> </Tag>
); ))
} : null}
export default connect( </div>
mapStateToProps, <TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>
mapDispatchToProps </div>
)(ChatConversationListComponent); <Badge count={item.messages_aggregate.aggregate.count || 0} />
</List.Item>
)}
footer={<span ref={ref}></span>}
/>
</div>
);
}
);
export default connect(mapStateToProps, mapDispatchToProps, null, {
forwardRef: true,
})(ChatConversationListComponent);

View File

@@ -4,7 +4,7 @@ import {
ShrinkOutlined, ShrinkOutlined,
SyncOutlined, SyncOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { useQuery } from "@apollo/client"; import { 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, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -12,7 +12,8 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { import {
CONVERSATION_LIST_QUERY, CONVERSATION_LIST_QUERY,
UNREAD_CONVERSATION_COUNT, CONVERSATION_LIST_SUBSCRIPTION,
UNREAD_CONVERSATION_COUNT_SUBSCRIPTION,
} from "../../graphql/conversations.queries"; } from "../../graphql/conversations.queries";
import { toggleChatVisible } from "../../redux/messaging/messaging.actions"; import { toggleChatVisible } from "../../redux/messaging/messaging.actions";
import { import {
@@ -24,6 +25,7 @@ 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,
@@ -40,19 +42,29 @@ 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 } = useQuery(UNREAD_CONVERSATION_COUNT, { const { data: unreadData } = useSubscription(
fetchPolicy: "network-only", UNREAD_CONVERSATION_COUNT_SUBSCRIPTION
nextFetchPolicy: "network-only", );
...(pollInterval > 0 ? { pollInterval } : {}),
});
const { loading, data, refetch, called } = useQuery(CONVERSATION_LIST_QUERY, { // const { data: unreadData, refetch: unreadRefetch } = useQuery(
fetchPolicy: "network-only", // UNREAD_CONVERSATION_COUNT,
nextFetchPolicy: "network-only", // {
skip: !chatVisible, // fetchPolicy: "network-only",
...(pollInterval > 0 ? { pollInterval } : {}), // nextFetchPolicy: "network-only",
}); // }
// );
const { loading, data, called, refetch, fetchMore, subscribeToMore } =
useQuery(CONVERSATION_LIST_QUERY, {
variables: {
offset: 0,
},
fetchPolicy: "cache-and-network",
nextFetchPolicy: "cache-first",
skip: !chatVisible,
});
const fcmToken = sessionStorage.getItem("fcmtoken"); const fcmToken = sessionStorage.getItem("fcmtoken");
@@ -68,12 +80,15 @@ export function ChatPopupComponent({
if (called && chatVisible) refetch(); if (called && chatVisible) refetch();
}, [chatVisible, called, refetch]); }, [chatVisible, called, refetch]);
// const unreadCount = data useEffect(() => {
// ? data.conversations.reduce( if (inView && data && data.conversations) {
// (acc, val) => val.messages_aggregate.aggregate.count + acc, fetchMore({
// 0 variables: {
// ) offset: data.conversations.length,
// : 0; },
});
}
}, [inView, data, fetchMore]);
const unreadCount = unreadData?.messages_aggregate.aggregate.count || 0; const unreadCount = unreadData?.messages_aggregate.aggregate.count || 0;
@@ -110,6 +125,26 @@ export function ChatPopupComponent({
) : ( ) : (
<ChatConversationListComponent <ChatConversationListComponent
conversationList={data ? data.conversations : []} conversationList={data ? data.conversations : []}
subscribeToMoreConversations={() => {
subscribeToMore({
document: CONVERSATION_LIST_SUBSCRIPTION,
variables: { offset: 0 },
updateQuery: (prev, { subscriptionData }) => {
if (
!subscriptionData.data ||
subscriptionData.data.conversations.length === 0
)
return prev;
const newConversations =
subscriptionData.data.conversations;
return Object.assign({}, prev, {
conversations: [...newConversations],
});
},
});
}}
ref={ref}
/> />
)} )}
</Col> </Col>

View File

@@ -1,35 +1,36 @@
import { gql } from "@apollo/client"; import { gql } from "@apollo/client";
// export const CONVERSATION_LIST_SUBSCRIPTION = gql` export const CONVERSATION_LIST_SUBSCRIPTION = gql`
// subscription CONVERSATION_LIST_SUBSCRIPTION { subscription CONVERSATION_LIST_SUBSCRIPTION($offset: Int!) {
// conversations( conversations(
// order_by: { updated_at: desc } order_by: { updated_at: desc }
// limit: 50 limit: 1
// where: { archived: { _eq: false } } offset: $offset
// ) { where: { archived: { _eq: false } }
// phone_num ) {
// id phone_num
// updated_at id
// unreadcnt updated_at
// messages_aggregate( unreadcnt
// where: { read: { _eq: false }, isoutbound: { _eq: false } } messages_aggregate(
// ) { where: { read: { _eq: false }, isoutbound: { _eq: false } }
// aggregate { ) {
// count aggregate {
// } count
// } }
// job_conversations { }
// job { job_conversations {
// id job {
// ro_number id
// ownr_fn ro_number
// ownr_ln ownr_fn
// ownr_co_nm ownr_ln
// } ownr_co_nm
// } }
// } }
// } }
// `; }
`;
export const UNREAD_CONVERSATION_COUNT = gql` export const UNREAD_CONVERSATION_COUNT = gql`
query UNREAD_CONVERSATION_COUNT { query UNREAD_CONVERSATION_COUNT {
@@ -43,11 +44,24 @@ export const UNREAD_CONVERSATION_COUNT = gql`
} }
`; `;
export const UNREAD_CONVERSATION_COUNT_SUBSCRIPTION = gql`
subscription UNREAD_CONVERSATION_COUNT_SUBSCRIPTION {
messages_aggregate(
where: { read: { _eq: false }, isoutbound: { _eq: false } }
) {
aggregate {
count
}
}
}
`;
export const CONVERSATION_LIST_QUERY = gql` export const CONVERSATION_LIST_QUERY = gql`
query CONVERSATION_LIST_QUERY { query CONVERSATION_LIST_QUERY($offset: Int!) {
conversations( conversations(
order_by: { updated_at: desc } order_by: { updated_at: desc }
limit: 50 limit: 20
offset: $offset
where: { archived: { _eq: false } } where: { archived: { _eq: false } }
) { ) {
phone_num phone_num

View File

@@ -3,7 +3,10 @@ import { setContext } from "@apollo/client/link/context";
import { HttpLink } from "@apollo/client/link/http"; //"apollo-link-http"; import { HttpLink } from "@apollo/client/link/http"; //"apollo-link-http";
import { RetryLink } from "@apollo/client/link/retry"; import { RetryLink } from "@apollo/client/link/retry";
import { WebSocketLink } from "@apollo/client/link/ws"; import { WebSocketLink } from "@apollo/client/link/ws";
import { getMainDefinition } from "@apollo/client/utilities"; import {
getMainDefinition,
offsetLimitPagination,
} from "@apollo/client/utilities";
//import { split } from "apollo-link"; //import { split } from "apollo-link";
import apolloLogger from "apollo-link-logger"; import apolloLogger from "apollo-link-logger";
//import axios from "axios"; //import axios from "axios";
@@ -140,7 +143,15 @@ middlewares.push(
) )
); );
const cache = new InMemoryCache({}); const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
conversations: offsetLimitPagination(),
},
},
},
});
const client = new ApolloClient({ const client = new ApolloClient({
link: ApolloLink.from(middlewares), link: ApolloLink.from(middlewares),

View File

@@ -11168,6 +11168,11 @@ react-image-lightbox@^5.1.4:
prop-types "^15.7.2" prop-types "^15.7.2"
react-modal "^3.11.1" react-modal "^3.11.1"
react-intersection-observer@^9.4.3:
version "9.4.3"
resolved "https://registry.yarnpkg.com/react-intersection-observer/-/react-intersection-observer-9.4.3.tgz#ec84ce0c25cad548075130ea045ac5c7adf908f3"
integrity sha512-WNRqMQvKpupr6MzecAQI0Pj0+JQong307knLP4g/nBex7kYfIaZsPpXaIhKHR+oV8z+goUbH9e10j6lGRnTzlQ==
react-is@^16.10.2, react-is@^16.12.0, react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0: react-is@^16.10.2, react-is@^16.12.0, react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0:
version "16.13.1" version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"