diff --git a/src/components/IncomingMessageList/ConversationPreviewModal.jsx b/src/components/IncomingMessageList/ConversationPreviewModal.jsx new file mode 100644 index 000000000..748f08cd5 --- /dev/null +++ b/src/components/IncomingMessageList/ConversationPreviewModal.jsx @@ -0,0 +1,178 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import gql from 'graphql-tag' +import { StyleSheet, css } from 'aphrodite' +import Dialog from 'material-ui/Dialog' +import FlatButton from 'material-ui/FlatButton' + +import loadData from '../../containers//hoc/load-data' +import wrapMutations from '../../containers/hoc/wrap-mutations' +import MessageResponse from './MessageResponse'; + +const styles = StyleSheet.create({ + conversationRow: { + color: 'white', + padding: '10px', + borderRadius: '5px', + fontWeight: 'normal', + } +}) + +class MessageList extends Component { + componentDidMount() { + this.refs.messageWindow.scrollTo(0, this.refs.messageWindow.scrollHeight) + } + + componentDidUpdate() { + this.refs.messageWindow.scrollTo(0, this.refs.messageWindow.scrollHeight) + } + + render() { + return ( +
+ {this.props.messages.map((message, index) => { + const isFromContact = message.isFromContact + const messageStyle = { + marginLeft: isFromContact ? undefined : '60px', + marginRight: isFromContact ? '60px' : undefined, + backgroundColor: isFromContact ? '#AAAAAA' : 'rgb(33, 150, 243)', + } + + return ( +

+ {message.text} +

+ ) + })} +
+ ) + } +} + +MessageList.propTypes = { + messages: PropTypes.arrayOf(PropTypes.object), +} + +class ConversationPreviewBody extends Component { + constructor(props) { + super(props) + + this.state = { + messages: props.conversation.contact.messages + } + + this.messagesChanged = this.messagesChanged.bind(this) + } + + messagesChanged(messages) { + this.setState({ messages }) + } + + render() { + return ( +
+ + +
+ ) + } +} + +ConversationPreviewBody.propTypes = { + conversation: PropTypes.object +} + +class ConversationPreviewModal extends Component { + constructor(props) { + super(props) + + this.state = { + optOutError: '' + } + } + + handleClickOptOut = async () => { + const { contact } = this.props.conversation + const optOut = { + cell: contact.cell, + assignmentId: contact.assignmentId + } + try { + const response = await this.props.mutations.createOptOut(optOut, campaignContactId) + if (response.errors) { + const errorText = response.errors.join('\n') + throw new Error(errorText) + } + } catch (error) { + this.setState({ optOutError: error.message }) + } + } + + render() { + const { conversation } = this.props, + isOpen = conversation !== undefined + + const primaryActions = [ + , + + ] + + return ( + +
+ {isOpen && } + +

{this.state.optOutError}

+
+
+
+ ) + } +} + +ConversationPreviewModal.propTypes = { + conversation: PropTypes.object, + onRequestClose: PropTypes.func +} + +const mapMutationsToProps = () => ({ + createOptOut: (optOut, campaignContactId) => ({ + mutation: gql` + mutation createOptOut($optOut: OptOutInput!, $campaignContactId: String!) { + createOptOut(optOut: $optOut, campaignContactId: $campaignContactId) { + id + optOut { + id + createdAt + } + } + } + `, + variables: { + optOut, + campaignContactId + } + }) +}) + +export default loadData(wrapMutations(ConversationPreviewModal), { + mapMutationsToProps +}) diff --git a/src/components/IncomingMessageList/MessageResponse.jsx b/src/components/IncomingMessageList/MessageResponse.jsx new file mode 100644 index 000000000..dca1e60d2 --- /dev/null +++ b/src/components/IncomingMessageList/MessageResponse.jsx @@ -0,0 +1,164 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import Form from 'react-formal' +import yup from 'yup' +import gql from 'graphql-tag' +import { StyleSheet, css } from 'aphrodite' +import Dialog from 'material-ui/Dialog' +import FlatButton from 'material-ui/FlatButton' + +import loadData from '../../containers//hoc/load-data' +import wrapMutations from '../../containers/hoc/wrap-mutations' +import GSForm from '../../components/forms/GSForm' +import SendButton from '../../components/SendButton' + +const styles = StyleSheet.create({ + messageField: { + padding: '0px 8px', + }, +}) + +class MessageResponse extends Component { + constructor(props) { + super(props) + + this.state = { + messageText: '', + isSending: false, + sendError: '' + } + + this.handleCloseErrorDialog = this.handleCloseErrorDialog.bind(this) + } + + createMessageToContact(text) { + const { contact, texter } = this.props.conversation + + return { + assignmentId: contact.assignmentId, + contactNumber: contact.cell, + userId: texter.id, + text + } + } + + handleMessageFormChange = ({ messageText }) => this.setState({ messageText }) + + handleMessageFormSubmit = async ({ messageText }) => { + const { contact } = this.props.conversation + const message = this.createMessageToContact(messageText) + if (this.state.isSending) { + return // stops from multi-send + } + this.setState({ isSending: true }) + + const finalState = { isSending: false } + try { + const response = await this.props.mutations.sendMessage(message, contact.id) + const { messages } = response.data.sendMessage + this.props.messagesChanged(messages) + finalState.messageText = '' + } catch (e) { + finalState.sendError = e.message + } + + this.setState(finalState) + } + + handleCloseErrorDialog() { + this.setState({ sendError: '' }) + } + + handleClickSendMessageButton = () => { + this.refs.messageForm.submit() + } + + render() { + const messageSchema = yup.object({ + messageText: yup.string().required('Can\'t send empty message').max(window.MAX_MESSAGE_LENGTH) + }) + + const { messageText, isSending } = this.state + const isSendDisabled = isSending || messageText.trim() === '' + + const errorActions = [ + + ] + + return ( +
+ +
+
+ +
+
+ +
+
+
+ +

{this.state.sendError}

+
+
+ ) + } +} + +MessageResponse.propTypes = { + conversation: PropTypes.object, + messagesChanged: PropTypes.func +} + +const mapMutationsToProps = () => ({ + sendMessage: (message, campaignContactId) => ({ + mutation: gql` + mutation sendMessage($message: MessageInput!, $campaignContactId: String!) { + sendMessage(message: $message, campaignContactId: $campaignContactId) { + id + messageStatus + messages { + id + createdAt + text + isFromContact + } + } + } + `, + variables: { + message, + campaignContactId + } + }) +}) + +export default loadData(wrapMutations(MessageResponse), { + mapMutationsToProps +}) diff --git a/src/components/IncomingMessageList.jsx b/src/components/IncomingMessageList/index.jsx similarity index 87% rename from src/components/IncomingMessageList.jsx rename to src/components/IncomingMessageList/index.jsx index 12ee039c4..f5f85605e 100644 --- a/src/components/IncomingMessageList.jsx +++ b/src/components/IncomingMessageList/index.jsx @@ -1,24 +1,25 @@ import React, { Component } from 'react' import type from 'prop-types' -import Dialog from 'material-ui/Dialog' import FlatButton from 'material-ui/FlatButton' import ActionOpenInNew from 'material-ui/svg-icons/action/open-in-new' -import loadData from '../containers/hoc/load-data' +import loadData from '../../containers/hoc/load-data' import { withRouter } from 'react-router' import gql from 'graphql-tag' -import LoadingIndicator from '../components/LoadingIndicator' +import LoadingIndicator from '../../components/LoadingIndicator' import DataTables from 'material-ui-datatables' +import ConversationPreviewModal from './ConversationPreviewModal'; -import { MESSAGE_STATUSES } from '../components/IncomingMessageFilter' +import { MESSAGE_STATUSES } from '../../components/IncomingMessageFilter' function prepareDataTableData(conversations) { - return conversations.map(conversation => { + return conversations.map((conversation, index) => { return { campaignTitle: conversation.campaign.title, texter: conversation.texter.displayName, to: conversation.contact.firstName + ' ' + conversation.contact.lastName + (conversation.contact.optOut.cell ? '⛔️' : ''), status: conversation.contact.messageStatus, - messages: conversation.contact.messages + messages: conversation.contact.messages, + index } }) } @@ -154,7 +155,7 @@ export class IncomingMessageList extends Component { { event.stopPropagation() - this.handleOpenConversation(row) + this.handleOpenConversation(row.index) }} icon={} /> @@ -192,8 +193,13 @@ export class IncomingMessageList extends Component { this.props.onConversationSelected(rowsSelected, selectedConversations) } - handleOpenConversation(contact) { - this.setState({ activeConversation: contact }) + handleOpenConversation(index) { + const conversation = this.props.conversations.conversations.conversations[index] + const activeConversation = { + contact: conversation.contact, + texter: conversation.texter + } + this.setState({ activeConversation }) } handleCloseConversation() { @@ -228,31 +234,10 @@ export class IncomingMessageList extends Component { rowSizeList={[10, 30, 50, 100, 500, 1000, 2000]} selectedRows={this.state.selectedRows} /> - - {this.state.activeConversation !== undefined && ( -
- {this.state.activeConversation.messages.map((message, index) => { - const isFromContact = message.isFromContact - const style = { - color: isFromContact ? 'blue' : 'black', - textAlign: isFromContact ? 'left' : 'right' - } - - return ( -

- {message.text} -

- ) - })} -
- )} -
+ /> ) } @@ -302,8 +287,10 @@ const mapQueriesToProps = ({ ownProps }) => ({ } contact { id + assignmentId firstName lastName + cell messageStatus messages { id diff --git a/src/server/api/conversations.js b/src/server/api/conversations.js index 4c9326bc5..12cfdb8b2 100644 --- a/src/server/api/conversations.js +++ b/src/server/api/conversations.js @@ -95,6 +95,7 @@ export async function getConversations( 'campaign_contact.id as cc_id', 'campaign_contact.first_name as cc_first_name', 'campaign_contact.last_name as cc_last_name', + 'campaign_contact.cell', 'campaign_contact.message_status', 'campaign_contact.is_opted_out', 'campaign_contact.updated_at', diff --git a/src/server/api/schema.js b/src/server/api/schema.js index 9696859dd..74e6db09d 100644 --- a/src/server/api/schema.js +++ b/src/server/api/schema.js @@ -973,7 +973,7 @@ const rootMutations = { await messageInstance.save() - if (contact.message_status === 'needsResponse') { + if (contact.message_status === 'needsResponse' || contact.message_status === 'convo') { const service = serviceMap[messageInstance.service || process.env.DEFAULT_SERVICE] contact.message_status = 'convo' contact.updated_at = 'now()'