Progress on dms, code cleanup

Progress on dms, code cleanup
This commit is contained in:
mgabdev
2020-12-02 23:22:51 -05:00
parent 20d4fc09af
commit 9a43c51085
103 changed files with 3656 additions and 859 deletions

View File

@@ -0,0 +1,47 @@
import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { openModal } from '../../../actions/modal'
import { MODAL_CHAT_CONVERSATION_CREATE } from '../../../constants'
import Text from '../../../components/text'
import Button from '../../../components/button'
class ChatEmptyMessageBlock extends React.PureComponent {
handleOnCreateNewChat = () => {
this.props.onOpenChatConversationCreateModal()
}
render () {
return (
<div className={[_s.d, _s.bgPrimary, _s.h100PC, _s.w100PC].join(' ')}>
<div className={[_s.d, _s.w100PC, _s.h100PC, _s.aiCenter, _s.jcCenter].join(' ')}>
<Text weight='bold' size='extraLarge'>
You dont have a message selected
</Text>
<Text size='medium' color='secondary' className={_s.py10}>
Choose one from your existing messages, or start a new one.
</Text>
<Button className={_s.mt10} onClick={this.handleOnCreateNewChat}>
<Text color='inherit' weight='bold' className={_s.px15}>
New Message
</Text>
</Button>
</div>
</div>
)
}
}
const mapDispatchToProps = (dispatch) => ({
onOpenChatConversationCreateModal() {
dispatch(openModal(MODAL_CHAT_CONVERSATION_CREATE))
}
})
ChatEmptyMessageBlock.propTypes = {
onOpenChatConversationCreateModal: PropTypes.func.isRequired,
}
export default connect(null, mapDispatchToProps)(ChatEmptyMessageBlock)

View File

@@ -0,0 +1,98 @@
import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import debounce from 'lodash.debounce'
import ImmutablePureComponent from 'react-immutable-pure-component'
import ImmutablePropTypes from 'react-immutable-proptypes'
import {
fetchChatConversations,
expandChatConversations,
fetchChatConversationRequested,
expandChatConversationRequested,
} from '../../../actions/chat_conversations'
import AccountPlaceholder from '../../../components/placeholder/account_placeholder'
import ChatConversationsListItem from './chat_conversations_list_item'
import ScrollableList from '../../../components/scrollable_list'
class ChatConversationsList extends ImmutablePureComponent {
componentDidMount() {
console.log("componentDidMount:", this.props.source)
this.props.onFetchChatConversations(this.props.source)
}
handleLoadMore = debounce(() => {
this.props.onExpandChatConversations(this.props.source)
}, 300, { leading: true })
render() {
const {
hasMore,
isLoading,
source,
chatConversationIds,
} = this.props
console.log("---source:", source, chatConversationIds)
return (
<div className={[_s.d, _s.w100PC, _s.overflowHidden, _s.boxShadowNone].join(' ')}>
<ScrollableList
scrollKey='chat-conversations'
onLoadMore={this.handleLoadMore}
hasMore={hasMore}
isLoading={isLoading}
showLoading={isLoading}
placeholderComponent={AccountPlaceholder}
placeholderCount={3}
emptyMessage='Empty'
>
{
!!chatConversationIds && chatConversationIds.map((chatConversationId, i) => (
<ChatConversationsListItem
key={`chat-conversation-${i}`}
chatConversationId={chatConversationId}
source={source}
/>
))
}
</ScrollableList>
</div>
)
}
}
const mapStateToProps = (state, { source }) => ({
chatConversationIds: state.getIn(['chat_conversation_lists', source, 'items']),
hasMore: !!state.getIn(['chat_conversation_lists', source, 'next']),
isLoading: state.getIn(['chat_conversation_lists', source, 'isLoading']),
})
const mapDispatchToProps = (dispatch) => ({
onFetchChatConversations(source) {
if (source ==='approved') {
dispatch(fetchChatConversations())
} else if (source ==='requested') {
dispatch(fetchChatConversationRequested())
}
},
onExpandChatConversations(source) {
if (source ==='approved') {
dispatch(expandChatConversations())
} else if (source ==='requested') {
dispatch(expandChatConversationRequested())
}
},
})
ChatConversationsList.propTypes = {
chatConversationIds: ImmutablePropTypes.list,
hasMore: PropTypes.bool,
isLoading: PropTypes.bool,
onFetchChatConversations: PropTypes.func.isRequired,
onExpandChatConversations: PropTypes.func.isRequired,
source: PropTypes.string.isRequired,
}
export default connect(mapStateToProps, mapDispatchToProps)(ChatConversationsList)

View File

