diff --git a/res/css/_components.scss b/res/css/_components.scss index a230842c77..d56f782ffb 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -108,6 +108,7 @@ @import "./views/rooms/_SearchableEntityList.scss"; @import "./views/rooms/_Stickers.scss"; @import "./views/rooms/_TopUnreadMessagesBar.scss"; +@import "./views/rooms/_WhoIsTypingTile.scss"; @import "./views/settings/_DevicesPanel.scss"; @import "./views/settings/_IntegrationsManager.scss"; @import "./views/settings/_Notifications.scss"; diff --git a/res/css/views/rooms/_WhoIsTypingTile.scss b/res/css/views/rooms/_WhoIsTypingTile.scss new file mode 100644 index 0000000000..217a10be8d --- /dev/null +++ b/res/css/views/rooms/_WhoIsTypingTile.scss @@ -0,0 +1,77 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_WhoIsTypingTile { + margin-left: -18px; //offset padding from mx_RoomView_MessageList to center avatars + padding-top: 18px; + display: flex; + align-items: center; +} + +/* position the indicator in the same place horizontally as .mx_EventTile_avatar. */ +.mx_WhoIsTypingTile_avatars { + flex: 0 0 83px; // 18 + 65 + text-align: center; +} + +.mx_WhoIsTypingTile_avatars > :not(:first-child) { + margin-left: -12px; +} + +.mx_WhoIsTypingTile_avatars .mx_BaseAvatar_image { + border: 1px solid $primary-bg-color; +} + +.mx_WhoIsTypingTile_avatars .mx_BaseAvatar_initial { + padding-top: 1px; +} + +.mx_WhoIsTypingTile_remainingAvatarPlaceholder { + display: inline-block; + color: #acacac; + background-color: #ddd; + border: 1px solid $primary-bg-color; + border-radius: 40px; + width: 24px; + height: 24px; + line-height: 24px; + font-size: 0.8em; + vertical-align: top; + text-align: center; +} + +.mx_WhoIsTypingTile_label { + flex: 1; + font-size: 14px; + font-weight: 600; + color: $eventtile-meta-color; +} + +.mx_WhoIsTypingTile_label > span { + background-image: url('../../img/typing-indicator-2x.gif'); + background-size: 25px; + background-position: left bottom; + background-repeat: no-repeat; + padding-bottom: 15px; + display: block; +} + +.mx_MatrixChat_useCompactLayout { + + .mx_WhoIsTypingTile { + padding-top: 4px; + } +} diff --git a/res/img/typing-indicator-2x.gif b/res/img/typing-indicator-2x.gif new file mode 100644 index 0000000000..86e34c7555 Binary files /dev/null and b/res/img/typing-indicator-2x.gif differ diff --git a/res/themes/dharma/css/_dharma.scss b/res/themes/dharma/css/_dharma.scss index 0b53202240..07da833cfb 100644 --- a/res/themes/dharma/css/_dharma.scss +++ b/res/themes/dharma/css/_dharma.scss @@ -119,6 +119,7 @@ $topleftmenu-color: #212121; $roomheader-color: #45474a; $roomheader-addroom-color: #929eb4; $roomtopic-color: #9fa9ba; +$eventtile-meta-color: $roomtopic-color; // ******************** diff --git a/res/themes/light/css/_base.scss b/res/themes/light/css/_base.scss index bee9157279..4c41037229 100644 --- a/res/themes/light/css/_base.scss +++ b/res/themes/light/css/_base.scss @@ -115,7 +115,7 @@ $topleftmenu-color: $primary-fg-color; $roomheader-color: $primary-fg-color; $roomheader-addroom-color: $primary-bg-color; $roomtopic-color: $settings-grey-fg-color; - +$eventtile-meta-color: $roomtopic-color; // ******************** $roomtile-name-color: rgba(69, 69, 69, 0.8); diff --git a/src/WhoIsTyping.js b/src/WhoIsTyping.js index 0edad8d4a5..78ca77ce5a 100644 --- a/src/WhoIsTyping.js +++ b/src/WhoIsTyping.js @@ -63,16 +63,16 @@ module.exports = { if (whoIsTyping.length == 0) { return ''; } else if (whoIsTyping.length == 1) { - return _t('%(displayName)s is typing', {displayName: whoIsTyping[0].name}); + return _t('%(displayName)s is typing …', {displayName: whoIsTyping[0].name}); } const names = whoIsTyping.map(function(m) { return m.name; }); if (othersCount>=1) { - return _t('%(names)s and %(count)s others are typing', {names: names.slice(0, limit - 1).join(', '), count: othersCount}); + return _t('%(names)s and %(count)s others are typing …', {names: names.slice(0, limit - 1).join(', '), count: othersCount}); } else { const lastPerson = names.pop(); - return _t('%(names)s and %(lastPerson)s are typing', {names: names.join(', '), lastPerson: lastPerson}); + return _t('%(names)s and %(lastPerson)s are typing …', {names: names.join(', '), lastPerson: lastPerson}); } }, }; diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index bbaea617f4..1107be8464 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -631,12 +631,20 @@ module.exports = React.createClass({ } }, + _scrollDownIfAtBottom: function() { + const scrollPanel = this.refs.scrollPanel; + if (scrollPanel) { + scrollPanel.checkScroll(); + } + }, + onResize: function() { dis.dispatch({ action: 'timeline_resize' }, true); }, render: function() { const ScrollPanel = sdk.getComponent("structures.ScrollPanel"); + const WhoIsTypingTile = sdk.getComponent("rooms.WhoIsTypingTile"); const Spinner = sdk.getComponent("elements.Spinner"); let topSpinner; let bottomSpinner; @@ -666,6 +674,7 @@ module.exports = React.createClass({ stickyBottom={this.props.stickyBottom}> { topSpinner } { this._getEventTiles() } + { bottomSpinner } ); diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index b4d70f3397..6502fb7c37 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -62,10 +62,6 @@ module.exports = React.createClass({ // more interesting) hasActiveCall: PropTypes.bool, - // Number of names to display in typing indication. E.g. set to 3, will - // result in "X, Y, Z and 100 others are typing." - whoIsTypingLimit: PropTypes.number, - // true if the room is being peeked at. This affects components that shouldn't // logically be shown when peeking, such as a prompt to invite people to a room. isPeeking: PropTypes.bool, @@ -103,24 +99,16 @@ module.exports = React.createClass({ onVisible: PropTypes.func, }, - getDefaultProps: function() { - return { - whoIsTypingLimit: 3, - }; - }, - getInitialState: function() { return { syncState: MatrixClientPeg.get().getSyncState(), syncStateData: MatrixClientPeg.get().getSyncStateData(), - usersTyping: WhoIsTyping.usersTypingApartFromMe(this.props.room), unsentMessages: getUnsentMessages(this.props.room), }; }, componentWillMount: function() { MatrixClientPeg.get().on("sync", this.onSyncStateChange); - MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping); MatrixClientPeg.get().on("Room.localEchoUpdated", this._onRoomLocalEchoUpdated); this._checkSize(); @@ -135,7 +123,6 @@ module.exports = React.createClass({ const client = MatrixClientPeg.get(); if (client) { client.removeListener("sync", this.onSyncStateChange); - client.removeListener("RoomMember.typing", this.onRoomMemberTyping); client.removeListener("Room.localEchoUpdated", this._onRoomLocalEchoUpdated); } }, @@ -150,12 +137,6 @@ module.exports = React.createClass({ }); }, - onRoomMemberTyping: function(ev, member) { - this.setState({ - usersTyping: WhoIsTyping.usersTypingApartFromMeAndIgnored(this.props.room), - }); - }, - _onSendWithoutVerifyingClick: function() { cryptodevices.getUnknownDevicesForRoom(MatrixClientPeg.get(), this.props.room).then((devices) => { cryptodevices.markAllDevicesKnown(MatrixClientPeg.get(), devices); @@ -199,7 +180,6 @@ module.exports = React.createClass({ // indicate other sizes. _getSize: function() { if (this._shouldShowConnectionError() || - (this.state.usersTyping.length > 0) || this.props.numUnreadMessages || !this.props.atEndOfLiveTimeline || this.props.hasActiveCall || @@ -213,10 +193,7 @@ module.exports = React.createClass({ }, // return suitable content for the image on the left of the status bar. - // - // if wantPlaceholder is true, we include a "..." placeholder if - // there is nothing better to put in. - _getIndicator: function(wantPlaceholder) { + _getIndicator: function() { if (this.props.numUnreadMessages) { return (
- { this._renderTypingIndicatorAvatars(this.props.whoIsTypingLimit) } -
- ); - } - return null; }, - _renderTypingIndicatorAvatars: function(limit) { - let users = this.state.usersTyping; - - let othersCount = 0; - if (users.length > limit) { - othersCount = users.length - limit + 1; - users = users.slice(0, limit - 1); - } - - const avatars = users.map((u) => { - return ( - - ); - }); - - if (othersCount > 0) { - avatars.push( - - +{ othersCount } - , - ); - } - - return avatars; - }, - _shouldShowConnectionError: function() { // no conn bar trumps unread count since you can't get unread messages // without a connection! (technically may already have some but meh) @@ -440,18 +377,6 @@ module.exports = React.createClass({ ); } - const typingString = WhoIsTyping.whoIsTypingString( - this.state.usersTyping, - this.props.whoIsTypingLimit, - ); - if (typingString) { - return ( -
- { typingString } -
- ); - } - if (this.props.hasActiveCall) { return (
@@ -483,7 +408,7 @@ module.exports = React.createClass({ render: function() { const content = this._getContent(); - const indicator = this._getIndicator(this.state.usersTyping.length > 0); + const indicator = this._getIndicator(); return (
diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 2d9443efb8..12af0eceac 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1613,7 +1613,6 @@ module.exports = React.createClass({ onResize={this.onChildResize} onVisible={this.onStatusBarVisible} onHidden={this.onStatusBarHidden} - whoIsTypingLimit={3} />; } diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 44d14cd7cf..f5dc6c7ef4 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -1154,6 +1154,7 @@ var TimelinePanel = React.createClass({ ); return (