Merge branch 'develop' into push-rules-settings
# Conflicts: # src/component-index.js # src/components/views/rooms/RoomSettings.js
This commit is contained in:
commit
33edeccb43
40 changed files with 2772 additions and 711 deletions
140
src/components/views/avatars/BaseAvatar.js
Normal file
140
src/components/views/avatars/BaseAvatar.js
Normal file
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var AvatarLogic = require("../../../Avatar");
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'BaseAvatar',
|
||||
|
||||
propTypes: {
|
||||
name: React.PropTypes.string.isRequired, // The name (first initial used as default)
|
||||
idName: React.PropTypes.string, // ID for generating hash colours
|
||||
title: React.PropTypes.string, // onHover title text
|
||||
url: React.PropTypes.string, // highest priority of them all, shortcut to set in urls[0]
|
||||
urls: React.PropTypes.array, // [highest_priority, ... , lowest_priority]
|
||||
width: React.PropTypes.number,
|
||||
height: React.PropTypes.number,
|
||||
resizeMethod: React.PropTypes.string,
|
||||
defaultToInitialLetter: React.PropTypes.bool // true to add default url
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
width: 40,
|
||||
height: 40,
|
||||
resizeMethod: 'crop',
|
||||
defaultToInitialLetter: true
|
||||
}
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return this._getState(this.props);
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
// work out if we need to call setState (if the image URLs array has changed)
|
||||
var newState = this._getState(nextProps);
|
||||
var newImageUrls = newState.imageUrls;
|
||||
var oldImageUrls = this.state.imageUrls;
|
||||
if (newImageUrls.length !== oldImageUrls.length) {
|
||||
this.setState(newState); // detected a new entry
|
||||
}
|
||||
else {
|
||||
// check each one to see if they are the same
|
||||
for (var i = 0; i < newImageUrls.length; i++) {
|
||||
if (oldImageUrls[i] !== newImageUrls[i]) {
|
||||
this.setState(newState); // detected a diff
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_getState: function(props) {
|
||||
// work out the full set of urls to try to load. This is formed like so:
|
||||
// imageUrls: [ props.url, props.urls, default image ]
|
||||
|
||||
var urls = props.urls || [];
|
||||
if (props.url) {
|
||||
urls.unshift(props.url); // put in urls[0]
|
||||
}
|
||||
|
||||
var defaultImageUrl = null;
|
||||
if (props.defaultToInitialLetter) {
|
||||
defaultImageUrl = AvatarLogic.defaultAvatarUrlForString(
|
||||
props.idName || props.name
|
||||
);
|
||||
urls.push(defaultImageUrl); // lowest priority
|
||||
}
|
||||
return {
|
||||
imageUrls: urls,
|
||||
defaultImageUrl: defaultImageUrl,
|
||||
urlsIndex: 0
|
||||
};
|
||||
},
|
||||
|
||||
onError: function(ev) {
|
||||
var nextIndex = this.state.urlsIndex + 1;
|
||||
if (nextIndex < this.state.imageUrls.length) {
|
||||
// try the next one
|
||||
this.setState({
|
||||
urlsIndex: nextIndex
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_getInitialLetter: function() {
|
||||
var name = this.props.name;
|
||||
var initial = name[0];
|
||||
if ((initial === '@' || initial === '#') && name[1]) {
|
||||
initial = name[1];
|
||||
}
|
||||
return initial.toUpperCase();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var name = this.props.name;
|
||||
|
||||
var imageUrl = this.state.imageUrls[this.state.urlsIndex];
|
||||
|
||||
if (imageUrl === this.state.defaultImageUrl) {
|
||||
var initialLetter = this._getInitialLetter();
|
||||
return (
|
||||
<span className="mx_BaseAvatar" {...this.props}>
|
||||
<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 }
|
||||
</span>
|
||||
<img className="mx_BaseAvatar_image" src={imageUrl}
|
||||
title={this.props.title} onError={this.onError}
|
||||
width={this.props.width} height={this.props.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}
|
||||
{...this.props} />
|
||||
);
|
||||
}
|
||||
});
|
|
@ -18,7 +18,7 @@ limitations under the License.
|
|||
|
||||
var React = require('react');
|
||||
var Avatar = require('../../../Avatar');
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
var sdk = require("../../../index");
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MemberAvatar',
|
||||
|
@ -27,7 +27,7 @@ module.exports = React.createClass({
|
|||
member: React.PropTypes.object.isRequired,
|
||||
width: React.PropTypes.number,
|
||||
height: React.PropTypes.number,
|
||||
resizeMethod: React.PropTypes.string,
|
||||
resizeMethod: React.PropTypes.string
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -38,75 +38,30 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
this.refreshUrl();
|
||||
},
|
||||
|
||||
defaultAvatarUrl: function(member, width, height, resizeMethod) {
|
||||
return Avatar.defaultAvatarUrlForString(member.userId);
|
||||
},
|
||||
|
||||
onError: function(ev) {
|
||||
// don't tightloop if the browser can't load a data url
|
||||
if (ev.target.src == this.defaultAvatarUrl(this.props.member)) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
imageUrl: this.defaultAvatarUrl(this.props.member)
|
||||
});
|
||||
},
|
||||
|
||||
_computeUrl: function() {
|
||||
return Avatar.avatarUrlForMember(this.props.member,
|
||||
this.props.width,
|
||||
this.props.height,
|
||||
this.props.resizeMethod);
|
||||
},
|
||||
|
||||
refreshUrl: function() {
|
||||
var newUrl = this._computeUrl();
|
||||
if (newUrl != this.currentUrl) {
|
||||
this.currentUrl = newUrl;
|
||||
this.setState({imageUrl: newUrl});
|
||||
}
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
imageUrl: this._computeUrl()
|
||||
};
|
||||
return this._getState(this.props);
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
this.setState(this._getState(nextProps));
|
||||
},
|
||||
|
||||
///////////////
|
||||
_getState: function(props) {
|
||||
return {
|
||||
name: props.member.name,
|
||||
title: props.member.userId,
|
||||
imageUrl: Avatar.avatarUrlForMember(props.member,
|
||||
props.width,
|
||||
props.height,
|
||||
props.resizeMethod)
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
// XXX: recalculates default avatar url constantly
|
||||
if (this.state.imageUrl === this.defaultAvatarUrl(this.props.member)) {
|
||||
var initial;
|
||||
if (this.props.member.name[0])
|
||||
initial = this.props.member.name[0].toUpperCase();
|
||||
if (initial === '@' && this.props.member.name[1])
|
||||
initial = this.props.member.name[1].toUpperCase();
|
||||
|
||||
return (
|
||||
<span className="mx_MemberAvatar" {...this.props}>
|
||||
<span className="mx_MemberAvatar_initial" aria-hidden="true"
|
||||
style={{ fontSize: (this.props.width * 0.65) + "px",
|
||||
width: this.props.width + "px",
|
||||
lineHeight: this.props.height + "px" }}>{ initial }</span>
|
||||
<img className="mx_MemberAvatar_image" src={this.state.imageUrl} title={this.props.member.name}
|
||||
onError={this.onError} width={this.props.width} height={this.props.height} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
return (
|
||||
<img className="mx_MemberAvatar mx_MemberAvatar_image" src={this.state.imageUrl}
|
||||
onError={this.onError}
|
||||
width={this.props.width} height={this.props.height}
|
||||
title={this.props.member.name}
|
||||
{...this.props}
|
||||
/>
|
||||
<BaseAvatar {...this.props} name={this.state.name} title={this.state.title}
|
||||
idName={this.props.member.userId} url={this.state.imageUrl} />
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -16,10 +16,18 @@ limitations under the License.
|
|||
var React = require('react');
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
var Avatar = require('../../../Avatar');
|
||||
var sdk = require("../../../index");
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomAvatar',
|
||||
|
||||
propTypes: {
|
||||
room: React.PropTypes.object.isRequired,
|
||||
width: React.PropTypes.number,
|
||||
height: React.PropTypes.number,
|
||||
resizeMethod: React.PropTypes.string
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
width: 36,
|
||||
|
@ -29,84 +37,54 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
getInitialState: function() {
|
||||
this._update();
|
||||
return {
|
||||
imageUrl: this._nextUrl()
|
||||
urls: this.getImageUrls(this.props)
|
||||
};
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
this.refreshImageUrl();
|
||||
componentWillReceiveProps: function(newProps) {
|
||||
this.setState({
|
||||
urls: this.getImageUrls(newProps)
|
||||
})
|
||||
},
|
||||
|
||||
refreshImageUrl: function(nextProps) {
|
||||
// If the list has changed, we start from scratch and re-check, but
|
||||
// don't do so unless the list has changed or we'd re-try fetching
|
||||
// images each time we re-rendered
|
||||
var newList = this.getUrlList();
|
||||
var differs = false;
|
||||
for (var i = 0; i < newList.length && i < this.urlList.length; ++i) {
|
||||
if (this.urlList[i] != newList[i]) differs = true;
|
||||
}
|
||||
if (this.urlList.length != newList.length) differs = true;
|
||||
|
||||
if (differs) {
|
||||
this._update();
|
||||
this.setState({
|
||||
imageUrl: this._nextUrl()
|
||||
});
|
||||
}
|
||||
getImageUrls: function(props) {
|
||||
return [
|
||||
this.getRoomAvatarUrl(props), // highest priority
|
||||
this.getOneToOneAvatar(props),
|
||||
this.getFallbackAvatar(props) // lowest priority
|
||||
].filter(function(url) {
|
||||
return url != null;
|
||||
});
|
||||
},
|
||||
|
||||
_update: function() {
|
||||
this.urlList = this.getUrlList();
|
||||
this.urlListIndex = -1;
|
||||
},
|
||||
|
||||
_nextUrl: function() {
|
||||
do {
|
||||
++this.urlListIndex;
|
||||
} while (
|
||||
this.urlList[this.urlListIndex] === null &&
|
||||
this.urlListIndex < this.urlList.length
|
||||
);
|
||||
if (this.urlListIndex < this.urlList.length) {
|
||||
return this.urlList[this.urlListIndex];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
// provided to the view class for convenience
|
||||
roomAvatarUrl: function() {
|
||||
var url = this.props.room.getAvatarUrl(
|
||||
getRoomAvatarUrl: function(props) {
|
||||
return props.room.getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
this.props.width, this.props.height, this.props.resizeMethod,
|
||||
props.width, props.height, props.resizeMethod,
|
||||
false
|
||||
);
|
||||
return url;
|
||||
},
|
||||
|
||||
// provided to the view class for convenience
|
||||
getOneToOneAvatar: function() {
|
||||
var userIds = Object.keys(this.props.room.currentState.members);
|
||||
getOneToOneAvatar: function(props) {
|
||||
var userIds = Object.keys(props.room.currentState.members);
|
||||
|
||||
if (userIds.length == 2) {
|
||||
var theOtherGuy = null;
|
||||
if (this.props.room.currentState.members[userIds[0]].userId == MatrixClientPeg.get().credentials.userId) {
|
||||
theOtherGuy = this.props.room.currentState.members[userIds[1]];
|
||||
if (props.room.currentState.members[userIds[0]].userId == MatrixClientPeg.get().credentials.userId) {
|
||||
theOtherGuy = props.room.currentState.members[userIds[1]];
|
||||
} else {
|
||||
theOtherGuy = this.props.room.currentState.members[userIds[0]];
|
||||
theOtherGuy = props.room.currentState.members[userIds[0]];
|
||||
}
|
||||
return theOtherGuy.getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
this.props.width, this.props.height, this.props.resizeMethod,
|
||||
props.width, props.height, props.resizeMethod,
|
||||
false
|
||||
);
|
||||
} else if (userIds.length == 1) {
|
||||
return this.props.room.currentState.members[userIds[0]].getAvatarUrl(
|
||||
return props.room.currentState.members[userIds[0]].getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
this.props.width, this.props.height, this.props.resizeMethod,
|
||||
props.width, props.height, props.resizeMethod,
|
||||
false
|
||||
);
|
||||
} else {
|
||||
|
@ -114,58 +92,15 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
|
||||
onError: function(ev) {
|
||||
this.setState({
|
||||
imageUrl: this._nextUrl()
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
|
||||
////////////
|
||||
|
||||
|
||||
getUrlList: function() {
|
||||
return [
|
||||
this.roomAvatarUrl(),
|
||||
this.getOneToOneAvatar(),
|
||||
this.getFallbackAvatar()
|
||||
];
|
||||
},
|
||||
|
||||
getFallbackAvatar: function() {
|
||||
return Avatar.defaultAvatarUrlForString(this.props.room.roomId);
|
||||
getFallbackAvatar: function(props) {
|
||||
return Avatar.defaultAvatarUrlForString(props.room.roomId);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var style = {
|
||||
width: this.props.width,
|
||||
height: this.props.height,
|
||||
};
|
||||
|
||||
// XXX: recalculates fallback avatar constantly
|
||||
if (this.state.imageUrl === this.getFallbackAvatar()) {
|
||||
var initial;
|
||||
if (this.props.room.name[0])
|
||||
initial = this.props.room.name[0].toUpperCase();
|
||||
if ((initial === '@' || initial === '#') && this.props.room.name[1])
|
||||
initial = this.props.room.name[1].toUpperCase();
|
||||
|
||||
return (
|
||||
<span>
|
||||
<span className="mx_RoomAvatar_initial" aria-hidden="true"
|
||||
style={{ fontSize: (this.props.width * 0.65) + "px",
|
||||
width: this.props.width + "px",
|
||||
lineHeight: this.props.height + "px" }}>{ initial }</span>
|
||||
<img className="mx_RoomAvatar" src={this.state.imageUrl}
|
||||
onError={this.onError} style={style} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
else {
|
||||
return <img className="mx_RoomAvatar" src={this.state.imageUrl}
|
||||
onError={this.onError} style={style} />
|
||||
}
|
||||
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
return (
|
||||
<BaseAvatar {...this.props} name={this.props.room.name}
|
||||
idName={this.props.room.roomId} urls={this.state.urls} />
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -18,13 +18,22 @@ limitations under the License.
|
|||
|
||||
var React = require('react');
|
||||
|
||||
const KEY_TAB = 9;
|
||||
const KEY_SHIFT = 16;
|
||||
const KEY_WINDOWS = 91;
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'EditableText',
|
||||
propTypes: {
|
||||
onValueChanged: React.PropTypes.func,
|
||||
initialValue: React.PropTypes.string,
|
||||
label: React.PropTypes.string,
|
||||
placeHolder: React.PropTypes.string,
|
||||
placeholder: React.PropTypes.string,
|
||||
className: React.PropTypes.string,
|
||||
labelClassName: React.PropTypes.string,
|
||||
placeholderClassName: React.PropTypes.string,
|
||||
blurToCancel: React.PropTypes.bool,
|
||||
editable: React.PropTypes.bool,
|
||||
},
|
||||
|
||||
Phases: {
|
||||
|
@ -36,38 +45,62 @@ module.exports = React.createClass({
|
|||
return {
|
||||
onValueChanged: function() {},
|
||||
initialValue: '',
|
||||
label: 'Click to set',
|
||||
label: '',
|
||||
placeholder: '',
|
||||
editable: true,
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
value: this.props.initialValue,
|
||||
phase: this.Phases.Display,
|
||||
}
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
this.setState({
|
||||
value: nextProps.initialValue
|
||||
});
|
||||
if (nextProps.initialValue !== this.props.initialValue) {
|
||||
this.value = nextProps.initialValue;
|
||||
if (this.refs.editable_div) {
|
||||
this.showPlaceholder(!this.value);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
// we track value as an JS object field rather than in React state
|
||||
// as React doesn't play nice with contentEditable.
|
||||
this.value = '';
|
||||
this.placeholder = false;
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.value = this.props.initialValue;
|
||||
if (this.refs.editable_div) {
|
||||
this.showPlaceholder(!this.value);
|
||||
}
|
||||
},
|
||||
|
||||
showPlaceholder: function(show) {
|
||||
if (show) {
|
||||
this.refs.editable_div.textContent = this.props.placeholder;
|
||||
this.refs.editable_div.setAttribute("class", this.props.className + " " + this.props.placeholderClassName);
|
||||
this.placeholder = true;
|
||||
this.value = '';
|
||||
}
|
||||
else {
|
||||
this.refs.editable_div.textContent = this.value;
|
||||
this.refs.editable_div.setAttribute("class", this.props.className);
|
||||
this.placeholder = false;
|
||||
}
|
||||
},
|
||||
|
||||
getValue: function() {
|
||||
return this.state.value;
|
||||
return this.value;
|
||||
},
|
||||
|
||||
setValue: function(val, shouldSubmit, suppressListener) {
|
||||
var self = this;
|
||||
this.setState({
|
||||
value: val,
|
||||
phase: this.Phases.Display,
|
||||
}, function() {
|
||||
if (!suppressListener) {
|
||||
self.onValueChanged(shouldSubmit);
|
||||
}
|
||||
});
|
||||
setValue: function(value) {
|
||||
this.value = value;
|
||||
this.showPlaceholder(!this.value);
|
||||
},
|
||||
|
||||
edit: function() {
|
||||
|
@ -80,65 +113,106 @@ module.exports = React.createClass({
|
|||
this.setState({
|
||||
phase: this.Phases.Display,
|
||||
});
|
||||
this.value = this.props.initialValue;
|
||||
this.showPlaceholder(!this.value);
|
||||
this.onValueChanged(false);
|
||||
},
|
||||
|
||||
onValueChanged: function(shouldSubmit) {
|
||||
this.props.onValueChanged(this.state.value, shouldSubmit);
|
||||
this.props.onValueChanged(this.value, shouldSubmit);
|
||||
},
|
||||
|
||||
onKeyDown: function(ev) {
|
||||
// console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
|
||||
|
||||
if (this.placeholder) {
|
||||
this.showPlaceholder(false);
|
||||
}
|
||||
|
||||
if (ev.key == "Enter") {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
// console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
|
||||
},
|
||||
|
||||
onKeyUp: function(ev) {
|
||||
// console.log("keyUp: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
|
||||
|
||||
if (!ev.target.textContent) {
|
||||
this.showPlaceholder(true);
|
||||
}
|
||||
else if (!this.placeholder) {
|
||||
this.value = ev.target.textContent;
|
||||
}
|
||||
|
||||
if (ev.key == "Enter") {
|
||||
this.onFinish(ev);
|
||||
} else if (ev.key == "Escape") {
|
||||
this.cancelEdit();
|
||||
}
|
||||
|
||||
// console.log("keyUp: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
|
||||
},
|
||||
|
||||
onClickDiv: function() {
|
||||
onClickDiv: function(ev) {
|
||||
if (!this.props.editable) return;
|
||||
|
||||
this.setState({
|
||||
phase: this.Phases.Edit,
|
||||
})
|
||||
},
|
||||
|
||||
onFocus: function(ev) {
|
||||
ev.target.setSelectionRange(0, ev.target.value.length);
|
||||
},
|
||||
//ev.target.setSelectionRange(0, ev.target.textContent.length);
|
||||
|
||||
onFinish: function(ev) {
|
||||
if (ev.target.value) {
|
||||
this.setValue(ev.target.value, ev.key === "Enter");
|
||||
} else {
|
||||
this.cancelEdit();
|
||||
var node = ev.target.childNodes[0];
|
||||
if (node) {
|
||||
var range = document.createRange();
|
||||
range.setStart(node, 0);
|
||||
range.setEnd(node, node.length);
|
||||
|
||||
var sel = window.getSelection();
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
}
|
||||
},
|
||||
|
||||
onBlur: function() {
|
||||
this.cancelEdit();
|
||||
onFinish: function(ev) {
|
||||
var self = this;
|
||||
var submit = (ev.key === "Enter");
|
||||
this.setState({
|
||||
phase: this.Phases.Display,
|
||||
}, function() {
|
||||
self.onValueChanged(submit);
|
||||
});
|
||||
},
|
||||
|
||||
onBlur: function(ev) {
|
||||
var sel = window.getSelection();
|
||||
sel.removeAllRanges();
|
||||
|
||||
if (this.props.blurToCancel)
|
||||
this.cancelEdit();
|
||||
else
|
||||
this.onFinish(ev);
|
||||
|
||||
this.showPlaceholder(!this.value);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var editable_el;
|
||||
|
||||
if (this.state.phase == this.Phases.Display) {
|
||||
if (this.state.value) {
|
||||
editable_el = <div ref="display_div" onClick={this.onClickDiv}>{this.state.value}</div>;
|
||||
} else {
|
||||
editable_el = <div ref="display_div" onClick={this.onClickDiv}>{this.props.label}</div>;
|
||||
}
|
||||
} else if (this.state.phase == this.Phases.Edit) {
|
||||
editable_el = (
|
||||
<div>
|
||||
<input type="text" defaultValue={this.state.value}
|
||||
onKeyUp={this.onKeyUp} onFocus={this.onFocus} onBlur={this.onBlur} placeholder={this.props.placeHolder} autoFocus/>
|
||||
</div>
|
||||
);
|
||||
if (!this.props.editable || (this.state.phase == this.Phases.Display && (this.props.label || this.props.labelClassName) && !this.value)) {
|
||||
// show the label
|
||||
editable_el = <div className={this.props.className + " " + this.props.labelClassName} onClick={this.onClickDiv}>{ this.props.label || this.props.initialValue }</div>;
|
||||
} else {
|
||||
// show the content editable div, but manually manage its contents as react and contentEditable don't play nice together
|
||||
editable_el = <div ref="editable_div" contentEditable="true" className={this.props.className}
|
||||
onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} onFocus={this.onFocus} onBlur={this.onBlur}></div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_EditableText">
|
||||
{editable_el}
|
||||
</div>
|
||||
);
|
||||
return editable_el;
|
||||
}
|
||||
});
|
||||
|
|
108
src/components/views/elements/PowerSelector.js
Normal file
108
src/components/views/elements/PowerSelector.js
Normal file
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
|
||||
var roles = {
|
||||
0: 'User',
|
||||
50: 'Moderator',
|
||||
100: 'Admin',
|
||||
};
|
||||
|
||||
var reverseRoles = {};
|
||||
Object.keys(roles).forEach(function(key) {
|
||||
reverseRoles[roles[key]] = key;
|
||||
});
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'PowerSelector',
|
||||
|
||||
propTypes: {
|
||||
value: React.PropTypes.number.isRequired,
|
||||
disabled: React.PropTypes.bool,
|
||||
onChange: React.PropTypes.func,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
custom: (roles[this.props.value] === undefined),
|
||||
};
|
||||
},
|
||||
|
||||
onSelectChange: function(event) {
|
||||
this.state.custom = (event.target.value === "Custom");
|
||||
this.props.onChange(this.getValue());
|
||||
},
|
||||
|
||||
onCustomBlur: function(event) {
|
||||
this.props.onChange(this.getValue());
|
||||
},
|
||||
|
||||
onCustomKeyDown: function(event) {
|
||||
if (event.key == "Enter") {
|
||||
this.props.onChange(this.getValue());
|
||||
}
|
||||
},
|
||||
|
||||
getValue: function() {
|
||||
var value;
|
||||
if (this.refs.select) {
|
||||
value = reverseRoles[ this.refs.select.value ];
|
||||
if (this.refs.custom) {
|
||||
if (value === undefined) value = parseInt( this.refs.custom.value );
|
||||
}
|
||||
}
|
||||
return value;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var customPicker;
|
||||
if (this.state.custom) {
|
||||
var input;
|
||||
if (this.props.disabled) {
|
||||
input = <span>{ this.props.value }</span>
|
||||
}
|
||||
else {
|
||||
input = <input ref="custom" type="text" size="3" defaultValue={ this.props.value } onBlur={ this.onCustomBlur } onKeyDown={ this.onCustomKeyDown }/>
|
||||
}
|
||||
customPicker = <span> of { input }</span>;
|
||||
}
|
||||
|
||||
var selectValue = roles[this.props.value] || "Custom";
|
||||
var select;
|
||||
if (this.props.disabled) {
|
||||
select = <span>{ selectValue }</span>;
|
||||
}
|
||||
else {
|
||||
select =
|
||||
<select ref="select" defaultValue={ selectValue } onChange={ this.onSelectChange }>
|
||||
<option value="User">User (0)</option>
|
||||
<option value="Moderator">Moderator (50)</option>
|
||||
<option value="Admin">Admin (100)</option>
|
||||
<option value="Custom">Custom level</option>
|
||||
</select>
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="mx_PowerSelector">
|
||||
{ select }
|
||||
{ customPicker }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
});
|
|
@ -22,7 +22,8 @@ var ReactDOM = require('react-dom');
|
|||
*/
|
||||
module.exports = React.createClass({displayName: 'PasswordLogin',
|
||||
propTypes: {
|
||||
onSubmit: React.PropTypes.func.isRequired // fn(username, password)
|
||||
onSubmit: React.PropTypes.func.isRequired, // fn(username, password)
|
||||
onForgotPasswordClick: React.PropTypes.func // fn()
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -46,6 +47,16 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
|
|||
},
|
||||
|
||||
render: function() {
|
||||
var forgotPasswordJsx;
|
||||
|
||||
if (this.props.onForgotPasswordClick) {
|
||||
forgotPasswordJsx = (
|
||||
<a className="mx_Login_forgot" onClick={this.props.onForgotPasswordClick} href="#">
|
||||
Forgot your password?
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={this.onSubmitForm}>
|
||||
|
@ -57,6 +68,7 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
|
|||
value={this.state.password} onChange={this.onPasswordChanged}
|
||||
placeholder="Password" />
|
||||
<br />
|
||||
{forgotPasswordJsx}
|
||||
<input className="mx_Login_submit" type="submit" value="Log in" />
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -17,8 +17,15 @@ limitations under the License.
|
|||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var Velocity = require('velocity-animate');
|
||||
require('velocity-ui-pack');
|
||||
var sdk = require('../../../index');
|
||||
|
||||
var FIELD_EMAIL = 'field_email';
|
||||
var FIELD_USERNAME = 'field_username';
|
||||
var FIELD_PASSWORD = 'field_password';
|
||||
var FIELD_PASSWORD_CONFIRM = 'field_password_confirm';
|
||||
|
||||
/**
|
||||
* A pure UI component which displays a registration form.
|
||||
*/
|
||||
|
@ -50,52 +57,151 @@ module.exports = React.createClass({
|
|||
email: this.props.defaultEmail,
|
||||
username: this.props.defaultUsername,
|
||||
password: null,
|
||||
passwordConfirm: null
|
||||
passwordConfirm: null,
|
||||
fieldValid: {}
|
||||
};
|
||||
},
|
||||
|
||||
onSubmit: function(ev) {
|
||||
ev.preventDefault();
|
||||
|
||||
var pwd1 = this.refs.password.value.trim();
|
||||
var pwd2 = this.refs.passwordConfirm.value.trim()
|
||||
// validate everything, in reverse order so
|
||||
// the error that ends up being displayed
|
||||
// is the one from the first invalid field.
|
||||
// It's not super ideal that this just calls
|
||||
// onError once for each invalid field.
|
||||
this.validateField(FIELD_PASSWORD_CONFIRM);
|
||||
this.validateField(FIELD_PASSWORD);
|
||||
this.validateField(FIELD_USERNAME);
|
||||
this.validateField(FIELD_EMAIL);
|
||||
|
||||
var errCode;
|
||||
if (!pwd1 || !pwd2) {
|
||||
errCode = "RegistrationForm.ERR_PASSWORD_MISSING";
|
||||
}
|
||||
else if (pwd1 !== pwd2) {
|
||||
errCode = "RegistrationForm.ERR_PASSWORD_MISMATCH";
|
||||
}
|
||||
else if (pwd1.length < this.props.minPasswordLength) {
|
||||
errCode = "RegistrationForm.ERR_PASSWORD_LENGTH";
|
||||
}
|
||||
if (errCode) {
|
||||
this.props.onError(errCode);
|
||||
return;
|
||||
}
|
||||
|
||||
var promise = this.props.onRegisterClick({
|
||||
username: this.refs.username.value.trim(),
|
||||
password: pwd1,
|
||||
email: this.refs.email.value.trim()
|
||||
});
|
||||
|
||||
if (promise) {
|
||||
ev.target.disabled = true;
|
||||
promise.finally(function() {
|
||||
ev.target.disabled = false;
|
||||
if (this.allFieldsValid()) {
|
||||
var promise = this.props.onRegisterClick({
|
||||
username: this.refs.username.value.trim(),
|
||||
password: this.refs.password.value.trim(),
|
||||
email: this.refs.email.value.trim()
|
||||
});
|
||||
|
||||
if (promise) {
|
||||
ev.target.disabled = true;
|
||||
promise.finally(function() {
|
||||
ev.target.disabled = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true if all fields were valid last time
|
||||
* they were validated.
|
||||
*/
|
||||
allFieldsValid: function() {
|
||||
var keys = Object.keys(this.state.fieldValid);
|
||||
for (var i = 0; i < keys.length; ++i) {
|
||||
if (this.state.fieldValid[keys[i]] == false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
validateField: function(field_id) {
|
||||
var pwd1 = this.refs.password.value.trim();
|
||||
var pwd2 = this.refs.passwordConfirm.value.trim()
|
||||
|
||||
switch (field_id) {
|
||||
case FIELD_EMAIL:
|
||||
this.markFieldValid(
|
||||
field_id,
|
||||
this.refs.email.value == '' || !!this.refs.email.value.match(/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i),
|
||||
"RegistrationForm.ERR_EMAIL_INVALID"
|
||||
);
|
||||
break;
|
||||
case FIELD_USERNAME:
|
||||
// XXX: SPEC-1
|
||||
if (encodeURIComponent(this.refs.username.value) != this.refs.username.value) {
|
||||
this.markFieldValid(
|
||||
field_id,
|
||||
false,
|
||||
"RegistrationForm.ERR_USERNAME_INVALID"
|
||||
);
|
||||
} else if (this.refs.username.value == '') {
|
||||
this.markFieldValid(
|
||||
field_id,
|
||||
false,
|
||||
"RegistrationForm.ERR_USERNAME_BLANK"
|
||||
);
|
||||
} else {
|
||||
this.markFieldValid(field_id, true);
|
||||
}
|
||||
break;
|
||||
case FIELD_PASSWORD:
|
||||
if (pwd1 == '') {
|
||||
this.markFieldValid(
|
||||
field_id,
|
||||
false,
|
||||
"RegistrationForm.ERR_PASSWORD_MISSING"
|
||||
);
|
||||
} else if (pwd1.length < this.props.minPasswordLength) {
|
||||
this.markFieldValid(
|
||||
field_id,
|
||||
false,
|
||||
"RegistrationForm.ERR_PASSWORD_LENGTH"
|
||||
);
|
||||
} else {
|
||||
this.markFieldValid(field_id, true);
|
||||
}
|
||||
break;
|
||||
case FIELD_PASSWORD_CONFIRM:
|
||||
this.markFieldValid(
|
||||
field_id, pwd1 == pwd2,
|
||||
"RegistrationForm.ERR_PASSWORD_MISMATCH"
|
||||
);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
markFieldValid: function(field_id, val, error_code) {
|
||||
var fieldValid = this.state.fieldValid;
|
||||
fieldValid[field_id] = val;
|
||||
this.setState({fieldValid: fieldValid});
|
||||
if (!val) {
|
||||
Velocity(this.fieldElementById(field_id), "callout.shake", 300);
|
||||
this.props.onError(error_code);
|
||||
}
|
||||
},
|
||||
|
||||
fieldElementById(field_id) {
|
||||
switch (field_id) {
|
||||
case FIELD_EMAIL:
|
||||
return this.refs.email;
|
||||
case FIELD_USERNAME:
|
||||
return this.refs.username;
|
||||
case FIELD_PASSWORD:
|
||||
return this.refs.password;
|
||||
case FIELD_PASSWORD_CONFIRM:
|
||||
return this.refs.passwordConfirm;
|
||||
}
|
||||
},
|
||||
|
||||
_styleField: function(field_id, baseStyle) {
|
||||
var style = baseStyle || {};
|
||||
if (this.state.fieldValid[field_id] === false) {
|
||||
style['borderColor'] = 'red';
|
||||
}
|
||||
return style;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var self = this;
|
||||
var emailSection, registerButton;
|
||||
if (this.props.showEmail) {
|
||||
emailSection = (
|
||||
<input className="mx_Login_field" type="text" ref="email"
|
||||
autoFocus={true} placeholder="Email address"
|
||||
defaultValue={this.state.email} />
|
||||
defaultValue={this.state.email}
|
||||
style={this._styleField(FIELD_EMAIL)}
|
||||
onBlur={function() {self.validateField(FIELD_EMAIL)}} />
|
||||
);
|
||||
}
|
||||
if (this.props.onRegisterClick) {
|
||||
|
@ -111,13 +217,19 @@ module.exports = React.createClass({
|
|||
<br />
|
||||
<input className="mx_Login_field" type="text" ref="username"
|
||||
placeholder="User name" defaultValue={this.state.username}
|
||||
style={this._styleField(FIELD_USERNAME)}
|
||||
onBlur={function() {self.validateField(FIELD_USERNAME)}}
|
||||
disabled={this.props.disableUsernameChanges} />
|
||||
<br />
|
||||
<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} />
|
||||
<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} />
|
||||
<br />
|
||||
{registerButton}
|
||||
|
|
|
@ -36,6 +36,9 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
componentDidUpdate: function() {
|
||||
// XXX: why don't we linkify here?
|
||||
// XXX: why do we bother doing this on update at all, given events are immutable?
|
||||
|
||||
if (this.props.mxEvent.getContent().format === "org.matrix.custom.html")
|
||||
HtmlUtils.highlightDom(ReactDOM.findDOMNode(this));
|
||||
},
|
||||
|
|
|
@ -58,15 +58,16 @@ module.exports = React.createClass({
|
|||
var roomId = this.props.member.roomId;
|
||||
var target = this.props.member.userId;
|
||||
MatrixClientPeg.get().kick(roomId, target).done(function() {
|
||||
// NO-OP; rely on the m.room.member event coming down else we could
|
||||
// get out of sync if we force setState here!
|
||||
console.log("Kick success");
|
||||
}, function(err) {
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Kick error",
|
||||
description: err.message
|
||||
});
|
||||
});
|
||||
// NO-OP; rely on the m.room.member event coming down else we could
|
||||
// get out of sync if we force setState here!
|
||||
console.log("Kick success");
|
||||
}, function(err) {
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Kick error",
|
||||
description: err.message
|
||||
});
|
||||
}
|
||||
);
|
||||
this.props.onFinished();
|
||||
},
|
||||
|
||||
|
@ -74,16 +75,18 @@ module.exports = React.createClass({
|
|||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
var roomId = this.props.member.roomId;
|
||||
var target = this.props.member.userId;
|
||||
MatrixClientPeg.get().ban(roomId, target).done(function() {
|
||||
// NO-OP; rely on the m.room.member event coming down else we could
|
||||
// get out of sync if we force setState here!
|
||||
console.log("Ban success");
|
||||
}, function(err) {
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Ban error",
|
||||
description: err.message
|
||||
});
|
||||
});
|
||||
MatrixClientPeg.get().ban(roomId, target).done(
|
||||
function() {
|
||||
// NO-OP; rely on the m.room.member event coming down else we could
|
||||
// get out of sync if we force setState here!
|
||||
console.log("Ban success");
|
||||
}, function(err) {
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Ban error",
|
||||
description: err.message
|
||||
});
|
||||
}
|
||||
);
|
||||
this.props.onFinished();
|
||||
},
|
||||
|
||||
|
@ -118,16 +121,17 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
MatrixClientPeg.get().setPowerLevel(roomId, target, level, powerLevelEvent).done(
|
||||
function() {
|
||||
// NO-OP; rely on the m.room.member event coming down else we could
|
||||
// get out of sync if we force setState here!
|
||||
console.log("Mute toggle success");
|
||||
}, function(err) {
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Mute error",
|
||||
description: err.message
|
||||
});
|
||||
});
|
||||
function() {
|
||||
// NO-OP; rely on the m.room.member event coming down else we could
|
||||
// get out of sync if we force setState here!
|
||||
console.log("Mute toggle success");
|
||||
}, function(err) {
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Mute error",
|
||||
description: err.message
|
||||
});
|
||||
}
|
||||
);
|
||||
this.props.onFinished();
|
||||
},
|
||||
|
||||
|
@ -154,22 +158,55 @@ module.exports = React.createClass({
|
|||
}
|
||||
var defaultLevel = powerLevelEvent.getContent().users_default;
|
||||
var modLevel = me.powerLevel - 1;
|
||||
if (modLevel > 50 && defaultLevel < 50) modLevel = 50; // try to stick with the vector level defaults
|
||||
// toggle the level
|
||||
var newLevel = this.state.isTargetMod ? defaultLevel : modLevel;
|
||||
MatrixClientPeg.get().setPowerLevel(roomId, target, newLevel, powerLevelEvent).done(
|
||||
function() {
|
||||
// NO-OP; rely on the m.room.member event coming down else we could
|
||||
// get out of sync if we force setState here!
|
||||
console.log("Mod toggle success");
|
||||
}, function(err) {
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Mod error",
|
||||
description: err.message
|
||||
});
|
||||
});
|
||||
function() {
|
||||
// NO-OP; rely on the m.room.member event coming down else we could
|
||||
// get out of sync if we force setState here!
|
||||
console.log("Mod toggle success");
|
||||
}, function(err) {
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Mod error",
|
||||
description: err.message
|
||||
});
|
||||
}
|
||||
);
|
||||
this.props.onFinished();
|
||||
},
|
||||
|
||||
onPowerChange: function(powerLevel) {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
var roomId = this.props.member.roomId;
|
||||
var target = this.props.member.userId;
|
||||
var room = MatrixClientPeg.get().getRoom(roomId);
|
||||
if (!room) {
|
||||
this.props.onFinished();
|
||||
return;
|
||||
}
|
||||
var powerLevelEvent = room.currentState.getStateEvents(
|
||||
"m.room.power_levels", ""
|
||||
);
|
||||
if (!powerLevelEvent) {
|
||||
this.props.onFinished();
|
||||
return;
|
||||
}
|
||||
MatrixClientPeg.get().setPowerLevel(roomId, target, powerLevel, powerLevelEvent).done(
|
||||
function() {
|
||||
// NO-OP; rely on the m.room.member event coming down else we could
|
||||
// get out of sync if we force setState here!
|
||||
console.log("Power change success");
|
||||
}, function(err) {
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Failure to change power level",
|
||||
description: err.message
|
||||
});
|
||||
}
|
||||
);
|
||||
this.props.onFinished();
|
||||
},
|
||||
|
||||
onChatClick: function() {
|
||||
// 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.
|
||||
|
@ -209,20 +246,22 @@ module.exports = React.createClass({
|
|||
MatrixClientPeg.get().createRoom({
|
||||
invite: [this.props.member.userId],
|
||||
preset: "private_chat"
|
||||
}).done(function(res) {
|
||||
self.setState({ creatingRoom: false });
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: res.room_id
|
||||
});
|
||||
self.props.onFinished();
|
||||
}, function(err) {
|
||||
self.setState({ creatingRoom: false });
|
||||
console.error(
|
||||
"Failed to create room: %s", JSON.stringify(err)
|
||||
);
|
||||
self.props.onFinished();
|
||||
});
|
||||
}).done(
|
||||
function(res) {
|
||||
self.setState({ creatingRoom: false });
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: res.room_id
|
||||
});
|
||||
self.props.onFinished();
|
||||
}, function(err) {
|
||||
self.setState({ creatingRoom: false });
|
||||
console.error(
|
||||
"Failed to create room: %s", JSON.stringify(err)
|
||||
);
|
||||
self.props.onFinished();
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -291,9 +330,15 @@ module.exports = React.createClass({
|
|||
(powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) ||
|
||||
powerLevels.state_default
|
||||
);
|
||||
var levelToSend = (
|
||||
(powerLevels.events ? powerLevels.events["m.room.message"] : null) ||
|
||||
powerLevels.events_default
|
||||
);
|
||||
|
||||
can.kick = me.powerLevel >= powerLevels.kick;
|
||||
can.ban = me.powerLevel >= powerLevels.ban;
|
||||
can.mute = me.powerLevel >= editPowerLevel;
|
||||
can.toggleMod = me.powerLevel > them.powerLevel && them.powerLevel >= levelToSend;
|
||||
can.modifyLevel = me.powerLevel > them.powerLevel;
|
||||
return can;
|
||||
},
|
||||
|
@ -317,12 +362,11 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
var interactButton, kickButton, banButton, muteButton, giveModButton, spinner;
|
||||
if (this.props.member.userId === MatrixClientPeg.get().credentials.userId) {
|
||||
interactButton = <div className="mx_MemberInfo_field" onClick={this.onLeaveClick}>Leave room</div>;
|
||||
}
|
||||
else {
|
||||
interactButton = <div className="mx_MemberInfo_field" onClick={this.onChatClick}>Start chat</div>;
|
||||
var startChat, kickButton, banButton, muteButton, giveModButton, spinner;
|
||||
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 }/>
|
||||
}
|
||||
|
||||
if (this.state.creatingRoom) {
|
||||
|
@ -346,35 +390,56 @@ module.exports = React.createClass({
|
|||
{muteLabel}
|
||||
</div>;
|
||||
}
|
||||
if (this.state.can.modifyLevel) {
|
||||
var giveOpLabel = this.state.isTargetMod ? "Revoke Mod" : "Make Mod";
|
||||
if (this.state.can.toggleMod) {
|
||||
var giveOpLabel = this.state.isTargetMod ? "Revoke Moderator" : "Make Moderator";
|
||||
giveModButton = <div className="mx_MemberInfo_field" onClick={this.onModToggle}>
|
||||
{giveOpLabel}
|
||||
</div>
|
||||
}
|
||||
|
||||
// TODO: we should have an invite button if this MemberInfo is showing a user who isn't actually in the current room yet
|
||||
// e.g. clicking on a linkified userid in a room
|
||||
|
||||
var adminTools;
|
||||
if (kickButton || banButton || muteButton || giveModButton) {
|
||||
adminTools =
|
||||
<div>
|
||||
<h3>Admin tools</h3>
|
||||
|
||||
<div className="mx_MemberInfo_buttons">
|
||||
{muteButton}
|
||||
{kickButton}
|
||||
{banButton}
|
||||
{giveModButton}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||
var PowerSelector = sdk.getComponent('elements.PowerSelector');
|
||||
return (
|
||||
<div className="mx_MemberInfo">
|
||||
<img className="mx_MemberInfo_cancel" src="img/cancel.svg" width="18" height="18" onClick={this.onCancel}/>
|
||||
<div className="mx_MemberInfo_avatar">
|
||||
<MemberAvatar member={this.props.member} width={48} height={48} />
|
||||
</div>
|
||||
|
||||
<h2>{ this.props.member.name }</h2>
|
||||
<div className="mx_MemberInfo_profileField">
|
||||
{ this.props.member.userId }
|
||||
</div>
|
||||
<div className="mx_MemberInfo_profileField">
|
||||
power: { this.props.member.powerLevelNorm }%
|
||||
</div>
|
||||
<div className="mx_MemberInfo_buttons">
|
||||
{interactButton}
|
||||
{muteButton}
|
||||
{kickButton}
|
||||
{banButton}
|
||||
{giveModButton}
|
||||
{spinner}
|
||||
|
||||
<div className="mx_MemberInfo_profile">
|
||||
<div className="mx_MemberInfo_profileField">
|
||||
{ this.props.member.userId }
|
||||
</div>
|
||||
<div className="mx_MemberInfo_profileField">
|
||||
Level: <b><PowerSelector value={ parseInt(this.props.member.powerLevel) } disabled={ !this.state.can.modifyLevel } onChange={ this.onPowerChange }/></b>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ startChat }
|
||||
|
||||
{ adminTools }
|
||||
|
||||
{ spinner }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -15,12 +15,19 @@ limitations under the License.
|
|||
*/
|
||||
var React = require('react');
|
||||
var classNames = require('classnames');
|
||||
var Matrix = require("matrix-js-sdk");
|
||||
var q = require('q');
|
||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
var Modal = require("../../../Modal");
|
||||
var sdk = require('../../../index');
|
||||
var GeminiScrollbar = require('react-gemini-scrollbar');
|
||||
|
||||
var INITIAL_LOAD_NUM_MEMBERS = 50;
|
||||
var SHARE_HISTORY_WARNING = "Newly invited users will see the history of this room. "+
|
||||
"If you'd prefer invited users not to see messages that were sent before they joined, "+
|
||||
"turn off, 'Share message history with new users' in the settings for this room.";
|
||||
|
||||
var shown_invite_warning_this_session = false;
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MemberList',
|
||||
|
@ -131,12 +138,41 @@ module.exports = React.createClass({
|
|||
return;
|
||||
}
|
||||
|
||||
var promise;
|
||||
var invite_defer = q.defer();
|
||||
|
||||
var room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||
var history_visibility = room.currentState.getStateEvents('m.room.history_visibility', '');
|
||||
if (history_visibility) history_visibility = history_visibility.getContent().history_visibility;
|
||||
|
||||
if (history_visibility == 'shared' && !shown_invite_warning_this_session) {
|
||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createDialog(QuestionDialog, {
|
||||
title: "Warning",
|
||||
description: SHARE_HISTORY_WARNING,
|
||||
button: "Invite",
|
||||
onFinished: function(should_invite) {
|
||||
if (should_invite) {
|
||||
shown_invite_warning_this_session = true;
|
||||
invite_defer.resolve();
|
||||
} else {
|
||||
invite_defer.reject(null);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
invite_defer.resolve();
|
||||
}
|
||||
|
||||
var promise = invite_defer.promise;;
|
||||
if (isEmailAddress) {
|
||||
promise = MatrixClientPeg.get().inviteByEmail(this.props.roomId, inputText);
|
||||
promise = promise.then(function() {
|
||||
MatrixClientPeg.get().inviteByEmail(self.props.roomId, inputText);
|
||||
});
|
||||
}
|
||||
else {
|
||||
promise = MatrixClientPeg.get().invite(this.props.roomId, inputText);
|
||||
promise = promise.then(function() {
|
||||
MatrixClientPeg.get().invite(self.props.roomId, inputText);
|
||||
});
|
||||
}
|
||||
|
||||
self.setState({
|
||||
|
@ -151,11 +187,13 @@ module.exports = React.createClass({
|
|||
inviting: false
|
||||
});
|
||||
}, function(err) {
|
||||
console.error("Failed to invite: %s", JSON.stringify(err));
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Server error whilst inviting",
|
||||
description: err.message
|
||||
});
|
||||
if (err !== null) {
|
||||
console.error("Failed to invite: %s", JSON.stringify(err));
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Server error whilst inviting",
|
||||
description: err.message
|
||||
});
|
||||
}
|
||||
self.setState({
|
||||
inviting: false
|
||||
});
|
||||
|
@ -229,7 +267,8 @@ module.exports = React.createClass({
|
|||
var MemberTile = sdk.getComponent("rooms.MemberTile");
|
||||
|
||||
var self = this;
|
||||
return self.state.members.filter(function(userId) {
|
||||
|
||||
var memberList = self.state.members.filter(function(userId) {
|
||||
var m = self.memberDict[userId];
|
||||
return m.membership == membership;
|
||||
}).map(function(userId) {
|
||||
|
@ -238,6 +277,31 @@ module.exports = React.createClass({
|
|||
<MemberTile key={userId} member={m} ref={userId} />
|
||||
);
|
||||
});
|
||||
|
||||
if (membership === "invite") {
|
||||
// include 3pid invites (m.room.third_party_invite) state events.
|
||||
// The HS may have already converted these into m.room.member invites so
|
||||
// we shouldn't add them if the 3pid invite state key (token) is in the
|
||||
// member invite (content.third_party_invite.signed.token)
|
||||
var room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||
if (room) {
|
||||
room.currentState.getStateEvents("m.room.third_party_invite").forEach(
|
||||
function(e) {
|
||||
// discard all invites which have a m.room.member event since we've
|
||||
// already added them.
|
||||
var memberEvent = room.currentState.getInviteForThreePidToken(e.getStateKey());
|
||||
if (memberEvent) {
|
||||
return;
|
||||
}
|
||||
memberList.push(
|
||||
<MemberTile key={e.getStateKey()} ref={e.getStateKey()}
|
||||
customDisplayName={e.getContent().display_name} />
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return memberList;
|
||||
},
|
||||
|
||||
onPopulateInvite: function(e) {
|
||||
|
@ -254,7 +318,7 @@ module.exports = React.createClass({
|
|||
} else {
|
||||
return (
|
||||
<form onSubmit={this.onPopulateInvite}>
|
||||
<input className="mx_MemberList_invite" ref="invite" placeholder="Invite user (email)"/>
|
||||
<input className="mx_MemberList_invite" ref="invite" id="mx_MemberList_invite" placeholder="Invite user (email)"/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -26,20 +26,19 @@ var Modal = require("../../../Modal");
|
|||
module.exports = React.createClass({
|
||||
displayName: 'MemberTile',
|
||||
|
||||
propTypes: {
|
||||
member: React.PropTypes.any, // RoomMember
|
||||
onFinished: React.PropTypes.func,
|
||||
customDisplayName: React.PropTypes.string // for 3pid invites
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {};
|
||||
},
|
||||
|
||||
onLeaveClick: function() {
|
||||
dis.dispatch({
|
||||
action: 'leave_room',
|
||||
room_id: this.props.member.roomId,
|
||||
});
|
||||
this.props.onFinished();
|
||||
},
|
||||
|
||||
shouldComponentUpdate: function(nextProps, nextState) {
|
||||
if (this.state.hover !== nextState.hover) return true;
|
||||
if (!this.props.member) { return false; } // e.g. 3pid members
|
||||
if (
|
||||
this.member_last_modified_time === undefined ||
|
||||
this.member_last_modified_time < nextProps.member.getLastModifiedTime()
|
||||
|
@ -65,117 +64,121 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onClick: function(e) {
|
||||
if (!this.props.member) { return; } // e.g. 3pid members
|
||||
|
||||
dis.dispatch({
|
||||
action: 'view_user',
|
||||
member: this.props.member,
|
||||
});
|
||||
},
|
||||
|
||||
getDuration: function(time) {
|
||||
if (!time) return;
|
||||
var t = parseInt(time / 1000);
|
||||
var s = t % 60;
|
||||
var m = parseInt(t / 60) % 60;
|
||||
var h = parseInt(t / (60 * 60)) % 24;
|
||||
var d = parseInt(t / (60 * 60 * 24));
|
||||
if (t < 60) {
|
||||
if (t < 0) {
|
||||
return "0s";
|
||||
}
|
||||
return s + "s";
|
||||
_getDisplayName: function() {
|
||||
if (this.props.customDisplayName) {
|
||||
return this.props.customDisplayName;
|
||||
}
|
||||
if (t < 60 * 60) {
|
||||
return m + "m";
|
||||
}
|
||||
if (t < 24 * 60 * 60) {
|
||||
return h + "h";
|
||||
}
|
||||
return d + "d ";
|
||||
},
|
||||
|
||||
getPrettyPresence: function(user) {
|
||||
if (!user) return "Unknown";
|
||||
var presence = user.presence;
|
||||
if (presence === "online") return "Online";
|
||||
if (presence === "unavailable") return "Idle"; // XXX: is this actually right?
|
||||
if (presence === "offline") return "Offline";
|
||||
return "Unknown";
|
||||
return this.props.member.name;
|
||||
},
|
||||
|
||||
getPowerLabel: function() {
|
||||
var label = this.props.member.userId;
|
||||
if (this.state.isTargetMod) {
|
||||
label += " - Mod (" + this.props.member.powerLevelNorm + "%)";
|
||||
if (!this.props.member) {
|
||||
return this._getDisplayName();
|
||||
}
|
||||
var label = this.props.member.userId + " (power " + this.props.member.powerLevel + ")";
|
||||
return label;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.member_last_modified_time = this.props.member.getLastModifiedTime();
|
||||
if (this.props.member.user) {
|
||||
this.user_last_modified_time = this.props.member.user.getLastModifiedTime();
|
||||
}
|
||||
|
||||
var isMyUser = MatrixClientPeg.get().credentials.userId == this.props.member.userId;
|
||||
|
||||
var power;
|
||||
// if (this.props.member && this.props.member.powerLevelNorm > 0) {
|
||||
// var img = "img/p/p" + Math.floor(20 * this.props.member.powerLevelNorm / 100) + ".png";
|
||||
// power = <img src={ img } className="mx_MemberTile_power" width="44" height="44" alt=""/>;
|
||||
// }
|
||||
var member = this.props.member;
|
||||
var isMyUser = false;
|
||||
var name = this._getDisplayName();
|
||||
var active = -1;
|
||||
var presenceClass = "mx_MemberTile_offline";
|
||||
var mainClassName = "mx_MemberTile ";
|
||||
if (this.props.member.user) {
|
||||
if (this.props.member.user.presence === "online") {
|
||||
presenceClass = "mx_MemberTile_online";
|
||||
|
||||
if (member) {
|
||||
if (member.user) {
|
||||
this.user_last_modified_time = member.user.getLastModifiedTime();
|
||||
|
||||
// FIXME: make presence data update whenever User.presence changes...
|
||||
active = (
|
||||
(Date.now() - (member.user.lastPresenceTs - member.user.lastActiveAgo)) || -1
|
||||
);
|
||||
|
||||
if (member.user.presence === "online") {
|
||||
presenceClass = "mx_MemberTile_online";
|
||||
}
|
||||
else if (member.user.presence === "unavailable") {
|
||||
presenceClass = "mx_MemberTile_unavailable";
|
||||
}
|
||||
}
|
||||
else if (this.props.member.user.presence === "unavailable") {
|
||||
presenceClass = "mx_MemberTile_unavailable";
|
||||
this.member_last_modified_time = member.getLastModifiedTime();
|
||||
isMyUser = MatrixClientPeg.get().credentials.userId == member.userId;
|
||||
|
||||
// if (this.props.member && this.props.member.powerLevelNorm > 0) {
|
||||
// var img = "img/p/p" + Math.floor(20 * this.props.member.powerLevelNorm / 100) + ".png";
|
||||
// power = <img src={ img } className="mx_MemberTile_power" width="44" height="44" alt=""/>;
|
||||
// }
|
||||
|
||||
var power;
|
||||
if (this.props.member) {
|
||||
var powerLevel = this.props.member.powerLevel;
|
||||
if (powerLevel >= 50 && powerLevel < 99) {
|
||||
power = <img src="img/mod.svg" className="mx_MemberTile_power" width="16" height="17" alt="Mod"/>;
|
||||
}
|
||||
if (powerLevel >= 99) {
|
||||
power = <img src="img/admin.svg" className="mx_MemberTile_power" width="16" height="17" alt="Admin"/>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var mainClassName = "mx_MemberTile ";
|
||||
mainClassName += presenceClass;
|
||||
if (this.state.hover) {
|
||||
mainClassName += " mx_MemberTile_hover";
|
||||
}
|
||||
|
||||
var name = this.props.member.name;
|
||||
// if (isMyUser) name += " (me)"; // this does nothing other than introduce line wrapping and pain
|
||||
//var leave = isMyUser ? <img className="mx_MemberTile_leave" src="img/delete.png" width="10" height="10" onClick={this.onLeaveClick}/> : null;
|
||||
|
||||
var nameEl;
|
||||
if (this.state.hover) {
|
||||
var presence;
|
||||
// FIXME: make presence data update whenever User.presence changes...
|
||||
var active = this.props.member.user ? ((Date.now() - (this.props.member.user.lastPresenceTs - this.props.member.user.lastActiveAgo)) || -1) : -1;
|
||||
if (active >= 0) {
|
||||
presence = <div className="mx_MemberTile_presence">{ this.getPrettyPresence(this.props.member.user) } { this.getDuration(active) } ago</div>;
|
||||
}
|
||||
else {
|
||||
presence = <div className="mx_MemberTile_presence">{ this.getPrettyPresence(this.props.member.user) }</div>;
|
||||
}
|
||||
|
||||
nameEl =
|
||||
if (this.state.hover && this.props.member) {
|
||||
var presenceState = (member && member.user) ? member.user.presence : null;
|
||||
var PresenceLabel = sdk.getComponent("rooms.PresenceLabel");
|
||||
nameEl = (
|
||||
<div className="mx_MemberTile_details">
|
||||
<img className="mx_MemberTile_chevron" src="img/member_chevron.png" width="8" height="12"/>
|
||||
<div className="mx_MemberTile_userId">{ name }</div>
|
||||
{ presence }
|
||||
<PresenceLabel activeAgo={active}
|
||||
presenceState={presenceState} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
else {
|
||||
nameEl =
|
||||
nameEl = (
|
||||
<div className="mx_MemberTile_name">
|
||||
{ name }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||
var BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
|
||||
var av;
|
||||
if (member) {
|
||||
av = (
|
||||
<MemberAvatar member={this.props.member} width={36} height={36} />
|
||||
);
|
||||
}
|
||||
else {
|
||||
av = (
|
||||
<BaseAvatar name={name} width={36} height={36} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={mainClassName} title={ this.getPowerLabel() }
|
||||
onClick={ this.onClick } onMouseEnter={ this.mouseEnter }
|
||||
onMouseLeave={ this.mouseLeave }>
|
||||
<div className="mx_MemberTile_avatar">
|
||||
<MemberAvatar member={this.props.member} width={36} height={36} />
|
||||
{ power }
|
||||
{ av }
|
||||
{ power }
|
||||
</div>
|
||||
{ nameEl }
|
||||
</div>
|
||||
|
|
|
@ -75,7 +75,7 @@ module.exports = React.createClass({
|
|||
|
||||
// a callback which is called when the height of the composer is
|
||||
// changed due to a change in content.
|
||||
onResize: React.PropTypes.function,
|
||||
onResize: React.PropTypes.func,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
|
@ -209,23 +209,18 @@ module.exports = React.createClass({
|
|||
this.sentHistory.push(input);
|
||||
this.onEnter(ev);
|
||||
}
|
||||
else if (ev.keyCode === KeyCode.UP) {
|
||||
var input = this.refs.textarea.value;
|
||||
var offset = this.refs.textarea.selectionStart || 0;
|
||||
if (ev.ctrlKey || !input.substr(0, offset).match(/\n/)) {
|
||||
this.sentHistory.next(1);
|
||||
ev.preventDefault();
|
||||
this.resizeInput();
|
||||
}
|
||||
}
|
||||
else if (ev.keyCode === KeyCode.DOWN) {
|
||||
var input = this.refs.textarea.value;
|
||||
var offset = this.refs.textarea.selectionStart || 0;
|
||||
if (ev.ctrlKey || !input.substr(offset).match(/\n/)) {
|
||||
this.sentHistory.next(-1);
|
||||
ev.preventDefault();
|
||||
this.resizeInput();
|
||||
}
|
||||
else if (ev.keyCode === KeyCode.UP || ev.keyCode === KeyCode.DOWN) {
|
||||
var oldSelectionStart = this.refs.textarea.selectionStart;
|
||||
// Remember the keyCode because React will recycle the synthetic event
|
||||
var keyCode = ev.keyCode;
|
||||
// set a callback so we can see if the cursor position changes as
|
||||
// a result of this event. If it doesn't, we cycle history.
|
||||
setTimeout(() => {
|
||||
if (this.refs.textarea.selectionStart == oldSelectionStart) {
|
||||
this.sentHistory.next(keyCode === KeyCode.UP ? 1 : -1);
|
||||
this.resizeInput();
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
if (this.props.tabComplete) {
|
||||
|
@ -341,7 +336,7 @@ module.exports = React.createClass({
|
|||
MatrixClientPeg.get().sendTextMessage(this.props.room.roomId, contentText);
|
||||
}
|
||||
|
||||
sendMessagePromise.then(function() {
|
||||
sendMessagePromise.done(function() {
|
||||
dis.dispatch({
|
||||
action: 'message_sent'
|
||||
});
|
||||
|
|
84
src/components/views/rooms/PresenceLabel.js
Normal file
84
src/components/views/rooms/PresenceLabel.js
Normal file
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
var sdk = require('../../../index');
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'PresenceLabel',
|
||||
|
||||
propTypes: {
|
||||
activeAgo: React.PropTypes.number,
|
||||
presenceState: React.PropTypes.string
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
ago: -1,
|
||||
presenceState: null
|
||||
};
|
||||
},
|
||||
|
||||
getDuration: function(time) {
|
||||
if (!time) return;
|
||||
var t = parseInt(time / 1000);
|
||||
var s = t % 60;
|
||||
var m = parseInt(t / 60) % 60;
|
||||
var h = parseInt(t / (60 * 60)) % 24;
|
||||
var d = parseInt(t / (60 * 60 * 24));
|
||||
if (t < 60) {
|
||||
if (t < 0) {
|
||||
return "0s";
|
||||
}
|
||||
return s + "s";
|
||||
}
|
||||
if (t < 60 * 60) {
|
||||
return m + "m";
|
||||
}
|
||||
if (t < 24 * 60 * 60) {
|
||||
return h + "h";
|
||||
}
|
||||
return d + "d ";
|
||||
},
|
||||
|
||||
getPrettyPresence: function(presence) {
|
||||
if (presence === "online") return "Online";
|
||||
if (presence === "unavailable") return "Idle"; // XXX: is this actually right?
|
||||
if (presence === "offline") return "Offline";
|
||||
return "Unknown";
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (this.props.activeAgo >= 0) {
|
||||
return (
|
||||
<div className="mx_PresenceLabel">
|
||||
{ this.getPrettyPresence(this.props.presenceState) } { this.getDuration(this.props.activeAgo) } ago
|
||||
</div>
|
||||
);
|
||||
}
|
||||
else {
|
||||
return (
|
||||
<div className="mx_PresenceLabel">
|
||||
{ this.getPrettyPresence(this.props.presenceState) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
|
@ -21,6 +21,12 @@ var sdk = require('../../../index');
|
|||
var dis = require("../../../dispatcher");
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
|
||||
var linkify = require('linkifyjs');
|
||||
var linkifyElement = require('linkifyjs/element');
|
||||
var linkifyMatrix = require('../../../linkify-matrix');
|
||||
|
||||
linkifyMatrix(linkify);
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomHeader',
|
||||
|
||||
|
@ -41,6 +47,25 @@ module.exports = React.createClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(newProps) {
|
||||
if (newProps.editing) {
|
||||
var topic = this.props.room.currentState.getStateEvents('m.room.topic', '');
|
||||
var name = this.props.room.currentState.getStateEvents('m.room.name', '');
|
||||
|
||||
this.setState({
|
||||
name: name ? name.getContent().name : '',
|
||||
defaultName: this.props.room.getDefaultRoomName(MatrixClientPeg.get().credentials.userId),
|
||||
topic: topic ? topic.getContent().topic : '',
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
componentDidUpdate: function() {
|
||||
if (this.refs.topic) {
|
||||
linkifyElement(this.refs.topic, linkifyMatrix.options);
|
||||
}
|
||||
},
|
||||
|
||||
onVideoClick: function(e) {
|
||||
dis.dispatch({
|
||||
action: 'place_call',
|
||||
|
@ -57,26 +82,59 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onNameChange: function(new_name) {
|
||||
if (this.props.room.name != new_name && new_name) {
|
||||
MatrixClientPeg.get().setRoomName(this.props.room.roomId, new_name);
|
||||
onNameChanged: function(value) {
|
||||
this.setState({ name : value });
|
||||
},
|
||||
|
||||
onTopicChanged: function(value) {
|
||||
this.setState({ topic : value });
|
||||
},
|
||||
|
||||
onAvatarPickerClick: function(ev) {
|
||||
if (this.refs.file_label) {
|
||||
this.refs.file_label.click();
|
||||
}
|
||||
},
|
||||
|
||||
onAvatarSelected: function(ev) {
|
||||
var self = this;
|
||||
var changeAvatar = this.refs.changeAvatar;
|
||||
if (!changeAvatar) {
|
||||
console.error("No ChangeAvatar found to upload image to!");
|
||||
return;
|
||||
}
|
||||
changeAvatar.onFileSelected(ev).done(function() {
|
||||
// dunno if the avatar changed, re-check it.
|
||||
self._refreshFromServer();
|
||||
}, function(err) {
|
||||
var errMsg = (typeof err === "string") ? err : (err.error || "");
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Error",
|
||||
description: "Failed to set avatar. " + errMsg
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
getRoomName: function() {
|
||||
return this.refs.name_edit.value;
|
||||
return this.state.name;
|
||||
},
|
||||
|
||||
getTopic: function() {
|
||||
return this.state.topic;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var EditableText = sdk.getComponent("elements.EditableText");
|
||||
var RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
|
||||
var RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
|
||||
var ChangeAvatar = sdk.getComponent("settings.ChangeAvatar");
|
||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
||||
var header;
|
||||
if (this.props.simpleHeader) {
|
||||
var cancel;
|
||||
if (this.props.onCancelClick) {
|
||||
cancel = <img className="mx_RoomHeader_simpleHeaderCancel" src="img/cancel-black.png" onClick={ this.props.onCancelClick } alt="Close" width="18" height="18"/>
|
||||
cancel = <img className="mx_RoomHeader_simpleHeaderCancel" src="img/cancel.svg" onClick={ this.props.onCancelClick } alt="Close" width="18" height="18"/>
|
||||
}
|
||||
header =
|
||||
<div className="mx_RoomHeader_wrapper">
|
||||
|
@ -87,27 +145,72 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
}
|
||||
else {
|
||||
var topic = this.props.room.currentState.getStateEvents('m.room.topic', '');
|
||||
|
||||
var name = null;
|
||||
var searchStatus = null;
|
||||
var topic_el = null;
|
||||
var cancel_button = null;
|
||||
var save_button = null;
|
||||
var settings_button = null;
|
||||
var actual_name = this.props.room.currentState.getStateEvents('m.room.name', '');
|
||||
if (actual_name) actual_name = actual_name.getContent().name;
|
||||
if (this.props.editing) {
|
||||
name =
|
||||
<div className="mx_RoomHeader_nameEditing">
|
||||
<input className="mx_RoomHeader_nameInput" type="text" defaultValue={actual_name} placeholder="Name" ref="name_edit"/>
|
||||
</div>
|
||||
// if (topic) topic_el = <div className="mx_RoomHeader_topic"><textarea>{ topic.getContent().topic }</textarea></div>
|
||||
cancel_button = <div className="mx_RoomHeader_textButton" onClick={this.props.onCancelClick}>Cancel</div>
|
||||
save_button = <div className="mx_RoomHeader_textButton" onClick={this.props.onSaveClick}>Save Changes</div>
|
||||
} else {
|
||||
// <EditableText label={this.props.room.name} initialValue={actual_name} placeHolder="Name" onValueChanged={this.onNameChange} />
|
||||
|
||||
// calculate permissions. XXX: this should be done on mount or something, and factored out with RoomSettings
|
||||
var power_levels = this.props.room.currentState.getStateEvents('m.room.power_levels', '');
|
||||
var events_levels = (power_levels ? power_levels.events : {}) || {};
|
||||
var user_id = MatrixClientPeg.get().credentials.userId;
|
||||
|
||||
if (power_levels) {
|
||||
power_levels = power_levels.getContent();
|
||||
var default_user_level = parseInt(power_levels.users_default || 0);
|
||||
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;
|
||||
} else {
|
||||
var default_user_level = 0;
|
||||
var user_levels = [];
|
||||
var current_user_level = 0;
|
||||
}
|
||||
var state_default = parseInt((power_levels ? power_levels.state_default : 0) || 0);
|
||||
|
||||
var room_avatar_level = state_default;
|
||||
if (events_levels['m.room.avatar'] !== undefined) {
|
||||
room_avatar_level = events_levels['m.room.avatar'];
|
||||
}
|
||||
var can_set_room_avatar = current_user_level >= room_avatar_level;
|
||||
|
||||
var room_name_level = state_default;
|
||||
if (events_levels['m.room.name'] !== undefined) {
|
||||
room_name_level = events_levels['m.room.name'];
|
||||
}
|
||||
var can_set_room_name = current_user_level >= room_name_level;
|
||||
|
||||
var room_topic_level = state_default;
|
||||
if (events_levels['m.room.topic'] !== undefined) {
|
||||
room_topic_level = events_levels['m.room.topic'];
|
||||
}
|
||||
var can_set_room_topic = current_user_level >= room_topic_level;
|
||||
|
||||
var placeholderName = "Unnamed Room";
|
||||
if (this.state.defaultName && this.state.defaultName !== '?') {
|
||||
placeholderName += " (" + this.state.defaultName + ")";
|
||||
}
|
||||
|
||||
save_button = <div className="mx_RoomHeader_textButton" onClick={this.props.onSaveClick}>Save</div>
|
||||
cancel_button = <div className="mx_RoomHeader_cancelButton" onClick={this.props.onCancelClick}><img src="img/cancel.svg" width="18" height="18" alt="Cancel"/> </div>
|
||||
}
|
||||
|
||||
if (can_set_room_name) {
|
||||
name =
|
||||
<div className="mx_RoomHeader_name">
|
||||
<EditableText
|
||||
className="mx_RoomHeader_nametext mx_RoomHeader_editable"
|
||||
placeholderClassName="mx_RoomHeader_placeholder"
|
||||
placeholder={ placeholderName }
|
||||
blurToCancel={ false }
|
||||
onValueChanged={ this.onNameChanged }
|
||||
initialValue={ this.state.name }/>
|
||||
</div>
|
||||
}
|
||||
else {
|
||||
var searchStatus;
|
||||
// don't display the search count until the search completes and
|
||||
// gives us a valid (possibly zero) searchCount.
|
||||
|
@ -123,14 +226,48 @@ module.exports = React.createClass({
|
|||
<TintableSvg src="img/settings.svg" width="12" height="12"/>
|
||||
</div>
|
||||
</div>
|
||||
if (topic) topic_el = <div className="mx_RoomHeader_topic" title={topic.getContent().topic}>{ topic.getContent().topic }</div>;
|
||||
}
|
||||
|
||||
if (can_set_room_topic) {
|
||||
topic_el =
|
||||
<EditableText
|
||||
className="mx_RoomHeader_topic mx_RoomHeader_editable"
|
||||
placeholderClassName="mx_RoomHeader_placeholder"
|
||||
placeholder="Add a topic"
|
||||
blurToCancel={ false }
|
||||
onValueChanged={ this.onTopicChanged }
|
||||
initialValue={ this.state.topic }/>
|
||||
} else {
|
||||
var topic = this.props.room.currentState.getStateEvents('m.room.topic', '');
|
||||
if (topic) topic_el = <div className="mx_RoomHeader_topic" ref="topic" title={ topic.getContent().topic }>{ topic.getContent().topic }</div>;
|
||||
}
|
||||
|
||||
var roomAvatar = null;
|
||||
if (this.props.room) {
|
||||
roomAvatar = (
|
||||
<RoomAvatar room={this.props.room} width="48" height="48" />
|
||||
);
|
||||
if (can_set_room_avatar) {
|
||||
roomAvatar = (
|
||||
<div className="mx_RoomHeader_avatarPicker">
|
||||
<div onClick={ this.onAvatarPickerClick }>
|
||||
<ChangeAvatar ref="changeAvatar" room={this.props.room} showUploadSection={false} width={48} height={48} />
|
||||
</div>
|
||||
<div className="mx_RoomHeader_avatarPicker_edit">
|
||||
<label htmlFor="avatarInput" ref="file_label">
|
||||
<img src="img/camera.svg"
|
||||
alt="Upload avatar" title="Upload avatar"
|
||||
width="17" height="15" />
|
||||
</label>
|
||||
<input id="avatarInput" type="file" onChange={ this.onAvatarSelected }/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
else {
|
||||
roomAvatar = (
|
||||
<div onClick={this.props.onSettingsClick}>
|
||||
<RoomAvatar room={this.props.room} width={48} height={48}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
var leave_button;
|
||||
|
@ -149,6 +286,18 @@ module.exports = React.createClass({
|
|||
</div>;
|
||||
}
|
||||
|
||||
var right_row;
|
||||
if (!this.props.editing) {
|
||||
right_row =
|
||||
<div className="mx_RoomHeader_rightRow">
|
||||
{ forget_button }
|
||||
{ leave_button }
|
||||
<div className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title="Search">
|
||||
<TintableSvg src="img/search.svg" width="21" height="19"/>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
header =
|
||||
<div className="mx_RoomHeader_wrapper">
|
||||
<div className="mx_RoomHeader_leftRow">
|
||||
|
@ -160,20 +309,14 @@ module.exports = React.createClass({
|
|||
{ topic_el }
|
||||
</div>
|
||||
</div>
|
||||
{cancel_button}
|
||||
{save_button}
|
||||
<div className="mx_RoomHeader_rightRow">
|
||||
{ forget_button }
|
||||
{ leave_button }
|
||||
<div className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title="Search">
|
||||
<TintableSvg src="img/search.svg" width="21" height="19"/>
|
||||
</div>
|
||||
</div>
|
||||
{cancel_button}
|
||||
{right_row}
|
||||
</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_RoomHeader">
|
||||
<div className={ "mx_RoomHeader " + (this.props.editing ? "mx_RoomHeader_editing" : "") }>
|
||||
{ header }
|
||||
</div>
|
||||
);
|
||||
|
|
56
src/components/views/rooms/RoomPreviewBar.js
Normal file
56
src/components/views/rooms/RoomPreviewBar.js
Normal file
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomPreviewBar',
|
||||
|
||||
propTypes: {
|
||||
onJoinClick: React.PropTypes.func,
|
||||
canJoin: React.PropTypes.bool
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
onJoinClick: function() {},
|
||||
canJoin: false
|
||||
};
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var joinBlock;
|
||||
|
||||
if (this.props.canJoin) {
|
||||
joinBlock = (
|
||||
<div className="mx_RoomPreviewBar_join_text">
|
||||
Would you like to <a onClick={this.props.onJoinClick}>join</a> this room?
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_RoomPreviewBar">
|
||||
<div className="mx_RoomPreviewBar_preview_text">
|
||||
This is a preview of this room. Room interactions have been disabled.
|
||||
</div>
|
||||
{joinBlock}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
|
@ -16,21 +16,98 @@ limitations under the License.
|
|||
|
||||
var React = require('react');
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
var Tinter = require('../../../Tinter');
|
||||
var sdk = require('../../../index');
|
||||
var Modal = require('../../../Modal');
|
||||
|
||||
var room_colors = [
|
||||
// magic room default values courtesy of Ribot
|
||||
["#76cfa6", "#eaf5f0"],
|
||||
["#81bddb", "#eaf1f4"],
|
||||
["#bd79cb", "#f3eaf5"],
|
||||
["#c65d94", "#f5eaef"],
|
||||
["#e55e5e", "#f5eaea"],
|
||||
["#eca46f", "#f5eeea"],
|
||||
["#dad658", "#f5f4ea"],
|
||||
["#80c553", "#eef5ea"],
|
||||
["#bb814e", "#eee8e3"],
|
||||
["#595959", "#ececec"],
|
||||
];
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomSettings',
|
||||
|
||||
propTypes: {
|
||||
room: React.PropTypes.object.isRequired,
|
||||
onSaveClick: React.PropTypes.func,
|
||||
onCancelClick: React.PropTypes.func,
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
// XXX: dirty hack to gutwrench to focus on the invite box
|
||||
if (this.props.room.getJoinedMembers().length == 1) {
|
||||
var inviteBox = document.getElementById("mx_MemberList_invite");
|
||||
if (inviteBox) setTimeout(function() { inviteBox.focus(); }, 0);
|
||||
}
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
// work out the initial color index
|
||||
var room_color_index = undefined;
|
||||
var color_scheme_event = this.props.room.getAccountData("org.matrix.room.color_scheme");
|
||||
if (color_scheme_event) {
|
||||
var color_scheme = color_scheme_event.getContent();
|
||||
if (color_scheme.primary_color) color_scheme.primary_color = color_scheme.primary_color.toLowerCase();
|
||||
if (color_scheme.secondary_color) color_scheme.secondary_color = color_scheme.secondary_color.toLowerCase();
|
||||
// XXX: we should validate these values
|
||||
for (var i = 0; i < room_colors.length; i++) {
|
||||
var room_color = room_colors[i];
|
||||
if (room_color[0] === color_scheme.primary_color &&
|
||||
room_color[1] === color_scheme.secondary_color)
|
||||
{
|
||||
room_color_index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (room_color_index === undefined) {
|
||||
// append the unrecognised colours to our palette
|
||||
room_color_index = room_colors.length;
|
||||
room_colors[room_color_index] = [ color_scheme.primary_color, color_scheme.secondary_color ];
|
||||
}
|
||||
}
|
||||
else {
|
||||
room_color_index = 0;
|
||||
}
|
||||
|
||||
// get the aliases
|
||||
var aliases = {};
|
||||
var domain = MatrixClientPeg.get().getDomain();
|
||||
var alias_events = this.props.room.currentState.getStateEvents('m.room.aliases');
|
||||
for (var i = 0; i < alias_events.length; i++) {
|
||||
aliases[alias_events[i].getStateKey()] = alias_events[i].getContent().aliases.slice(); // shallow copy
|
||||
}
|
||||
aliases[domain] = aliases[domain] || [];
|
||||
|
||||
var tags = {};
|
||||
Object.keys(this.props.room.tags).forEach(function(tagName) {
|
||||
tags[tagName] = {};
|
||||
});
|
||||
|
||||
return {
|
||||
power_levels_changed: false
|
||||
power_levels_changed: false,
|
||||
color_scheme_changed: false,
|
||||
color_scheme_index: room_color_index,
|
||||
aliases_changed: false,
|
||||
aliases: aliases,
|
||||
tags_changed: false,
|
||||
tags: tags,
|
||||
};
|
||||
},
|
||||
|
||||
resetState: function() {
|
||||
this.set.state(this.getInitialState());
|
||||
},
|
||||
|
||||
canGuestsJoin: function() {
|
||||
return this.refs.guests_join.checked;
|
||||
},
|
||||
|
@ -40,7 +117,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
getTopic: function() {
|
||||
return this.refs.topic.value;
|
||||
return this.refs.topic ? this.refs.topic.value : "";
|
||||
},
|
||||
|
||||
getJoinRules: function() {
|
||||
|
@ -62,13 +139,13 @@ module.exports = React.createClass({
|
|||
power_levels = power_levels.getContent();
|
||||
|
||||
var new_power_levels = {
|
||||
ban: parseInt(this.refs.ban.value),
|
||||
kick: parseInt(this.refs.kick.value),
|
||||
redact: parseInt(this.refs.redact.value),
|
||||
invite: parseInt(this.refs.invite.value),
|
||||
events_default: parseInt(this.refs.events_default.value),
|
||||
state_default: parseInt(this.refs.state_default.value),
|
||||
users_default: parseInt(this.refs.users_default.value),
|
||||
ban: parseInt(this.refs.ban.getValue()),
|
||||
kick: parseInt(this.refs.kick.getValue()),
|
||||
redact: parseInt(this.refs.redact.getValue()),
|
||||
invite: parseInt(this.refs.invite.getValue()),
|
||||
events_default: parseInt(this.refs.events_default.getValue()),
|
||||
state_default: parseInt(this.refs.state_default.getValue()),
|
||||
users_default: parseInt(this.refs.users_default.getValue()),
|
||||
users: power_levels.users,
|
||||
events: power_levels.events,
|
||||
};
|
||||
|
@ -76,17 +153,231 @@ module.exports = React.createClass({
|
|||
return new_power_levels;
|
||||
},
|
||||
|
||||
getCanonicalAlias: function() {
|
||||
return this.refs.canonical_alias ? this.refs.canonical_alias.value : "";
|
||||
},
|
||||
|
||||
getAliasOperations: function() {
|
||||
if (!this.state.aliases_changed) return undefined;
|
||||
|
||||
// work out the delta from room state to UI state
|
||||
var ops = [];
|
||||
|
||||
// calculate original ("old") aliases
|
||||
var oldAliases = {};
|
||||
var aliases = this.state.aliases;
|
||||
var alias_events = this.props.room.currentState.getStateEvents('m.room.aliases');
|
||||
for (var i = 0; i < alias_events.length; i++) {
|
||||
var domain = alias_events[i].getStateKey();
|
||||
oldAliases[domain] = alias_events[i].getContent().aliases.slice(); // shallow copy
|
||||
}
|
||||
|
||||
// FIXME: this whole delta-based set comparison function used for domains, aliases & tags
|
||||
// should be factored out asap rather than duplicated like this.
|
||||
|
||||
// work out whether any domains have entirely disappeared or appeared
|
||||
var domainDelta = {}
|
||||
Object.keys(oldAliases).forEach(function(domain) {
|
||||
domainDelta[domain] = domainDelta[domain] || 0;
|
||||
domainDelta[domain]--;
|
||||
});
|
||||
Object.keys(aliases).forEach(function(domain) {
|
||||
domainDelta[domain] = domainDelta[domain] || 0;
|
||||
domainDelta[domain]++;
|
||||
});
|
||||
|
||||
Object.keys(domainDelta).forEach(function(domain) {
|
||||
switch (domainDelta[domain]) {
|
||||
case 1: // entirely new domain
|
||||
aliases[domain].forEach(function(alias) {
|
||||
ops.push({ type: "put", alias : alias });
|
||||
});
|
||||
break;
|
||||
case -1: // entirely removed domain
|
||||
oldAliases[domain].forEach(function(alias) {
|
||||
ops.push({ type: "delete", alias : alias });
|
||||
});
|
||||
break;
|
||||
case 0: // mix of aliases in this domain.
|
||||
// compare old & new aliases for this domain
|
||||
var delta = {};
|
||||
oldAliases[domain].forEach(function(item) {
|
||||
delta[item] = delta[item] || 0;
|
||||
delta[item]--;
|
||||
});
|
||||
aliases[domain].forEach(function(item) {
|
||||
delta[item] = delta[item] || 0;
|
||||
delta[item]++;
|
||||
});
|
||||
|
||||
Object.keys(delta).forEach(function(alias) {
|
||||
if (delta[alias] == 1) {
|
||||
ops.push({ type: "put", alias: alias });
|
||||
} else if (delta[alias] == -1) {
|
||||
ops.push({ type: "delete", alias: alias });
|
||||
} else {
|
||||
console.error("Calculated alias delta of " + delta[alias] +
|
||||
" - this should never happen!");
|
||||
}
|
||||
});
|
||||
break;
|
||||
default:
|
||||
console.error("Calculated domain delta of " + domainDelta[domain] +
|
||||
" - this should never happen!");
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return ops;
|
||||
},
|
||||
|
||||
getTagOperations: function() {
|
||||
if (!this.state.tags_changed) return undefined;
|
||||
|
||||
var ops = [];
|
||||
|
||||
var delta = {};
|
||||
Object.keys(this.props.room.tags).forEach(function(oldTag) {
|
||||
delta[oldTag] = delta[oldTag] || 0;
|
||||
delta[oldTag]--;
|
||||
});
|
||||
Object.keys(this.state.tags).forEach(function(newTag) {
|
||||
delta[newTag] = delta[newTag] || 0;
|
||||
delta[newTag]++;
|
||||
});
|
||||
Object.keys(delta).forEach(function(tag) {
|
||||
if (delta[tag] == 1) {
|
||||
ops.push({ type: "put", tag: tag });
|
||||
} else if (delta[tag] == -1) {
|
||||
ops.push({ type: "delete", tag: tag });
|
||||
} else {
|
||||
console.error("Calculated tag delta of " + delta[tag] +
|
||||
" - this should never happen!");
|
||||
}
|
||||
});
|
||||
|
||||
return ops;
|
||||
},
|
||||
|
||||
onPowerLevelsChanged: function() {
|
||||
this.setState({
|
||||
power_levels_changed: true
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
|
||||
getColorScheme: function() {
|
||||
if (!this.state.color_scheme_changed) return undefined;
|
||||
|
||||
var topic = this.props.room.currentState.getStateEvents('m.room.topic', '');
|
||||
if (topic) topic = topic.getContent().topic;
|
||||
return {
|
||||
primary_color: room_colors[this.state.color_scheme_index][0],
|
||||
secondary_color: room_colors[this.state.color_scheme_index][1],
|
||||
};
|
||||
},
|
||||
|
||||
onColorSchemeChanged: function(index) {
|
||||
// preview what the user just changed the scheme to.
|
||||
Tinter.tint(room_colors[index][0], room_colors[index][1]);
|
||||
|
||||
this.setState({
|
||||
color_scheme_changed: true,
|
||||
color_scheme_index: index,
|
||||
});
|
||||
},
|
||||
|
||||
onAliasChanged: function(domain, index, alias) {
|
||||
if (alias === "") return; // hit the delete button to delete please
|
||||
var oldAlias;
|
||||
if (this.isAliasValid(alias)) {
|
||||
oldAlias = this.state.aliases[domain][index];
|
||||
this.state.aliases[domain][index] = alias;
|
||||
this.setState({ aliases_changed : true });
|
||||
}
|
||||
else {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Invalid alias format",
|
||||
description: "'" + alias + "' is not a valid format for an alias",
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onAliasDeleted: function(domain, index) {
|
||||
// It's a bit naughty to directly manipulate this.state, and React would
|
||||
// normally whine at you, but it can't see us doing the splice. Given we
|
||||
// promptly setState anyway, it's just about acceptable. The alternative
|
||||
// would be to arbitrarily deepcopy to a temp variable and then setState
|
||||
// that, but why bother when we can cut this corner.
|
||||
var alias = this.state.aliases[domain].splice(index, 1);
|
||||
this.setState({
|
||||
aliases: this.state.aliases
|
||||
});
|
||||
|
||||
this.setState({ aliases_changed : true });
|
||||
},
|
||||
|
||||
onAliasAdded: function(alias) {
|
||||
if (alias === "") return; // ignore attempts to create blank aliases
|
||||
if (alias === undefined) {
|
||||
alias = this.refs.add_alias ? this.refs.add_alias.getValue() : undefined;
|
||||
if (alias === undefined || alias === "") return;
|
||||
}
|
||||
|
||||
if (this.isAliasValid(alias)) {
|
||||
var domain = alias.replace(/^.*?:/, '');
|
||||
// XXX: do we need to deep copy aliases before editing it?
|
||||
this.state.aliases[domain] = this.state.aliases[domain] || [];
|
||||
this.state.aliases[domain].push(alias);
|
||||
this.setState({
|
||||
aliases: this.state.aliases
|
||||
});
|
||||
|
||||
// reset the add field
|
||||
this.refs.add_alias.setValue('');
|
||||
|
||||
this.setState({ aliases_changed : true });
|
||||
}
|
||||
else {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Invalid alias format",
|
||||
description: "'" + alias + "' is not a valid format for an alias",
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
isAliasValid: function(alias) {
|
||||
// XXX: FIXME SPEC-1
|
||||
return (alias.match(/^#([^\/:,]+?):(.+)$/) && encodeURI(alias) === alias);
|
||||
},
|
||||
|
||||
onTagChange: function(tagName, event) {
|
||||
if (event.target.checked) {
|
||||
if (tagName === 'm.favourite') {
|
||||
delete this.state.tags['m.lowpriority'];
|
||||
}
|
||||
else if (tagName === 'm.lowpriority') {
|
||||
delete this.state.tags['m.favourite'];
|
||||
}
|
||||
|
||||
this.state.tags[tagName] = this.state.tags[tagName] || {};
|
||||
}
|
||||
else {
|
||||
delete this.state.tags[tagName];
|
||||
}
|
||||
|
||||
// XXX: hacky say to deep-edit state
|
||||
this.setState({
|
||||
tags: this.state.tags,
|
||||
tags_changed: true
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
// TODO: go through greying out things you don't have permission to change
|
||||
// (or turning them into informative stuff)
|
||||
|
||||
var EditableText = sdk.getComponent('elements.EditableText');
|
||||
var PowerSelector = sdk.getComponent('elements.PowerSelector');
|
||||
|
||||
var join_rule = this.props.room.currentState.getStateEvents('m.room.join_rules', '');
|
||||
if (join_rule) join_rule = join_rule.getContent().join_rule;
|
||||
|
@ -108,7 +399,10 @@ module.exports = React.createClass({
|
|||
}
|
||||
}
|
||||
|
||||
var events_levels = power_levels.events || {};
|
||||
var events_levels = (power_levels ? power_levels.events : {}) || {};
|
||||
|
||||
var user_id = MatrixClientPeg.get().credentials.userId;
|
||||
|
||||
|
||||
if (power_levels) {
|
||||
power_levels = power_levels.getContent();
|
||||
|
@ -127,8 +421,6 @@ module.exports = React.createClass({
|
|||
|
||||
var user_levels = power_levels.users || {};
|
||||
|
||||
var user_id = MatrixClientPeg.get().credentials.userId;
|
||||
|
||||
var current_user_level = user_levels[user_id];
|
||||
if (current_user_level == undefined) current_user_level = default_user_level;
|
||||
|
||||
|
@ -157,117 +449,308 @@ module.exports = React.createClass({
|
|||
var can_change_levels = false;
|
||||
}
|
||||
|
||||
var room_avatar_level = parseInt(power_levels.state_default || 0);
|
||||
if (events_levels['m.room.avatar'] !== undefined) {
|
||||
room_avatar_level = events_levels['m.room.avatar'];
|
||||
}
|
||||
var can_set_room_avatar = current_user_level >= room_avatar_level;
|
||||
var state_default = (parseInt(power_levels ? power_levels.state_default : 0) || 0);
|
||||
|
||||
var change_avatar;
|
||||
if (can_set_room_avatar) {
|
||||
change_avatar = <div>
|
||||
<h3>Room Icon</h3>
|
||||
<ChangeAvatar room={this.props.room} />
|
||||
var room_aliases_level = state_default;
|
||||
if (events_levels['m.room.aliases'] !== undefined) {
|
||||
room_avatar_level = events_levels['m.room.aliases'];
|
||||
}
|
||||
var can_set_room_aliases = current_user_level >= room_aliases_level;
|
||||
|
||||
var canonical_alias_level = state_default;
|
||||
if (events_levels['m.room.canonical_alias'] !== undefined) {
|
||||
room_avatar_level = events_levels['m.room.canonical_alias'];
|
||||
}
|
||||
var can_set_canonical_alias = current_user_level >= canonical_alias_level;
|
||||
|
||||
var tag_level = state_default;
|
||||
if (events_levels['m.tag'] !== undefined) {
|
||||
tag_level = events_levels['m.tag'];
|
||||
}
|
||||
var can_set_tag = current_user_level >= tag_level;
|
||||
|
||||
var self = this;
|
||||
|
||||
var canonical_alias_event = this.props.room.currentState.getStateEvents('m.room.canonical_alias', '');
|
||||
var canonical_alias = canonical_alias_event ? canonical_alias_event.getContent().alias : "";
|
||||
var domain = MatrixClientPeg.get().getDomain();
|
||||
|
||||
var remote_domains = Object.keys(this.state.aliases).filter(function(alias) { return alias !== domain });
|
||||
|
||||
var remote_aliases_section;
|
||||
if (remote_domains.length) {
|
||||
remote_aliases_section =
|
||||
<div>
|
||||
<div className="mx_RoomSettings_aliasLabel">
|
||||
This room can be found elsewhere as:
|
||||
</div>
|
||||
<div className="mx_RoomSettings_aliasesTable">
|
||||
{ remote_domains.map(function(state_key, i) {
|
||||
self.state.aliases[state_key].map(function(alias, j) {
|
||||
return (
|
||||
<div className="mx_RoomSettings_aliasesTableRow" key={ i + "_" + j }>
|
||||
<EditableText
|
||||
className="mx_RoomSettings_alias mx_RoomSettings_editable"
|
||||
blurToCancel={ false }
|
||||
editable={ false }
|
||||
initialValue={ alias } />
|
||||
<div className="mx_RoomSettings_deleteAlias">
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
var canonical_alias_section;
|
||||
if (can_set_canonical_alias) {
|
||||
canonical_alias_section =
|
||||
<select ref="canonical_alias" defaultValue={ canonical_alias }>
|
||||
{ Object.keys(self.state.aliases).map(function(stateKey, i) {
|
||||
return self.state.aliases[stateKey].map(function(alias, j) {
|
||||
return <option value={ alias } key={ i + "_" + j }>{ alias }</option>
|
||||
});
|
||||
})}
|
||||
<option value="" key="unset">not set</option>
|
||||
</select>
|
||||
}
|
||||
else {
|
||||
canonical_alias_section = <b>{ canonical_alias || "not set" }</b>;
|
||||
}
|
||||
|
||||
var aliases_section =
|
||||
<div>
|
||||
<h3>Directory</h3>
|
||||
<div className="mx_RoomSettings_aliasLabel">
|
||||
{ this.state.aliases[domain].length
|
||||
? "This room can be found on " + domain + " as:"
|
||||
: "This room is not findable on " + domain }
|
||||
</div>
|
||||
<div className="mx_RoomSettings_aliasesTable">
|
||||
{ this.state.aliases[domain].map(function(alias, i) {
|
||||
var deleteButton;
|
||||
if (can_set_room_aliases) {
|
||||
deleteButton = <img src="img/cancel-small.svg" width="14" height="14" alt="Delete" onClick={ self.onAliasDeleted.bind(self, domain, i) }/>;
|
||||
}
|
||||
return (
|
||||
<div className="mx_RoomSettings_aliasesTableRow" key={ i }>
|
||||
<EditableText
|
||||
className="mx_RoomSettings_alias mx_RoomSettings_editable"
|
||||
placeholderClassName="mx_RoomSettings_aliasPlaceholder"
|
||||
placeholder={ "New alias (e.g. #foo:" + domain + ")" }
|
||||
blurToCancel={ false }
|
||||
onValueChanged={ self.onAliasChanged.bind(self, domain, i) }
|
||||
editable={ can_set_room_aliases }
|
||||
initialValue={ alias } />
|
||||
<div className="mx_RoomSettings_deleteAlias">
|
||||
{ deleteButton }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<div className="mx_RoomSettings_aliasesTableRow" key="new">
|
||||
<EditableText
|
||||
ref="add_alias"
|
||||
className="mx_RoomSettings_alias mx_RoomSettings_editable"
|
||||
placeholderClassName="mx_RoomSettings_aliasPlaceholder"
|
||||
placeholder={ "New alias (e.g. #foo:" + domain + ")" }
|
||||
blurToCancel={ false }
|
||||
onValueChanged={ self.onAliasAdded } />
|
||||
<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>
|
||||
|
||||
{ remote_aliases_section }
|
||||
|
||||
<div className="mx_RoomSettings_aliasLabel">The official way to refer to this room is: { canonical_alias_section }</div>
|
||||
</div>;
|
||||
|
||||
var room_colors_section =
|
||||
<div>
|
||||
<h3>Room Colour</h3>
|
||||
<div className="mx_RoomSettings_roomColors">
|
||||
{room_colors.map(function(room_color, i) {
|
||||
var selected;
|
||||
if (i === self.state.color_scheme_index) {
|
||||
selected =
|
||||
<div className="mx_RoomSettings_roomColor_selected">
|
||||
<img src="img/tick.svg" width="17" height="14" alt="./"/>
|
||||
</div>
|
||||
}
|
||||
var boundClick = self.onColorSchemeChanged.bind(self, i)
|
||||
return (
|
||||
<div className="mx_RoomSettings_roomColor"
|
||||
key={ "room_color_" + i }
|
||||
style={{ backgroundColor: room_color[1] }}
|
||||
onClick={ boundClick }>
|
||||
{ selected }
|
||||
<div className="mx_RoomSettings_roomColorPrimary" style={{ backgroundColor: room_color[0] }}></div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
var user_levels_section;
|
||||
if (user_levels.length) {
|
||||
user_levels_section =
|
||||
<div>
|
||||
<div>
|
||||
Users with specific roles are:
|
||||
</div>
|
||||
<div>
|
||||
{Object.keys(user_levels).map(function(user, i) {
|
||||
return (
|
||||
<div className="mx_RoomSettings_userLevel" key={user}>
|
||||
{ user } is a
|
||||
<PowerSelector value={ user_levels[user] } disabled={true}/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
else {
|
||||
user_levels_section = <div>No users have specific privileges in this room.</div>
|
||||
}
|
||||
|
||||
var banned = this.props.room.getMembersWithMembership("ban");
|
||||
var banned_users_section;
|
||||
if (banned.length) {
|
||||
banned_users_section =
|
||||
<div>
|
||||
<h3>Banned users</h3>
|
||||
<div className="mx_RoomSettings_banned">
|
||||
{banned.map(function(member, i) {
|
||||
return (
|
||||
<div key={i}>
|
||||
{member.userId}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
var create_event = this.props.room.currentState.getStateEvents('m.room.create', '');
|
||||
var unfederatable_section;
|
||||
if (create_event.getContent()["m.federate"] === false) {
|
||||
unfederatable_section = <div className="mx_RoomSettings_powerLevel">Ths room is not accessible by remote Matrix servers.</div>
|
||||
}
|
||||
|
||||
// TODO: support editing custom events_levels
|
||||
// TODO: support editing custom user_levels
|
||||
|
||||
var tags = [
|
||||
{ name: "m.favourite", label: "Favourite", ref: "tag_favourite" },
|
||||
{ name: "m.lowpriority", label: "Low priority", ref: "tag_lowpriority" },
|
||||
];
|
||||
|
||||
Object.keys(this.state.tags).sort().forEach(function(tagName) {
|
||||
if (tagName !== 'm.favourite' && tagName !== 'm.lowpriority') {
|
||||
tags.push({ name: tagName, label: tagName });
|
||||
}
|
||||
});
|
||||
|
||||
var tags_section =
|
||||
<div className="mx_RoomSettings_tags">
|
||||
This room is tagged as
|
||||
{ can_set_tag ?
|
||||
tags.map(function(tag, i) {
|
||||
return (<label key={ i }>
|
||||
<input type="checkbox"
|
||||
ref={ tag.ref }
|
||||
checked={ tag.name in self.state.tags }
|
||||
onChange={ self.onTagChange.bind(self, tag.name) }/>
|
||||
{ tag.label }
|
||||
</label>);
|
||||
}) : tags.map(function(tag) { return tag.label; }).join(", ")
|
||||
}
|
||||
</div>
|
||||
|
||||
return (
|
||||
<div className="mx_RoomSettings">
|
||||
<textarea className="mx_RoomSettings_description" placeholder="Topic" defaultValue={topic} ref="topic"/> <br/>
|
||||
<label><input type="checkbox" ref="is_private" defaultChecked={join_rule != "public"}/> Make this room private</label> <br/>
|
||||
<label><input type="checkbox" ref="share_history" defaultChecked={history_visibility == "shared"}/> Share message history with new users</label> <br/>
|
||||
<label>
|
||||
<input type="checkbox" ref="guests_read" defaultChecked={history_visibility === "world_readable"}/>
|
||||
Allow guests to read messages in this room
|
||||
</label> <br/>
|
||||
<label>
|
||||
<input type="checkbox" ref="guests_join" defaultChecked={guest_access === "can_join"}/>
|
||||
Allow guests to join this room
|
||||
</label> <br/>
|
||||
<label className="mx_RoomSettings_encrypt"><input type="checkbox" /> Encrypt room</label> <br/>
|
||||
<label><input type="checkbox" ref="guests_read" defaultChecked={history_visibility === "world_readable"}/> Allow guests to read messages in this room</label> <br/>
|
||||
<label><input type="checkbox" ref="guests_join" defaultChecked={guest_access === "can_join"}/> Allow guests to join this room</label> <br/>
|
||||
<label className="mx_RoomSettings_encrypt"><input type="checkbox" /> Encrypt room</label>
|
||||
|
||||
{ tags_section }
|
||||
|
||||
{ room_colors_section }
|
||||
|
||||
{ aliases_section }
|
||||
|
||||
<h3>Notifications</h3>
|
||||
<div className="mx_RoomSettings_settings">
|
||||
<label><input type="checkbox" ref="are_notifications_muted" defaultChecked={are_notifications_muted}/> Mute notifications for this room</label>
|
||||
</div>
|
||||
|
||||
<h3>Power levels</h3>
|
||||
<div className="mx_RoomSettings_power_levels mx_RoomSettings_settings">
|
||||
<div>
|
||||
<label htmlFor="mx_RoomSettings_ban_level">Ban level</label>
|
||||
<input type="text" defaultValue={ban_level} size="3" ref="ban" id="mx_RoomSettings_ban_level"
|
||||
disabled={!can_change_levels || current_user_level < ban_level} onChange={this.onPowerLevelsChanged}/>
|
||||
<h3>Permissions</h3>
|
||||
<div className="mx_RoomSettings_powerLevels mx_RoomSettings_settings">
|
||||
<div className="mx_RoomSettings_powerLevel">
|
||||
<span className="mx_RoomSettings_powerLevelKey">The default role for new room members is </span>
|
||||
<PowerSelector ref="users_default" value={default_user_level} disabled={!can_change_levels || current_user_level < default_user_level} onChange={this.onPowerLevelsChanged}/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="mx_RoomSettings_kick_level">Kick level</label>
|
||||
<input type="text" defaultValue={kick_level} size="3" ref="kick" id="mx_RoomSettings_kick_level"
|
||||
disabled={!can_change_levels || current_user_level < kick_level} onChange={this.onPowerLevelsChanged}/>
|
||||
<div className="mx_RoomSettings_powerLevel">
|
||||
<span className="mx_RoomSettings_powerLevelKey">To send messages, you must be a </span>
|
||||
<PowerSelector ref="events_default" value={send_level} disabled={!can_change_levels || current_user_level < send_level} onChange={this.onPowerLevelsChanged}/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="mx_RoomSettings_redact_level">Redact level</label>
|
||||
<input type="text" defaultValue={redact_level} size="3" ref="redact" id="mx_RoomSettings_redact_level"
|
||||
disabled={!can_change_levels || current_user_level < redact_level} onChange={this.onPowerLevelsChanged}/>
|
||||
<div className="mx_RoomSettings_powerLevel">
|
||||
<span className="mx_RoomSettings_powerLevelKey">To invite users into the room, you must be a </span>
|
||||
<PowerSelector ref="invite" value={invite_level} disabled={!can_change_levels || current_user_level < invite_level} onChange={this.onPowerLevelsChanged}/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="mx_RoomSettings_invite_level">Invite level</label>
|
||||
<input type="text" defaultValue={invite_level} size="3" ref="invite" id="mx_RoomSettings_invite_level"
|
||||
disabled={!can_change_levels || current_user_level < invite_level} onChange={this.onPowerLevelsChanged}/>
|
||||
<div className="mx_RoomSettings_powerLevel">
|
||||
<span className="mx_RoomSettings_powerLevelKey">To configure the room, you must be a </span>
|
||||
<PowerSelector ref="state_default" value={state_level} disabled={!can_change_levels || current_user_level < state_level} onChange={this.onPowerLevelsChanged}/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="mx_RoomSettings_event_level">Send event level</label>
|
||||
<input type="text" defaultValue={send_level} size="3" ref="events_default" id="mx_RoomSettings_event_level"
|
||||
disabled={!can_change_levels || current_user_level < send_level} onChange={this.onPowerLevelsChanged}/>
|
||||
<div className="mx_RoomSettings_powerLevel">
|
||||
<span className="mx_RoomSettings_powerLevelKey">To kick users, you must be a </span>
|
||||
<PowerSelector ref="kick" value={kick_level} disabled={!can_change_levels || current_user_level < kick_level} onChange={this.onPowerLevelsChanged}/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="mx_RoomSettings_state_level">Set state level</label>
|
||||
<input type="text" defaultValue={state_level} size="3" ref="state_default" id="mx_RoomSettings_state_level"
|
||||
disabled={!can_change_levels || current_user_level < state_level} onChange={this.onPowerLevelsChanged}/>
|
||||
<div className="mx_RoomSettings_powerLevel">
|
||||
<span className="mx_RoomSettings_powerLevelKey">To ban users, you must be a </span>
|
||||
<PowerSelector ref="ban" value={ban_level} disabled={!can_change_levels || current_user_level < ban_level} onChange={this.onPowerLevelsChanged}/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="mx_RoomSettings_user_level">Default user level</label>
|
||||
<input type="text" defaultValue={default_user_level} size="3" ref="users_default"
|
||||
id="mx_RoomSettings_user_level" disabled={!can_change_levels || current_user_level < default_user_level}
|
||||
onChange={this.onPowerLevelsChanged}/>
|
||||
<div className="mx_RoomSettings_powerLevel">
|
||||
<span className="mx_RoomSettings_powerLevelKey">To redact messages, you must be a </span>
|
||||
<PowerSelector ref="redact" value={redact_level} disabled={!can_change_levels || current_user_level < redact_level} onChange={this.onPowerLevelsChanged}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>User levels</h3>
|
||||
<div className="mx_RoomSettings_user_levels mx_RoomSettings_settings">
|
||||
{Object.keys(user_levels).map(function(user, i) {
|
||||
return (
|
||||
<div key={user}>
|
||||
<label htmlFor={"mx_RoomSettings_user_"+i}>{user}</label>
|
||||
<input type="text" defaultValue={user_levels[user]} size="3" id={"mx_RoomSettings_user_"+i} disabled/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<h3>Event levels</h3>
|
||||
<div className="mx_RoomSettings_event_lvels mx_RoomSettings_settings">
|
||||
{Object.keys(events_levels).map(function(event_type, i) {
|
||||
return (
|
||||
<div key={event_type}>
|
||||
<label htmlFor={"mx_RoomSettings_event_"+i}>{event_type}</label>
|
||||
<input type="text" defaultValue={events_levels[event_type]} size="3" id={"mx_RoomSettings_event_"+i} disabled/>
|
||||
<div className="mx_RoomSettings_powerLevel" key={event_type}>
|
||||
<span className="mx_RoomSettings_powerLevelKey">To send events of type <code>{ event_type }</code>, you must be a </span>
|
||||
<PowerSelector value={ events_levels[event_type] } disabled={true} onChange={self.onPowerLevelsChanged}/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{ unfederatable_section }
|
||||
</div>
|
||||
|
||||
<h3>Banned users</h3>
|
||||
<div className="mx_RoomSettings_banned">
|
||||
{banned.map(function(member, i) {
|
||||
return (
|
||||
<div key={i}>
|
||||
{member.userId}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<h3>Users</h3>
|
||||
<div className="mx_RoomSettings_userLevels mx_RoomSettings_settings">
|
||||
<div>
|
||||
Your role in this room is currently <b><PowerSelector room={ this.props.room } value={current_user_level} disabled={true}/></b>.
|
||||
</div>
|
||||
|
||||
{ user_levels_section }
|
||||
</div>
|
||||
{change_avatar}
|
||||
|
||||
{ banned_users_section }
|
||||
|
||||
<h3>Advanced</h3>
|
||||
<div className="mx_RoomSettings_settings">
|
||||
This room's internal ID is <code>{ this.props.room.roomId }</code>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -123,7 +123,7 @@ module.exports = React.createClass({
|
|||
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" />
|
||||
<RoomAvatar room={this.props.room} width={24} height={24} />
|
||||
{ badge }
|
||||
</div>
|
||||
{ label }
|
||||
|
|
|
@ -18,6 +18,7 @@ limitations under the License.
|
|||
|
||||
var React = require('react');
|
||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
var CommandEntry = require("../../../TabCompleteEntries").CommandEntry;
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'TabCompleteBar',
|
||||
|
@ -31,8 +32,9 @@ module.exports = React.createClass({
|
|||
<div className="mx_TabCompleteBar">
|
||||
{this.props.entries.map(function(entry, i) {
|
||||
return (
|
||||
<div key={entry.getKey() || i + ""} className="mx_TabCompleteBar_item"
|
||||
onClick={entry.onClick.bind(entry)} >
|
||||
<div key={entry.getKey() || i + ""}
|
||||
className={ "mx_TabCompleteBar_item " + (entry instanceof CommandEntry ? "mx_TabCompleteBar_command" : "") }
|
||||
onClick={entry.onClick.bind(entry)} >
|
||||
{entry.getImageJsx()}
|
||||
<span className="mx_TabCompleteBar_text">
|
||||
{entry.getText()}
|
||||
|
|
|
@ -25,6 +25,8 @@ module.exports = React.createClass({
|
|||
room: React.PropTypes.object,
|
||||
// if false, you need to call changeAvatar.onFileSelected yourself.
|
||||
showUploadSection: React.PropTypes.bool,
|
||||
width: React.PropTypes.number,
|
||||
height: React.PropTypes.number,
|
||||
className: React.PropTypes.string
|
||||
},
|
||||
|
||||
|
@ -37,7 +39,9 @@ module.exports = React.createClass({
|
|||
getDefaultProps: function() {
|
||||
return {
|
||||
showUploadSection: true,
|
||||
className: "mx_Dialog_content" // FIXME - shouldn't be this by default
|
||||
className: "",
|
||||
width: 80,
|
||||
height: 80,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -111,13 +115,15 @@ module.exports = React.createClass({
|
|||
// Having just set an avatar we just display that since it will take a little
|
||||
// time to propagate through to the RoomAvatar.
|
||||
if (this.props.room && !this.avatarSet) {
|
||||
avatarImg = <RoomAvatar room={this.props.room} width='320' height='240' resizeMethod='scale' />;
|
||||
avatarImg = <RoomAvatar room={this.props.room} width={ this.props.width } height={ this.props.height } resizeMethod='crop' />;
|
||||
} else {
|
||||
var style = {
|
||||
maxWidth: 320,
|
||||
maxHeight: 240,
|
||||
width: this.props.width,
|
||||
height: this.props.height,
|
||||
objectFit: 'cover',
|
||||
};
|
||||
avatarImg = <img src={this.state.avatarUrl} style={style} />;
|
||||
// FIXME: surely we should be using MemberAvatar or UserAvatar or something here...
|
||||
avatarImg = <img className="mx_RoomAvatar" src={this.state.avatarUrl} style={style} />;
|
||||
}
|
||||
|
||||
var uploadSection;
|
||||
|
|
|
@ -99,7 +99,9 @@ module.exports = React.createClass({
|
|||
var EditableText = sdk.getComponent('elements.EditableText');
|
||||
return (
|
||||
<EditableText ref="displayname_edit" initialValue={this.state.displayName}
|
||||
label="Click to set display name."
|
||||
className="mx_EditableText"
|
||||
placeholderClassName="mx_EditableText_placeholder"
|
||||
placeholder="No display name"
|
||||
onValueChanged={this.onValueChanged} />
|
||||
);
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ module.exports = React.createClass({
|
|||
propTypes: {
|
||||
// a callback which is called when the video within the callview
|
||||
// due to a change in video metadata
|
||||
onResize: React.PropTypes.function,
|
||||
onResize: React.PropTypes.func,
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
|
|
|
@ -24,7 +24,7 @@ module.exports = React.createClass({
|
|||
propTypes: {
|
||||
// a callback which is called when the video element is resized
|
||||
// due to a change in video metadata
|
||||
onResize: React.PropTypes.function,
|
||||
onResize: React.PropTypes.func,
|
||||
},
|
||||
|
||||
componentDidMount() {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue