merge in develop

This commit is contained in:
Matthew Hodgson
2016-08-04 13:39:47 +01:00
90 changed files with 3640 additions and 1145 deletions

View File

@@ -18,6 +18,7 @@ limitations under the License.
var React = require('react');
var AvatarLogic = require("../../../Avatar");
import {emojifyText} from '../../../HtmlUtils';
module.exports = React.createClass({
displayName: 'BaseAvatar',
@@ -132,32 +133,36 @@ module.exports = React.createClass({
},
render: function() {
var name = this.props.name;
var imageUrl = this.state.imageUrls[this.state.urlsIndex];
const {
name, idName, title, url, urls, width, height, resizeMethod,
defaultToInitialLetter,
...otherProps
} = this.props;
if (imageUrl === this.state.defaultImageUrl) {
var initialLetter = this._getInitialLetter(this.props.name);
var initialLetter = emojifyText(this._getInitialLetter(name));
return (
<span className="mx_BaseAvatar" {...this.props}>
<span className="mx_BaseAvatar" {...otherProps}>
<span className="mx_BaseAvatar_initial" aria-hidden="true"
style={{ fontSize: (this.props.width * 0.65) + "px",
width: this.props.width + "px",
lineHeight: this.props.height + "px" }}>
{ initialLetter }
style={{ fontSize: (width * 0.65) + "px",
width: width + "px",
lineHeight: height + "px" }}
dangerouslySetInnerHTML={initialLetter}>
</span>
<img className="mx_BaseAvatar_image" src={imageUrl}
alt="" title={this.props.title} onError={this.onError}
width={this.props.width} height={this.props.height} />
alt="" title={title} onError={this.onError}
width={width} height={height} />
</span>
);
}
return (
<img className="mx_BaseAvatar mx_BaseAvatar_image" src={imageUrl}
onError={this.onError}
width={this.props.width} height={this.props.height}
title={this.props.title} alt=""
{...this.props} />
width={width} height={height}
title={title} alt=""
{...otherProps} />
);
}
});

View File

@@ -59,9 +59,12 @@ module.exports = React.createClass({
render: function() {
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
var {member, ...otherProps} = this.props;
return (
<BaseAvatar {...this.props} name={this.state.name} title={this.state.title}
idName={this.props.member.userId} url={this.state.imageUrl} />
<BaseAvatar {...otherProps} name={this.state.name} title={this.state.title}
idName={member.userId} url={this.state.imageUrl} />
);
}
});

View File

@@ -126,11 +126,13 @@ module.exports = React.createClass({
render: function() {
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
var roomName = this.props.room ? this.props.room.name : this.props.oobData.name;
var {room, oobData, ...otherProps} = this.props;
var roomName = room ? room.name : oobData.name;
return (
<BaseAvatar {...this.props} name={roomName}
idName={this.props.room ? this.props.room.roomId : null}
<BaseAvatar {...otherProps} name={roomName}
idName={room ? room.roomId : null}
urls={this.state.urls} />
);
}

View File

@@ -59,7 +59,7 @@ module.exports = React.createClass({
{this.props.description}
</div>
<div className="mx_Dialog_buttons">
<button onClick={this.props.onFinished} autoFocus={this.props.focus}>
<button className="mx_Dialog_primary" onClick={this.props.onFinished} autoFocus={this.props.focus}>
{this.props.button}
</button>
</div>

View File

@@ -46,7 +46,7 @@ module.exports = React.createClass({
Sign out?
</div>
<div className="mx_Dialog_buttons" onKeyDown={ this.onKeyDown }>
<button autoFocus onClick={this.logOut}>Sign Out</button>
<button className="mx_Dialog_primary" autoFocus onClick={this.logOut}>Sign Out</button>
<button onClick={this.cancelPrompt}>Cancel</button>
</div>
</div>

View File

@@ -63,7 +63,7 @@ module.exports = React.createClass({
{this.props.description}
</div>
<div className="mx_Dialog_buttons">
<button onClick={this.props.onFinished} autoFocus={true}>
<button className="mx_Dialog_primary" onClick={this.props.onFinished} autoFocus={true}>
Cancel
</button>
<button onClick={this.onRegisterClicked}>

View File

@@ -56,7 +56,7 @@ module.exports = React.createClass({
{this.props.description}
</div>
<div className="mx_Dialog_buttons">
<button onClick={this.onOk} autoFocus={this.props.focus}>
<button className="mx_Dialog_primary" onClick={this.onOk} autoFocus={this.props.focus}>
{this.props.button}
</button>

View File

@@ -76,7 +76,7 @@ module.exports = React.createClass({
/>
</div>
<div className="mx_Dialog_buttons">
<input type="submit" value="Set" />
<input className="mx_Dialog_primary" type="submit" value="Set" />
</div>
</form>
</div>

View File

@@ -86,7 +86,7 @@ module.exports = React.createClass({
<button onClick={this.onCancel}>
Cancel
</button>
<button onClick={this.onOk}>
<button className="mx_Dialog_primary" onClick={this.onOk}>
{this.props.button}
</button>
</div>

View File

@@ -49,6 +49,8 @@ module.exports = React.createClass({
label: '',
placeholder: '',
editable: true,
className: "mx_EditableText",
placeholderClassName: "mx_EditableText_placeholder",
};
},
@@ -92,7 +94,7 @@ module.exports = React.createClass({
this.refs.editable_div.textContent = this.value;
this.refs.editable_div.setAttribute("class", this.props.className);
this.placeholder = false;
}
}
},
getValue: function() {
@@ -101,7 +103,7 @@ module.exports = React.createClass({
setValue: function(value) {
this.value = value;
this.showPlaceholder(!this.value);
this.showPlaceholder(!this.value);
},
edit: function() {
@@ -125,7 +127,7 @@ module.exports = React.createClass({
onKeyDown: function(ev) {
// console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
if (this.placeholder) {
this.showPlaceholder(false);
}
@@ -173,7 +175,7 @@ module.exports = React.createClass({
var range = document.createRange();
range.setStart(node, 0);
range.setEnd(node, node.length);
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);

View File

@@ -0,0 +1,147 @@
/*
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.
*/
import React from 'react';
import sdk from '../../../index';
import q from 'q';
/**
* A component which wraps an EditableText, with a spinner while updates take
* place.
*
* Parent components should supply an 'onSubmit' callback which returns a
* promise; a spinner is shown until the promise resolves.
*
* The parent can also supply a 'getIntialValue' callback, which works in a
* similarly asynchronous way. If this is not provided, the initial value is
* taken from the 'initialValue' property.
*/
export default class EditableTextContainer extends React.Component {
constructor(props, context) {
super(props, context);
this._unmounted = false;
this.state = {
busy: false,
errorString: null,
value: props.initialValue,
};
this._onValueChanged = this._onValueChanged.bind(this);
}
componentWillMount() {
if (this.props.getInitialValue === undefined) {
// use whatever was given in the initialValue property.
return;
}
this.setState({busy: true});
this.props.getInitialValue().done(
(result) => {
if (this._unmounted) { return; }
this.setState({
busy: false,
value: result,
});
},
(error) => {
if (this._unmounted) { return; }
this.setState({
errorString: error.toString(),
busy: false,
});
}
);
}
componentWillUnmount() {
this._unmounted = true;
}
_onValueChanged(value, shouldSubmit) {
if (!shouldSubmit) {
return;
}
this.setState({
busy: true,
errorString: null,
});
this.props.onSubmit(value).done(
() => {
if (this._unmounted) { return; }
this.setState({
busy: false,
value: value,
});
},
(error) => {
if (this._unmounted) { return; }
this.setState({
errorString: error.toString(),
busy: false,
});
}
);
}
render() {
if (this.state.busy) {
var Loader = sdk.getComponent("elements.Spinner");
return (
<Loader />
);
} else if (this.state.errorString) {
return (
<div className="error">{this.state.errorString}</div>
);
} else {
var EditableText = sdk.getComponent('elements.EditableText');
return (
<EditableText initialValue={this.state.value}
placeholder={this.props.placeholder}
onValueChanged={this._onValueChanged}
/>
);
}
}
}
EditableTextContainer.propTypes = {
/* callback to retrieve the initial value. */
getInitialValue: React.PropTypes.func,
/* initial value; used if getInitialValue is not given */
initialValue: React.PropTypes.string,
/* placeholder text to use when the value is empty (and not being
* edited) */
placeholder: React.PropTypes.string,
/* callback to update the value. Called with a single argument: the new
* value. */
onSubmit: React.PropTypes.func,
};
EditableTextContainer.defaultProps = {
initialValue: "",
placeholder: "",
onSubmit: function(v) {return q(); },
};

View File

@@ -34,10 +34,15 @@ module.exports = React.createClass({
propTypes: {
value: React.PropTypes.number.isRequired,
// if true, the <select/> should be a 'controlled' form element and updated by React
// to reflect the current value, rather than left freeform.
// MemberInfo uses controlled; RoomSettings uses non-controlled.
controlled: React.PropTypes.bool.isRequired,
//
// ignored if disabled is truthy. false by default.
controlled: React.PropTypes.bool,
// should the user be able to change the value? false by default.
disabled: React.PropTypes.bool,
onChange: React.PropTypes.func,
},

View File

@@ -35,8 +35,16 @@ module.exports = React.createClass({
displayName: 'RegistrationForm',
propTypes: {
// Values pre-filled in the input boxes when the component loads
defaultEmail: React.PropTypes.string,
defaultUsername: React.PropTypes.string,
defaultPassword: React.PropTypes.string,
// A username that will be used if no username is entered.
// Specifying this param will also warn the user that entering
// a different username will cause a fresh account to be generated.
guestUsername: React.PropTypes.string,
showEmail: React.PropTypes.bool,
minPasswordLength: React.PropTypes.number,
onError: React.PropTypes.func,
@@ -55,10 +63,6 @@ module.exports = React.createClass({
getInitialState: function() {
return {
email: this.props.defaultEmail,
username: null,
password: null,
passwordConfirm: null,
fieldValid: {}
};
},
@@ -103,7 +107,7 @@ module.exports = React.createClass({
_doSubmit: function() {
var promise = this.props.onRegisterClick({
username: this.refs.username.value.trim() || this.props.defaultUsername,
username: this.refs.username.value.trim() || this.props.guestUsername,
password: this.refs.password.value.trim(),
email: this.refs.email.value.trim()
});
@@ -144,7 +148,7 @@ module.exports = React.createClass({
break;
case FIELD_USERNAME:
// XXX: SPEC-1
var username = this.refs.username.value.trim() || this.props.defaultUsername;
var username = this.refs.username.value.trim() || this.props.guestUsername;
if (encodeURIComponent(username) != username) {
this.markFieldValid(
field_id,
@@ -225,7 +229,7 @@ module.exports = React.createClass({
emailSection = (
<input className="mx_Login_field" type="text" ref="email"
autoFocus={true} placeholder="Email address (optional)"
defaultValue={this.state.email}
defaultValue={this.props.defaultEmail}
style={this._styleField(FIELD_EMAIL)}
onBlur={function() {self.validateField(FIELD_EMAIL)}} />
);
@@ -237,8 +241,8 @@ module.exports = React.createClass({
}
var placeholderUserName = "User name";
if (this.props.defaultUsername) {
placeholderUserName += " (default: " + this.props.defaultUsername + ")"
if (this.props.guestUsername) {
placeholderUserName += " (default: " + this.props.guestUsername + ")"
}
return (
@@ -247,23 +251,23 @@ module.exports = React.createClass({
{emailSection}
<br />
<input className="mx_Login_field" type="text" ref="username"
placeholder={ placeholderUserName } defaultValue={this.state.username}
placeholder={ placeholderUserName } defaultValue={this.props.defaultUsername}
style={this._styleField(FIELD_USERNAME)}
onBlur={function() {self.validateField(FIELD_USERNAME)}} />
<br />
{ this.props.defaultUsername ?
{ this.props.guestUsername ?
<div className="mx_Login_fieldLabel">Setting a user name will create a fresh account</div> : null
}
<input className="mx_Login_field" type="password" ref="password"
style={this._styleField(FIELD_PASSWORD)}
onBlur={function() {self.validateField(FIELD_PASSWORD)}}
placeholder="Password" defaultValue={this.state.password} />
placeholder="Password" defaultValue={this.props.defaultPassword} />
<br />
<input className="mx_Login_field" type="password" ref="passwordConfirm"
placeholder="Confirm password"
style={this._styleField(FIELD_PASSWORD_CONFIRM)}
onBlur={function() {self.validateField(FIELD_PASSWORD_CONFIRM)}}
defaultValue={this.state.passwordConfirm} />
defaultValue={this.props.defaultPassword} />
<br />
{registerButton}
</form>

View File

@@ -34,7 +34,7 @@ module.exports = React.createClass({
}
if (fullWidth < thumbWidth && fullHeight < thumbHeight) {
// no scaling needs to be applied
return fullHeight;
return 1;
}
var widthMulti = thumbWidth / fullWidth;
var heightMulti = thumbHeight / fullHeight;

View File

@@ -38,6 +38,9 @@ module.exports = React.createClass({
/* link URL for the highlights */
highlightLink: React.PropTypes.string,
/* should show URL previews for this event */
showUrlPreview: React.PropTypes.bool,
/* callback called when dynamic content in events are loaded */
onWidgetLoad: React.PropTypes.func,
},
@@ -71,6 +74,7 @@ module.exports = React.createClass({
return <BodyType ref="body" mxEvent={this.props.mxEvent} highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview}
onWidgetLoad={this.props.onWidgetLoad} />;
},
});

View File

@@ -39,6 +39,9 @@ module.exports = React.createClass({
/* link URL for the highlights */
highlightLink: React.PropTypes.string,
/* should show URL previews for this event */
showUrlPreview: React.PropTypes.bool,
/* callback for when our widget has loaded */
onWidgetLoad: React.PropTypes.func,
},
@@ -56,34 +59,47 @@ module.exports = React.createClass({
componentDidMount: function() {
linkifyElement(this.refs.content, linkifyMatrix.options);
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) {
var hidden = global.localStorage.getItem("hide_preview_" + this.props.mxEvent.getId());
this.setState({ widgetHidden: hidden });
}
}
this.calculateUrlPreview();
if (this.props.mxEvent.getContent().format === "org.matrix.custom.html")
HtmlUtils.highlightDom(ReactDOM.findDOMNode(this));
},
componentDidUpdate: function() {
this.calculateUrlPreview();
},
shouldComponentUpdate: function(nextProps, nextState) {
//console.log("shouldComponentUpdate: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
// 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 ||
nextProps.showUrlPreview !== this.props.showUrlPreview ||
nextState.links !== this.state.links ||
nextState.widgetHidden !== this.state.widgetHidden);
},
calculateUrlPreview: function() {
//console.log("calculateUrlPreview: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
if (this.props.showUrlPreview && !this.state.links.length) {
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) {
var hidden = global.localStorage.getItem("hide_preview_" + this.props.mxEvent.getId());
this.setState({ widgetHidden: hidden });
}
}
}
},
findLinks: function(nodes) {
var links = [];
for (var i = 0; i < nodes.length; i++) {
@@ -163,12 +179,14 @@ module.exports = React.createClass({
render: function() {
var mxEvent = this.props.mxEvent;
var content = mxEvent.getContent();
var body = HtmlUtils.bodyToHtml(content, this.props.highlights,
{highlightLink: this.props.highlightLink});
var body = HtmlUtils.bodyToHtml(content, this.props.highlights, {});
if (this.props.highlightLink) {
body = <a href={ this.props.highlightLink }>{ body }</a>;
}
var widgets;
if (this.state.links.length && !this.state.widgetHidden) {
if (this.state.links.length && !this.state.widgetHidden && this.props.showUrlPreview) {
var LinkPreviewWidget = sdk.getComponent('rooms.LinkPreviewWidget');
widgets = this.state.links.map((link)=>{
return <LinkPreviewWidget

View File

@@ -19,6 +19,7 @@ limitations under the License.
var React = require('react');
var TextForEvent = require('../../../TextForEvent');
import {emojifyText} from '../../../HtmlUtils';
module.exports = React.createClass({
displayName: 'TextualEvent',
@@ -31,11 +32,11 @@ module.exports = React.createClass({
render: function() {
var text = TextForEvent.textForEvent(this.props.mxEvent);
if (text == null || text.length == 0) return null;
if (text == null || text.length === 0) return null;
let textHTML = emojifyText(TextForEvent.textForEvent(this.props.mxEvent));
return (
<div className="mx_TextualEvent">
{TextForEvent.textForEvent(this.props.mxEvent)}
<div className="mx_TextualEvent" dangerouslySetInnerHTML={textHTML}>
</div>
);
},

View File

@@ -83,13 +83,11 @@ module.exports = React.createClass({
alias: this.state.canonicalAlias
}, ""
)
);
);
}
// save new aliases for m.room.aliases
var aliasOperations = this.getAliasOperations();
var promises = [];
for (var i = 0; i < aliasOperations.length; i++) {
var alias_operation = aliasOperations[i];
console.log("alias %s %s", alias_operation.place, alias_operation.val);
@@ -301,7 +299,7 @@ module.exports = React.createClass({
<div className="mx_RoomSettings_addAlias">
<img src="img/plus.svg" width="14" height="14" alt="Add"
onClick={ self.onAliasAdded.bind(self, undefined) }/>
</div>
</div>
</div> : ""
}
</div>

View File

@@ -57,7 +57,7 @@ module.exports = React.createClass({
data.primary_color = scheme.primary_color;
data.secondary_color = scheme.secondary_color;
data.index = this._getColorIndex(data);
if (data.index === -1) {
// append the unrecognised colours to our palette
data.index = ROOM_COLORS.length;

View File

@@ -0,0 +1,157 @@
/*
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 q = require("q");
var React = require('react');
var MatrixClientPeg = require('../../../MatrixClientPeg');
var sdk = require("../../../index");
var Modal = require("../../../Modal");
var UserSettingsStore = require('../../../UserSettingsStore');
module.exports = React.createClass({
displayName: 'UrlPreviewSettings',
propTypes: {
room: React.PropTypes.object,
},
getInitialState: function() {
var cli = MatrixClientPeg.get();
var roomState = this.props.room.currentState;
var roomPreviewUrls = this.props.room.currentState.getStateEvents('org.matrix.room.preview_urls', '');
var userPreviewUrls = this.props.room.getAccountData("org.matrix.room.preview_urls");
return {
globalDisableUrlPreview: (roomPreviewUrls && roomPreviewUrls.getContent().disable) || false,
userDisableUrlPreview: (userPreviewUrls && (userPreviewUrls.getContent().disable === true)) || false,
userEnableUrlPreview: (userPreviewUrls && (userPreviewUrls.getContent().disable === false)) || false,
};
},
componentDidMount: function() {
this.originalState = Object.assign({}, this.state);
},
saveSettings: function() {
var promises = [];
if (this.state.globalDisableUrlPreview !== this.originalState.globalDisableUrlPreview) {
console.log("UrlPreviewSettings: Updating room's preview_urls state event");
promises.push(
MatrixClientPeg.get().sendStateEvent(
this.props.room.roomId, "org.matrix.room.preview_urls", {
disable: this.state.globalDisableUrlPreview
}, ""
)
);
}
var content = undefined;
if (this.state.userDisableUrlPreview !== this.originalState.userDisableUrlPreview) {
console.log("UrlPreviewSettings: Disabling user's per-room preview_urls");
content = this.state.userDisableUrlPreview ? { disable : true } : {};
}
if (this.state.userEnableUrlPreview !== this.originalState.userEnableUrlPreview) {
console.log("UrlPreviewSettings: Enabling user's per-room preview_urls");
if (!content || content.disable === undefined) {
content = this.state.userEnableUrlPreview ? { disable : false } : {};
}
}
if (content) {
promises.push(
MatrixClientPeg.get().setRoomAccountData(
this.props.room.roomId, "org.matrix.room.preview_urls", content
)
);
}
console.log("UrlPreviewSettings: saveSettings: " + JSON.stringify(promises));
return promises;
},
onGlobalDisableUrlPreviewChange: function() {
this.setState({
globalDisableUrlPreview: this.refs.globalDisableUrlPreview.checked ? true : false,
});
},
onUserEnableUrlPreviewChange: function() {
this.setState({
userDisableUrlPreview: false,
userEnableUrlPreview: this.refs.userEnableUrlPreview.checked ? true : false,
});
},
onUserDisableUrlPreviewChange: function() {
this.setState({
userDisableUrlPreview: this.refs.userDisableUrlPreview.checked ? true : false,
userEnableUrlPreview: false,
});
},
render: function() {
var self = this;
var roomState = this.props.room.currentState;
var cli = MatrixClientPeg.get();
var maySetRoomPreviewUrls = roomState.mayClientSendStateEvent('org.matrix.room.preview_urls', cli);
var disableRoomPreviewUrls;
if (maySetRoomPreviewUrls) {
disableRoomPreviewUrls =
<label>
<input type="checkbox" ref="globalDisableUrlPreview"
onChange={ this.onGlobalDisableUrlPreviewChange }
checked={ this.state.globalDisableUrlPreview } />
Disable URL previews by default for participants in this room
</label>
}
else {
disableRoomPreviewUrls =
<label>
URL previews are { this.state.globalDisableUrlPreview ? "disabled" : "enabled" } by default for participants in this room.
</label>
}
return (
<div className="mx_RoomSettings_toggles">
<h3>URL Previews</h3>
<label>
You have <a href="#/settings">{ UserSettingsStore.getUrlPreviewsDisabled() ? 'disabled' : 'enabled' }</a> URL previews by default.
</label>
{ disableRoomPreviewUrls }
<label>
<input type="checkbox" ref="userEnableUrlPreview"
onChange={ this.onUserEnableUrlPreviewChange }
checked={ this.state.userEnableUrlPreview } />
Enable URL previews for this room (affects only you)
</label>
<label>
<input type="checkbox" ref="userDisableUrlPreview"
onChange={ this.onUserDisableUrlPreviewChange }
checked={ this.state.userDisableUrlPreview } />
Disable URL previews for this room (affects only you)
</label>
</div>
);
}
});

View File

@@ -0,0 +1,164 @@
import React from 'react';
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
import classNames from 'classnames';
import flatMap from 'lodash/flatMap';
import {getCompletions} from '../../../autocomplete/Autocompleter';
export default class Autocomplete extends React.Component {
constructor(props) {
super(props);
this.onConfirm = this.onConfirm.bind(this);
this.state = {
// list of completionResults, each containing completions
completions: [],
// array of completions, so we can look up current selection by offset quickly
completionList: [],
// how far down the completion list we are
selectionOffset: 0,
};
}
componentWillReceiveProps(props, state) {
if (props.query === this.props.query) {
return;
}
getCompletions(props.query, props.selection).forEach(completionResult => {
try {
completionResult.completions.then(completions => {
let i = this.state.completions.findIndex(
completion => completion.provider === completionResult.provider
);
i = i === -1 ? this.state.completions.length : i;
let newCompletions = Object.assign([], this.state.completions);
completionResult.completions = completions;
newCompletions[i] = completionResult;
this.setState({
completions: newCompletions,
completionList: flatMap(newCompletions, provider => provider.completions),
});
}, err => {
console.error(err);
});
} catch (e) {
// An error in one provider shouldn't mess up the rest.
console.error(e);
}
});
}
countCompletions(): number {
return this.state.completions.map(completionResult => {
return completionResult.completions.length;
}).reduce((l, r) => l + r);
}
// called from MessageComposerInput
onUpArrow(): boolean {
let completionCount = this.countCompletions(),
selectionOffset = (completionCount + this.state.selectionOffset - 1) % completionCount;
if (!completionCount) {
return false;
}
this.setSelection(selectionOffset);
return true;
}
// called from MessageComposerInput
onDownArrow(): boolean {
let completionCount = this.countCompletions(),
selectionOffset = (this.state.selectionOffset + 1) % completionCount;
if (!completionCount) {
return false;
}
this.setSelection(selectionOffset);
return true;
}
/** called from MessageComposerInput
* @returns {boolean} whether confirmation was handled
*/
onConfirm(): boolean {
if (this.countCompletions() === 0) {
return false;
}
let selectedCompletion = this.state.completionList[this.state.selectionOffset];
this.props.onConfirm(selectedCompletion.range, selectedCompletion.completion);
return true;
}
setSelection(selectionOffset: number) {
this.setState({selectionOffset});
}
render() {
let position = 0;
let renderedCompletions = this.state.completions.map((completionResult, i) => {
let completions = completionResult.completions.map((completion, i) => {
let className = classNames('mx_Autocomplete_Completion', {
'selected': position === this.state.selectionOffset,
});
let componentPosition = position;
position++;
let onMouseOver = () => this.setSelection(componentPosition);
let onClick = () => {
this.setSelection(componentPosition);
this.onConfirm();
};
return (
<div key={i}
className={className}
onMouseOver={onMouseOver}
onClick={onClick}>
{completion.component}
</div>
);
});
return completions.length > 0 ? (
<div key={i} className="mx_Autocomplete_ProviderSection">
<span className="mx_Autocomplete_provider_name">{completionResult.provider.getName()}</span>
<ReactCSSTransitionGroup
component="div"
transitionName="autocomplete"
transitionEnterTimeout={300}
transitionLeaveTimeout={300}>
{completions}
</ReactCSSTransitionGroup>
</div>
) : null;
});
return (
<div className="mx_Autocomplete">
<ReactCSSTransitionGroup
component="div"
transitionName="autocomplete"
transitionEnterTimeout={300}
transitionLeaveTimeout={300}>
{renderedCompletions}
</ReactCSSTransitionGroup>
</div>
);
}
}
Autocomplete.propTypes = {
// the query string for which to show autocomplete suggestions
query: React.PropTypes.string.isRequired,
// method invoked with range and text content when completion is confirmed
onConfirm: React.PropTypes.func.isRequired,
};

View File

@@ -20,6 +20,7 @@ var React = require('react');
var MatrixClientPeg = require('../../../MatrixClientPeg');
var sdk = require('../../../index');
import {emojifyText} from '../../../HtmlUtils';
var PRESENCE_CLASS = {
@@ -28,6 +29,23 @@ var PRESENCE_CLASS = {
"unavailable": "mx_EntityTile_unavailable"
};
function presenceClassForMember(presenceState, lastActiveAgo) {
// offline is split into two categories depending on whether we have
// a last_active_ago for them.
if (presenceState == 'offline') {
if (lastActiveAgo) {
return PRESENCE_CLASS['offline'] + '_beenactive';
} else {
return PRESENCE_CLASS['offline'] + '_neveractive';
}
} else if (presenceState) {
return PRESENCE_CLASS[presenceState];
} else {
return PRESENCE_CLASS['offline'] + '_neveractive';
}
}
module.exports = React.createClass({
displayName: 'EntityTile',
@@ -78,10 +96,14 @@ module.exports = React.createClass({
},
render: function() {
var presenceClass = PRESENCE_CLASS[this.props.presenceState] || "mx_EntityTile_offline";
const presenceClass = presenceClassForMember(
this.props.presenceState, this.props.presenceLastActiveAgo
);
var mainClassName = "mx_EntityTile ";
mainClassName += presenceClass + (this.props.className ? (" " + this.props.className) : "");
var nameEl;
let nameHTML = emojifyText(this.props.name);
if (this.state.hover && !this.props.suppressOnHover) {
var activeAgo = this.props.presenceLastActiveAgo ?
@@ -92,7 +114,7 @@ module.exports = React.createClass({
nameEl = (
<div className="mx_EntityTile_details">
<img className="mx_EntityTile_chevron" src="img/member_chevron.png" width="8" height="12"/>
<div className="mx_EntityTile_name_hover">{ this.props.name }</div>
<div className="mx_EntityTile_name_hover" dangerouslySetInnerHTML={nameHTML}></div>
<PresenceLabel activeAgo={ activeAgo }
currentlyActive={this.props.presenceCurrentlyActive}
presenceState={this.props.presenceState} />
@@ -101,8 +123,7 @@ module.exports = React.createClass({
}
else {
nameEl = (
<div className="mx_EntityTile_name">
{ this.props.name }
<div className="mx_EntityTile_name" dangerouslySetInnerHTML={nameHTML}>
</div>
);
}

View File

@@ -23,7 +23,7 @@ var sdk = require('../../../index');
var MatrixClientPeg = require('../../../MatrixClientPeg')
var TextForEvent = require('../../../TextForEvent');
var ContextualMenu = require('../../../ContextualMenu');
var ContextualMenu = require('../../structures/ContextualMenu');
var dispatcher = require("../../../dispatcher");
var ObjectUtils = require('../../../ObjectUtils');
@@ -101,6 +101,9 @@ module.exports = React.createClass({
/* link URL for the highlights */
highlightLink: React.PropTypes.string,
/* should show URL previews for this event */
showUrlPreview: React.PropTypes.bool,
/* is this the focused event */
isSelectedEvent: React.PropTypes.bool,
@@ -139,7 +142,8 @@ module.exports = React.createClass({
componentDidMount: function() {
this._suppressReadReceiptAnimation = false;
MatrixClientPeg.get().on("deviceVerified", this.onDeviceVerified);
MatrixClientPeg.get().on("deviceVerificationChanged",
this.onDeviceVerificationChanged);
},
componentWillReceiveProps: function (nextProps) {
@@ -163,11 +167,12 @@ module.exports = React.createClass({
componentWillUnmount: function() {
var client = MatrixClientPeg.get();
if (client) {
client.removeListener("deviceVerified", this.onDeviceVerified);
client.removeListener("deviceVerificationChanged",
this.onDeviceVerificationChanged);
}
},
onDeviceVerified: function(userId, device) {
onDeviceVerificationChanged: function(userId, device) {
if (userId == this.props.mxEvent.getSender()) {
this._verifyEvent(this.props.mxEvent);
}
@@ -244,12 +249,15 @@ module.exports = React.createClass({
},
onEditClicked: function(e) {
var MessageContextMenu = sdk.getComponent('rooms.MessageContextMenu');
var MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu');
var buttonRect = e.target.getBoundingClientRect()
var x = buttonRect.right;
var y = buttonRect.top + (e.target.height / 2);
// The window X and Y offsets are to adjust position when zoomed in to page
var x = buttonRect.right + window.pageXOffset;
var y = (buttonRect.top + (e.target.height / 2) + window.pageYOffset) - 19;
var self = this;
ContextualMenu.createMenu(MessageContextMenu, {
chevronOffset: 10,
mxEvent: this.props.mxEvent,
left: x,
top: y,
@@ -357,6 +365,8 @@ module.exports = React.createClass({
var SenderProfile = sdk.getComponent('messages.SenderProfile');
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
//console.log("EventTile showUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
var content = this.props.mxEvent.getContent();
var msgtype = content.msgtype;
@@ -418,6 +428,7 @@ module.exports = React.createClass({
<div className="mx_EventTile_line">
<EventTileType ref="tile" mxEvent={this.props.mxEvent} highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview}
onWidgetLoad={this.props.onWidgetLoad} />
</div>
</div>

View File

@@ -37,17 +37,14 @@ module.exports = React.createClass({
},
componentWillMount: function() {
this._room = MatrixClientPeg.get().getRoom(this.props.roomId);
var cli = MatrixClientPeg.get();
cli.on("RoomState.members", this.onRoomStateMember);
this._emailEntity = null;
// Load the complete user list for inviting new users
// TODO: Keep this list bleeding-edge up-to-date. Practically speaking,
// it will do for now not being updated as random new users join different
// rooms as this list will be reloaded every room swap.
if (this._room) {
this._userList = MatrixClientPeg.get().getUsers().filter((u) => {
return !this._room.hasMembershipState(u.userId, "join");
});
}
// we have to update the list whenever membership changes
// particularly to avoid bug https://github.com/vector-im/vector-web/issues/1813
this._updateList();
},
componentDidMount: function() {
@@ -55,6 +52,28 @@ module.exports = React.createClass({
this.onSearchQueryChanged('');
},
componentWillUnmount: function() {
var cli = MatrixClientPeg.get();
if (cli) {
cli.removeListener("RoomState.members", this.onRoomStateMember);
}
},
_updateList: function() {
this._room = MatrixClientPeg.get().getRoom(this.props.roomId);
// Load the complete user list for inviting new users
if (this._room) {
this._userList = MatrixClientPeg.get().getUsers().filter((u) => {
return (!this._room.hasMembershipState(u.userId, "join") &&
!this._room.hasMembershipState(u.userId, "invite"));
});
}
},
onRoomStateMember: function(ev, state, member) {
this._updateList();
},
onInvite: function(ev) {
this.props.onInvite(this._input);
},

View File

@@ -36,32 +36,75 @@ module.exports = React.createClass({
);
},
onBlockClick: function() {
MatrixClientPeg.get().setDeviceBlocked(
this.props.userId, this.props.device.id, true
);
},
onUnblockClick: function() {
MatrixClientPeg.get().setDeviceBlocked(
this.props.userId, this.props.device.id, false
);
},
render: function() {
var indicator = null, button = null;
if (this.props.device.verified) {
indicator = (
<div className="mx_MemberDeviceInfo_verified">&#x2714;</div>
var indicator = null, blockButton = null, verifyButton = null;
if (this.props.device.blocked) {
blockButton = (
<div className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unblock"
onClick={this.onUnblockClick}>
Unblock
</div>
);
button = (
} else {
blockButton = (
<div className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_block"
onClick={this.onBlockClick}>
Block
</div>
);
}
if (this.props.device.verified) {
verifyButton = (
<div className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unverify"
onClick={this.onUnverifyClick}>
Unverify
</div>
);
} else {
button = (
verifyButton = (
<div className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_verify"
onClick={this.onVerifyClick}>
Verify
</div>
);
}
if (this.props.device.blocked) {
indicator = (
<div className="mx_MemberDeviceInfo_blocked">&#x2716;</div>
);
} else if (this.props.device.verified) {
indicator = (
<div className="mx_MemberDeviceInfo_verified">&#x2714;</div>
);
} else {
indicator = (
<div className="mx_MemberDeviceInfo_unverified">?</div>
);
}
var deviceName = this.props.device.display_name || this.props.device.id;
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>
<div className="mx_MemberDeviceInfo_deviceId">{deviceName}</div>
{indicator}
{button}
{verifyButton}
{blockButton}
</div>
);
},

View File

@@ -32,6 +32,7 @@ var Modal = require("../../../Modal");
var sdk = require('../../../index');
var UserSettingsStore = require('../../../UserSettingsStore');
var createRoom = require('../../../createRoom');
import {emojifyText} from '../../../HtmlUtils';
module.exports = React.createClass({
displayName: 'MemberInfo',
@@ -60,17 +61,21 @@ module.exports = React.createClass({
updating: 0,
devicesLoading: true,
devices: null,
existingOneToOneRoomId: null,
}
},
componentWillMount: function() {
this._cancelDeviceList = null;
this.setState({
existingOneToOneRoomId: this.getExistingOneToOneRoomId()
});
},
componentDidMount: function() {
this._updateStateForNewMember(this.props.member);
MatrixClientPeg.get().on("deviceVerified", this.onDeviceVerified);
MatrixClientPeg.get().on("deviceVerificationChanged", this.onDeviceVerificationChanged);
},
componentWillReceiveProps: function(newProps) {
@@ -82,14 +87,67 @@ module.exports = React.createClass({
componentWillUnmount: function() {
var client = MatrixClientPeg.get();
if (client) {
client.removeListener("deviceVerified", this.onDeviceVerified);
client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
}
if (this._cancelDeviceList) {
this._cancelDeviceList();
}
},
onDeviceVerified: function(userId, device) {
getExistingOneToOneRoomId: function() {
const rooms = MatrixClientPeg.get().getRooms();
const userIds = [
this.props.member.userId,
MatrixClientPeg.get().credentials.userId
];
let existingRoomId = null;
let invitedRoomId = null;
// roomId can be null here because of a hack in MatrixChat.onUserClick where we
// abuse this to view users rather than room members.
let currentMembers;
if (this.props.member.roomId) {
const currentRoom = MatrixClientPeg.get().getRoom(this.props.member.roomId);
currentMembers = currentRoom.getJoinedMembers();
}
// reuse the first private 1:1 we find
existingRoomId = null;
for (let i = 0; i < rooms.length; i++) {
// don't try to reuse public 1:1 rooms
const join_rules = rooms[i].currentState.getStateEvents("m.room.join_rules", '');
if (join_rules && join_rules.getContent().join_rule === 'public') continue;
const members = rooms[i].getJoinedMembers();
if (members.length === 2 &&
userIds.indexOf(members[0].userId) !== -1 &&
userIds.indexOf(members[1].userId) !== -1)
{
existingRoomId = rooms[i].roomId;
break;
}
const invited = rooms[i].getMembersWithMembership('invite');
if (members.length === 1 &&
invited.length === 1 &&
userIds.indexOf(members[0].userId) !== -1 &&
userIds.indexOf(invited[0].userId) !== -1 &&
invitedRoomId === null)
{
invitedRoomId = rooms[i].roomId;
// keep looking: we'll use this one if there's nothing better
}
}
if (existingRoomId === null) {
existingRoomId = invitedRoomId;
}
return existingRoomId;
},
onDeviceVerificationChanged: function(userId, device) {
if (userId == this.props.member.userId) {
// no need to re-download the whole thing; just update our copy of
// the list.
@@ -348,61 +406,29 @@ module.exports = React.createClass({
onChatClick: function() {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
// TODO: keep existingOneToOneRoomId updated if we see any room member changes anywhere
const useExistingOneToOneRoom = this.state.existingOneToOneRoomId && (this.state.existingOneToOneRoomId !== this.props.member.roomId);
// check if there are any existing rooms with just us and them (1:1)
// If so, just view that room. If not, create a private room with them.
var self = this;
var rooms = MatrixClientPeg.get().getRooms();
var userIds = [
this.props.member.userId,
MatrixClientPeg.get().credentials.userId
];
var existingRoomId;
var currentRoom = MatrixClientPeg.get().getRoom(this.props.member.roomId);
var currentMembers = currentRoom.getJoinedMembers();
// if we're currently in a 1:1 with this user, start a new chat
if (currentMembers.length === 2 &&
userIds.indexOf(currentMembers[0].userId) !== -1 &&
userIds.indexOf(currentMembers[1].userId) !== -1)
{
existingRoomId = null;
}
// otherwise reuse the first private 1:1 we find
else {
existingRoomId = null;
for (var i = 0; i < rooms.length; i++) {
// don't try to reuse public 1:1 rooms
var join_rules = rooms[i].currentState.getStateEvents("m.room.join_rules", '');
if (join_rules && join_rules.getContent().join_rule === 'public') continue;
var members = rooms[i].getJoinedMembers();
if (members.length === 2 &&
userIds.indexOf(members[0].userId) !== -1 &&
userIds.indexOf(members[1].userId) !== -1)
{
existingRoomId = rooms[i].roomId;
break;
}
}
}
if (existingRoomId) {
if (useExistingOneToOneRoom) {
dis.dispatch({
action: 'view_room',
room_id: existingRoomId
room_id: this.state.existingOneToOneRoomId,
});
this.props.onFinished();
}
else {
self.setState({ updating: self.state.updating + 1 });
this.setState({ updating: this.state.updating + 1 });
createRoom({
createOpts: {
invite: [this.props.member.userId],
},
}).finally(function() {
self.props.onFinished();
self.setState({ updating: self.state.updating - 1 });
}).finally(() => {
this.props.onFinished();
this.setState({ updating: this.state.updating - 1 });
}).done();
}
},
@@ -535,7 +561,9 @@ module.exports = React.createClass({
return (
<div>
<h3>Devices</h3>
{devComponents}
<div className="mx_MemberInfo_devices">
{devComponents}
</div>
</div>
);
},
@@ -545,7 +573,22 @@ module.exports = React.createClass({
if (this.props.member.userId !== MatrixClientPeg.get().credentials.userId) {
// FIXME: we're referring to a vector component from react-sdk
var BottomLeftMenuTile = sdk.getComponent('rooms.BottomLeftMenuTile');
startChat = <BottomLeftMenuTile collapsed={ false } img="img/create-big.svg" label="Start chat" onClick={ this.onChatClick }/>
var label;
if (this.state.existingOneToOneRoomId) {
if (this.state.existingOneToOneRoomId == this.props.member.roomId) {
label = "Start new direct chat";
}
else {
label = "Go to direct chat";
}
}
else {
label = "Start direct chat";
}
startChat = <BottomLeftMenuTile collapsed={ false } img="img/create-big.svg"
label={ label } onClick={ this.onChatClick }/>
}
if (this.state.updating) {
@@ -594,6 +637,8 @@ module.exports = React.createClass({
</div>
}
let memberNameHTML = emojifyText(this.props.member.name);
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
var PowerSelector = sdk.getComponent('elements.PowerSelector');
return (
@@ -603,7 +648,7 @@ module.exports = React.createClass({
<MemberAvatar onClick={this.onMemberAvatarClick} member={this.props.member} width={48} height={48} />
</div>
<h2>{ this.props.member.name }</h2>
<h2 dangerouslySetInnerHTML={memberNameHTML}></h2>
<div className="mx_MemberInfo_profile">
<div className="mx_MemberInfo_profileField">

View File

@@ -54,7 +54,7 @@ module.exports = React.createClass({
this.memberDict = this.getMemberDict();
state.members = this.roomMembers(INITIAL_LOAD_NUM_MEMBERS);
state.members = this.roomMembers();
return state;
},
@@ -64,7 +64,10 @@ module.exports = React.createClass({
cli.on("RoomMember.name", this.onRoomMemberName);
cli.on("RoomState.events", this.onRoomStateEvent);
cli.on("Room", this.onRoom); // invites
cli.on("User.presence", this.onUserPresence);
// We listen for changes to the lastPresenceTs which is essentially
// listening for all presence events (we display most of not all of
// the information contained in presence events).
cli.on("User.lastPresenceTs", this.onUserLastPresenceTs);
// cli.on("Room.timeline", this.onRoomTimeline);
},
@@ -75,24 +78,11 @@ module.exports = React.createClass({
cli.removeListener("RoomMember.name", this.onRoomMemberName);
cli.removeListener("RoomState.events", this.onRoomStateEvent);
cli.removeListener("Room", this.onRoom);
cli.removeListener("User.presence", this.onUserPresence);
cli.removeListener("User.lastPresenceTs", this.onUserLastPresenceTs);
// cli.removeListener("Room.timeline", this.onRoomTimeline);
}
},
componentDidMount: function() {
var self = this;
// Lazy-load in more than the first N members
setTimeout(function() {
if (!self.isMounted()) return;
// lazy load to prevent it blocking the first render
self.setState({
members: self.roomMembers()
});
}, 50);
},
/*
onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) {
// ignore anything but real-time updates at the end of the room:
@@ -121,7 +111,7 @@ module.exports = React.createClass({
},
*/
onUserPresence(event, user) {
onUserLastPresenceTs(event, user) {
// Attach a SINGLE listener for global presence changes then locate the
// member tile and re-render it. This is more efficient than every tile
// evar attaching their own listener.
@@ -325,7 +315,7 @@ module.exports = React.createClass({
return all_members;
},
roomMembers: function(limit) {
roomMembers: function() {
var all_members = this.memberDict || {};
var all_user_ids = Object.keys(all_members);
var ConferenceHandler = CallHandler.getConferenceHandler();
@@ -334,7 +324,7 @@ module.exports = React.createClass({
var to_display = [];
var count = 0;
for (var i = 0; i < all_user_ids.length && (limit === undefined || count < limit); ++i) {
for (var i = 0; i < all_user_ids.length; ++i) {
var user_id = all_user_ids[i];
var m = all_members[user_id];
@@ -425,27 +415,7 @@ module.exports = React.createClass({
// For now, let's just order things by timestamp. It's really annoying
// that a user disappears from sight just because they temporarily go offline
/*
var presenceMap = {
online: 3,
unavailable: 2,
offline: 1
};
var presenceOrdA = userA ? presenceMap[userA.presence] : 0;
var presenceOrdB = userB ? presenceMap[userB.presence] : 0;
if (presenceOrdA != presenceOrdB) {
return presenceOrdB - presenceOrdA;
}
*/
var lastActiveTsA = userA && userA.lastActiveTs ? userA.lastActiveTs : 0;
var lastActiveTsB = userB && userB.lastActiveTs ? userB.lastActiveTs : 0;
// console.log("comparing ts: " + lastActiveTsA + " and " + lastActiveTsB);
return lastActiveTsB - lastActiveTsA;
return userB.getLastActiveTs() - userA.getLastActiveTs();
},
onSearchQueryChanged: function(input) {
@@ -462,9 +432,16 @@ module.exports = React.createClass({
var memberList = self.state.members.filter(function(userId) {
var m = self.memberDict[userId];
if (query && m.name.toLowerCase().indexOf(query) === -1) {
return false;
if (query) {
const matchesName = m.name.toLowerCase().indexOf(query) !== -1;
const matchesId = m.userId.toLowerCase().indexOf(query) !== -1;
if (!matchesName && !matchesId) {
return false;
}
}
return m.membership == membership;
}).map(function(userId) {
var m = self.memberDict[userId];
@@ -515,7 +492,7 @@ module.exports = React.createClass({
invitedSection = (
<div className="mx_MemberList_invited">
<h2>Invited</h2>
<div autoshow={true} className="mx_MemberList_wrapper">
<div className="mx_MemberList_wrapper">
{invitedMemberTiles}
</div>
</div>
@@ -544,7 +521,6 @@ module.exports = React.createClass({
<div className="mx_MemberList">
{inviteMemberListSection}
<GeminiScrollbar autoshow={true}
relayoutOnUpdate={false}
className="mx_MemberList_joined mx_MemberList_outerWrapper">
<TruncatedList className="mx_MemberList_wrapper" truncateAt={this.state.truncateAt}
createOverflowElement={this._createOverflowTile}>

View File

@@ -20,54 +20,53 @@ var MatrixClientPeg = require('../../../MatrixClientPeg');
var Modal = require('../../../Modal');
var sdk = require('../../../index');
var dis = require('../../../dispatcher');
import Autocomplete from './Autocomplete';
import UserSettingsStore from '../../../UserSettingsStore';
module.exports = React.createClass({
displayName: 'MessageComposer',
export default class MessageComposer extends React.Component {
constructor(props, context) {
super(props, context);
this.onCallClick = this.onCallClick.bind(this);
this.onHangupClick = this.onHangupClick.bind(this);
this.onUploadClick = this.onUploadClick.bind(this);
this.onUploadFileSelected = this.onUploadFileSelected.bind(this);
this.onVoiceCallClick = this.onVoiceCallClick.bind(this);
this.onInputContentChanged = this.onInputContentChanged.bind(this);
this.onUpArrow = this.onUpArrow.bind(this);
this.onDownArrow = this.onDownArrow.bind(this);
this._tryComplete = this._tryComplete.bind(this);
this._onAutocompleteConfirm = this._onAutocompleteConfirm.bind(this);
propTypes: {
tabComplete: React.PropTypes.any,
this.state = {
autocompleteQuery: '',
selection: null,
};
// a callback which is called when the height of the composer is
// changed due to a change in content.
onResize: React.PropTypes.func,
}
// js-sdk Room object
room: React.PropTypes.object.isRequired,
// string representing the current voip call state
callState: React.PropTypes.string,
// callback when a file to upload is chosen
uploadFile: React.PropTypes.func.isRequired,
// opacity for dynamic UI fading effects
opacity: React.PropTypes.number,
},
onUploadClick: function(ev) {
onUploadClick(ev) {
if (MatrixClientPeg.get().isGuest()) {
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
let NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, {
title: "Please Register",
description: "Guest users can't upload files. Please register to upload."
description: "Guest users can't upload files. Please register to upload.",
});
return;
}
this.refs.uploadInput.click();
},
}
onUploadFileSelected: function(ev) {
var files = ev.target.files;
onUploadFileSelected(ev) {
let files = ev.target.files;
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
var TintableSvg = sdk.getComponent("elements.TintableSvg");
let QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
let TintableSvg = sdk.getComponent("elements.TintableSvg");
var fileList = [];
for(var i=0; i<files.length; i++) {
let fileList = [];
for (let i=0; i<files.length; i++) {
fileList.push(<li>
<TintableSvg key={i} src="img/files.svg" width="16" height="16" /> {files[i].name}
</li>);
@@ -94,11 +93,11 @@ module.exports = React.createClass({
}
this.refs.uploadInput.value = null;
}
},
});
},
}
onHangupClick: function() {
onHangupClick() {
var call = CallHandler.getCallForRoom(this.props.room.roomId);
//var call = CallHandler.getAnyActiveCall();
if (!call) {
@@ -108,27 +107,55 @@ module.exports = React.createClass({
action: 'hangup',
// hangup the call for this room, which may not be the room in props
// (e.g. conferences which will hangup the 1:1 room instead)
room_id: call.roomId
room_id: call.roomId,
});
},
}
onCallClick: function(ev) {
onCallClick(ev) {
dis.dispatch({
action: 'place_call',
type: ev.shiftKey ? "screensharing" : "video",
room_id: this.props.room.roomId
room_id: this.props.room.roomId,
});
},
}
onVoiceCallClick: function(ev) {
onVoiceCallClick(ev) {
dis.dispatch({
action: 'place_call',
type: 'voice',
room_id: this.props.room.roomId
room_id: this.props.room.roomId,
});
},
}
render: function() {
onInputContentChanged(content: string, selection: {start: number, end: number}) {
this.setState({
autocompleteQuery: content,
selection,
});
}
onUpArrow() {
return this.refs.autocomplete.onUpArrow();
}
onDownArrow() {
return this.refs.autocomplete.onDownArrow();
}
_tryComplete(): boolean {
if (this.refs.autocomplete) {
return this.refs.autocomplete.onConfirm();
}
return false;
}
_onAutocompleteConfirm(range, completion) {
if (this.messageComposerInput) {
this.messageComposerInput.onConfirmAutocompletion(range, completion);
}
}
render() {
var me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
var uploadInputStyle = {display: 'none'};
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
@@ -154,12 +181,12 @@ module.exports = React.createClass({
else {
callButton =
<div key="controls_call" className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick} title="Voice call">
<TintableSvg src="img/voice.svg" width="16" height="26"/>
</div>
<TintableSvg src="img/icon-call.svg" width="35" height="35"/>
</div>;
videoCallButton =
<div key="controls_videocall" className="mx_MessageComposer_videocall" onClick={this.onCallClick} title="Video call">
<TintableSvg src="img/call.svg" width="30" height="22"/>
</div>
<TintableSvg src="img/icons-video.svg" width="35" height="35"/>
</div>;
}
var canSendMessages = this.props.room.currentState.maySendMessage(
@@ -172,7 +199,7 @@ module.exports = React.createClass({
var uploadButton = (
<div key="controls_upload" className="mx_MessageComposer_upload"
onClick={this.onUploadClick} title="Upload file">
<TintableSvg src="img/upload.svg" width="19" height="24"/>
<TintableSvg src="img/icons-upload.svg" width="35" height="35"/>
<input ref="uploadInput" type="file"
style={uploadInputStyle}
multiple
@@ -181,8 +208,16 @@ module.exports = React.createClass({
);
controls.push(
<MessageComposerInput key="controls_input" tabComplete={this.props.tabComplete}
onResize={this.props.onResize} room={this.props.room} />,
<MessageComposerInput
ref={c => this.messageComposerInput = c}
key="controls_input"
onResize={this.props.onResize}
room={this.props.room}
tryComplete={this._tryComplete}
onUpArrow={this.onUpArrow}
onDownArrow={this.onDownArrow}
tabComplete={this.props.tabComplete} // used for old messagecomposerinput/tabcomplete
onContentChanged={this.onInputContentChanged} />,
uploadButton,
hangupButton,
callButton,
@@ -198,6 +233,13 @@ module.exports = React.createClass({
return (
<div className="mx_MessageComposer mx_fadable" style={{ opacity: this.props.opacity }}>
<div className="mx_MessageComposer_autocomplete_wrapper">
<Autocomplete
ref="autocomplete"
onConfirm={this._onAutocompleteConfirm}
query={this.state.autocompleteQuery}
selection={this.state.selection} />
</div>
<div className="mx_MessageComposer_wrapper">
<div className="mx_MessageComposer_row">
{controls}
@@ -206,5 +248,24 @@ module.exports = React.createClass({
</div>
);
}
});
};
MessageComposer.propTypes = {
tabComplete: React.PropTypes.any,
// a callback which is called when the height of the composer is
// changed due to a change in content.
onResize: React.PropTypes.func,
// js-sdk Room object
room: React.PropTypes.object.isRequired,
// string representing the current voip call state
callState: React.PropTypes.string,
// callback when a file to upload is chosen
uploadFile: React.PropTypes.func.isRequired,
// opacity for dynamic UI fading effects
opacity: React.PropTypes.number
};

View File

@@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
var marked = require("marked");
import type SyntheticKeyboardEvent from 'react/lib/SyntheticKeyboardEvent';
import marked from 'marked';
marked.setOptions({
renderer: new marked.Renderer(),
gfm: true,
@@ -24,7 +24,7 @@ marked.setOptions({
pedantic: false,
sanitize: true,
smartLists: true,
smartypants: false
smartypants: false,
});
import {Editor, EditorState, RichUtils, CompositeDecorator,
@@ -33,14 +33,14 @@ import {Editor, EditorState, RichUtils, CompositeDecorator,
import {stateToMarkdown} from 'draft-js-export-markdown';
var MatrixClientPeg = require("../../../MatrixClientPeg");
var SlashCommands = require("../../../SlashCommands");
var Modal = require("../../../Modal");
var MemberEntry = require("../../../TabCompleteEntries").MemberEntry;
var sdk = require('../../../index');
import MatrixClientPeg from '../../../MatrixClientPeg';
import type {MatrixClient} from 'matrix-js-sdk/lib/matrix';
import SlashCommands from '../../../SlashCommands';
import Modal from '../../../Modal';
import sdk from '../../../index';
var dis = require("../../../dispatcher");
var KeyCode = require("../../../KeyCode");
import dis from '../../../dispatcher';
import KeyCode from '../../../KeyCode';
import * as RichText from '../../../RichText';
@@ -49,8 +49,8 @@ const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000;
const KEY_M = 77;
// FIXME Breaks markdown with multiple paragraphs, since it only strips first and last <p>
function mdownToHtml(mdown) {
var html = marked(mdown) || "";
function mdownToHtml(mdown: string): string {
let html = marked(mdown) || "";
html = html.trim();
// strip start and end <p> tags else you get 'orrible spacing
if (html.indexOf("<p>") === 0) {
@@ -66,23 +66,38 @@ function mdownToHtml(mdown) {
* The textInput part of the MessageComposer
*/
export default class MessageComposerInput extends React.Component {
static getKeyBinding(e: SyntheticKeyboardEvent): string {
// C-m => Toggles between rich text and markdown modes
if (e.keyCode === KEY_M && KeyBindingUtil.isCtrlKeyCommand(e)) {
return 'toggle-mode';
}
return getDefaultKeyBinding(e);
}
client: MatrixClient;
constructor(props, context) {
super(props, context);
this.onAction = this.onAction.bind(this);
this.onInputClick = this.onInputClick.bind(this);
this.handleReturn = this.handleReturn.bind(this);
this.handleKeyCommand = this.handleKeyCommand.bind(this);
this.onChange = this.onChange.bind(this);
this.setEditorState = this.setEditorState.bind(this);
this.onUpArrow = this.onUpArrow.bind(this);
this.onDownArrow = this.onDownArrow.bind(this);
this.onTab = this.onTab.bind(this);
this.onConfirmAutocompletion = this.onConfirmAutocompletion.bind(this);
let isRichtextEnabled = window.localStorage.getItem('mx_editor_rte_enabled');
if(isRichtextEnabled == null) {
if (isRichtextEnabled == null) {
isRichtextEnabled = 'true';
}
isRichtextEnabled = isRichtextEnabled === 'true';
this.state = {
isRichtextEnabled: isRichtextEnabled,
editorState: null
editorState: null,
};
// bit of a hack, but we need to do this here since createEditorState needs isRichtextEnabled
@@ -91,15 +106,6 @@ export default class MessageComposerInput extends React.Component {
this.client = MatrixClientPeg.get();
}
static getKeyBinding(e: SyntheticKeyboardEvent): string {
// C-m => Toggles between rich text and markdown modes
if(e.keyCode == KEY_M && KeyBindingUtil.isCtrlKeyCommand(e)) {
return 'toggle-mode';
}
return getDefaultKeyBinding(e);
}
/**
* "Does the right thing" to create an EditorState, based on:
* - whether we've got rich text mode enabled
@@ -207,11 +213,9 @@ export default class MessageComposerInput extends React.Component {
let contentJSON = window.sessionStorage.getItem("mx_messagecomposer_input_" + this.roomId);
if (contentJSON) {
let content = convertFromRaw(JSON.parse(contentJSON));
component.setState({
editorState: component.createEditorState(component.state.isRichtextEnabled, content)
});
component.setEditorState(component.createEditorState(component.state.isRichtextEnabled, content));
}
}
},
};
}
@@ -233,7 +237,7 @@ export default class MessageComposerInput extends React.Component {
}
onAction(payload) {
var editor = this.refs.editor;
let editor = this.refs.editor;
switch (payload.action) {
case 'focus_composer':
@@ -251,7 +255,7 @@ export default class MessageComposerInput extends React.Component {
payload.displayname
);
this.setState({
editorState: EditorState.push(this.state.editorState, contentState, 'insert-characters')
editorState: EditorState.push(this.state.editorState, contentState, 'insert-characters'),
});
editor.focus();
}
@@ -344,28 +348,31 @@ export default class MessageComposerInput extends React.Component {
this.refs.editor.focus();
}
onChange(editorState: EditorState) {
setEditorState(editorState: EditorState) {
editorState = RichText.attachImmutableEntitiesToEmoji(editorState);
this.setState({editorState});
if(editorState.getCurrentContent().hasText()) {
this.onTypingActivity()
if (editorState.getCurrentContent().hasText()) {
this.onTypingActivity();
} else {
this.onFinishedTyping();
}
if (this.props.onContentChanged) {
this.props.onContentChanged(editorState.getCurrentContent().getPlainText(),
RichText.selectionStateToTextOffsets(editorState.getSelection(),
editorState.getCurrentContent().getBlocksAsArray()));
}
}
enableRichtext(enabled: boolean) {
if (enabled) {
let html = mdownToHtml(this.state.editorState.getCurrentContent().getPlainText());
this.setState({
editorState: this.createEditorState(enabled, RichText.HTMLtoContentState(html))
});
this.setEditorState(this.createEditorState(enabled, RichText.HTMLtoContentState(html)));
} else {
let markdown = stateToMarkdown(this.state.editorState.getCurrentContent()),
contentState = ContentState.createFromText(markdown);
this.setState({
editorState: this.createEditorState(enabled, contentState)
});
this.setEditorState(this.createEditorState(enabled, contentState));
}
window.localStorage.setItem('mx_editor_rte_enabled', enabled);
@@ -376,7 +383,7 @@ export default class MessageComposerInput extends React.Component {
}
handleKeyCommand(command: string): boolean {
if(command === 'toggle-mode') {
if (command === 'toggle-mode') {
this.enableRichtext(!this.state.isRichtextEnabled);
return true;
}
@@ -384,7 +391,7 @@ export default class MessageComposerInput extends React.Component {
let newState: ?EditorState = null;
// Draft handles rich text mode commands by default but we need to do it ourselves for Markdown.
if(!this.state.isRichtextEnabled) {
if (!this.state.isRichtextEnabled) {
let contentState = this.state.editorState.getCurrentContent(),
selection = this.state.editorState.getSelection();
@@ -392,10 +399,10 @@ export default class MessageComposerInput extends React.Component {
bold: text => `**${text}**`,
italic: text => `*${text}*`,
underline: text => `_${text}_`, // there's actually no valid underline in Markdown, but *shrug*
code: text => `\`${text}\``
code: text => `\`${text}\``,
}[command];
if(modifyFn) {
if (modifyFn) {
newState = EditorState.push(
this.state.editorState,
RichText.modifyText(contentState, selection, modifyFn),
@@ -404,23 +411,26 @@ export default class MessageComposerInput extends React.Component {
}
}
if(newState == null)
if (newState == null)
newState = RichUtils.handleKeyCommand(this.state.editorState, command);
if (newState != null) {
this.onChange(newState);
this.setEditorState(newState);
return true;
}
return false;
}
handleReturn(ev) {
if(ev.shiftKey)
if (ev.shiftKey) {
return false;
}
const contentState = this.state.editorState.getCurrentContent();
if(!contentState.hasText())
if (!contentState.hasText()) {
return true;
}
let contentText = contentState.getPlainText(), contentHTML;
@@ -489,10 +499,47 @@ export default class MessageComposerInput extends React.Component {
return true;
}
onUpArrow(e) {
if (this.props.onUpArrow && this.props.onUpArrow()) {
e.preventDefault();
}
}
onDownArrow(e) {
if (this.props.onDownArrow && this.props.onDownArrow()) {
e.preventDefault();
}
}
onTab(e) {
if (this.props.tryComplete) {
if (this.props.tryComplete()) {
e.preventDefault();
}
}
}
onConfirmAutocompletion(range, content: string) {
let contentState = Modifier.replaceText(
this.state.editorState.getCurrentContent(),
RichText.textOffsetsToSelectionState(range, this.state.editorState.getCurrentContent().getBlocksAsArray()),
content
);
let editorState = EditorState.push(this.state.editorState, contentState, 'insert-characters');
editorState = EditorState.forceSelection(editorState, contentState.getSelectionAfter());
this.setEditorState(editorState);
// for some reason, doing this right away does not update the editor :(
setTimeout(() => this.refs.editor.focus(), 50);
}
render() {
let className = "mx_MessageComposer_input";
if(this.state.isRichtextEnabled) {
if (this.state.isRichtextEnabled) {
className += " mx_MessageComposer_input_rte"; // placeholder indicator for RTE mode
}
@@ -502,11 +549,14 @@ export default class MessageComposerInput extends React.Component {
<Editor ref="editor"
placeholder="Type a message…"
editorState={this.state.editorState}
onChange={this.onChange}
onChange={this.setEditorState}
keyBindingFn={MessageComposerInput.getKeyBinding}
handleKeyCommand={this.handleKeyCommand}
handleReturn={this.handleReturn}
stripPastedStyles={!this.state.isRichtextEnabled}
onTab={this.onTab}
onUpArrow={this.onUpArrow}
onDownArrow={this.onDownArrow}
spellCheck={true} />
</div>
);
@@ -521,5 +571,15 @@ MessageComposerInput.propTypes = {
onResize: React.PropTypes.func,
// js-sdk Room object
room: React.PropTypes.object.isRequired
room: React.PropTypes.object.isRequired,
// called with current plaintext content (as a string) whenever it changes
onContentChanged: React.PropTypes.func,
onUpArrow: React.PropTypes.func,
onDownArrow: React.PropTypes.func,
// attempts to confirm currently selected completion, returns whether actually confirmed
tryComplete: React.PropTypes.func,
};

View File

@@ -163,13 +163,13 @@ module.exports = React.createClass({
};
return (
<Velociraptor>
<Velociraptor
startStyles={this.state.startStyles}
enterTransitionOpts={this.state.enterTransitionOpts} >
<MemberAvatar
member={this.props.member}
width={14} height={14} resizeMethod="crop"
style={style}
startStyle={this.state.startStyles}
enterTransitionOpts={this.state.enterTransitionOpts}
onClick={this.props.onClick}
/>
</Velociraptor>

View File

@@ -24,6 +24,7 @@ var Modal = require("../../../Modal");
var linkify = require('linkifyjs');
var linkifyElement = require('linkifyjs/element');
var linkifyMatrix = require('../../../linkify-matrix');
import {emojifyText} from '../../../HtmlUtils';
linkifyMatrix(linkify);
@@ -211,13 +212,12 @@ module.exports = React.createClass({
roomName = this.props.room.name;
}
let roomNameHTML = emojifyText(roomName);
name =
<div className="mx_RoomHeader_name" onClick={this.props.onSettingsClick}>
<div className={ "mx_RoomHeader_nametext " + (settingsHint ? "mx_RoomHeader_settingsHint" : "") } title={ roomName }>{ roomName }</div>
<div className={ "mx_RoomHeader_nametext " + (settingsHint ? "mx_RoomHeader_settingsHint" : "") } title={ roomName } dangerouslySetInnerHTML={roomNameHTML}></div>
{ searchStatus }
<div className="mx_RoomHeader_settingsButton" title="Settings">
<TintableSvg src="img/settings.svg" width="12" height="12"/>
</div>
</div>
}
@@ -263,10 +263,18 @@ module.exports = React.createClass({
);
}
var settings_button;
if (this.props.onSettingsClick) {
settings_button =
<div className="mx_RoomHeader_button" onClick={this.props.onSettingsClick} title="Settings">
<TintableSvg src="img/icons-settings-room.svg" width="16" height="16"/>
</div>;
}
var leave_button;
if (this.props.onLeaveClick) {
leave_button =
<div className="mx_RoomHeader_button mx_RoomHeader_leaveButton" onClick={this.props.onLeaveClick} title="Leave room">
<div className="mx_RoomHeader_button" onClick={this.props.onLeaveClick} title="Leave room">
<TintableSvg src="img/leave.svg" width="26" height="20"/>
</div>;
}
@@ -274,7 +282,7 @@ module.exports = React.createClass({
var forget_button;
if (this.props.onForgetClick) {
forget_button =
<div className="mx_RoomHeader_button mx_RoomHeader_leaveButton" onClick={this.props.onForgetClick} title="Forget room">
<div className="mx_RoomHeader_button" onClick={this.props.onForgetClick} title="Forget room">
<TintableSvg src="img/leave.svg" width="26" height="20"/>
</div>;
}
@@ -288,10 +296,11 @@ module.exports = React.createClass({
if (!this.props.editing) {
right_row =
<div className="mx_RoomHeader_rightRow">
{ settings_button }
{ forget_button }
{ leave_button }
<div className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title="Search">
<TintableSvg src="img/search.svg" width="21" height="19"/>
<TintableSvg src="img/icons-search.svg" width="35" height="35"/>
</div>
{ rightPanel_buttons }
</div>;

View File

@@ -268,9 +268,11 @@ module.exports = React.createClass({
},
_repositionTooltip: function(e) {
if (this.tooltip && this.tooltip.parentElement) {
// We access the parent of the parent, as the tooltip is inside a container
// Needs refactoring into a better multipurpose tooltip
if (this.tooltip && this.tooltip.parentElement && this.tooltip.parentElement.parentElement) {
var scroll = ReactDOM.findDOMNode(this);
this.tooltip.style.top = (70 + scroll.parentElement.offsetTop + this.tooltip.parentElement.offsetTop - this._getScrollNode().scrollTop) + "px";
this.tooltip.style.top = (3 + scroll.parentElement.offsetTop + this.tooltip.parentElement.parentElement.offsetTop - this._getScrollNode().scrollTop) + "px";
}
},
@@ -325,7 +327,6 @@ module.exports = React.createClass({
return (
<GeminiScrollbar className="mx_RoomList_scrollbar"
relayoutOnUpdate={false}
autoshow={true} onScroll={ self._repositionTooltips } ref="gemscroll">
<div className="mx_RoomList">
<RoomSubList list={ self.state.lists['im.vector.fake.invite'] }

View File

@@ -33,16 +33,24 @@ module.exports = React.createClass({
// If invited by 3rd party invite, the email address the invite was sent to
invitedEmail: React.PropTypes.string,
canJoin: React.PropTypes.bool,
// A standard client/server API error object. If supplied, indicates that the
// caller was unable to fetch details about the room for the given reason.
error: React.PropTypes.object,
canPreview: React.PropTypes.bool,
spinner: React.PropTypes.bool,
room: React.PropTypes.object,
// The alias that was used to access this room, if appropriate
// If given, this will be how the room is referred to (eg.
// in error messages).
roomAlias: React.PropTypes.object,
},
getDefaultProps: function() {
return {
onJoinClick: function() {},
canJoin: false,
canPreview: true,
};
},
@@ -115,8 +123,24 @@ module.exports = React.createClass({
);
}
else if (this.props.canJoin) {
var name = this.props.room ? this.props.room.name : "";
else if (this.props.error) {
var name = this.props.roomAlias || "This room";
var error;
if (this.props.error.errcode == 'M_NOT_FOUND') {
error = name + " does not exist";
} else {
error = name + " is not accessible at this time";
}
joinBlock = (
<div>
<div className="mx_RoomPreviewBar_join_text">
{ error }
</div>
</div>
);
}
else {
var name = this.props.room ? this.props.room.name : (this.props.room_alias || "");
name = name ? <b>{ name }</b> : "a room";
joinBlock = (
<div>

View File

@@ -23,6 +23,14 @@ var Modal = require('../../../Modal');
var ObjectUtils = require("../../../ObjectUtils");
var dis = require("../../../dispatcher");
var ScalarAuthClient = require("../../../ScalarAuthClient");
var UserSettingsStore = require('../../../UserSettingsStore');
// parse a string as an integer; if the input is undefined, or cannot be parsed
// as an integer, return a default.
function parseIntWithDefault(val, def) {
var res = parseInt(val);
return isNaN(res) ? def : res;
}
module.exports = React.createClass({
displayName: 'RoomSettings',
@@ -59,9 +67,18 @@ module.exports = React.createClass({
tags_changed: false,
tags: tags,
areNotifsMuted: areNotifsMuted,
<<<<<<< HEAD
isRoomPublished: this._originalIsRoomPublished, // loaded async in componentWillMount
scalar_token: null,
scalar_error: null,
=======
// isRoomPublished is loaded async in componentWillMount so when the component
// inits, the saved value will always be undefined, however getInitialState()
// is also called from the saving code so we must return the correct value here
// if we have it (although this could race if the user saves before we load whether
// the room is published or not).
isRoomPublished: this._originalIsRoomPublished,
>>>>>>> develop
};
},
@@ -209,11 +226,17 @@ module.exports = React.createClass({
}
});
}
console.log("Performing %s operations", promises.length);
// color scheme
promises.push(this.saveColor());
// url preview settings
promises.push(this.saveUrlPreviewSettings());
// encryption
promises.push(this.saveEncryption());
console.log("Performing %s operations: %s", promises.length, JSON.stringify(promises));
return q.allSettled(promises);
},
@@ -227,6 +250,24 @@ module.exports = React.createClass({
return this.refs.color_settings.saveSettings();
},
saveUrlPreviewSettings: function() {
if (!this.refs.url_preview_settings) { return q(); }
return this.refs.url_preview_settings.saveSettings();
},
saveEncryption: function () {
if (!this.refs.encrypt) { return q(); }
var encrypt = this.refs.encrypt.checked;
if (!encrypt) { return q(); }
var roomId = this.props.room.roomId;
return MatrixClientPeg.get().sendStateEvent(
roomId, "m.room.encryption",
{ algorithm: "m.olm.v1.curve25519-aes-sha2" }
);
},
_hasDiff: function(strA, strB) {
// treat undefined as an empty string because other components may blindly
// call setName("") when there has been no diff made to the name!
@@ -261,7 +302,7 @@ module.exports = React.createClass({
power_levels_changed: true
});
},
_yankValueFromEvent: function(stateEventType, keyName, defaultValue) {
// E.g.("m.room.name","name") would yank the "name" content key from "m.room.name"
var event = this.props.room.currentState.getStateEvents(stateEventType, '');
@@ -296,7 +337,7 @@ module.exports = React.createClass({
},
});
},
_onRoomAccessRadioToggle: function(ev) {
// join_rule
@@ -400,68 +441,72 @@ module.exports = React.createClass({
}, "");
},
_renderEncryptionSection: function() {
if (!UserSettingsStore.isFeatureEnabled("e2e_encryption")) {
return null;
}
var cli = MatrixClientPeg.get();
var roomState = this.props.room.currentState;
var isEncrypted = cli.isRoomEncrypted(this.props.room.roomId);
var text = "Encryption is " + (isEncrypted ? "" : "not ") +
"enabled in this room.";
var button;
if (!isEncrypted &&
roomState.mayClientSendStateEvent("m.room.encryption", cli)) {
button = (
<label>
<input type="checkbox" ref="encrypt" />
Enable encryption (warning: cannot be disabled again!)
</label>
);
}
return (
<div className="mx_RoomSettings_toggles">
<h3>Encryption</h3>
<label>{text}</label>
{button}
</div>
);
},
render: function() {
// TODO: go through greying out things you don't have permission to change
// (or turning them into informative stuff)
var AliasSettings = sdk.getComponent("room_settings.AliasSettings");
var ColorSettings = sdk.getComponent("room_settings.ColorSettings");
var UrlPreviewSettings = sdk.getComponent("room_settings.UrlPreviewSettings");
var EditableText = sdk.getComponent('elements.EditableText');
var PowerSelector = sdk.getComponent('elements.PowerSelector');
var Loader = sdk.getComponent("elements.Spinner")
var power_levels = this.props.room.currentState.getStateEvents('m.room.power_levels', '');
var events_levels = (power_levels ? power_levels.getContent().events : {}) || {};
var cli = MatrixClientPeg.get();
var roomState = this.props.room.currentState;
var user_id = cli.credentials.userId;
if (power_levels) {
power_levels = power_levels.getContent();
var power_level_event = roomState.getStateEvents('m.room.power_levels', '');
var power_levels = power_level_event ? power_level_event.getContent() : {};
var events_levels = power_levels.events || {};
var user_levels = power_levels.users || {};
var ban_level = parseInt(power_levels.ban);
var kick_level = parseInt(power_levels.kick);
var redact_level = parseInt(power_levels.redact);
var invite_level = parseInt(power_levels.invite || 0);
var send_level = parseInt(power_levels.events_default || 0);
var state_level = parseInt(power_levels.state_default || 50);
var default_user_level = parseInt(power_levels.users_default || 0);
var ban_level = parseIntWithDefault(power_levels.ban, 50);
var kick_level = parseIntWithDefault(power_levels.kick, 50);
var redact_level = parseIntWithDefault(power_levels.redact, 50);
var invite_level = parseIntWithDefault(power_levels.invite, 50);
var send_level = parseIntWithDefault(power_levels.events_default, 0);
var state_level = power_level_event ? parseIntWithDefault(power_levels.state_default, 50) : 0;
var default_user_level = parseIntWithDefault(power_levels.users_default, 0);
if (power_levels.ban == undefined) ban_level = 50;
if (power_levels.kick == undefined) kick_level = 50;
if (power_levels.redact == undefined) redact_level = 50;
var user_levels = power_levels.users || {};
var current_user_level = user_levels[user_id];
if (current_user_level == undefined) current_user_level = default_user_level;
var power_level_level = events_levels["m.room.power_levels"];
if (power_level_level == undefined) {
power_level_level = state_level;
}
var can_change_levels = current_user_level >= power_level_level;
} else {
var ban_level = 50;
var kick_level = 50;
var redact_level = 50;
var invite_level = 0;
var send_level = 0;
var state_level = 0;
var default_user_level = 0;
var user_levels = [];
var events_levels = [];
var current_user_level = 0;
var power_level_level = 0;
var can_change_levels = false;
var current_user_level = user_levels[user_id];
if (current_user_level === undefined) {
current_user_level = default_user_level;
}
var state_default = (parseInt(power_levels ? power_levels.state_default : 0) || 0);
var can_change_levels = roomState.mayClientSendStateEvent("m.room.power_levels", cli);
var canSetTag = !cli.isGuest();
@@ -530,7 +575,7 @@ module.exports = React.createClass({
var tagsSection = null;
if (canSetTag || self.state.tags) {
var tagsSection =
var tagsSection =
<div className="mx_RoomSettings_tags">
Tagged as: { canSetTag ?
(tags.map(function(tag, i) {
@@ -666,10 +711,6 @@ module.exports = React.createClass({
Members only (since they joined)
</label>
</div>
<label className="mx_RoomSettings_encrypt">
<input type="checkbox" />
Encrypt room
</label>
</div>
@@ -690,6 +731,8 @@ module.exports = React.createClass({
canonicalAliasEvent={this.props.room.currentState.getStateEvents('m.room.canonical_alias', '')}
aliasEvents={this.props.room.currentState.getStateEvents('m.room.aliases')} />
<UrlPreviewSettings ref="url_preview_settings" room={this.props.room} />
<h3>Permissions</h3>
<div className="mx_RoomSettings_powerLevels mx_RoomSettings_settings">
<div className="mx_RoomSettings_powerLevel">
@@ -737,6 +780,8 @@ module.exports = React.createClass({
{ bannedUsersSection }
{ this._renderEncryptionSection() }
<h3>Advanced</h3>
<div className="mx_RoomSettings_settings">
This room's internal ID is <code>{ this.props.room.roomId }</code>

View File

@@ -21,6 +21,8 @@ var classNames = require('classnames');
var dis = require("../../../dispatcher");
var MatrixClientPeg = require('../../../MatrixClientPeg');
var sdk = require('../../../index');
var ContextualMenu = require('../../structures/ContextualMenu');
import {emojifyText} from '../../../HtmlUtils';
module.exports = React.createClass({
displayName: 'RoomTile',
@@ -42,13 +44,48 @@ module.exports = React.createClass({
},
getInitialState: function() {
return( { hover : false });
var areNotifsMuted = false;
var cli = MatrixClientPeg.get();
if (!cli.isGuest()) {
var roomPushRule = cli.getRoomPushRule("global", this.props.room.roomId);
if (roomPushRule) {
if (0 <= roomPushRule.actions.indexOf("dont_notify")) {
areNotifsMuted = true;
}
}
}
return({
hover : false,
badgeHover : false,
menu: false,
areNotifsMuted: areNotifsMuted,
});
},
onAction: function(payload) {
switch (payload.action) {
case 'notification_change':
// Is the notification about this room?
if (payload.roomId === this.props.room.roomId) {
this.setState( { areNotifsMuted : payload.isMuted });
}
break;
}
},
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
},
componentWillUnmount: function() {
dis.unregister(this.dispatcherRef);
},
onClick: function() {
dis.dispatch({
action: 'view_room',
room_id: this.props.room.roomId
room_id: this.props.room.roomId,
});
},
@@ -60,6 +97,48 @@ module.exports = React.createClass({
this.setState( { hover : false });
},
badgeOnMouseEnter: function() {
// Only allow none guests to access the context menu
// and only change it if it needs to change
if (!MatrixClientPeg.get().isGuest() && !this.state.badgeHover) {
this.setState( { badgeHover : true } );
}
},
badgeOnMouseLeave: function() {
this.setState( { badgeHover : false } );
},
onBadgeClicked: function(e) {
// Only allow none guests to access the context menu
if (!MatrixClientPeg.get().isGuest()) {
// If the badge is clicked, then no longer show tooltip
if (this.props.collapsed) {
this.setState({ hover: false });
}
var Menu = sdk.getComponent('context_menus.NotificationStateContextMenu');
var elementRect = e.target.getBoundingClientRect();
// The window X and Y offsets are to adjust position when zoomed in to page
var x = elementRect.right + window.pageXOffset + 3;
var y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset) - 53;
var self = this;
ContextualMenu.createMenu(Menu, {
menuWidth: 188,
menuHeight: 126,
chevronOffset: 45,
left: x,
top: y,
room: this.props.room,
onFinished: function() {
self.setState({ menu: false });
}
});
this.setState({ menu: true });
}
},
render: function() {
var myUserId = MatrixClientPeg.get().credentials.userId;
var me = this.props.room.currentState.members[myUserId];
@@ -72,42 +151,64 @@ module.exports = React.createClass({
'mx_RoomTile_selected': this.props.selected,
'mx_RoomTile_unread': this.props.unread,
'mx_RoomTile_unreadNotify': notificationCount > 0,
'mx_RoomTile_read': !(this.props.highlight || notificationCount > 0),
'mx_RoomTile_highlight': this.props.highlight,
'mx_RoomTile_invited': (me && me.membership == 'invite'),
'mx_RoomTile_menu': this.state.menu,
});
var avatarClasses = classNames({
'mx_RoomTile_avatar': true,
'mx_RoomTile_mute': this.state.areNotifsMuted,
});
var badgeClasses = classNames({
'mx_RoomTile_badge': true,
'mx_RoomTile_badgeButton': this.state.badgeHover || this.state.menu,
'mx_RoomTile_badgeMute': this.state.areNotifsMuted,
});
// XXX: We should never display raw room IDs, but sometimes the
// room name js sdk gives is undefined (cannot repro this -- k)
var name = this.props.room.name || this.props.room.roomId;
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
var badge;
if (this.props.highlight || notificationCount > 0) {
badge = <div className="mx_RoomTile_badge">{ notificationCount ? notificationCount : '!' }</div>;
var badgeContent;
if (this.state.badgeHover || this.state.menu) {
badgeContent = "\u00B7\u00B7\u00B7";
} else if (this.props.highlight || notificationCount > 0) {
var limitedCount = (notificationCount > 99) ? '99+' : notificationCount;
badgeContent = notificationCount ? limitedCount : '!';
} else {
badgeContent = '\u200B';
}
/*
if (this.props.highlight) {
badge = <div className="mx_RoomTile_badge">!</div>;
if (this.state.areNotifsMuted && !(this.state.badgeHover || this.state.menu)) {
badge = <div className={ badgeClasses } onClick={this.onBadgeClicked} onMouseEnter={this.badgeOnMouseEnter} onMouseLeave={this.badgeOnMouseLeave}><img className="mx_RoomTile_badgeIcon" src="img/icon-context-mute.svg" width="16" height="12" /></div>;
} else {
badge = <div className={ badgeClasses } onClick={this.onBadgeClicked} onMouseEnter={this.badgeOnMouseEnter} onMouseLeave={this.badgeOnMouseLeave}>{ badgeContent }</div>;
}
else if (this.props.unread) {
badge = <div className="mx_RoomTile_badge">1</div>;
}
var nameCell;
if (badge) {
nameCell = <div className="mx_RoomTile_nameBadge"><div className="mx_RoomTile_name">{name}</div><div className="mx_RoomTile_badgeCell">{badge}</div></div>;
}
else {
nameCell = <div className="mx_RoomTile_name">{name}</div>;
}
*/
var label;
var tooltip;
if (!this.props.collapsed) {
var className = 'mx_RoomTile_name' + (this.props.isInvite ? ' mx_RoomTile_invite' : '');
var nameClasses = classNames({
'mx_RoomTile_name': true,
'mx_RoomTile_invite': this.props.isInvite,
'mx_RoomTile_mute': this.state.areNotifsMuted,
'mx_RoomTile_badgeShown': this.props.highlight || notificationCount > 0 || this.state.badgeHover || this.state.menu || this.state.areNotifsMuted,
});
let nameHTML = emojifyText(name);
if (this.props.selected) {
name = <span>{ name }</span>;
let nameSelected = <span dangerouslySetInnerHTML={nameHTML}></span>;
label = <div title={ name } onClick={this.onClick} className={ nameClasses }>{ nameSelected }</div>;
} else {
label = <div title={ name } onClick={this.onClick} className={ nameClasses } dangerouslySetInnerHTML={nameHTML}></div>;
}
label = <div className={ className }>{ name }</div>;
}
else if (this.state.hover) {
var RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
@@ -129,13 +230,16 @@ module.exports = React.createClass({
var connectDropTarget = this.props.connectDropTarget;
return connectDragSource(connectDropTarget(
<div className={classes} onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
<div className="mx_RoomTile_avatar">
<RoomAvatar room={this.props.room} width={24} height={24} />
<div className={classes} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
<div className={avatarClasses}>
<RoomAvatar onClick={this.onClick} room={this.props.room} width={24} height={24} />
</div>
<div className="mx_RoomTile_nameContainer">
{ label }
{ badge }
</div>
{ label }
{ badge }
{ incomingCallBox }
{ tooltip }
</div>
));
}

View File

@@ -179,7 +179,6 @@ var SearchableEntityList = React.createClass({
}
list = (
<GeminiScrollbar autoshow={true}
relayoutOnUpdate={false}
className="mx_SearchableEntityList_listWrapper">
{ list }
</GeminiScrollbar>

View File

@@ -24,17 +24,17 @@ module.exports = React.createClass({
displayName: 'TabCompleteBar',
propTypes: {
entries: React.PropTypes.array.isRequired
tabComplete: React.PropTypes.object.isRequired
},
render: function() {
return (
<div className="mx_TabCompleteBar">
{this.props.entries.map(function(entry, i) {
{this.props.tabComplete.peek(6).map((entry, i) => {
return (
<div key={entry.getKey() || i + ""}
className={ "mx_TabCompleteBar_item " + (entry instanceof CommandEntry ? "mx_TabCompleteBar_command" : "") }
onClick={entry.onClick.bind(entry)} >
className={ "mx_TabCompleteBar_item " + (entry instanceof CommandEntry ? "mx_TabCompleteBar_command" : "") }
onClick={this.props.tabComplete.onEntryClick.bind(this.props.tabComplete, entry)} >
{entry.getImageJsx()}
<span className="mx_TabCompleteBar_text">
{entry.getText()}

View File

@@ -21,29 +21,10 @@ var MatrixClientPeg = require("../../../MatrixClientPeg");
module.exports = React.createClass({
displayName: 'ChangeDisplayName',
propTypes: {
onFinished: React.PropTypes.func
},
getDefaultProps: function() {
return {
onFinished: function() {},
};
},
getInitialState: function() {
return {
busy: false,
errorString: null
}
},
componentWillMount: function() {
_getDisplayName: function() {
var cli = MatrixClientPeg.get();
this.setState({busy: true});
var self = this;
cli.getProfileInfo(cli.credentials.userId).done(function(result) {
return cli.getProfileInfo(cli.credentials.userId).then(function(result) {
var displayname = result.displayname;
if (!displayname) {
if (MatrixClientPeg.get().isGuest()) {
@@ -53,68 +34,26 @@ module.exports = React.createClass({
displayname = MatrixClientPeg.get().getUserIdLocalpart();
}
}
self.setState({
displayName: displayname,
busy: false
});
return displayname;
}, function(error) {
self.setState({
errorString: "Failed to fetch display name",
busy: false
});
throw new Error("Failed to fetch display name");
});
},
changeDisplayname: function(new_displayname) {
this.setState({
busy: true,
errorString: null,
})
var self = this;
MatrixClientPeg.get().setDisplayName(new_displayname).then(function() {
self.setState({
busy: false,
displayName: new_displayname
});
}, function(error) {
self.setState({
busy: false,
errorString: "Failed to set display name"
});
_changeDisplayName: function(new_displayname) {
var cli = MatrixClientPeg.get();
return cli.setDisplayName(new_displayname).catch(function(e) {
throw new Error("Failed to set display name");
});
},
edit: function() {
this.refs.displayname_edit.edit()
},
onValueChanged: function(new_value, shouldSubmit) {
if (shouldSubmit) {
this.changeDisplayname(new_value);
}
},
render: function() {
if (this.state.busy) {
var Loader = sdk.getComponent("elements.Spinner");
return (
<Loader />
);
} else if (this.state.errorString) {
return (
<div className="error">{this.state.errorString}</div>
);
} else {
var EditableText = sdk.getComponent('elements.EditableText');
return (
<EditableText ref="displayname_edit" initialValue={this.state.displayName}
className="mx_EditableText"
placeholderClassName="mx_EditableText_placeholder"
placeholder="No display name"
onValueChanged={this.onValueChanged} />
);
}
var EditableTextContainer = sdk.getComponent('elements.EditableTextContainer');
return (
<EditableTextContainer
getInitialValue={this._getDisplayName}
placeholder="No display name"
onSubmit={this._changeDisplayName} />
);
}
});

View File

@@ -0,0 +1,138 @@
/*
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.
*/
import React from 'react';
import classNames from 'classnames';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
export default class DevicesPanel extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {
devices: undefined,
deviceLoadError: undefined,
};
this._unmounted = false;
this._renderDevice = this._renderDevice.bind(this);
}
componentDidMount() {
this._loadDevices();
}
componentWillUnmount() {
this._unmounted = true;
}
_loadDevices() {
MatrixClientPeg.get().getDevices().done(
(resp) => {
if (this._unmounted) { return; }
this.setState({devices: resp.devices || []});
},
(error) => {
if (this._unmounted) { return; }
var errtxt;
if (err.httpStatus == 404) {
// 404 probably means the HS doesn't yet support the API.
errtxt = "Your home server does not support device management.";
} else {
console.error("Error loading devices:", error);
errtxt = "Unable to load device list.";
}
this.setState({deviceLoadError: errtxt});
}
);
}
/**
* compare two devices, sorting from most-recently-seen to least-recently-seen
* (and then, for stability, by device id)
*/
_deviceCompare(a, b) {
// return < 0 if a comes before b, > 0 if a comes after b.
const lastSeenDelta =
(b.last_seen_ts || 0) - (a.last_seen_ts || 0);
if (lastSeenDelta !== 0) { return lastSeenDelta; }
const idA = a.device_id;
const idB = b.device_id;
return (idA < idB) ? -1 : (idA > idB) ? 1 : 0;
}
_onDeviceDeleted(device) {
if (this._unmounted) { return; }
// delete the removed device from our list.
const removed_id = device.device_id;
this.setState((state, props) => {
const newDevices = state.devices.filter(
d => { return d.device_id != removed_id }
);
return { devices: newDevices };
});
}
_renderDevice(device) {
var DevicesPanelEntry = sdk.getComponent('settings.DevicesPanelEntry');
return (
<DevicesPanelEntry key={device.device_id} device={device}
onDeleted={()=>{this._onDeviceDeleted(device)}} />
);
}
render() {
const Spinner = sdk.getComponent("elements.Spinner");
if (this.state.deviceLoadError !== undefined) {
const classes = classNames(this.props.className, "error");
return (
<div className={classes}>
{this.state.deviceLoadError}
</div>
);
}
const devices = this.state.devices;
if (devices === undefined) {
// still loading
const classes = this.props.className;
return <Spinner className={classes}/>;
}
devices.sort(this._deviceCompare);
const classes = classNames(this.props.className, "mx_DevicesPanel");
return (
<div className={classes}>
<div className="mx_DevicesPanel_header">
<div className="mx_DevicesPanel_deviceName">Name</div>
<div className="mx_DevicesPanel_deviceLastSeen">Last seen</div>
<div className="mx_DevicesPanel_deviceButtons"></div>
</div>
{devices.map(this._renderDevice)}
</div>
);
}
}

View File

@@ -0,0 +1,136 @@
/*
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.
*/
import React from 'react';
import classNames from 'classnames';
import q from 'q';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
import DateUtils from '../../../DateUtils';
export default class DevicesPanelEntry extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {
deleting: false,
deleteError: undefined,
};
this._unmounted = false;
this._onDeleteClick = this._onDeleteClick.bind(this);
this._onDisplayNameChanged = this._onDisplayNameChanged.bind(this);
}
componentWillUnmount() {
this._unmounted = true;
}
_onDisplayNameChanged(value) {
const device = this.props.device;
return MatrixClientPeg.get().setDeviceDetails(device.device_id, {
display_name: value,
}).catch((e) => {
console.error("Error setting device display name", e);
throw new Error("Failed to set display name");
});
}
_onDeleteClick() {
const device = this.props.device;
this.setState({deleting: true});
MatrixClientPeg.get().deleteDevice(device.device_id).done(
() => {
this.props.onDeleted();
if (this._unmounted) { return; }
this.setState({ deleting: false });
},
(e) => {
console.error("Error deleting device", e);
if (this._unmounted) { return; }
this.setState({
deleting: false,
deleteError: "Failed to delete device",
});
}
);
}
render() {
const EditableTextContainer = sdk.getComponent('elements.EditableTextContainer');
const device = this.props.device;
if (this.state.deleting) {
const Spinner = sdk.getComponent("elements.Spinner");
return (
<div className="mx_DevicesPanel_device">
<Spinner />
</div>
);
}
let lastSeen = "";
if (device.last_seen_ts) {
// todo: format the timestamp as "5 minutes ago" or whatever.
const lastSeenDate = new Date(device.last_seen_ts);
lastSeen = device.last_seen_ip + " @ " +
lastSeenDate.toLocaleString();
}
let deleteButton;
if (this.state.deleteError) {
deleteButton = <div className="error">{this.state.deleteError}</div>
} else {
deleteButton = (
<div className="textButton"
onClick={this._onDeleteClick}>
Delete
</div>
);
}
return (
<div className="mx_DevicesPanel_device">
<div className="mx_DevicesPanel_deviceName">
<EditableTextContainer initialValue={device.display_name}
onSubmit={this._onDisplayNameChanged}
placeholder={device.device_id}
/>
</div>
<div className="mx_DevicesPanel_lastSeen">
{lastSeen}
</div>
<div className="mx_DevicesPanel_deviceButtons">
{deleteButton}
</div>
</div>
);
}
}
DevicesPanelEntry.propTypes = {
device: React.PropTypes.object.isRequired,
onDeleted: React.PropTypes.func,
};
DevicesPanelEntry.defaultProps = {
onDeleted: function() {},
};