Merge branch 'develop' into matthew/notif-panel

pull/21833/head
Matthew Hodgson 2016-09-11 02:38:16 +01:00
commit 83209197f4
13 changed files with 461 additions and 187 deletions

View File

@ -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 = {

View File

@ -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;

View File

@ -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 (
<CasLogin />
<CasLogin onSubmit={this.onCasLogin} />
);
default:
if (!step) {

View File

@ -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 (
<div>
<button onClick={this.onCasClicked}>Sign in with CAS</button>
<button onClick={this.props.onSubmit}>Sign in with CAS</button>
</div>
);
}

View File

@ -99,12 +99,16 @@ export default class MemberDeviceInfo extends React.Component {
);
}
var deviceName = this.props.device.display_name || this.props.device.deviceId;
var deviceName = this.props.device.getDisplayName() || this.props.device.deviceId;
// add the deviceId as a titletext to help with debugging
return (
<div className="mx_MemberDeviceInfo">
<div className="mx_MemberDeviceInfo" title={this.props.device.deviceId}>
<div className="mx_MemberDeviceInfo_deviceId">{deviceName}</div>
{indicator}
<div className="mx_MemberDeviceInfo_deviceKey">
{this.props.device.getFingerprint()}
</div>
{verifyButton}
{blockButton}
</div>

View File

@ -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(
<RoomTile key={room.roomId} room={room}
collapsed={false}
selected={false}
unread={Unread.doesRoomHaveUnreadMessages(room)}
highlight={highlight}
isInvite={me.membership == "invite"}
/>
);
}
}
else {
label = "Start direct chat";
}
startChat = <BottomLeftMenuTile collapsed={ false } img="img/create-big.svg"
label={ label } onClick={ this.onChatClick }/>
const labelClasses = classNames({
mx_MemberInfo_createRoom_label: true,
mx_RoomTile_name: true,
});
const startNewChat = <div
className="mx_MemberInfo_createRoom"
onClick={this.onNewDMClick}
>
<div className="mx_RoomTile_avatar">
<img src="img/create-big.svg" width="26" height="26" />
</div>
<div className={labelClasses}><i>Start new direct chat</i></div>
</div>
startChat = <div>
{tiles}
{startNewChat}
</div>;
}
if (this.state.updating) {

View File

@ -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 <p>
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'
});

View File

@ -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);
}

View File

@ -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 = <img src="img/icon_person.svg" className="mx_RoomTile_dm" width="11" height="13" alt="dm"/>;
}
// 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 = (
<div className={classes} onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
<div className={avatarClasses}>
<div className="mx_RoomTile_avatar_menu" onClick={this.onAvatarClicked}>
<div className={avatarContainerClasses}>
<RoomAvatar room={this.props.room} width={24} height={24} />
{directMessageIndicator}
</div>
</div>
</div>
@ -283,6 +303,11 @@ module.exports = React.createClass({
{ incomingCallBox }
{ tooltip }
</div>
));
);
if (connectDropTarget) ret = connectDropTarget(ret);
if (connectDragSource) ret = connectDragSource(ret);
return ret;
}
});

View File

@ -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;
}
}
}
}

32
src/utils/Receipt.js Normal file
View File

@ -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;
}

View File

@ -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(
<MessageComposerInput
room={room}
client={client}
/>,
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('<em>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]);
});
});

View File

@ -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: [],
},
};
};
}