Merge branch 'develop' into feature-autocomplete

This commit is contained in:
Aviral Dasgupta 2016-06-12 14:10:23 +05:30
commit 0df201c483
24 changed files with 590 additions and 240 deletions

View file

@ -1,3 +1,43 @@
Changes in [0.6.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.6.3) (2016-06-03)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.6.2...v0.6.3)
* Change invite text field wording
* Fix bug with new email invite UX where the invite could get wedged
* Label app versions sensibly in UserSettings
Changes in [0.6.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.6.2) (2016-06-02)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.6.1...v0.6.2)
* Correctly bump dep on matrix-js-sdk 0.5.4
Changes in [0.6.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.6.1) (2016-06-02)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.6.0...v0.6.1)
* Fix focusing race in new UX for 3pid invites
* Fix jenkins.sh
Changes in [0.6.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.6.0) (2016-06-02)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.5.2...v0.6.0)
* implement new UX for 3pid invites
[\#297](https://github.com/matrix-org/matrix-react-sdk/pull/297)
* multiple URL preview support
[\#290](https://github.com/matrix-org/matrix-react-sdk/pull/290)
* Add a fallback home server to log into
[\#293](https://github.com/matrix-org/matrix-react-sdk/pull/293)
* Hopefully fix memory leak with velocity
[\#291](https://github.com/matrix-org/matrix-react-sdk/pull/291)
* Support for enabling email notifications
[\#289](https://github.com/matrix-org/matrix-react-sdk/pull/289)
* Correct Readme instructions how to customize the UI
[\#286](https://github.com/matrix-org/matrix-react-sdk/pull/286)
* Avoid rerendering during Room unmount
[\#285](https://github.com/matrix-org/matrix-react-sdk/pull/285)
Changes in [0.5.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.5.2) (2016-04-22)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.5.1...v0.5.2)

View file

@ -8,9 +8,6 @@ nvm use 4
set -x
# install the version of js-sdk provided to us by jenkins
npm install ./node_modules/matrix-js-sdk-*.tgz
# install the other dependencies
npm install

View file

@ -1,6 +1,6 @@
{
"name": "matrix-react-sdk",
"version": "0.5.2",
"version": "0.6.3",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"repository": {
@ -31,15 +31,14 @@
"highlight.js": "^8.9.1",
"linkifyjs": "^2.0.0-beta.4",
"marked": "^0.3.5",
"matrix-js-sdk": "^0.5.2",
"matrix-js-sdk": "matrix-org/matrix-js-sdk#develop",
"optimist": "^0.6.1",
"q": "^1.4.1",
"react": "^15.0.1",
"react-dom": "^15.0.1",
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#c3d942e",
"sanitize-html": "^1.11.1",
"velocity-animate": "^1.2.3",
"velocity-ui-pack": "^1.2.2"
"velocity-vector": "vector-im/velocity#059e3b2"
},
"//babelversion": [
"brief experiments with babel6 seems to show that it generates source ",

View file

@ -100,7 +100,7 @@ module.exports = {
return this.getEmailPusher(pushers, address) !== undefined;
},
addEmailPusher: function(address) {
addEmailPusher: function(address, data) {
return MatrixClientPeg.get().setPusher({
kind: 'email',
app_id: "m.email",
@ -108,7 +108,7 @@ module.exports = {
app_display_name: 'Email Notifications',
device_display_name: address,
lang: navigator.language,
data: {},
data: data,
append: true, // We always append for email pushers since we don't want to stop other accounts notifying to the same email address
});
},

View file

@ -1,6 +1,6 @@
var React = require('react');
var ReactDom = require('react-dom');
var Velocity = require('velocity-animate');
var Velocity = require('velocity-vector');
/**
* The Velociraptor contains components and animates transitions with velocity.
@ -117,7 +117,8 @@ module.exports = React.createClass({
// and the FAQ entry, "Preventing memory leaks when
// creating/destroying large numbers of elements"
// (https://github.com/julianshapiro/velocity/issues/47)
Velocity.Utilities.removeData(this.nodes[k]);
var domNode = ReactDom.findDOMNode(this.nodes[k]);
Velocity.Utilities.removeData(domNode);
}
this.nodes[k] = node;
},

View file

@ -1,4 +1,4 @@
var Velocity = require('velocity-animate');
var Velocity = require('velocity-vector');
// courtesy of https://github.com/julianshapiro/velocity/issues/283
// We only use easeOutBounce (easeInBounce is just sort of nonsensical)

View file

@ -79,6 +79,7 @@ module.exports.components['views.rooms.EntityTile'] = require('./components/view
module.exports.components['views.rooms.EventTile'] = require('./components/views/rooms/EventTile');
module.exports.components['views.rooms.InviteMemberList'] = require('./components/views/rooms/InviteMemberList');
module.exports.components['views.rooms.LinkPreviewWidget'] = require('./components/views/rooms/LinkPreviewWidget');
module.exports.components['views.rooms.MemberDeviceInfo'] = require('./components/views/rooms/MemberDeviceInfo');
module.exports.components['views.rooms.MemberInfo'] = require('./components/views/rooms/MemberInfo');
module.exports.components['views.rooms.MemberList'] = require('./components/views/rooms/MemberList');
module.exports.components['views.rooms.MemberTile'] = require('./components/views/rooms/MemberTile');

View file

@ -37,11 +37,13 @@ var MatrixTools = require('../../MatrixTools');
var linkifyMatrix = require("../../linkify-matrix");
var KeyCode = require('../../KeyCode');
var createRoom = require("../../createRoom");
module.exports = React.createClass({
displayName: 'MatrixChat',
propTypes: {
config: React.PropTypes.object.isRequired,
config: React.PropTypes.object,
ConferenceHandler: React.PropTypes.any,
onNewScreen: React.PropTypes.func,
registrationUrl: React.PropTypes.string,
@ -84,7 +86,8 @@ module.exports = React.createClass({
getDefaultProps: function() {
return {
startingQueryParams: {}
startingQueryParams: {},
config: {},
};
},
@ -97,10 +100,9 @@ module.exports = React.createClass({
else if (window.localStorage && window.localStorage.getItem("mx_hs_url")) {
return window.localStorage.getItem("mx_hs_url");
}
else if (this.props.config) {
return this.props.config.default_hs_url
else {
return this.props.config.default_hs_url || "https://matrix.org";
}
return "https://matrix.org";
},
getFallbackHsUrl: function() {
@ -116,10 +118,9 @@ module.exports = React.createClass({
else if (window.localStorage && window.localStorage.getItem("mx_is_url")) {
return window.localStorage.getItem("mx_is_url");
}
else if (this.props.config) {
return this.props.config.default_is_url
else {
return this.props.config.default_is_url || "https://vector.im"
}
return "https://matrix.org";
},
componentWillMount: function() {
@ -391,6 +392,10 @@ module.exports = React.createClass({
});
break;
case 'view_room':
// Takes both room ID and room alias: if switching to a room the client is already
// know to be in (eg. user clicks on a room in the recents panel), supply only the
// ID. If the user is clicking on a room in the context of the alias being presented
// to them, supply the room alias and optionally the room ID.
this._viewRoom(
payload.room_id, payload.room_alias, payload.show_settings, payload.event_id,
payload.third_party_invite, payload.oob_data
@ -422,42 +427,6 @@ module.exports = React.createClass({
this._viewRoom(allRooms[roomIndex].roomId);
}
break;
case 'view_room_alias':
if (!this.state.logged_in) {
this.starting_room_alias_payload = payload;
// Login is the default screen, so we'd do this anyway,
// but this will set the URL bar appropriately.
dis.dispatch({ action: 'start_login' });
return;
}
var foundRoom = MatrixTools.getRoomForAlias(
MatrixClientPeg.get().getRooms(), payload.room_alias
);
if (foundRoom) {
dis.dispatch({
action: 'view_room',
room_id: foundRoom.roomId,
room_alias: payload.room_alias,
event_id: payload.event_id,
third_party_invite: payload.third_party_invite,
oob_data: payload.oob_data,
});
return;
}
// resolve the alias and *then* view it
MatrixClientPeg.get().getRoomIdForAlias(payload.room_alias).done(
function(result) {
dis.dispatch({
action: 'view_room',
room_id: result.room_id,
room_alias: payload.room_alias,
event_id: payload.event_id,
third_party_invite: payload.third_party_invite,
oob_data: payload.oob_data,
});
});
break;
case 'view_user_settings':
this._setPage(this.PageTypes.UserSettings);
this.notifyNewScreen('settings');
@ -466,48 +435,7 @@ module.exports = React.createClass({
//this._setPage(this.PageTypes.CreateRoom);
//this.notifyNewScreen('new');
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
var Loader = sdk.getComponent("elements.Spinner");
var modal = Modal.createDialog(Loader);
if (MatrixClientPeg.get().isGuest()) {
Modal.createDialog(NeedToRegisterDialog, {
title: "Please Register",
description: "Guest users can't create new rooms. Please register to create room and start a chat."
});
return;
}
// XXX: FIXME: deduplicate this with MemberInfo's 'start chat' impl
MatrixClientPeg.get().createRoom({
preset: "private_chat",
// Allow guests by default since the room is private and they'd
// need an invite. This means clicking on a 3pid invite email can
// actually drop you right in to a chat.
initial_state: [
{
content: {
guest_access: 'can_join'
},
type: 'm.room.guest_access',
state_key: '',
}
],
}).done(function(res) {
modal.close();
dis.dispatch({
action: 'view_room',
room_id: res.room_id,
// show_settings: true,
});
}, function(err) {
modal.close();
Modal.createDialog(ErrorDialog, {
title: "Failed to create room",
description: err.toString()
});
});
createRoom().done();
break;
case 'view_room_directory':
this._setPage(this.PageTypes.RoomDirectory);
@ -572,8 +500,6 @@ module.exports = React.createClass({
this.focusComposer = true;
var newState = {
currentRoom: roomId,
currentRoomAlias: roomAlias,
initialEventId: eventId,
highlightedEventId: eventId,
initialEventPixelOffset: undefined,
@ -582,6 +508,18 @@ module.exports = React.createClass({
roomOobData: oob_data,
};
// If an alias has been provided, we use that and only that,
// since otherwise we'll prefer to pass in an ID to RoomView
// but if we're not in the room, we should join by alias rather
// than ID.
if (roomAlias) {
newState.currentRoomAlias = roomAlias;
newState.currentRoom = null;
} else {
newState.currentRoomAlias = null;
newState.currentRoom = roomId;
}
// if we aren't given an explicit event id, look for one in the
// scrollStateMap.
if (!eventId) {
@ -858,22 +796,28 @@ module.exports = React.createClass({
inviterName: params.inviter_name,
};
var payload = {
action: 'view_room',
event_id: eventId,
third_party_invite: third_party_invite,
oob_data: oob_data,
};
if (roomString[0] == '#') {
dis.dispatch({
action: 'view_room_alias',
room_alias: roomString,
event_id: eventId,
third_party_invite: third_party_invite,
oob_data: oob_data,
});
payload.room_alias = roomString;
} else {
dis.dispatch({
action: 'view_room',
room_id: roomString,
event_id: eventId,
third_party_invite: third_party_invite,
oob_data: oob_data,
});
payload.room_id = roomString;
}
// we can't view a room unless we're logged in
// (a guest account is fine)
if (!this.state.logged_in) {
this.starting_room_alias_payload = payload;
// Login is the default screen, so we'd do this anyway,
// but this will set the URL bar appropriately.
dis.dispatch({ action: 'start_login' });
return;
} else {
dis.dispatch(payload);
}
}
else {
@ -889,7 +833,7 @@ module.exports = React.createClass({
onAliasClick: function(event, alias) {
event.preventDefault();
dis.dispatch({action: 'view_room_alias', room_alias: alias});
dis.dispatch({action: 'view_room', room_alias: alias});
},
onUserClick: function(event, userId) {
@ -1084,14 +1028,14 @@ module.exports = React.createClass({
oobData={this.state.roomOobData}
highlightedEventId={this.state.highlightedEventId}
eventPixelOffset={this.state.initialEventPixelOffset}
key={this.state.currentRoom}
key={this.state.currentRoom || this.state.currentRoomAlias}
opacity={this.state.middleOpacity}
ConferenceHandler={this.props.ConferenceHandler} />
);
right_panel = <RightPanel roomId={this.state.currentRoom} collapsed={this.state.collapse_rhs} opacity={this.state.sideOpacity} />
break;
case this.PageTypes.UserSettings:
page_element = <UserSettings onClose={this.onUserSettingsClose} version={this.state.version} />
page_element = <UserSettings onClose={this.onUserSettingsClose} version={this.state.version} brand={this.props.config.brand} />
right_panel = <RightPanel collapsed={this.state.collapse_rhs} opacity={this.state.sideOpacity}/>
break;
case this.PageTypes.CreateRoom:
@ -1159,6 +1103,7 @@ module.exports = React.createClass({
guestAccessToken={this.state.guestAccessToken}
defaultHsUrl={this.props.config.default_hs_url}
defaultIsUrl={this.props.config.default_is_url}
brand={this.props.config.brand}
customHsUrl={this.getCurrentHsUrl()}
customIsUrl={this.getCurrentIsUrl()}
registrationUrl={this.props.registrationUrl}

View file

@ -86,6 +86,10 @@ module.exports = React.createClass({
// to manage its animations
this._readReceiptMap = {};
// Remember the read marker ghost node so we can do the cleanup that
// Velocity requires
this._readMarkerGhostNode = null;
this._isMounted = true;
},
@ -422,9 +426,16 @@ module.exports = React.createClass({
},
_startAnimation: function(ghostNode) {
Velocity(ghostNode, {opacity: '0', width: '10%'},
{duration: 400, easing: 'easeInSine',
delay: 1000});
if (this._readMarkerGhostNode) {
Velocity.Utilities.removeData(this._readMarkerGhostNode);
}
this._readMarkerGhostNode = ghostNode;
if (ghostNode) {
Velocity(ghostNode, {opacity: '0', width: '10%'},
{duration: 400, easing: 'easeInSine',
delay: 1000});
}
},
_getReadMarkerGhostTile: function() {

View file

@ -39,6 +39,7 @@ var dis = require("../../dispatcher");
var Tinter = require("../../Tinter");
var rate_limited_func = require('../../ratelimitedfunc');
var ObjectUtils = require('../../ObjectUtils');
var MatrixTools = require('../../MatrixTools');
var DEBUG = false;
@ -55,13 +56,6 @@ module.exports = React.createClass({
ConferenceHandler: React.PropTypes.any,
// the ID for this room (or, if we don't know it, an alias for it)
//
// XXX: if this is an alias, we will display a 'join' dialogue,
// regardless of whether we are already a member, or if the room is
// peekable. Currently there is a big mess, where at least four
// different components (RoomView, MatrixChat, RoomDirectory,
// SlashCommands) have logic for turning aliases into rooms, and each
// of them do it differently and have different edge cases.
roomAddress: React.PropTypes.string.isRequired,
// An object representing a third party invite to join this room
@ -100,7 +94,14 @@ module.exports = React.createClass({
},
getInitialState: function() {
var room = MatrixClientPeg.get().getRoom(this.props.roomAddress);
var room;
if (this.props.roomAddress[0] == '!') {
room = MatrixClientPeg.get().getRoom(this.props.roomAddress);
} else {
room = MatrixTools.getRoomForAlias(
MatrixClientPeg.get().getRooms(), this.props.roomAddress
);
}
return {
room: room,
roomLoading: !room,
@ -677,6 +678,16 @@ module.exports = React.createClass({
uploadFile: function(file) {
var self = this;
if (MatrixClientPeg.get().isGuest()) {
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, {
title: "Please Register",
description: "Guest users can't upload files. Please register to upload."
});
return;
}
ContentMessages.sendContentToRoom(
file, this.state.room.roomId, MatrixClientPeg.get()
).done(undefined, function(error) {

View file

@ -31,7 +31,9 @@ module.exports = React.createClass({
propTypes: {
version: React.PropTypes.string,
onClose: React.PropTypes.func
onClose: React.PropTypes.func,
// The brand string given when creating email pushers
brand: React.PropTypes.string,
},
getDefaultProps: function() {
@ -244,6 +246,23 @@ module.exports = React.createClass({
});
},
_renderDeviceInfo: function() {
var client = MatrixClientPeg.get();
var deviceId = client.deviceId;
var olmKey = client.getDeviceEd25519Key() || "<not supported>";
return (
<div>
<h3>Cryptography</h3>
<div className="mx_UserSettings_section">
<ul>
<li>Device ID: {deviceId}</li>
<li>Device key: {olmKey}</li>
</ul>
</div>
</div>
);
},
render: function() {
var self = this;
var Loader = sdk.getComponent("elements.Spinner");
@ -299,7 +318,7 @@ module.exports = React.createClass({
onValueChanged={ this.onAddThreepidClicked } />
</div>
<div className="mx_UserSettings_addThreepid">
<img src="img/plus.svg" width="14" height="14" alt="Add" onClick={ this.onAddThreepidClicked }/>
<img src="img/plus.svg" width="14" height="14" alt="Add" onClick={ this.onAddThreepidClicked.bind(this, undefined, true) }/>
</div>
</div>
);
@ -333,7 +352,7 @@ module.exports = React.createClass({
<h3>Notifications</h3>
<div className="mx_UserSettings_section">
<Notifications threepids={this.state.threepids} />
<Notifications threepids={this.state.threepids} brand={this.props.brand} />
</div>
</div>);
}
@ -390,6 +409,8 @@ module.exports = React.createClass({
{notification_area}
{this._renderDeviceInfo()}
<h3>Advanced</h3>
<div className="mx_UserSettings_section">
@ -403,9 +424,8 @@ module.exports = React.createClass({
Identity Server is { MatrixClientPeg.get().getIdentityServerUrl() }
</div>
<div className="mx_UserSettings_advanced">
Version {this.state.clientVersion}
<br />
{this.props.version}
matrix-react-sdk version: {this.state.clientVersion}<br/>
vector-web version: {this.props.version}<br/>
</div>
</div>

View file

@ -22,6 +22,7 @@ var sdk = require('../../../index');
var dis = require('../../../dispatcher');
var Signup = require("../../../Signup");
var ServerConfig = require("../../views/login/ServerConfig");
var MatrixClientPeg = require("../../../MatrixClientPeg");
var RegistrationForm = require("../../views/login/RegistrationForm");
var CaptchaForm = require("../../views/login/CaptchaForm");
@ -40,6 +41,7 @@ module.exports = React.createClass({
customIsUrl: React.PropTypes.string,
defaultHsUrl: React.PropTypes.string,
defaultIsUrl: React.PropTypes.string,
brand: React.PropTypes.string,
email: React.PropTypes.string,
username: React.PropTypes.string,
guestAccessToken: React.PropTypes.string,
@ -145,6 +147,26 @@ module.exports = React.createClass({
identityServerUrl: self.registerLogic.getIdentityServerUrl(),
accessToken: response.access_token
});
if (self.props.brand) {
MatrixClientPeg.get().getPushers().done((resp)=>{
var pushers = resp.pushers;
for (var i = 0; i < pushers.length; ++i) {
if (pushers[i].kind == 'email') {
var emailPusher = pushers[i];
emailPusher.data = { brand: self.props.brand };
MatrixClientPeg.get().setPusher(emailPusher).done(() => {
console.log("Set email branding to " + self.props.brand);
}, (error) => {
console.error("Couldn't set email branding: " + error);
});
}
}
}, (error) => {
console.error("Couldn't get pushers: " + error);
});
}
}, function(err) {
if (err.message) {
self.setState({

View file

@ -39,11 +39,11 @@ module.exports = React.createClass({
focus: true
};
},
componentDidMount: function() {
if (this.props.focus) {
// Set the cursor at the end of the text input
this.refs.textinput.value = this.props.value;
// Set the cursor at the end of the text input
this.refs.textinput.value = this.props.value;
}
},
@ -83,13 +83,12 @@ module.exports = React.createClass({
</div>
</div>
<div className="mx_Dialog_buttons">
<button onClick={this.onOk}>
{this.props.button}
</button>
<button onClick={this.onCancel}>
Cancel
</button>
<button onClick={this.onOk}>
{this.props.button}
</button>
</div>
</div>
);

View file

@ -17,8 +17,8 @@ limitations under the License.
'use strict';
var React = require('react');
var Velocity = require('velocity-animate');
require('velocity-ui-pack');
var Velocity = require('velocity-vector');
require('velocity-vector/velocity.ui');
var sdk = require('../../../index');
var Email = require('../../../email');
var Modal = require("../../../Modal");

View file

@ -45,9 +45,9 @@ module.exports = React.createClass({
getInitialState: function() {
return {
// the URL (if any) to be previewed with a LinkPreviewWidget
// the URLs (if any) to be previewed with a LinkPreviewWidget
// inside this TextualBody.
link: null,
links: [],
// track whether the preview widget is hidden
widgetHidden: false,
@ -57,9 +57,11 @@ module.exports = React.createClass({
componentDidMount: function() {
linkifyElement(this.refs.content, linkifyMatrix.options);
var link = this.findLink(this.refs.content.children);
if (link) {
this.setState({ link: link.getAttribute("href") });
var links = this.findLinks(this.refs.content.children);
if (links.length) {
this.setState({ links: links.map((link)=>{
return link.getAttribute("href");
})});
// lazy-load the hidden state of the preview widget from localstorage
if (global.localStorage) {
@ -74,27 +76,32 @@ module.exports = React.createClass({
shouldComponentUpdate: function(nextProps, nextState) {
// exploit that events are immutable :)
// ...and that .links is only ever set in componentDidMount and never changes
return (nextProps.mxEvent.getId() !== this.props.mxEvent.getId() ||
nextProps.highlights !== this.props.highlights ||
nextProps.highlightLink !== this.props.highlightLink ||
nextState.link !== this.state.link ||
nextState.links !== this.state.links ||
nextState.widgetHidden !== this.state.widgetHidden);
},
findLink: function(nodes) {
findLinks: function(nodes) {
var links = [];
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
if (node.tagName === "A" && node.getAttribute("href"))
{
return this.isLinkPreviewable(node) ? node : undefined;
if (this.isLinkPreviewable(node)) {
links.push(node);
}
}
else if (node.tagName === "PRE" || node.tagName === "CODE") {
return;
continue;
}
else if (node.children && node.children.length) {
return this.findLink(node.children)
links = links.concat(this.findLinks(node.children));
}
}
return links;
},
isLinkPreviewable: function(node) {
@ -160,14 +167,17 @@ module.exports = React.createClass({
{highlightLink: this.props.highlightLink});
var widget;
if (this.state.link && !this.state.widgetHidden) {
var widgets;
if (this.state.links.length && !this.state.widgetHidden) {
var LinkPreviewWidget = sdk.getComponent('rooms.LinkPreviewWidget');
widget = <LinkPreviewWidget
link={ this.state.link }
mxEvent={ this.props.mxEvent }
onCancelClick={ this.onCancelClick }
onWidgetLoad={ this.props.onWidgetLoad }/>;
widgets = this.state.links.map((link)=>{
return <LinkPreviewWidget
key={ link }
link={ link }
mxEvent={ this.props.mxEvent }
onCancelClick={ this.onCancelClick }
onWidgetLoad={ this.props.onWidgetLoad }/>;
});
}
switch (content.msgtype) {
@ -176,21 +186,21 @@ module.exports = React.createClass({
return (
<span ref="content" className="mx_MEmoteBody mx_EventTile_content">
* { name } { body }
{ widget }
{ widgets }
</span>
);
case "m.notice":
return (
<span ref="content" className="mx_MNoticeBody mx_EventTile_content">
{ body }
{ widget }
{ widgets }
</span>
);
default: // including "m.text"
return (
<span ref="content" className="mx_MTextBody mx_EventTile_content">
{ body }
{ widget }
{ widgets }
</span>
);
}

View file

@ -128,16 +128,24 @@ module.exports = React.createClass({
},
getInitialState: function() {
return {menu: false, allReadAvatars: false};
return {menu: false, allReadAvatars: false, verified: null};
},
componentWillMount: function() {
// don't do RR animations until we are mounted
this._suppressReadReceiptAnimation = true;
this._verifyEvent(this.props.mxEvent);
},
componentDidMount: function() {
this._suppressReadReceiptAnimation = false;
MatrixClientPeg.get().on("deviceVerified", this.onDeviceVerified);
},
componentWillReceiveProps: function (nextProps) {
if (nextProps.mxEvent !== this.props.mxEvent) {
this._verifyEvent(nextProps.mxEvent);
}
},
shouldComponentUpdate: function (nextProps, nextState) {
@ -152,6 +160,31 @@ module.exports = React.createClass({
return false;
},
componentWillUnmount: function() {
var client = MatrixClientPeg.get();
if (client) {
client.removeListener("deviceVerified", this.onDeviceVerified);
}
},
onDeviceVerified: function(userId, device) {
if (userId == this.props.mxEvent.getSender()) {
this._verifyEvent(this.props.mxEvent);
}
},
_verifyEvent: function(mxEvent) {
var verified = null;
if (mxEvent.isEncrypted()) {
verified = MatrixClientPeg.get().isEventSenderVerified(mxEvent);
}
this.setState({
verified: verified
});
},
_propsEqual: function(objA, objB) {
var keysA = Object.keys(objA);
var keysB = Object.keys(objB);
@ -346,6 +379,8 @@ module.exports = React.createClass({
mx_EventTile_last: this.props.last,
mx_EventTile_contextual: this.props.contextual,
menu: this.state.menu,
mx_EventTile_verified: this.state.verified == true,
mx_EventTile_unverified: this.state.verified == false,
});
var timestamp = <a href={ "#/room/" + this.props.mxEvent.getRoomId() +"/"+ this.props.mxEvent.getId() }>
<MessageTimestamp ts={this.props.mxEvent.getTs()} />

View file

@ -26,6 +26,7 @@ module.exports = React.createClass({
propTypes: {
roomId: React.PropTypes.string.isRequired,
onInvite: React.PropTypes.func.isRequired, // fn(inputText)
onThirdPartyInvite: React.PropTypes.func.isRequired, // fn(inputText)
onSearchQueryChanged: React.PropTypes.func // fn(inputText)
},
@ -49,10 +50,19 @@ module.exports = React.createClass({
}
},
componentDidMount: function() {
// initialise the email tile
this.onSearchQueryChanged('');
},
onInvite: function(ev) {
this.props.onInvite(this._input);
},
onThirdPartyInvite: function(ev) {
this.props.onThirdPartyInvite(this._input);
},
onSearchQueryChanged: function(input) {
this._input = input;
var EntityTile = sdk.getComponent("rooms.EntityTile");
@ -68,9 +78,10 @@ module.exports = React.createClass({
this._emailEntity = new Entities.newEntity(
<EntityTile key="dynamic_invite_tile" suppressOnHover={true} showInviteButton={true}
avatarJsx={ <BaseAvatar name="@" width={36} height={36} /> }
className="mx_EntityTile_invitePlaceholder"
presenceState="online" onClick={this.onInvite} name={label} />,
avatarJsx={ <BaseAvatar name="@" width={36} height={36} /> }
className="mx_EntityTile_invitePlaceholder"
presenceState="online" onClick={this.onThirdPartyInvite} name={"Invite by email"}
/>,
function(query) {
return true; // always show this
}
@ -89,7 +100,7 @@ module.exports = React.createClass({
}
return (
<SearchableEntityList searchPlaceholderText={"Invite/search by name, email, id"}
<SearchableEntityList searchPlaceholderText={"Search/invite by name, email, id"}
onSubmit={this.props.onInvite}
onQueryChanged={this.onSearchQueryChanged}
entities={entities}

View file

@ -0,0 +1,55 @@
/*
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.
*/
var React = require('react');
var MatrixClientPeg = require("../../../MatrixClientPeg");
module.exports = React.createClass({
displayName: 'MemberDeviceInfo',
propTypes: {
userId: React.PropTypes.string.isRequired,
device: React.PropTypes.object.isRequired,
},
onVerifyClick: function() {
MatrixClientPeg.get().setDeviceVerified(this.props.userId,
this.props.device.id);
},
render: function() {
var indicator = null, button = null;
if (this.props.device.verified) {
indicator = (
<div className="mx_MemberDeviceInfo_verified">&#x2714;</div>
);
} else {
button = (
<div className="mx_MemberDeviceInfo_textButton"
onClick={this.onVerifyClick}>
Verify
</div>
);
}
return (
<div className="mx_MemberDeviceInfo">
<div className="mx_MemberDeviceInfo_deviceId">{this.props.device.id}</div>
<div className="mx_MemberDeviceInfo_deviceKey">{this.props.device.key}</div>
{indicator}
{button}
</div>
);
},
});

View file

@ -30,27 +30,106 @@ var MatrixClientPeg = require("../../../MatrixClientPeg");
var dis = require("../../../dispatcher");
var Modal = require("../../../Modal");
var sdk = require('../../../index');
var createRoom = require('../../../createRoom');
module.exports = React.createClass({
displayName: 'MemberInfo',
propTypes: {
member: React.PropTypes.object.isRequired,
onFinished: React.PropTypes.func,
},
getDefaultProps: function() {
return {
onFinished: function() {}
};
},
componentDidMount: function() {
// work out the current state
if (this.props.member) {
var memberState = this._calculateOpsPermissions(this.props.member);
this.setState(memberState);
getInitialState: function() {
return {
can: {
kick: false,
ban: false,
mute: false,
modifyLevel: false
},
muted: false,
isTargetMod: false,
updating: 0,
devicesLoading: true,
devices: null,
}
},
componentWillMount: function() {
this._cancelDeviceList = null;
},
componentDidMount: function() {
this._updateStateForNewMember(this.props.member);
MatrixClientPeg.get().on("deviceVerified", this.onDeviceVerified);
},
componentWillReceiveProps: function(newProps) {
var memberState = this._calculateOpsPermissions(newProps.member);
this.setState(memberState);
if (this.props.member.userId != newProps.member.userId) {
this._updateStateForNewMember(newProps.member);
}
},
componentWillUnmount: function() {
var client = MatrixClientPeg.get();
if (client) {
client.removeListener("deviceVerified", this.onDeviceVerified);
}
if (this._cancelDeviceList) {
this._cancelDeviceList();
}
},
onDeviceVerified: function(userId, device) {
if (userId == this.props.member.userId) {
// no need to re-download the whole thing; just update our copy of
// the list.
var devices = MatrixClientPeg.get().listDeviceKeys(userId);
this.setState({devices: devices});
}
},
_updateStateForNewMember: function(member) {
var newState = this._calculateOpsPermissions(member);
newState.devicesLoading = true;
newState.devices = null;
this.setState(newState);
if (this._cancelDeviceList) {
this._cancelDeviceList();
this._cancelDeviceList = null;
}
this._downloadDeviceList(member);
},
_downloadDeviceList: function(member) {
var cancelled = false;
this._cancelDeviceList = function() { cancelled = true; }
var client = MatrixClientPeg.get();
var self = this;
client.downloadKeys([member.userId], true).finally(function() {
self._cancelDeviceList = null;
}).done(function() {
if (cancelled) {
// we got cancelled - presumably a different user now
return;
}
var devices = client.listDeviceKeys(member.userId);
self.setState({devicesLoading: false, devices: devices});
}, function(err) {
console.log("Error downloading devices", err);
self.setState({devicesLoading: false});
});
},
onKick: function() {
@ -315,50 +394,15 @@ module.exports = React.createClass({
this.props.onFinished();
}
else {
if (MatrixClientPeg.get().isGuest()) {
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, {
title: "Please Register",
description: "Guest users can't create new rooms. Please register to create room and start a chat."
});
self.props.onFinished();
return;
}
self.setState({ updating: self.state.updating + 1 });
MatrixClientPeg.get().createRoom({
// XXX: FIXME: deduplicate this with "view_create_room" in MatrixChat
invite: [this.props.member.userId],
preset: "private_chat",
// Allow guests by default since the room is private and they'd
// need an invite. This means clicking on a 3pid invite email can
// actually drop you right in to a chat.
initial_state: [
{
content: {
guest_access: 'can_join'
},
type: 'm.room.guest_access',
state_key: '',
}
],
}).then(
function(res) {
dis.dispatch({
action: 'view_room',
room_id: res.room_id
});
self.props.onFinished();
}, function(err) {
Modal.createDialog(ErrorDialog, {
title: "Failure to start chat",
description: err.message
});
self.props.onFinished();
}
).finally(()=>{
createRoom({
createOpts: {
invite: [this.props.member.userId],
},
}).finally(function() {
self.props.onFinished();
self.setState({ updating: self.state.updating - 1 });
});
}).done();
}
},
@ -367,21 +411,7 @@ module.exports = React.createClass({
action: 'leave_room',
room_id: this.props.member.roomId,
});
this.props.onFinished();
},
getInitialState: function() {
return {
can: {
kick: false,
ban: false,
mute: false,
modifyLevel: false
},
muted: false,
isTargetMod: false,
updating: 0,
}
this.props.onFinished();
},
_calculateOpsPermissions: function(member) {
@ -475,6 +505,36 @@ module.exports = React.createClass({
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
},
_renderDevices: function() {
var devices = this.state.devices;
var MemberDeviceInfo = sdk.getComponent('rooms.MemberDeviceInfo');
var Spinner = sdk.getComponent("elements.Spinner");
var devComponents;
if (this.state.devicesLoading) {
// still loading
devComponents = <Spinner />;
} else if (devices === null) {
devComponents = "Unable to load device list";
} else if (devices.length === 0) {
devComponents = "No registered devices";
} else {
devComponents = [];
for (var i = 0; i < devices.length; i++) {
devComponents.push(<MemberDeviceInfo key={i}
userId={this.props.member.userId}
device={devices[i]}/>);
}
}
return (
<div>
<h3>Devices</h3>
{devComponents}
</div>
);
},
render: function() {
var startChat, kickButton, banButton, muteButton, giveModButton, spinner;
if (this.props.member.userId !== MatrixClientPeg.get().credentials.userId) {
@ -551,6 +611,8 @@ module.exports = React.createClass({
{ startChat }
{ this._renderDevices() }
{ adminTools }
{ spinner }
@ -558,4 +620,3 @@ module.exports = React.createClass({
);
}
});

View file

@ -166,6 +166,25 @@ module.exports = React.createClass({
});
}, 500),
onThirdPartyInvite: function(inputText) {
var TextInputDialog = sdk.getComponent("dialogs.TextInputDialog");
Modal.createDialog(TextInputDialog, {
title: "Invite members by email",
description: "Please enter one or more email addresses",
value: inputText,
button: "Invite",
onFinished: (should_invite, addresses)=>{
if (should_invite) {
// defer the actual invite to the next event loop to give this
// Modal a chance to unmount in case onInvite() triggers a new one
setTimeout(()=>{
this.onInvite(addresses);
}, 0);
}
}
});
},
onInvite: function(inputText) {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
@ -387,7 +406,9 @@ module.exports = React.createClass({
// console.log(memberA + " and " + memberB + " have same power level");
if (memberA.name && memberB.name) {
// console.log("comparing names: " + memberA.name + " and " + memberB.name);
return memberA.name.localeCompare(memberB.name);
var nameA = memberA.name[0] === '@' ? memberA.name.substr(1) : memberA.name;
var nameB = memberB.name[0] === '@' ? memberB.name.substr(1) : memberB.name;
return nameA.localeCompare(nameB);
}
else {
return 0;
@ -512,6 +533,7 @@ module.exports = React.createClass({
inviteMemberListSection = (
<InviteMemberList roomId={this.props.roomId}
onSearchQueryChanged={this.onSearchQueryChanged}
onThirdPartyInvite={this.onThirdPartyInvite}
onInvite={this.onInvite} />
);
}

View file

@ -53,6 +53,15 @@ module.exports = React.createClass({
},
onUploadClick: function(ev) {
if (MatrixClientPeg.get().isGuest()) {
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, {
title: "Please Register",
description: "Guest users can't upload files. Please register to upload."
});
return;
}
this.refs.uploadInput.click();
},

View file

@ -34,7 +34,7 @@ module.exports = React.createClass({
getInitialState: function() {
var tags = {};
Object.keys(this.props.room.tags).forEach(function(tagName) {
tags[tagName] = {};
tags[tagName] = ['yep'];
});
var areNotifsMuted = false;
@ -180,7 +180,7 @@ module.exports = React.createClass({
// tags
if (this.state.tags_changed) {
var tagDiffs = ObjectUtils.getKeyValueArrayDiffs(originalState.tags, this.state.tags);
// [ {place: add, key: "m.favourite", val: "yep"} ]
// [ {place: add, key: "m.favourite", val: ["yep"]} ]
tagDiffs.forEach(function(diff) {
switch (diff.place) {
case "add":

View file

@ -48,6 +48,7 @@ var SearchableEntityList = React.createClass({
getInitialState: function() {
return {
query: "",
focused: false,
truncateAt: this.props.truncateAt,
results: this.getSearchResults("", this.props.entities)
};
@ -101,7 +102,7 @@ var SearchableEntityList = React.createClass({
getSearchResults: function(query, entities) {
if (!query || query.length === 0) {
return this.props.emptyQueryShowsAll ? entities : []
return this.props.emptyQueryShowsAll ? entities : [ entities[0] ]
}
return entities.filter(function(e) {
return e.matches(query);
@ -134,13 +135,27 @@ var SearchableEntityList = React.createClass({
<form onSubmit={this.onQuerySubmit} autoComplete="off">
<input className="mx_SearchableEntityList_query" id="mx_SearchableEntityList_query" type="text"
onChange={this.onQueryChanged} value={this.state.query}
onFocus={ ()=>{
if (this._blurTimeout) {
clearTimeout(this.blurTimeout);
}
this.setState({ focused: true });
} }
onBlur={ ()=>{
// nasty setTimeout heuristic to avoid the 'invite by email' prompt disappearing
// due to the onBlur before we can click on it
this._blurTimeout = setTimeout(
()=>{ this.setState({ focused: false }) },
300
);
} }
placeholder={this.props.searchPlaceholderText} />
</form>
);
}
var list;
if (this.state.results.length) {
if (this.state.results.length > 1 || this.state.focused) {
if (this.props.truncateAt) { // caller wants list truncated
var TruncatedList = sdk.getComponent("elements.TruncatedList");
list = (
@ -172,10 +187,10 @@ var SearchableEntityList = React.createClass({
}
return (
<div className={ "mx_SearchableEntityList " + (this.state.query.length ? "mx_SearchableEntityList_expanded" : "") }>
<div className={ "mx_SearchableEntityList " + (list ? "mx_SearchableEntityList_expanded" : "") }>
{ inputBox }
{ list }
{ this.state.query.length ? <div className="mx_SearchableEntityList_hrWrapper"><hr/></div> : '' }
{ list ? <div className="mx_SearchableEntityList_hrWrapper"><hr/></div> : '' }
</div>
);
}

86
src/createRoom.js Normal file
View file

@ -0,0 +1,86 @@
/*
Copyright 2015, 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.
*/
var MatrixClientPeg = require('./MatrixClientPeg');
var Modal = require('./Modal');
var sdk = require('./index');
var dis = require("./dispatcher");
var q = require('q');
/**
* Create a new room, and switch to it.
*
* Returns a promise which resolves to the room id, or null if the
* action was aborted or failed.
*
* @param {object=} opts parameters for creating the room
* @param {object=} opts.createOpts set of options to pass to createRoom call.
*/
function createRoom(opts) {
var opts = opts || {};
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
var Loader = sdk.getComponent("elements.Spinner");
var client = MatrixClientPeg.get();
if (client.isGuest()) {
Modal.createDialog(NeedToRegisterDialog, {
title: "Please Register",
description: "Guest users can't create new rooms. Please register to create room and start a chat."
});
return q(null);
}
// set some defaults for the creation
var createOpts = opts.createOpts || {};
createOpts.preset = createOpts.preset || 'private_chat';
createOpts.visibility = createOpts.visibility || 'private';
// Allow guests by default since the room is private and they'd
// need an invite. This means clicking on a 3pid invite email can
// actually drop you right in to a chat.
createOpts.initial_state = createOpts.initial_state || [
{
content: {
guest_access: 'can_join'
},
type: 'm.room.guest_access',
state_key: '',
}
];
var modal = Modal.createDialog(Loader);
return client.createRoom(createOpts).finally(function() {
modal.close();
}).then(function(res) {
dis.dispatch({
action: 'view_room',
room_id: res.room_id
});
return res.room_id;
}, function(err) {
Modal.createDialog(ErrorDialog, {
title: "Failure to create room",
description: err.toString()
});
return null;
});
}
module.exports = createRoom;