Progress on DMs responsiveness
Progress on DMs responsiveness
This commit is contained in:
parent
137a36b810
commit
f6a7422704
|
@ -1 +1 @@
|
|||
2.6.5
|
||||
2.6.1
|
||||
|
|
|
@ -4,7 +4,7 @@ class Api::BaseController < ApplicationController
|
|||
DEFAULT_STATUSES_LIMIT = 20
|
||||
DEFAULT_ACCOUNTS_LIMIT = 20
|
||||
DEFAULT_CHAT_CONVERSATION_LIMIT = 12
|
||||
DEFAULT_CHAT_CONVERSATION_MESSAGE_LIMIT = 10
|
||||
DEFAULT_CHAT_CONVERSATION_MESSAGE_LIMIT = 20
|
||||
|
||||
include RateLimitHeaders
|
||||
|
||||
|
|
|
@ -55,7 +55,7 @@ class Api::V1::ChatConversations::MessagesController < Api::BaseController
|
|||
end
|
||||
|
||||
def insert_pagination_headers
|
||||
set_pagination_headers(next_path, nil)
|
||||
set_pagination_headers(next_path, prev_path)
|
||||
end
|
||||
|
||||
def pagination_params(core_params)
|
||||
|
@ -63,10 +63,27 @@ class Api::V1::ChatConversations::MessagesController < Api::BaseController
|
|||
end
|
||||
|
||||
def next_path
|
||||
api_v1_chat_conversations_message_url params[:id], pagination_params(since_id: pagination_since_id)
|
||||
if records_continue?
|
||||
api_v1_chat_conversations_message_url params[:id], pagination_params(max_id: pagination_max_id)
|
||||
end
|
||||
end
|
||||
|
||||
def prev_path
|
||||
unless @chats.empty?
|
||||
api_v1_chat_conversations_message_url params[:id], pagination_params(since_id: pagination_since_id)
|
||||
end
|
||||
end
|
||||
|
||||
def pagination_max_id
|
||||
@chats.last.id
|
||||
end
|
||||
|
||||
def pagination_since_id
|
||||
@chats.first.id
|
||||
end
|
||||
|
||||
def records_continue?
|
||||
@chats.size == limit_param(DEFAULT_CHAT_CONVERSATION_MESSAGE_LIMIT)
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -72,15 +72,17 @@ export const expandChatMessages = (chatConversationId, params = {}, done = noop)
|
|||
|
||||
dispatch(expandChatMessagesRequest(chatConversationId, isLoadingMore))
|
||||
|
||||
api(getState).get(`/api/v1/chat_conversations/messages/${chatConversationId}`, { params }).then((response) => {
|
||||
console.log("response:", response)
|
||||
api(getState).get(`/api/v1/chat_conversations/messages/${chatConversationId}`, {
|
||||
params: {
|
||||
max_id: params.maxId,
|
||||
since_id: params.sinceId,
|
||||
}
|
||||
}).then((response) => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next')
|
||||
console.log("next:", next, getLinks(response).refs)
|
||||
dispatch(importFetchedChatMessages(response.data))
|
||||
dispatch(expandChatMessagesSuccess(chatConversationId, response.data, next ? next.uri : null, response.code === 206, isLoadingRecent, isLoadingMore))
|
||||
done()
|
||||
}).catch((error) => {
|
||||
console.log("error:", error)
|
||||
dispatch(expandChatMessagesFail(chatConversationId, error, isLoadingMore))
|
||||
done()
|
||||
})
|
||||
|
|
|
@ -18,6 +18,7 @@ class AvatarGroup extends ImmutablePureComponent {
|
|||
return (
|
||||
<div className={[_s.d].join(' ')}>
|
||||
{
|
||||
!!accounts &&
|
||||
accounts.map((account) => {
|
||||
const isPro = account.get('is_pro')
|
||||
const alt = `${account.get('display_name')} ${isPro ? '(PRO)' : ''}`.trim()
|
||||
|
|
|
@ -19,6 +19,7 @@ class BackButton extends React.PureComponent {
|
|||
|
||||
handleBackClick = () => {
|
||||
this.historyBack()
|
||||
if (!!this.props.onClick) this.props.onClick()
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes'
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component'
|
||||
import { makeGetChatConversation } from '../../selectors'
|
||||
import { openPopover } from '../../actions/popover'
|
||||
import { setChatConversationSelected } from '../../actions/chats'
|
||||
import { POPOVER_CHAT_CONVERSATION_OPTIONS } from '../../constants'
|
||||
import Heading from '../heading'
|
||||
import Button from '../button'
|
||||
import BackButton from '../back_button'
|
||||
import Text from '../text'
|
||||
import AvatarGroup from '../avatar_group'
|
||||
|
||||
class ChatNavigationBar extends React.PureComponent {
|
||||
|
||||
handleOnOpenChatConversationOptionsPopover = () => {
|
||||
this.props.onOpenChatConversationOptionsPopover(this.props.chatConversationId, this.optionsBtnRef)
|
||||
}
|
||||
|
||||
handleOnBack = () => {
|
||||
this.props.onSetChatConversationSelectedEmpty()
|
||||
}
|
||||
|
||||
setOptionsBtnRef = (c) => {
|
||||
this.optionsBtnRef = c
|
||||
}
|
||||
|
||||
render() {
|
||||
const { chatConversation } = this.props
|
||||
|
||||
const otherAccounts = chatConversation ? chatConversation.get('other_accounts') : null
|
||||
const nameHTML = !!otherAccounts ? otherAccounts.get(0).get('display_name_html') : ''
|
||||
|
||||
return (
|
||||
<div className={[_s.d, _s.z4, _s.h53PX, _s.w100PC].join(' ')}>
|
||||
<div className={[_s.d, _s.h53PX, _s.bgNavigation, _s.aiCenter, _s.z3, _s.top0, _s.right0, _s.left0, _s.posFixed].join(' ')} >
|
||||
|
||||
<div className={[_s.d, _s.flexRow, _s.saveAreaInsetPT, _s.saveAreaInsetPL, _s.saveAreaInsetPR, _s.w100PC].join(' ')}>
|
||||
|
||||
<BackButton
|
||||
className={[_s.h53PX, _s.pl10, _s.pr10].join(' ')}
|
||||
iconSize='18px'
|
||||
onClick={this.handleOnBack}
|
||||
iconClassName={[_s.mr5, _s.fillNavigation].join(' ')}
|
||||
/>
|
||||
|
||||
<div className={[_s.d, _s.h53PX, _s.flexRow, _s.jcCenter, _s.aiCenter, _s.mrAuto].join(' ')}>
|
||||
<AvatarGroup accounts={otherAccounts} size={35} noHover />
|
||||
<Heading size='h1'>
|
||||
<div className={[_s.dangerousContent, _s.pl10, _s.fs19PX].join(' ')} dangerouslySetInnerHTML={{ __html: nameHTML }} />
|
||||
</Heading>
|
||||
</div>
|
||||
|
||||
<div className={[_s.d, _s.h53PX, _s.mlAuto, _s.aiCenter, _s.jcCenter, _s.mr15].join(' ')}>
|
||||
<Button
|
||||
isNarrow
|
||||
backgroundColor='tertiary'
|
||||
color='primary'
|
||||
onClick={this.handleOnOpenChatConversationOptionsPopover}
|
||||
className={[_s.px5].join(' ')}
|
||||
icon='ellipsis'
|
||||
iconClassName={[_s.cSecondary, _s.px5, _s.py5].join(' ')}
|
||||
iconSize='15px'
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, { chatConversationId }) => ({
|
||||
chatConversation: makeGetChatConversation()(state, { id: chatConversationId }),
|
||||
})
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
onSetChatConversationSelectedEmpty() {
|
||||
dispatch(setChatConversationSelected(null))
|
||||
},
|
||||
onOpenChatConversationOptionsPopover(chatConversationId, targetRef) {
|
||||
dispatch(openPopover(POPOVER_CHAT_CONVERSATION_OPTIONS, {
|
||||
chatConversationId,
|
||||
targetRef,
|
||||
position: 'bottom',
|
||||
}))
|
||||
},
|
||||
})
|
||||
|
||||
ChatNavigationBar.propTypes = {
|
||||
chatConversation: ImmutablePropTypes.map,
|
||||
chatConversationId: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ChatNavigationBar)
|
|
@ -42,6 +42,10 @@ class ChatMessageDeletePopover extends React.PureComponent {
|
|||
const mapDispatchToProps = (dispatch) => ({
|
||||
onDeleteChatMessage(chatMessageId) {
|
||||
dispatch(deleteChatMessage(chatMessageId))
|
||||
dispatch(closePopover())
|
||||
},
|
||||
onClosePopover() {
|
||||
dispatch(closePopover())
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
@ -4,12 +4,11 @@ import throttle from 'lodash.throttle'
|
|||
import { List as ImmutableList } from 'immutable'
|
||||
import IntersectionObserverArticle from './intersection_observer_article'
|
||||
import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper'
|
||||
import { MOUSE_IDLE_DELAY } from '../constants'
|
||||
import Block from './block'
|
||||
import ColumnIndicator from './column_indicator'
|
||||
import LoadMore from './load_more'
|
||||
|
||||
const MOUSE_IDLE_DELAY = 300
|
||||
|
||||
class ScrollableList extends React.PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
|
@ -27,7 +26,7 @@ class ScrollableList extends React.PureComponent {
|
|||
lastScrollWasSynthetic = false;
|
||||
scrollToTopOnMouseIdle = false;
|
||||
|
||||
setScrollTop = newScrollTop => {
|
||||
setScrollTop = (newScrollTop) => {
|
||||
if (this.documentElement.scrollTop !== newScrollTop) {
|
||||
this.lastScrollWasSynthetic = true;
|
||||
this.documentElement.scrollTop = newScrollTop;
|
||||
|
@ -104,8 +103,6 @@ class ScrollableList extends React.PureComponent {
|
|||
|
||||
if (scrollTop < 100 && this.props.onScrollToTop) {
|
||||
this.props.onScrollToTop()
|
||||
} else if (scrollTop < 100 && this.props.onScrollToBottom) {
|
||||
this.props.onScrollToBottom()
|
||||
} else if (this.props.onScroll) {
|
||||
this.props.onScroll()
|
||||
}
|
||||
|
@ -194,7 +191,6 @@ class ScrollableList extends React.PureComponent {
|
|||
placeholderComponent: Placeholder,
|
||||
placeholderCount,
|
||||
onScrollToTop,
|
||||
onScrollToBottom,
|
||||
} = this.props
|
||||
const childrenCount = React.Children.count(children);
|
||||
|
||||
|
@ -221,16 +217,6 @@ class ScrollableList extends React.PureComponent {
|
|||
return (
|
||||
<div onMouseMove={this.handleMouseMove} ref={this.setRef}>
|
||||
<div role='feed'>
|
||||
{
|
||||
(hasMore && onLoadMore && !isLoading) && !!onScrollToBottom &&
|
||||
<LoadMore onClick={this.handleLoadMore} />
|
||||
}
|
||||
|
||||
{
|
||||
isLoading && !!onScrollToBottom &&
|
||||
<ColumnIndicator type='loading' />
|
||||
}
|
||||
|
||||
{
|
||||
!!this.props.children &&
|
||||
React.Children.map(this.props.children, (child, index) => (
|
||||
|
@ -287,7 +273,6 @@ ScrollableList.propTypes = {
|
|||
]),
|
||||
children: PropTypes.node,
|
||||
onScrollToTop: PropTypes.func,
|
||||
onScrollToBottom: PropTypes.func,
|
||||
onScroll: PropTypes.func,
|
||||
placeholderComponent: PropTypes.node,
|
||||
placeholderCount: PropTypes.number,
|
||||
|
|
|
@ -51,7 +51,6 @@ class TimelineInjectionRoot extends React.PureComponent {
|
|||
|
||||
handleResize = () => {
|
||||
const { width } = getWindowDimension()
|
||||
|
||||
this.setState({ width })
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,8 @@ export const BREAKPOINT_MEDIUM = 1160
|
|||
export const BREAKPOINT_SMALL = 1080
|
||||
export const BREAKPOINT_EXTRA_SMALL = 992
|
||||
|
||||
export const MOUSE_IDLE_DELAY = 300
|
||||
|
||||
export const LAZY_LOAD_SCROLL_OFFSET = 50
|
||||
|
||||
export const ALLOWED_AROUND_SHORT_CODE = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d'
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import ResponsiveClassesComponent from '../../ui/util/responsive_classes_component'
|
||||
import ChatConversationsSearch from './chat_conversations_search'
|
||||
import ChatConversationsList from './chat_conversations_list'
|
||||
import ChatConversationRequestsListItem from './chat_conversations_requests_list_item'
|
||||
|
||||
class ChatApprovedConversationsSidebar extends React.PureComponent {
|
||||
|
||||
render() {
|
||||
const { source } = this.props
|
||||
|
||||
return (
|
||||
<ResponsiveClassesComponent
|
||||
classNames={[_s.d, _s.w340PX, _s.h100PC, _s.bgPrimary, _s.borderLeft1PX, _s.borderRight1PX, _s.borderColorSecondary].join(' ')}
|
||||
classNamesSmall={[_s.d, _s.w300PX, _s.h100PC, _s.bgPrimary, _s.borderLeft1PX, _s.borderRight1PX, _s.borderColorSecondary].join(' ')}
|
||||
classNamesXS={[_s.d, _s.w100PC, _s.h100PC, _s.overflowYScroll, _s.bgPrimary].join(' ')}
|
||||
>
|
||||
<div className={[_s.d, _s.h100PC, _s.overflowHidden, _s.w100PC, _s.boxShadowNone].join(' ')}>
|
||||
<ChatConversationsSearch />
|
||||
<ResponsiveClassesComponent
|
||||
classNames={[_s.d, _s.w100PC, _s.posAbs, _s.bottom0, _s.top60PX, _s.overflowYScroll].join(' ')}
|
||||
classNamesXS={[_s.d, _s.w100PC].join(' ')}
|
||||
>
|
||||
<ChatConversationRequestsListItem />
|
||||
<ChatConversationsList source={source} />
|
||||
</ResponsiveClassesComponent>
|
||||
</div>
|
||||
</ResponsiveClassesComponent>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ChatApprovedConversationsSidebar.propTypes = {
|
||||
source: PropTypes.string,
|
||||
}
|
||||
|
||||
export default ChatApprovedConversationsSidebar
|
|
@ -0,0 +1,61 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import { openPopover } from '../../../actions/popover'
|
||||
import { approveChatConversationRequest } from '../../../actions/chat_conversations'
|
||||
import {
|
||||
POPOVER_CHAT_CONVERSATION_OPTIONS
|
||||
} from '../../../constants'
|
||||
import Button from '../../../components/button'
|
||||
import Text from '../../../components/text'
|
||||
|
||||
class ChatConversationRequestApproveBar extends React.PureComponent {
|
||||
|
||||
handleOnApproveMessageRequest = () => {
|
||||
this.props.onApproveChatConversationRequest(this.props.chatConversationId)
|
||||
}
|
||||
|
||||
setOptionsBtnRef = (c) => {
|
||||
this.optionsBtnRef = c
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className={[_s.d, _s.z4, _s.minH53PX, _s.w100PC].join(' ')}>
|
||||
<div className={[_s.d, _s.minH53PX, _s.bgNavigation, _s.aiCenter, _s.z3, _s.bottom0, _s.right0, _s.left0, _s.posFixed].join(' ')} >
|
||||
<div className={[_s.d, _s.w100PC, _s.pt15, _s.px15, _s.aiCenter, _s.jcCenter, _s.saveAreaInsetPB, _s.saveAreaInsetPL, _s.saveAreaInsetPR, _s.w100PC].join(' ')}>
|
||||
<Button
|
||||
isNarrow
|
||||
onClick={this.handleOnApproveMessageRequest}
|
||||
>
|
||||
<Text color='inherit' align='center'>
|
||||
Approve Message Request
|
||||
</Text>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
onApproveChatConversationRequest(chatConversationId) {
|
||||
dispatch(approveChatConversationRequest(chatConversationId))
|
||||
},
|
||||
onOpenChatConversationOptionsPopover(chatConversationId, targetRef) {
|
||||
dispatch(openPopover(POPOVER_CHAT_CONVERSATION_OPTIONS, {
|
||||
chatConversationId,
|
||||
targetRef,
|
||||
position: 'bottom',
|
||||
}))
|
||||
},
|
||||
})
|
||||
|
||||
ChatConversationRequestApproveBar.propTypes = {
|
||||
chatConversationId: PropTypes.string,
|
||||
onApproveChatConversationRequest: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
export default connect(null, mapDispatchToProps)(ChatConversationRequestApproveBar)
|
|
@ -28,55 +28,51 @@ class ChatMessagesComposeForm extends React.PureComponent {
|
|||
}
|
||||
|
||||
onBlur = () => {
|
||||
this.setState({ focused: false });
|
||||
this.setState({ focused: false })
|
||||
}
|
||||
|
||||
onFocus = () => {
|
||||
this.setState({ focused: true });
|
||||
this.setState({ focused: true })
|
||||
}
|
||||
|
||||
onKeyDown = (e) => {
|
||||
const { disabled } = this.props;
|
||||
const { disabled } = this.props
|
||||
|
||||
if (disabled) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (disabled) return e.preventDefault()
|
||||
|
||||
// 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;
|
||||
if (e.which === 229) return
|
||||
|
||||
switch (e.key) {
|
||||
case 'Escape':
|
||||
document.querySelector('#gabsocial').focus()
|
||||
break;
|
||||
break
|
||||
case 'Enter':
|
||||
this.handleOnSendChatMessage()
|
||||
return e.preventDefault()
|
||||
case 'Tab':
|
||||
//
|
||||
break;
|
||||
this.sendBtn.focus()
|
||||
return e.preventDefault()
|
||||
break
|
||||
}
|
||||
|
||||
// if (e.defaultPrevented || !this.props.onKeyDown) return;
|
||||
if (e.defaultPrevented) return
|
||||
}
|
||||
|
||||
setTextbox = (c) => {
|
||||
this.textbox = c
|
||||
}
|
||||
|
||||
setSendBtn = (c) => {
|
||||
this.sendBtn = c
|
||||
}
|
||||
|
||||
render () {
|
||||
const { chatConversationId } = this.props
|
||||
const { isXS, 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,
|
||||
|
@ -95,31 +91,59 @@ class ChatMessagesComposeForm extends React.PureComponent {
|
|||
py10: 1,
|
||||
})
|
||||
|
||||
const textarea = (
|
||||
<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'
|
||||
/>
|
||||
)
|
||||
|
||||
const button = (
|
||||
<Button
|
||||
buttonRef={this.setSendBtn}
|
||||
disabled={disabled}
|
||||
onClick={this.handleOnSendChatMessage}
|
||||
>
|
||||
<Text color='inherit' className={_s.px10}>Send</Text>
|
||||
</Button>
|
||||
)
|
||||
|
||||
if (isXS) {
|
||||
return (
|
||||
<div className={[_s.d, _s.z4, _s.minH58PX, _s.w100PC].join(' ')}>
|
||||
<div className={[_s.d, _s.minH58PX, _s.bgPrimary, _s.aiCenter, _s.z3, _s.bottom0, _s.right0, _s.left0, _s.posFixed].join(' ')} >
|
||||
<div className={[_s.d, _s.w100PC, _s.pb5, _s.px15, _s.aiCenter, _s.jcCenter, _s.saveAreaInsetPB, _s.saveAreaInsetPL, _s.saveAreaInsetPR, _s.w100PC].join(' ')}>
|
||||
<div className={[_s.d, _s.flexRow, _s.aiCenter, _s.minH58PX, _s.w100PC, _s.borderTop1PX, _s.borderColorSecondary, _s.px10].join(' ')}>
|
||||
<div className={[_s.d, _s.pr15, _s.flexGrow1, _s.py10].join(' ')}>
|
||||
{textarea}
|
||||
</div>
|
||||
<div className={[_s.d, _s.h100PC, _s.aiCenter, _s.jcCenter].join(' ')}>
|
||||
{button}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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].join(' ')}>
|
||||
<div className={[_s.d, _s.pr15, _s.flexGrow1, _s.py10].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'
|
||||
/>
|
||||
{textarea}
|
||||
</div>
|
||||
<div className={[_s.d, _s.h100PC, _s.aiCenter, _s.jcCenter].join(' ')}>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
onClick={this.handleOnSendChatMessage}
|
||||
>
|
||||
<Text color='inherit' className={_s.px10}>Send</Text>
|
||||
</Button>
|
||||
{button}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -135,6 +159,7 @@ const mapDispatchToProps = (dispatch) => ({
|
|||
|
||||
ChatMessagesComposeForm.propTypes = {
|
||||
chatConversationId: PropTypes.string,
|
||||
isXS: PropTypes.bool,
|
||||
onSendMessage: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
|
|
|
@ -75,7 +75,7 @@ class ChatMessageItem extends ImmutablePureComponent {
|
|||
const account = chatMessage.get('account')
|
||||
if (!account) return <div />
|
||||
|
||||
const content = { __html: chatMessage.get('text') }
|
||||
const content = { __html: chatMessage.get('text_html') }
|
||||
const alt = account.get('id', null) === me
|
||||
const createdAt = chatMessage.get('created_at')
|
||||
|
||||
|
|
|
@ -2,20 +2,25 @@ import React from 'react'
|
|||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import moment from 'moment-mini'
|
||||
import throttle from 'lodash.throttle'
|
||||
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 { CX, MOUSE_IDLE_DELAY } from '../../../constants'
|
||||
import { setChatConversationSelected } from '../../../actions/chats'
|
||||
import {
|
||||
expandChatMessages,
|
||||
scrollBottomChatMessageConversation,
|
||||
} from '../../../actions/chat_conversation_messages'
|
||||
import ScrollableList from '../../../components/scrollable_list'
|
||||
import IntersectionObserverArticle from '../../../components/intersection_observer_article'
|
||||
import IntersectionObserverWrapper from '../../ui/util/intersection_observer_wrapper'
|
||||
import ChatMessagePlaceholder from '../../../components/placeholder/chat_message_placeholder'
|
||||
import ChatMessageItem from './chat_message_item'
|
||||
import ColumnIndicator from '../../../components/column_indicator'
|
||||
import LoadMore from '../../../components/load_more'
|
||||
|
||||
class ChatMessageScrollingList extends ImmutablePureComponent {
|
||||
|
||||
|
@ -23,6 +28,13 @@ class ChatMessageScrollingList extends ImmutablePureComponent {
|
|||
isRefreshing: false,
|
||||
}
|
||||
|
||||
intersectionObserverWrapper = new IntersectionObserverWrapper()
|
||||
|
||||
mouseIdleTimer = null
|
||||
mouseMovedRecently = false
|
||||
lastScrollWasSynthetic = false
|
||||
scrollToTopOnMouseIdle = false
|
||||
|
||||
componentDidMount () {
|
||||
const { chatConversationId } = this.props
|
||||
this.props.onExpandChatMessages(chatConversationId)
|
||||
|
@ -30,6 +42,8 @@ class ChatMessageScrollingList extends ImmutablePureComponent {
|
|||
|
||||
componentWillUnmount() {
|
||||
this.props.onSetChatConversationSelected(null)
|
||||
this.detachScrollListener()
|
||||
this.detachIntersectionObserver()
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
|
@ -40,20 +54,48 @@ class ChatMessageScrollingList extends ImmutablePureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
handleLoadMore = (sinceId) => {
|
||||
const { chatConversationId, dispatch } = this.props
|
||||
this.props.onExpandChatMessages(chatConversationId, { sinceId })
|
||||
}
|
||||
componentDidUpdate(prevProps, prevState, snapshot) {
|
||||
// Reset the scroll position when a new child comes in in order not to
|
||||
// jerk the scrollbar around if you're already scrolled down the page.
|
||||
if (snapshot !== null && this.scrollContainerRef) {
|
||||
console.log("snapshot:", snapshot)
|
||||
this.setScrollTop(this.scrollContainerRef.scrollHeight - snapshot)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
if (prevProps.chatMessageIds.size === 0 && this.props.chatMessageIds.size > 0 && this.scrollContainerRef) {
|
||||
this.scrollContainerRef.scrollTop = this.scrollContainerRef.scrollHeight
|
||||
}
|
||||
}
|
||||
|
||||
attachScrollListener() {
|
||||
if (!this.scrollContainerRef) return
|
||||
this.scrollContainerRef.addEventListener('scroll', this.handleScroll)
|
||||
this.scrollContainerRef.addEventListener('wheel', this.handleWheel)
|
||||
}
|
||||
|
||||
detachScrollListener() {
|
||||
if (!this.scrollContainerRef) return
|
||||
this.scrollContainerRef.removeEventListener('scroll', this.handleScroll)
|
||||
this.scrollContainerRef.removeEventListener('wheel', this.handleWheel)
|
||||
}
|
||||
|
||||
attachIntersectionObserver() {
|
||||
this.intersectionObserverWrapper.connect()
|
||||
}
|
||||
|
||||
detachIntersectionObserver() {
|
||||
this.intersectionObserverWrapper.disconnect()
|
||||
}
|
||||
|
||||
onLoadMore = (maxId) => {
|
||||
const { chatConversationId } = this.props
|
||||
this.props.onExpandChatMessages(chatConversationId, { maxId })
|
||||
}
|
||||
|
||||
getCurrentChatMessageIndex = (id) => {
|
||||
// : todo :
|
||||
return this.props.chatMessageIds.indexOf(id)
|
||||
|
@ -69,14 +111,13 @@ class ChatMessageScrollingList extends ImmutablePureComponent {
|
|||
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 })
|
||||
setScrollTop = (newScrollTop) => {
|
||||
if (!this.scrollContainerRef) return
|
||||
if (this.scrollContainerRef.scrollTop !== newScrollTop) {
|
||||
this.lastScrollWasSynthetic = true
|
||||
this.scrollContainerRef.scrollTop = newScrollTop
|
||||
}
|
||||
}
|
||||
|
||||
_selectChild(index, align_top) {
|
||||
const container = this.node.node
|
||||
|
@ -92,6 +133,83 @@ class ChatMessageScrollingList extends ImmutablePureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
handleLoadOlder = debounce(() => {
|
||||
const maxId = this.props.chatMessageIds.size > 0 ? this.props.chatMessageIds.last() : undefined
|
||||
this.onLoadMore(maxId)
|
||||
}, 300, { leading: true })
|
||||
|
||||
handleScroll = throttle(() => {
|
||||
if (this.scrollContainerRef) {
|
||||
const { offsetHeight, scrollTop, scrollHeight } = this.scrollContainerRef
|
||||
const offset = scrollHeight - scrollTop - offsetHeight
|
||||
|
||||
if (scrollTop < 100 && this.props.hasMore && !this.props.isLoading) {
|
||||
this.handleLoadOlder()
|
||||
}
|
||||
|
||||
if (offset < 100) {
|
||||
this.props.onScrollToBottom()
|
||||
}
|
||||
|
||||
if (!this.lastScrollWasSynthetic) {
|
||||
// If the last scroll wasn't caused by setScrollTop(), assume it was
|
||||
// intentional and cancel any pending scroll reset on mouse idle
|
||||
this.scrollToTopOnMouseIdle = false
|
||||
}
|
||||
this.lastScrollWasSynthetic = false
|
||||
}
|
||||
}, 150, {
|
||||
trailing: true,
|
||||
})
|
||||
|
||||
handleWheel = throttle(() => {
|
||||
this.scrollToTopOnMouseIdle = false
|
||||
}, 150, {
|
||||
trailing: true,
|
||||
})
|
||||
|
||||
clearMouseIdleTimer = () => {
|
||||
if (this.mouseIdleTimer === null) return
|
||||
|
||||
clearTimeout(this.mouseIdleTimer)
|
||||
this.mouseIdleTimer = null
|
||||
}
|
||||
|
||||
handleMouseMove = throttle(() => {
|
||||
// As long as the mouse keeps moving, clear and restart the idle timer.
|
||||
this.clearMouseIdleTimer()
|
||||
this.mouseIdleTimer = setTimeout(this.handleMouseIdle, MOUSE_IDLE_DELAY)
|
||||
|
||||
// Only set if we just started moving and are scrolled to the top.
|
||||
if (!this.mouseMovedRecently && this.scrollContainerRef.scrollTop === 0) {
|
||||
this.scrollToTopOnMouseIdle = true
|
||||
}
|
||||
|
||||
// Save setting this flag for last, so we can do the comparison above.
|
||||
this.mouseMovedRecently = true
|
||||
}, MOUSE_IDLE_DELAY / 2)
|
||||
|
||||
handleMouseIdle = () => {
|
||||
if (this.scrollToTopOnMouseIdle) {
|
||||
this.setScrollTop(0)
|
||||
}
|
||||
|
||||
this.mouseMovedRecently = false
|
||||
this.scrollToTopOnMouseIdle = false
|
||||
}
|
||||
|
||||
getSnapshotBeforeUpdate(prevProps) {
|
||||
const someItemInserted = prevProps.chatMessageIds.size > 0 &&
|
||||
prevProps.chatMessageIds.size < this.props.chatMessageIds.size &&
|
||||
prevProps.chatMessageIds.get(prevProps.chatMessageIds.size - 1) !== this.props.chatMessageIds.get(this.props.chatMessageIds.size - 1)
|
||||
|
||||
if (someItemInserted && (this.scrollContainerRef.scrollTop > 0 || this.mouseMovedRecently)) {
|
||||
return this.scrollContainerRef.scrollHeight - this.scrollContainerRef.scrollTop
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
setRef = (c) => {
|
||||
this.node = c
|
||||
}
|
||||
|
@ -100,6 +218,15 @@ class ChatMessageScrollingList extends ImmutablePureComponent {
|
|||
this.containerNode = c
|
||||
}
|
||||
|
||||
setScrollContainerRef = (c) => {
|
||||
this.scrollContainerRef = c
|
||||
|
||||
this.attachScrollListener()
|
||||
this.attachIntersectionObserver()
|
||||
// Handle initial scroll posiiton
|
||||
this.handleScroll()
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
chatConversationId,
|
||||
|
@ -109,16 +236,13 @@ class ChatMessageScrollingList extends ImmutablePureComponent {
|
|||
hasMore,
|
||||
onScrollToBottom,
|
||||
onScroll,
|
||||
isXS,
|
||||
} = 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)
|
||||
|
@ -128,8 +252,8 @@ class ChatMessageScrollingList extends ImmutablePureComponent {
|
|||
<div
|
||||
key={`chat-message-gap:${(i + 1)}`}
|
||||
disabled={isLoading}
|
||||
sinceId={i > 0 ? chatMessageIds.get(i - 1) : null}
|
||||
onClick={this.handleLoadMore}
|
||||
maxId={i > 0 ? chatMessageIds.get(i - 1) : null}
|
||||
onClick={this.handleLoadOlder}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
|
@ -148,23 +272,67 @@ class ChatMessageScrollingList extends ImmutablePureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
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}
|
||||
const childrenCount = React.Children.count(scrollableContent)
|
||||
if (isLoading || childrenCount > 0 || hasMore) {
|
||||
const containerClasses = CX({
|
||||
d: 1,
|
||||
bgPrimary: 1,
|
||||
boxShadowNone: 1,
|
||||
posAbs: !isXS,
|
||||
bottom60PX: !isXS,
|
||||
left0: !isXS,
|
||||
right0: !isXS,
|
||||
top60PX: !isXS,
|
||||
w100PC: 1,
|
||||
overflowHidden: 1,
|
||||
})
|
||||
return (
|
||||
<div
|
||||
onMouseMove={this.handleMouseMove}
|
||||
className={containerClasses}
|
||||
ref={this.containerRef}
|
||||
>
|
||||
{scrollableContent}
|
||||
</ScrollableList>
|
||||
<div
|
||||
className={[_s.d, _s.h100PC, _s.w100PC, _s.px15, _s.py15, _s.overflowYScroll].join(' ')}
|
||||
ref={this.setScrollContainerRef}
|
||||
>
|
||||
{
|
||||
(hasMore && !isLoading) &&
|
||||
<LoadMore onClick={this.handleLoadOlder} />
|
||||
}
|
||||
|
||||
{
|
||||
isLoading &&
|
||||
<ColumnIndicator type='loading' />
|
||||
}
|
||||
|
||||
<div role='feed'>
|
||||
{
|
||||
!!scrollableContent &&
|
||||
scrollableContent.map((child, index) => (
|
||||
<IntersectionObserverArticle
|
||||
key={`chat_message:${chatConversationId}:${index}`}
|
||||
id={`chat_message:${chatConversationId}:${index}`}
|
||||
index={index}
|
||||
listLength={childrenCount}
|
||||
intersectionObserverWrapper={this.intersectionObserverWrapper}
|
||||
saveHeightKey={`chat_messages:${chatConversationId}:${index}`}
|
||||
>
|
||||
{child}
|
||||
</IntersectionObserverArticle>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={[_s.d, _s.boxShadowNone, _s.posAbs, _s.bottom60PX, _s.left0, _s.right0, _s.top60PX, _s.w100PC, _s.overflowHidden].join(' ')}>
|
||||
<div className={[_s.d, _s.h100PC, _s.w100PC, _s.px15, _s.py15, _s.overflowYScroll].join(' ')}>
|
||||
<ColumnIndicator type='error' message='No chat messages found' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -207,6 +375,7 @@ ChatMessageScrollingList.propTypes = {
|
|||
onClearTimeline: PropTypes.func.isRequired,
|
||||
onScrollToTop: PropTypes.func.isRequired,
|
||||
onScroll: PropTypes.func.isRequired,
|
||||
isXS: PropTypes.bool.isRequired,
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ChatMessageScrollingList)
|
|
@ -0,0 +1,42 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import ResponsiveClassesComponent from '../../ui/util/responsive_classes_component'
|
||||
import ChatSettingsHeader from './chat_settings_header'
|
||||
import List from '../../../components/list'
|
||||
|
||||
class ChatSettingsSidebar extends React.PureComponent {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ResponsiveClassesComponent
|
||||
classNames={[_s.d, _s.w340PX, _s.h100PC, _s.bgPrimary, _s.borderLeft1PX, _s.borderRight1PX, _s.borderColorSecondary].join(' ')}
|
||||
classNamesSmall={[_s.d, _s.w300PX, _s.h100PC, _s.bgPrimary, _s.borderLeft1PX, _s.borderRight1PX, _s.borderColorSecondary].join(' ')}
|
||||
>
|
||||
<ChatSettingsHeader />
|
||||
<List
|
||||
items={[
|
||||
{
|
||||
title: 'Preferences',
|
||||
to: '/messages/settings',
|
||||
},
|
||||
{
|
||||
title: 'Message Requests',
|
||||
to: '/messages/requests',
|
||||
},
|
||||
{
|
||||
title: 'Blocked Chats',
|
||||
to: '/messages/blocks',
|
||||
},
|
||||
{
|
||||
title: 'Muted Chats',
|
||||
to: '/messages/mutes',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</ResponsiveClassesComponent>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default ChatSettingsSidebar
|
|
@ -10,7 +10,7 @@ class Messages extends React.PureComponent {
|
|||
|
||||
render () {
|
||||
const {
|
||||
account,
|
||||
isXS,
|
||||
selectedChatConversationId,
|
||||
chatConverationIsRequest,
|
||||
} = this.props
|
||||
|
@ -48,6 +48,7 @@ const mapStateToProps = (state, props) => {
|
|||
}
|
||||
|
||||
Messages.propTypes = {
|
||||
isXS: PropTypes.bool,
|
||||
selectedChatConversationId: PropTypes.string,
|
||||
chatConverationIsRequest: PropTypes.bool.isRequired,
|
||||
}
|
||||
|
|
|
@ -204,6 +204,7 @@ class SwitchingArea extends React.PureComponent {
|
|||
<WrappedRoute path='/news/view/:trendsRSSId' page={NewsPage} component={NewsView} content={children} componentParams={{ title: 'News RSS Feed' }} />
|
||||
|
||||
<WrappedRoute path='/messages' exact page={MessagesPage} component={Messages} content={children} componentParams={{ source: 'approved' }} />
|
||||
<WrappedRoute path='/messages/new' exact page={BasicPage} component={ChatConversationCreate} content={children} componentParams={{ title: 'New Message' }} />
|
||||
<WrappedRoute path='/messages/settings' exact page={MessagesPage} component={MessagesSettings} content={children} componentParams={{ isSettings: true }} />
|
||||
<WrappedRoute path='/messages/requests' exact page={MessagesPage} component={ChatConversationRequests} content={children} componentParams={{ isSettings: true, source: 'requested' }} />
|
||||
<WrappedRoute path='/messages/blocks' exact page={MessagesPage} component={ChatConversationBlockedAccounts} content={children} componentParams={{ isSettings: true }} />
|
||||
|
|
|
@ -23,9 +23,9 @@ export default class IntersectionObserverWrapper {
|
|||
};
|
||||
|
||||
this.observer = new IntersectionObserver(onIntersection, options);
|
||||
this.observerBacklog.forEach(([ id, node, callback ]) => {
|
||||
Array.isArray(this.observerBacklog) ? this.observerBacklog.forEach(([ id, node, callback ]) => {
|
||||
this.observe(id, node, callback);
|
||||
});
|
||||
}) : null;
|
||||
this.observerBacklog = null;
|
||||
}
|
||||
|
||||
|
|
|
@ -8,17 +8,41 @@ import {
|
|||
BREAKPOINT_EXTRA_SMALL,
|
||||
MODAL_CHAT_CONVERSATION_CREATE,
|
||||
} from '../constants'
|
||||
import { getWindowDimension } from '../utils/is_mobile'
|
||||
import Layout from './layout'
|
||||
import Responsive from '../features/ui/util/responsive_component'
|
||||
import List from '../components/list'
|
||||
import ResponsiveClassesComponent from '../features/ui/util/responsive_classes_component'
|
||||
import ChatConversationsSearch from '../features/messages/components/chat_conversations_search'
|
||||
import ChatConversationsList from '../features/messages/components/chat_conversations_list'
|
||||
import ChatSettingsHeader from '../features/messages/components/chat_settings_header'
|
||||
import ChatConversationRequestsListItem from '../features/messages/components/chat_conversations_requests_list_item'
|
||||
import ChatSettingsSidebar from '../features/messages/components/chat_settings_sidebar'
|
||||
import ChatApprovedConversationsSidebar from '../features/messages/components/chat_approved_conversations_sidebar'
|
||||
import FooterBar from '../components/footer_bar'
|
||||
import DefaultNavigationBar from '../components/navigation_bar/default_navigation_bar'
|
||||
import ChatNavigationBar from '../components/navigation_bar/chat_navigation_bar_xs'
|
||||
import ChatMessageScrollingList from '../features/messages/components/chat_message_scrolling_list'
|
||||
import ChatMessageComposeForm from '../features/messages/components/chat_message_compose_form'
|
||||
import ChatConversationRequestApproveBar from '../features/messages/components/chat_conversation_request_approve_bar'
|
||||
|
||||
const initialState = getWindowDimension()
|
||||
|
||||
class MessagesLayout extends React.PureComponent {
|
||||
|
||||
state = {
|
||||
width: initialState.width,
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.handleResize()
|
||||
window.addEventListener('resize', this.handleResize, false)
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.handleResize, false)
|
||||
}
|
||||
|
||||
handleResize = () => {
|
||||
const { width } = getWindowDimension()
|
||||
this.setState({ width })
|
||||
}
|
||||
|
||||
onClickAdd = () => {
|
||||
this.props.onOpenChatConversationCreateModal()
|
||||
}
|
||||
|
@ -31,15 +55,54 @@ class MessagesLayout extends React.PureComponent {
|
|||
showBackBtn,
|
||||
source,
|
||||
currentConversationIsRequest,
|
||||
selectedChatConversationId,
|
||||
} = this.props
|
||||
const { width } = this.state
|
||||
|
||||
const mainBlockClasses = CX({
|
||||
d: 1,
|
||||
w1015PX: 1,
|
||||
h100PC: 1,
|
||||
flexRow: 1,
|
||||
jcEnd: 1,
|
||||
})
|
||||
const isXS = width <= BREAKPOINT_EXTRA_SMALL
|
||||
|
||||
if (isXS) {
|
||||
if (!selectedChatConversationId) {
|
||||
return (
|
||||
<div className={[_s.d, _s.w100PC, _s.minH100VH, _s.bgTertiary].join(' ')}>
|
||||
<DefaultNavigationBar
|
||||
showBackBtn
|
||||
actions={[
|
||||
{
|
||||
icon: 'add',
|
||||
to: '/messages/new',
|
||||
}
|
||||
]}
|
||||
title={title}
|
||||
/>
|
||||
<main role='main' className={[_s.d, _s.w100PC].join(' ')}>
|
||||
<div className={[_s.d, _s.w100PC, _s.flexRow, _s.pb15].join(' ')}>
|
||||
{
|
||||
(isSettings || currentConversationIsRequest) &&
|
||||
<ChatSettingsSidebar />
|
||||
}
|
||||
{
|
||||
!isSettings && !currentConversationIsRequest &&
|
||||
<ChatApprovedConversationsSidebar source={source} />
|
||||
}
|
||||
</div>
|
||||
<FooterBar />
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<div className={[_s.d, _s.w100PC, _s.minH100VH, _s.bgTertiary].join(' ')}>
|
||||
<ChatNavigationBar chatConversationId={selectedChatConversationId} />
|
||||
<main role='main' className={[_s.d, _s.w100PC].join(' ')}>
|
||||
<ChatMessageScrollingList chatConversationId={selectedChatConversationId} isXS={isXS} />
|
||||
</main>
|
||||
{ currentConversationIsRequest && <ChatConversationRequestApproveBar /> }
|
||||
{ !currentConversationIsRequest && <ChatMessageComposeForm chatConversationId={selectedChatConversationId} isXS={isXS} /> }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout
|
||||
|
@ -68,57 +131,17 @@ class MessagesLayout extends React.PureComponent {
|
|||
classNamesXS={[_s.d, _s.w100PC].join(' ')}
|
||||
>
|
||||
<ResponsiveClassesComponent
|
||||
classNames={mainBlockClasses}
|
||||
classNamesXS={[_s.d, _s.w100PC, _s.jcEnd].join(' ')}
|
||||
classNames={[_s.d, _s.w1015PX, _s.h100PC, _s.flexRow, _s.jcEnd].join(' ')}
|
||||
classNamesXS={[_s.d, _s.w100PC, _s.h100PC, _s.jcEnd].join(' ')}
|
||||
>
|
||||
|
||||
<Responsive min={BREAKPOINT_EXTRA_SMALL}>
|
||||
<div className={[_s.d, _s.w340PX, _s.h100PC, _s.bgPrimary, _s.borderLeft1PX, _s.borderRight1PX, _s.borderColorSecondary].join(' ')}>
|
||||
<div className={[_s.d, _s.h100PC, _s.overflowHidden, _s.w100PC, _s.boxShadowNone].join(' ')}>
|
||||
|
||||
{
|
||||
(isSettings || currentConversationIsRequest) &&
|
||||
<React.Fragment>
|
||||
<ChatSettingsHeader />
|
||||
<List
|
||||
items={[
|
||||
{
|
||||
title: 'Preferences',
|
||||
to: '/messages/settings',
|
||||
},
|
||||
{
|
||||
title: 'Message Requests',
|
||||
to: '/messages/requests',
|
||||
},
|
||||
{
|
||||
title: 'Blocked Chats',
|
||||
to: '/messages/blocks',
|
||||
},
|
||||
{
|
||||
title: 'Muted Chats',
|
||||
to: '/messages/mutes',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</React.Fragment>
|
||||
}
|
||||
|
||||
{
|
||||
!isSettings && !currentConversationIsRequest &&
|
||||
<React.Fragment>
|
||||
<ChatConversationsSearch />
|
||||
<div className={[_s.d, _s.w100PC, _s.posAbs, _s.bottom0, _s.top60PX, _s.overflowYScroll].join(' ')}>
|
||||
<ChatConversationRequestsListItem />
|
||||
<ChatConversationsList source={source} />
|
||||
</div>
|
||||
</React.Fragment>
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</Responsive>
|
||||
|
||||
|
||||
{
|
||||
(isSettings || currentConversationIsRequest) &&
|
||||
<ChatSettingsSidebar />
|
||||
}
|
||||
{
|
||||
!isSettings && !currentConversationIsRequest &&
|
||||
<ChatApprovedConversationsSidebar source={source} />
|
||||
}
|
||||
<div className={[_s.d, _s.flexGrow1, _s.h100PC, _s.bgPrimary, _s.borderColorSecondary, _s.borderRight1PX, _s.z1].join(' ')}>
|
||||
<div className={[_s.d, _s.w100PC, _s.h100PC].join(' ')}>
|
||||
{children}
|
||||
|
@ -134,10 +157,13 @@ class MessagesLayout extends React.PureComponent {
|
|||
}
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const selectedId = state.getIn(['chats', 'selectedChatConversationId'], null)
|
||||
const currentConversationIsRequest = selectedId ? !state.getIn(['chat_conversations', selectedId, 'is_approved'], true) : false
|
||||
const selectedChatConversationId = state.getIn(['chats', 'selectedChatConversationId'], null)
|
||||
const currentConversationIsRequest = selectedChatConversationId ? !state.getIn(['chat_conversations', selectedChatConversationId, 'is_approved'], true) : false
|
||||
|
||||
return { currentConversationIsRequest }
|
||||
return {
|
||||
selectedChatConversationId,
|
||||
currentConversationIsRequest,
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
|
|
|
@ -11,7 +11,7 @@ class EntityCache
|
|||
Rails.cache.fetch(to_key(:mention, username, domain), expires_in: MAX_EXPIRATION) { Account.select(:username, :domain, :url).find_local(username) }
|
||||
end
|
||||
|
||||
def emoji(shortcodes, domain)
|
||||
def emoji(shortcodes, domain = nil)
|
||||
shortcodes = [shortcodes] unless shortcodes.is_a?(Array)
|
||||
cached = Rails.cache.read_multi(*shortcodes.map { |shortcode| to_key(:emoji, shortcode, domain) })
|
||||
uncached_ids = []
|
||||
|
|
|
@ -80,6 +80,21 @@ class Formatter
|
|||
|
||||
include ActionView::Helpers::TextHelper
|
||||
|
||||
def chatMessageText(chatMessage)
|
||||
raw_content = chatMessage.text
|
||||
|
||||
return '' if raw_content.blank?
|
||||
|
||||
html = raw_content
|
||||
html = encode_and_link_urls(html, nil, keep_html: false)
|
||||
html = reformat(html, true)
|
||||
html = encode_custom_emojis(html, chatMessage.emojis)
|
||||
|
||||
html.html_safe # rubocop:disable Rails/OutputSafety
|
||||
|
||||
html
|
||||
end
|
||||
|
||||
def format(status, **options)
|
||||
if options[:use_markdown]
|
||||
raw_content = status.markdown
|
||||
|
|
|
@ -434,7 +434,7 @@ class Account < ApplicationRecord
|
|||
end
|
||||
|
||||
def emojis
|
||||
@emojis ||= CustomEmoji.from_text(emojifiable_text, domain)
|
||||
@emojis ||= CustomEmoji.from_text(emojifiable_text)
|
||||
end
|
||||
|
||||
before_create :generate_keys
|
||||
|
|
|
@ -25,4 +25,9 @@ class ChatMessage < ApplicationRecord
|
|||
|
||||
scope :recent, -> { reorder(created_at: :desc) }
|
||||
|
||||
def emojis
|
||||
return @emojis if defined?(@emojis)
|
||||
|
||||
@emojis = CustomEmoji.from_text(text)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -54,14 +54,14 @@ class CustomEmoji < ApplicationRecord
|
|||
end
|
||||
|
||||
class << self
|
||||
def from_text(text, domain)
|
||||
def from_text(text)
|
||||
return [] if text.blank?
|
||||
|
||||
shortcodes = text.scan(SCAN_RE).map(&:first).uniq
|
||||
|
||||
return [] if shortcodes.empty?
|
||||
|
||||
EntityCache.instance.emoji(shortcodes, domain)
|
||||
EntityCache.instance.emoji(shortcodes)
|
||||
end
|
||||
|
||||
def search(shortcode)
|
||||
|
@ -72,10 +72,7 @@ class CustomEmoji < ApplicationRecord
|
|||
private
|
||||
|
||||
def remove_entity_cache
|
||||
Rails.cache.delete(EntityCache.instance.to_key(:emoji, shortcode, domain))
|
||||
Rails.cache.delete(EntityCache.instance.to_key(:emoji, shortcode,))
|
||||
end
|
||||
|
||||
def downcase_domain
|
||||
self.domain = domain.downcase unless domain.nil?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -57,7 +57,7 @@ class Poll < ApplicationRecord
|
|||
end
|
||||
|
||||
def emojis
|
||||
@emojis ||= CustomEmoji.from_text(options.join(' '), account.domain)
|
||||
@emojis ||= CustomEmoji.from_text(options.join(' '))
|
||||
end
|
||||
|
||||
class Option < ActiveModelSerializers::Model
|
||||
|
|
|
@ -231,7 +231,7 @@ class Status < ApplicationRecord
|
|||
fields = [spoiler_text, text]
|
||||
fields += preloadable_poll.options unless preloadable_poll.nil?
|
||||
|
||||
@emojis = CustomEmoji.from_text(fields.join(' '), account.domain)
|
||||
@emojis = CustomEmoji.from_text(fields.join(' '))
|
||||
end
|
||||
|
||||
def mark_for_mass_destruction!
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class REST::ChatMessageSerializer < ActiveModel::Serializer
|
||||
attributes :id, :text, :language, :from_account_id,
|
||||
attributes :id, :text_html, :text, :language, :from_account_id,
|
||||
:chat_conversation_id, :created_at
|
||||
|
||||
def id
|
||||
object.id.to_s
|
||||
end
|
||||
|
||||
def text_html
|
||||
Formatter.instance.chatMessageText(object).strip
|
||||
end
|
||||
|
||||
def from_account_id
|
||||
object.from_account_id.to_s
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue