Merged in release/2021-12-10 (pull request #293)

release/2021-12-10

Approved-by: Patrick Fic
This commit is contained in:
Patrick Fic
2021-12-08 21:18:27 +00:00
30 changed files with 632 additions and 302 deletions

View File

@@ -1,4 +1,4 @@
<babeledit_project version="1.2" be_version="2.7.1">
<babeledit_project be_version="2.7.1" version="1.2">
<!--
BabelEdit project file
@@ -14199,6 +14199,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>tryagain</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>view</name>
<definition_loaded>false</definition_loaded>
@@ -14246,6 +14267,27 @@
<folder_node>
<name>errors</name>
<children>
<concept_node>
<name>fcm</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>notfound</name>
<definition_loaded>false</definition_loaded>
@@ -14634,6 +14676,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>help</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>hours</name>
<definition_loaded>false</definition_loaded>

View File

@@ -1,53 +1,57 @@
importScripts("https://www.gstatic.com/firebasejs/7.14.2/firebase-app.js");
importScripts(
"https://www.gstatic.com/firebasejs/7.14.2/firebase-messaging.js"
);
// Scripts for firebase and firebase messaging
importScripts("https://www.gstatic.com/firebasejs/8.2.0/firebase-app.js");
importScripts("https://www.gstatic.com/firebasejs/8.2.0/firebase-messaging.js");
firebase.initializeApp({
apiKey: "AIzaSyDSezy-jGJreo7ulgpLdlpOwAOrgcaEkhU",
authDomain: "imex-prod.firebaseapp.com",
databaseURL: "https://imex-prod.firebaseio.com",
projectId: "imex-prod",
storageBucket: "imex-prod.appspot.com",
messagingSenderId: "253497221485",
appId: "1:253497221485:web:3c81c483b94db84b227a64",
measurementId: "G-NTWBKG2L0M",
});
// Initialize the Firebase app in the service worker by passing the generated config
let firebaseConfig;
switch (this.location.hostname) {
case "localhost":
firebaseConfig = {
apiKey: "AIzaSyDPLT8GiDHDR1R4nI66Qi0BY1aYviDPioc",
authDomain: "imex-dev.firebaseapp.com",
databaseURL: "https://imex-dev.firebaseio.com",
projectId: "imex-dev",
storageBucket: "imex-dev.appspot.com",
messagingSenderId: "759548147434",
appId: "1:759548147434:web:e8239868a48ceb36700993",
measurementId: "G-K5XRBVVB4S",
};
break;
case "test.imex.online":
firebaseConfig = {
apiKey: "AIzaSyBw7_GTy7GtQyfkIRPVrWHEGKfcqeyXw0c",
authDomain: "imex-test.firebaseapp.com",
projectId: "imex-test",
storageBucket: "imex-test.appspot.com",
messagingSenderId: "991923618608",
appId: "1:991923618608:web:633437569cdad78299bef5",
// measurementId: "${config.measurementId}",
};
break;
case "imex.online":
default:
firebaseConfig = {
apiKey: "AIzaSyDSezy-jGJreo7ulgpLdlpOwAOrgcaEkhU",
authDomain: "imex-prod.firebaseapp.com",
databaseURL: "https://imex-prod.firebaseio.com",
projectId: "imex-prod",
storageBucket: "imex-prod.appspot.com",
messagingSenderId: "253497221485",
appId: "1:253497221485:web:3c81c483b94db84b227a64",
measurementId: "G-NTWBKG2L0M",
};
}
// firebase.initializeApp({
// apiKey: "AIzaSyDPLT8GiDHDR1R4nI66Qi0BY1aYviDPioc",
// authDomain: "imex-dev.firebaseapp.com",
// databaseURL: "https://imex-dev.firebaseio.com",
// projectId: "imex-dev",
// storageBucket: "imex-dev.appspot.com",
// messagingSenderId: "759548147434",
// appId: "1:759548147434:web:e8239868a48ceb36700993",
// measurementId: "G-K5XRBVVB4S",
// });
firebase.initializeApp(firebaseConfig);
// Retrieve firebase messaging
const messaging = firebase.messaging();
self.addEventListener("fetch", (fetch) => {
//required for installation as a PWA. Can ignore for now.
//console.log("fetch", fetch);
});
messaging.onBackgroundMessage(function (payload) {
console.log("FCM BG MSG", payload);
// Customize notification here
const channel = new BroadcastChannel("imex-sw-messages");
channel.postMessage(payload);
messaging.setBackgroundMessageHandler(function (payload) {
return self.registration.showNotification(
"[SW]" + payload.notification.title,
payload.notification
);
});
//Handles the notification getting clicked.
self.addEventListener("notificationclick", function (event) {
console.log("SW notificationclick", event);
// event.notification.close();
if (event.action === "archive") {
// Archive action was clicked
archiveEmail();
} else {
// Main body of notification was clicked
clients.openWindow("/inbox");
}
//self.registration.showNotification(notificationTitle, notificationOptions);
});

View File

@@ -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);

View File

@@ -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>
);
}

