Merge branch 'develop' of https://github.com/matrix-org/matrix-react-sdk into rxl881/appFixes

This commit is contained in:
Richard Lewis 2017-08-17 17:47:46 +01:00
commit 0907fff080
48 changed files with 5355 additions and 988 deletions

View file

@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 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.
@ -15,40 +16,37 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import sdk from '../../../index';
import { getAddressType, inviteMultipleToRoom } from '../../../Invite';
import createRoom from '../../../createRoom';
import MatrixClientPeg from '../../../MatrixClientPeg';
import DMRoomMap from '../../../utils/DMRoomMap';
import Modal from '../../../Modal';
import AccessibleButton from '../elements/AccessibleButton';
import Promise from 'bluebird';
import dis from '../../../dispatcher';
import { addressTypes, getAddressType } from '../../../UserAddress.js';
const TRUNCATE_QUERY_LIST = 40;
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
module.exports = React.createClass({
displayName: "ChatInviteDialog",
displayName: "UserPickerDialog",
propTypes: {
title: React.PropTypes.string.isRequired,
description: React.PropTypes.oneOfType([
React.PropTypes.element,
React.PropTypes.string,
]),
value: React.PropTypes.string,
placeholder: React.PropTypes.string,
roomId: React.PropTypes.string,
button: React.PropTypes.string,
focus: React.PropTypes.bool,
onFinished: React.PropTypes.func.isRequired,
title: PropTypes.string.isRequired,
description: PropTypes.node,
value: PropTypes.string,
placeholder: PropTypes.string,
roomId: PropTypes.string,
button: PropTypes.string,
focus: PropTypes.bool,
validAddressTypes: PropTypes.arrayOf(PropTypes.oneOfType(addressTypes)),
onFinished: PropTypes.func.isRequired,
},
getDefaultProps: function() {
return {
value: "",
focus: true,
validAddressTypes: addressTypes,
};
},
@ -56,9 +54,9 @@ module.exports = React.createClass({
return {
error: false,
// List of AddressTile.InviteAddressType objects representing
// List of UserAddressType objects representing
// the list of addresses we're going to invite
inviteList: [],
userList: [],
// Whether a search is ongoing
busy: false,
@ -68,7 +66,7 @@ module.exports = React.createClass({
serverSupportsUserDirectory: true,
// The query being searched for
query: "",
// List of AddressTile.InviteAddressType objects representing
// List of UserAddressType objects representing
// the set of auto-completion results for the current search
// query.
queryList: [],
@ -83,57 +81,14 @@ module.exports = React.createClass({
},
onButtonClick: function() {
let inviteList = this.state.inviteList.slice();
let userList = this.state.userList.slice();
// Check the text input field to see if user has an unconverted address
// If there is and it's valid add it to the local inviteList
// If there is and it's valid add it to the local userList
if (this.refs.textinput.value !== '') {
inviteList = this._addInputToList();
if (inviteList === null) return;
}
const addrTexts = inviteList.map(addr => addr.address);
if (inviteList.length > 0) {
if (this._isDmChat(addrTexts)) {
const userId = inviteList[0].address;
// Direct Message chat
const rooms = this._getDirectMessageRooms(userId);
if (rooms.length > 0) {
// A Direct Message room already exists for this user, so select a
// room from a list that is similar to the one in MemberInfo panel
const ChatCreateOrReuseDialog = sdk.getComponent(
"views.dialogs.ChatCreateOrReuseDialog",
);
const close = Modal.createTrackedDialog('Create or Reuse', '', ChatCreateOrReuseDialog, {
userId: userId,
onFinished: (success) => {
this.props.onFinished(success);
},
onNewDMClick: () => {
dis.dispatch({
action: 'start_chat',
user_id: userId,
});
close(true);
},
onExistingRoomSelected: (roomId) => {
dis.dispatch({
action: 'view_room',
room_id: roomId,
});
close(true);
},
}).close;
} else {
this._startChat(inviteList);
}
} else {
// Multi invite chat
this._startChat(inviteList);
}
} else {
// No addresses supplied
this.setState({ error: true });
userList = this._addInputToList();
if (userList === null) return;
}
this.props.onFinished(true, userList);
},
onCancel: function() {
@ -157,10 +112,10 @@ module.exports = React.createClass({
e.stopPropagation();
e.preventDefault();
if (this.addressSelector) this.addressSelector.chooseSelection();
} else if (this.refs.textinput.value.length === 0 && this.state.inviteList.length && e.keyCode === 8) { // backspace
} else if (this.refs.textinput.value.length === 0 && this.state.userList.length && e.keyCode === 8) { // backspace
e.stopPropagation();
e.preventDefault();
this.onDismissed(this.state.inviteList.length - 1)();
this.onDismissed(this.state.userList.length - 1)();
} else if (e.keyCode === 13) { // enter
e.stopPropagation();
e.preventDefault();
@ -201,12 +156,11 @@ module.exports = React.createClass({
},
onDismissed: function(index) {
var self = this;
return () => {
var inviteList = self.state.inviteList.slice();
inviteList.splice(index, 1);
self.setState({
inviteList: inviteList,
const userList = this.state.userList.slice();
userList.splice(index, 1);
this.setState({
userList: userList,
queryList: [],
query: "",
});
@ -215,17 +169,16 @@ module.exports = React.createClass({
},
onClick: function(index) {
var self = this;
return function() {
self.onSelected(index);
return () => {
this.onSelected(index);
};
},
onSelected: function(index) {
var inviteList = this.state.inviteList.slice();
inviteList.push(this.state.queryList[index]);
const userList = this.state.userList.slice();
userList.push(this.state.queryList[index]);
this.setState({
inviteList: inviteList,
userList: userList,
queryList: [],
query: "",
});
@ -297,7 +250,7 @@ module.exports = React.createClass({
return;
}
// Return objects, structure of which is defined
// by InviteAddressType
// by UserAddressType
queryList.push({
addressType: 'mx',
address: user.user_id,
@ -311,7 +264,7 @@ module.exports = React.createClass({
// This is important, otherwise there's no way to invite
// a perfectly valid address if there are close matches.
const addrType = getAddressType(query);
if (addrType !== null) {
if (this.props.validAddressTypes.includes(addrType)) {
queryList.unshift({
addressType: addrType,
address: query,
@ -330,132 +283,6 @@ module.exports = React.createClass({
});
},
_getDirectMessageRooms: function(addr) {
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
const dmRooms = dmRoomMap.getDMRoomsForUserId(addr);
const rooms = [];
dmRooms.forEach(dmRoom => {
let room = MatrixClientPeg.get().getRoom(dmRoom);
if (room) {
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
if (me.membership == 'join') {
rooms.push(room);
}
}
});
return rooms;
},
_startChat: function(addrs) {
if (MatrixClientPeg.get().isGuest()) {
dis.dispatch({action: 'view_set_mxid'});
return;
}
const addrTexts = addrs.map((addr) => {
return addr.address;
});
if (this.props.roomId) {
// Invite new user to a room
var self = this;
inviteMultipleToRoom(this.props.roomId, addrTexts)
.then(function(addrs) {
var room = MatrixClientPeg.get().getRoom(self.props.roomId);
return self._showAnyInviteErrors(addrs, room);
})
.catch(function(err) {
console.error(err.stack);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, {
title: _t("Failed to invite"),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
return null;
})
.done();
} else if (this._isDmChat(addrTexts)) {
// Start the DM chat
createRoom({dmUserId: addrTexts[0]})
.catch(function(err) {
console.error(err.stack);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to invite user', '', ErrorDialog, {
title: _t("Failed to invite user"),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
return null;
})
.done();
} else {
// Start multi user chat
var self = this;
var room;
createRoom().then(function(roomId) {
room = MatrixClientPeg.get().getRoom(roomId);
return inviteMultipleToRoom(roomId, addrTexts);
})
.then(function(addrs) {
return self._showAnyInviteErrors(addrs, room);
})
.catch(function(err) {
console.error(err.stack);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, {
title: _t("Failed to invite"),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
return null;
})
.done();
}
// Close - this will happen before the above, as that is async
this.props.onFinished(true, addrTexts);
},
_isOnInviteList: function(uid) {
for (let i = 0; i < this.state.inviteList.length; i++) {
if (
this.state.inviteList[i].addressType == 'mx' &&
this.state.inviteList[i].address.toLowerCase() === uid
) {
return true;
}
}
return false;
},
_isDmChat: function(addrTexts) {
if (addrTexts.length === 1 &&
getAddressType(addrTexts[0]) === "mx" &&
!this.props.roomId
) {
return true;
} else {
return false;
}
},
_showAnyInviteErrors: function(addrs, room) {
// Show user any errors
var errorList = [];
for (var addr in addrs) {
if (addrs.hasOwnProperty(addr) && addrs[addr] === "error") {
errorList.push(addr);
}
}
if (errorList.length > 0) {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to invite the following users to the room', '', ErrorDialog, {
title: _t("Failed to invite the following users to the %(roomName)s room:", {roomName: room.name}),
description: errorList.join(", "),
});
}
return addrs;
},
_addInputToList: function() {
const addressText = this.refs.textinput.value.trim();
const addrType = getAddressType(addressText);
@ -476,15 +303,15 @@ module.exports = React.createClass({
}
}
const inviteList = this.state.inviteList.slice();
inviteList.push(addrObj);
const userList = this.state.userList.slice();
userList.push(addrObj);
this.setState({
inviteList: inviteList,
userList: userList,
queryList: [],
query: "",
});
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
return inviteList;
return userList;
},
_lookupThreepid: function(medium, address) {
@ -495,7 +322,7 @@ module.exports = React.createClass({
// not like they leak.
this._cancelThreepidLookup = function() {
cancelled = true;
}
};
// wait a bit to let the user finish typing
return Promise.delay(500).then(() => {
@ -511,7 +338,7 @@ module.exports = React.createClass({
if (cancelled) return null;
this.setState({
queryList: [{
// an InviteAddressType
// a UserAddressType
addressType: medium,
address: address,
displayName: res.displayname,
@ -527,20 +354,20 @@ module.exports = React.createClass({
const AddressSelector = sdk.getComponent("elements.AddressSelector");
this.scrollElement = null;
var query = [];
const query = [];
// create the invite list
if (this.state.inviteList.length > 0) {
var AddressTile = sdk.getComponent("elements.AddressTile");
for (let i = 0; i < this.state.inviteList.length; i++) {
if (this.state.userList.length > 0) {
const AddressTile = sdk.getComponent("elements.AddressTile");
for (let i = 0; i < this.state.userList.length; i++) {
query.push(
<AddressTile key={i} address={this.state.inviteList[i]} canDismiss={true} onDismissed={ this.onDismissed(i) } />
<AddressTile key={i} address={this.state.userList[i]} canDismiss={true} onDismissed={ this.onDismissed(i) } />,
);
}
}
// Add the query at the end
query.push(
<textarea key={this.state.inviteList.length}
<textarea key={this.state.userList.length}
rows="1"
id="textinput"
ref="textinput"
@ -555,7 +382,9 @@ module.exports = React.createClass({
let error;
let addressSelector;
if (this.state.error) {
error = <div className="mx_ChatInviteDialog_error">{_t("You have entered an invalid contact. Try using their Matrix ID or email address.")}</div>;
error = <div className="mx_ChatInviteDialog_error">
{_t("You have entered an invalid contact. Try using their Matrix ID or email address.")}
</div>;
} else if (this.state.searchError) {
error = <div className="mx_ChatInviteDialog_error">{this.state.searchError}</div>;
} else if (
@ -598,5 +427,5 @@ module.exports = React.createClass({
</div>
</div>
);
}
},
});

View file

@ -20,7 +20,7 @@ limitations under the License.
import React from 'react';
import sdk from '../../../index';
import classNames from 'classnames';
import { InviteAddressType } from './AddressTile';
import { UserAddressType } from '../../../UserAddress';
export default React.createClass({
displayName: 'AddressSelector',
@ -29,7 +29,7 @@ export default React.createClass({
onSelected: React.PropTypes.func.isRequired,
// List of the addresses to display
addressList: React.PropTypes.arrayOf(InviteAddressType).isRequired,
addressList: React.PropTypes.arrayOf(UserAddressType).isRequired,
truncateAt: React.PropTypes.number.isRequired,
selected: React.PropTypes.number,

View file

@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 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.
@ -14,38 +15,19 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
import React from 'react';
import classNames from 'classnames';
import sdk from "../../../index";
import MatrixClientPeg from "../../../MatrixClientPeg";
import { _t } from '../../../languageHandler';
// React PropType definition for an object describing
// an address that can be invited to a room (which
// could be a third party identifier or a matrix ID)
// along with some additional information about the
// address / target.
export const InviteAddressType = React.PropTypes.shape({
addressType: React.PropTypes.oneOf([
'mx', 'email'
]).isRequired,
address: React.PropTypes.string.isRequired,
displayName: React.PropTypes.string,
avatarMxc: React.PropTypes.string,
// true if the address is known to be a valid address (eg. is a real
// user we've seen) or false otherwise (eg. is just an address the
// user has entered)
isKnown: React.PropTypes.bool,
});
import { UserAddressType } from '../../../UserAddress.js';
export default React.createClass({
displayName: 'AddressTile',
propTypes: {
address: InviteAddressType.isRequired,
address: UserAddressType.isRequired,
canDismiss: React.PropTypes.bool,
onDismissed: React.PropTypes.func,
justified: React.PropTypes.bool,

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import React from 'react';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import classNames from 'classnames';
import { Room, RoomMember } from 'matrix-js-sdk';
import PropTypes from 'prop-types';
@ -140,6 +141,12 @@ const Pill = React.createClass({
});
},
onUserPillClicked: function() {
dis.dispatch({
action: 'view_user',
member: this.state.member,
});
},
render: function() {
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
const RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
@ -150,6 +157,8 @@ const Pill = React.createClass({
let linkText = resource;
let pillClass;
let userId;
let href = this.props.url;
let onClick;
switch (this.state.pillType) {
case Pill.TYPE_USER_MENTION: {
// If this user is not a member of this room, default to the empty member
@ -161,6 +170,8 @@ const Pill = React.createClass({
avatar = <MemberAvatar member={member} width={16} height={16}/>;
}
pillClass = 'mx_UserPill';
href = null;
onClick = this.onUserPillClicked.bind(this);
}
}
break;
@ -183,7 +194,7 @@ const Pill = React.createClass({
if (this.state.pillType) {
return this.props.inMessage ?
<a className={classes} href={this.props.url} title={resource} data-offset-key={this.props.offsetKey}>
<a className={classes} href={href} onClick={onClick} title={resource} data-offset-key={this.props.offsetKey}>
{avatar}
{linkText}
</a> :

View file

@ -17,7 +17,7 @@ limitations under the License.
'use strict';
import React from 'react';
import { _t } from '../../../languageHandler';
import { _t, _tJsx } from '../../../languageHandler';
var DIV_ID = 'mx_recaptcha';
@ -66,7 +66,11 @@ module.exports = React.createClass({
// * jumping straight to a hosted captcha page (but we don't support that yet)
// * embedding the captcha in an iframe (if that works)
// * using a better captcha lib
warning.innerHTML = "Robot check is currently unavailable on desktop - please use a <a href='https://riot.im/app'>web browser</a>.";
warning.innerHTML = _tJsx(
"Robot check is currently unavailable on desktop - please use a <a>web browser</a>",
/<a>(.*?)<\/a>/,
(sub) => { return "<a href='https://riot.im/app'>{ sub }</a>"; }
);
this.refs.recaptchaContainer.appendChild(warning);
}
else {

View file

@ -808,15 +808,10 @@ export default class MessageComposerInput extends React.Component {
let sendHtmlFn = this.client.sendHtmlMessage;
let sendTextFn = this.client.sendTextMessage;
if (this.state.isRichtextEnabled) {
this.historyManager.addItem(
contentHTML ? contentHTML : contentText,
contentHTML ? 'html' : 'markdown',
);
} else {
// Always store MD input as input history
this.historyManager.addItem(contentText, 'markdown');
}
this.historyManager.save(
contentState,
this.state.isRichtextEnabled ? 'html' : 'markdown',
);
if (contentText.startsWith('/me')) {
contentText = contentText.substring(4);
@ -890,6 +885,7 @@ export default class MessageComposerInput extends React.Component {
}
} else {
this.moveAutocompleteSelection(up);
e.preventDefault();
}
};

View file

@ -71,7 +71,7 @@ export default class DevicesPanelEntry extends React.Component {
// pop up an interactive auth dialog
var InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
Modal.createTrackedDialog('Delete Device Dialog', InteractiveAuthDialog, {
Modal.createTrackedDialog('Delete Device Dialog', '', InteractiveAuthDialog, {
title: _t("Authentication"),
matrixClient: MatrixClientPeg.get(),
authData: error.data,