@@ -0,0 +1,116 @@
import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import ImmutablePureComponent from 'react-immutable-pure-component'
import ImmutablePropTypes from 'react-immutable-proptypes'
import { makeGetChatConversation } from '../../../selectors'
import { setChatConversationSelected } from '../../../actions/chats'
import { CX } from '../../../constants'
import Input from '../../../components/input'
import DisplayNameGroup from '../../../components/display_name_group'
import DisplayName from '../../../components/display_name'
import AvatarGroup from '../../../components/avatar_group'
import Text from '../../../components/text'
import RelativeTimestamp from '../../../components/relative_timestamp'
class ChatConversationsListItem extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
}
handleOnClick = () => {
const { chatConversationId } = this.props
this.props.onSetChatConversationSelected(chatConversationId)
this.context.router.history.push(`/messages/${chatConversationId}`)
}
render() {
const {
selected,
selectedId,
chatConversation,
chatConversationId,
} = this.props
if (!chatConversation) return <div/>
const containerClasses = CX({
d: 1,
w100PC: 1,
bgTransparent: 1,
bgSubtle_onHover: 1,
borderBottom1PX: 1,
borderColorSecondary: 1,
noUnderline: 1,
outlineNone: 1,
cursorPointer: 1,
})
const innerContainerClasses = CX({
d: 1,
flexRow: 1,
aiStart: 1,
aiCenter: 0,
px15: 1,
py15: 1,
borderRight4PX: selected,
borderColorBrand: selected,
})
const avatarSize = 46
const otherAccounts = chatConversation.get('other_accounts')
const lastMessage = chatConversation.get('last_chat_message', null)
const content = { __html: !!lastMessage ? lastMessage.get('text', '') : '' }
const date = !!lastMessage ? lastMessage.get('created_at') : chatConversation.get('created_at')
return (
<button
className={containerClasses}
onClick={this.handleOnClick}
>
<div className={innerContainerClasses}>
<AvatarGroup accounts={otherAccounts} size={avatarSize} noHover />
<div className={[_s.d, _s.pl10, _s.overflowHidden, _s.flexNormal].join(' ')}>
<div className={[_s.d, _s.flexRow, _s.aiCenter].join(' ')}>
<div className={[_s.d, _s.pt2, _s.pr5, _s.noUnderline, _s.overflowHidden, _s.flexNormal, _s.flexRow, _s.aiStart, _s.aiCenter].join(' ')}>
<div className={_s.maxW100PC42PX}>
<DisplayName account={otherAccounts.get(0)} noHover />
</div>
<Text size='extraSmall' color='secondary' className={_s.mlAuto}>
<RelativeTimestamp timestamp={date} />
</Text>
</div>
</div>
<div className={[_s.py5, _s.dangerousContent, _s.textAlignLeft].join(' ')} dangerouslySetInnerHTML={content} />
</div>
</div>
</button>
)
}
}
const mapStateToProps = (state, { chatConversationId }) => ({
chatConversation: makeGetChatConversation()(state, { id: chatConversationId }),
selectedId: state.getIn(['chats', 'selectedChatConversationId'], null),
selected: state.getIn(['chats', 'selectedChatConversationId'], null) === chatConversationId,
})
const mapDispatchToProps = (dispatch) => ({
onSetChatConversationSelected: (chatConversationId) => {
dispatch(setChatConversationSelected(chatConversationId))
},
})
ChatConversationsListItem.propTypes = {
chatConversationId: PropTypes.string.isRequired,
chatConversation: ImmutablePropTypes.map,
onSetChatConversationSelected: PropTypes.func.isRequired,
selected: PropTypes.bool.isRequired,
source: PropTypes.string.isRequired,
}
export default connect(mapStateToProps, mapDispatchToProps)(ChatConversationsListItem)

View File

