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_STATUSES_LIMIT = 20
|
||||||
DEFAULT_ACCOUNTS_LIMIT = 20
|
DEFAULT_ACCOUNTS_LIMIT = 20
|
||||||
DEFAULT_CHAT_CONVERSATION_LIMIT = 12
|
DEFAULT_CHAT_CONVERSATION_LIMIT = 12
|
||||||
DEFAULT_CHAT_CONVERSATION_MESSAGE_LIMIT = 10
|
DEFAULT_CHAT_CONVERSATION_MESSAGE_LIMIT = 20
|
||||||
|
|
||||||
include RateLimitHeaders
|
include RateLimitHeaders
|
||||||
|
|
||||||
|
|
|
@ -55,7 +55,7 @@ class Api::V1::ChatConversations::MessagesController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def insert_pagination_headers
|
def insert_pagination_headers
|
||||||
set_pagination_headers(next_path, nil)
|
set_pagination_headers(next_path, prev_path)
|
||||||
end
|
end
|
||||||
|
|
||||||
def pagination_params(core_params)
|
def pagination_params(core_params)
|
||||||
|
@ -63,10 +63,27 @@ class Api::V1::ChatConversations::MessagesController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def next_path
|
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
|
end
|
||||||
|
|
||||||
def pagination_since_id
|
def pagination_since_id
|
||||||
@chats.first.id
|
@chats.first.id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def records_continue?
|
||||||
|
@chats.size == limit_param(DEFAULT_CHAT_CONVERSATION_MESSAGE_LIMIT)
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -72,15 +72,17 @@ export const expandChatMessages = (chatConversationId, params = {}, done = noop)
|
||||||
|
|
||||||
dispatch(expandChatMessagesRequest(chatConversationId, isLoadingMore))
|
dispatch(expandChatMessagesRequest(chatConversationId, isLoadingMore))
|
||||||
|
|
||||||
api(getState).get(`/api/v1/chat_conversations/messages/${chatConversationId}`, { params }).then((response) => {
|
api(getState).get(`/api/v1/chat_conversations/messages/${chatConversationId}`, {
|
||||||
console.log("response:", response)
|
params: {
|
||||||
|
max_id: params.maxId,
|
||||||
|
since_id: params.sinceId,
|
||||||
|
}
|
||||||
|
}).then((response) => {
|
||||||
const next = getLinks(response).refs.find(link => link.rel === 'next')
|
const next = getLinks(response).refs.find(link => link.rel === 'next')
|
||||||
console.log("next:", next, getLinks(response).refs)
|
|
||||||
dispatch(importFetchedChatMessages(response.data))
|
dispatch(importFetchedChatMessages(response.data))
|
||||||
dispatch(expandChatMessagesSuccess(chatConversationId, response.data, next ? next.uri : null, response.code === 206, isLoadingRecent, isLoadingMore))
|
dispatch(expandChatMessagesSuccess(chatConversationId, response.data, next ? next.uri : null, response.code === 206, isLoadingRecent, isLoadingMore))
|
||||||
done()
|
done()
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
console.log("error:", error)
|
|
||||||
dispatch(expandChatMessagesFail(chatConversationId, error, isLoadingMore))
|
dispatch(expandChatMessagesFail(chatConversationId, error, isLoadingMore))
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
|
|
|
@ -18,6 +18,7 @@ class AvatarGroup extends ImmutablePureComponent {
|
||||||
return (
|
return (
|
||||||
<div className={[_s.d].join(' ')}>
|
<div className={[_s.d].join(' ')}>
|
||||||
{
|
{
|
||||||
|
!!accounts &&
|
||||||
accounts.map((account) => {
|
accounts.map((account) => {
|
||||||
const isPro = account.get('is_pro')
|
const isPro = account.get('is_pro')
|
||||||
const alt = `${account.get('display_name')} ${isPro ? '(PRO)' : ''}`.trim()
|
const alt = `${account.get('display_name')} ${isPro ? '(PRO)' : ''}`.trim()
|
||||||
|
|
|
@ -19,6 +19,7 @@ class BackButton extends React.PureComponent {
|
||||||
|
|
||||||
handleBackClick = () => {
|
handleBackClick = () => {
|
||||||
this.historyBack()
|
this.historyBack()
|
||||||
|
if (!!this.props.onClick) this.props.onClick()
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
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) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
onDeleteChatMessage(chatMessageId) {
|
onDeleteChatMessage(chatMessageId) {
|
||||||
dispatch(deleteChatMessage(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 { List as ImmutableList } from 'immutable'
|
||||||
import IntersectionObserverArticle from './intersection_observer_article'
|
import IntersectionObserverArticle from './intersection_observer_article'
|
||||||
import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper'
|
import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper'
|
||||||
|
import { MOUSE_IDLE_DELAY } from '../constants'
|
||||||
import Block from './block'
|
import Block from './block'
|
||||||
import ColumnIndicator from './column_indicator'
|
import ColumnIndicator from './column_indicator'
|
||||||
import LoadMore from './load_more'
|
import LoadMore from './load_more'
|
||||||
|
|
||||||
const MOUSE_IDLE_DELAY = 300
|
|
||||||
|
|
||||||
class ScrollableList extends React.PureComponent {
|
class ScrollableList extends React.PureComponent {
|
||||||
|
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
|
@ -27,7 +26,7 @@ class ScrollableList extends React.PureComponent {
|
||||||
lastScrollWasSynthetic = false;
|
lastScrollWasSynthetic = false;
|
||||||
scrollToTopOnMouseIdle = false;
|
scrollToTopOnMouseIdle = false;
|
||||||
|
|
||||||
setScrollTop = newScrollTop => {
|
setScrollTop = (newScrollTop) => {
|
||||||
if (this.documentElement.scrollTop !== newScrollTop) {
|
if (this.documentElement.scrollTop !== newScrollTop) {
|
||||||
this.lastScrollWasSynthetic = true;
|
this.lastScrollWasSynthetic = true;
|
||||||
this.documentElement.scrollTop = newScrollTop;
|
this.documentElement.scrollTop = newScrollTop;
|
||||||
|
@ -104,8 +103,6 @@ class ScrollableList extends React.PureComponent {
|
||||||
|
|
||||||
if (scrollTop < 100 && this.props.onScrollToTop) {
|
if (scrollTop < 100 && this.props.onScrollToTop) {
|
||||||
this.props.onScrollToTop()
|
this.props.onScrollToTop()
|
||||||
} else if (scrollTop < 100 && this.props.onScrollToBottom) {
|
|
||||||
this.props.onScrollToBottom()
|
|
||||||
} else if (this.props.onScroll) {
|
} else if (this.props.onScroll) {
|
||||||
this.props.onScroll()
|
this.props.onScroll()
|
||||||
}
|
}
|
||||||
|
@ -194,7 +191,6 @@ class ScrollableList extends React.PureComponent {
|
||||||
placeholderComponent: Placeholder,
|
placeholderComponent: Placeholder,
|
||||||
placeholderCount,
|
placeholderCount,
|
||||||
onScrollToTop,
|
onScrollToTop,
|
||||||
onScrollToBottom,
|
|
||||||
} = this.props
|
} = this.props
|
||||||
const childrenCount = React.Children.count(children);
|
const childrenCount = React.Children.count(children);
|
||||||
|
|
||||||
|
@ -221,16 +217,6 @@ class ScrollableList extends React.PureComponent {
|
||||||
return (
|
return (
|
||||||
<div onMouseMove={this.handleMouseMove} ref={this.setRef}>
|
<div onMouseMove={this.handleMouseMove} ref={this.setRef}>
|
||||||
<div role='feed'>
|
<div role='feed'>
|
||||||
{
|
|
||||||
(hasMore && onLoadMore && !isLoading) && !!onScrollToBottom &&
|
|
||||||
<LoadMore onClick={this.handleLoadMore} />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isLoading && !!onScrollToBottom &&
|
|
||||||
<ColumnIndicator type='loading' />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
{
|
||||||
!!this.props.children &&
|
!!this.props.children &&
|
||||||
React.Children.map(this.props.children, (child, index) => (
|
React.Children.map(this.props.children, (child, index) => (
|
||||||
|
@ -287,7 +273,6 @@ ScrollableList.propTypes = {
|
||||||
]),
|
]),
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
onScrollToTop: PropTypes.func,
|
onScrollToTop: PropTypes.func,
|
||||||
onScrollToBottom: PropTypes.func,
|
|
||||||
onScroll: PropTypes.func,
|
onScroll: PropTypes.func,
|
||||||
placeholderComponent: PropTypes.node,
|
placeholderComponent: PropTypes.node,
|
||||||
placeholderCount: PropTypes.number,
|
placeholderCount: PropTypes.number,
|
||||||
|
|
|
@ -51,7 +51,6 @@ class TimelineInjectionRoot extends React.PureComponent {
|
||||||
|
|
||||||
handleResize = () => {
|
handleResize = () => {
|
||||||
const { width } = getWindowDimension()
|
const { width } = getWindowDimension()
|
||||||
|
|
||||||
this.setState({ width })
|
this.setState({ width })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,8 @@ export const BREAKPOINT_MEDIUM = 1160
|
||||||
export const BREAKPOINT_SMALL = 1080
|
export const BREAKPOINT_SMALL = 1080
|
||||||
export const BREAKPOINT_EXTRA_SMALL = 992
|
export const BREAKPOINT_EXTRA_SMALL = 992
|
||||||
|
|
||||||
|
export const MOUSE_IDLE_DELAY = 300
|
||||||
|
|
||||||
export const LAZY_LOAD_SCROLL_OFFSET = 50
|
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'
|
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 = () => {
|
onBlur = () => {
|
||||||
this.setState({ focused: false });
|
this.setState({ focused: false })
|
||||||
}
|
}
|
||||||
|
|
||||||
onFocus = () => {
|
onFocus = () => {
|
||||||
this.setState({ focused: true });
|
this.setState({ focused: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
onKeyDown = (e) => {
|
onKeyDown = (e) => {
|
||||||
const { disabled } = this.props;
|
const { disabled } = this.props
|
||||||
|
|
||||||
if (disabled) {
|
if (disabled) return e.preventDefault()
|
||||||
e.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignore key events during text composition
|
// 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)
|
// 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) {
|
switch (e.key) {
|
||||||
case 'Escape':
|
case 'Escape':
|
||||||
document.querySelector('#gabsocial').focus()
|
document.querySelector('#gabsocial').focus()
|
||||||
break;
|
break
|
||||||
case 'Enter':
|
case 'Enter':
|
||||||
|
this.handleOnSendChatMessage()
|
||||||
|
return e.preventDefault()
|
||||||
case 'Tab':
|
case 'Tab':
|
||||||
//
|
this.sendBtn.focus()
|
||||||
break;
|
return e.preventDefault()
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (e.defaultPrevented || !this.props.onKeyDown) return;
|
if (e.defaultPrevented) return
|
||||||
}
|
}
|
||||||
|
|
||||||
setTextbox = (c) => {
|
setTextbox = (c) => {
|
||||||
this.textbox = c
|
this.textbox = c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setSendBtn = (c) => {
|
||||||
|
this.sendBtn = c
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { chatConversationId } = this.props
|
const { isXS, chatConversationId } = this.props
|
||||||
const { value } = this.state
|
const { value } = this.state
|
||||||
const disabled = false
|
const disabled = false
|
||||||
|
|
||||||
const textareaContainerClasses = CX({
|
|
||||||
d: 1,
|
|
||||||
maxW100PC: 1,
|
|
||||||
flexGrow1: 1,
|
|
||||||
jcCenter: 1,
|
|
||||||
py5: 1,
|
|
||||||
})
|
|
||||||
|
|
||||||
const textareaClasses = CX({
|
const textareaClasses = CX({
|
||||||
d: 1,
|
d: 1,
|
||||||
font: 1,
|
font: 1,
|
||||||
|
@ -95,31 +91,59 @@ class ChatMessagesComposeForm extends React.PureComponent {
|
||||||
py10: 1,
|
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 (
|
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.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(' ')}>
|
<div className={[_s.d, _s.pr15, _s.flexGrow1, _s.py10].join(' ')}>
|
||||||
<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'
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className={[_s.d, _s.h100PC, _s.aiCenter, _s.jcCenter].join(' ')}>
|
<div className={[_s.d, _s.h100PC, _s.aiCenter, _s.jcCenter].join(' ')}>
|
||||||
<Button
|
{button}
|
||||||
disabled={disabled}
|
|
||||||
onClick={this.handleOnSendChatMessage}
|
|
||||||
>
|
|
||||||
<Text color='inherit' className={_s.px10}>Send</Text>
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -135,6 +159,7 @@ const mapDispatchToProps = (dispatch) => ({
|
||||||
|
|
||||||
ChatMessagesComposeForm.propTypes = {
|
ChatMessagesComposeForm.propTypes = {
|
||||||
chatConversationId: PropTypes.string,
|
chatConversationId: PropTypes.string,
|
||||||
|
isXS: PropTypes.bool,
|
||||||
onSendMessage: PropTypes.func.isRequired,
|
onSendMessage: PropTypes.func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -75,7 +75,7 @@ class ChatMessageItem extends ImmutablePureComponent {
|
||||||
const account = chatMessage.get('account')
|
const account = chatMessage.get('account')
|
||||||
if (!account) return <div />
|
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 alt = account.get('id', null) === me
|
||||||
const createdAt = chatMessage.get('created_at')
|
const createdAt = chatMessage.get('created_at')
|
||||||
|
|
||||||
|
|
|
@ -2,20 +2,25 @@ import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
import moment from 'moment-mini'
|
import moment from 'moment-mini'
|
||||||
|
import throttle from 'lodash.throttle'
|
||||||
import { List as ImmutableList } from 'immutable'
|
import { List as ImmutableList } from 'immutable'
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes'
|
import ImmutablePropTypes from 'react-immutable-proptypes'
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component'
|
import ImmutablePureComponent from 'react-immutable-pure-component'
|
||||||
import { createSelector } from 'reselect'
|
import { createSelector } from 'reselect'
|
||||||
import debounce from 'lodash.debounce'
|
import debounce from 'lodash.debounce'
|
||||||
import { me } from '../../../initial_state'
|
import { me } from '../../../initial_state'
|
||||||
|
import { CX, MOUSE_IDLE_DELAY } from '../../../constants'
|
||||||
import { setChatConversationSelected } from '../../../actions/chats'
|
import { setChatConversationSelected } from '../../../actions/chats'
|
||||||
import {
|
import {
|
||||||
expandChatMessages,
|
expandChatMessages,
|
||||||
scrollBottomChatMessageConversation,
|
scrollBottomChatMessageConversation,
|
||||||
} from '../../../actions/chat_conversation_messages'
|
} 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 ChatMessagePlaceholder from '../../../components/placeholder/chat_message_placeholder'
|
||||||
import ChatMessageItem from './chat_message_item'
|
import ChatMessageItem from './chat_message_item'
|
||||||
|
import ColumnIndicator from '../../../components/column_indicator'
|
||||||
|
import LoadMore from '../../../components/load_more'
|
||||||
|
|
||||||
class ChatMessageScrollingList extends ImmutablePureComponent {
|
class ChatMessageScrollingList extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
@ -23,6 +28,13 @@ class ChatMessageScrollingList extends ImmutablePureComponent {
|
||||||
isRefreshing: false,
|
isRefreshing: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
intersectionObserverWrapper = new IntersectionObserverWrapper()
|
||||||
|
|
||||||
|
mouseIdleTimer = null
|
||||||
|
mouseMovedRecently = false
|
||||||
|
lastScrollWasSynthetic = false
|
||||||
|
scrollToTopOnMouseIdle = false
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
const { chatConversationId } = this.props
|
const { chatConversationId } = this.props
|
||||||
this.props.onExpandChatMessages(chatConversationId)
|
this.props.onExpandChatMessages(chatConversationId)
|
||||||
|
@ -30,6 +42,8 @@ class ChatMessageScrollingList extends ImmutablePureComponent {
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.props.onSetChatConversationSelected(null)
|
this.props.onSetChatConversationSelected(null)
|
||||||
|
this.detachScrollListener()
|
||||||
|
this.detachIntersectionObserver()
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps (nextProps) {
|
componentWillReceiveProps (nextProps) {
|
||||||
|
@ -40,20 +54,48 @@ class ChatMessageScrollingList extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLoadMore = (sinceId) => {
|
componentDidUpdate(prevProps, prevState, snapshot) {
|
||||||
const { chatConversationId, dispatch } = this.props
|
// Reset the scroll position when a new child comes in in order not to
|
||||||
this.props.onExpandChatMessages(chatConversationId, { sinceId })
|
// 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) {
|
if (this.state.isRefreshing) {
|
||||||
this.setState({ isRefreshing: false })
|
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) => {
|
getCurrentChatMessageIndex = (id) => {
|
||||||
// : todo :
|
// : todo :
|
||||||
return this.props.chatMessageIds.indexOf(id)
|
return this.props.chatMessageIds.indexOf(id)
|
||||||
|
@ -69,14 +111,13 @@ class ChatMessageScrollingList extends ImmutablePureComponent {
|
||||||
this._selectChild(elementIndex, false)
|
this._selectChild(elementIndex, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLoadOlder = debounce(() => {
|
setScrollTop = (newScrollTop) => {
|
||||||
this.handleLoadMore(this.props.chatMessageIds.size > 0 ? this.props.chatMessageIds.last() : undefined)
|
if (!this.scrollContainerRef) return
|
||||||
}, 300, { leading: true })
|
if (this.scrollContainerRef.scrollTop !== newScrollTop) {
|
||||||
|
this.lastScrollWasSynthetic = true
|
||||||
handleOnReload = debounce(() => {
|
this.scrollContainerRef.scrollTop = newScrollTop
|
||||||
this.handleLoadMore()
|
}
|
||||||
this.setState({ isRefreshing: true })
|
}
|
||||||
}, 300, { trailing: true })
|
|
||||||
|
|
||||||
_selectChild(index, align_top) {
|
_selectChild(index, align_top) {
|
||||||
const container = this.node.node
|
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) => {
|
setRef = (c) => {
|
||||||
this.node = c
|
this.node = c
|
||||||
}
|
}
|
||||||
|
@ -100,6 +218,15 @@ class ChatMessageScrollingList extends ImmutablePureComponent {
|
||||||
this.containerNode = c
|
this.containerNode = c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setScrollContainerRef = (c) => {
|
||||||
|
this.scrollContainerRef = c
|
||||||
|
|
||||||
|
this.attachScrollListener()
|
||||||
|
this.attachIntersectionObserver()
|
||||||
|
// Handle initial scroll posiiton
|
||||||
|
this.handleScroll()
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
chatConversationId,
|
chatConversationId,
|
||||||
|
@ -109,13 +236,10 @@ class ChatMessageScrollingList extends ImmutablePureComponent {
|
||||||
hasMore,
|
hasMore,
|
||||||
onScrollToBottom,
|
onScrollToBottom,
|
||||||
onScroll,
|
onScroll,
|
||||||
|
isXS,
|
||||||
} = this.props
|
} = this.props
|
||||||
const { isRefreshing } = this.state
|
const { isRefreshing } = this.state
|
||||||
|
|
||||||
if (isPartial || (isLoading && chatMessageIds.size === 0)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
let scrollableContent = []
|
let scrollableContent = []
|
||||||
let emptyContent = []
|
let emptyContent = []
|
||||||
|
|
||||||
|
@ -128,8 +252,8 @@ class ChatMessageScrollingList extends ImmutablePureComponent {
|
||||||
<div
|
<div
|
||||||
key={`chat-message-gap:${(i + 1)}`}
|
key={`chat-message-gap:${(i + 1)}`}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
sinceId={i > 0 ? chatMessageIds.get(i - 1) : null}
|
maxId={i > 0 ? chatMessageIds.get(i - 1) : null}
|
||||||
onClick={this.handleLoadMore}
|
onClick={this.handleLoadOlder}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
@ -148,23 +272,67 @@ class ChatMessageScrollingList extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const childrenCount = React.Children.count(scrollableContent)
|
||||||
<div
|
if (isLoading || childrenCount > 0 || hasMore) {
|
||||||
className={[_s.d, _s.boxShadowNone, _s.posAbs, _s.bottom60PX, _s.left0, _s.right0, _s.px15, _s.py15, _s.top60PX, _s.w100PC, _s.overflowYScroll].join(' ')}
|
const containerClasses = CX({
|
||||||
ref={this.containerRef}
|
d: 1,
|
||||||
>
|
bgPrimary: 1,
|
||||||
<ScrollableList
|
boxShadowNone: 1,
|
||||||
scrollRef={this.setRef}
|
posAbs: !isXS,
|
||||||
onLoadMore={this.handleLoadMore && this.handleLoadOlder}
|
bottom60PX: !isXS,
|
||||||
scrollKey='chat_messages'
|
left0: !isXS,
|
||||||
hasMore={hasMore}
|
right0: !isXS,
|
||||||
emptyMessage='No chats found'
|
top60PX: !isXS,
|
||||||
onScrollToBottom={onScrollToBottom}
|
w100PC: 1,
|
||||||
onScroll={onScroll}
|
overflowHidden: 1,
|
||||||
isLoading={isLoading}
|
})
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onMouseMove={this.handleMouseMove}
|
||||||
|
className={containerClasses}
|
||||||
|
ref={this.containerRef}
|
||||||
>
|
>
|
||||||
{scrollableContent}
|
<div
|
||||||
</ScrollableList>
|
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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -207,6 +375,7 @@ ChatMessageScrollingList.propTypes = {
|
||||||
onClearTimeline: PropTypes.func.isRequired,
|
onClearTimeline: PropTypes.func.isRequired,
|
||||||
onScrollToTop: PropTypes.func.isRequired,
|
onScrollToTop: PropTypes.func.isRequired,
|
||||||
onScroll: PropTypes.func.isRequired,
|
onScroll: PropTypes.func.isRequired,
|
||||||
|
isXS: PropTypes.bool.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ChatMessageScrollingList)
|
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 () {
|
render () {
|
||||||
const {
|
const {
|
||||||
account,
|
isXS,
|
||||||
selectedChatConversationId,
|
selectedChatConversationId,
|
||||||
chatConverationIsRequest,
|
chatConverationIsRequest,
|
||||||
} = this.props
|
} = this.props
|
||||||
|
@ -48,6 +48,7 @@ const mapStateToProps = (state, props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
Messages.propTypes = {
|
Messages.propTypes = {
|
||||||
|
isXS: PropTypes.bool,
|
||||||
selectedChatConversationId: PropTypes.string,
|
selectedChatConversationId: PropTypes.string,
|
||||||
chatConverationIsRequest: PropTypes.bool.isRequired,
|
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='/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' 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/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/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 }} />
|
<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.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);
|
this.observe(id, node, callback);
|
||||||
});
|
}) : null;
|
||||||
this.observerBacklog = null;
|
this.observerBacklog = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,17 +8,41 @@ import {
|
||||||
BREAKPOINT_EXTRA_SMALL,
|
BREAKPOINT_EXTRA_SMALL,
|
||||||
MODAL_CHAT_CONVERSATION_CREATE,
|
MODAL_CHAT_CONVERSATION_CREATE,
|
||||||
} from '../constants'
|
} from '../constants'
|
||||||
|
import { getWindowDimension } from '../utils/is_mobile'
|
||||||
import Layout from './layout'
|
import Layout from './layout'
|
||||||
import Responsive from '../features/ui/util/responsive_component'
|
import Responsive from '../features/ui/util/responsive_component'
|
||||||
import List from '../components/list'
|
|
||||||
import ResponsiveClassesComponent from '../features/ui/util/responsive_classes_component'
|
import ResponsiveClassesComponent from '../features/ui/util/responsive_classes_component'
|
||||||
import ChatConversationsSearch from '../features/messages/components/chat_conversations_search'
|
import ChatSettingsSidebar from '../features/messages/components/chat_settings_sidebar'
|
||||||
import ChatConversationsList from '../features/messages/components/chat_conversations_list'
|
import ChatApprovedConversationsSidebar from '../features/messages/components/chat_approved_conversations_sidebar'
|
||||||
import ChatSettingsHeader from '../features/messages/components/chat_settings_header'
|
import FooterBar from '../components/footer_bar'
|
||||||
import ChatConversationRequestsListItem from '../features/messages/components/chat_conversations_requests_list_item'
|
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 {
|
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 = () => {
|
onClickAdd = () => {
|
||||||
this.props.onOpenChatConversationCreateModal()
|
this.props.onOpenChatConversationCreateModal()
|
||||||
}
|
}
|
||||||
|
@ -31,15 +55,54 @@ class MessagesLayout extends React.PureComponent {
|
||||||
showBackBtn,
|
showBackBtn,
|
||||||
source,
|
source,
|
||||||
currentConversationIsRequest,
|
currentConversationIsRequest,
|
||||||
|
selectedChatConversationId,
|
||||||
} = this.props
|
} = this.props
|
||||||
|
const { width } = this.state
|
||||||
|
|
||||||
const mainBlockClasses = CX({
|
const isXS = width <= BREAKPOINT_EXTRA_SMALL
|
||||||
d: 1,
|
|
||||||
w1015PX: 1,
|
if (isXS) {
|
||||||
h100PC: 1,
|
if (!selectedChatConversationId) {
|
||||||
flexRow: 1,
|
return (
|
||||||
jcEnd: 1,
|
<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 (
|
return (
|
||||||
<Layout
|
<Layout
|
||||||
|
@ -68,57 +131,17 @@ class MessagesLayout extends React.PureComponent {
|
||||||
classNamesXS={[_s.d, _s.w100PC].join(' ')}
|
classNamesXS={[_s.d, _s.w100PC].join(' ')}
|
||||||
>
|
>
|
||||||
<ResponsiveClassesComponent
|
<ResponsiveClassesComponent
|
||||||
classNames={mainBlockClasses}
|
classNames={[_s.d, _s.w1015PX, _s.h100PC, _s.flexRow, _s.jcEnd].join(' ')}
|
||||||
classNamesXS={[_s.d, _s.w100PC, _s.jcEnd].join(' ')}
|
classNamesXS={[_s.d, _s.w100PC, _s.h100PC, _s.jcEnd].join(' ')}
|
||||||
>
|
>
|
||||||
|
{
|
||||||
<Responsive min={BREAKPOINT_EXTRA_SMALL}>
|
(isSettings || currentConversationIsRequest) &&
|
||||||
<div className={[_s.d, _s.w340PX, _s.h100PC, _s.bgPrimary, _s.borderLeft1PX, _s.borderRight1PX, _s.borderColorSecondary].join(' ')}>
|
<ChatSettingsSidebar />
|
||||||
<div className={[_s.d, _s.h100PC, _s.overflowHidden, _s.w100PC, _s.boxShadowNone].join(' ')}>
|
}
|
||||||
|
{
|
||||||
{
|
!isSettings && !currentConversationIsRequest &&
|
||||||
(isSettings || currentConversationIsRequest) &&
|
<ChatApprovedConversationsSidebar source={source} />
|
||||||
<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>
|
|
||||||
|
|
||||||
|
|
||||||
<div className={[_s.d, _s.flexGrow1, _s.h100PC, _s.bgPrimary, _s.borderColorSecondary, _s.borderRight1PX, _s.z1].join(' ')}>
|
<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(' ')}>
|
<div className={[_s.d, _s.w100PC, _s.h100PC].join(' ')}>
|
||||||
{children}
|
{children}
|
||||||
|
@ -134,10 +157,13 @@ class MessagesLayout extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state) => {
|
const mapStateToProps = (state) => {
|
||||||
const selectedId = state.getIn(['chats', 'selectedChatConversationId'], null)
|
const selectedChatConversationId = state.getIn(['chats', 'selectedChatConversationId'], null)
|
||||||
const currentConversationIsRequest = selectedId ? !state.getIn(['chat_conversations', selectedId, 'is_approved'], true) : false
|
const currentConversationIsRequest = selectedChatConversationId ? !state.getIn(['chat_conversations', selectedChatConversationId, 'is_approved'], true) : false
|
||||||
|
|
||||||
return { currentConversationIsRequest }
|
return {
|
||||||
|
selectedChatConversationId,
|
||||||
|
currentConversationIsRequest,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
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) }
|
Rails.cache.fetch(to_key(:mention, username, domain), expires_in: MAX_EXPIRATION) { Account.select(:username, :domain, :url).find_local(username) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def emoji(shortcodes, domain)
|
def emoji(shortcodes, domain = nil)
|
||||||
shortcodes = [shortcodes] unless shortcodes.is_a?(Array)
|
shortcodes = [shortcodes] unless shortcodes.is_a?(Array)
|
||||||
cached = Rails.cache.read_multi(*shortcodes.map { |shortcode| to_key(:emoji, shortcode, domain) })
|
cached = Rails.cache.read_multi(*shortcodes.map { |shortcode| to_key(:emoji, shortcode, domain) })
|
||||||
uncached_ids = []
|
uncached_ids = []
|
||||||
|
|
|
@ -80,6 +80,21 @@ class Formatter
|
||||||
|
|
||||||
include ActionView::Helpers::TextHelper
|
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)
|
def format(status, **options)
|
||||||
if options[:use_markdown]
|
if options[:use_markdown]
|
||||||
raw_content = status.markdown
|
raw_content = status.markdown
|
||||||
|
|
|
@ -434,7 +434,7 @@ class Account < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def emojis
|
def emojis
|
||||||
@emojis ||= CustomEmoji.from_text(emojifiable_text, domain)
|
@emojis ||= CustomEmoji.from_text(emojifiable_text)
|
||||||
end
|
end
|
||||||
|
|
||||||
before_create :generate_keys
|
before_create :generate_keys
|
||||||
|
|
|
@ -25,4 +25,9 @@ class ChatMessage < ApplicationRecord
|
||||||
|
|
||||||
scope :recent, -> { reorder(created_at: :desc) }
|
scope :recent, -> { reorder(created_at: :desc) }
|
||||||
|
|
||||||
|
def emojis
|
||||||
|
return @emojis if defined?(@emojis)
|
||||||
|
|
||||||
|
@emojis = CustomEmoji.from_text(text)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -54,14 +54,14 @@ class CustomEmoji < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def from_text(text, domain)
|
def from_text(text)
|
||||||
return [] if text.blank?
|
return [] if text.blank?
|
||||||
|
|
||||||
shortcodes = text.scan(SCAN_RE).map(&:first).uniq
|
shortcodes = text.scan(SCAN_RE).map(&:first).uniq
|
||||||
|
|
||||||
return [] if shortcodes.empty?
|
return [] if shortcodes.empty?
|
||||||
|
|
||||||
EntityCache.instance.emoji(shortcodes, domain)
|
EntityCache.instance.emoji(shortcodes)
|
||||||
end
|
end
|
||||||
|
|
||||||
def search(shortcode)
|
def search(shortcode)
|
||||||
|
@ -72,10 +72,7 @@ class CustomEmoji < ApplicationRecord
|
||||||
private
|
private
|
||||||
|
|
||||||
def remove_entity_cache
|
def remove_entity_cache
|
||||||
Rails.cache.delete(EntityCache.instance.to_key(:emoji, shortcode, domain))
|
Rails.cache.delete(EntityCache.instance.to_key(:emoji, shortcode,))
|
||||||
end
|
end
|
||||||
|
|
||||||
def downcase_domain
|
|
||||||
self.domain = domain.downcase unless domain.nil?
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -57,7 +57,7 @@ class Poll < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def emojis
|
def emojis
|
||||||
@emojis ||= CustomEmoji.from_text(options.join(' '), account.domain)
|
@emojis ||= CustomEmoji.from_text(options.join(' '))
|
||||||
end
|
end
|
||||||
|
|
||||||
class Option < ActiveModelSerializers::Model
|
class Option < ActiveModelSerializers::Model
|
||||||
|
|
|
@ -231,7 +231,7 @@ class Status < ApplicationRecord
|
||||||
fields = [spoiler_text, text]
|
fields = [spoiler_text, text]
|
||||||
fields += preloadable_poll.options unless preloadable_poll.nil?
|
fields += preloadable_poll.options unless preloadable_poll.nil?
|
||||||
|
|
||||||
@emojis = CustomEmoji.from_text(fields.join(' '), account.domain)
|
@emojis = CustomEmoji.from_text(fields.join(' '))
|
||||||
end
|
end
|
||||||
|
|
||||||
def mark_for_mass_destruction!
|
def mark_for_mass_destruction!
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class REST::ChatMessageSerializer < ActiveModel::Serializer
|
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
|
:chat_conversation_id, :created_at
|
||||||
|
|
||||||
def id
|
def id
|
||||||
object.id.to_s
|
object.id.to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def text_html
|
||||||
|
Formatter.instance.chatMessageText(object).strip
|
||||||
|
end
|
||||||
|
|
||||||
def from_account_id
|
def from_account_id
|
||||||
object.from_account_id.to_s
|
object.from_account_id.to_s
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue