- {
- (hasMore && onLoadMore && !isLoading) && !!onScrollToBottom &&
-
- }
-
- {
- isLoading && !!onScrollToBottom &&
-
- }
-
{
!!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,
diff --git a/app/javascript/gabsocial/components/timeline_injections/timeline_injection_root.js b/app/javascript/gabsocial/components/timeline_injections/timeline_injection_root.js
index d8ace773..98680940 100644
--- a/app/javascript/gabsocial/components/timeline_injections/timeline_injection_root.js
+++ b/app/javascript/gabsocial/components/timeline_injections/timeline_injection_root.js
@@ -51,7 +51,6 @@ class TimelineInjectionRoot extends React.PureComponent {
handleResize = () => {
const { width } = getWindowDimension()
-
this.setState({ width })
}
diff --git a/app/javascript/gabsocial/constants.js b/app/javascript/gabsocial/constants.js
index a048b4c7..d7770923 100644
--- a/app/javascript/gabsocial/constants.js
+++ b/app/javascript/gabsocial/constants.js
@@ -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'
diff --git a/app/javascript/gabsocial/features/messages/components/chat_approved_conversations_sidebar.js b/app/javascript/gabsocial/features/messages/components/chat_approved_conversations_sidebar.js
new file mode 100644
index 00000000..cae6e28a
--- /dev/null
+++ b/app/javascript/gabsocial/features/messages/components/chat_approved_conversations_sidebar.js
@@ -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 (
+
+
+
+
+
+
+
+
+
+ )
+ }
+
+}
+
+ChatApprovedConversationsSidebar.propTypes = {
+ source: PropTypes.string,
+}
+
+export default ChatApprovedConversationsSidebar
\ No newline at end of file
diff --git a/app/javascript/gabsocial/features/messages/components/chat_conversation_request_approve_bar.js b/app/javascript/gabsocial/features/messages/components/chat_conversation_request_approve_bar.js
new file mode 100644
index 00000000..6fd7a7a2
--- /dev/null
+++ b/app/javascript/gabsocial/features/messages/components/chat_conversation_request_approve_bar.js
@@ -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 (
+
+
+
+
+
+
+
+ )
+ }
+
+}
+
+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)
\ No newline at end of file
diff --git a/app/javascript/gabsocial/features/messages/components/chat_message_compose_form.js b/app/javascript/gabsocial/features/messages/components/chat_message_compose_form.js
index 8c340a4b..ea78b7a3 100644
--- a/app/javascript/gabsocial/features/messages/components/chat_message_compose_form.js
+++ b/app/javascript/gabsocial/features/messages/components/chat_message_compose_form.js
@@ -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 = (
+
+ )
+
+ const button = (
+
+ )
+
+ if (isXS) {
+ return (
+
+
+
+
+
+ {textarea}
+
+
+ {button}
+
+
+
+
+
+ )
+ }
+
return (
-
+ {textarea}
-
+ {button}
)
@@ -135,6 +159,7 @@ const mapDispatchToProps = (dispatch) => ({
ChatMessagesComposeForm.propTypes = {
chatConversationId: PropTypes.string,
+ isXS: PropTypes.bool,
onSendMessage: PropTypes.func.isRequired,
}
diff --git a/app/javascript/gabsocial/features/messages/components/chat_message_item.js b/app/javascript/gabsocial/features/messages/components/chat_message_item.js
index 70c10e45..05a29d41 100644
--- a/app/javascript/gabsocial/features/messages/components/chat_message_item.js
+++ b/app/javascript/gabsocial/features/messages/components/chat_message_item.js
@@ -75,7 +75,7 @@ class ChatMessageItem extends ImmutablePureComponent {
const account = chatMessage.get('account')
if (!account) return
- 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')
diff --git a/app/javascript/gabsocial/features/messages/components/chat_message_scrolling_list.js b/app/javascript/gabsocial/features/messages/components/chat_message_scrolling_list.js
index eae2b4f1..2ff88a80 100644
--- a/app/javascript/gabsocial/features/messages/components/chat_message_scrolling_list.js
+++ b/app/javascript/gabsocial/features/messages/components/chat_message_scrolling_list.js
@@ -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 {
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 (
-
-
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 (
+
- {scrollableContent}
-
+
+ {
+ (hasMore && !isLoading) &&
+
+ }
+
+ {
+ isLoading &&
+
+ }
+
+
+ {
+ !!scrollableContent &&
+ scrollableContent.map((child, index) => (
+
+ {child}
+
+ ))
+ }
+
+
+
+ )
+ }
+
+ return (
+
)
}
@@ -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)
\ No newline at end of file
diff --git a/app/javascript/gabsocial/features/messages/components/chat_settings_sidebar.js b/app/javascript/gabsocial/features/messages/components/chat_settings_sidebar.js
new file mode 100644
index 00000000..bf1995ef
--- /dev/null
+++ b/app/javascript/gabsocial/features/messages/components/chat_settings_sidebar.js
@@ -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 (
+
+
+
+
+ )
+ }
+
+}
+
+export default ChatSettingsSidebar
\ No newline at end of file
diff --git a/app/javascript/gabsocial/features/messages/messages.js b/app/javascript/gabsocial/features/messages/messages.js
index 620a5b72..385470c7 100644
--- a/app/javascript/gabsocial/features/messages/messages.js
+++ b/app/javascript/gabsocial/features/messages/messages.js
@@ -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,
}
diff --git a/app/javascript/gabsocial/features/ui/ui.js b/app/javascript/gabsocial/features/ui/ui.js
index 44660071..52046899 100644
--- a/app/javascript/gabsocial/features/ui/ui.js
+++ b/app/javascript/gabsocial/features/ui/ui.js
@@ -204,6 +204,7 @@ class SwitchingArea extends React.PureComponent {
+
diff --git a/app/javascript/gabsocial/features/ui/util/intersection_observer_wrapper.js b/app/javascript/gabsocial/features/ui/util/intersection_observer_wrapper.js
index 298643b3..0cedf432 100644
--- a/app/javascript/gabsocial/features/ui/util/intersection_observer_wrapper.js
+++ b/app/javascript/gabsocial/features/ui/util/intersection_observer_wrapper.js
@@ -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;
}
diff --git a/app/javascript/gabsocial/layouts/messages_layout.js b/app/javascript/gabsocial/layouts/messages_layout.js
index 03b9dbea..7bd0c73b 100644
--- a/app/javascript/gabsocial/layouts/messages_layout.js
+++ b/app/javascript/gabsocial/layouts/messages_layout.js
@@ -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 (
+
+
+
+
+ {
+ (isSettings || currentConversationIsRequest) &&
+
+ }
+ {
+ !isSettings && !currentConversationIsRequest &&
+
+ }
+
+
+
+
+ )
+ } else {
+ return (
+
+
+
+
+
+ { currentConversationIsRequest && }
+ { !currentConversationIsRequest && }
+
+ )
+ }
+ }
return (
-
-
-
-
-
- {
- (isSettings || currentConversationIsRequest) &&
-
-
-
-
- }
-
- {
- !isSettings && !currentConversationIsRequest &&
-
-
-
-
-
-
-
- }
-
-
-
-
-
-
+ {
+ (isSettings || currentConversationIsRequest) &&
+
+ }
+ {
+ !isSettings && !currentConversationIsRequest &&
+
+ }
{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) => ({
diff --git a/app/lib/entity_cache.rb b/app/lib/entity_cache.rb
index 31a84b73..19297fb1 100644
--- a/app/lib/entity_cache.rb
+++ b/app/lib/entity_cache.rb
@@ -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 = []
diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb
index cc178f88..db5dc0ad 100644
--- a/app/lib/formatter.rb
+++ b/app/lib/formatter.rb
@@ -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
diff --git a/app/models/account.rb b/app/models/account.rb
index 4895449d..9a5bc3ec 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -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
diff --git a/app/models/chat_message.rb b/app/models/chat_message.rb
index 845410da..269cf800 100644
--- a/app/models/chat_message.rb
+++ b/app/models/chat_message.rb
@@ -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
diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb
index 7db7fdd3..9cfdc168 100644
--- a/app/models/custom_emoji.rb
+++ b/app/models/custom_emoji.rb
@@ -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
diff --git a/app/models/poll.rb b/app/models/poll.rb
index e27b71da..d4f16ab3 100644
--- a/app/models/poll.rb
+++ b/app/models/poll.rb
@@ -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
diff --git a/app/models/status.rb b/app/models/status.rb
index 082d1363..f7538228 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -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!
diff --git a/app/serializers/rest/chat_message_serializer.rb b/app/serializers/rest/chat_message_serializer.rb
index fea6519c..813177cf 100644
--- a/app/serializers/rest/chat_message_serializer.rb
+++ b/app/serializers/rest/chat_message_serializer.rb
@@ -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