View File

@@ -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>
) : (

View File

@@ -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"

View File

@@ -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}
/>
);

View File

@@ -46,7 +46,7 @@ export function ChatNewConversation({ openChatByPhone }) {
return (
<Popover trigger="click" content={popContent}>
<PlusCircleFilled style={{ margin: "1rem" }} />
<PlusCircleFilled />
</Popover>
);
}

View File

@@ -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);

View File

@@ -2,8 +2,7 @@ import { getAnalytics, logEvent } from "firebase/analytics";
import { initializeApp } from "firebase/app";
import { getAuth, updatePassword, updateProfile } from "firebase/auth";
import { getFirestore } from "firebase/firestore";
import { tracker } from "../App/App.container";
//import { getMessaging } from "firebase/messaging";
import { getMessaging, getToken, onMessage } from "firebase/messaging";
import { store } from "../redux/store";
const config = JSON.parse(process.env.REACT_APP_FIREBASE_CONFIG);
@@ -40,17 +39,34 @@ export const updateCurrentPassword = async (password) => {
return updatePassword(currentUser, password);
};
//let messaging;
// try {
// messaging = getMessaging();
// // Project Settings => Cloud Messaging => Web Push certificates
// messaging.usePublicVapidKey(process.env.REACT_APP_FIREBASE_PUBLIC_VAPID_KEY);
// console.log("[FCM UTIL] FCM initialized successfully.");
// } catch {
// console.log("[FCM UTIL] Firebase Messaging is likely unsupported.");
// }
export const messaging = getMessaging();
// export { messaging };
export const requestForToken = () => {
return getToken(messaging, {
vapidKey: process.env.REACT_APP_FIREBASE_PUBLIC_VAPID_KEY,
})
.then((currentToken) => {
if (currentToken) {
console.log("current token for client: ", currentToken);
// Perform any other necessary action with the token
} else {
// Show permission request UI
console.log(
"No registration token available. Request permission to generate one."
);
}
})
.catch((err) => {
console.log("An error occurred while retrieving token. ", err);
});
};
export const onMessageListener = () =>
new Promise((resolve) => {
onMessage(messaging, (payload) => {
console.log("Inbound FCM Message", payload);
resolve(payload);
});
});
export const logImEXEvent = (eventName, additionalParams, stateProp = null) => {
const state = stateProp || store.getState();
@@ -70,9 +86,6 @@ export const logImEXEvent = (eventName, additionalParams, stateProp = null) => {
// eventParams
// );
logEvent(analytics, eventName, eventParams);
//Log event to OpenReplay server.
tracker.event(eventName, eventParams);
};
// if (messaging) {

View File

@@ -1,15 +1,54 @@
import { gql } from "@apollo/client";
export const CONVERSATION_LIST_SUBSCRIPTION = gql`
subscription CONVERSATION_LIST_SUBSCRIPTION {
// export const CONVERSATION_LIST_SUBSCRIPTION = gql`
// subscription CONVERSATION_LIST_SUBSCRIPTION {
// conversations(
// order_by: { updated_at: desc }
// limit: 50
// where: { archived: { _eq: false } }
// ) {
// phone_num
// id
// updated_at
// unreadcnt
// messages_aggregate(
// where: { read: { _eq: false }, isoutbound: { _eq: false } }
// ) {
// aggregate {
// count
// }
// }
// job_conversations {
// job {
// id
// ro_number
// ownr_fn
// ownr_ln
// ownr_co_nm
// }
// }
// }
// }
// `;
export const CONVERSATION_LIST_QUERY = gql`
query CONVERSATION_LIST_QUERY {
conversations(
order_by: { updated_at: desc }
limit: 100
limit: 50
where: { archived: { _eq: false } }
) {
phone_num
id
updated_at
unreadcnt
messages_aggregate(
where: { read: { _eq: false }, isoutbound: { _eq: false } }
) {
aggregate {
count
}
}
job_conversations {
job {
id
@@ -17,17 +56,6 @@ export const CONVERSATION_LIST_SUBSCRIPTION = gql`
ownr_fn
ownr_ln
ownr_co_nm
owner {
id
allow_text_message
}
}
}
messages_aggregate(
where: { read: { _eq: false }, isoutbound: { _eq: false } }
) {
aggregate {
count
}
}
}
@@ -36,24 +64,26 @@ export const CONVERSATION_LIST_SUBSCRIPTION = gql`
export const CONVERSATION_SUBSCRIPTION_BY_PK = gql`
subscription CONVERSATION_SUBSCRIPTION_BY_PK($conversationId: uuid!) {
messages(
order_by: { created_at: asc_nulls_first }
where: { conversationid: { _eq: $conversationId } }
) {
id
status
text
isoutbound
image
image_path
userid
created_at
read
}
}
`;
export const GET_CONVERSATION_DETAILS = gql`
query GET_CONVERSATION_DETAILS($conversationId: uuid!) {
conversations_by_pk(id: $conversationId) {
messages(order_by: { created_at: asc_nulls_first }) {
id
status
text
isoutbound
image
image_path
userid
created_at
}
messages_aggregate(
where: { read: { _eq: false }, isoutbound: { _eq: false } }
) {
aggregate {
count
}
}
id
phone_num
archived

View File

@@ -8,6 +8,8 @@ export const MARK_MESSAGES_AS_READ_BY_CONVERSATION = gql`
) {
returning {
id
read
isoutbound
}
}
}

View File

@@ -5,13 +5,12 @@ import preval from "preval.macro";
import React, { lazy, Suspense, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import ErrorBoundary from "../../components/error-boundary/error-boundary.component";
import { Link, Route, Switch } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import BreadCrumbs from "../../components/breadcrumbs/breadcrumbs.component";
import ChatAffixContainer from "../../components/chat-affix/chat-affix.container";
import ConflictComponent from "../../components/conflict/conflict.component";
import FcmNotification from "../../components/fcm-notification/fcm-notification.component";
import ErrorBoundary from "../../components/error-boundary/error-boundary.component";
//import FooterComponent from "../../components/footer/footer.component";
//Component Imports
import HeaderContainer from "../../components/header/header.container";
@@ -20,12 +19,11 @@ import PartnerPingComponent from "../../components/partner-ping/partner-ping.com
import PrintCenterModalContainer from "../../components/print-center-modal/print-center-modal.container";
import ShopSubStatusComponent from "../../components/shop-sub-status/shop-sub-status.component";
import TestComponent from "../../components/_test/test.component";
import { QUERY_STRIPE_ID } from "../../graphql/bodyshop.queries";
import { requestForToken } from "../../firebase/firebase.utils";
import {
selectBodyshop,
selectInstanceConflict,
} from "../../redux/user/user.selectors";
import client from "../../utils/GraphQLClient";
import "./manage.page.styles.scss";
const ManageRootPage = lazy(() =>
@@ -166,16 +164,15 @@ const Dms = lazy(() => import("../dms/dms.container"));
const { Content, Footer } = Layout;
const stripePromise = new Promise((resolve, reject) => {
client.query({ query: QUERY_STRIPE_ID }).then((resp) => {
if (resp.data.bodyshops[0])
resolve(
loadStripe(process.env.REACT_APP_STRIPE_PUBLIC_KEY, {
stripeAccount:
resp.data.bodyshops[0].stripe_acct_id || "No Stripe Id Resolve",
})
);
reject();
});
// client.query({ query: QUERY_STRIPE_ID }).then((resp) => {
// if (resp.data.bodyshops[0])
resolve(
loadStripe(process.env.REACT_APP_STRIPE_PUBLIC_KEY, {
stripeAccount: "No Stripe Id Resolve",
})
);
// reject();
// });
});
const mapStateToProps = createStructuredSelector({
@@ -188,7 +185,9 @@ export function Manage({ match, conflict, bodyshop }) {
useEffect(() => {
const widgetId = "IABVNO4scRKY11XBQkNr";
window.noticeable.render("widget", widgetId);
requestForToken();
}, []);
useEffect(() => {
document.title = t("titles.app");
}, [t]);
@@ -395,7 +394,6 @@ export function Manage({ match, conflict, bodyshop }) {
<HeaderContainer />
<Content className="content-container">
<FcmNotification />
<PartnerPingComponent />
<ErrorBoundary>{PageContent}</ErrorBoundary>
<BackTop />

View File

@@ -5,7 +5,7 @@ import { connect } from "react-redux";
import { Redirect, Route, Switch } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import ErrorBoundary from "../../components/error-boundary/error-boundary.component";
import FcmNotification from "../../components/fcm-notification/fcm-notification.component";
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
import TechHeader from "../../components/tech-header/tech-header.component";
import TechSider from "../../components/tech-sider/tech-sider.component";
@@ -58,7 +58,6 @@ export function TechPage({ technician, match }) {
{technician ? null : <Redirect to={`${match.path}/login`} />}
<TechHeader />
<Content className="tech-content-container">
<FcmNotification />
<ErrorBoundary>
<Suspense
fallback={

View File

@@ -899,10 +899,12 @@
"selectall": "Select All",
"senderrortosupport": "Send Error to Support",
"submit": "Submit",
"tryagain": "Try Again",
"view": "View",
"viewreleasenotes": "See What's Changed"
},
"errors": {
"fcm": "You must allow notification permissions to have real time messaging. Click to try again.",
"notfound": "No record was found."
},
"itemtypes": {
@@ -925,6 +927,7 @@
"exceptiontitle": "An error has occurred.",
"friday": "Friday",
"globalsearch": "Global Search",
"help": "Help",
"hours": "hrs",
"in": "In",
"instanceconflictext": "Your $t(titles.app) account can only be used on one device at any given time. Refresh your session to take control.",

View File

@@ -899,10 +899,12 @@
"selectall": "",
"senderrortosupport": "",
"submit": "",
"tryagain": "",
"view": "",
"viewreleasenotes": ""
},
"errors": {
"fcm": "",
"notfound": ""
},
"itemtypes": {
@@ -925,6 +927,7 @@
"exceptiontitle": "",
"friday": "",
"globalsearch": "",
"help": "",
"hours": "",
"in": "en",
"instanceconflictext": "",

View File

@@ -899,10 +899,12 @@
"selectall": "",
"senderrortosupport": "",
"submit": "",
"tryagain": "",
"view": "",
"viewreleasenotes": ""
},
"errors": {
"fcm": "",
"notfound": ""
},
"itemtypes": {
@@ -925,6 +927,7 @@
"exceptiontitle": "",
"friday": "",
"globalsearch": "",
"help": "",
"hours": "",
"in": "dans",
"instanceconflictext": "",

View File

@@ -91,13 +91,11 @@ export default function CiecaSelect(parts = true, labor = true) {
}
export function GetPartTypeName(part_type) {
console.log(part_type);
if (!part_type) return null;
return i18n.t(`joblines.fields.part_types.${part_type.toUpperCase()}`);
}
export function Get(part_type) {
console.log(part_type);
if (!part_type) return null;
return i18n.t(`joblines.fields.part_types.${part_type.toUpperCase()}`);
}

View File

@@ -50,13 +50,18 @@ const roundTripLink = new ApolloLink((operation, forward) => {
const TrackExecutionTime = async (operationName, time) => {
const rdxStore = store.getState();
try {
console.log("trying");
axios.post("/ioevent", {
operationName,
time,
dbevent: true,
user: rdxStore.user.currentUser.email,
imexshopid: rdxStore.user.bodyshop.imexshopid,
user:
rdxStore.user &&
rdxStore.user.currentUser &&
rdxStore.user.currentUser.email,
imexshopid:
rdxStore.user &&
rdxStore.user.bodyshop &&
rdxStore.user.bodyshop.imexshopid,
});
} catch (error) {
console.log("IOEvent Error", error);

View File

@@ -148,8 +148,6 @@ export async function RenderTemplates(
},
};
console.log("reportRequest", reportRequest);
try {
const render = await jsreport.renderAsync(reportRequest);
if (!renderAsHtml) {

View File

@@ -0,0 +1,34 @@
export default async function FcmHandler({ client, payload }) {
console.log("Handling payload type", payload);
switch (payload.type) {
case "messaging-inbound":
client.cache.modify({
id: client.cache.identify({
__typename: "conversations",
id: payload.conversationid,
}),
fields: {
messages_aggregate(cached) {
return { aggregate: { count: cached.aggregate.count + 1 } };
},
},
});
break;
case "messaging-mark-conversation-read":
client.cache.modify({
id: client.cache.identify({
__typename: "conversations",
id: payload.conversationid,
}),
fields: {
messages_aggregate(cached) {
return { aggregate: { count: 0 } };
},
},
});
break;
default:
console.log("No payload type set.");
break;
}
}

View File

@@ -1172,11 +1172,12 @@
- active:
_eq: true
columns:
- id
- created_at
- updated_at
- bodyshopid
- created_at
- id
- phone_num
- unreadcnt
- updated_at
select_permissions:
- role: user
permission:
@@ -1186,6 +1187,7 @@
- created_at
- id
- phone_num
- unreadcnt
- updated_at
filter:
bodyshop:
@@ -1206,6 +1208,7 @@
- created_at
- id
- phone_num
- unreadcnt
- updated_at
filter:
bodyshop:

View File

@@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."conversations" add column "unreadcnt" numeric
-- not null default '0';

View File

@@ -0,0 +1,2 @@
alter table "public"."conversations" add column "unreadcnt" numeric
not null default '0';

View File

@@ -66,7 +66,6 @@ app.get("/test", async function (req, res) {
"git rev-parse --short HEAD"
);
logger.log("test-api-status", "DEBUG", "api", { commit });
res.status(200).send(`OK - ${commit}`);
});
@@ -114,6 +113,7 @@ app.post(
twilio.webhook({ validate: process.env.NODE_ENV === "PRODUCTION" }),
smsStatus.status
);
app.post("/sms/markConversationRead", smsStatus.markConversationRead);
var job = require("./server/job/job");
app.post("/job/totals", fb.validateFirebaseIdToken, job.totals);
@@ -133,9 +133,10 @@ app.post("/render/inlinecss", fb.validateFirebaseIdToken, inlineCss.inlinecss);
app.post(
"/notifications/send",
fb.validateFirebaseIdToken,
fb.sendNotification
);
app.post("/notifications/subscribe", fb.validateFirebaseIdToken, fb.subscribe);
app.post("/adm/updateuser", fb.validateFirebaseIdToken, fb.updateUser);
//Stripe Processing

View File

@@ -74,31 +74,48 @@ exports.updateUser = (req, res) => {
});
};
exports.sendNotification = (req, res) => {
var registrationToken =
"fqIWg8ENDFyrRrMWJ1sItR:APA91bHirdZ05Zo66flMlvala97SMXoiQGwP4oCvMwd-vVrSauD_WoNim3kXHGqyP-bzENjkXwA5icyUAReFbeHn6dIaPcbpcsXuY73-eJAXvZiu1gIsrd1BOsnj3dEMT7Q4F6mTPth1";
var message = {
notification: { title: "The Title", body: "The Body" },
data: {
jobid: "1234",
},
token: registrationToken,
};
exports.sendNotification = async (req, res) => {
setTimeout(() => {
// Send a message to the device corresponding to the provided
// registration token.
admin
.messaging()
.send({
topic: "PRD_PATRICK-messaging",
notification: {
title: `ImEX Online Message - +16049992002"`,
body: "Test Noti.",
imageUrl: "https://thinkimex.com/img/logo512.png",
},
data: {
type: "messaging-inbound",
conversationid: "e0eb17c3-3a78-4e3f-b932-55ef35aa2297",
text: "Hello. ",
image_path: "",
phone_num: "+16049992002",
},
})
.then((response) => {
// Response is a message ID string.
console.log("Successfully sent message:", response);
})
.catch((error) => {
console.log("Error sending message:", error);
});
// Send a message to the device corresponding to the provided
// registration token.
admin
res.sendStatus(200);
}, 500);
};
exports.subscribe = async (req, res) => {
const result = await admin
.messaging()
.send(message)
.then((response) => {
// Response is a message ID string.
console.log("Successfully sent message:", response);
})
.catch((error) => {
console.log("Error sending message:", error);
});
.subscribeToTopic(
req.body.fcm_tokens,
`${req.body.imexshopid}-${req.body.type}`
);
res.sendStatus(200);
res.json(result);
};
exports.validateFirebaseIdToken = async (req, res, next) => {

View File

@@ -22,48 +22,74 @@ mutation UNARCHIVE_CONVERSATION($id: uuid!) {
exports.RECEIVE_MESSAGE = `
mutation RECEIVE_MESSAGE($msg: [messages_insert_input!]!) {
insert_messages(objects: $msg) {
insert_messages(objects: $msg) {
returning {
conversation {
id
archived
bodyshop {
associations(where: {active: {_eq: true}}) {
user {
fcmtokens
}
}
bodyshop{
imexshopid
}
created_at
updated_at
unreadcnt
phone_num
}
conversationid
created_at
id
image_path
image
isoutbound
msid
read
text
updated_at
status
userid
}
}
}
`;
exports.INSERT_MESSAGE = `
mutation INSERT_MESSAGE($msg: [messages_insert_input!]!, $conversationid: uuid!) {
update_conversations_by_pk(pk_columns: {id: $conversationid}, _set: {archived: false}) {
id
archived
}
insert_messages(objects: $msg) {
insert_messages(objects: $msg) {
returning {
conversation {
id
archived
bodyshop {
associations(where: {active: {_eq: true}}) {
user {
fcmtokens
}
}
bodyshop{
imexshopid
}
created_at
updated_at
unreadcnt
phone_num
}
conversationid
created_at
id
image_path
image
isoutbound
msid
read
text
updated_at
status
userid
}
}
}
`;
exports.UPDATE_MESSAGE_STATUS = `

View File

@@ -9,7 +9,7 @@ require("dotenv").config({
const client = require("../graphql-client/graphql-client").client;
const queries = require("../graphql-client/queries");
const { phone } = require("phone");
const admin = require("../firebase/firebase-handler").admin;
const { admin } = require("../firebase/firebase-handler");
const logger = require("../utils/logger");
exports.receive = async (req, res) => {
//Perform request validation
@@ -78,21 +78,43 @@ exports.receive = async (req, res) => {
});
}
try {
let insertresp;
if (response.bodyshops[0].conversations[0]) {
const r3 = await client.request(queries.INSERT_MESSAGE, {
insertresp = await client.request(queries.INSERT_MESSAGE, {
msg: newMessage,
conversationid:
response.bodyshops[0].conversations[0] &&
response.bodyshops[0].conversations[0].id,
});
} else {
const r2 = await client.request(queries.RECEIVE_MESSAGE, {
insertresp = await client.request(queries.RECEIVE_MESSAGE, {
msg: newMessage,
});
}
const message = insertresp.insert_messages.returning[0];
const data = {
type: "messaging-inbound",
conversationid: message.conversationid || "",
text: message.text || "",
image_path: message.image_path || "",
image: (message.image && message.image.toString()) || "",
messageid: message.id || "",
phone_num: message.conversation.phone_num || "",
};
const fcmresp = await admin.messaging().send({
topic: `${message.conversation.bodyshop.imexshopid}-messaging`,
notification: {
title: `ImEX Online Message - ${data.phone_num}`,
body: message.image_path ? `Image ${message.text}` : message.text,
imageUrl: "https://thinkimex.com/img/logo512.png",
},
data,
});
logger.log("sms-inbound-success", "DEBUG", "api", null, {
newMessage,
fcmresp,
});
res.status(200).send("");
} catch (e2) {

View File

@@ -10,6 +10,7 @@ const client = require("../graphql-client/graphql-client").client;
const queries = require("../graphql-client/queries");
const { phone } = require("phone");
const logger = require("../utils/logger");
const { admin } = require("../firebase/firebase-handler");
exports.status = (req, res) => {
const { SmsSid, SmsStatus } = req.body;
@@ -34,6 +35,23 @@ exports.status = (req, res) => {
res.sendStatus(200);
};
exports.markConversationRead = async (req, res) => {
const { conversationid, imexshopid } = req.body;
admin.messaging().send({
topic: `${imexshopid}-messaging`,
// notification: {
// title: `ImEX Online Message - ${data.phone_num}`,
// body: message.image_path ? `Image ${message.text}` : message.text,
// imageUrl: "https://thinkimex.com/img/logo512.png",
// },
data: {
type: "messaging-mark-conversation-read",
conversationid: conversationid || "",
},
});
res.send(200);
};
// Inbound Sample
// {
// "SmsSid": "SM5205ea340e06437799d9345e7283457c",

View File

@@ -5,13 +5,14 @@ const logger = new graylog2.graylog({
});
function log(message, type, user, record, object) {
console.log(message, {
type,
env: process.env.NODE_ENV || "development",
user,
record,
...object,
});
if (type !== "ioevent")
console.log(message, {
type,
env: process.env.NODE_ENV || "development",
user,
record,
...object,
});
logger.log(message, {
type,
env: process.env.NODE_ENV || "development",