UNPKG

instantjob-recruiter-client

Version:

a set of tools for creating an instantjob recruiter react client

351 lines (325 loc) 13.3 kB
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; `