BOD-14 Functional 2 way messaging from app.

This commit is contained in:
Patrick Fic
2020-03-25 18:45:48 -07:00
parent 546d2d82b7
commit 4c35337d36
26 changed files with 591 additions and 64 deletions

View File

@@ -25,17 +25,17 @@ export default class AppContainer extends Component {
uri: process.env.REACT_APP_GRAPHQL_ENDPOINT_WS,
options: {
//lazy: true,
reconnect: true
// connectionParams: () => {
// const token = localStorage.getItem("token");
// if (token) {
// return {
// headers: {
// authorization: token ? `Bearer ${token}` : ""
// }
// };
// }
// }
reconnect: true,
connectionParams: () => {
const token = localStorage.getItem("token");
if (token) {
return {
headers: {
authorization: token ? `Bearer ${token}` : ""
}
};
}
}
}
});
const subscriptionMiddleware = {

View File

@@ -0,0 +1,60 @@
import { ShrinkOutlined } from "@ant-design/icons";
import { Avatar, Badge, Col, List, Row } from "antd";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { openConversation, toggleChatVisible } from "../../redux/messaging/messaging.actions";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = dispatch => ({
toggleChatVisible: () => dispatch(toggleChatVisible()),
openConversation: number => dispatch(openConversation(number))
});
export function ChatConversationListComponent({
toggleChatVisible,
conversationList,
openConversation
}) {
console.log("conversationList", conversationList);
return (
<div>
<Row>
<Col span={12}>Title</Col>
<Col span={2} offset={10}>
<ShrinkOutlined onClick={() => toggleChatVisible()} />
</Col>
</Row>
<Row>
<List
dataSource={conversationList}
renderItem={item => (
<Badge count={item.messages_aggregate.aggregate.count || 0}>
<List.Item
key={item.id}
style={{ cursor: "pointer" }}
onClick={() =>
openConversation({ phone_num: item.phone_num, id: item.id })
}>
<List.Item.Meta
avatar={
<Avatar src='https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png' />
}
title={item.phone_num}
description='Some sort of RO info? '
/>
</List.Item>
</Badge>
)}
/>
</Row>
</div>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(ChatConversationListComponent);

View File

@@ -0,0 +1,16 @@
import React from "react";
import ChatConversationListComponent from "./chat-conversation-list.component";
import { useSubscription } from "@apollo/react-hooks";
import { CONVERSATION_LIST_SUBSCRIPTION } from "../../graphql/conversations.queries";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import AlertComponent from "../alert/alert.component";
export default function ChatConversationListContainer() {
const { loading, error, data } = useSubscription(
CONVERSATION_LIST_SUBSCRIPTION
);
if (loading) return <LoadingSpinner />;
if (error) return <AlertComponent message={error.message} type='error' />;
return <ChatConversationListComponent conversationList={data.conversations || []} />;
}

View File

@@ -1,7 +1,11 @@
import { Button } from "antd";
import React from "react";
import { connect } from "react-redux";
import { closeConversation, sendMessage, toggleConversationVisible } from "../../redux/messaging/messaging.actions";
import {
closeConversation,
sendMessage,
toggleConversationVisible
} from "../../redux/messaging/messaging.actions";
import PhoneFormatter from "../../utils/PhoneFormatter";
const mapDispatchToProps = dispatch => ({
@@ -18,13 +22,13 @@ function ChatConversationClosedComponent({
}) {
return (
<div style={{ display: "flex" }}>
<div onClick={() => toggleConversationVisible(conversation.phone)}>
<PhoneFormatter>{conversation.phone}</PhoneFormatter>
<div onClick={() => toggleConversationVisible(conversation.phone_num)}>
<PhoneFormatter>{conversation.phone_num}</PhoneFormatter>
</div>
<Button
type='dashed'
shape='circle-outline'
onClick={() => closeConversation(conversation.phone)}>
onClick={() => closeConversation(conversation.phone_num)}>
X
</Button>
</div>

View File

@@ -1,4 +1,4 @@
import { Button, Card } from "antd";
import { Button, Card, Badge } from "antd";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -25,24 +25,26 @@ const mapDispatchToProps = dispatch => ({
export function ChatConversationComponent({
conversation,
toggleConversationVisible,
closeConversation
closeConversation,
messages,
subState
}) {
const messages = [];
return (
<div>
<Badge count={messages.length}>
<Card
title={
conversation.open ? (
<div style={{ display: "flex" }}>
<div
onClick={() => toggleConversationVisible(conversation.phone)}>
<PhoneFormatter>{conversation.phone}</PhoneFormatter>
onClick={() =>
toggleConversationVisible(conversation.phone_num)
}>
<PhoneFormatter>{conversation.phone_num}</PhoneFormatter>
</div>
<Button
type='danger'
shape='circle-outline'
onClick={() => closeConversation(conversation.phone)}>
onClick={() => closeConversation(conversation.phone_num)}>
X
</Button>
</div>
@@ -57,12 +59,13 @@ export function ChatConversationComponent({
<ChatConversationOpenComponent
messages={messages}
conversation={conversation}
subState={subState}
/>
) : (
<ChatConversationClosedComponent conversation={conversation} />
)}
</Card>
</div>
</Badge>
);
}

View File

@@ -1,17 +1,33 @@
import { useSubscription } from "@apollo/react-hooks";
import React from "react";
import ChatConversationComponent from "./chat-conversation.component";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { MESSAGES_SUBSCRIPTION } from "../../graphql/messages.queries";
import ChatConversationComponent from "./chat-conversation.component";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = dispatch => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export function ChatConversationContainer({ conversation }) {
console.log("conversation", conversation);
const { loading, error, data } = useSubscription(MESSAGES_SUBSCRIPTION, {
variables: { conversationId: conversation.id }
});
return (
<ChatConversationComponent
subState={[loading, error]}
conversation={conversation}
messages={(data && data.messages) || []}
/>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(function ChatConversationContainer({ conversation }) {
return <ChatConversationComponent conversation={conversation} />;
});
)(ChatConversationContainer);

View File

@@ -1,10 +1,16 @@
import React from "react";
import AlertComponent from "../alert/alert.component";
import ChatSendMessage from "../chat-send-message/chat-send-message.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
export default function ChatConversationOpenComponent({
conversation,
messages
messages,
subState
}) {
if (!!!messages) return <div>No Messages</div>;
const [loading, error] = subState;
if (loading) return <LoadingSpinner />;
if (error) return <AlertComponent message={error.message} type='error' />;
return (
<div>
@@ -12,11 +18,13 @@ export default function ChatConversationOpenComponent({
<ul>
{messages.map(item => (
<li
key={item.sid}
className={`${
item.direction === "inbound" ? "sent" : "replies"
}`}>
<p> {item.body}</p>
key={item.id}
className={`${item.isoutbound ? "replies" : "sent"}`}>
<div>
<p>
{item.text} <br /> <i>{item.status}</i>
</p>
</div>
</li>
))}
</ul>

View File

@@ -2,6 +2,8 @@ import { Badge, Card } from "antd";
import { MessageFilled } from "@ant-design/icons";
import React from "react";
import { useTranslation } from "react-i18next";
import ChatConversationListContainer from "../chat-conversation-list/chat-conversation-list.container";
export default function ChatWindowComponent({
chatVisible,
toggleChatVisible
@@ -11,19 +13,19 @@ export default function ChatWindowComponent({
<div>
<Badge count={5}>
<Card
onClick={() => toggleChatVisible()}
style={{
width: chatVisible ? "300px" : "125px",
margin: "0px 10px"
}}
size="small"
>
size='small'>
{chatVisible ? (
<div className="messages" style={{ height: "400px" }}>
List of chats here.
<div className='messages' style={{ height: "400px" }}>
<ChatConversationListContainer />
</div>
) : (
<div>
<div
style={{ cursor: "pointer" }}
onClick={() => toggleChatVisible()}>
<MessageFilled />
<strong style={{ paddingLeft: "10px" }}>
{t("messaging.labels.messaging")}

View File

@@ -12,19 +12,17 @@ import ChatOverlayComponent from "./chat-overlay.component";
const mapStateToProps = createStructuredSelector({
chatVisible: selectChatVisible,
conversations: selectConversations
activeConversations: selectConversations
});
const mapDispatchToProps = dispatch => ({
toggleChatVisible: () => dispatch(toggleChatVisible())
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(function ChatWindowContainer({
export function ChatOverlayContainer({
chatVisible,
toggleChatVisible,
conversations
activeConversations
}) {
return (
<Affix offsetBottom={0}>
@@ -35,14 +33,20 @@ export default connect(
toggleChatVisible={toggleChatVisible}
/>
</Badge>
{conversations
? conversations.map((conversation, idx) => (
<Badge key={idx} count={5}>
<ChatConversationContainer conversation={conversation} />
</Badge>
{activeConversations
? activeConversations.map(conversation => (
<ChatConversationContainer
conversation={conversation}
key={conversation.id}
/>
))
: null}
</div>
</Affix>
);
});
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(ChatOverlayContainer);

View File

@@ -19,11 +19,12 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage }) {
const handleEnter = () => {
sendMessage({
to: conversation.phone,
to: conversation.phone_num,
body: message,
messagingServiceSid: bodyshop.messagingservicesid,
conversationid: conversation.id
});
setMessage("");
};
return (
@@ -31,6 +32,7 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage }) {
<Input.TextArea
allowClear
autoSize={{ minRows: 1, maxRows: 4 }}
value={message}
placeholder={t("messaging.labels.typeamessage")}
// enterButton={}
onChange={e => setMessage(e.target.value)}

View File

@@ -9,7 +9,9 @@ const errorLink = onError(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
)
);
if (networkError) console.log(`[Network error]: ${networkError}`);
if (networkError)
console.log(`[Network error]: ${JSON.stringify(networkError)}`);
console.log(operation.getContext());
}
);

View File

@@ -0,0 +1,15 @@
import { gql } from "apollo-boost";
export const CONVERSATION_LIST_SUBSCRIPTION = gql`
subscription CONVERSATION_LIST_SUBSCRIPTION {
conversations {
phone_num
id
messages_aggregate(where: { read: { _eq: false } }) {
aggregate {
count
}
}
}
}
`;

View File

@@ -0,0 +1,13 @@
import { gql } from "apollo-boost";
export const MESSAGES_SUBSCRIPTION = gql`
subscription MESSAGES_SUBSCRIPTION($conversationId: uuid!) {
messages(where: { conversationid: { _eq: $conversationId } }) {
text
created_at
id
status
isoutbound
}
}
`;

View File

@@ -2,18 +2,20 @@ import MessagingActionTypes from "./messaging.types";
const INITIAL_STATE = {
visible: false,
unread: 0,
conversations: [
{
phone: "6049992002",
phone_num: "6049992002",
id: "519ba10d-6467-4fa5-9c22-59ae891edeb6",
open: false
},
{
phone: "6049992991",
phone_num: "6049992991",
id: "ab57deba-eeb9-40db-b5ae-23f3ce8d7c7b",
open: false
}
]
],
error: null
};
const messagingReducer = (state = INITIAL_STATE, action) => {
@@ -29,11 +31,13 @@ const messagingReducer = (state = INITIAL_STATE, action) => {
visible: true
};
case MessagingActionTypes.OPEN_CONVERSATION:
if (state.conversations.find(c => c.phone === action.payload))
if (
state.conversations.find(c => c.phone_num === action.payload.phone_num)
)
return {
...state,
conversations: state.conversations.map(c =>
c.phone === action.payload ? { ...c, open: true } : c
c.phone_num === action.payload.phone_num ? { ...c, open: true } : c
)
};
else
@@ -41,23 +45,29 @@ const messagingReducer = (state = INITIAL_STATE, action) => {
...state,
conversations: [
...state.conversations,
{ phone: action.payload, open: true }
{
phone_num: action.payload.phone_num,
id: action.payload.id,
open: true
}
]
};
case MessagingActionTypes.CLOSE_CONVERSATION:
return {
...state,
conversations: state.conversations.filter(
c => c.phone !== action.payload
c => c.phone_num !== action.payload
)
};
case MessagingActionTypes.TOGGLE_CONVERSATION_VISIBLE:
return {
...state,
conversations: state.conversations.map(c =>
c.phone === action.payload ? { ...c, open: !c.open } : c
c.phone_num === action.payload ? { ...c, open: !c.open } : c
)
};
case MessagingActionTypes.SEND_MESSAGE_FAILURE:
return { ...state, error: action.payload };
default:
return state;
}

View File

@@ -0,0 +1,5 @@
- args:
cascade: false
read_only: false
sql: ALTER TABLE "public"."messages" DROP COLUMN "read";
type: run_sql

View File

@@ -0,0 +1,6 @@
- args:
cascade: false
read_only: false
sql: ALTER TABLE "public"."messages" ADD COLUMN "read" boolean NOT NULL DEFAULT
false;
type: run_sql

View File

@@ -0,0 +1,38 @@
- args:
role: user
table:
name: messages
schema: public
type: drop_insert_permission
- args:
permission:
check:
conversation:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
columns:
- id
- created_at
- updated_at
- msid
- conversationid
- text
- image
- image_path
- isoutbound
- status
localPresets:
- key: ""
value: ""
set: {}
role: user
table:
name: messages
schema: public
type: create_insert_permission

View File

@@ -0,0 +1,39 @@
- args:
role: user
table:
name: messages
schema: public
type: drop_insert_permission
- args:
permission:
check:
conversation:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
columns:
- id
- created_at
- updated_at
- msid
- conversationid
- text
- image
- image_path
- isoutbound
- status
- read
localPresets:
- key: ""
value: ""
set: {}
role: user
table:
name: messages
schema: public
type: create_insert_permission

View File

@@ -0,0 +1,36 @@
- args:
role: user
table:
name: messages
schema: public
type: drop_select_permission
- args:
permission:
allow_aggregations: false
columns:
- conversationid
- created_at
- id
- image
- image_path
- isoutbound
- msid
- status
- text
- updated_at
computed_fields: []
filter:
conversation:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
role: user
table:
name: messages
schema: public
type: create_select_permission

View File

@@ -0,0 +1,37 @@
- args:
role: user
table:
name: messages
schema: public
type: drop_select_permission
- args:
permission:
allow_aggregations: false
columns:
- image
- isoutbound
- read
- image_path
- msid
- status
- text
- created_at
- updated_at
- conversationid
- id
computed_fields: []
filter:
conversation:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
role: user
table:
name: messages
schema: public
type: create_select_permission

View File

@@ -0,0 +1,38 @@
- args:
role: user
table:
name: messages
schema: public
type: drop_update_permission
- args:
permission:
columns:
- image
- isoutbound
- image_path
- msid
- status
- text
- created_at
- updated_at
- conversationid
- id
filter:
conversation:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
localPresets:
- key: ""
value: ""
set: {}
role: user
table:
name: messages
schema: public
type: create_update_permission

View File

@@ -0,0 +1,39 @@
- args:
role: user
table:
name: messages
schema: public
type: drop_update_permission
- args:
permission:
columns:
- image
- isoutbound
- read
- image_path
- msid
- status
- text
- created_at
- updated_at
- conversationid
- id
filter:
conversation:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
localPresets:
- key: ""
value: ""
set: {}
role: user
table:
name: messages
schema: public
type: create_update_permission

View File

@@ -0,0 +1,37 @@
- args:
role: user
table:
name: messages
schema: public
type: drop_select_permission
- args:
permission:
allow_aggregations: false
columns:
- image
- isoutbound
- read
- image_path
- msid
- status
- text
- created_at
- updated_at
- conversationid
- id
computed_fields: []
filter:
conversation:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
role: user
table:
name: messages
schema: public
type: create_select_permission

View File

@@ -0,0 +1,37 @@
- args:
role: user
table:
name: messages
schema: public
type: drop_select_permission
- args:
permission:
allow_aggregations: true
columns:
- image
- isoutbound
- read
- image_path
- msid
- status
- text
- created_at
- updated_at
- conversationid
- id
computed_fields: []
filter:
conversation:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
role: user
table:
name: messages
schema: public
type: create_select_permission

View File

@@ -0,0 +1,30 @@
- args:
role: user
table:
name: conversations
schema: public
type: drop_select_permission
- args:
permission:
allow_aggregations: false
columns:
- phone_num
- created_at
- updated_at
- bodyshopid
- id
computed_fields: []
filter:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
role: user
table:
name: conversations
schema: public
type: create_select_permission

View File

@@ -0,0 +1,30 @@
- args:
role: user
table:
name: conversations
schema: public
type: drop_select_permission
- args:
permission:
allow_aggregations: true
columns:
- phone_num
- created_at
- updated_at
- bodyshopid
- id
computed_fields: []
filter:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
role: user
table:
name: conversations
schema: public
type: create_select_permission