IO-1551 Refactor messaging.
This commit is contained in:
@@ -1,43 +0,0 @@
|
||||
import { MessageOutlined } from "@ant-design/icons";
|
||||
import { Badge, Card } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { toggleChatVisible } from "../../redux/messaging/messaging.actions";
|
||||
import { selectChatVisible } from "../../redux/messaging/messaging.selectors";
|
||||
import ChatPopupComponent from '../chat-popup/chat-popup.component'
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
chatVisible: selectChatVisible,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
toggleChatVisible: () => dispatch(toggleChatVisible()),
|
||||
});
|
||||
|
||||
export function ChatAffixComponent({
|
||||
chatVisible,
|
||||
toggleChatVisible,
|
||||
conversationList,
|
||||
unreadCount,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Badge count={unreadCount}>
|
||||
<Card size='small'>
|
||||
{chatVisible ? (
|
||||
<ChatPopupComponent conversationList={conversationList} />
|
||||
) : (
|
||||
<div
|
||||
onClick={() => toggleChatVisible()}
|
||||
style={{ cursor: "pointer" }}>
|
||||
<MessageOutlined />
|
||||
<strong>{t("messaging.labels.messaging")}</strong>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ChatAffixComponent);
|
||||
@@ -1,13 +1,16 @@
|
||||
import { useSubscription } from "@apollo/client";
|
||||
import React from "react";
|
||||
import { useApolloClient } from "@apollo/client";
|
||||
import { getToken, onMessage } from "@firebase/messaging";
|
||||
import { Button, notification, Space } from "antd";
|
||||
import axios from "axios";
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { CONVERSATION_LIST_SUBSCRIPTION } from "../../graphql/conversations.queries";
|
||||
import { messaging, requestForToken } from "../../firebase/firebase.utils";
|
||||
import { selectChatVisible } from "../../redux/messaging/messaging.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||
import ChatAffixComponent from "./chat-affix.component";
|
||||
import FcmHandler from "../../utils/fcm-handler";
|
||||
import ChatPopupComponent from "../chat-popup/chat-popup.component";
|
||||
import "./chat-affix.styles.scss";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -16,32 +19,85 @@ const mapStateToProps = createStructuredSelector({
|
||||
});
|
||||
|
||||
export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
||||
const { loading, error, data } = useSubscription(
|
||||
CONVERSATION_LIST_SUBSCRIPTION,
|
||||
{
|
||||
skip: !bodyshop || (bodyshop && !bodyshop.messagingservicesid),
|
||||
}
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
const client = useApolloClient();
|
||||
useEffect(() => {
|
||||
if (!bodyshop || !bodyshop.messagingservicesid) return;
|
||||
|
||||
if (loading) return <LoadingSpinner />;
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
async function SubscribeToTopic() {
|
||||
try {
|
||||
const r = await axios.post("/notifications/subscribe", {
|
||||
fcm_tokens: await getToken(messaging, {
|
||||
vapidKey: process.env.REACT_APP_FIREBASE_PUBLIC_VAPID_KEY,
|
||||
}),
|
||||
type: "messaging",
|
||||
imexshopid: bodyshop.imexshopid,
|
||||
});
|
||||
console.log("FCM Topic Subscription", r.data);
|
||||
} catch (error) {
|
||||
console.log(
|
||||
"Error attempting to subscribe to messaging topic: ",
|
||||
error
|
||||
);
|
||||
notification.open({
|
||||
type: "warning",
|
||||
message: t("general.errors.fcm"),
|
||||
btn: (
|
||||
<Space>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
const resp = await requestForToken();
|
||||
console.log(
|
||||
"🚀 ~ file: chat-affix.container.jsx ~ line 44 ~ resp",
|
||||
resp
|
||||
);
|
||||
SubscribeToTopic();
|
||||
}}
|
||||
>
|
||||
{t("general.actions.tryagain")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const win = window.open(
|
||||
"https://help.imex.online/en/article/enabling-notifications-o978xi/",
|
||||
"_blank"
|
||||
);
|
||||
win.focus();
|
||||
}}
|
||||
>
|
||||
{t("general.labels.help")}
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
SubscribeToTopic();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [bodyshop]);
|
||||
|
||||
useEffect(() => {
|
||||
function handleMessage(payload) {
|
||||
FcmHandler({
|
||||
client,
|
||||
payload: (payload && payload.data && payload.data.data) || payload.data,
|
||||
});
|
||||
}
|
||||
const stopMessageListenr = onMessage(messaging, handleMessage);
|
||||
const channel = new BroadcastChannel("imex-sw-messages");
|
||||
channel.addEventListener("message", handleMessage);
|
||||
return () => {
|
||||
stopMessageListenr();
|
||||
channel.removeEventListener("message", handleMessage);
|
||||
};
|
||||
}, [client]);
|
||||
|
||||
if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
|
||||
|
||||
return (
|
||||
<div className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
|
||||
{bodyshop && bodyshop.messagingservicesid ? (
|
||||
<ChatAffixComponent
|
||||
conversationList={(data && data.conversations) || []}
|
||||
unreadCount={
|
||||
(data &&
|
||||
data.conversations.reduce((acc, val) => {
|
||||
return (acc = acc + val.messages_aggregate.aggregate.count);
|
||||
}, 0)) ||
|
||||
0
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
{bodyshop && bodyshop.messagingservicesid ? <ChatPopupComponent /> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { Badge, List, Tag, Tooltip } from "antd";
|
||||
import { AlertFilled } from "@ant-design/icons";
|
||||
import { Badge, List, Tag } from "antd";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { setSelectedConversation } from "../../redux/messaging/messaging.actions";
|
||||
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
||||
import { TimeAgoFormatter } from "../../utils/DateFormatter";
|
||||
import PhoneFormatter from "../../utils/PhoneFormatter";
|
||||
import "./chat-conversation-list.styles.scss";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TimeAgoFormatter } from "../../utils/DateFormatter";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
selectedConversation: selectSelectedConversation,
|
||||
@@ -24,8 +22,6 @@ export function ChatConversationListComponent({
|
||||
selectedConversation,
|
||||
setSelectedConversation,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="chat-list-container">
|
||||
<List
|
||||
@@ -33,6 +29,7 @@ export function ChatConversationListComponent({
|
||||
dataSource={conversationList}
|
||||
renderItem={(item) => (
|
||||
<List.Item
|
||||
key={item.id}
|
||||
onClick={() => setSelectedConversation(item.id)}
|
||||
className={`chat-list-item ${
|
||||
item.id === selectedConversation
|
||||
@@ -43,19 +40,9 @@ export function ChatConversationListComponent({
|
||||
{item.job_conversations.length > 0 ? (
|
||||
<div className="chat-name">
|
||||
{item.job_conversations.map((j, idx) => (
|
||||
<div key={idx} style={{ display: "flex" }}>
|
||||
{j.job.owner && !j.job.owner.allow_text_message && (
|
||||
<Tooltip title={t("messaging.labels.noallowtxt")}>
|
||||
<AlertFilled
|
||||
className="production-alert"
|
||||
style={{ marginRight: ".3rem", alignItems: "center" }}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
<div>{`${j.job.ownr_fn || ""} ${j.job.ownr_ln || ""} ${
|
||||
j.job.ownr_co_nm || ""
|
||||
} `}</div>
|
||||
</div>
|
||||
<div key={idx}>{`${j.job.ownr_fn || ""} ${
|
||||
j.job.ownr_ln || ""
|
||||
} ${j.job.ownr_co_nm || ""} `}</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -9,6 +9,7 @@ import "./chat-conversation.styles.scss";
|
||||
export default function ChatConversationComponent({
|
||||
subState,
|
||||
conversation,
|
||||
messages,
|
||||
handleMarkConversationAsRead,
|
||||
}) {
|
||||
const [loading, error] = subState;
|
||||
@@ -16,8 +17,6 @@ export default function ChatConversationComponent({
|
||||
if (loading) return <LoadingSkeleton />;
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
|
||||
const messages = (conversation && conversation.messages) || [];
|
||||
|
||||
return (
|
||||
<div
|
||||
className="chat-conversation"
|
||||
|
||||
@@ -1,18 +1,32 @@
|
||||
import { useMutation, useSubscription } from "@apollo/client";
|
||||
import { useMutation, useQuery, useSubscription } from "@apollo/client";
|
||||
import React, { useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { CONVERSATION_SUBSCRIPTION_BY_PK } from "../../graphql/conversations.queries";
|
||||
import {
|
||||
CONVERSATION_SUBSCRIPTION_BY_PK,
|
||||
GET_CONVERSATION_DETAILS,
|
||||
} from "../../graphql/conversations.queries";
|
||||
import { MARK_MESSAGES_AS_READ_BY_CONVERSATION } from "../../graphql/messages.queries";
|
||||
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
||||
import ChatConversationComponent from "./chat-conversation.component";
|
||||
import axios from "axios";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
selectedConversation: selectSelectedConversation,
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, null)(ChatConversationContainer);
|
||||
|
||||
export function ChatConversationContainer({ selectedConversation }) {
|
||||
export function ChatConversationContainer({ bodyshop, selectedConversation }) {
|
||||
const {
|
||||
loading: convoLoading,
|
||||
error: convoError,
|
||||
data: convoData,
|
||||
} = useQuery(GET_CONVERSATION_DETAILS, {
|
||||
variables: { conversationId: selectedConversation },
|
||||
});
|
||||
|
||||
const { loading, error, data } = useSubscription(
|
||||
CONVERSATION_SUBSCRIPTION_BY_PK,
|
||||
{
|
||||
@@ -26,30 +40,46 @@ export function ChatConversationContainer({ selectedConversation }) {
|
||||
MARK_MESSAGES_AS_READ_BY_CONVERSATION,
|
||||
{
|
||||
variables: { conversationId: selectedConversation },
|
||||
update(cache) {
|
||||
cache.modify({
|
||||
id: cache.identify({
|
||||
__typename: "conversations",
|
||||
id: selectedConversation,
|
||||
}),
|
||||
fields: {
|
||||
messages_aggregate(cached) {
|
||||
return { aggregate: { count: 0 } };
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const unreadCount =
|
||||
(data &&
|
||||
data.conversations_by_pk &&
|
||||
data.conversations_by_pk &&
|
||||
data.conversations_by_pk.messages_aggregate &&
|
||||
data.conversations_by_pk.messages_aggregate.aggregate &&
|
||||
data.conversations_by_pk.messages_aggregate.aggregate.count) ||
|
||||
0;
|
||||
data &&
|
||||
data.messages &&
|
||||
data.messages.reduce((acc, val) => {
|
||||
return !val.read && !val.isoutbound ? acc + 1 : acc;
|
||||
}, 0);
|
||||
|
||||
const handleMarkConversationAsRead = async () => {
|
||||
if (unreadCount > 0 && !!selectedConversation && !markingAsReadInProgress) {
|
||||
setMarkingAsReadInProgress(true);
|
||||
await markConversationRead();
|
||||
await markConversationRead({});
|
||||
await axios.post("/sms/markConversationRead", {
|
||||
conversationid: selectedConversation,
|
||||
imexshopid: bodyshop.imexshopid,
|
||||
});
|
||||
setMarkingAsReadInProgress(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ChatConversationComponent
|
||||
subState={[loading, error]}
|
||||
conversation={data ? data.conversations_by_pk : {}}
|
||||
subState={[loading || convoLoading, error || convoError]}
|
||||
conversation={convoData ? convoData.conversations_by_pk : {}}
|
||||
messages={data ? data.messages : []}
|
||||
handleMarkConversationAsRead={handleMarkConversationAsRead}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -46,7 +46,7 @@ export function ChatNewConversation({ openChatByPhone }) {
|
||||
|
||||
return (
|
||||
<Popover trigger="click" content={popContent}>
|
||||
<PlusCircleFilled style={{ margin: "1rem" }} />
|
||||
<PlusCircleFilled />
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,54 +1,108 @@
|
||||
import { ShrinkOutlined, InfoCircleOutlined } from "@ant-design/icons";
|
||||
import { Col, Row, Tooltip, Typography } from "antd";
|
||||
import React from "react";
|
||||
import {
|
||||
ShrinkOutlined,
|
||||
InfoCircleOutlined,
|
||||
SyncOutlined,
|
||||
} from "@ant-design/icons";
|
||||
|
||||
import { Col, Row, Tooltip, Space, Typography, Badge, Card } from "antd";
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { toggleChatVisible } from "../../redux/messaging/messaging.actions";
|
||||
import ChatConversationListComponent from "../chat-conversation-list/chat-conversation-list.component";
|
||||
import ChatConversationContainer from "../chat-conversation/chat-conversation.container";
|
||||
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
||||
import {
|
||||
selectChatVisible,
|
||||
selectSelectedConversation,
|
||||
} from "../../redux/messaging/messaging.selectors";
|
||||
import "./chat-popup.styles.scss";
|
||||
import ChatNewConversation from "../chat-new-conversation/chat-new-conversation.component";
|
||||
import { CONVERSATION_LIST_QUERY } from "../../graphql/conversations.queries";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||
import { MessageOutlined } from "@ant-design/icons";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
selectedConversation: selectSelectedConversation,
|
||||
chatVisible: selectChatVisible,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
toggleChatVisible: () => dispatch(toggleChatVisible()),
|
||||
});
|
||||
|
||||
export function ChatPopupComponent({
|
||||
conversationList,
|
||||
chatVisible,
|
||||
selectedConversation,
|
||||
toggleChatVisible,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="chat-popup">
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<Typography.Title level={4}>
|
||||
{t("messaging.labels.messaging")}
|
||||
</Typography.Title>
|
||||
<ChatNewConversation />
|
||||
<Tooltip title={t("messaging.labels.recentonly")}>
|
||||
<InfoCircleOutlined />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<ShrinkOutlined
|
||||
onClick={() => toggleChatVisible()}
|
||||
style={{ position: "absolute", right: ".5rem", top: ".5rem" }}
|
||||
/>
|
||||
|
||||
<Row gutter={[8, 8]} className="chat-popup-content">
|
||||
<Col span={8}>
|
||||
<ChatConversationListComponent conversationList={conversationList} />
|
||||
</Col>
|
||||
<Col span={16}>
|
||||
{selectedConversation ? <ChatConversationContainer /> : null}
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
const { loading, data, refetch, called } = useQuery(
|
||||
CONVERSATION_LIST_QUERY,
|
||||
{}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (called && chatVisible) refetch();
|
||||
}, [chatVisible, called, refetch]);
|
||||
|
||||
const unreadCount = data
|
||||
? data.conversations.reduce(
|
||||
(acc, val) => val.messages_aggregate.aggregate.count + acc,
|
||||
0
|
||||
)
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<Badge count={unreadCount}>
|
||||
<Card size="small">
|
||||
{chatVisible ? (
|
||||
<div className="chat-popup">
|
||||
<Space align="center">
|
||||
<Typography.Title level={4}>
|
||||
{t("messaging.labels.messaging")}
|
||||
</Typography.Title>
|
||||
<ChatNewConversation />
|
||||
<Tooltip title={t("messaging.labels.recentonly")}>
|
||||
<InfoCircleOutlined />
|
||||
</Tooltip>
|
||||
<SyncOutlined
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => refetch()}
|
||||
/>
|
||||
</Space>
|
||||
<ShrinkOutlined
|
||||
onClick={() => toggleChatVisible()}
|
||||
style={{ position: "absolute", right: ".5rem", top: ".5rem" }}
|
||||
/>
|
||||
|
||||
<Row gutter={[8, 8]} className="chat-popup-content">
|
||||
<Col span={8}>
|
||||
{loading ? (
|
||||
<LoadingSpinner />
|
||||
) : (
|
||||
<ChatConversationListComponent
|
||||
conversationList={data ? data.conversations : []}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
<Col span={16}>
|
||||
{selectedConversation ? <ChatConversationContainer /> : null}
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
onClick={() => toggleChatVisible()}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<MessageOutlined />
|
||||
<strong>{t("messaging.labels.messaging")}</strong>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ChatPopupComponent);
|
||||
|
||||
Reference in New Issue
Block a user