From 7df8951efa0a11415237f1bb35a8fa3fcba24a94 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 17 Jan 2019 16:30:55 +0100 Subject: [PATCH 01/13] wait to hide typing bar until event arrives or 5s timeout --- src/components/views/rooms/WhoIsTypingTile.js | 89 +++++++++++++++++-- 1 file changed, 84 insertions(+), 5 deletions(-) diff --git a/src/components/views/rooms/WhoIsTypingTile.js b/src/components/views/rooms/WhoIsTypingTile.js index 9d49c35d83..144057dfd6 100644 --- a/src/components/views/rooms/WhoIsTypingTile.js +++ b/src/components/views/rooms/WhoIsTypingTile.js @@ -19,6 +19,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import sdk from '../../../index'; import WhoIsTyping from '../../../WhoIsTyping'; +import Timer from '../../../utils/Timer'; import MatrixClientPeg from '../../../MatrixClientPeg'; import MemberAvatar from '../avatars/MemberAvatar'; @@ -43,11 +44,13 @@ module.exports = React.createClass({ getInitialState: function() { return { usersTyping: WhoIsTyping.usersTypingApartFromMe(this.props.room), + userTimers: {}, }; }, componentWillMount: function() { MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping); + MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); }, componentDidUpdate: function(_, prevState) { @@ -64,18 +67,89 @@ module.exports = React.createClass({ const client = MatrixClientPeg.get(); if (client) { client.removeListener("RoomMember.typing", this.onRoomMemberTyping); + client.removeListener("Room.timeline", this.onRoomTimeline); + } + Object.values(this.state.userTimers).forEach((t) => t.abort()); + }, + + onRoomTimeline: function(event, room) { + if (room.roomId === this.props.room.roomId) { + console.log(`WhoIsTypingTile: incoming timeline event for ${event.getSender()}`); + this._abortUserTimer(event.getSender(), "timeline event"); } }, onRoomMemberTyping: function(ev, member) { + console.log(`WhoIsTypingTile: incoming typing event for`, ev.getContent().user_ids); + const usersTyping = WhoIsTyping.usersTypingApartFromMeAndIgnored(this.props.room); this.setState({ - usersTyping: WhoIsTyping.usersTypingApartFromMeAndIgnored(this.props.room), + userTimers: this._updateUserTimers(usersTyping), + usersTyping, }); }, - _renderTypingIndicatorAvatars: function(limit) { - let users = this.state.usersTyping; + _updateUserTimers(usersTyping) { + const usersThatStoppedTyping = this.state.usersTyping.filter((a) => { + return !usersTyping.some((b) => a.userId === b.userId); + }); + const usersThatStartedTyping = usersTyping.filter((a) => { + return !this.state.usersTyping.some((b) => a.userId === b.userId); + }); + // abort all the timers for the users that started typing again + usersThatStartedTyping.forEach((m) => { + const timer = this.state.userTimers[m.userId]; + timer && timer.abort(); + }); + // prepare new userTimers object to update state with + let userTimers = Object.assign({}, this.state.userTimers); + // remove members that started typing again + userTimers = usersThatStartedTyping.reduce((userTimers, m) => { + if (userTimers[m.userId]) { + console.log(`WhoIsTypingTile: stopping timer for ${m.userId} because started typing again`); + } + delete userTimers[m.userId]; + return userTimers; + }, userTimers); + // start timer for members that stopped typing + userTimers = usersThatStoppedTyping.reduce((userTimers, m) => { + if (!userTimers[m.userId]) { + console.log(`WhoIsTypingTile: starting 5s timer for ${m.userId}`); + const timer = new Timer(5000); + userTimers[m.userId] = timer; + timer.start(); + timer.finished().then( + () => { + console.log(`WhoIsTypingTile: elapsed 5s timer for ${m.userId}`); + this._removeUserTimer(m.userId); + }, //on elapsed + () => {/* aborted */}, + ); + } + return userTimers; + }, userTimers); + return userTimers; + }, + + _abortUserTimer: function(userId, reason) { + const timer = this.state.userTimers[userId]; + if (timer) { + console.log(`WhoIsTypingTile: aborting timer for ${userId} because ${reason}`); + timer.abort(); + this._removeUserTimer(userId); + } + }, + + _removeUserTimer: function(userId) { + const timer = this.state.userTimers[userId]; + if (timer) { + const userTimers = Object.assign({}, this.state.userTimers); + delete userTimers[userId]; + this.setState({userTimers}); + } + }, + + _renderTypingIndicatorAvatars: function(users, limit) { let othersCount = 0; if (users.length > limit) { othersCount = users.length - limit + 1; @@ -106,8 +180,13 @@ module.exports = React.createClass({ }, render: function() { + let usersTyping = this.state.usersTyping; + const stoppedUsersOnTimer = Object.keys(this.state.userTimers) + .map((userId) => this.props.room.getMember(userId)); + usersTyping = usersTyping.concat(stoppedUsersOnTimer); + const typingString = WhoIsTyping.whoIsTypingString( - this.state.usersTyping, + usersTyping, this.props.whoIsTypingLimit, ); if (!typingString) { @@ -119,7 +198,7 @@ module.exports = React.createClass({ return (
  • - { this._renderTypingIndicatorAvatars(this.props.whoIsTypingLimit) } + { this._renderTypingIndicatorAvatars(usersTyping, this.props.whoIsTypingLimit) }
    { typingString } From d787d3d821d704683b411802dee0462a69c7a8de Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 17 Jan 2019 18:26:11 +0100 Subject: [PATCH 02/13] clear typing bar when receiving event from user --- src/components/views/rooms/WhoIsTypingTile.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/WhoIsTypingTile.js b/src/components/views/rooms/WhoIsTypingTile.js index 144057dfd6..2097f7e99d 100644 --- a/src/components/views/rooms/WhoIsTypingTile.js +++ b/src/components/views/rooms/WhoIsTypingTile.js @@ -74,12 +74,23 @@ module.exports = React.createClass({ onRoomTimeline: function(event, room) { if (room.roomId === this.props.room.roomId) { - console.log(`WhoIsTypingTile: incoming timeline event for ${event.getSender()}`); - this._abortUserTimer(event.getSender(), "timeline event"); + const userId = event.getSender(); + const userWasTyping = this.state.usersTyping.some((m) => m.userId === userId); + if (userWasTyping) { + console.log(`WhoIsTypingTile: remove ${userId} from usersTyping`); + } + const usersTyping = this.state.usersTyping.filter((m) => m.userId !== userId); + this.setState({usersTyping}); + + if (this.state.userTimers[userId]) { + console.log(`WhoIsTypingTile: incoming timeline event for ${userId}`); + } + this._abortUserTimer(userId, "timeline event"); } }, onRoomMemberTyping: function(ev, member) { + //TODO: don't we need to check the roomId here? console.log(`WhoIsTypingTile: incoming typing event for`, ev.getContent().user_ids); const usersTyping = WhoIsTyping.usersTypingApartFromMeAndIgnored(this.props.room); this.setState({ From 1db6f1b652494741b0c84e954f22303737cb83a6 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 17 Jan 2019 18:32:46 +0100 Subject: [PATCH 03/13] remove logging --- src/components/views/rooms/WhoIsTypingTile.js | 25 +++---------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/src/components/views/rooms/WhoIsTypingTile.js b/src/components/views/rooms/WhoIsTypingTile.js index 2097f7e99d..c704f4fd11 100644 --- a/src/components/views/rooms/WhoIsTypingTile.js +++ b/src/components/views/rooms/WhoIsTypingTile.js @@ -75,23 +75,14 @@ module.exports = React.createClass({ onRoomTimeline: function(event, room) { if (room.roomId === this.props.room.roomId) { const userId = event.getSender(); - const userWasTyping = this.state.usersTyping.some((m) => m.userId === userId); - if (userWasTyping) { - console.log(`WhoIsTypingTile: remove ${userId} from usersTyping`); - } + // remove user from usersTyping const usersTyping = this.state.usersTyping.filter((m) => m.userId !== userId); this.setState({usersTyping}); - - if (this.state.userTimers[userId]) { - console.log(`WhoIsTypingTile: incoming timeline event for ${userId}`); - } - this._abortUserTimer(userId, "timeline event"); + this._abortUserTimer(userId); } }, onRoomMemberTyping: function(ev, member) { - //TODO: don't we need to check the roomId here? - console.log(`WhoIsTypingTile: incoming typing event for`, ev.getContent().user_ids); const usersTyping = WhoIsTyping.usersTypingApartFromMeAndIgnored(this.props.room); this.setState({ userTimers: this._updateUserTimers(usersTyping), @@ -115,24 +106,17 @@ module.exports = React.createClass({ let userTimers = Object.assign({}, this.state.userTimers); // remove members that started typing again userTimers = usersThatStartedTyping.reduce((userTimers, m) => { - if (userTimers[m.userId]) { - console.log(`WhoIsTypingTile: stopping timer for ${m.userId} because started typing again`); - } delete userTimers[m.userId]; return userTimers; }, userTimers); // start timer for members that stopped typing userTimers = usersThatStoppedTyping.reduce((userTimers, m) => { if (!userTimers[m.userId]) { - console.log(`WhoIsTypingTile: starting 5s timer for ${m.userId}`); const timer = new Timer(5000); userTimers[m.userId] = timer; timer.start(); timer.finished().then( - () => { - console.log(`WhoIsTypingTile: elapsed 5s timer for ${m.userId}`); - this._removeUserTimer(m.userId); - }, //on elapsed + () => this._removeUserTimer(m.userId), //on elapsed () => {/* aborted */}, ); } @@ -142,10 +126,9 @@ module.exports = React.createClass({ return userTimers; }, - _abortUserTimer: function(userId, reason) { + _abortUserTimer: function(userId) { const timer = this.state.userTimers[userId]; if (timer) { - console.log(`WhoIsTypingTile: aborting timer for ${userId} because ${reason}`); timer.abort(); this._removeUserTimer(userId); } From 146e651daec47d2e50450beea0c2026154ac70cb Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 17 Jan 2019 18:32:53 +0100 Subject: [PATCH 04/13] add comments --- src/components/views/rooms/WhoIsTypingTile.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/components/views/rooms/WhoIsTypingTile.js b/src/components/views/rooms/WhoIsTypingTile.js index c704f4fd11..f15e2c8e52 100644 --- a/src/components/views/rooms/WhoIsTypingTile.js +++ b/src/components/views/rooms/WhoIsTypingTile.js @@ -45,6 +45,11 @@ module.exports = React.createClass({ return { usersTyping: WhoIsTyping.usersTypingApartFromMe(this.props.room), userTimers: {}, + // a map with userid => Timer to delay + // hiding the "x is typing" message for a + // user so hiding it can coincide + // with the sent message by the other side + // resulting in less timeline jumpiness }; }, @@ -78,6 +83,7 @@ module.exports = React.createClass({ // remove user from usersTyping const usersTyping = this.state.usersTyping.filter((m) => m.userId !== userId); this.setState({usersTyping}); + // abort timer if any this._abortUserTimer(userId); } }, @@ -177,6 +183,9 @@ module.exports = React.createClass({ let usersTyping = this.state.usersTyping; const stoppedUsersOnTimer = Object.keys(this.state.userTimers) .map((userId) => this.props.room.getMember(userId)); + // append the users that have been reported not typing anymore + // but have a timeout timer running so they can disappear + // when a message comes in usersTyping = usersTyping.concat(stoppedUsersOnTimer); const typingString = WhoIsTyping.whoIsTypingString( From 2920deaefe10a102cf431f56c809f51aa3b0e5f9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 17 Jan 2019 18:52:22 +0100 Subject: [PATCH 05/13] better naming --- src/components/views/rooms/WhoIsTypingTile.js | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/src/components/views/rooms/WhoIsTypingTile.js b/src/components/views/rooms/WhoIsTypingTile.js index f15e2c8e52..5a2b6afc96 100644 --- a/src/components/views/rooms/WhoIsTypingTile.js +++ b/src/components/views/rooms/WhoIsTypingTile.js @@ -44,12 +44,12 @@ module.exports = React.createClass({ getInitialState: function() { return { usersTyping: WhoIsTyping.usersTypingApartFromMe(this.props.room), - userTimers: {}, // a map with userid => Timer to delay // hiding the "x is typing" message for a // user so hiding it can coincide // with the sent message by the other side // resulting in less timeline jumpiness + delayedStopTypingTimers: {}, }; }, @@ -74,7 +74,7 @@ module.exports = React.createClass({ client.removeListener("RoomMember.typing", this.onRoomMemberTyping); client.removeListener("Room.timeline", this.onRoomTimeline); } - Object.values(this.state.userTimers).forEach((t) => t.abort()); + Object.values(this.state.delayedStopTypingTimers).forEach((t) => t.abort()); }, onRoomTimeline: function(event, room) { @@ -91,12 +91,12 @@ module.exports = React.createClass({ onRoomMemberTyping: function(ev, member) { const usersTyping = WhoIsTyping.usersTypingApartFromMeAndIgnored(this.props.room); this.setState({ - userTimers: this._updateUserTimers(usersTyping), + delayedStopTypingTimers: this._updateDelayedStopTypingTimers(usersTyping), usersTyping, }); }, - _updateUserTimers(usersTyping) { + _updateDelayedStopTypingTimers(usersTyping) { const usersThatStoppedTyping = this.state.usersTyping.filter((a) => { return !usersTyping.some((b) => a.userId === b.userId); }); @@ -105,35 +105,35 @@ module.exports = React.createClass({ }); // abort all the timers for the users that started typing again usersThatStartedTyping.forEach((m) => { - const timer = this.state.userTimers[m.userId]; + const timer = this.state.delayedStopTypingTimers[m.userId]; timer && timer.abort(); }); - // prepare new userTimers object to update state with - let userTimers = Object.assign({}, this.state.userTimers); + // prepare new delayedStopTypingTimers object to update state with + let delayedStopTypingTimers = Object.assign({}, this.state.delayedStopTypingTimers); // remove members that started typing again - userTimers = usersThatStartedTyping.reduce((userTimers, m) => { - delete userTimers[m.userId]; - return userTimers; - }, userTimers); + delayedStopTypingTimers = usersThatStartedTyping.reduce((delayedStopTypingTimers, m) => { + delete delayedStopTypingTimers[m.userId]; + return delayedStopTypingTimers; + }, delayedStopTypingTimers); // start timer for members that stopped typing - userTimers = usersThatStoppedTyping.reduce((userTimers, m) => { - if (!userTimers[m.userId]) { + delayedStopTypingTimers = usersThatStoppedTyping.reduce((delayedStopTypingTimers, m) => { + if (!delayedStopTypingTimers[m.userId]) { const timer = new Timer(5000); - userTimers[m.userId] = timer; + delayedStopTypingTimers[m.userId] = timer; timer.start(); timer.finished().then( () => this._removeUserTimer(m.userId), //on elapsed () => {/* aborted */}, ); } - return userTimers; - }, userTimers); + return delayedStopTypingTimers; + }, delayedStopTypingTimers); - return userTimers; + return delayedStopTypingTimers; }, _abortUserTimer: function(userId) { - const timer = this.state.userTimers[userId]; + const timer = this.state.delayedStopTypingTimers[userId]; if (timer) { timer.abort(); this._removeUserTimer(userId); @@ -141,11 +141,11 @@ module.exports = React.createClass({ }, _removeUserTimer: function(userId) { - const timer = this.state.userTimers[userId]; + const timer = this.state.delayedStopTypingTimers[userId]; if (timer) { - const userTimers = Object.assign({}, this.state.userTimers); - delete userTimers[userId]; - this.setState({userTimers}); + const delayedStopTypingTimers = Object.assign({}, this.state.delayedStopTypingTimers); + delete delayedStopTypingTimers[userId]; + this.setState({delayedStopTypingTimers}); } }, @@ -181,7 +181,7 @@ module.exports = React.createClass({ render: function() { let usersTyping = this.state.usersTyping; - const stoppedUsersOnTimer = Object.keys(this.state.userTimers) + const stoppedUsersOnTimer = Object.keys(this.state.delayedStopTypingTimers) .map((userId) => this.props.room.getMember(userId)); // append the users that have been reported not typing anymore // but have a timeout timer running so they can disappear From c9d5c4903b3215b870a52dab4508e6b7e688cbf1 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 18 Jan 2019 18:53:54 +0100 Subject: [PATCH 06/13] set min-height of messagelist to current height when showing typing bar this ensures the timeline never shrinks, and avoids jumpiness when typing bar disappears again. --- src/components/structures/MessagePanel.js | 8 +++++--- src/components/structures/ScrollPanel.js | 25 +++++++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 5fe2aae471..5383cf15dc 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -631,9 +631,11 @@ module.exports = React.createClass({ } }, - _scrollDownIfAtBottom: function() { + _onTypingVisible: function() { const scrollPanel = this.refs.scrollPanel; - if (scrollPanel) { + if (scrollPanel && scrollPanel.getScrollState().stuckAtBottom) { + scrollPanel.blockShrinking(); + // scroll down if at bottom scrollPanel.checkScroll(); } }, @@ -666,7 +668,7 @@ module.exports = React.createClass({ let whoIsTyping; if (this.props.room) { - whoIsTyping = (); + whoIsTyping = (); } return ( diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index 0fdbc9a349..91a9f1ddfa 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -223,6 +223,8 @@ module.exports = React.createClass({ onResize: function() { this.props.onResize(); + // clear min-height as the height might have changed + this.clearBlockShrinking(); this.checkScroll(); if (this._gemScroll) this._gemScroll.forceUpdate(); }, @@ -372,6 +374,8 @@ module.exports = React.createClass({ } this._unfillDebouncer = setTimeout(() => { this._unfillDebouncer = null; + // if timeline shrinks, min-height should be cleared + this.clearBlockShrinking(); this.props.onUnfillRequest(backwards, markerScrollToken); }, UNFILL_REQUEST_DEBOUNCE_MS); } @@ -677,6 +681,27 @@ module.exports = React.createClass({ _collectGeminiScroll: function(gemScroll) { this._gemScroll = gemScroll; }, + /** + * Set the current height as the min height for the message list + * so the timeline cannot shrink. This is used to avoid + * jumping when the typing indicator gets replaced by a smaller message. + */ + blockShrinking: function() { + const messageList = this.refs.itemlist; + if (messageList) { + const currentHeight = messageList.clientHeight - 18; + messageList.style.minHeight = `${currentHeight}px`; + } + }, + /** + * Clear the previously set min height + */ + clearBlockShrinking: function() { + const messageList = this.refs.itemlist; + if (messageList) { + messageList.style.minHeight = null; + } + }, render: function() { const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); From 018f3d2a5ce7c501b61b1c5a9348ba437938b487 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 21 Jan 2019 17:30:02 +0100 Subject: [PATCH 07/13] use box-sizing: border-box to make clientHeight === actual height --- res/css/structures/_RoomView.scss | 4 ++++ src/components/structures/ScrollPanel.js | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss index d8926c68e4..02d5cc67b0 100644 --- a/res/css/structures/_RoomView.scss +++ b/res/css/structures/_RoomView.scss @@ -171,6 +171,10 @@ limitations under the License. .mx_RoomView_MessageList { list-style-type: none; padding: 18px; + margin: 0; + /* needed as min-height is set to clientHeight in ScrollPanel + to prevent shrinking when WhoIsTypingTile is hidden */ + box-sizing: border-box; } .mx_RoomView_MessageList li { diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index 91a9f1ddfa..96318d64e1 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -689,7 +689,7 @@ module.exports = React.createClass({ blockShrinking: function() { const messageList = this.refs.itemlist; if (messageList) { - const currentHeight = messageList.clientHeight - 18; + const currentHeight = messageList.clientHeight; messageList.style.minHeight = `${currentHeight}px`; } }, From 25aa58f29f526baba9bb6b212fd1f67e2b385284 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 21 Jan 2019 17:35:40 +0100 Subject: [PATCH 08/13] increase/clear min-height on timeline on new message(s) depending on whether the typing bar is visible --- src/components/structures/MessagePanel.js | 16 +++++++++++++++- src/components/structures/TimelinePanel.js | 11 ++++++++--- src/components/views/rooms/WhoIsTypingTile.js | 4 ++++ 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 5383cf15dc..fc3b421e89 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -640,6 +640,20 @@ module.exports = React.createClass({ } }, + updateTimelineMinHeight: function() { + const scrollPanel = this.refs.scrollPanel; + const whoIsTyping = this.refs.whoIsTyping; + const isTypingVisible = whoIsTyping && whoIsTyping.isVisible(); + + if (scrollPanel) { + if (isTypingVisible) { + scrollPanel.blockShrinking(); + } else { + scrollPanel.clearBlockShrinking(); + } + } + }, + onResize: function() { dis.dispatch({ action: 'timeline_resize' }, true); }, @@ -668,7 +682,7 @@ module.exports = React.createClass({ let whoIsTyping; if (this.props.room) { - whoIsTyping = (); + whoIsTyping = (); } return ( diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index ab10ec4aca..9fe83c2c2d 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -455,7 +455,7 @@ var TimelinePanel = React.createClass({ // const myUserId = MatrixClientPeg.get().credentials.userId; const sender = ev.sender ? ev.sender.userId : null; - var callback = null; + var callRMUpdated = false; if (sender != myUserId && !UserActivity.userCurrentlyActive()) { updatedState.readMarkerVisible = true; } else if (lastEv && this.getReadMarkerPosition() === 0) { @@ -465,11 +465,16 @@ var TimelinePanel = React.createClass({ this._setReadMarker(lastEv.getId(), lastEv.getTs(), true); updatedState.readMarkerVisible = false; updatedState.readMarkerEventId = lastEv.getId(); - callback = this.props.onReadMarkerUpdated; + callRMUpdated = true; } } - this.setState(updatedState, callback); + this.setState(updatedState, () => { + this.refs.messagePanel.updateTimelineMinHeight(); + if (callRMUpdated) { + this.props.onReadMarkerUpdated(); + } + }); }); }, diff --git a/src/components/views/rooms/WhoIsTypingTile.js b/src/components/views/rooms/WhoIsTypingTile.js index 5a2b6afc96..bef8aca0c4 100644 --- a/src/components/views/rooms/WhoIsTypingTile.js +++ b/src/components/views/rooms/WhoIsTypingTile.js @@ -77,6 +77,10 @@ module.exports = React.createClass({ Object.values(this.state.delayedStopTypingTimers).forEach((t) => t.abort()); }, + isVisible: function() { + return this.state.usersTyping.length !== 0 || Object.keys(this.state.delayedStopTypingTimers) !== 0; + }, + onRoomTimeline: function(event, room) { if (room.roomId === this.props.room.roomId) { const userId = event.getSender(); From 86357fde511fdae5158c38c749a1a8eba5e95499 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 21 Jan 2019 17:49:46 +0100 Subject: [PATCH 09/13] sort combined typing users by name so order is stable --- src/components/views/rooms/WhoIsTypingTile.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/views/rooms/WhoIsTypingTile.js b/src/components/views/rooms/WhoIsTypingTile.js index bef8aca0c4..4c97110797 100644 --- a/src/components/views/rooms/WhoIsTypingTile.js +++ b/src/components/views/rooms/WhoIsTypingTile.js @@ -191,6 +191,9 @@ module.exports = React.createClass({ // but have a timeout timer running so they can disappear // when a message comes in usersTyping = usersTyping.concat(stoppedUsersOnTimer); + // sort them so the typing members don't change order when + // moved to delayedStopTypingTimers + usersTyping.sort((a, b) => a.name.localeCompare(b.name)); const typingString = WhoIsTyping.whoIsTypingString( usersTyping, From 881353037e30767957f638a7a06521efe1424994 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 22 Jan 2019 10:34:54 +0000 Subject: [PATCH 10/13] forgot .length somehow Co-Authored-By: bwindels --- src/components/views/rooms/WhoIsTypingTile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/WhoIsTypingTile.js b/src/components/views/rooms/WhoIsTypingTile.js index 4c97110797..b540abf61e 100644 --- a/src/components/views/rooms/WhoIsTypingTile.js +++ b/src/components/views/rooms/WhoIsTypingTile.js @@ -78,7 +78,7 @@ module.exports = React.createClass({ }, isVisible: function() { - return this.state.usersTyping.length !== 0 || Object.keys(this.state.delayedStopTypingTimers) !== 0; + return this.state.usersTyping.length !== 0 || Object.keys(this.state.delayedStopTypingTimers).length !== 0; }, onRoomTimeline: function(event, room) { From 5d45d5dfac1dd0f166f7a6ae778dd36735401336 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 22 Jan 2019 11:37:09 +0100 Subject: [PATCH 11/13] formatting --- src/components/structures/ScrollPanel.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index 96318d64e1..be5f23c420 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -681,6 +681,7 @@ module.exports = React.createClass({ _collectGeminiScroll: function(gemScroll) { this._gemScroll = gemScroll; }, + /** * Set the current height as the min height for the message list * so the timeline cannot shrink. This is used to avoid @@ -693,6 +694,7 @@ module.exports = React.createClass({ messageList.style.minHeight = `${currentHeight}px`; } }, + /** * Clear the previously set min height */ From 0e0128c29749651ff733de7eeddad5e18ea46583 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 22 Jan 2019 11:37:18 +0100 Subject: [PATCH 12/13] if instead of short-circuit and for readability --- src/components/views/rooms/WhoIsTypingTile.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/WhoIsTypingTile.js b/src/components/views/rooms/WhoIsTypingTile.js index b540abf61e..e826380469 100644 --- a/src/components/views/rooms/WhoIsTypingTile.js +++ b/src/components/views/rooms/WhoIsTypingTile.js @@ -110,7 +110,9 @@ module.exports = React.createClass({ // abort all the timers for the users that started typing again usersThatStartedTyping.forEach((m) => { const timer = this.state.delayedStopTypingTimers[m.userId]; - timer && timer.abort(); + if (timer) { + timer.abort(); + } }); // prepare new delayedStopTypingTimers object to update state with let delayedStopTypingTimers = Object.assign({}, this.state.delayedStopTypingTimers); From f83411ea0b97b92bd73091f21451b55a95817087 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 22 Jan 2019 11:46:02 +0100 Subject: [PATCH 13/13] whitespace --- src/components/views/rooms/WhoIsTypingTile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/WhoIsTypingTile.js b/src/components/views/rooms/WhoIsTypingTile.js index e826380469..4ee11d77b2 100644 --- a/src/components/views/rooms/WhoIsTypingTile.js +++ b/src/components/views/rooms/WhoIsTypingTile.js @@ -128,7 +128,7 @@ module.exports = React.createClass({ delayedStopTypingTimers[m.userId] = timer; timer.start(); timer.finished().then( - () => this._removeUserTimer(m.userId), //on elapsed + () => this._removeUserTimer(m.userId), // on elapsed () => {/* aborted */}, ); }