Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into feature-rte

This commit is contained in:
Aviral Dasgupta 2016-06-10 04:43:44 +05:30
commit bf8e56e04c
24 changed files with 543 additions and 178 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) 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) [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 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 # install the other dependencies
npm install npm install

View file

@ -1,6 +1,6 @@
{ {
"name": "matrix-react-sdk", "name": "matrix-react-sdk",
"version": "0.5.2", "version": "0.6.3",
"description": "SDK for matrix.org using React", "description": "SDK for matrix.org using React",
"author": "matrix.org", "author": "matrix.org",
"repository": { "repository": {
@ -32,15 +32,14 @@
"highlight.js": "^8.9.1", "highlight.js": "^8.9.1",
"linkifyjs": "^2.0.0-beta.4", "linkifyjs": "^2.0.0-beta.4",
"marked": "^0.3.5", "marked": "^0.3.5",
"matrix-js-sdk": "^0.5.2", "matrix-js-sdk": "matrix-org/matrix-js-sdk#develop",
"optimist": "^0.6.1", "optimist": "^0.6.1",
"q": "^1.4.1", "q": "^1.4.1",
"react": "^15.0.1", "react": "^15.0.1",
"react-dom": "^15.0.1", "react-dom": "^15.0.1",
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#c3d942e", "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#c3d942e",
"sanitize-html": "^1.11.1", "sanitize-html": "^1.11.1",
"velocity-animate": "^1.2.3", "velocity-vector": "vector-im/velocity#059e3b2"
"velocity-ui-pack": "^1.2.2"
}, },
"//babelversion": [ "//babelversion": [
"brief experiments with babel6 seems to show that it generates source ", "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; return this.getEmailPusher(pushers, address) !== undefined;
}, },
addEmailPusher: function(address) { addEmailPusher: function(address, data) {
return MatrixClientPeg.get().setPusher({ return MatrixClientPeg.get().setPusher({
kind: 'email', kind: 'email',
app_id: "m.email", app_id: "m.email",
@ -108,7 +108,7 @@ module.exports = {
app_display_name: 'Email Notifications', app_display_name: 'Email Notifications',
device_display_name: address, device_display_name: address,
lang: navigator.language, 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 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 React = require('react');
var ReactDom = require('react-dom'); var ReactDom = require('react-dom');
var Velocity = require('velocity-animate'); var Velocity = require('velocity-vector');
/** /**
* The Velociraptor contains components and animates transitions with velocity. * 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 // and the FAQ entry, "Preventing memory leaks when
// creating/destroying large numbers of elements" // creating/destroying large numbers of elements"
// (https://github.com/julianshapiro/velocity/issues/47) // (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; 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 // courtesy of https://github.com/julianshapiro/velocity/issues/283
// We only use easeOutBounce (easeInBounce is just sort of nonsensical) // 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.EventTile'] = require('./components/views/rooms/EventTile');
module.exports.components['views.rooms.InviteMemberList'] = require('./components/views/rooms/InviteMemberList'); 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.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.MemberInfo'] = require('./components/views/rooms/MemberInfo');
module.exports.components['views.rooms.MemberList'] = require('./components/views/rooms/MemberList'); module.exports.components['views.rooms.MemberList'] = require('./components/views/rooms/MemberList');
module.exports.components['views.rooms.MemberTile'] = require('./components/views/rooms/MemberTile'); 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 linkifyMatrix = require("../../linkify-matrix");
var KeyCode = require('../../KeyCode'); var KeyCode = require('../../KeyCode');
var createRoom = require("../../createRoom");
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'MatrixChat', displayName: 'MatrixChat',
propTypes: { propTypes: {
config: React.PropTypes.object.isRequired, config: React.PropTypes.object,
ConferenceHandler: React.PropTypes.any, ConferenceHandler: React.PropTypes.any,
onNewScreen: React.PropTypes.func, onNewScreen: React.PropTypes.func,
registrationUrl: React.PropTypes.string, registrationUrl: React.PropTypes.string,
@ -84,7 +86,8 @@ module.exports = React.createClass({
getDefaultProps: function() { getDefaultProps: function() {
return { return {
startingQueryParams: {} startingQueryParams: {},
config: {},
}; };
}, },
@ -97,10 +100,9 @@ module.exports = React.createClass({
else if (window.localStorage && window.localStorage.getItem("mx_hs_url")) { else if (window.localStorage && window.localStorage.getItem("mx_hs_url")) {
return window.localStorage.getItem("mx_hs_url"); return window.localStorage.getItem("mx_hs_url");
} }
else if (this.props.config) { else {
return this.props.config.default_hs_url return this.props.config.default_hs_url || "https://matrix.org";
} }
return "https://matrix.org";
}, },
getFallbackHsUrl: function() { getFallbackHsUrl: function() {
@ -116,10 +118,9 @@ module.exports = React.createClass({
else if (window.localStorage && window.localStorage.getItem("mx_is_url")) { else if (window.localStorage && window.localStorage.getItem("mx_is_url")) {
return window.localStorage.getItem("mx_is_url"); return window.localStorage.getItem("mx_is_url");
} }
else if (this.props.config) { else {
return this.props.config.default_is_url return this.props.config.default_is_url || "https://vector.im"
} }
return "https://matrix.org";
}, },
componentWillMount: function() { componentWillMount: function() {
@ -466,48 +467,7 @@ module.exports = React.createClass({
//this._setPage(this.PageTypes.CreateRoom); //this._setPage(this.PageTypes.CreateRoom);
//this.notifyNewScreen('new'); //this.notifyNewScreen('new');
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); createRoom().done();
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()
});
});
break; break;
case 'view_room_directory': case 'view_room_directory':
this._setPage(this.PageTypes.RoomDirectory); this._setPage(this.PageTypes.RoomDirectory);
@ -1091,7 +1051,7 @@ module.exports = React.createClass({
right_panel = <RightPanel roomId={this.state.currentRoom} collapsed={this.state.collapse_rhs} opacity={this.state.sideOpacity} /> right_panel = <RightPanel roomId={this.state.currentRoom} collapsed={this.state.collapse_rhs} opacity={this.state.sideOpacity} />
break; break;
case this.PageTypes.UserSettings: 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}/> right_panel = <RightPanel collapsed={this.state.collapse_rhs} opacity={this.state.sideOpacity}/>
break; break;
case this.PageTypes.CreateRoom: case this.PageTypes.CreateRoom:
@ -1159,6 +1119,7 @@ module.exports = React.createClass({
guestAccessToken={this.state.guestAccessToken} guestAccessToken={this.state.guestAccessToken}
defaultHsUrl={this.props.config.default_hs_url} defaultHsUrl={this.props.config.default_hs_url}
defaultIsUrl={this.props.config.default_is_url} defaultIsUrl={this.props.config.default_is_url}
brand={this.props.config.brand}
customHsUrl={this.getCurrentHsUrl()} customHsUrl={this.getCurrentHsUrl()}
customIsUrl={this.getCurrentIsUrl()} customIsUrl={this.getCurrentIsUrl()}
registrationUrl={this.props.registrationUrl} registrationUrl={this.props.registrationUrl}

View file

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

View file

@ -677,6 +677,16 @@ module.exports = React.createClass({
uploadFile: function(file) { uploadFile: function(file) {
var self = this; 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( ContentMessages.sendContentToRoom(
file, this.state.room.roomId, MatrixClientPeg.get() file, this.state.room.roomId, MatrixClientPeg.get()
).done(undefined, function(error) { ).done(undefined, function(error) {

View file

@ -31,7 +31,9 @@ module.exports = React.createClass({
propTypes: { propTypes: {
version: React.PropTypes.string, 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() { 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() { render: function() {
var self = this; var self = this;
var Loader = sdk.getComponent("elements.Spinner"); var Loader = sdk.getComponent("elements.Spinner");
@ -299,7 +318,7 @@ module.exports = React.createClass({
onValueChanged={ this.onAddThreepidClicked } /> onValueChanged={ this.onAddThreepidClicked } />
</div> </div>
<div className="mx_UserSettings_addThreepid"> <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>
</div> </div>
); );
@ -333,7 +352,7 @@ module.exports = React.createClass({
<h3>Notifications</h3> <h3>Notifications</h3>
<div className="mx_UserSettings_section"> <div className="mx_UserSettings_section">
<Notifications threepids={this.state.threepids} /> <Notifications threepids={this.state.threepids} brand={this.props.brand} />
</div> </div>
</div>); </div>);
} }
@ -390,6 +409,8 @@ module.exports = React.createClass({
{notification_area} {notification_area}
{this._renderDeviceInfo()}
<h3>Advanced</h3> <h3>Advanced</h3>
<div className="mx_UserSettings_section"> <div className="mx_UserSettings_section">
@ -403,9 +424,8 @@ module.exports = React.createClass({
Identity Server is { MatrixClientPeg.get().getIdentityServerUrl() } Identity Server is { MatrixClientPeg.get().getIdentityServerUrl() }
</div> </div>
<div className="mx_UserSettings_advanced"> <div className="mx_UserSettings_advanced">
Version {this.state.clientVersion} matrix-react-sdk version: {this.state.clientVersion}<br/>
<br /> vector-web version: {this.props.version}<br/>
{this.props.version}
</div> </div>
</div> </div>

View file

@ -22,6 +22,7 @@ var sdk = require('../../../index');
var dis = require('../../../dispatcher'); var dis = require('../../../dispatcher');
var Signup = require("../../../Signup"); var Signup = require("../../../Signup");
var ServerConfig = require("../../views/login/ServerConfig"); var ServerConfig = require("../../views/login/ServerConfig");
var MatrixClientPeg = require("../../../MatrixClientPeg");
var RegistrationForm = require("../../views/login/RegistrationForm"); var RegistrationForm = require("../../views/login/RegistrationForm");
var CaptchaForm = require("../../views/login/CaptchaForm"); var CaptchaForm = require("../../views/login/CaptchaForm");
@ -40,6 +41,7 @@ module.exports = React.createClass({
customIsUrl: React.PropTypes.string, customIsUrl: React.PropTypes.string,
defaultHsUrl: React.PropTypes.string, defaultHsUrl: React.PropTypes.string,
defaultIsUrl: React.PropTypes.string, defaultIsUrl: React.PropTypes.string,
brand: React.PropTypes.string,
email: React.PropTypes.string, email: React.PropTypes.string,
username: React.PropTypes.string, username: React.PropTypes.string,
guestAccessToken: React.PropTypes.string, guestAccessToken: React.PropTypes.string,
@ -145,6 +147,26 @@ module.exports = React.createClass({
identityServerUrl: self.registerLogic.getIdentityServerUrl(), identityServerUrl: self.registerLogic.getIdentityServerUrl(),
accessToken: response.access_token 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) { }, function(err) {
if (err.message) { if (err.message) {
self.setState({ self.setState({

View file

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

View file

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

View file

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

View file

@ -128,16 +128,24 @@ module.exports = React.createClass({
}, },
getInitialState: function() { getInitialState: function() {
return {menu: false, allReadAvatars: false}; return {menu: false, allReadAvatars: false, verified: null};
}, },
componentWillMount: function() { componentWillMount: function() {
// don't do RR animations until we are mounted // don't do RR animations until we are mounted
this._suppressReadReceiptAnimation = true; this._suppressReadReceiptAnimation = true;
this._verifyEvent(this.props.mxEvent);
}, },
componentDidMount: function() { componentDidMount: function() {
this._suppressReadReceiptAnimation = false; 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) { shouldComponentUpdate: function (nextProps, nextState) {
@ -152,6 +160,31 @@ module.exports = React.createClass({
return false; 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) { _propsEqual: function(objA, objB) {
var keysA = Object.keys(objA); var keysA = Object.keys(objA);
var keysB = Object.keys(objB); var keysB = Object.keys(objB);
@ -346,6 +379,8 @@ module.exports = React.createClass({
mx_EventTile_last: this.props.last, mx_EventTile_last: this.props.last,
mx_EventTile_contextual: this.props.contextual, mx_EventTile_contextual: this.props.contextual,
menu: this.state.menu, 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() }> var timestamp = <a href={ "#/room/" + this.props.mxEvent.getRoomId() +"/"+ this.props.mxEvent.getId() }>
<MessageTimestamp ts={this.props.mxEvent.getTs()} /> <MessageTimestamp ts={this.props.mxEvent.getTs()} />

View file

@ -26,6 +26,7 @@ module.exports = React.createClass({
propTypes: { propTypes: {
roomId: React.PropTypes.string.isRequired, roomId: React.PropTypes.string.isRequired,
onInvite: React.PropTypes.func.isRequired, // fn(inputText) onInvite: React.PropTypes.func.isRequired, // fn(inputText)
onThirdPartyInvite: React.PropTypes.func.isRequired, // fn(inputText)
onSearchQueryChanged: React.PropTypes.func // 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) { onInvite: function(ev) {
this.props.onInvite(this._input); this.props.onInvite(this._input);
}, },
onThirdPartyInvite: function(ev) {
this.props.onThirdPartyInvite(this._input);
},
onSearchQueryChanged: function(input) { onSearchQueryChanged: function(input) {
this._input = input; this._input = input;
var EntityTile = sdk.getComponent("rooms.EntityTile"); var EntityTile = sdk.getComponent("rooms.EntityTile");
@ -68,9 +78,10 @@ module.exports = React.createClass({
this._emailEntity = new Entities.newEntity( this._emailEntity = new Entities.newEntity(
<EntityTile key="dynamic_invite_tile" suppressOnHover={true} showInviteButton={true} <EntityTile key="dynamic_invite_tile" suppressOnHover={true} showInviteButton={true}
avatarJsx={ <BaseAvatar name="@" width={36} height={36} /> } avatarJsx={ <BaseAvatar name="@" width={36} height={36} /> }
className="mx_EntityTile_invitePlaceholder" className="mx_EntityTile_invitePlaceholder"
presenceState="online" onClick={this.onInvite} name={label} />, presenceState="online" onClick={this.onThirdPartyInvite} name={"Invite by email"}
/>,
function(query) { function(query) {
return true; // always show this return true; // always show this
} }
@ -89,7 +100,7 @@ module.exports = React.createClass({
} }
return ( return (
<SearchableEntityList searchPlaceholderText={"Invite/search by name, email, id"} <SearchableEntityList searchPlaceholderText={"Search/invite by name, email, id"}
onSubmit={this.props.onInvite} onSubmit={this.props.onInvite}
onQueryChanged={this.onSearchQueryChanged} onQueryChanged={this.onSearchQueryChanged}
entities={entities} 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 dis = require("../../../dispatcher");
var Modal = require("../../../Modal"); var Modal = require("../../../Modal");
var sdk = require('../../../index'); var sdk = require('../../../index');
var createRoom = require('../../../createRoom');
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'MemberInfo', displayName: 'MemberInfo',
propTypes: {
member: React.PropTypes.object.isRequired,
onFinished: React.PropTypes.func,
},
getDefaultProps: function() { getDefaultProps: function() {
return { return {
onFinished: function() {} onFinished: function() {}
}; };
}, },
componentDidMount: function() { getInitialState: function() {
// work out the current state return {
if (this.props.member) { can: {
var memberState = this._calculateOpsPermissions(this.props.member); kick: false,
this.setState(memberState); 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) { componentWillReceiveProps: function(newProps) {
var memberState = this._calculateOpsPermissions(newProps.member); if (this.props.member.userId != newProps.member.userId) {
this.setState(memberState); 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() { onKick: function() {
@ -315,50 +394,15 @@ module.exports = React.createClass({
this.props.onFinished(); this.props.onFinished();
} }
else { 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 }); self.setState({ updating: self.state.updating + 1 });
MatrixClientPeg.get().createRoom({ createRoom({
// XXX: FIXME: deduplicate this with "view_create_room" in MatrixChat createOpts: {
invite: [this.props.member.userId], invite: [this.props.member.userId],
preset: "private_chat", },
// Allow guests by default since the room is private and they'd }).finally(function() {
// need an invite. This means clicking on a 3pid invite email can self.props.onFinished();
// 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(()=>{
self.setState({ updating: self.state.updating - 1 }); self.setState({ updating: self.state.updating - 1 });
}); }).done();
} }
}, },
@ -367,21 +411,7 @@ module.exports = React.createClass({
action: 'leave_room', action: 'leave_room',
room_id: this.props.member.roomId, room_id: this.props.member.roomId,
}); });
this.props.onFinished(); this.props.onFinished();
},
getInitialState: function() {
return {
can: {
kick: false,
ban: false,
mute: false,
modifyLevel: false
},
muted: false,
isTargetMod: false,
updating: 0,
}
}, },
_calculateOpsPermissions: function(member) { _calculateOpsPermissions: function(member) {
@ -475,6 +505,36 @@ module.exports = React.createClass({
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox"); 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() { render: function() {
var startChat, kickButton, banButton, muteButton, giveModButton, spinner; var startChat, kickButton, banButton, muteButton, giveModButton, spinner;
if (this.props.member.userId !== MatrixClientPeg.get().credentials.userId) { if (this.props.member.userId !== MatrixClientPeg.get().credentials.userId) {
@ -551,6 +611,8 @@ module.exports = React.createClass({
{ startChat } { startChat }
{ this._renderDevices() }
{ adminTools } { adminTools }
{ spinner } { spinner }
@ -558,4 +620,3 @@ module.exports = React.createClass({
); );
} }
}); });

View file

@ -166,6 +166,25 @@ module.exports = React.createClass({
}); });
}, 500), }, 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) { onInvite: function(inputText) {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog"); var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
@ -387,7 +406,9 @@ module.exports = React.createClass({
// console.log(memberA + " and " + memberB + " have same power level"); // console.log(memberA + " and " + memberB + " have same power level");
if (memberA.name && memberB.name) { if (memberA.name && memberB.name) {
// console.log("comparing names: " + memberA.name + " and " + 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 { else {
return 0; return 0;
@ -512,6 +533,7 @@ module.exports = React.createClass({
inviteMemberListSection = ( inviteMemberListSection = (
<InviteMemberList roomId={this.props.roomId} <InviteMemberList roomId={this.props.roomId}
onSearchQueryChanged={this.onSearchQueryChanged} onSearchQueryChanged={this.onSearchQueryChanged}
onThirdPartyInvite={this.onThirdPartyInvite}
onInvite={this.onInvite} /> onInvite={this.onInvite} />
); );
} }

View file

@ -46,6 +46,15 @@ module.exports = React.createClass({
}, },
onUploadClick: function(ev) { 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(); this.refs.uploadInput.click();
}, },

View file

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

View file

@ -48,6 +48,7 @@ var SearchableEntityList = React.createClass({
getInitialState: function() { getInitialState: function() {
return { return {
query: "", query: "",
focused: false,
truncateAt: this.props.truncateAt, truncateAt: this.props.truncateAt,
results: this.getSearchResults("", this.props.entities) results: this.getSearchResults("", this.props.entities)
}; };
@ -101,7 +102,7 @@ var SearchableEntityList = React.createClass({
getSearchResults: function(query, entities) { getSearchResults: function(query, entities) {
if (!query || query.length === 0) { if (!query || query.length === 0) {
return this.props.emptyQueryShowsAll ? entities : [] return this.props.emptyQueryShowsAll ? entities : [ entities[0] ]
} }
return entities.filter(function(e) { return entities.filter(function(e) {
return e.matches(query); return e.matches(query);
@ -134,13 +135,27 @@ var SearchableEntityList = React.createClass({
<form onSubmit={this.onQuerySubmit} autoComplete="off"> <form onSubmit={this.onQuerySubmit} autoComplete="off">
<input className="mx_SearchableEntityList_query" id="mx_SearchableEntityList_query" type="text" <input className="mx_SearchableEntityList_query" id="mx_SearchableEntityList_query" type="text"
onChange={this.onQueryChanged} value={this.state.query} 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} /> placeholder={this.props.searchPlaceholderText} />
</form> </form>
); );
} }
var list; var list;
if (this.state.results.length) { if (this.state.results.length > 1 || this.state.focused) {
if (this.props.truncateAt) { // caller wants list truncated if (this.props.truncateAt) { // caller wants list truncated
var TruncatedList = sdk.getComponent("elements.TruncatedList"); var TruncatedList = sdk.getComponent("elements.TruncatedList");
list = ( list = (
@ -172,10 +187,10 @@ var SearchableEntityList = React.createClass({
} }
return ( return (
<div className={ "mx_SearchableEntityList " + (this.state.query.length ? "mx_SearchableEntityList_expanded" : "") }> <div className={ "mx_SearchableEntityList " + (list ? "mx_SearchableEntityList_expanded" : "") }>
{ inputBox } { inputBox }
{ list } { list }
{ this.state.query.length ? <div className="mx_SearchableEntityList_hrWrapper"><hr/></div> : '' } { list ? <div className="mx_SearchableEntityList_hrWrapper"><hr/></div> : '' }
</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;