diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index 9a26f65f5..1bf35685a 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -10168,6 +10168,32 @@ messaging + + actions + + + link + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + labels diff --git a/client/package.json b/client/package.json index ca531d8df..eb77df85c 100644 --- a/client/package.json +++ b/client/package.json @@ -43,6 +43,7 @@ "react-router-dom": "^5.1.2", "react-scripts": "3.4.1", "react-trello": "^2.2.5", + "react-virtualized": "^9.21.2", "redux": "^4.0.5", "redux-persist": "^6.0.0", "redux-saga": "^1.1.3", diff --git a/client/src/components/chat-conversation-title-tags/chat-conversation-title-tags.component.jsx b/client/src/components/chat-conversation-title-tags/chat-conversation-title-tags.component.jsx index f185aaa14..5eab7d6cb 100644 --- a/client/src/components/chat-conversation-title-tags/chat-conversation-title-tags.component.jsx +++ b/client/src/components/chat-conversation-title-tags/chat-conversation-title-tags.component.jsx @@ -1,15 +1,37 @@ import React from "react"; import { Tag } from "antd"; import { Link } from "react-router-dom"; +import { useMutation } from "@apollo/react-hooks"; +import { REMOVE_CONVERSATION_TAG } from "../../graphql/job-conversations.queries"; + export default function ChatConversationTitleTags({ jobConversations }) { + const [removeJobConversation] = useMutation(REMOVE_CONVERSATION_TAG); + + const handleRemoveTag = (jobId) => { + const convId = jobConversations[0].conversationid; + if (!!convId) { + removeJobConversation({ + variables: { + conversationId: convId, + jobId: jobId, + }, + }); + } + }; + return (
{jobConversations.map((item) => ( - - + handleRemoveTag(item.job.id)}> + {item.job.ro_number || "?"} - - + + ))}
); diff --git a/client/src/components/chat-conversation/chat-conversation.component.jsx b/client/src/components/chat-conversation/chat-conversation.component.jsx index 5ff01b68a..fcb42d5c5 100644 --- a/client/src/components/chat-conversation/chat-conversation.component.jsx +++ b/client/src/components/chat-conversation/chat-conversation.component.jsx @@ -6,7 +6,11 @@ import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component.jsx" import AlertComponent from "../alert/alert.component"; import ChatConversationTitle from "../chat-conversation-title/chat-conversation-title.component"; -export default function ChatConversationComponent({ subState, conversation }) { +export default function ChatConversationComponent({ + subState, + conversation, + handleMarkConversationAsRead, +}) { const [loading, error] = subState; if (loading) return ; @@ -20,12 +24,13 @@ export default function ChatConversationComponent({ subState, conversation }) { conversation.messages_aggregate.aggregate.count) || 0; - const messages = - (conversation && conversation.messages) || - []; + const messages = (conversation && conversation.messages) || []; return ( -
+
diff --git a/client/src/components/chat-conversation/chat-conversation.container.jsx b/client/src/components/chat-conversation/chat-conversation.container.jsx index 22b88e4df..603848fa3 100644 --- a/client/src/components/chat-conversation/chat-conversation.container.jsx +++ b/client/src/components/chat-conversation/chat-conversation.container.jsx @@ -1,11 +1,11 @@ -import { useSubscription } from "@apollo/react-hooks"; +import { useMutation, useSubscription } from "@apollo/react-hooks"; import React from "react"; -import { CONVERSATION_SUBSCRIPTION_BY_PK } from "../../graphql/conversations.queries"; -import ChatConversationComponent from "./chat-conversation.component"; - import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; +import { CONVERSATION_SUBSCRIPTION_BY_PK } 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"; const mapStateToProps = createStructuredSelector({ selectedConversation: selectSelectedConversation, }); @@ -13,10 +13,6 @@ const mapStateToProps = createStructuredSelector({ export default connect(mapStateToProps, null)(ChatConversationContainer); export function ChatConversationContainer({ selectedConversation }) { - console.log( - "ChatConversationContainer -> selectedConversation", - selectedConversation - ); const { loading, error, data } = useSubscription( CONVERSATION_SUBSCRIPTION_BY_PK, { @@ -24,12 +20,33 @@ export function ChatConversationContainer({ selectedConversation }) { } ); + const [markConversationRead] = useMutation( + MARK_MESSAGES_AS_READ_BY_CONVERSATION, + { + variables: { conversationId: selectedConversation }, + } + ); + + 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; + + const handleMarkConversationAsRead = () => { + if (unreadCount > 0 && !!selectedConversation) { + markConversationRead(); + } + }; + return ( ); } diff --git a/client/src/components/chat-messages-list/chat-message-list.component.jsx b/client/src/components/chat-messages-list/chat-message-list.component.jsx index 59a7a04f6..308c465f0 100644 --- a/client/src/components/chat-messages-list/chat-message-list.component.jsx +++ b/client/src/components/chat-messages-list/chat-message-list.component.jsx @@ -1,44 +1,68 @@ import { CheckCircleOutlined, CheckOutlined } from "@ant-design/icons"; import React, { useEffect, useRef } from "react"; import "./chat-message-list.styles.scss"; +import { List, CellMeasurer, CellMeasurerCache } from "react-virtualized"; export default function ChatMessageListComponent({ messages }) { - const messagesEndRef = useRef(null); + const virtualizedListRef = useRef(null); + + const _cache = new CellMeasurerCache({ + fixedWidth: true, + minHeight: 20, + }); const scrollToBottom = () => { - !!messagesEndRef.current && - messagesEndRef.current.scrollIntoView({ behavior: "smooth" }); + console.log("SCrolling to", messages.length); + !!virtualizedListRef.current && + virtualizedListRef.current.scrollToRow(messages.length - 1); + + //TODO Outstanding isue on virtualization: https://github.com/bvaughn/react-virtualized/issues/1179 + //Scrolling does not work on this version of React. }; useEffect(scrollToBottom, [messages]); - const StatusRender = (status) => { - switch (status) { - case "sent": - return ; - case "delivered": - return ( - - ); - default: - return null; - } + + const _rowRenderer = ({ index, key, parent, style }) => { + return ( + + {({ measure, registerChild }) => ( +
  • +

    + {messages[index].text} + {StatusRender(messages[index].status)} +

    +
  • + )} +
    + ); }; return (
      - {messages.map((item) => ( -
    • -

      - {item.text} - {StatusRender(item.status)} -

      -
    • - ))} -
    • +
    ); } + +const StatusRender = (status) => { + switch (status) { + case "sent": + return ; + case "delivered": + return ; + default: + return null; + } +}; diff --git a/client/src/components/chat-messages-list/chat-message-list.styles.scss b/client/src/components/chat-messages-list/chat-message-list.styles.scss index 57129a7b9..3b3461870 100644 --- a/client/src/components/chat-messages-list/chat-message-list.styles.scss +++ b/client/src/components/chat-messages-list/chat-message-list.styles.scss @@ -1,22 +1,22 @@ -.messages { - height: 350px; - min-height: calc(100% - 10px); - max-height: calc(100% - 93px); - overflow-y: scroll; - overflow-x: hidden; -} -@media screen and (max-width: 735px) { - .messages { - max-height: calc(100% - 105px); - } -} -.messages::-webkit-scrollbar { - width: 8px; - background: transparent; -} -.messages::-webkit-scrollbar-thumb { - background-color: rgba(0, 0, 0, 0.3); -} +// .messages { +// height: 300px; +// min-height: calc(100% - 10px); +// max-height: calc(100% - 93px); +// overflow-y: scroll; +// overflow-x: hidden; +// } +// @media screen and (max-width: 735px) { +// .messages { +// max-height: calc(100% - 105px); +// } +// } +// .messages::-webkit-scrollbar { +// width: 8px; +// background: transparent; +// } +// .messages::-webkit-scrollbar-thumb { +// background-color: rgba(0, 0, 0, 0.3); +// } .messages ul li { display: inline-block; clear: both; diff --git a/client/src/components/chat-tag-ro/chat-tag-ro.component.jsx b/client/src/components/chat-tag-ro/chat-tag-ro.component.jsx index d6f6497c1..e32acd0b7 100644 --- a/client/src/components/chat-tag-ro/chat-tag-ro.component.jsx +++ b/client/src/components/chat-tag-ro/chat-tag-ro.component.jsx @@ -1,13 +1,18 @@ import React from "react"; import { AutoComplete } from "antd"; -import { LoadingOutlined } from "@ant-design/icons"; +import { LoadingOutlined, CloseCircleOutlined } from "@ant-design/icons"; +import { useTranslation } from "react-i18next"; + export default function ChatTagRoComponent({ searchQueryState, roOptions, loading, executeSearch, handleInsertTag, + setVisible, }) { + const { t } = useTranslation(); + const setSearchQuery = searchQueryState[1]; const handleSearchQuery = (value) => { setSearchQuery(value); @@ -20,19 +25,26 @@ export default function ChatTagRoComponent({ }; return ( - : null} - style={{ width: 200 }} - onSearch={handleSearchQuery} - onSelect={handleInsertTag} - onKeyDown={handleKeyDown}> - {roOptions.map((item, idx) => ( - - {` ${item.ro_number || ""} | ${item.ownr_fn || ""} ${ - item.ownr_ln || "" - }`} - - ))} - + + + {roOptions.map((item, idx) => ( + + {` ${item.ro_number || ""} | ${item.ownr_fn || ""} ${ + item.ownr_ln || "" + }`} + + ))} + + {loading ? ( + + ) : ( + setVisible(false)} /> + )} + ); } diff --git a/client/src/components/chat-tag-ro/chat-tag-ro.container.jsx b/client/src/components/chat-tag-ro/chat-tag-ro.container.jsx index 45c63a599..81000d12e 100644 --- a/client/src/components/chat-tag-ro/chat-tag-ro.container.jsx +++ b/client/src/components/chat-tag-ro/chat-tag-ro.container.jsx @@ -3,9 +3,14 @@ import ChatTagRo from "./chat-tag-ro.component"; import { useLazyQuery, useMutation } from "@apollo/react-hooks"; import { SEARCH_FOR_JOBS } from "../../graphql/jobs.queries"; import { INSERT_CONVERSATION_TAG } from "../../graphql/job-conversations.queries"; +import { Tag } from "antd"; +import { useTranslation } from "react-i18next"; +import { PlusOutlined } from "@ant-design/icons"; + export default function ChatTagRoContainer({ conversation }) { console.log("ChatTagRoContainer -> conversation", conversation); - + const { t } = useTranslation(); + const [visible, setVisible] = useState(false); const searchQueryState = useState(""); const searchText = searchQueryState[0]; @@ -28,19 +33,33 @@ export default function ChatTagRoContainer({ conversation }) { }); const handleInsertTag = (value, option) => { - console.log("value, option", value, option); insertTag({ variables: { jobId: option.key } }); + setVisible(false); }; + const existingJobTags = conversation.job_conversations.map((i) => i.jobid); + + const roOptions = data + ? data.jobs.filter((job) => !existingJobTags.includes(job.id)) + : []; + return (
    - + {visible ? ( + + ) : ( + setVisible(true)}> + + {t("messaging.actions.link")} + + )}
    ); } diff --git a/client/src/graphql/conversations.queries.js b/client/src/graphql/conversations.queries.js index a34415f0d..17d4b9f11 100644 --- a/client/src/graphql/conversations.queries.js +++ b/client/src/graphql/conversations.queries.js @@ -19,7 +19,7 @@ export const CONVERSATION_LIST_SUBSCRIPTION = gql` export const CONVERSATION_SUBSCRIPTION_BY_PK = gql` subscription CONVERSATION_SUBSCRIPTION_BY_PK($conversationId: uuid!) { conversations_by_pk(id: $conversationId) { - messages { + messages(order_by: { created_at: asc_nulls_first }) { id status text diff --git a/client/src/graphql/job-conversations.queries.js b/client/src/graphql/job-conversations.queries.js index d0072aee0..41024075c 100644 --- a/client/src/graphql/job-conversations.queries.js +++ b/client/src/graphql/job-conversations.queries.js @@ -12,3 +12,18 @@ export const INSERT_CONVERSATION_TAG = gql` } } `; + +export const REMOVE_CONVERSATION_TAG = gql` + mutation REMOVE_CONVERSATION_TAG($conversationId: uuid!, $jobId: uuid!) { + delete_job_conversations( + where: { + _and: { + jobid: { _eq: $jobId } + conversationid: { _eq: $conversationId } + } + } + ) { + affected_rows + } + } +`; diff --git a/client/src/graphql/messages.queries.js b/client/src/graphql/messages.queries.js new file mode 100644 index 000000000..09de83a9a --- /dev/null +++ b/client/src/graphql/messages.queries.js @@ -0,0 +1,14 @@ +import { gql } from "apollo-boost"; + +export const MARK_MESSAGES_AS_READ_BY_CONVERSATION = gql` + mutation MARK_MESSAGES_AS_READ_BY_CONVERSATION($conversationId: uuid) { + update_messages( + where: { conversationid: { _eq: $conversationId } } + _set: { read: true } + ) { + returning { + id + } + } + } +`; diff --git a/client/src/redux/root.reducer.js b/client/src/redux/root.reducer.js index 4d8b164e1..7eb3709a3 100644 --- a/client/src/redux/root.reducer.js +++ b/client/src/redux/root.reducer.js @@ -10,8 +10,8 @@ import applicationReducer from "./application/application.reducer"; const persistConfig = { key: "root", storage, - //whitelist: ["user"] - blacklist: ["user", "email", "messaging", "modals"] + whitelist: ["messaging"], + blacklist: ["user", "email", "modals"], }; const rootReducer = combineReducers({ @@ -19,7 +19,7 @@ const rootReducer = combineReducers({ messaging: messagingReducer, email: emailReducer, modals: modalsReducer, - application: applicationReducer + application: applicationReducer, }); export default persistReducer(persistConfig, rootReducer); diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 9465b5037..42895c7fa 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -641,6 +641,9 @@ } }, "messaging": { + "actions": { + "link": "Link to Job" + }, "labels": { "messaging": "Messaging", "typeamessage": "Send a message..." diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 54cb5b3ec..a7e83e405 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -641,6 +641,9 @@ } }, "messaging": { + "actions": { + "link": "" + }, "labels": { "messaging": "Mensajería", "typeamessage": "Enviar un mensaje..." diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 803b2d42b..2419d027e 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -641,6 +641,9 @@ } }, "messaging": { + "actions": { + "link": "" + }, "labels": { "messaging": "Messagerie", "typeamessage": "Envoyer un message..." diff --git a/client/yarn.lock b/client/yarn.lock index b7c3bdc37..937457fb4 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -1294,6 +1294,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.8.7": + version "7.9.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.6.tgz#a9102eb5cadedf3f31d08a9ecf294af7827ea29f" + integrity sha512-64AF1xY3OAkFHqOb9s4jpgk1Mm5vDZ4L3acHvAml+53nO1XbXLuDodsVpO4OIUsmemlUHMxNdYMNJmsvOwLrvQ== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.4.0", "@babel/template@^7.8.3", "@babel/template@^7.8.6": version "7.8.6" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b" @@ -3978,7 +3985,7 @@ clone-deep@^4.0.1: kind-of "^6.0.2" shallow-clone "^3.0.0" -clsx@^1.0.4: +clsx@^1.0.1, clsx@^1.0.4: version "1.1.0" resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.0.tgz#62937c6adfea771247c34b54d320fb99624f5702" integrity sha512-3avwM37fSK5oP6M5rQ9CNe99lwxhXDOeSWVPAOYF6OazUTgZCMb0yWlJpmdD74REy1gkEaFiub2ULv4fq9GUhA== @@ -4964,6 +4971,14 @@ dom-helpers@^3.4.0: dependencies: "@babel/runtime" "^7.1.2" +dom-helpers@^5.0.0: + version "5.1.4" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.1.4.tgz#4609680ab5c79a45f2531441f1949b79d6587f4b" + integrity sha512-TjMyeVUvNEnOnhzs6uAn9Ya47GmMo3qq7m+Lr/3ON0Rs5kHvb8I+SQYjLUSYn7qhEm0QjW0yrBkvz9yOrwwz1A== + dependencies: + "@babel/runtime" "^7.8.7" + csstype "^2.6.7" + dom-helpers@^5.1.0: version "5.1.3" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.1.3.tgz#7233248eb3a2d1f74aafca31e52c5299cc8ce821" @@ -8205,7 +8220,7 @@ long@^4.0.0: resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.0, loose-envify@^1.3.1, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -11281,6 +11296,18 @@ react-trello@^2.2.5: trello-smooth-dnd "1.0.0" uuid "^3.3.2" +react-virtualized@^9.21.2: + version "9.21.2" + resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.21.2.tgz#02e6df65c1e020c8dbf574ec4ce971652afca84e" + integrity sha512-oX7I7KYiUM7lVXQzmhtF4Xg/4UA5duSA+/ZcAvdWlTLFCoFYq1SbauJT5gZK9cZS/wdYR6TPGpX/dqzvTqQeBA== + dependencies: + babel-runtime "^6.26.0" + clsx "^1.0.1" + dom-helpers "^5.0.0" + loose-envify "^1.3.0" + prop-types "^15.6.0" + react-lifecycles-compat "^3.0.4" + "react@>= 16.3", react@^16.13.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"