Merge branch 'develop' into travis/granular

This commit is contained in:
Travis Ralston 2017-11-03 21:56:46 -06:00
commit 69939e2fe3
21 changed files with 697 additions and 229 deletions

View file

@ -15,35 +15,24 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from 'matrix-react-sdk';
import { _t } from 'matrix-react-sdk/lib/languageHandler';
import MatrixClientPeg from 'matrix-react-sdk/lib/MatrixClientPeg';
class SendCustomEvent extends React.Component {
static propTypes = {
roomId: React.PropTypes.string.isRequired,
onBack: React.PropTypes.func.isRequired,
eventType: React.PropTypes.string.isRequired,
evContent: React.PropTypes.string.isRequired,
class DevtoolsComponent extends React.Component {
static contextTypes = {
roomId: PropTypes.string.isRequired,
};
}
static defaultProps = {
eventType: '',
evContent: '{\n\n}',
};
class GenericEditor extends DevtoolsComponent {
// static propTypes = {onBack: PropTypes.func.isRequired};
constructor(props, context) {
super(props, context);
this._send = this._send.bind(this);
this.onBack = this.onBack.bind(this);
this._onChange = this._onChange.bind(this);
this.state = {
message: null,
input_eventType: this.props.eventType,
input_evContent: this.props.evContent,
};
this.onBack = this.onBack.bind(this);
}
onBack() {
@ -54,6 +43,10 @@ class SendCustomEvent extends React.Component {
}
}
_onChange(e) {
this.setState({[e.target.id]: e.target.type === 'checkbox' ? e.target.checked : e.target.value});
}
_buttons() {
return <div className="mx_Dialog_buttons">
<button onClick={this.onBack}>{ _t('Back') }</button>
@ -61,19 +54,64 @@ class SendCustomEvent extends React.Component {
</div>;
}
textInput(id, label) {
return <div className="mx_DevTools_inputRow">
<div className="mx_DevTools_inputLabelCell">
<label htmlFor={id}>{ label }</label>
</div>
<div className="mx_DevTools_inputCell">
<input id={id} onChange={this._onChange} value={this.state[id]} size="32" />
</div>
</div>;
}
}
class SendCustomEvent extends GenericEditor {
static getLabel() { return _t('Send Custom Event'); }
static propTypes = {
onBack: PropTypes.func.isRequired,
forceStateEvent: PropTypes.bool,
inputs: PropTypes.object,
};
constructor(props, context) {
super(props, context);
this._send = this._send.bind(this);
const {eventType, stateKey, evContent} = Object.assign({
eventType: '',
stateKey: '',
evContent: '{\n\n}',
}, this.props.inputs);
this.state = {
isStateEvent: Boolean(this.props.forceStateEvent),
eventType,
stateKey,
evContent,
};
}
send(content) {
return MatrixClientPeg.get().sendEvent(this.props.roomId, this.state.input_eventType, content);
const cli = MatrixClientPeg.get();
if (this.state.isStateEvent) {
return cli.sendStateEvent(this.context.roomId, this.state.eventType, content, this.state.stateKey);
} else {
return cli.sendEvent(this.context.roomId, this.state.eventType, content);
}
}
async _send() {
if (this.state.input_eventType === '') {
if (this.state.eventType === '') {
this.setState({ message: _t('You must specify an event type!') });
return;
}
let message;
try {
const content = JSON.parse(this.state.input_evContent);
const content = JSON.parse(this.state.evContent);
await this.send(content);
message = _t('Event sent!');
} catch (e) {
@ -82,14 +120,6 @@ class SendCustomEvent extends React.Component {
this.setState({ message });
}
_additionalFields() {
return <div />;
}
_onChange(e) {
this.setState({[`input_${e.target.id}`]: e.target.value});
}
render() {
if (this.state.message) {
return <div>
@ -102,87 +132,176 @@ class SendCustomEvent extends React.Component {
return <div>
<div className="mx_Dialog_content">
{ this._additionalFields() }
<div className="mx_TextInputDialog_label">
<label htmlFor="eventType"> { _t('Event Type') } </label>
</div>
<div>
<input id="eventType" onChange={this._onChange} value={this.state.input_eventType} className="mx_TextInputDialog_input" size="64" />
</div>
{ this.textInput('eventType', _t('Event Type')) }
{ this.state.isStateEvent && this.textInput('stateKey', _t('State Key')) }
<div className="mx_TextInputDialog_label">
<br />
<div className="mx_UserSettings_profileLabelCell">
<label htmlFor="evContent"> { _t('Event Content') } </label>
</div>
<div>
<textarea id="evContent" onChange={this._onChange} value={this.state.input_evContent} className="mx_TextInputDialog_input" cols="63" rows="5" />
<textarea id="evContent" onChange={this._onChange} value={this.state.evContent} className="mx_TextInputDialog_input" cols="63" rows="5" />
</div>
</div>
{ this._buttons() }
<div className="mx_Dialog_buttons">
<button onClick={this.onBack}>{ _t('Back') }</button>
{ !this.state.message && <button onClick={this._send}>{ _t('Send') }</button> }
{ !this.state.message && !this.props.forceStateEvent && <div style={{float: "right"}}>
<input id="isStateEvent" className="mx_DevTools_tgl mx_DevTools_tgl-flip" type="checkbox" onChange={this._onChange} checked={this.state.isStateEvent} />
<label className="mx_DevTools_tgl-btn" data-tg-off="Event" data-tg-on="State Event" htmlFor="isStateEvent" />
</div> }
</div>
</div>;
}
}
class SendCustomStateEvent extends SendCustomEvent {
class SendAccountData extends GenericEditor {
static getLabel() { return _t('Send Account Data'); }
static propTypes = {
roomId: React.PropTypes.string.isRequired,
onBack: React.PropTypes.func.isRequired,
eventType: React.PropTypes.string.isRequired,
evContent: React.PropTypes.string.isRequired,
stateKey: React.PropTypes.string.isRequired,
};
static defaultProps = {
eventType: '',
evContent: '{\n\n}',
stateKey: '',
isRoomAccountData: PropTypes.bool,
forceMode: PropTypes.bool,
inputs: PropTypes.object,
};
constructor(props, context) {
super(props, context);
this.state['input_stateKey'] = this.props.stateKey;
this._send = this._send.bind(this);
const {eventType, evContent} = Object.assign({
eventType: '',
evContent: '{\n\n}',
}, this.props.inputs);
this.state = {
isRoomAccountData: Boolean(this.props.isRoomAccountData),
eventType,
evContent,
};
}
send(content) {
const cli = MatrixClientPeg.get();
return cli.sendStateEvent(this.props.roomId, this.state.input_eventType, content, this.state.input_stateKey);
if (this.state.isRoomAccountData) {
return cli.setRoomAccountData(this.context.roomId, this.state.eventType, content);
}
return cli.setAccountData(this.state.eventType, content);
}
_additionalFields() {
async _send() {
if (this.state.eventType === '') {
this.setState({ message: _t('You must specify an event type!') });
return;
}
let message;
try {
const content = JSON.parse(this.state.evContent);
await this.send(content);
message = _t('Event sent!');
} catch (e) {
message = _t('Failed to send custom event.') + ' (' + e.toString() + ')';
}
this.setState({ message });
}
render() {
if (this.state.message) {
return <div>
<div className="mx_Dialog_content">
{ this.state.message }
</div>
{ this._buttons() }
</div>;
}
return <div>
<div className="mx_TextInputDialog_label">
<label htmlFor="stateKey"> { _t('State Key') } </label>
<div className="mx_Dialog_content">
{ this.textInput('eventType', _t('Event Type')) }
<br />
<div className="mx_UserSettings_profileLabelCell">
<label htmlFor="evContent"> { _t('Event Content') } </label>
</div>
<div>
<textarea id="evContent" onChange={this._onChange} value={this.state.evContent} className="mx_TextInputDialog_input" cols="63" rows="5" />
</div>
</div>
<div>
<input id="stateKey" onChange={this._onChange} value={this.state.input_stateKey} className="mx_TextInputDialog_input" size="64" />
<div className="mx_Dialog_buttons">
<button onClick={this.onBack}>{ _t('Back') }</button>
{ !this.state.message && <button onClick={this._send}>{ _t('Send') }</button> }
{ !this.state.message && <div style={{float: "right"}}>
<input id="isRoomAccountData" className="mx_DevTools_tgl mx_DevTools_tgl-flip" type="checkbox" onChange={this._onChange} checked={this.state.isRoomAccountData} disabled={this.props.forceMode} />
<label className="mx_DevTools_tgl-btn" data-tg-off="Account Data" data-tg-on="Room Data" htmlFor="isRoomAccountData" />
</div> }
</div>
</div>;
}
}
class RoomStateExplorer extends React.Component {
class FilteredList extends React.Component {
static propTypes = {
setMode: React.PropTypes.func.isRequired,
roomId: React.PropTypes.string.isRequired,
onBack: React.PropTypes.func.isRequired,
children: PropTypes.any,
};
constructor(props, context) {
super(props, context);
this.onQuery = this.onQuery.bind(this);
this.state = {
query: '',
};
}
onQuery(ev) {
this.setState({ query: ev.target.value });
}
filterChildren() {
if (this.state.query) {
const lowerQuery = this.state.query.toLowerCase();
return this.props.children.filter((child) => child.key.toLowerCase().includes(lowerQuery));
}
return this.props.children;
}
render() {
return <div>
<input size="64"
onChange={this.onQuery}
value={this.state.query}
placeholder={_t('Filter results')}
className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query" />
{ this.filterChildren() }
</div>;
}
}
class RoomStateExplorer extends DevtoolsComponent {
static getLabel() { return _t('Explore Room State'); }
static propTypes = {
onBack: PropTypes.func.isRequired,
};
constructor(props, context) {
super(props, context);
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
const room = MatrixClientPeg.get().getRoom(this.context.roomId);
this.roomStateEvents = room.currentState.events;
this.onBack = this.onBack.bind(this);
this.editEv = this.editEv.bind(this);
this.onQuery = this.onQuery.bind(this);
}
state = {
query: '',
eventType: null,
event: null,
};
this.state = {
eventType: null,
event: null,
editing: false,
};
}
browseEventType(eventType) {
return () => {
@ -197,7 +316,9 @@ class RoomStateExplorer extends React.Component {
}
onBack() {
if (this.state.event) {
if (this.state.editing) {
this.setState({ editing: false });
} else if (this.state.event) {
this.setState({ event: null });
} else if (this.state.eventType) {
this.setState({ eventType: null });
@ -207,20 +328,19 @@ class RoomStateExplorer extends React.Component {
}
editEv() {
const ev = this.state.event;
this.props.setMode(SendCustomStateEvent, {
eventType: ev.getType(),
evContent: JSON.stringify(ev.getContent(), null, '\t'),
stateKey: ev.getStateKey(),
});
}
onQuery(ev) {
this.setState({ query: ev.target.value });
this.setState({ editing: true });
}
render() {
if (this.state.event) {
if (this.state.editing) {
return <SendCustomEvent forceStateEvent={true} onBack={this.onBack} inputs={{
eventType: this.state.event.getType(),
evContent: JSON.stringify(this.state.event.getContent(), null, '\t'),
stateKey: this.state.event.getStateKey(),
}} />;
}
return <div className="mx_ViewSource">
<div className="mx_Dialog_content">
<pre>{ JSON.stringify(this.state.event.event, null, 2) }</pre>
@ -234,11 +354,9 @@ class RoomStateExplorer extends React.Component {
const rows = [];
const classes = 'mx_DevTools_RoomStateExplorer_button';
if (this.state.eventType === null) {
Object.keys(this.roomStateEvents).forEach((evType) => {
// Skip this entry if does not contain search query
if (this.state.query && !evType.toLowerCase().includes(this.state.query.toLowerCase())) return;
const stateGroup = this.roomStateEvents[evType];
const stateKeys = Object.keys(stateGroup);
@ -249,7 +367,7 @@ class RoomStateExplorer extends React.Component {
onClickFn = this.onViewSourceClick(stateGroup[stateKeys[0]]);
}
rows.push(<button className="mx_DevTools_RoomStateExplorer_button" key={evType} onClick={onClickFn}>
rows.push(<button className={classes} key={evType} onClick={onClickFn}>
{ evType }
</button>);
});
@ -257,12 +375,8 @@ class RoomStateExplorer extends React.Component {
const evType = this.state.eventType;
const stateGroup = this.roomStateEvents[evType];
Object.keys(stateGroup).forEach((stateKey) => {
// Skip this entry if does not contain search query
if (this.state.query && !stateKey.toLowerCase().includes(this.state.query.toLowerCase())) return;
const ev = stateGroup[stateKey];
rows.push(<button className="mx_DevTools_RoomStateExplorer_button" key={stateKey}
onClick={this.onViewSourceClick(ev)}>
rows.push(<button className={classes} key={stateKey} onClick={this.onViewSourceClick(ev)}>
{ stateKey }
</button>);
});
@ -270,8 +384,9 @@ class RoomStateExplorer extends React.Component {
return <div>
<div className="mx_Dialog_content">
<input onChange={this.onQuery} placeholder={_t('Filter results')} size="64" className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query" value={this.state.query} />
{ rows }
<FilteredList>
{ rows }
</FilteredList>
</div>
<div className="mx_Dialog_buttons">
<button onClick={this.onBack}>{ _t('Back') }</button>
@ -280,40 +395,157 @@ class RoomStateExplorer extends React.Component {
}
}
export default class DevtoolsDialog extends React.Component {
class AccountDataExplorer extends DevtoolsComponent {
static getLabel() { return _t('Explore Account Data'); }
static propTypes = {
roomId: React.PropTypes.string.isRequired,
onFinished: React.PropTypes.func.isRequired,
onBack: PropTypes.func.isRequired,
};
state = {
mode: null,
modeArgs: {},
constructor(props, context) {
super(props, context);
this.onBack = this.onBack.bind(this);
this.editEv = this.editEv.bind(this);
this._onChange = this._onChange.bind(this);
this.state = {
isRoomAccountData: false,
event: null,
editing: false,
};
}
getData() {
const cli = MatrixClientPeg.get();
if (this.state.isRoomAccountData) {
return cli.getRoom(this.context.roomId).accountData;
}
return cli.store.accountData;
}
onViewSourceClick(event) {
return () => {
this.setState({ event });
};
}
onBack() {
if (this.state.editing) {
this.setState({ editing: false });
} else if (this.state.event) {
this.setState({ event: null });
} else {
this.props.onBack();
}
}
_onChange(e) {
this.setState({[e.target.id]: e.target.type === 'checkbox' ? e.target.checked : e.target.value});
}
editEv() {
this.setState({ editing: true });
}
render() {
if (this.state.event) {
if (this.state.editing) {
return <SendAccountData isRoomAccountData={this.state.isRoomAccountData} onBack={this.onBack} inputs={{
eventType: this.state.event.getType(),
evContent: JSON.stringify(this.state.event.getContent(), null, '\t'),
}} forceMode={true} />;
}
return <div className="mx_ViewSource">
<div className="mx_Dialog_content">
<pre>{ JSON.stringify(this.state.event.event, null, 2) }</pre>
</div>
<div className="mx_Dialog_buttons">
<button onClick={this.onBack}>{ _t('Back') }</button>
<button onClick={this.editEv}>{ _t('Edit') }</button>
</div>
</div>;
}
const rows = [];
const classes = 'mx_DevTools_RoomStateExplorer_button';
const data = this.getData();
Object.keys(data).forEach((evType) => {
const ev = data[evType];
rows.push(<button className={classes} key={evType} onClick={this.onViewSourceClick(ev)}>
{ evType }
</button>);
});
return <div>
<div className="mx_Dialog_content">
<FilteredList>
{ rows }
</FilteredList>
</div>
<div className="mx_Dialog_buttons">
<button onClick={this.onBack}>{ _t('Back') }</button>
{ !this.state.message && <div style={{float: "right"}}>
<input id="isRoomAccountData" className="mx_DevTools_tgl mx_DevTools_tgl-flip" type="checkbox" onChange={this._onChange} checked={this.state.isRoomAccountData} />
<label className="mx_DevTools_tgl-btn" data-tg-off="Account Data" data-tg-on="Room Data" htmlFor="isRoomAccountData" />
</div> }
</div>
</div>;
}
}
const Entries = [
SendCustomEvent,
RoomStateExplorer,
SendAccountData,
AccountDataExplorer,
];
export default class DevtoolsDialog extends React.Component {
static childContextTypes = {
roomId: PropTypes.string.isRequired,
// client: PropTypes.instanceOf(MatixClient),
};
static propTypes = {
roomId: PropTypes.string.isRequired,
onFinished: PropTypes.func.isRequired,
};
constructor(props, context) {
super(props, context);
this.onBack = this.onBack.bind(this);
this.setMode = this.setMode.bind(this);
this.onCancel = this.onCancel.bind(this);
this.state = {
mode: null,
};
}
componentWillUnmount() {
this._unmounted = true;
}
getChildContext() {
return { roomId: this.props.roomId };
}
_setMode(mode) {
return () => {
this.setMode(mode);
this.setState({ mode });
};
}
setMode(mode, modeArgs={}) {
this.setState({ mode, modeArgs });
}
onBack() {
this.setState({ mode: null });
if (this.prevMode) {
this.setState({ mode: this.prevMode });
this.prevMode = null;
} else {
this.setState({ mode: null });
}
}
onCancel() {
@ -324,14 +556,27 @@ export default class DevtoolsDialog extends React.Component {
let body;
if (this.state.mode) {
body =
<this.state.mode {...this.props} {...this.state.modeArgs} onBack={this.onBack} setMode={this.setMode} />;
} else {
body = <div>
<div className="mx_Dialog_content">
<button onClick={this._setMode(SendCustomEvent)}>{ _t('Send Custom Event') }</button>
<button onClick={this._setMode(SendCustomStateEvent)}>{ _t('Send Custom State Event') }</button>
<button onClick={this._setMode(RoomStateExplorer)}>{ _t('Explore Room State') }</button>
<div className="mx_DevTools_label_left">{ this.state.mode.getLabel() }</div>
<div className="mx_DevTools_label_right">Room ID: { this.props.roomId }</div>
<div className="mx_DevTools_label_bottom" />
<this.state.mode onBack={this.onBack} />
</div>;
} else {
const classes = "mx_DevTools_RoomStateExplorer_button";
body = <div>
<div>
<div className="mx_DevTools_label_left">{ _t('Toolbox') }</div>
<div className="mx_DevTools_label_right">Room ID: { this.props.roomId }</div>
<div className="mx_DevTools_label_bottom" />
<div className="mx_Dialog_content">
{ Entries.map((Entry) => {
const label = Entry.getLabel();
const onClick = this._setMode(Entry);
return <button className={classes} key={label} onClick={onClick}>{ label }</button>;
}) }
</div>
</div>
<div className="mx_Dialog_buttons">
<button onClick={this.onCancel}>{ _t('Cancel') }</button>
@ -342,7 +587,6 @@ export default class DevtoolsDialog extends React.Component {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished} title={_t('Developer Tools')}>
<div>Room ID: { this.props.roomId }</div>
{ body }
</BaseDialog>
);

View file

@ -0,0 +1,33 @@
/*
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.
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.
*/
const React = require('react');
module.exports = React.createClass({
displayName: 'InlineSpinner',
render: function() {
var w = this.props.w || 16;
var h = this.props.h || 16;
var imgClass = this.props.imgClassName || "";
return (
<div className="mx_InlineSpinner">
<img src="img/spinner.gif" width={w} height={h} className={imgClass}/>
</div>
);
}
});