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 (
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;
+ },
+
+ render: function() {
+ const typingString = WhoIsTyping.whoIsTypingString(
+ this.state.usersTyping,
+ this.props.whoIsTypingLimit,
+ );
+ if (!typingString) {
+ return ();
+ }
+
+ const EmojiText = sdk.getComponent('elements.EmojiText');
+
+ return (
+
+
+ { this._renderTypingIndicatorAvatars(this.props.whoIsTypingLimit) }
+
+
+ { typingString }
+
+
+ );
+ },
+});
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index d0b34296bf..713d6a9cba 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -202,10 +202,10 @@
"%(widgetName)s widget modified by %(senderName)s": "%(widgetName)s widget modified by %(senderName)s",
"%(widgetName)s widget added by %(senderName)s": "%(widgetName)s widget added by %(senderName)s",
"%(widgetName)s widget removed by %(senderName)s": "%(widgetName)s widget removed by %(senderName)s",
- "%(displayName)s is typing": "%(displayName)s is typing",
- "%(names)s and %(count)s others are typing|other": "%(names)s and %(count)s others are typing",
- "%(names)s and %(count)s others are typing|one": "%(names)s and one other is typing",
- "%(names)s and %(lastPerson)s are typing": "%(names)s and %(lastPerson)s are typing",
+ "%(displayName)s is typing …": "%(displayName)s is typing …",
+ "%(names)s and %(count)s others are typing …|other": "%(names)s and %(count)s others are typing …",
+ "%(names)s and %(count)s others are typing …|one": "%(names)s and one other is typing …",
+ "%(names)s and %(lastPerson)s are typing …": "%(names)s and %(lastPerson)s are typing …",
"Failure to create room": "Failure to create room",
"Server may be unavailable, overloaded, or you hit a bug.": "Server may be unavailable, overloaded, or you hit a bug.",
"Send anyway": "Send anyway",
diff --git a/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js
index e7176e2c16..0a51cb8918 100644
--- a/test/components/structures/MessagePanel-test.js
+++ b/test/components/structures/MessagePanel-test.js
@@ -26,11 +26,13 @@ const sdk = require('matrix-react-sdk');
const MessagePanel = sdk.getComponent('structures.MessagePanel');
import MatrixClientPeg from '../../../src/MatrixClientPeg';
+import Matrix from 'matrix-js-sdk';
const test_utils = require('test-utils');
const mockclock = require('mock-clock');
let client;
+const room = new Matrix.Room();
// wrap MessagePanel with a component which provides the MatrixClient in the context.
const WrappedMessagePanel = React.createClass({
@@ -45,7 +47,7 @@ const WrappedMessagePanel = React.createClass({
},
render: function() {
- return ;
+ return ;
},
});
diff --git a/test/components/structures/TimelinePanel-test.js b/test/components/structures/TimelinePanel-test.js
index ea62d427bc..01ea6d8421 100644
--- a/test/components/structures/TimelinePanel-test.js
+++ b/test/components/structures/TimelinePanel-test.js
@@ -82,6 +82,8 @@ describe('TimelinePanel', function() {
sandbox = test_utils.stubClient(sandbox);
room = sinon.createStubInstance(jssdk.Room);
+ room.currentState = sinon.createStubInstance(jssdk.RoomState);
+ room.currentState.members = {};
room.roomId = ROOM_ID;
timelineSet = sinon.createStubInstance(jssdk.EventTimelineSet);