diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js
index 952648010c..bb6b4d02cb 100644
--- a/src/ScalarMessaging.js
+++ b/src/ScalarMessaging.js
@@ -256,6 +256,7 @@ function returnStateEvent(event, roomId, eventType, stateKey) {
}
var currentRoomId = null;
+var currentRoomAlias = null;
// Listen for when a room is viewed
dis.register(onAction);
@@ -264,6 +265,7 @@ function onAction(payload) {
return;
}
currentRoomId = payload.room_id;
+ currentRoomAlias = payload.room_alias;
}
const onMessage = function(event) {
@@ -287,45 +289,59 @@ const onMessage = function(event) {
sendError(event, "Missing room_id in request");
return;
}
+ let promise = Promise.resolve(currentRoomId);
if (!currentRoomId) {
- sendError(event, "Must be viewing a room");
- return;
- }
- if (roomId !== currentRoomId) {
- sendError(event, "Room " + roomId + " not visible");
- return;
+ if (!currentRoomAlias) {
+ sendError(event, "Must be viewing a room");
+ return;
+ }
+ // no room ID but there is an alias, look it up.
+ console.log("Looking up alias " + currentRoomAlias);
+ promise = MatrixClientPeg.get().getRoomIdForAlias(currentRoomAlias).then((res) => {
+ return res.room_id;
+ });
}
- // Getting join rules does not require userId
- if (event.data.action === "join_rules_state") {
- getJoinRules(event, roomId);
- return;
- }
+ promise.then((viewingRoomId) => {
+ if (roomId !== viewingRoomId) {
+ sendError(event, "Room " + roomId + " not visible");
+ return;
+ }
- if (!userId) {
- sendError(event, "Missing user_id in request");
- return;
- }
- switch (event.data.action) {
- case "membership_state":
- getMembershipState(event, roomId, userId);
- break;
- case "invite":
- inviteUser(event, roomId, userId);
- break;
- case "bot_options":
- botOptions(event, roomId, userId);
- break;
- case "set_bot_options":
- setBotOptions(event, roomId, userId);
- break;
- case "set_bot_power":
- setBotPower(event, roomId, userId, event.data.level);
- break;
- default:
- console.warn("Unhandled postMessage event with action '" + event.data.action +"'");
- break;
- }
+ // Getting join rules does not require userId
+ if (event.data.action === "join_rules_state") {
+ getJoinRules(event, roomId);
+ return;
+ }
+
+ if (!userId) {
+ sendError(event, "Missing user_id in request");
+ return;
+ }
+ switch (event.data.action) {
+ case "membership_state":
+ getMembershipState(event, roomId, userId);
+ break;
+ case "invite":
+ inviteUser(event, roomId, userId);
+ break;
+ case "bot_options":
+ botOptions(event, roomId, userId);
+ break;
+ case "set_bot_options":
+ setBotOptions(event, roomId, userId);
+ break;
+ case "set_bot_power":
+ setBotPower(event, roomId, userId, event.data.level);
+ break;
+ default:
+ console.warn("Unhandled postMessage event with action '" + event.data.action +"'");
+ break;
+ }
+ }, (err) => {
+ console.error(err);
+ sendError(event, "Failed to lookup current room.");
+ })
};
module.exports = {
diff --git a/src/Signup.js b/src/Signup.js
index 1ac92f3218..eaf1906059 100644
--- a/src/Signup.js
+++ b/src/Signup.js
@@ -6,6 +6,7 @@ var MatrixClientPeg = require("./MatrixClientPeg");
var SignupStages = require("./SignupStages");
var dis = require("./dispatcher");
var q = require("q");
+var url = require("url");
const EMAIL_STAGE_TYPE = "m.login.email.identity";
@@ -413,6 +414,15 @@ class Login extends Signup {
throw error;
});
}
+
+ redirectToCas() {
+ var client = this._createTemporaryClient();
+ var parsedUrl = url.parse(window.location.href, true);
+ parsedUrl.query["homeserver"] = client.getHomeserverUrl();
+ parsedUrl.query["identityServer"] = client.getIdentityServerUrl();
+ var casUrl = client.getCasLoginUrl(url.format(parsedUrl));
+ window.location.href = casUrl;
+ }
}
module.exports.Register = Register;
diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js
index 0315a3186a..ff4ad576f1 100644
--- a/src/components/structures/login/Login.js
+++ b/src/components/structures/login/Login.js
@@ -92,6 +92,10 @@ module.exports = React.createClass({
}).done();
},
+ onCasLogin: function() {
+ this._loginLogic.redirectToCas();
+ },
+
_onLoginAsGuestClick: function() {
var self = this;
self.setState({
@@ -228,7 +232,7 @@ module.exports = React.createClass({
);
case 'm.login.cas':
return (
-
+
);
default:
if (!step) {
diff --git a/src/components/views/login/CasLogin.js b/src/components/views/login/CasLogin.js
index 5c89fd3706..c818586d52 100644
--- a/src/components/views/login/CasLogin.js
+++ b/src/components/views/login/CasLogin.js
@@ -16,26 +16,19 @@ limitations under the License.
'use strict';
-var MatrixClientPeg = require("../../../MatrixClientPeg");
var React = require('react');
-var url = require("url");
module.exports = React.createClass({
displayName: 'CasLogin',
- onCasClicked: function(ev) {
- var cli = MatrixClientPeg.get();
- var parsedUrl = url.parse(window.location.href, true);
- parsedUrl.query["homeserver"] = cli.getHomeserverUrl();
- parsedUrl.query["identityServer"] = cli.getIdentityServerUrl();
- var casUrl = MatrixClientPeg.get().getCasLoginUrl(url.format(parsedUrl));
- window.location.href = casUrl;
+ propTypes: {
+ onSubmit: React.PropTypes.func, // fn()
},
render: function() {
return (
+
{deviceName}
{indicator}
+
+ {this.props.device.getFingerprint()}
+
{verifyButton}
{blockButton}
diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js
index be6663be67..b6944a3d4e 100644
--- a/src/components/views/rooms/MemberInfo.js
+++ b/src/components/views/rooms/MemberInfo.js
@@ -26,12 +26,16 @@ limitations under the License.
* 'isTargetMod': boolean
*/
var React = require('react');
+var classNames = require('classnames');
var MatrixClientPeg = require("../../../MatrixClientPeg");
var dis = require("../../../dispatcher");
var Modal = require("../../../Modal");
var sdk = require('../../../index');
var UserSettingsStore = require('../../../UserSettingsStore');
var createRoom = require('../../../createRoom');
+var DMRoomMap = require('../../../utils/DMRoomMap');
+var Unread = require('../../../Unread');
+var Receipt = require('../../../utils/Receipt');
module.exports = React.createClass({
displayName: 'MemberInfo',
@@ -60,7 +64,6 @@ module.exports = React.createClass({
updating: 0,
devicesLoading: true,
devices: null,
- existingOneToOneRoomId: null,
}
},
@@ -72,14 +75,20 @@ module.exports = React.createClass({
this._enableDevices = MatrixClientPeg.get().isCryptoEnabled() &&
UserSettingsStore.isFeatureEnabled("e2e_encryption");
- this.setState({
- existingOneToOneRoomId: this.getExistingOneToOneRoomId()
- });
+ const cli = MatrixClientPeg.get();
+ cli.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
+ cli.on("Room", this.onRoom);
+ cli.on("deleteRoom", this.onDeleteRoom);
+ cli.on("Room.timeline", this.onRoomTimeline);
+ cli.on("Room.name", this.onRoomName);
+ cli.on("Room.receipt", this.onRoomReceipt);
+ cli.on("RoomState.events", this.onRoomStateEvents);
+ cli.on("RoomMember.name", this.onRoomMemberName);
+ cli.on("accountData", this.onAccountData);
},
componentDidMount: function() {
this._updateStateForNewMember(this.props.member);
- MatrixClientPeg.get().on("deviceVerificationChanged", this.onDeviceVerificationChanged);
},
componentWillReceiveProps: function(newProps) {
@@ -92,65 +101,20 @@ module.exports = React.createClass({
var client = MatrixClientPeg.get();
if (client) {
client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
+ client.removeListener("Room", this.onRoom);
+ client.removeListener("deleteRoom", this.onDeleteRoom);
+ client.removeListener("Room.timeline", this.onRoomTimeline);
+ client.removeListener("Room.name", this.onRoomName);
+ client.removeListener("Room.receipt", this.onRoomReceipt);
+ client.removeListener("RoomState.events", this.onRoomStateEvents);
+ client.removeListener("RoomMember.name", this.onRoomMemberName);
+ client.removeListener("accountData", this.onAccountData);
}
if (this._cancelDeviceList) {
this._cancelDeviceList();
}
},
- getExistingOneToOneRoomId: function() {
- const rooms = MatrixClientPeg.get().getRooms();
- const userIds = [
- this.props.member.userId,
- MatrixClientPeg.get().credentials.userId
- ];
- let existingRoomId = null;
- let invitedRoomId = null;
-
- // roomId can be null here because of a hack in MatrixChat.onUserClick where we
- // abuse this to view users rather than room members.
- let currentMembers;
- if (this.props.member.roomId) {
- const currentRoom = MatrixClientPeg.get().getRoom(this.props.member.roomId);
- currentMembers = currentRoom.getJoinedMembers();
- }
-
- // reuse the first private 1:1 we find
- existingRoomId = null;
-
- for (let i = 0; i < rooms.length; i++) {
- // don't try to reuse public 1:1 rooms
- const join_rules = rooms[i].currentState.getStateEvents("m.room.join_rules", '');
- if (join_rules && join_rules.getContent().join_rule === 'public') continue;
-
- const members = rooms[i].getJoinedMembers();
- if (members.length === 2 &&
- userIds.indexOf(members[0].userId) !== -1 &&
- userIds.indexOf(members[1].userId) !== -1)
- {
- existingRoomId = rooms[i].roomId;
- break;
- }
-
- const invited = rooms[i].getMembersWithMembership('invite');
- if (members.length === 1 &&
- invited.length === 1 &&
- userIds.indexOf(members[0].userId) !== -1 &&
- userIds.indexOf(invited[0].userId) !== -1 &&
- invitedRoomId === null)
- {
- invitedRoomId = rooms[i].roomId;
- // keep looking: we'll use this one if there's nothing better
- }
- }
-
- if (existingRoomId === null) {
- existingRoomId = invitedRoomId;
- }
-
- return existingRoomId;
- },
-
onDeviceVerificationChanged: function(userId, device) {
if (!this._enableDevices) {
return;
@@ -164,6 +128,45 @@ module.exports = React.createClass({
}
},
+ onRoom: function(room) {
+ this.forceUpdate();
+ },
+
+ onDeleteRoom: function(roomId) {
+ this.forceUpdate();
+ },
+
+ onRoomTimeline: function(ev, room, toStartOfTimeline) {
+ if (toStartOfTimeline) return;
+ this.forceUpdate();
+ },
+
+ onRoomName: function(room) {
+ this.forceUpdate();
+ },
+
+ onRoomReceipt: function(receiptEvent, room) {
+ // because if we read a notification, it will affect notification count
+ // only bother updating if there's a receipt from us
+ if (Receipt.findReadReceiptFromUserId(receiptEvent, MatrixClientPeg.get().credentials.userId)) {
+ this.forceUpdate();
+ }
+ },
+
+ onRoomStateEvents: function(ev, state) {
+ this.forceUpdate();
+ },
+
+ onRoomMemberName: function(ev, member) {
+ this.forceUpdate();
+ },
+
+ onAccountData: function(ev) {
+ if (ev.getType() == 'm.direct') {
+ this.forceUpdate();
+ }
+ },
+
_updateStateForNewMember: function(member) {
var newState = this._calculateOpsPermissions(member);
newState.devicesLoading = true;
@@ -416,33 +419,16 @@ module.exports = React.createClass({
}
},
- onChatClick: function() {
- var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
-
- // TODO: keep existingOneToOneRoomId updated if we see any room member changes anywhere
-
- const useExistingOneToOneRoom = this.state.existingOneToOneRoomId && (this.state.existingOneToOneRoomId !== this.props.member.roomId);
-
- // check if there are any existing rooms with just us and them (1:1)
- // If so, just view that room. If not, create a private room with them.
- if (useExistingOneToOneRoom) {
- dis.dispatch({
- action: 'view_room',
- room_id: this.state.existingOneToOneRoomId,
- });
+ onNewDMClick: function() {
+ this.setState({ updating: this.state.updating + 1 });
+ createRoom({
+ createOpts: {
+ invite: [this.props.member.userId],
+ },
+ }).finally(() => {
this.props.onFinished();
- }
- else {
- this.setState({ updating: this.state.updating + 1 });
- createRoom({
- createOpts: {
- invite: [this.props.member.userId],
- },
- }).finally(() => {
- this.props.onFinished();
- this.setState({ updating: this.state.updating - 1 });
- }).done();
- }
+ this.setState({ updating: this.state.updating - 1 });
+ }).done();
},
onLeaveClick: function() {
@@ -583,24 +569,50 @@ module.exports = React.createClass({
render: function() {
var startChat, kickButton, banButton, muteButton, giveModButton, spinner;
if (this.props.member.userId !== MatrixClientPeg.get().credentials.userId) {
- // FIXME: we're referring to a vector component from react-sdk
- var BottomLeftMenuTile = sdk.getComponent('rooms.BottomLeftMenuTile');
+ const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
+ const dmRooms = dmRoomMap.getDMRoomsForUserId(this.props.member.userId);
- var label;
- if (this.state.existingOneToOneRoomId) {
- if (this.state.existingOneToOneRoomId == this.props.member.roomId) {
- label = "Start new direct chat";
- }
- else {
- label = "Go to direct chat";
+ const RoomTile = sdk.getComponent("rooms.RoomTile");
+
+ const tiles = [];
+ for (const roomId of dmRooms) {
+ const room = MatrixClientPeg.get().getRoom(roomId);
+ if (room) {
+ const me = room.getMember(MatrixClientPeg.get().credentials.userId);
+ const highlight = (
+ room.getUnreadNotificationCount('highlight') > 0 ||
+ me.membership == "invite"
+ );
+ tiles.push(
+
+ );
}
}
- else {
- label = "Start direct chat";
- }
- startChat =
+ const labelClasses = classNames({
+ mx_MemberInfo_createRoom_label: true,
+ mx_RoomTile_name: true,
+ });
+ const startNewChat =
+
+

+
+
Start new direct chat
+
+
+ startChat =
+ {tiles}
+ {startNewChat}
+
;
}
if (this.state.updating) {
diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js
index 1f5b303fe0..c326a7c4f5 100644
--- a/src/components/views/rooms/MessageComposerInput.js
+++ b/src/components/views/rooms/MessageComposerInput.js
@@ -31,7 +31,7 @@ import {Editor, EditorState, RichUtils, CompositeDecorator,
convertFromRaw, convertToRaw, Modifier, EditorChangeType,
getDefaultKeyBinding, KeyBindingUtil, ContentState, ContentBlock, SelectionState} from 'draft-js';
-import {stateToMarkdown} from 'draft-js-export-markdown';
+import {stateToMarkdown as __stateToMarkdown} from 'draft-js-export-markdown';
import classNames from 'classnames';
import escape from 'lodash/escape';
@@ -51,6 +51,16 @@ const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000;
const KEY_M = 77;
+const ZWS_CODE = 8203;
+const ZWS = String.fromCharCode(ZWS_CODE); // zero width space
+function stateToMarkdown(state) {
+ return __stateToMarkdown(state)
+ .replace(
+ ZWS, // draft-js-export-markdown adds these
+ ''); // this is *not* a zero width space, trust me :)
+}
+
+
// FIXME Breaks markdown with multiple paragraphs, since it only strips first and last
function mdownToHtml(mdown: string): string {
let html = marked(mdown) || "";
@@ -480,7 +490,7 @@ export default class MessageComposerInput extends React.Component {
});
}
if (cmd.promise) {
- cmd.promise.done(function() {
+ cmd.promise.then(function() {
console.log("Command success.");
}, function(err) {
console.error("Command failure: %s", err);
@@ -520,7 +530,7 @@ export default class MessageComposerInput extends React.Component {
this.sentHistory.push(contentHTML);
let sendMessagePromise = sendFn.call(this.client, this.props.room.roomId, contentText, contentHTML);
- sendMessagePromise.done(() => {
+ sendMessagePromise.then(() => {
dis.dispatch({
action: 'message_sent'
});
diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js
index 497b4f94b4..980fa622eb 100644
--- a/src/components/views/rooms/RoomList.js
+++ b/src/components/views/rooms/RoomList.js
@@ -27,6 +27,7 @@ var sdk = require('../../../index');
var rate_limited_func = require('../../../ratelimitedfunc');
var Rooms = require('../../../Rooms');
var DMRoomMap = require('../../../utils/DMRoomMap');
+var Receipt = require('../../../utils/Receipt');
var HIDE_CONFERENCE_CHANS = true;
@@ -156,13 +157,8 @@ module.exports = React.createClass({
onRoomReceipt: function(receiptEvent, room) {
// because if we read a notification, it will affect notification count
// only bother updating if there's a receipt from us
- var receiptKeys = Object.keys(receiptEvent.getContent());
- for (var i = 0; i < receiptKeys.length; ++i) {
- var rcpt = receiptEvent.getContent()[receiptKeys[i]];
- if (rcpt['m.read'] && rcpt['m.read'][MatrixClientPeg.get().credentials.userId]) {
- this._delayedRefreshRoomList();
- break;
- }
+ if (Receipt.findReadReceiptFromUserId(receiptEvent, MatrixClientPeg.get().credentials.userId)) {
+ this._delayedRefreshRoomList();
}
},
@@ -235,10 +231,6 @@ module.exports = React.createClass({
else if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, me, self.props.ConferenceHandler)) {
// skip past this room & don't put it in any lists
}
- else if (dmRoomMap.getUserIdForRoomId(room.roomId)) {
- // "Direct Message" rooms
- s.lists["im.vector.fake.direct"].push(room);
- }
else if (me.membership == "join" || me.membership === "ban" ||
(me.membership === "leave" && me.events.member.getSender() !== me.events.member.getStateKey()))
{
@@ -252,6 +244,10 @@ module.exports = React.createClass({
s.lists[tagNames[i]].push(room);
}
}
+ else if (dmRoomMap.getUserIdForRoomId(room.roomId)) {
+ // "Direct Message" rooms (that we're still in and that aren't otherwise tagged)
+ s.lists["im.vector.fake.direct"].push(room);
+ }
else {
s.lists["im.vector.fake.recent"].push(room);
}
diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js
index 2128b68488..f25b5140f4 100644
--- a/src/components/views/rooms/RoomTile.js
+++ b/src/components/views/rooms/RoomTile.js
@@ -21,6 +21,7 @@ var ReactDOM = require("react-dom");
var classNames = require('classnames');
var dis = require("../../../dispatcher");
var MatrixClientPeg = require('../../../MatrixClientPeg');
+var DMRoomMap = require('../../../utils/DMRoomMap');
var sdk = require('../../../index');
var ContextualMenu = require('../../structures/ContextualMenu');
var RoomNotifs = require('../../../RoomNotifs');
@@ -29,10 +30,9 @@ module.exports = React.createClass({
displayName: 'RoomTile',
propTypes: {
- // TODO: We should *optionally* support DND stuff and ideally be impl agnostic about it
- connectDragSource: React.PropTypes.func.isRequired,
- connectDropTarget: React.PropTypes.func.isRequired,
- isDragging: React.PropTypes.bool.isRequired,
+ connectDragSource: React.PropTypes.func,
+ connectDropTarget: React.PropTypes.func,
+ isDragging: React.PropTypes.bool,
room: React.PropTypes.object.isRequired,
collapsed: React.PropTypes.bool.isRequired,
@@ -40,11 +40,15 @@ module.exports = React.createClass({
unread: React.PropTypes.bool.isRequired,
highlight: React.PropTypes.bool.isRequired,
isInvite: React.PropTypes.bool.isRequired,
- roomSubList: React.PropTypes.object.isRequired,
- refreshSubList: React.PropTypes.func.isRequired,
incomingCall: React.PropTypes.object,
},
+ getDefaultProps: function() {
+ return {
+ isDragging: false,
+ };
+ },
+
getInitialState: function() {
return({
hover : false,
@@ -64,6 +68,16 @@ module.exports = React.createClass({
return this.state.notifState != RoomNotifs.MUTE;
},
+ _isDirectMessageRoom: function(roomId) {
+ const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
+ var dmRooms = dmRoomMap.getUserIdForRoomId(roomId);
+ if (dmRooms) {
+ return true;
+ } else {
+ return false;
+ }
+ },
+
onAccountData: function(accountDataEvent) {
if (accountDataEvent.getType() == 'm.push_rules') {
this.setState({
@@ -261,18 +275,24 @@ module.exports = React.createClass({
var RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
+ var directMessageIndicator;
+ if (this._isDirectMessageRoom(this.props.room.roomId)) {
+ directMessageIndicator =
;
+ }
+
// These props are injected by React DnD,
// as defined by your `collect` function above:
var isDragging = this.props.isDragging;
var connectDragSource = this.props.connectDragSource;
var connectDropTarget = this.props.connectDropTarget;
- return connectDragSource(connectDropTarget(
+ let ret = (
+ {directMessageIndicator}
@@ -283,6 +303,11 @@ module.exports = React.createClass({
{ incomingCallBox }
{ tooltip }
- ));
+ );
+
+ if (connectDropTarget) ret = connectDropTarget(ret);
+ if (connectDragSource) ret = connectDragSource(ret);
+
+ return ret;
}
});
diff --git a/src/utils/DMRoomMap.js b/src/utils/DMRoomMap.js
index d92ae87e64..3bcc492a9f 100644
--- a/src/utils/DMRoomMap.js
+++ b/src/utils/DMRoomMap.js
@@ -21,26 +21,43 @@ limitations under the License.
*/
export default class DMRoomMap {
constructor(matrixClient) {
+ this.roomToUser = null;
+
const mDirectEvent = matrixClient.getAccountData('m.direct');
if (!mDirectEvent) {
this.userToRooms = {};
- this.roomToUser = {};
} else {
this.userToRooms = mDirectEvent.getContent();
- this.roomToUser = {};
- for (const user of Object.keys(this.userToRooms)) {
- for (const roomId of this.userToRooms[user]) {
- this.roomToUser[roomId] = user;
- }
- }
}
}
getDMRoomsForUserId(userId) {
- return this.userToRooms[userId];
+ // Here, we return the empty list if there are no rooms,
+ // since the number of conversations you have with this user is zero.
+ return this.userToRooms[userId] || [];
}
getUserIdForRoomId(roomId) {
+ if (this.roomToUser == null) {
+ // we lazily populate roomToUser so you can use
+ // this class just to call getDMRoomsForUserId
+ // which doesn't do very much, but is a fairly
+ // convenient wrapper and there's no point
+ // iterating through the map if getUserIdForRoomId()
+ // is never called.
+ this._populateRoomToUser();
+ }
+ // Here, we return undefined if the room is not in the map:
+ // the room ID you gave is not a DM room for any user.
return this.roomToUser[roomId];
}
+
+ _populateRoomToUser() {
+ this.roomToUser = {};
+ for (const user of Object.keys(this.userToRooms)) {
+ for (const roomId of this.userToRooms[user]) {
+ this.roomToUser[roomId] = user;
+ }
+ }
+ }
}
diff --git a/src/utils/Receipt.js b/src/utils/Receipt.js
new file mode 100644
index 0000000000..549a0fd8b3
--- /dev/null
+++ b/src/utils/Receipt.js
@@ -0,0 +1,32 @@
+/*
+Copyright 2016 OpenMarket 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.
+*/
+
+/**
+ * Given MatrixEvent containing receipts, return the first
+ * read receipt from the given user ID, or null if no such
+ * receipt exists.
+ */
+export function findReadReceiptFromUserId(receiptEvent, userId) {
+ var receiptKeys = Object.keys(receiptEvent.getContent());
+ for (var i = 0; i < receiptKeys.length; ++i) {
+ var rcpt = receiptEvent.getContent()[receiptKeys[i]];
+ if (rcpt['m.read'] && rcpt['m.read'][userId]) {
+ return rcpt;
+ }
+ }
+
+ return null;
+}
diff --git a/test/components/views/rooms/MessageComposerInput-test.js b/test/components/views/rooms/MessageComposerInput-test.js
new file mode 100644
index 0000000000..89f838ba87
--- /dev/null
+++ b/test/components/views/rooms/MessageComposerInput-test.js
@@ -0,0 +1,144 @@
+import React from 'react';
+import ReactTestUtils from 'react-addons-test-utils';
+import ReactDOM from 'react-dom';
+import expect, {createSpy} from 'expect';
+import sinon from 'sinon';
+import Q from 'q';
+import * as testUtils from '../../../test-utils';
+import sdk from 'matrix-react-sdk';
+import UserSettingsStore from '../../../../src/UserSettingsStore';
+const MessageComposerInput = sdk.getComponent('views.rooms.MessageComposerInput');
+import MatrixClientPeg from 'MatrixClientPeg';
+
+function addTextToDraft(text) {
+ const components = document.getElementsByClassName('public-DraftEditor-content');
+ if (components && components.length) {
+ const textarea = components[0];
+ const textEvent = document.createEvent('TextEvent');
+ textEvent.initTextEvent('textInput', true, true, null, text);
+ textarea.dispatchEvent(textEvent);
+ }
+}
+
+describe('MessageComposerInput', () => {
+ let parentDiv = null,
+ sandbox = null,
+ client = null,
+ mci = null,
+ room = testUtils.mkStubRoom('!DdJkzRliezrwpNebLk:matrix.org');
+
+ // TODO Remove when RTE is out of labs.
+
+ beforeEach(() => {
+ sandbox = testUtils.stubClient(sandbox);
+ client = MatrixClientPeg.get();
+ UserSettingsStore.isFeatureEnabled = sinon.stub()
+ .withArgs('rich_text_editor').returns(true);
+
+ parentDiv = document.createElement('div');
+ document.body.appendChild(parentDiv);
+ mci = ReactDOM.render(
+
,
+ parentDiv);
+ });
+
+ afterEach(() => {
+ if (parentDiv) {
+ ReactDOM.unmountComponentAtNode(parentDiv);
+ parentDiv.remove();
+ parentDiv = null;
+ }
+ sandbox.restore();
+ });
+
+ it('should change mode if indicator is clicked', () => {
+ mci.enableRichtext(true);
+
+ setTimeout(() => {
+ const indicator = ReactTestUtils.findRenderedDOMComponentWithClass(
+ mci,
+ 'mx_MessageComposer_input_markdownIndicator');
+ ReactTestUtils.Simulate.click(indicator);
+
+ expect(mci.state.isRichtextEnabled).toEqual(false, 'should have changed mode');
+ });
+ });
+
+ it('should not send messages when composer is empty', () => {
+ const spy = sinon.spy(client, 'sendHtmlMessage');
+ mci.enableRichtext(true);
+ mci.handleReturn(sinon.stub());
+
+ expect(spy.calledOnce).toEqual(false, 'should not send message');
+ });
+
+ it('should not change content unnecessarily on RTE -> Markdown conversion', () => {
+ const spy = sinon.spy(client, 'sendHtmlMessage');
+ mci.enableRichtext(true);
+ addTextToDraft('a');
+ mci.handleKeyCommand('toggle-mode');
+ mci.handleReturn(sinon.stub());
+
+ expect(spy.calledOnce).toEqual(true);
+ expect(spy.args[0][1]).toEqual('a');
+ });
+
+ it('should not change content unnecessarily on Markdown -> RTE conversion', () => {
+ const spy = sinon.spy(client, 'sendHtmlMessage');
+ mci.enableRichtext(false);
+ addTextToDraft('a');
+ mci.handleKeyCommand('toggle-mode');
+ mci.handleReturn(sinon.stub());
+ expect(spy.calledOnce).toEqual(true);
+ expect(spy.args[0][1]).toEqual('a');
+ });
+
+ it('should send emoji messages in rich text', () => {
+ const spy = sinon.spy(client, 'sendHtmlMessage');
+ mci.enableRichtext(true);
+ addTextToDraft('☹');
+ mci.handleReturn(sinon.stub());
+
+ expect(spy.calledOnce).toEqual(true, 'should send message');
+ });
+
+ it('should send emoji messages in Markdown', () => {
+ const spy = sinon.spy(client, 'sendHtmlMessage');
+ mci.enableRichtext(false);
+ addTextToDraft('☹');
+ mci.handleReturn(sinon.stub());
+
+ expect(spy.calledOnce).toEqual(true, 'should send message');
+ });
+
+ it('should convert basic Markdown to rich text correctly', () => {
+ const spy = sinon.spy(client, 'sendHtmlMessage');
+ mci.enableRichtext(false);
+ addTextToDraft('*abc*');
+ mci.handleKeyCommand('toggle-mode');
+ mci.handleReturn(sinon.stub());
+ expect(spy.args[0][2]).toContain('
abc');
+ });
+
+ it('should convert basic rich text to Markdown correctly', () => {
+ const spy = sinon.spy(client, 'sendHtmlMessage');
+ mci.enableRichtext(true);
+ mci.handleKeyCommand('italic');
+ addTextToDraft('abc');
+ mci.handleKeyCommand('toggle-mode');
+ mci.handleReturn(sinon.stub());
+ expect(['_abc_', '*abc*']).toContain(spy.args[0][1]);
+ });
+
+ it('should insert formatting characters in Markdown mode', () => {
+ const spy = sinon.spy(client, 'sendHtmlMessage');
+ mci.enableRichtext(false);
+ mci.handleKeyCommand('italic');
+ mci.handleReturn(sinon.stub());
+ expect(['__', '**']).toContain(spy.args[0][1]);
+ });
+
+});
diff --git a/test/test-utils.js b/test/test-utils.js
index 799f04ce54..78349b7824 100644
--- a/test/test-utils.js
+++ b/test/test-utils.js
@@ -12,7 +12,7 @@ var MatrixEvent = jssdk.MatrixEvent;
* name to stdout.
* @param {Mocha.Context} context The test context
*/
-module.exports.beforeEach = function(context) {
+export function beforeEach(context) {
var desc = context.currentTest.fullTitle();
console.log();
console.log(desc);
@@ -26,7 +26,7 @@ module.exports.beforeEach = function(context) {
*
* @returns {sinon.Sandbox}; remember to call sandbox.restore afterwards.
*/
-module.exports.stubClient = function() {
+export function stubClient() {
var sandbox = sinon.sandbox.create();
var client = {
@@ -44,6 +44,16 @@ module.exports.stubClient = function() {
sendReadReceipt: sinon.stub().returns(q()),
getRoomIdForAlias: sinon.stub().returns(q()),
getProfileInfo: sinon.stub().returns(q({})),
+ getAccountData: (type) => {
+ return mkEvent({
+ type,
+ event: true,
+ content: {},
+ });
+ },
+ setAccountData: sinon.stub(),
+ sendTyping: sinon.stub().returns(q({})),
+ sendHtmlMessage: () => q({}),
};
// stub out the methods in MatrixClientPeg
@@ -73,7 +83,7 @@ module.exports.stubClient = function() {
* @param {boolean} opts.event True to make a MatrixEvent.
* @return {Object} a JSON object representing this event.
*/
-module.exports.mkEvent = function(opts) {
+export function mkEvent(opts) {
if (!opts.type || !opts.content) {
throw new Error("Missing .type or .content =>" + JSON.stringify(opts));
}
@@ -101,7 +111,7 @@ module.exports.mkEvent = function(opts) {
* @param {Object} opts Values for the presence.
* @return {Object|MatrixEvent} The event
*/
-module.exports.mkPresence = function(opts) {
+export function mkPresence(opts) {
if (!opts.user) {
throw new Error("Missing user");
}
@@ -132,7 +142,7 @@ module.exports.mkPresence = function(opts) {
* @param {boolean} opts.event True to make a MatrixEvent.
* @return {Object|MatrixEvent} The event
*/
-module.exports.mkMembership = function(opts) {
+export function mkMembership(opts) {
opts.type = "m.room.member";
if (!opts.skey) {
opts.skey = opts.user;
@@ -145,7 +155,7 @@ module.exports.mkMembership = function(opts) {
};
if (opts.name) { opts.content.displayname = opts.name; }
if (opts.url) { opts.content.avatar_url = opts.url; }
- return module.exports.mkEvent(opts);
+ return mkEvent(opts);
};
/**
@@ -157,7 +167,7 @@ module.exports.mkMembership = function(opts) {
* @param {boolean} opts.event True to make a MatrixEvent.
* @return {Object|MatrixEvent} The event
*/
-module.exports.mkMessage = function(opts) {
+export function mkMessage(opts) {
opts.type = "m.room.message";
if (!opts.msg) {
opts.msg = "Random->" + Math.random();
@@ -169,11 +179,12 @@ module.exports.mkMessage = function(opts) {
msgtype: "m.text",
body: opts.msg
};
- return module.exports.mkEvent(opts);
-};
+ return mkEvent(opts);
+}
-module.exports.mkStubRoom = function() {
+export function mkStubRoom(roomId = null) {
return {
+ roomId,
getReceiptsForEvent: sinon.stub().returns([]),
getMember: sinon.stub().returns({}),
getJoinedMembers: sinon.stub().returns([]),
@@ -182,4 +193,4 @@ module.exports.mkStubRoom = function() {
members: [],
},
};
-};
+}