@@ -0,0 +1,57 @@
import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { NavLink } from 'react-router-dom'
import { shortNumberFormat } from '../../../utils/numbers'
import { fetchChatConversationRequestedCount } from '../../../actions/chat_conversations'
import Text from '../../../components/text'
import Icon from '../../../components/icon'
class ChatConversationRequestsListItem extends React.PureComponent {
componentDidMount() {
this.props.onFetchChatConversationRequestedCount()
}
render() {
const { requestCount } = this.props
if (!requestCount || requestCount < 1) return null
return (
<NavLink
className={[_s.d, _s.w100PC, _s.bgTransparent, _s.bgSubtle_onHover, _s.borderBottom1PX, _s.borderColorSecondary, _s.noUnderline, _s.outlineNone, _s.cursorPointer].join(' ')}
to='/messages/requests'
>
<div className={[_s.d, _s.px15, _s.py15, _s.aiCenter, _s.flexRow].join(' ')}>
<Text size='medium'>Message Requests</Text>
<Text size='medium' className={[_s.mlAuto, _s.mr15].join(' ')}>
{shortNumberFormat(requestCount)}
</Text>
<Icon id='angle-right' size='10px' className={_s.cPrimary} />
</div>
</NavLink>
)
}
}
const mapStateToProps = (state) => ({
requestCount: state.getIn(['chats', 'chatConversationRequestCount'], 0),
})
const mapDispatchToProps = (dispatch) => ({
onFetchChatConversationRequestedCount: () => dispatch(fetchChatConversationRequestedCount()),
})
ChatConversationRequestsListItem.propTypes = {
requestCount: PropTypes.number,
onFetchChatConversationRequestedCount: PropTypes.func.isRequired,
}
ChatConversationRequestsListItem.defaultProps = {
requestCount: 0,
}
export default connect(mapStateToProps, mapDispatchToProps)(ChatConversationRequestsListItem)

View File

