Skip to content
This repository was archived by the owner on Sep 20, 2023. It is now read-only.

Commit b43ee46

Browse files
committed
Merge branch 'release-0.3.0'
2 parents 6ff71f6 + 1222034 commit b43ee46

File tree

9 files changed

+343
-296
lines changed

9 files changed

+343
-296
lines changed
Lines changed: 1 addition & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,11 @@
11
import React, { Component } from 'react'
22
import { connect } from 'react-redux'
3-
import { defineMessages, injectIntl } from 'react-intl'
4-
5-
import { fetchUser } from 'actions/User'
63

74
import Avatar from 'components/avatar'
85

96
import origin from '../services/origin'
107

118
class ConversationListItem extends Component {
12-
constructor(props) {
13-
super(props)
14-
15-
this.preFetchUsers = this.preFetchUsers.bind(this)
16-
17-
this.intlMessages = defineMessages({
18-
unnamedUser: {
19-
id: 'conversation-list-item.unnamedUser',
20-
defaultMessage: 'Unnamed User'
21-
},
22-
})
23-
}
24-
25-
componentDidMount() {
26-
this.preFetchUsers()
27-
}
28-
29-
componentDidUpdate() {
30-
this.preFetchUsers()
31-
}
32-
33-
preFetchUsers() {
34-
const { conversation, fetchUser, intl, users } = this.props
35-
const { recipients, senderAddress } = conversation.values[0]
36-
const addresses = [ ...recipients, senderAddress ]
37-
38-
addresses.filter(addr => {
39-
return !users.find(({ address }) => {
40-
return address === addr
41-
})
42-
}).forEach(addr => {
43-
fetchUser(addr, intl.formatMessage(this.intlMessages.unnamedUser))
44-
})
45-
}
46-
479
render() {
4810
const { active, conversation, handleConversationSelect, key, users, web3Account } = this.props
4911
const lastMessage = conversation.values.sort((a, b) => a.created < b.created ? -1 : 1)[conversation.values.length - 1]
@@ -91,8 +53,4 @@ const mapStateToProps = state => {
9153
}
9254
}
9355

94-
const mapDispatchToProps = dispatch => ({
95-
fetchUser: (addr, msg) => dispatch(fetchUser(addr, msg))
96-
})
97-
98-
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(ConversationListItem))
56+
export default connect(mapStateToProps)(ConversationListItem)

src/components/conversation.js

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
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

Comments
 (0)