-
-
-
- {activities.map((activity, index) => (
+ componentDidMount() {
+ this.bindEvents()
+ }
+
+ componentDidUpdate(prevProps) {
+ if (prevProps.activities.length < this.props.activities.length) {
+ this.scrollToBottomIfShould()
+ }
+ }
+
+ onScroll() {
+ const messageStreamHeight = this.messageStream.clientHeight
+ const activitiesListHeight = this.activitiesList.clientHeight
+
+ const bodyRect = document.body.getBoundingClientRect()
+ const elemRect = this.activitiesList.getBoundingClientRect()
+ const offset = elemRect.top - bodyRect.top
+ const activitiesListYPos = offset
+
+ const scrolledToBottom = (activitiesListHeight + (activitiesListYPos - 60)) <= messageStreamHeight
+ if (scrolledToBottom) {
+ if (!this.props.scrolledToBottom) {
+ this.props.setScrolledToBottom(true)
+ }
+ } else if (this.props.scrolledToBottom) {
+ this.props.setScrolledToBottom(false)
+ }
+ }
+
+ scrollToBottomIfShould() {
+ if (this.props.scrolledToBottom) {
+ setTimeout(() => {
+ this.messageStream.scrollTop = this.messageStream.scrollHeight
+ }, 0)
+ }
+ }
+
+ scrollToBottom() {
+ this.messageStream.scrollTop = this.messageStream.scrollHeight
+ this.props.setScrolledToBottom(true)
+ }
+
+ bindEvents() {
+ this.messageStream.addEventListener('scroll', this.onScroll.bind(this))
+ }
+
+ handleChatClick() {
+ this.setState({ focusChat: true })
+ defer(() => this.setState({ focusChat: false }))
+ }
+
+ render() {
+ return (
+
+
this.messageStream = el} data-testid="main-div">
+
this.activitiesList = el}>
+
+ {this.props.activities.map((activity, index) => (
-
-
+
- ))}
-
+ ))}
+
+
+
+
+
-
-
-
-
- )
+ )
+ }
}
ActivityList.propTypes = {
activities: PropTypes.array.isRequired,
openModal: PropTypes.func.isRequired,
+ setScrolledToBottom: PropTypes.func.isRequired,
+ scrolledToBottom: PropTypes.bool.isRequired,
}
-export default ActivityList
+export default ActivityList;
diff --git a/client/src/components/Home/ActivityList.test.js b/client/src/components/Home/ActivityList.test.js
index ebe234c..af1c769 100644
--- a/client/src/components/Home/ActivityList.test.js
+++ b/client/src/components/Home/ActivityList.test.js
@@ -1,5 +1,5 @@
import React from 'react';
-import { render, fireEvent } from '@testing-library/react';
+import { render, fireEvent, waitFor } from '@testing-library/react';
import ActivityList from './ActivityList';
import { Provider } from 'react-redux';
import configureStore from 'store';
@@ -12,7 +12,7 @@ describe('ActivityList component', () => {
it('should display', () => {
const { asFragment } = render(
-
+
,
);
@@ -39,7 +39,7 @@ describe('ActivityList component', () => {
];
const { asFragment } = render(
-
+
,
);
@@ -51,7 +51,7 @@ describe('ActivityList component', () => {
const { getByText } = render(
-
+
,
);
@@ -59,33 +59,79 @@ describe('ActivityList component', () => {
jest.runAllTimers();
expect(mockOpenModal.mock.calls[0][0]).toBe('About');
- jest.runAllTimers();
});
it('should focus chat', () => {
const { getByTestId } = render(
-
+
,
);
fireEvent.click(getByTestId('main-div'));
jest.runAllTimers();
});
- it('should scroll to bottom on new message if not scrolled', () => {
- jest.spyOn(Element.prototype, 'clientHeight', 'get').mockReturnValueOnce(400).mockReturnValueOnce(200);
+ it('should handle scroll', () => {
+ const mockSetScrollToBottom = jest.fn();
+ jest
+ .spyOn(Element.prototype, 'clientHeight', 'get')
+ .mockReturnValueOnce(400)
+ .mockReturnValueOnce(200)
+ .mockReturnValueOnce(400)
+ .mockReturnValueOnce(200);
Element.prototype.getBoundingClientRect = jest
.fn()
.mockReturnValueOnce({ top: 0 })
+ .mockReturnValueOnce({ top: 60 })
+ .mockReturnValueOnce({ top: 0 })
.mockReturnValueOnce({ top: 261 });
+ const { getByTestId, rerender } = render(
+
+
+ ,
+ );
+ fireEvent.scroll(getByTestId('main-div'));
+
+ expect(mockSetScrollToBottom).toHaveBeenLastCalledWith(true);
+
+ rerender(
+
+
+ ,
+ );
+
+ fireEvent.scroll(getByTestId('main-div'));
+
+ expect(mockSetScrollToBottom).toHaveBeenCalledTimes(2);
+ expect(mockSetScrollToBottom).toHaveBeenLastCalledWith(false);
+ });
+
+ it('should scroll to bottom on new message', () => {
+ const mockSetScrollToBottom = jest.fn();
+
jest.spyOn(Element.prototype, 'scrollHeight', 'get').mockReturnValue(42);
const mockScrollTop = jest.spyOn(Element.prototype, 'scrollTop', 'set');
- const { rerender, getByTestId } = render(
+ const { rerender } = render(
-
+
,
);
@@ -101,39 +147,14 @@ describe('ActivityList component', () => {
text: 'Hi!',
},
]}
+ setScrolledToBottom={mockSetScrollToBottom}
+ scrolledToBottom={true}
/>
,
);
jest.runAllTimers();
- expect(mockScrollTop).toHaveBeenCalledTimes(2);
expect(mockScrollTop).toHaveBeenLastCalledWith(42);
-
- fireEvent.scroll(getByTestId('main-div'));
-
- rerender(
-
-
- ,
- );
-
- expect(mockScrollTop).toHaveBeenCalledTimes(2);
});
});
diff --git a/client/src/components/Home/Home.js b/client/src/components/Home/Home.js
index a96fb65..ef2761b 100644
--- a/client/src/components/Home/Home.js
+++ b/client/src/components/Home/Home.js
@@ -11,6 +11,7 @@ import Settings from 'components/Settings'
import Welcome from 'components/Welcome'
import RoomLocked from 'components/RoomLocked'
import { X, AlertCircle } from 'react-feather'
+import { defer } from 'lodash'
import Tinycon from 'tinycon'
import beepFile from 'audio/beep.mp3'
import classNames from 'classnames'
@@ -23,6 +24,13 @@ const crypto = new Crypto()
Modal.setAppElement('#root');
class Home extends Component {
+ constructor(props) {
+ super(props)
+
+ this.state = {
+ zoomableImages: []
+ }
+ }
async componentWillMount() {
const roomId = encodeURI(this.props.match.params.roomId)
@@ -137,6 +145,7 @@ class Home extends Component {
}
bindEvents() {
+
window.onfocus = () => {
this.props.toggleWindowFocus(true)
}
@@ -166,6 +175,11 @@ class Home extends Component {
})
}
+ handleChatClick() {
+ this.setState({ focusChat: true })
+ defer(() => this.setState({ focusChat: false }))
+ }
+
render() {
const modalOpts = this.getModal()
return (
@@ -187,9 +201,11 @@ class Home extends Component {
translations={this.props.translations}
/>
-