@@ -1,11 +1,8 @@
import React from 'react'
import PropTypes from 'prop-types'
import { defineMessages, injectIntl } from 'react-intl'
import ImmutablePureComponent from 'react-immutable-pure-component'
import ImmutablePropTypes from 'react-immutable-proptypes'
import Input from '../../../components/input'
class MessagesSearch extends ImmutablePureComponent {
class ChatConversationsSearch extends React.PureComponent {
static contextTypes = {
router: PropTypes.object,
@@ -17,7 +14,7 @@ class MessagesSearch extends ImmutablePureComponent {
render() {
const {
intl,
children
} = this.props
return (
@@ -34,12 +31,8 @@ class MessagesSearch extends ImmutablePureComponent {
}
const messages = defineMessages({
placeholder: { id: 'compose_form.placeholder', defaultMessage: "What's on your mind?" },
})
MessagesSearch.propTypes = {
intl: PropTypes.object.isRequired,
ChatConversationsSearch.propTypes = {
//
}
export default injectIntl(MessagesSearch)
export default ChatConversationsSearch

View File

@@ -0,0 +1,140 @@
import React from 'react'
import PropTypes from 'prop-types'
import ImmutablePureComponent from 'react-immutable-pure-component'
import ImmutablePropTypes from 'react-immutable-proptypes'
import { connect } from 'react-redux'
import Textarea from 'react-textarea-autosize'
import { openModal } from '../../../actions/modal'
import { sendChatMessage } from '../../../actions/chat_messages'
import { CX } from '../../../constants'
import Button from '../../../components/button'
import Input from '../../../components/input'
class ChatMessagesComposeForm extends React.PureComponent {
state = {
focused: false,
value: '',
}
handleOnSendChatMessage = () => {
this.props.onSendChatMessage(this.state.value, this.props.chatConversationId)
this.setState({ value: '' })
}
onChange = (e) => {
this.setState({ value: e.target.value })
}
onBlur = () => {
this.setState({ focused: false });
}
onFocus = () => {
this.setState({ focused: true });
}
onKeyDown = (e) => {
const { disabled } = this.props;
if (disabled) {
e.preventDefault();
return;
}
// Ignore key events during text composition
// e.key may be a name of the physical key even in this case (e.x. Safari / Chrome on Mac)
if (e.which === 229 || e.isComposing) return;
switch (e.key) {
case 'Escape':
document.querySelector('#gabsocial').focus()
break;
case 'Enter':
case 'Tab':
//
break;
}
// if (e.defaultPrevented || !this.props.onKeyDown) return;
}
setTextbox = (c) => {
this.textbox = c
}
render () {
const { chatConversationId } = this.props
const { value } = this.state
const disabled = false
const textareaContainerClasses = CX({
d: 1,
maxW100PC: 1,
flexGrow1: 1,
jcCenter: 1,
py5: 1,
})
const textareaClasses = CX({
d: 1,
font: 1,
wrap: 1,
resizeNone: 1,
bgTransparent: 1,
outlineNone: 1,
lineHeight125: 1,
cPrimary: 1,
px10: 1,
fs14PX: 1,
maxH200PX: 1,
borderColorSecondary: 1,
border1PX: 1,
radiusSmall: 1,
py10: 1,
})
return (
<div className={[_s.d, _s.posAbs, _s.bottom0, _s.left0, _s.right0, _s.flexRow, _s.aiCenter, _s.minH58PX, _s.bgPrimary, _s.w100PC, _s.borderTop1PX, _s.borderColorSecondary, _s.px15, _s.py5].join(' ')}>
<div className={[_s.d, _s.pr15, _s.flexGrow1].join(' ')}>
<Textarea
id='chat-message-compose-input'
inputRef={this.setTextbox}
className={textareaClasses}
disabled={disabled}
placeholder='Type a new message...'
autoFocus={false}
value={value}
onChange={this.onChange}
onFocus={this.onFocus}
onBlur={this.onBlur}
onKeyDown={this.onKeyDown}
aria-autocomplete='list'
/>
</div>
<div className={[_s.d, _s.h100PC, _s.mtAuto, _s.pb5].join(' ')}>
<Button
disabled={disabled}
onClick={this.handleOnSendChatMessage}
>
Send
</Button>
</div>
</div>
)
}
}
const mapDispatchToProps = (dispatch) => ({
onSendChatMessage(text, chatConversationId) {
dispatch(sendChatMessage(text, chatConversationId))
},
})
ChatMessagesComposeForm.propTypes = {
chatConversationId: PropTypes.string,
onSendMessage: PropTypes.func.isRequired,
}
export default connect(null, mapDispatchToProps)(ChatMessagesComposeForm)

View File

@@ -0,0 +1,84 @@
import React from 'react'
import PropTypes from 'prop-types'
import ImmutablePureComponent from 'react-immutable-pure-component'
import ImmutablePropTypes from 'react-immutable-proptypes'
import { connect } from 'react-redux'
import { makeGetChatConversation } from '../../../selectors'
import { openModal } from '../../../actions/modal'
import { approveChatConversationRequest } from '../../../actions/chat_conversations'
import { MODAL_CHAT_CONVERSATION_CREATE } from '../../../constants'
import Button from '../../../components/button'
import AvatarGroup from '../../../components/avatar_group'
import DisplayName from '../../../components/display_name'
import Text from '../../../components/text'
class ChatMessageHeader extends React.PureComponent {
handleOnApproveMessageRequest = () => {
this.props.onApproveChatConversationRequest(this.props.chatConversationId)
}
render () {
const { chatConversation } = this.props
const isChatConversationRequest = !!chatConversation ? !chatConversation.get('is_approved') : false
const otherAccounts = !!chatConversation ? chatConversation.get('other_accounts') : null
return (
<div className={[_s.d, _s.posAbs, _s.top0, _s.left0, _s.right0, _s.flexRow, _s.aiCenter, _s.h60PX, _s.w100PC, _s.borderBottom1PX, _s.borderColorSecondary, _s.bgPrimary, _s.px15, _s.py5].join(' ')}>
{
!!otherAccounts &&
<React.Fragment>
<AvatarGroup accounts={otherAccounts} size={34} noHover />
<div className={[_s.d, _s.pl10, _s.maxW100PC86PX, _s.overflowHidden].join(' ')}>
<DisplayName account={otherAccounts.get(0)} isMultiline />
</div>
</React.Fragment>
}
<Button
isNarrow
onClick={undefined}
color='primary'
backgroundColor='secondary'
className={[_s.mlAuto, _s.px5].join(' ')}
icon='ellipsis'
iconSize='18px'
/>
{
isChatConversationRequest &&
<Button
isNarrow
onClick={this.handleOnApproveMessageRequest}
className={_s.ml10}
>
<Text>
Approve Message Request
</Text>
</Button>
}
</div>
)
}
}
const mapStateToProps = (state, { chatConversationId }) => ({
chatConversation: makeGetChatConversation()(state, { id: chatConversationId }),
})
const mapDispatchToProps = (dispatch) => ({
onOpenChatConversationCreateModal() {
dispatch(openModal(MODAL_CHAT_CONVERSATION_CREATE))
},
onApproveChatConversationRequest(chatConversationId) {
dispatch(approveChatConversationRequest(chatConversationId))
}
})
ChatMessageHeader.propTypes = {
onOpenChatConversationCreateModal: PropTypes.func.isRequired,
chatConversationId: PropTypes.string,
}
export default connect(mapStateToProps, mapDispatchToProps)(ChatMessageHeader)

View File

@@ -0,0 +1,179 @@
import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import moment from 'moment-mini'
import ImmutablePureComponent from 'react-immutable-pure-component'
import ImmutablePropTypes from 'react-immutable-proptypes'
import { NavLink } from 'react-router-dom'
import { CX } from '../../../constants'
import { me } from '../../../initial_state'
import Input from '../../../components/input'
import Avatar from '../../../components/avatar'
import Button from '../../../components/button'
import Text from '../../../components/text'
import { makeGetChatMessage } from '../../../selectors'
class ChatMessageItem extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
}
state = {
hovering: false,
isNewDay: false,
isCloseToMyLast: false,
}
componentDidMount() {
const { lastChatMessageSameSender, lastChatMessageDate } = this.props
if (lastChatMessageDate) {
const createdAt = this.props.chatMessage.get('created_at')
const isNewDay = moment(createdAt).format('L') !== moment(lastChatMessageDate).format('L')
const isCloseToMyLast = moment(lastChatMessageDate).diff(createdAt, 'minutes') < 60 && lastChatMessageSameSender && !isNewDay
this.setState({
isNewDay,
isCloseToMyLast,
})
}
}
handleOnMouseEnter = () => {
this.setState({ isHovering: true })
}
handleOnMouseLeave = () => {
this.setState({ isHovering: false })
}
handleMoreClick = () => {
//
}
render() {
const {
chatMessage,
isHidden,
lastChatMessageDate,
} = this.props
const {
isCloseToMyLast,
isHovering,
isNewDay,
} = this.state
if (!chatMessage) return <div />
const account = chatMessage.get('account')
const content = { __html: chatMessage.get('text') }
const alt = account.get('id', null) === me
const createdAt = chatMessage.get('created_at')
if (isHidden) {
return (
<React.Fragment>
{account.get('display_name')}
<div dangerouslySetInnerHTML={content} />
</React.Fragment>
)
}
const messageContainerClasses = CX({
d: 1,
flexRow: !alt,
flexRowReverse: alt,
pb5: 1,
})
const messageInnerContainerClasses = CX({
d: 1,
px15: 1,
py5: 1,
bgTertiary: !alt,
bgSecondary: alt,
circle: 1,
ml10: 1,
mr10: 1,
})
const lowerContainerClasses = CX({
d: 1,
pt10: 1,
posAbs: 1,
bottom0: 1,
right0: alt,
left0: !alt,
displayNone: !isHovering,
pl50: !alt,
pr50: alt,
})
const buttonContainerClasses = CX({
d: 1,
flexRow: 1,
displayNone: !isHovering && alt,
})
return (
<div
className={[_s.d, _s.w100PC, _s.pb10].join(' ')}
onMouseEnter={this.handleOnMouseEnter}
onMouseLeave={this.handleOnMouseLeave}
>
{
!!lastChatMessageDate && isNewDay &&
<Text color='secondary' size='small' align='center' className={[_s.d, _s.py10].join(' ')}>
{moment(createdAt).format('lll')}
</Text>
}
<div className={[_s.d, _s.w100PC, _s.pb15].join(' ')}>
<div className={messageContainerClasses}>
<Avatar account={chatMessage.get('account')} size={38} />
<div className={messageInnerContainerClasses}>
<div className={[_s.py5, _s.dangerousContent, _s.cPrimary].join(' ')} dangerouslySetInnerHTML={content} />
</div>
{
alt &&
<div className={buttonContainerClasses}>
<Button
onClick={this.handleMoreClick}
color='tertiary'
backgroundColor='none'
icon='ellipsis'
iconSize='18px'
/>
</div>
}
</div>
<div className={lowerContainerClasses}>
<Text size='extraSmall' color='tertiary' align={alt ? 'right' : 'left'}>
{moment(createdAt).format('lll')}
</Text>
</div>
</div>
</div>
)
}
}
const mapStateToProps = (state, { lastChatMessageId, chatMessageId }) => ({
chatMessage: makeGetChatMessage()(state, { id: chatMessageId }),
lastChatMessageDate: lastChatMessageId ? state.getIn(['chat_messages', `${lastChatMessageId}`, 'created_at'], null) : null,
lastChatMessageSameSender: lastChatMessageId ? state.getIn(['chat_messages', `${lastChatMessageId}`, 'from_account_id'], null) === state.getIn(['chat_messages', `${chatMessageId}`, 'from_account_id'], null) : false,
})
ChatMessageItem.propTypes = {
intl: PropTypes.object.isRequired,
lastChatMessageId: PropTypes.string,
lastChatMessageDate: PropTypes.string,
lastChatMessageSameSender: PropTypes.string,
chatMessageId: PropTypes.string.isRequired,
chatMessage: ImmutablePropTypes.map,
isHidden: PropTypes.bool,
alt: PropTypes.bool,
}
export default connect(mapStateToProps)(ChatMessageItem)

View File

@@ -0,0 +1,212 @@
import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import moment from 'moment-mini'
import { List as ImmutableList } from 'immutable'
import ImmutablePropTypes from 'react-immutable-proptypes'
import ImmutablePureComponent from 'react-immutable-pure-component'
import { createSelector } from 'reselect'
import debounce from 'lodash.debounce'
import { me } from '../../../initial_state'
import { setChatConversationSelected } from '../../../actions/chats'
import {
expandChatMessages,
scrollBottomChatMessageConversation,
} from '../../../actions/chat_conversation_messages'
import ScrollableList from '../../../components/scrollable_list'
import ChatMessagePlaceholder from '../../../components/placeholder/chat_message_placeholder'
import ChatMessageItem from './chat_message_item'
class ChatMessageScrollingList extends ImmutablePureComponent {
state = {
isRefreshing: false,
}
componentDidMount () {
const { chatConversationId } = this.props
this.props.onExpandChatMessages(chatConversationId)
}
componentWillUnmount() {
this.props.onSetChatConversationSelected(null)
}
componentWillReceiveProps (nextProps) {
const { chatConversationId } = nextProps
if (chatConversationId !== this.props.chatConversationId) {
this.props.onExpandChatMessages(chatConversationId)
}
}
handleLoadMore = (sinceId) => {
const { chatConversationId, dispatch } = this.props
this.props.onExpandChatMessages(chatConversationId, { sinceId })
}
componentDidUpdate(prevProps, prevState) {
if (this.state.isRefreshing) {
this.setState({ isRefreshing: false })
}
if (prevProps.chatMessageIds.size === 0 && this.props.chatMessageIds.size > 0) {
this.containerNode.scrollTop = this.containerNode.scrollHeight
}
}
getCurrentChatMessageIndex = (id) => {
// : todo :
return this.props.chatMessageIds.indexOf(id)
}
handleMoveUp = (id) => {
const elementIndex = this.getCurrentChatMessageIndex(id) - 1
this._selectChild(elementIndex, true)
}
handleMoveDown = (id) => {
const elementIndex = this.getCurrentChatMessageIndex(id) + 1
this._selectChild(elementIndex, false)
}
handleLoadOlder = debounce(() => {
this.handleLoadMore(this.props.chatMessageIds.size > 0 ? this.props.chatMessageIds.last() : undefined)
}, 300, { leading: true })
handleOnReload = debounce(() => {
this.handleLoadMore()
this.setState({ isRefreshing: true })
}, 300, { trailing: true })
_selectChild(index, align_top) {
const container = this.node.node
const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`)
if (element) {
if (align_top && container.scrollTop > element.offsetTop) {
element.scrollIntoView(true)
} else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) {
element.scrollIntoView(false)
}
element.focus()
}
}
setRef = (c) => {
this.node = c
}
containerRef = (c) => {
this.containerNode = c
}
render() {
const {
chatConversationId,
chatMessageIds,
isLoading,
isPartial,
hasMore,
onScrollToBottom,
onScroll,
} = this.props
const { isRefreshing } = this.state
if (isPartial || (isLoading && chatMessageIds.size === 0)) {
return null
}
let scrollableContent = []
let emptyContent = []
if (isLoading || chatMessageIds.size > 0) {
for (let i = 0; i < chatMessageIds.count(); i++) {
const chatMessageId = chatMessageIds.get(i)
const lastChatMessageId = i > 0 ? chatMessageIds.get(i - 1) : null
if (!chatMessageId) {
scrollableContent.unshift(
<div
key={`chat-message-gap:${(i + 1)}`}
disabled={isLoading}
sinceId={i > 0 ? chatMessageIds.get(i - 1) : null}
onClick={this.handleLoadMore}
/>
)
} else {
scrollableContent.unshift(
<ChatMessageItem
key={`chat-message-${chatConversationId}-${i}`}
chatMessageId={chatMessageId}
lastChatMessageId={lastChatMessageId}
onMoveUp={this.handleMoveUp}
onMoveDown={this.handleMoveDown}
commentsLimited
/>
)
}
}
}
return (
<div
className={[_s.d, _s.boxShadowNone, _s.posAbs, _s.bottom60PX, _s.left0, _s.right0, _s.px15, _s.py15, _s.top60PX, _s.w100PC, _s.overflowYScroll].join(' ')}
ref={this.containerRef}
>
<ScrollableList
scrollRef={this.setRef}
onLoadMore={this.handleLoadMore && this.handleLoadOlder}
scrollKey='chat_messages'
hasMore={hasMore}
emptyMessage='No chats found'
onScrollToBottom={onScrollToBottom}
onScroll={onScroll}
isLoading={isLoading}
>
{scrollableContent}
</ScrollableList>
</div>
)
}
}
const mapStateToProps = (state, { chatConversationId }) => {
if (!chatConversationId) return {}
return {
chatMessageIds: state.getIn(['chat_conversation_messages', chatConversationId, 'items'], ImmutableList()),
isLoading: state.getIn(['chat_conversation_messages', chatConversationId, 'isLoading'], true),
isPartial: state.getIn(['chat_conversation_messages', chatConversationId, 'isPartial'], false),
hasMore: state.getIn(['chat_conversation_messages', chatConversationId, 'hasMore']),
}
}
const mapDispatchToProps = (dispatch, ownProps) => ({
onScrollToBottom: debounce(() => {
dispatch(scrollBottomChatMessageConversation(ownProps.chatConversationId, true))
}, 100),
onScroll: debounce(() => {
dispatch(scrollBottomChatMessageConversation(ownProps.chatConversationId, false))
}, 100),
onExpandChatMessages(chatConversationId, params) {
dispatch(expandChatMessages(chatConversationId, params))
},
onSetChatConversationSelected: (chatConversationId) => {
dispatch(setChatConversationSelected(chatConversationId))
},
})
ChatMessageScrollingList.propTypes = {
chatMessageIds: ImmutablePropTypes.list.isRequired,
chatConversationId: PropTypes.string.isRequired,
onExpandChatMessages: PropTypes.func.isRequired,
isLoading: PropTypes.bool,
isPartial: PropTypes.bool,
hasMore: PropTypes.bool,
onClearTimeline: PropTypes.func.isRequired,
onScrollToTop: PropTypes.func.isRequired,
onScroll: PropTypes.func.isRequired,
}
export default connect(mapStateToProps, mapDispatchToProps)(ChatMessageScrollingList)

View File

@@ -6,7 +6,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'
import Heading from '../../../components/heading'
import Button from '../../../components/button'
class MessagesHeader extends ImmutablePureComponent {
class ChatSettingsHeader extends ImmutablePureComponent {
render() {
const {
@@ -16,27 +16,19 @@ class MessagesHeader extends ImmutablePureComponent {
return (
<div className={[_s.d, _s.w100PC, _s.h60PX, _s.borderBottom1PX, _s.borderColorSecondary].join(' ')}>
<div className={[_s.d, _s.flexRow, _s.pl15, _s.pr5, _s.py15].join(' ')}>
<Button
noClasses
className={[_s.d, _s.noUnderline, _s.jcCenter, _s.mr5, _s.aiCenter, _s.bgTransparent, _s.cursorPointer, _s.outlineNone].join(' ')}
to='/messages'
color='primary'
backgroundColor='none'
icon='angle-left'
iconSize='16px'
iconClassName={[_s.mr5, _s.cPrimary].join(' ')}
/>
<Heading size='h1'>
Messages
Chat Settings
</Heading>
<div className={[_s.d, _s.bgTransparent, _s.flexRow, _s.aiCenter, _s.jcCenter, _s.mlAuto].join(' ')}>
<Button
isNarrow
onClick={undefined}
className={[_s.ml5, _s.px15].join(' ')}
>
New
</Button>
<Button
isNarrow
onClick={undefined}
color='brand'
backgroundColor='none'
className={_s.ml5}
icon='cog'
iconSize='18px'
/>
</div>
</div>
</div>
)
@@ -44,4 +36,4 @@ class MessagesHeader extends ImmutablePureComponent {
}
export default MessagesHeader
export default ChatSettingsHeader

View File

@@ -1,116 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import ImmutablePureComponent from 'react-immutable-pure-component'
import ImmutablePropTypes from 'react-immutable-proptypes'
import { NavLink } from 'react-router-dom'
import { CX } from '../../../constants'
import Input from '../../../components/input'
import Avatar from '../../../components/avatar'
import Button from '../../../components/button'
import Text from '../../../components/text'
import RelativeTimestamp from '../../../components/relative_timestamp'
import { makeGetAccount } from '../../../selectors'
class MessagesItem extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
}
state = {
hovering: false,
}
handleOnMouseEnter = () => {
this.setState({ isHovering: true })
}
handleOnMouseLeave = () => {
this.setState({ isHovering: false })
}
render() {
const {
account,
intl,
alt,
} = this.props
const { isHovering } = this.state
const content = { __html: 'REEEE i heard you have the sauce2?' }
const messageContainerClasses = CX({
d: 1,
flexRow: !alt,
flexRowReverse: alt,
})
const messageInnerContainerClasses = CX({
d: 1,
px15: 1,
py5: 1,
bgSecondary: !alt,
bgBrandLight: alt,
circle: 1,
ml10: 1,
mr10: 1,
})
const lowerContainerClasses = CX({
d: 1,
pt10: 1,
pl50: !alt,
pr50: alt,
})
const buttonContainerClasses = CX({
d: 1,
flexRow: 1,
displayNone: !isHovering,
})
return (
<div
className={[_s.d, _s.w100PC, _s.pb10].join(' ')}
onMouseEnter={this.handleOnMouseEnter}
onMouseLeave={this.handleOnMouseLeave}
>
<div className={[_s.d, _s.w100PC, _s.pb15].join(' ')}>
<div className={messageContainerClasses}>
<Avatar account={account} size={38} />
<div className={messageInnerContainerClasses}>
<div className={[_s.py5, _s.dangerousContent].join(' ')} dangerouslySetInnerHTML={content} />
</div>
<div className={buttonContainerClasses}>
<Button
onClick={undefined}
color='tertiary'
backgroundColor='none'
icon='ellipsis'
iconSize='18px'
/>
</div>
</div>
<div className={lowerContainerClasses}>
<Text size='small' color='tertiary' align={alt ? 'right' : 'left'}>
Apr 16, 2020, 8:20 AM
{ /* <RelativeTimestamp timestamp={'2020-20-10'} /> */ }
</Text>
</div>
</div>
</div>
)
}
}
const mapStateToProps = (state, props) => ({
account: makeGetAccount()(state, '1'),
})
MessagesItem.propTypes = {
intl: PropTypes.object.isRequired,
alt: PropTypes.bool,
}
export default connect(mapStateToProps)(MessagesItem)

View File

@@ -1,36 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import ImmutablePureComponent from 'react-immutable-pure-component'
import ImmutablePropTypes from 'react-immutable-proptypes'
import MessagesListItem from './messages_list_item'
import { makeGetAccount } from '../../../selectors'
class MessagesList extends ImmutablePureComponent {
render() {
const {
account,
} = this.props
return (
<div className={[_s.d, _s.w100PC].join(' ')}>
<MessagesListItem />
<MessagesListItem selected />
<MessagesListItem />
<MessagesListItem />
</div>
)
}
}
const mapStateToProps = (state, props) => ({
account: makeGetAccount()(state, '1'),
})
MessagesList.propTypes = {
intl: PropTypes.object.isRequired,
}
export default connect(mapStateToProps)(MessagesList)

View File

@@ -1,107 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import ImmutablePureComponent from 'react-immutable-pure-component'
import ImmutablePropTypes from 'react-immutable-proptypes'
import { NavLink } from 'react-router-dom'
import { CX } from '../../../constants'
import Input from '../../../components/input'
import DisplayName from '../../../components/display_name'
import Avatar from '../../../components/avatar'
import Text from '../../../components/text'
import RelativeTimestamp from '../../../components/relative_timestamp'
import { makeGetAccount } from '../../../selectors'
class MessagesListItem extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
}
state = {
composeFocused: false,
}
render() {
const {
account,
intl,
selected,
} = this.props
const buttonClasses = CX({
d: 1,
pt2: 1,
pr5: 1,
noUnderline: 1,
overflowHidden: 1,
flexNormal: 1,
flexRow: 1,
aiStart: 1,
aiCenter: 1,
})
const containerClasses = CX({
d: 1,
bgSubtle_onHover: 1,
borderBottom1PX: 1,
borderColorSecondary: 1,
noUnderline: 1,
})
const innerContainerClasses = CX({
d: 1,
flexRow: 1,
aiStart: 1,
aiCenter: 0,
px15: 1,
py15: 1,
borderRight4PX: selected,
borderColorBrand: selected,
})
const avatarSize = 49
const content = { __html: 'REEEE i heard you have the sauce?' }
return (
<NavLink
className={containerClasses}
title={account.get('acct')}
to={`/messages/conversation-id`}
>
<div className={innerContainerClasses}>
<Avatar account={account} size={avatarSize} noHover />
<div className={[_s.d, _s.pl10, _s.overflowHidden, _s.flexNormal].join(' ')}>
<div className={[_s.d, _s.flexRow, _s.aiCenter].join(' ')}>
<div className={buttonClasses}>
<div className={_s.maxW100PC42PX}>
<DisplayName account={account} noHover />
</div>
<Text size='extraSmall' color='secondary' className={_s.mlAuto}>
May 1
{ /* <RelativeTimestamp timestamp={'2020-20-10'} /> */ }
</Text>
</div>
</div>
<div className={[_s.py5, _s.dangerousContent].join(' ')} dangerouslySetInnerHTML={content} />
</div>
</div>
</NavLink>
)
}
}
const mapStateToProps = (state, props) => ({
account: makeGetAccount()(state, '1'),
})
MessagesListItem.propTypes = {
intl: PropTypes.object.isRequired,
selected: PropTypes.bool,
}
export default connect(mapStateToProps)(MessagesListItem)