instantjob-recruiter-client
Version:
a set of tools for creating an instantjob recruiter react client
351 lines (325 loc) • 13.3 kB
JSX
import React, {Component} from 'react'
import {connect} from 'react-redux'
import styled from 'styled-components'
import UnstyledTextarea from 'react-textarea-autosize'
import {browserHistory} from 'react-router'
import {FaAngleLeft, FaBell, FaBellSlash} from 'react-icons/lib/fa'
import Linkify from 'react-linkify'
import ProfileImage from 'components/profile_image'
import {create_message, store_messages, update_message_id} from '../actions/messages'
import {mute_user, unmute_user, store_muting_recruiters} from '../actions/mutes'
import {set_current_recipient, store_recruiter_users} from '../actions/users'
import {alert_success} from '../actions/display'
import {color} from '../common/constants'
import moment from '../common/moment'
import store from '../common/store'
import request from '../common/request'
import {with_recipient, read_last_messages} from '../common/messages'
import {array_contains, group_consecutive, exist} from '../common/utilities'
import auto_bind from '../common/auto_bind'
import {get_unread_messages, get_messages} from '../selectors/messages'
import {get_current_recipient} from '../selectors/users'
import {tolerant_equal} from '../selectors/base'
class MessagesStatic extends Component {
constructor(props) {
super(props)
auto_bind(this)
}
send_message(draft) {
this.props.send_message(draft)
if (this.props.muted) {
this.props.unmute_user()
}
}
componentDidMount() {
this.props.fetch_messages()
.then(() => read_last_messages(this.props.recipient.id))
this.scroll_bottom()
}
componentWillReceiveProps(props) {
if (props.recipient.id != this.props.recipient.id) {
this.props.fetch_messages()
}
if (!tolerant_equal(props.messages, this.props.messages) && exist(props.messages, (message) => !message.read_at && message.recipient_type == "Agency")) {
read_last_messages(props.recipient.id)
}
this.should_scroll_bottom = this.should_scroll_bottom || props.messages.length !== this.props.messages.length
}
scroll_bottom() {
this.scroll_container.scrollTop = this.scroll_container.scrollHeight
}
componentWillUpdate() {
this.should_scroll_bottom = this.should_scroll_bottom || this.scroll_container.scrollTop + this.scroll_container.offsetHeight === this.scroll_container.scrollHeight
}
componentDidUpdate() {
if (this.should_scroll_bottom) {
this.scroll_bottom()
this.should_scroll_bottom = false
}
}
render() {
return (
<div style={{justifyContent: 'space-between', alignItems: 'stretch', flexDirection: 'column', display: "flex", height: "100%", flex: 1}}>
<div style={{height: 50, display: 'flex', flexShrink: 0, borderBottomStyle: 'solid', borderBottomColor: color('black', 'bright'), borderBottomWidth: 1, padding: 5, flexDirection: 'row', alignItems: 'center'}}>
<div style={{display: 'flex', flex: 1, flexDirection: 'row', alignItems: 'space-between', alignItems: 'center'}}>
<div className="link" onClick={() => store.dispatch(set_current_recipient())} style={{height: 50, alignItems: "center", display: "flex"}}>
<FaAngleLeft style={{fontSize: 30}}/>
{this.props.unread_messages > 0 ? (
<div>
({this.props.unread_messages})
</div>
) : null}
</div>
<div className="link" onClick={() => this.props.set_current_user()} style={{marginLeft: 10}}>
{this.props.recipient.full_name}
</div>
</div>
{
// this.props.muted ? (
// <div className="link" onClick={() => this.props.unmute_user()}>
// <FaBellSlash style={{fontSize: 20, margin: 10, color: color('primary', 'light')}}/>
// </div>
// ) : (this.props.can_mute ? (
// <div className="link" onClick={() => this.props.mute_user()}>
// <FaBell style={{fontSize: 20, margin: 10, opacity: 0.54}}/>
// </div>
// ) : (
// <div style={{cursor: 'not-allowed'}}>
// <FaBell style={{fontSize: 20, margin: 10, opacity: 0.38}}/>
// </div>
// ))
}
</div>
<div style={{flexGrow: 10000, flexShrink: 1, overflow: 'scroll'}} ref={(scroll_container) => this.scroll_container = scroll_container}>
<div style={{display: 'flex', flexDirection: 'column', alignItems: 'stretch'}}>
<Conversations {...this.props}/>
</div>
</div>
<MessageInput send_message={this.send_message} />
</div>
)
}
}
class MessageInput extends Component {
constructor(props) {
super(props)
this.state = {
draft: '',
}
}
render() {
return (
<div style={{flexGrow: 1, flexShrink: 0, bottom: 0, minHeight: 40, backgroundColor: color('black', 'bright'), padding: 3}}>
<Textarea
placeholder="Message"
value={this.state.draft}
onChange={(event) => {
let draft = event.target.value
if (draft.slice(0, -1) != this.state.draft) {
this.setState({draft})
} else {
if (draft.slice(-1) === '\n') {
this.props.send_message(this.state.draft)
this.setState({draft: ''})
} else {
this.setState({draft})
}
}
}}
onFocus={(event) => {
event.target.select()
}}
style={{borderStyle: 'none', paddingHeight: 2}}
/>
</div>
)
}
}
const conversation_gap = {unit: 'hours', value: 1}
const Conversations = (props) => {
let conversations = group_consecutive(props.messages, (m1, m2) => {
return moment(m2.created_at).diff(moment(m1.created_at), conversation_gap.unit) < conversation_gap.value
})
return (
<div style={{display: 'flex', flexDirection: 'column', alignItems: 'stretch'}}>
{conversations.map((conversation, index) => <Conversation messages={conversation} key={index} recruiter={props.recruiter} recipient={props.recipient}/>)}
</div>
)
}
class Conversation extends Component {
componentDidMount() {
read_last_messages(this.props.recipient.id)
}
shouldComponentUpdate({recruiter, recipient, messages}) {
return recipient !== this.props.recipient ||
recruiter !== this.props.recruiter ||
!tolerant_equal(messages, this.props.messages)
}
render () {
let message_groups = group_consecutive(this.props.messages, (m1, m2) => {
return (m1.sender_id == m2.sender_id) && (m1.sender_type == m2.sender_type)
})
return (
<div style={{display: 'flex', flexDirection: 'column', alignItems: 'stretch'}}>
<span style={{marginRight: 0, flex: 1, textAlign: 'center', color: color('black', 'bright')}}>
{moment(this.props.messages[this.props.messages.length - 1].created_at).calendar()}
</span>
{message_groups.map((group, index) => <MessageGroup messages={group} key={index} recruiter={this.props.recruiter}/>)}
</div>
)
}
}
const MessageGroup = (props) => {
let group_length = props.messages.length - 1
let from_user = props.messages[0].sender_type === 'User'
return (
<div style={{display: 'flex', flexDirection: 'column', alignItems: 'stretch', paddingTop: 10, paddingBottom: 10}}>
<div style={{display: 'flex', flexDirection: 'row', alignItems: 'flex-end'}}>
<div style={Object.assign({display: 'flex', flexDirection: 'column', alignItems: 'stretch'}, !from_user ? {marginLeft: 'auto'} : {})}>
{props.messages.map((message, index) => <Message message={message} key={message.id} recruiter={props.recruiter} first={index == 0} last={index == group_length}/>)}
</div>
{from_user ? null : (
<ProfileImage {...props.messages[0].sender} hide_status />
)}
</div>
{props.messages[group_length].last_read_message ? (
<p style={{marginRight: 0, flex: 1, textAlign: 'right', color: color('black', 'bright')}}>Lu {moment(props.messages[group_length].read_at).fromNow()}</p>
) : (props.messages[group_length].last_message ? (
<p style={{marginRight: 0, flex: 1, textAlign: 'right', color: color('black', 'bright')}}>Envoyé {moment(props.messages[group_length].created_at).fromNow()}</p>
) : null)}
</div>
)
}
const border_normal = 10
const border_small = 3
const Message = (props) => {
let from_user = props.message.sender_type === 'User'
let from_self = props.message.sender_type === 'Recruiter' && props.message.sender_id === props.recruiter
return (
<div>
<div style={Object.assign({
display: "flex",
flexDirection: 'column',
}, !from_user ? {
marginRight: 5,
float: 'right',
alignItems: 'flex-end',
} : {
marginLeft: 5,
float: 'left',
alignItems: 'flex-start',
}, props.message.last_read_message ? {} : {
marginBottom: 2,
})}>
<div style={Object.assign({
padding: 10,
maxWidth: 200,
borderRadius: border_normal,
wordWrap: "break-word",
}, from_self ? {
backgroundColor: color('primary', 'light'),
color: 'white',
} : {
backgroundColor: '#DDD',
color: 'black',
}, !from_user ? {
borderTopRightRadius: props.first ? border_normal : border_small,
borderBottomRightRadius: props.last ? border_normal : border_small,
} : {
borderTopLeftRadius: props.first ? border_normal : border_small,
borderBottomLeftRadius: props.last ? border_normal : border_small,
}
)}>
<Linkify properties={{className: 'link', style: {color: color('white'), textDecoration: 'underline'}}}>{props.message.content}</Linkify>
</div>
</div>
</div>
)
}
const Messages = connect(
(state) => {
let messages = get_messages(state)
.filter((message) => with_recipient(message, state.users.current_recipient))
.sort((m1, m2) => moment(m1.created_at).isAfter(m2.created_at) ? 1 : -1)
let i = messages.length - 1
let last_message = messages[i]
messages[i] = {...last_message, last_message: true}
while (i >= 0 && (messages[i].sender_type === 'User' || messages[i].read_at == null)) {
i--
}
if (i >= 0) {
messages[i] = {...messages[i], last_read_message: true, last_message: false}
}
return {
messages,
recipient: get_current_recipient(state),
recruiter: state.profile.id,
muted: array_contains(state.mutes.muted_users, state.users.current_recipient),
can_mute: messages.reduce((can_mute, message) =>
can_mute || (
message.sender_type === 'Recruiter' &&
message.sender_id != state.profile.id &&
!array_contains(state.mutes.mutes_by_user[state.users.current_recipient], message.sender_id)
),
false
) && messages.filter((message) => message.sender_type === 'Recruiter').length > 0,
unread_messages: get_unread_messages(state)
}
},
(dispatch) => {
let toggle_mute_user = (mute) => {
let state = store.getState()
let user = get_current_recipient(state)
let action, triggered_action, alert_text
if (mute) {
action = unmute_user
triggered_action = 'mutes.unmute'
alert_text = `Vous recevrez à nouveau les alertes pour les nouveaux messages de ${user.full_name}`
} else {
action = mute_user
triggered_action = 'mutes.mute'
alert_text = `Vous ne recevrez plus d'alertes pour les nouveaux messages de ${user.full_name}`
}
dispatch(action(user.id))
dispatch(alert_success(alert_text))
}
return {
send_message: (content) => {
let state = store.getState()
let user_id = state.users.current_recipient
let temp_id = state.messages.idGenerator
let message = {
id: temp_id,
content,
sender_id: state.profile.id,
sender_type: "Recruiter",
recipient_id: user_id,
recipient_type: "User",
}
dispatch(create_message(message))
request.post('messages', {message: {content}, user_id})
.then((message) => dispatch(update_message_id(message, temp_id)))
},
fetch_messages: () => {
let state = store.getState()
request.get(`users/${state.users.current_recipient}/muting_recruiters`)
.then((recruiters) => dispatch(store_muting_recruiters(state.users.current_recipient, recruiters.map((recruiter) => recruiter.id))))
return request.get('messages', {user_id: state.users.current_recipient})
.then((messages) => dispatch(store_messages(messages)))
},
set_current_user: () => {
browserHistory.push(`/users/${store.getState().users.current_recipient}`)
},
mute_user: () => toggle_mute_user(false),
unmute_user: () => toggle_mute_user(true),
}
}
)(MessagesStatic)
export default Messages
const Textarea = styled(UnstyledTextarea)`
font-size: 14px;
padding: 10px 15px;
width: 85%;
margin: 2px;
border-radius: 5px;
`