|
| 1 | +import React, { Component } from 'react' |
| 2 | +import { FormattedDate, FormattedMessage, defineMessages, injectIntl } from 'react-intl' |
| 3 | +import { connect } from 'react-redux' |
| 4 | +import { Link } from 'react-router-dom' |
| 5 | + |
| 6 | +import CompactMessages from 'components/compact-messages' |
| 7 | +import PurchaseProgress from 'components/purchase-progress' |
| 8 | + |
| 9 | +import origin from '../services/origin' |
| 10 | + |
| 11 | +class Conversation extends Component { |
| 12 | + constructor(props) { |
| 13 | + super(props) |
| 14 | + |
| 15 | + this.intlMessages = defineMessages({ |
| 16 | + newMessagePlaceholder: { |
| 17 | + id: 'Messages.newMessagePlaceholder', |
| 18 | + defaultMessage: 'Type something...', |
| 19 | + }, |
| 20 | + }) |
| 21 | + |
| 22 | + this.handleKeyDown = this.handleKeyDown.bind(this) |
| 23 | + this.handleSubmit = this.handleSubmit.bind(this) |
| 24 | + this.conversationDiv = React.createRef() |
| 25 | + this.textarea = React.createRef() |
| 26 | + |
| 27 | + this.state = { |
| 28 | + counterparty: {}, |
| 29 | + listing: {}, |
| 30 | + purchase: {} |
| 31 | + } |
| 32 | + } |
| 33 | + |
| 34 | + componentDidMount() { |
| 35 | + // try to detect the user before rendering |
| 36 | + this.identifyCounterparty() |
| 37 | + } |
| 38 | + |
| 39 | + componentDidUpdate(prevProps) { |
| 40 | + const { id, messages, users } = this.props |
| 41 | + |
| 42 | + // on conversation change |
| 43 | + if (id !== prevProps.id) { |
| 44 | + // textarea is an uncontrolled component and might maintain internal state |
| 45 | + (this.textarea.current || {}).value = '' |
| 46 | + // refresh the counterparty |
| 47 | + this.identifyCounterparty() |
| 48 | + // refresh the listing/purchase context |
| 49 | + this.loadListing() |
| 50 | + } |
| 51 | + |
| 52 | + // on new message |
| 53 | + if (messages.length > prevProps.messages.length) { |
| 54 | + this.loadListing() |
| 55 | + // auto-scroll to most recent message |
| 56 | + this.scrollToBottom() |
| 57 | + } |
| 58 | + |
| 59 | + // on user found |
| 60 | + if (users.length > prevProps.users.length) { |
| 61 | + this.identifyCounterparty() |
| 62 | + } |
| 63 | + } |
| 64 | + |
| 65 | + handleKeyDown(e) { |
| 66 | + const { key, shiftKey } = e |
| 67 | + |
| 68 | + if (!shiftKey && key === 'Enter') { |
| 69 | + this.handleSubmit(e) |
| 70 | + } |
| 71 | + } |
| 72 | + |
| 73 | + async handleSubmit(e) { |
| 74 | + e.preventDefault() |
| 75 | + |
| 76 | + const { id, web3Account } = this.props |
| 77 | + const el = this.textarea.current |
| 78 | + const newMessage = el.value |
| 79 | + |
| 80 | + if (!newMessage.length) { |
| 81 | + return alert('Please add a message to send') |
| 82 | + } |
| 83 | + |
| 84 | + try { |
| 85 | + await originTest.messaging.sendConvMessage(id, newMessage.trim()) |
| 86 | + |
| 87 | + el.value = '' |
| 88 | + } catch(err) { |
| 89 | + console.error(err) |
| 90 | + } |
| 91 | + } |
| 92 | + |
| 93 | + identifyCounterparty() { |
| 94 | + const { id, users, web3Account } = this.props |
| 95 | + const recipients = origin.messaging.getRecipients(id) |
| 96 | + const address = recipients.find(addr => addr !== web3Account) |
| 97 | + const counterparty = users.find(u => u.address === address) || {} |
| 98 | + |
| 99 | + this.setState({ counterparty }) |
| 100 | + this.loadPurchase() |
| 101 | + } |
| 102 | + |
| 103 | + async loadListing() { |
| 104 | + const { messages } = this.props |
| 105 | + // find the most recent listing context or set empty value |
| 106 | + const { listingAddress } = messages.reverse().find(m => m.listingAddress) || {} |
| 107 | + // get the listing |
| 108 | + const listing = listingAddress ? (await origin.listings.get(listingAddress)) : {} |
| 109 | + // if listing does not match state, store and check for a purchase |
| 110 | + if (listing.address !== this.state.listing.address) { |
| 111 | + this.setState({ listing }) |
| 112 | + this.loadPurchase() |
| 113 | + this.scrollToBottom() |
| 114 | + } |
| 115 | + } |
| 116 | + |
| 117 | + async loadPurchase() { |
| 118 | + const { web3Account } = this.props |
| 119 | + const { counterparty, listing, purchase } = this.state |
| 120 | + const { address, sellerAddress } = listing |
| 121 | + |
| 122 | + // listing may not be found |
| 123 | + if (!address) { |
| 124 | + return |
| 125 | + } |
| 126 | + |
| 127 | + const len = await origin.listings.purchasesLength(address) |
| 128 | + const purchaseAddresses = await Promise.all([...Array(len).keys()].map(async i => { |
| 129 | + return await origin.listings.purchaseAddressByIndex(address, i) |
| 130 | + })) |
| 131 | + const purchases = await Promise.all(purchaseAddresses.map(async addr => { |
| 132 | + return await origin.purchases.get(addr) |
| 133 | + })) |
| 134 | + const involvingCounterparty = purchases.filter(p => p.buyerAddress === counterparty.address || p.buyerAddress === web3Account) |
| 135 | + const mostRecent = involvingCounterparty.sort((a, b) => a.index > b.index ? -1 : 1)[0] |
| 136 | + // purchase may not be found |
| 137 | + if (!mostRecent) { |
| 138 | + return |
| 139 | + } |
| 140 | + // compare with existing state |
| 141 | + if ( |
| 142 | + // purchase is different |
| 143 | + mostRecent.address !== purchase.address || |
| 144 | + // stage has changed |
| 145 | + mostRecent.stage !== purchase.stage |
| 146 | + ) { |
| 147 | + this.setState({ purchase: mostRecent }) |
| 148 | + this.scrollToBottom() |
| 149 | + } |
| 150 | + } |
| 151 | + |
| 152 | + scrollToBottom() { |
| 153 | + const el = this.conversationDiv.current |
| 154 | + |
| 155 | + if (el) { |
| 156 | + // delay allows for listing summary to change conversation height |
| 157 | + setTimeout(() => { |
| 158 | + el.scrollTop = el.scrollHeight |
| 159 | + }, 400) |
| 160 | + } |
| 161 | + } |
| 162 | + |
| 163 | + render() { |
| 164 | + const { activeForm, id, intl, messages, web3Account } = this.props |
| 165 | + const { counterparty, listing, purchase } = this.state |
| 166 | + const { address, name, pictures } = listing |
| 167 | + const { buyerAddress, created } = purchase |
| 168 | + const perspective = buyerAddress ? (buyerAddress === web3Account ? 'buyer' : 'seller') : null |
| 169 | + const soldAt = created ? created * 1000 /* convert seconds since epoch to ms */ : null |
| 170 | + const photo = pictures && pictures.length > 0 && (new URL(pictures[0])).protocol === "data:" && pictures[0] |
| 171 | + const canDeliverMessage = origin.messaging.canConverseWith(counterparty.address) |
| 172 | + const shouldEnableForm = canDeliverMessage && id |
| 173 | + |
| 174 | + return ( |
| 175 | + <div className="conversation-col col-12 col-sm-8 col-lg-9 d-flex flex-column"> |
| 176 | + {address && |
| 177 | + <div className="listing-summary d-flex"> |
| 178 | + <div className="aspect-ratio"> |
| 179 | + <div className={`${photo ? '' : 'placeholder '}image-container d-flex justify-content-center`}> |
| 180 | + <img src={photo || 'images/default-image.svg'} role="presentation" /> |
| 181 | + </div> |
| 182 | + </div> |
| 183 | + <div className="content-container d-flex flex-column"> |
| 184 | + {buyerAddress && |
| 185 | + <div className="brdcrmb"> |
| 186 | + {perspective === 'buyer' && |
| 187 | + <FormattedMessage |
| 188 | + id={ 'purchase-summary.purchasedFrom' } |
| 189 | + defaultMessage={ 'Purchased from {sellerLink}' } |
| 190 | + values={{ sellerLink: <Link to={`/users/${counterparty.address}`}>{counterparty.fullName}</Link> }} |
| 191 | + /> |
| 192 | + } |
| 193 | + {perspective === 'seller' && |
| 194 | + <FormattedMessage |
| 195 | + id={ 'purchase-summary.soldTo' } |
| 196 | + defaultMessage={ 'Sold to {buyerLink}' } |
| 197 | + values={{ buyerLink: <Link to={`/users/${counterparty.address}`}>{counterparty.fullName}</Link> }} |
| 198 | + /> |
| 199 | + } |
| 200 | + </div> |
| 201 | + } |
| 202 | + <h1>{name}</h1> |
| 203 | + {buyerAddress && |
| 204 | + <div className="state"> |
| 205 | + {perspective === 'buyer' && |
| 206 | + <FormattedMessage |
| 207 | + id={ 'purchase-summary.purchasedFromOn' } |
| 208 | + defaultMessage={ 'Purchased from {sellerName} on {date}' } |
| 209 | + values={{ sellerName: counterparty.fullName, date: <FormattedDate value={soldAt} /> }} |
| 210 | + /> |
| 211 | + } |
| 212 | + {perspective === 'seller' && |
| 213 | + <FormattedMessage |
| 214 | + id={ 'purchase-summary.soldToOn' } |
| 215 | + defaultMessage={ 'Sold to {buyerName} on {date}' } |
| 216 | + values={{ buyerName: counterparty.fullName, date: <FormattedDate value={soldAt} /> }} |
| 217 | + /> |
| 218 | + } |
| 219 | + </div> |
| 220 | + } |
| 221 | + {buyerAddress && |
| 222 | + <PurchaseProgress |
| 223 | + purchase={purchase} |
| 224 | + perspective={perspective} |
| 225 | + subdued={true} |
| 226 | + /> |
| 227 | + } |
| 228 | + </div> |
| 229 | + </div> |
| 230 | + } |
| 231 | + <div ref={this.conversationDiv} className="conversation"> |
| 232 | + <CompactMessages messages={messages}/> |
| 233 | + </div> |
| 234 | + {!shouldEnableForm && |
| 235 | + <form className="add-message d-flex"> |
| 236 | + <textarea tabIndex="0" disabled></textarea> |
| 237 | + <button type="submit" className="btn btn-sm btn-primary" disabled>Send</button> |
| 238 | + </form> |
| 239 | + } |
| 240 | + {shouldEnableForm && |
| 241 | + <form className="add-message d-flex" onSubmit={this.handleSubmit}> |
| 242 | + <textarea |
| 243 | + ref={this.textarea} |
| 244 | + placeholder={intl.formatMessage(this.intlMessages.newMessagePlaceholder)} |
| 245 | + onKeyDown={this.handleKeyDown} |
| 246 | + tabIndex="0" |
| 247 | + autoFocus> |
| 248 | + </textarea> |
| 249 | + <button type="submit" className="btn btn-sm btn-primary">Send</button> |
| 250 | + </form> |
| 251 | + } |
| 252 | + </div> |
| 253 | + ) |
| 254 | + } |
| 255 | +} |
| 256 | + |
| 257 | +const mapStateToProps = state => { |
| 258 | + return { |
| 259 | + users: state.users, |
| 260 | + web3Account: state.app.web3.account |
| 261 | + } |
| 262 | +} |
| 263 | + |
| 264 | +export default connect(mapStateToProps)(injectIntl(Conversation)) |
0 commit comments