Merge branch 'develop' into kegan/invite-search

pull/21833/head
Kegan Dougal 2016-01-18 17:31:26 +00:00
commit 0465323ca6
21 changed files with 1425 additions and 367 deletions

View File

@ -141,8 +141,7 @@ var commands = {
return reject("Usage: /join #alias:domain"); return reject("Usage: /join #alias:domain");
} }
if (!room_alias.match(/:/)) { if (!room_alias.match(/:/)) {
var domain = MatrixClientPeg.get().credentials.userId.replace(/^.*:/, ''); room_alias += ':' + MatrixClientPeg.get().getDomain();
room_alias += ':' + domain;
} }
// Try to find a room with this alias // Try to find a room with this alias
@ -188,8 +187,7 @@ var commands = {
return reject(this.getUsage()); return reject(this.getUsage());
} }
if (!room_alias.match(/:/)) { if (!room_alias.match(/:/)) {
var domain = MatrixClientPeg.get().credentials.userId.replace(/^.*:/, ''); room_alias += ':' + MatrixClientPeg.get().getDomain();
room_alias += ':' + domain;
} }
// Try to find a room with this alias // Try to find a room with this alias

View File

@ -66,7 +66,7 @@ function textForMemberEvent(ev) {
function textForTopicEvent(ev) { function textForTopicEvent(ev) {
var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
return senderDisplayName + ' changed the topic to, "' + ev.getContent().topic + '"'; return senderDisplayName + ' changed the topic to "' + ev.getContent().topic + '"';
}; };
function textForRoomNameEvent(ev) { function textForRoomNameEvent(ev) {

View File

@ -23,7 +23,6 @@ limitations under the License.
module.exports.components = {}; module.exports.components = {};
module.exports.components['structures.CreateRoom'] = require('./components/structures/CreateRoom'); module.exports.components['structures.CreateRoom'] = require('./components/structures/CreateRoom');
module.exports.components['structures.login.ForgotPassword'] = require('./components/structures/login/ForgotPassword');
module.exports.components['structures.login.Login'] = require('./components/structures/login/Login'); module.exports.components['structures.login.Login'] = require('./components/structures/login/Login');
module.exports.components['structures.login.PostRegistration'] = require('./components/structures/login/PostRegistration'); module.exports.components['structures.login.PostRegistration'] = require('./components/structures/login/PostRegistration');
module.exports.components['structures.login.Registration'] = require('./components/structures/login/Registration'); module.exports.components['structures.login.Registration'] = require('./components/structures/login/Registration');
@ -32,6 +31,10 @@ module.exports.components['structures.RoomView'] = require('./components/structu
module.exports.components['structures.ScrollPanel'] = require('./components/structures/ScrollPanel'); module.exports.components['structures.ScrollPanel'] = require('./components/structures/ScrollPanel');
module.exports.components['structures.UploadBar'] = require('./components/structures/UploadBar'); module.exports.components['structures.UploadBar'] = require('./components/structures/UploadBar');
module.exports.components['structures.UserSettings'] = require('./components/structures/UserSettings'); module.exports.components['structures.UserSettings'] = require('./components/structures/UserSettings');
module.exports.components['structures.login.ForgotPassword'] = require('./components/structures/login/ForgotPassword');
module.exports.components['structures.login.Login'] = require('./components/structures/login/Login');
module.exports.components['structures.login.PostRegistration'] = require('./components/structures/login/PostRegistration');
module.exports.components['structures.login.Registration'] = require('./components/structures/login/Registration');
module.exports.components['views.avatars.BaseAvatar'] = require('./components/views/avatars/BaseAvatar'); module.exports.components['views.avatars.BaseAvatar'] = require('./components/views/avatars/BaseAvatar');
module.exports.components['views.avatars.MemberAvatar'] = require('./components/views/avatars/MemberAvatar'); module.exports.components['views.avatars.MemberAvatar'] = require('./components/views/avatars/MemberAvatar');
module.exports.components['views.avatars.RoomAvatar'] = require('./components/views/avatars/RoomAvatar'); module.exports.components['views.avatars.RoomAvatar'] = require('./components/views/avatars/RoomAvatar');
@ -41,7 +44,9 @@ module.exports.components['views.create_room.RoomAlias'] = require('./components
module.exports.components['views.dialogs.ErrorDialog'] = require('./components/views/dialogs/ErrorDialog'); module.exports.components['views.dialogs.ErrorDialog'] = require('./components/views/dialogs/ErrorDialog');
module.exports.components['views.dialogs.LogoutPrompt'] = require('./components/views/dialogs/LogoutPrompt'); module.exports.components['views.dialogs.LogoutPrompt'] = require('./components/views/dialogs/LogoutPrompt');
module.exports.components['views.dialogs.QuestionDialog'] = require('./components/views/dialogs/QuestionDialog'); module.exports.components['views.dialogs.QuestionDialog'] = require('./components/views/dialogs/QuestionDialog');
module.exports.components['views.dialogs.TextInputDialog'] = require('./components/views/dialogs/TextInputDialog');
module.exports.components['views.elements.EditableText'] = require('./components/views/elements/EditableText'); module.exports.components['views.elements.EditableText'] = require('./components/views/elements/EditableText');
module.exports.components['views.elements.PowerSelector'] = require('./components/views/elements/PowerSelector');
module.exports.components['views.elements.ProgressBar'] = require('./components/views/elements/ProgressBar'); module.exports.components['views.elements.ProgressBar'] = require('./components/views/elements/ProgressBar');
module.exports.components['views.elements.TintableSvg'] = require('./components/views/elements/TintableSvg'); module.exports.components['views.elements.TintableSvg'] = require('./components/views/elements/TintableSvg');
module.exports.components['views.elements.UserSelector'] = require('./components/views/elements/UserSelector'); module.exports.components['views.elements.UserSelector'] = require('./components/views/elements/UserSelector');

View File

@ -251,7 +251,7 @@ module.exports = React.createClass({
var UserSelector = sdk.getComponent("elements.UserSelector"); var UserSelector = sdk.getComponent("elements.UserSelector");
var RoomHeader = sdk.getComponent("rooms.RoomHeader"); var RoomHeader = sdk.getComponent("rooms.RoomHeader");
var domain = MatrixClientPeg.get().credentials.userId.replace(/^.*:/, ''); var domain = MatrixClientPeg.get().getDomain();
return ( return (
<div className="mx_CreateRoom"> <div className="mx_CreateRoom">

View File

@ -64,7 +64,7 @@ module.exports = React.createClass({
collapse_lhs: false, collapse_lhs: false,
collapse_rhs: false, collapse_rhs: false,
ready: false, ready: false,
width: 10000 width: 10000,
}; };
if (s.logged_in) { if (s.logged_in) {
if (MatrixClientPeg.get().getRooms().length) { if (MatrixClientPeg.get().getRooms().length) {
@ -304,7 +304,7 @@ module.exports = React.createClass({
}); });
break; break;
case 'view_room': case 'view_room':
this._viewRoom(payload.room_id); this._viewRoom(payload.room_id, payload.show_settings);
break; break;
case 'view_prev_room': case 'view_prev_room':
roomIndexDelta = -1; roomIndexDelta = -1;
@ -357,8 +357,29 @@ module.exports = React.createClass({
this.notifyNewScreen('settings'); this.notifyNewScreen('settings');
break; break;
case 'view_create_room': case 'view_create_room':
this._setPage(this.PageTypes.CreateRoom); //this._setPage(this.PageTypes.CreateRoom);
this.notifyNewScreen('new'); //this.notifyNewScreen('new');
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
var Loader = sdk.getComponent("elements.Spinner");
var modal = Modal.createDialog(Loader);
MatrixClientPeg.get().createRoom({
preset: "private_chat"
}).done(function(res) {
modal.close();
dis.dispatch({
action: 'view_room',
room_id: res.room_id,
show_settings: true,
});
}, function(err) {
modal.close();
Modal.createDialog(ErrorDialog, {
title: "Failed to create room",
description: err.toString()
});
});
break; break;
case 'view_room_directory': case 'view_room_directory':
this._setPage(this.PageTypes.RoomDirectory); this._setPage(this.PageTypes.RoomDirectory);
@ -399,7 +420,7 @@ module.exports = React.createClass({
}); });
}, },
_viewRoom: function(roomId) { _viewRoom: function(roomId, showSettings) {
// before we switch room, record the scroll state of the current room // before we switch room, record the scroll state of the current room
this._updateScrollMap(); this._updateScrollMap();
@ -437,6 +458,9 @@ module.exports = React.createClass({
var scrollState = this.scrollStateMap[roomId]; var scrollState = this.scrollStateMap[roomId];
this.refs.roomView.restoreScrollState(scrollState); this.refs.roomView.restoreScrollState(scrollState);
} }
if (this.refs.roomView && showSettings) {
this.refs.roomView.showSettings(true);
}
}, },
// update scrollStateMap according to the current scroll state of the // update scrollStateMap according to the current scroll state of the
@ -522,7 +546,9 @@ module.exports = React.createClass({
UserActivity.start(); UserActivity.start();
Presence.start(); Presence.start();
cli.startClient({ cli.startClient({
pendingEventOrdering: "end" pendingEventOrdering: "end",
// deliberately huge limit for now to avoid hitting gappy /sync's until gappy /sync performance improves
initialSyncLimit: 250,
}); });
}, },
@ -636,6 +662,8 @@ module.exports = React.createClass({
onUserClick: function(event, userId) { onUserClick: function(event, userId) {
event.preventDefault(); event.preventDefault();
/*
var MemberInfo = sdk.getComponent('rooms.MemberInfo'); var MemberInfo = sdk.getComponent('rooms.MemberInfo');
var member = new Matrix.RoomMember(null, userId); var member = new Matrix.RoomMember(null, userId);
ContextualMenu.createMenu(MemberInfo, { ContextualMenu.createMenu(MemberInfo, {
@ -643,6 +671,14 @@ module.exports = React.createClass({
right: window.innerWidth - event.pageX, right: window.innerWidth - event.pageX,
top: event.pageY top: event.pageY
}); });
*/
var member = new Matrix.RoomMember(null, userId);
if (!member) { return; }
dis.dispatch({
action: 'view_user',
member: member,
});
}, },
onLogoutClick: function(event) { onLogoutClick: function(event) {

View File

@ -142,16 +142,16 @@ module.exports = React.createClass({
// (We could use isMounted, but facebook have deprecated that.) // (We could use isMounted, but facebook have deprecated that.)
this.unmounted = true; this.unmounted = true;
if (this.refs.messagePanel) { if (this.refs.roomView) {
// disconnect the D&D event listeners from the message panel. This // disconnect the D&D event listeners from the room view. This
// is really just for hygiene - the messagePanel is going to be // is really just for hygiene - we're going to be
// deleted anyway, so it doesn't matter if the event listeners // deleted anyway, so it doesn't matter if the event listeners
// don't get cleaned up. // don't get cleaned up.
var messagePanel = ReactDOM.findDOMNode(this.refs.messagePanel); var roomView = ReactDOM.findDOMNode(this.refs.roomView);
messagePanel.removeEventListener('drop', this.onDrop); roomView.removeEventListener('drop', this.onDrop);
messagePanel.removeEventListener('dragover', this.onDragOver); roomView.removeEventListener('dragover', this.onDragOver);
messagePanel.removeEventListener('dragleave', this.onDragLeaveOrEnd); roomView.removeEventListener('dragleave', this.onDragLeaveOrEnd);
messagePanel.removeEventListener('dragend', this.onDragLeaveOrEnd); roomView.removeEventListener('dragend', this.onDragLeaveOrEnd);
} }
dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
if (MatrixClientPeg.get()) { if (MatrixClientPeg.get()) {
@ -414,6 +414,14 @@ module.exports = React.createClass({
window.addEventListener('resize', this.onResize); window.addEventListener('resize', this.onResize);
this.onResize(); this.onResize();
if (this.refs.roomView) {
var roomView = ReactDOM.findDOMNode(this.refs.roomView);
roomView.addEventListener('drop', this.onDrop);
roomView.addEventListener('dragover', this.onDragOver);
roomView.addEventListener('dragleave', this.onDragLeaveOrEnd);
roomView.addEventListener('dragend', this.onDragLeaveOrEnd);
}
this._updateTabCompleteList(this.state.room); this._updateTabCompleteList(this.state.room);
}, },
@ -432,11 +440,6 @@ module.exports = React.createClass({
var messagePanel = ReactDOM.findDOMNode(this.refs.messagePanel); var messagePanel = ReactDOM.findDOMNode(this.refs.messagePanel);
this.refs.messagePanel.initialised = true; this.refs.messagePanel.initialised = true;
messagePanel.addEventListener('drop', this.onDrop);
messagePanel.addEventListener('dragover', this.onDragOver);
messagePanel.addEventListener('dragleave', this.onDragLeaveOrEnd);
messagePanel.addEventListener('dragend', this.onDragLeaveOrEnd);
this.scrollToBottom(); this.scrollToBottom();
this.sendReadReceipt(); this.sendReadReceipt();
@ -884,9 +887,27 @@ module.exports = React.createClass({
old_history_visibility = "shared"; old_history_visibility = "shared";
} }
var old_guest_read = (old_history_visibility === "world_readable");
var old_guest_join = this.state.room.currentState.getStateEvents('m.room.guest_access', '');
if (old_guest_join) {
old_guest_join = (old_guest_join.getContent().guest_access === "can_join");
}
else {
old_guest_join = false;
}
var old_canonical_alias = this.state.room.currentState.getStateEvents('m.room.canonical_alias', '');
if (old_canonical_alias) {
old_canonical_alias = old_canonical_alias.getContent().alias;
}
else {
old_canonical_alias = "";
}
var deferreds = []; var deferreds = [];
if (old_name != newVals.name && newVals.name != undefined && newVals.name) { if (old_name != newVals.name && newVals.name != undefined) {
deferreds.push( deferreds.push(
MatrixClientPeg.get().setRoomName(this.state.room.roomId, newVals.name) MatrixClientPeg.get().setRoomName(this.state.room.roomId, newVals.name)
); );
@ -919,6 +940,13 @@ module.exports = React.createClass({
); );
} }
// setRoomMutePushRule will do nothing if there is no change
deferreds.push(
MatrixClientPeg.get().setRoomMutePushRule(
"global", this.state.room.roomId, newVals.are_notifications_muted
)
);
if (newVals.power_levels) { if (newVals.power_levels) {
deferreds.push( deferreds.push(
MatrixClientPeg.get().sendStateEvent( MatrixClientPeg.get().sendStateEvent(
@ -927,6 +955,83 @@ module.exports = React.createClass({
); );
} }
if (newVals.alias_operations) {
var oplist = [];
for (var i = 0; i < newVals.alias_operations.length; i++) {
var alias_operation = newVals.alias_operations[i];
switch (alias_operation.type) {
case 'put':
oplist.push(
MatrixClientPeg.get().createAlias(
alias_operation.alias, this.state.room.roomId
)
);
break;
case 'delete':
oplist.push(
MatrixClientPeg.get().deleteAlias(
alias_operation.alias
)
);
break;
default:
console.log("Unknown alias operation, ignoring: " + alias_operation.type);
}
}
if (oplist.length) {
var deferred = oplist[0];
oplist.splice(1).forEach(function (f) {
deferred = deferred.then(f);
});
deferreds.push(deferred);
}
}
if (newVals.tag_operations) {
// FIXME: should probably be factored out with alias_operations above
var oplist = [];
for (var i = 0; i < newVals.tag_operations.length; i++) {
var tag_operation = newVals.tag_operations[i];
switch (tag_operation.type) {
case 'put':
oplist.push(
MatrixClientPeg.get().setRoomTag(
this.props.roomId, tag_operation.tag, {}
)
);
break;
case 'delete':
oplist.push(
MatrixClientPeg.get().deleteRoomTag(
this.props.roomId, tag_operation.tag
)
);
break;
default:
console.log("Unknown tag operation, ignoring: " + tag_operation.type);
}
}
if (oplist.length) {
var deferred = oplist[0];
oplist.splice(1).forEach(function (f) {
deferred = deferred.then(f);
});
deferreds.push(deferred);
}
}
if (old_canonical_alias !== newVals.canonical_alias) {
deferreds.push(
MatrixClientPeg.get().sendStateEvent(
this.state.room.roomId, "m.room.canonical_alias", {
alias: newVals.canonical_alias
}, ""
)
);
}
if (newVals.color_scheme) { if (newVals.color_scheme) {
deferreds.push( deferreds.push(
MatrixClientPeg.get().setRoomAccountData( MatrixClientPeg.get().setRoomAccountData(
@ -935,26 +1040,43 @@ module.exports = React.createClass({
); );
} }
deferreds.push( if (old_guest_read != newVals.guest_read ||
MatrixClientPeg.get().setGuestAccess(this.state.room.roomId, { old_guest_join != newVals.guest_join)
allowRead: newVals.guest_read, {
allowJoin: newVals.guest_join deferreds.push(
}) MatrixClientPeg.get().setGuestAccess(this.state.room.roomId, {
); allowRead: newVals.guest_read,
allowJoin: newVals.guest_join
})
);
}
if (deferreds.length) { if (deferreds.length) {
var self = this; var self = this;
q.all(deferreds).fail(function(err) { q.allSettled(deferreds).then(
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); function(results) {
Modal.createDialog(ErrorDialog, { var fails = results.filter(function(result) { return result.state !== "fulfilled" });
title: "Failed to set state", if (fails.length) {
description: err.toString() fails.forEach(function(result) {
console.error(result.reason);
});
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Failed to set state",
description: fails.map(function(result) { return result.reason }).join("\n"),
});
self.refs.room_settings.resetState();
}
else {
self.setState({
editingRoomSettings: false
});
}
}).finally(function() {
self.setState({
uploadingRoomSettings: false,
});
}); });
}).finally(function() {
self.setState({
uploadingRoomSettings: false,
});
});
} else { } else {
this.setState({ this.setState({
editingRoomSettings: false, editingRoomSettings: false,
@ -1022,16 +1144,19 @@ module.exports = React.createClass({
onSaveClick: function() { onSaveClick: function() {
this.setState({ this.setState({
editingRoomSettings: false,
uploadingRoomSettings: true, uploadingRoomSettings: true,
}); });
this.uploadNewState({ this.uploadNewState({
name: this.refs.header.getRoomName(), name: this.refs.header.getRoomName(),
topic: this.refs.room_settings.getTopic(), topic: this.refs.header.getTopic(),
join_rule: this.refs.room_settings.getJoinRules(), join_rule: this.refs.room_settings.getJoinRules(),
history_visibility: this.refs.room_settings.getHistoryVisibility(), history_visibility: this.refs.room_settings.getHistoryVisibility(),
are_notifications_muted: this.refs.room_settings.areNotificationsMuted(),
power_levels: this.refs.room_settings.getPowerLevels(), power_levels: this.refs.room_settings.getPowerLevels(),
alias_operations: this.refs.room_settings.getAliasOperations(),
tag_operations: this.refs.room_settings.getTagOperations(),
canonical_alias: this.refs.room_settings.getCanonicalAlias(),
guest_join: this.refs.room_settings.canGuestsJoin(), guest_join: this.refs.room_settings.canGuestsJoin(),
guest_read: this.refs.room_settings.canGuestsRead(), guest_read: this.refs.room_settings.canGuestsRead(),
color_scheme: this.refs.room_settings.getColorScheme(), color_scheme: this.refs.room_settings.getColorScheme(),
@ -1187,26 +1312,32 @@ module.exports = React.createClass({
// a minimum of the height of the video element, whilst also capping it from pushing out the page // a minimum of the height of the video element, whilst also capping it from pushing out the page
// so we have to do it via JS instead. In this implementation we cap the height by putting // so we have to do it via JS instead. In this implementation we cap the height by putting
// a maxHeight on the underlying remote video tag. // a maxHeight on the underlying remote video tag.
var auxPanelMaxHeight;
// header + footer + status + give us at least 120px of scrollback at all times.
var auxPanelMaxHeight = window.innerHeight -
(83 + // height of RoomHeader
36 + // height of the status area
72 + // minimum height of the message compmoser
120); // amount of desired scrollback
// XXX: this is a bit of a hack and might possibly cause the video to push out the page anyway
// but it's better than the video going missing entirely
if (auxPanelMaxHeight < 50) auxPanelMaxHeight = 50;
if (this.refs.callView) { if (this.refs.callView) {
var video = this.refs.callView.getVideoView().getRemoteVideoElement(); var video = this.refs.callView.getVideoView().getRemoteVideoElement();
// header + footer + status + give us at least 100px of scrollback at all times.
auxPanelMaxHeight = window.innerHeight -
(83 + 72 +
sdk.getComponent('rooms.MessageComposer').MAX_HEIGHT +
100);
// XXX: this is a bit of a hack and might possibly cause the video to push out the page anyway
// but it's better than the video going missing entirely
if (auxPanelMaxHeight < 50) auxPanelMaxHeight = 50;
video.style.maxHeight = auxPanelMaxHeight + "px"; video.style.maxHeight = auxPanelMaxHeight + "px";
// the above might have made the video panel resize itself, so now // the above might have made the video panel resize itself, so now
// we need to tell the gemini panel to adapt. // we need to tell the gemini panel to adapt.
this.onChildResize(); this.onChildResize();
} }
// we need to do this for general auxPanels too
if (this.refs.auxPanel) {
this.refs.auxPanel.style.maxHeight = auxPanelMaxHeight + "px";
}
}, },
onFullscreenClick: function() { onFullscreenClick: function() {
@ -1249,6 +1380,13 @@ module.exports = React.createClass({
} }
}, },
showSettings: function(show) {
// XXX: this is a bit naughty; we should be doing this via props
if (show) {
this.setState({editingRoomSettings: true});
}
},
render: function() { render: function() {
var RoomHeader = sdk.getComponent('rooms.RoomHeader'); var RoomHeader = sdk.getComponent('rooms.RoomHeader');
var MessageComposer = sdk.getComponent('rooms.MessageComposer'); var MessageComposer = sdk.getComponent('rooms.MessageComposer');
@ -1399,7 +1537,7 @@ module.exports = React.createClass({
var aux = null; var aux = null;
if (this.state.editingRoomSettings) { if (this.state.editingRoomSettings) {
aux = <RoomSettings ref="room_settings" onSaveClick={this.onSaveClick} room={this.state.room} />; aux = <RoomSettings ref="room_settings" onSaveClick={this.onSaveClick} onCancelClick={this.onCancelClick} room={this.state.room} />;
} }
else if (this.state.uploadingRoomSettings) { else if (this.state.uploadingRoomSettings) {
var Loader = sdk.getComponent("elements.Spinner"); var Loader = sdk.getComponent("elements.Spinner");
@ -1433,7 +1571,7 @@ module.exports = React.createClass({
fileDropTarget = <div className="mx_RoomView_fileDropTarget"> fileDropTarget = <div className="mx_RoomView_fileDropTarget">
<div className="mx_RoomView_fileDropTargetLabel" title="Drop File Here"> <div className="mx_RoomView_fileDropTargetLabel" title="Drop File Here">
<TintableSvg src="img/upload-big.svg" width="45" height="59"/><br/> <TintableSvg src="img/upload-big.svg" width="45" height="59"/><br/>
Drop File Here Drop file here to upload
</div> </div>
</div>; </div>;
} }
@ -1534,7 +1672,7 @@ module.exports = React.createClass({
); );
return ( return (
<div className={ "mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "") }> <div className={ "mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "") } ref="roomView">
<RoomHeader ref="header" room={this.state.room} searchInfo={searchInfo} <RoomHeader ref="header" room={this.state.room} searchInfo={searchInfo}
editing={this.state.editingRoomSettings} editing={this.state.editingRoomSettings}
onSearchClick={this.onSearchClick} onSearchClick={this.onSearchClick}
@ -1547,8 +1685,8 @@ module.exports = React.createClass({
onLeaveClick={ onLeaveClick={
(myMember && myMember.membership === "join") ? this.onLeaveClick : null (myMember && myMember.membership === "join") ? this.onLeaveClick : null
} /> } />
{ fileDropTarget } <div className="mx_RoomView_auxPanel" ref="auxPanel">
<div className="mx_RoomView_auxPanel"> { fileDropTarget }
<CallView ref="callView" room={this.state.room} ConferenceHandler={this.props.ConferenceHandler} <CallView ref="callView" room={this.state.room} ConferenceHandler={this.props.ConferenceHandler}
onResize={this.onChildResize} /> onResize={this.onChildResize} />
{ conferenceCallNotification } { conferenceCallNotification }

View File

@ -21,6 +21,7 @@ var dis = require("../../dispatcher");
var q = require('q'); var q = require('q');
var version = require('../../../package.json').version; var version = require('../../../package.json').version;
var UserSettingsStore = require('../../UserSettingsStore'); var UserSettingsStore = require('../../UserSettingsStore');
var GeminiScrollbar = require('react-gemini-scrollbar');
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'UserSettings', displayName: 'UserSettings',
@ -83,6 +84,12 @@ module.exports = React.createClass({
} }
}, },
onAvatarPickerClick: function(ev) {
if (this.refs.file_label) {
this.refs.file_label.click();
}
},
onAvatarSelected: function(ev) { onAvatarSelected: function(ev) {
var self = this; var self = this;
var changeAvatar = this.refs.changeAvatar; var changeAvatar = this.refs.changeAvatar;
@ -145,10 +152,6 @@ module.exports = React.createClass({
this.logoutModal.closeDialog(); this.logoutModal.closeDialog();
}, },
onEnableNotificationsChange: function(event) {
UserSettingsStore.setEnableNotifications(event.target.checked);
},
render: function() { render: function() {
switch (this.state.phase) { switch (this.state.phase) {
case "UserSettings.LOADING": case "UserSettings.LOADING":
@ -166,6 +169,7 @@ module.exports = React.createClass({
var ChangeDisplayName = sdk.getComponent("views.settings.ChangeDisplayName"); var ChangeDisplayName = sdk.getComponent("views.settings.ChangeDisplayName");
var ChangePassword = sdk.getComponent("views.settings.ChangePassword"); var ChangePassword = sdk.getComponent("views.settings.ChangePassword");
var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar'); var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
var Notifications = sdk.getComponent("settings.Notifications");
var avatarUrl = ( var avatarUrl = (
this.state.avatarUrl ? MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl) : null this.state.avatarUrl ? MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl) : null
); );
@ -175,7 +179,7 @@ module.exports = React.createClass({
if (MatrixClientPeg.get().isGuest()) { if (MatrixClientPeg.get().isGuest()) {
accountJsx = ( accountJsx = (
<div className="mx_UserSettings_button" onClick={this.onUpgradeClicked}> <div className="mx_UserSettings_button" onClick={this.onUpgradeClicked}>
Upgrade (It's free!) Create an account
</div> </div>
); );
} }
@ -196,6 +200,8 @@ module.exports = React.createClass({
<div className="mx_UserSettings"> <div className="mx_UserSettings">
<RoomHeader simpleHeader="Settings" /> <RoomHeader simpleHeader="Settings" />
<GeminiScrollbar className="mx_UserSettings_body" autoshow={true}>
<h2>Profile</h2> <h2>Profile</h2>
<div className="mx_UserSettings_section"> <div className="mx_UserSettings_section">
@ -225,13 +231,15 @@ module.exports = React.createClass({
</div> </div>
<div className="mx_UserSettings_avatarPicker"> <div className="mx_UserSettings_avatarPicker">
<ChangeAvatar ref="changeAvatar" initialAvatarUrl={avatarUrl} <div onClick={ this.onAvatarPickerClick }>
showUploadSection={false} className="mx_UserSettings_avatarPicker_img"/> <ChangeAvatar ref="changeAvatar" initialAvatarUrl={avatarUrl}
showUploadSection={false} className="mx_UserSettings_avatarPicker_img"/>
</div>
<div className="mx_UserSettings_avatarPicker_edit"> <div className="mx_UserSettings_avatarPicker_edit">
<label htmlFor="avatarInput"> <label htmlFor="avatarInput" ref="file_label">
<img src="img/upload.svg" <img src="img/camera.svg"
alt="Upload avatar" title="Upload avatar" alt="Upload avatar" title="Upload avatar"
width="19" height="24" /> width="17" height="15" />
</label> </label>
<input id="avatarInput" type="file" onChange={this.onAvatarSelected}/> <input id="avatarInput" type="file" onChange={this.onAvatarSelected}/>
</div> </div>
@ -241,34 +249,18 @@ module.exports = React.createClass({
<h2>Account</h2> <h2>Account</h2>
<div className="mx_UserSettings_section"> <div className="mx_UserSettings_section">
{accountJsx}
</div> <div className="mx_UserSettings_logout mx_UserSettings_button" onClick={this.onLogoutClicked}>
<div className="mx_UserSettings_logout">
<div className="mx_UserSettings_button" onClick={this.onLogoutClicked}>
Log out Log out
</div> </div>
{accountJsx}
</div> </div>
<h2>Notifications</h2> <h2>Notifications</h2>
<div className="mx_UserSettings_section"> <div className="mx_UserSettings_section">
<div className="mx_UserSettings_notifTable"> <Notifications/>
<div className="mx_UserSettings_notifTableRow">
<div className="mx_UserSettings_notifInputCell">
<input id="enableNotifications"
ref="enableNotifications"
type="checkbox"
checked={ UserSettingsStore.getEnableNotifications() }
onChange={ this.onEnableNotificationsChange } />
</div>
<div className="mx_UserSettings_notifLabelCell">
<label htmlFor="enableNotifications">
Enable desktop notifications
</label>
</div>
</div>
</div>
</div> </div>
<h2>Advanced</h2> <h2>Advanced</h2>
@ -281,6 +273,8 @@ module.exports = React.createClass({
Version {this.state.clientVersion} Version {this.state.clientVersion}
</div> </div>
</div> </div>
</GeminiScrollbar>
</div> </div>
); );
} }

View File

@ -48,7 +48,7 @@ module.exports = React.createClass({
render: function() { render: function() {
return ( return (
<div className="mx_ErrorDialog"> <div className="mx_ErrorDialog">
<div className="mx_ErrorDialogTitle"> <div className="mx_Dialog_title">
{this.props.title} {this.props.title}
</div> </div>
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">

View File

@ -46,7 +46,7 @@ module.exports = React.createClass({
render: function() { render: function() {
return ( return (
<div className="mx_QuestionDialog"> <div className="mx_QuestionDialog">
<div className="mx_QuestionDialogTitle"> <div className="mx_Dialog_title">
{this.props.title} {this.props.title}
</div> </div>
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">

View File

@ -0,0 +1,94 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var React = require("react");
module.exports = React.createClass({
displayName: 'TextInputDialog',
propTypes: {
title: React.PropTypes.string,
description: React.PropTypes.string,
value: React.PropTypes.string,
button: React.PropTypes.string,
focus: React.PropTypes.bool,
onFinished: React.PropTypes.func.isRequired
},
getDefaultProps: function() {
return {
title: "",
value: "",
description: "",
button: "OK",
focus: true
};
},
componentDidMount: function() {
if (this.props.focus) {
// Set the cursor at the end of the text input
this.refs.textinput.value = this.props.value;
}
},
onOk: function() {
this.props.onFinished(true, this.refs.textinput.value);
},
onCancel: function() {
this.props.onFinished(false);
},
onKeyDown: function(e) {
if (e.keyCode === 27) { // escape
e.stopPropagation();
e.preventDefault();
this.props.onFinished(false);
}
else if (e.keyCode === 13) { // enter
e.stopPropagation();
e.preventDefault();
this.props.onFinished(true, this.refs.textinput.value);
}
},
render: function() {
return (
<div className="mx_TextInputDialog">
<div className="mx_Dialog_title">
{this.props.title}
</div>
<div className="mx_Dialog_content">
<div className="mx_TextInputDialog_label">
<label htmlFor="textinput"> {this.props.description} </label>
</div>
<div>
<input id="textinput" ref="textinput" className="mx_TextInputDialog_input" defaultValue={this.props.value} autoFocus={this.props.focus} size="64" onKeyDown={this.onKeyDown}/>
</div>
</div>
<div className="mx_Dialog_buttons">
<button onClick={this.onOk}>
{this.props.button}
</button>
<button onClick={this.onCancel}>
Cancel
</button>
</div>
</div>
);
}
});

View File

@ -18,13 +18,22 @@ limitations under the License.
var React = require('react'); var React = require('react');
const KEY_TAB = 9;
const KEY_SHIFT = 16;
const KEY_WINDOWS = 91;
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'EditableText', displayName: 'EditableText',
propTypes: { propTypes: {
onValueChanged: React.PropTypes.func, onValueChanged: React.PropTypes.func,
initialValue: React.PropTypes.string, initialValue: React.PropTypes.string,
label: 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: { Phases: {
@ -36,38 +45,62 @@ module.exports = React.createClass({
return { return {
onValueChanged: function() {}, onValueChanged: function() {},
initialValue: '', initialValue: '',
label: 'Click to set', label: '',
placeholder: '', placeholder: '',
editable: true,
}; };
}, },
getInitialState: function() { getInitialState: function() {
return { return {
value: this.props.initialValue,
phase: this.Phases.Display, phase: this.Phases.Display,
} }
}, },
componentWillReceiveProps: function(nextProps) { componentWillReceiveProps: function(nextProps) {
this.setState({ if (nextProps.initialValue !== this.props.initialValue) {
value: nextProps.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() { getValue: function() {
return this.state.value; return this.value;
}, },
setValue: function(val, shouldSubmit, suppressListener) { setValue: function(value) {
var self = this; this.value = value;
this.setState({ this.showPlaceholder(!this.value);
value: val,
phase: this.Phases.Display,
}, function() {
if (!suppressListener) {
self.onValueChanged(shouldSubmit);
}
});
}, },
edit: function() { edit: function() {
@ -80,65 +113,106 @@ module.exports = React.createClass({
this.setState({ this.setState({
phase: this.Phases.Display, phase: this.Phases.Display,
}); });
this.value = this.props.initialValue;
this.showPlaceholder(!this.value);
this.onValueChanged(false); this.onValueChanged(false);
}, },
onValueChanged: function(shouldSubmit) { 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) { 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") { if (ev.key == "Enter") {
this.onFinish(ev); this.onFinish(ev);
} else if (ev.key == "Escape") { } else if (ev.key == "Escape") {
this.cancelEdit(); 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({ this.setState({
phase: this.Phases.Edit, phase: this.Phases.Edit,
}) })
}, },
onFocus: function(ev) { onFocus: function(ev) {
ev.target.setSelectionRange(0, ev.target.value.length); //ev.target.setSelectionRange(0, ev.target.textContent.length);
},
onFinish: function(ev) { var node = ev.target.childNodes[0];
if (ev.target.value) { if (node) {
this.setValue(ev.target.value, ev.key === "Enter"); var range = document.createRange();
} else { range.setStart(node, 0);
this.cancelEdit(); range.setEnd(node, node.length);
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
} }
}, },
onBlur: function() { onFinish: function(ev) {
this.cancelEdit(); 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() { render: function() {
var editable_el; var editable_el;
if (this.state.phase == this.Phases.Display) { if (!this.props.editable || (this.state.phase == this.Phases.Display && (this.props.label || this.props.labelClassName) && !this.value)) {
if (this.state.value) { // show the label
editable_el = <div ref="display_div" onClick={this.onClickDiv}>{this.state.value}</div>; editable_el = <div className={this.props.className + " " + this.props.labelClassName} onClick={this.onClickDiv}>{ this.props.label || this.props.initialValue }</div>;
} else { } else {
editable_el = <div ref="display_div" onClick={this.onClickDiv}>{this.props.label}</div>; // 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}
} else if (this.state.phase == this.Phases.Edit) { onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} onFocus={this.onFocus} onBlur={this.onBlur}></div>;
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>
);
} }
return ( return editable_el;
<div className="mx_EditableText">
{editable_el}
</div>
);
} }
}); });

View 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>
);
}
});

View File

@ -22,7 +22,7 @@ module.exports = React.createClass({
render: function() { render: function() {
return ( return (
<div className="mx_ErrorDialog"> <div className="mx_ErrorDialog">
<div className="mx_ErrorDialogTitle"> <div className="mx_Dialog_title">
Custom Server Options Custom Server Options
</div> </div>
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">

View File

@ -36,6 +36,9 @@ module.exports = React.createClass({
}, },
componentDidUpdate: function() { 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") if (this.props.mxEvent.getContent().format === "org.matrix.custom.html")
HtmlUtils.highlightDom(ReactDOM.findDOMNode(this)); HtmlUtils.highlightDom(ReactDOM.findDOMNode(this));
}, },

View File

@ -58,15 +58,16 @@ module.exports = React.createClass({
var roomId = this.props.member.roomId; var roomId = this.props.member.roomId;
var target = this.props.member.userId; var target = this.props.member.userId;
MatrixClientPeg.get().kick(roomId, target).done(function() { MatrixClientPeg.get().kick(roomId, target).done(function() {
// NO-OP; rely on the m.room.member event coming down else we could // NO-OP; rely on the m.room.member event coming down else we could
// get out of sync if we force setState here! // get out of sync if we force setState here!
console.log("Kick success"); console.log("Kick success");
}, function(err) { }, function(err) {
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Kick error", title: "Kick error",
description: err.message description: err.message
}); });
}); }
);
this.props.onFinished(); this.props.onFinished();
}, },
@ -74,16 +75,18 @@ module.exports = React.createClass({
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
var roomId = this.props.member.roomId; var roomId = this.props.member.roomId;
var target = this.props.member.userId; var target = this.props.member.userId;
MatrixClientPeg.get().ban(roomId, target).done(function() { MatrixClientPeg.get().ban(roomId, target).done(
// NO-OP; rely on the m.room.member event coming down else we could function() {
// get out of sync if we force setState here! // NO-OP; rely on the m.room.member event coming down else we could
console.log("Ban success"); // get out of sync if we force setState here!
}, function(err) { console.log("Ban success");
Modal.createDialog(ErrorDialog, { }, function(err) {
title: "Ban error", Modal.createDialog(ErrorDialog, {
description: err.message title: "Ban error",
}); description: err.message
}); });
}
);
this.props.onFinished(); this.props.onFinished();
}, },
@ -118,16 +121,17 @@ module.exports = React.createClass({
} }
MatrixClientPeg.get().setPowerLevel(roomId, target, level, powerLevelEvent).done( MatrixClientPeg.get().setPowerLevel(roomId, target, level, powerLevelEvent).done(
function() { function() {
// NO-OP; rely on the m.room.member event coming down else we could // NO-OP; rely on the m.room.member event coming down else we could
// get out of sync if we force setState here! // get out of sync if we force setState here!
console.log("Mute toggle success"); console.log("Mute toggle success");
}, function(err) { }, function(err) {
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Mute error", title: "Mute error",
description: err.message description: err.message
}); });
}); }
);
this.props.onFinished(); this.props.onFinished();
}, },
@ -154,22 +158,55 @@ module.exports = React.createClass({
} }
var defaultLevel = powerLevelEvent.getContent().users_default; var defaultLevel = powerLevelEvent.getContent().users_default;
var modLevel = me.powerLevel - 1; var modLevel = me.powerLevel - 1;
if (modLevel > 50 && defaultLevel < 50) modLevel = 50; // try to stick with the vector level defaults
// toggle the level // toggle the level
var newLevel = this.state.isTargetMod ? defaultLevel : modLevel; var newLevel = this.state.isTargetMod ? defaultLevel : modLevel;
MatrixClientPeg.get().setPowerLevel(roomId, target, newLevel, powerLevelEvent).done( MatrixClientPeg.get().setPowerLevel(roomId, target, newLevel, powerLevelEvent).done(
function() { function() {
// NO-OP; rely on the m.room.member event coming down else we could // NO-OP; rely on the m.room.member event coming down else we could
// get out of sync if we force setState here! // get out of sync if we force setState here!
console.log("Mod toggle success"); console.log("Mod toggle success");
}, function(err) { }, function(err) {
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Mod error", title: "Mod error",
description: err.message description: err.message
}); });
}); }
);
this.props.onFinished(); 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() { onChatClick: function() {
// check if there are any existing rooms with just us and them (1:1) // 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. // 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({ MatrixClientPeg.get().createRoom({
invite: [this.props.member.userId], invite: [this.props.member.userId],
preset: "private_chat" preset: "private_chat"
}).done(function(res) { }).done(
self.setState({ creatingRoom: false }); function(res) {
dis.dispatch({ self.setState({ creatingRoom: false });
action: 'view_room', dis.dispatch({
room_id: res.room_id action: 'view_room',
}); room_id: res.room_id
self.props.onFinished(); });
}, function(err) { self.props.onFinished();
self.setState({ creatingRoom: false }); }, function(err) {
console.error( self.setState({ creatingRoom: false });
"Failed to create room: %s", JSON.stringify(err) console.error(
); "Failed to create room: %s", JSON.stringify(err)
self.props.onFinished(); );
}); self.props.onFinished();
}
);
} }
}, },
@ -291,9 +330,15 @@ module.exports = React.createClass({
(powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) || (powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) ||
powerLevels.state_default powerLevels.state_default
); );
var levelToSend = (
(powerLevels.events ? powerLevels.events["m.room.message"] : null) ||
powerLevels.events_default
);
can.kick = me.powerLevel >= powerLevels.kick; can.kick = me.powerLevel >= powerLevels.kick;
can.ban = me.powerLevel >= powerLevels.ban; can.ban = me.powerLevel >= powerLevels.ban;
can.mute = me.powerLevel >= editPowerLevel; can.mute = me.powerLevel >= editPowerLevel;
can.toggleMod = me.powerLevel > them.powerLevel && them.powerLevel >= levelToSend;
can.modifyLevel = me.powerLevel > them.powerLevel; can.modifyLevel = me.powerLevel > them.powerLevel;
return can; return can;
}, },
@ -317,12 +362,11 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
var interactButton, kickButton, banButton, muteButton, giveModButton, spinner; var startChat, kickButton, banButton, muteButton, giveModButton, spinner;
if (this.props.member.userId === MatrixClientPeg.get().credentials.userId) { if (this.props.member.userId !== MatrixClientPeg.get().credentials.userId) {
interactButton = <div className="mx_MemberInfo_field" onClick={this.onLeaveClick}>Leave room</div>; // FIXME: we're referring to a vector component from react-sdk
} var BottomLeftMenuTile = sdk.getComponent('rooms.BottomLeftMenuTile');
else { startChat = <BottomLeftMenuTile collapsed={ false } img="img/create-big.svg" label="Start chat" onClick={ this.onChatClick }/>
interactButton = <div className="mx_MemberInfo_field" onClick={this.onChatClick}>Start chat</div>;
} }
if (this.state.creatingRoom) { if (this.state.creatingRoom) {
@ -346,35 +390,56 @@ module.exports = React.createClass({
{muteLabel} {muteLabel}
</div>; </div>;
} }
if (this.state.can.modifyLevel) { if (this.state.can.toggleMod) {
var giveOpLabel = this.state.isTargetMod ? "Revoke Mod" : "Make Mod"; var giveOpLabel = this.state.isTargetMod ? "Revoke Moderator" : "Make Moderator";
giveModButton = <div className="mx_MemberInfo_field" onClick={this.onModToggle}> giveModButton = <div className="mx_MemberInfo_field" onClick={this.onModToggle}>
{giveOpLabel} {giveOpLabel}
</div> </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 MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
var PowerSelector = sdk.getComponent('elements.PowerSelector');
return ( return (
<div className="mx_MemberInfo"> <div className="mx_MemberInfo">
<img className="mx_MemberInfo_cancel" src="img/cancel.svg" width="18" height="18" onClick={this.onCancel}/> <img className="mx_MemberInfo_cancel" src="img/cancel.svg" width="18" height="18" onClick={this.onCancel}/>
<div className="mx_MemberInfo_avatar"> <div className="mx_MemberInfo_avatar">
<MemberAvatar member={this.props.member} width={48} height={48} /> <MemberAvatar member={this.props.member} width={48} height={48} />
</div> </div>
<h2>{ this.props.member.name }</h2> <h2>{ this.props.member.name }</h2>
<div className="mx_MemberInfo_profileField">
{ this.props.member.userId } <div className="mx_MemberInfo_profile">
</div> <div className="mx_MemberInfo_profileField">
<div className="mx_MemberInfo_profileField"> { this.props.member.userId }
power: { this.props.member.powerLevelNorm }% </div>
</div> <div className="mx_MemberInfo_profileField">
<div className="mx_MemberInfo_buttons"> Level: <b><PowerSelector value={ parseInt(this.props.member.powerLevel) } disabled={ !this.state.can.modifyLevel } onChange={ this.onPowerChange }/></b>
{interactButton} </div>
{muteButton}
{kickButton}
{banButton}
{giveModButton}
{spinner}
</div> </div>
{ startChat }
{ adminTools }
{ spinner }
</div> </div>
); );
} }

View File

@ -63,7 +63,7 @@ module.exports = React.createClass({
}, },
getPowerLabel: function() { getPowerLabel: function() {
return this.props.member.userId; return this.props.member.userId + " (power " + this.props.member.powerLevel + ")";
}, },
render: function() { render: function() {
@ -79,6 +79,14 @@ module.exports = React.createClass({
var av = ( var av = (
<MemberAvatar member={member} width={36} height={36} /> <MemberAvatar member={member} width={36} height={36} />
); );
var power;
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"/>;
}
if (member.user) { if (member.user) {
this.user_last_modified_time = member.user.getLastModifiedTime(); this.user_last_modified_time = member.user.getLastModifiedTime();
@ -94,7 +102,7 @@ module.exports = React.createClass({
<EntityTile {...this.props} presenceActiveAgo={active} presenceState={presenceState} <EntityTile {...this.props} presenceActiveAgo={active} presenceState={presenceState}
avatarJsx={av} title={this.getPowerLabel()} onClick={this.onClick} avatarJsx={av} title={this.getPowerLabel()} onClick={this.onClick}
shouldComponentUpdate={this.shouldComponentUpdate.bind(this)} shouldComponentUpdate={this.shouldComponentUpdate.bind(this)}
name={name} /> name={name} powerLevel={this.props.member.powerLevel} />
); );
} }
}); });

View File

@ -209,23 +209,18 @@ module.exports = React.createClass({
this.sentHistory.push(input); this.sentHistory.push(input);
this.onEnter(ev); this.onEnter(ev);
} }
else if (ev.keyCode === KeyCode.UP) { else if (ev.keyCode === KeyCode.UP || ev.keyCode === KeyCode.DOWN) {
var input = this.refs.textarea.value; var oldSelectionStart = this.refs.textarea.selectionStart;
var offset = this.refs.textarea.selectionStart || 0; // Remember the keyCode because React will recycle the synthetic event
if (ev.ctrlKey || !input.substr(0, offset).match(/\n/)) { var keyCode = ev.keyCode;
this.sentHistory.next(1); // set a callback so we can see if the cursor position changes as
ev.preventDefault(); // a result of this event. If it doesn't, we cycle history.
this.resizeInput(); setTimeout(() => {
} if (this.refs.textarea.selectionStart == oldSelectionStart) {
} this.sentHistory.next(keyCode === KeyCode.UP ? 1 : -1);
else if (ev.keyCode === KeyCode.DOWN) { this.resizeInput();
var input = this.refs.textarea.value; }
var offset = this.refs.textarea.selectionStart || 0; }, 0);
if (ev.ctrlKey || !input.substr(offset).match(/\n/)) {
this.sentHistory.next(-1);
ev.preventDefault();
this.resizeInput();
}
} }
if (this.props.tabComplete) { if (this.props.tabComplete) {

View File

@ -21,6 +21,12 @@ var sdk = require('../../../index');
var dis = require("../../../dispatcher"); var dis = require("../../../dispatcher");
var MatrixClientPeg = require('../../../MatrixClientPeg'); var MatrixClientPeg = require('../../../MatrixClientPeg');
var linkify = require('linkifyjs');
var linkifyElement = require('linkifyjs/element');
var linkifyMatrix = require('../../../linkify-matrix');
linkifyMatrix(linkify);
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'RoomHeader', 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) { onVideoClick: function(e) {
dis.dispatch({ dis.dispatch({
action: 'place_call', action: 'place_call',
@ -57,26 +82,59 @@ module.exports = React.createClass({
}); });
}, },
onNameChange: function(new_name) { onNameChanged: function(value) {
if (this.props.room.name != new_name && new_name) { this.setState({ name : value });
MatrixClientPeg.get().setRoomName(this.props.room.roomId, new_name); },
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() { getRoomName: function() {
return this.refs.name_edit.value; return this.state.name;
},
getTopic: function() {
return this.state.topic;
}, },
render: function() { render: function() {
var EditableText = sdk.getComponent("elements.EditableText"); 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 TintableSvg = sdk.getComponent("elements.TintableSvg");
var header; var header;
if (this.props.simpleHeader) { if (this.props.simpleHeader) {
var cancel; var cancel;
if (this.props.onCancelClick) { 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 = header =
<div className="mx_RoomHeader_wrapper"> <div className="mx_RoomHeader_wrapper">
@ -87,27 +145,72 @@ module.exports = React.createClass({
</div> </div>
} }
else { else {
var topic = this.props.room.currentState.getStateEvents('m.room.topic', '');
var name = null; var name = null;
var searchStatus = null; var searchStatus = null;
var topic_el = null; var topic_el = null;
var cancel_button = null; var cancel_button = null;
var save_button = null; var save_button = null;
var settings_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) { 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; var searchStatus;
// don't display the search count until the search completes and // don't display the search count until the search completes and
// gives us a valid (possibly zero) searchCount. // gives us a valid (possibly zero) searchCount.
@ -116,21 +219,55 @@ module.exports = React.createClass({
} }
name = name =
<div className="mx_RoomHeader_name"> <div className="mx_RoomHeader_name" onClick={this.props.onSettingsClick}>
<div className="mx_RoomHeader_nametext" title={ this.props.room.name }>{ this.props.room.name }</div> <div className="mx_RoomHeader_nametext" title={ this.props.room.name }>{ this.props.room.name }</div>
{ searchStatus } { searchStatus }
<div className="mx_RoomHeader_settingsButton" title="Settings"> <div className="mx_RoomHeader_settingsButton" title="Settings">
<TintableSvg src="img/settings.svg" width="12" height="12"/> <TintableSvg src="img/settings.svg" width="12" height="12"/>
</div> </div>
</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; var roomAvatar = null;
if (this.props.room) { if (this.props.room) {
roomAvatar = ( if (can_set_room_avatar) {
<RoomAvatar room={this.props.room} width={48} height={48} /> 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; var leave_button;
@ -149,9 +286,21 @@ module.exports = React.createClass({
</div>; </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 = header =
<div className="mx_RoomHeader_wrapper"> <div className="mx_RoomHeader_wrapper">
<div className="mx_RoomHeader_leftRow" onClick={this.props.onSettingsClick}> <div className="mx_RoomHeader_leftRow">
<div className="mx_RoomHeader_avatar"> <div className="mx_RoomHeader_avatar">
{ roomAvatar } { roomAvatar }
</div> </div>
@ -160,20 +309,14 @@ module.exports = React.createClass({
{ topic_el } { topic_el }
</div> </div>
</div> </div>
{cancel_button}
{save_button} {save_button}
<div className="mx_RoomHeader_rightRow"> {cancel_button}
{ forget_button } {right_row}
{ leave_button }
<div className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title="Search">
<TintableSvg src="img/search.svg" width="21" height="19"/>
</div>
</div>
</div> </div>
} }
return ( return (
<div className="mx_RoomHeader"> <div className={ "mx_RoomHeader " + (this.props.editing ? "mx_RoomHeader_editing" : "") }>
{ header } { header }
</div> </div>
); );

View File

@ -18,6 +18,7 @@ var React = require('react');
var MatrixClientPeg = require('../../../MatrixClientPeg'); var MatrixClientPeg = require('../../../MatrixClientPeg');
var Tinter = require('../../../Tinter'); var Tinter = require('../../../Tinter');
var sdk = require('../../../index'); var sdk = require('../../../index');
var Modal = require('../../../Modal');
var room_colors = [ var room_colors = [
// magic room default values courtesy of Ribot // magic room default values courtesy of Ribot
@ -38,6 +39,16 @@ module.exports = React.createClass({
propTypes: { propTypes: {
room: React.PropTypes.object.isRequired, 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() { getInitialState: function() {
@ -68,13 +79,35 @@ module.exports = React.createClass({
room_color_index = 0; 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 { return {
power_levels_changed: false, power_levels_changed: false,
color_scheme_changed: false, color_scheme_changed: false,
color_scheme_index: room_color_index, 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() { canGuestsJoin: function() {
return this.refs.guests_join.checked; return this.refs.guests_join.checked;
}, },
@ -84,7 +117,7 @@ module.exports = React.createClass({
}, },
getTopic: function() { getTopic: function() {
return this.refs.topic.value; return this.refs.topic ? this.refs.topic.value : "";
}, },
getJoinRules: function() { getJoinRules: function() {
@ -95,6 +128,10 @@ module.exports = React.createClass({
return this.refs.share_history.checked ? "shared" : "invited"; return this.refs.share_history.checked ? "shared" : "invited";
}, },
areNotificationsMuted: function() {
return this.refs.are_notifications_muted.checked;
},
getPowerLevels: function() { getPowerLevels: function() {
if (!this.state.power_levels_changed) return undefined; if (!this.state.power_levels_changed) return undefined;
@ -102,13 +139,13 @@ module.exports = React.createClass({
power_levels = power_levels.getContent(); power_levels = power_levels.getContent();
var new_power_levels = { var new_power_levels = {
ban: parseInt(this.refs.ban.value), ban: parseInt(this.refs.ban.getValue()),
kick: parseInt(this.refs.kick.value), kick: parseInt(this.refs.kick.getValue()),
redact: parseInt(this.refs.redact.value), redact: parseInt(this.refs.redact.getValue()),
invite: parseInt(this.refs.invite.value), invite: parseInt(this.refs.invite.getValue()),
events_default: parseInt(this.refs.events_default.value), events_default: parseInt(this.refs.events_default.getValue()),
state_default: parseInt(this.refs.state_default.value), state_default: parseInt(this.refs.state_default.getValue()),
users_default: parseInt(this.refs.users_default.value), users_default: parseInt(this.refs.users_default.getValue()),
users: power_levels.users, users: power_levels.users,
events: power_levels.events, events: power_levels.events,
}; };
@ -116,6 +153,112 @@ module.exports = React.createClass({
return new_power_levels; 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() { onPowerLevelsChanged: function() {
this.setState({ this.setState({
power_levels_changed: true power_levels_changed: true
@ -141,11 +284,100 @@ module.exports = React.createClass({
}); });
}, },
render: function() { onAliasChanged: function(domain, index, alias) {
var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar'); 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",
});
}
},
var topic = this.props.room.currentState.getStateEvents('m.room.topic', ''); onAliasDeleted: function(domain, index) {
if (topic) topic = topic.getContent().topic; // 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', ''); var join_rule = this.props.room.currentState.getStateEvents('m.room.join_rules', '');
if (join_rule) join_rule = join_rule.getContent().join_rule; if (join_rule) join_rule = join_rule.getContent().join_rule;
@ -159,7 +391,18 @@ module.exports = React.createClass({
guest_access = guest_access.getContent().guest_access; guest_access = guest_access.getContent().guest_access;
} }
var events_levels = power_levels.events || {}; var are_notifications_muted;
var roomPushRule = MatrixClientPeg.get().getRoomPushRule("global", this.props.room.roomId);
if (roomPushRule) {
if (0 <= roomPushRule.actions.indexOf("dont_notify")) {
are_notifications_muted = true;
}
}
var events_levels = (power_levels ? power_levels.events : {}) || {};
var user_id = MatrixClientPeg.get().credentials.userId;
if (power_levels) { if (power_levels) {
power_levels = power_levels.getContent(); power_levels = power_levels.getContent();
@ -178,8 +421,6 @@ module.exports = React.createClass({
var user_levels = power_levels.users || {}; var user_levels = power_levels.users || {};
var user_id = MatrixClientPeg.get().credentials.userId;
var current_user_level = user_levels[user_id]; var current_user_level = user_levels[user_id];
if (current_user_level == undefined) current_user_level = default_user_level; if (current_user_level == undefined) current_user_level = default_user_level;
@ -208,14 +449,127 @@ module.exports = React.createClass({
var can_change_levels = false; var can_change_levels = false;
} }
var room_avatar_level = parseInt(power_levels.state_default || 0); var state_default = (parseInt(power_levels ? power_levels.state_default : 0) || 0);
if (events_levels['m.room.avatar'] !== undefined) {
room_avatar_level = events_levels['m.room.avatar']; 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_avatar = current_user_level >= room_avatar_level; 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 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 = var room_colors_section =
<div> <div>
<h3>Room Colour</h3> <h3>Room Colour</h3>
@ -242,35 +596,30 @@ module.exports = React.createClass({
</div> </div>
</div>; </div>;
var change_avatar; var user_levels_section;
if (can_set_room_avatar) { if (user_levels.length) {
change_avatar = user_levels_section =
<div> <div>
<h3>Room Icon</h3> <div>
<ChangeAvatar room={this.props.room} /> Users with specific roles are:
</div>; </div>
} <div>
{Object.keys(user_levels).map(function(user, i) {
var banned = this.props.room.getMembersWithMembership("ban");
var events_levels_section;
if (events_levels.length) {
events_levels_section =
<div>
<h3>Event levels</h3>
<div className="mx_RoomSettings_eventLevels mx_RoomSettings_settings">
{Object.keys(events_levels).map(function(event_type, i) {
return ( return (
<div key={event_type}> <div className="mx_RoomSettings_userLevel" key={user}>
<label htmlFor={"mx_RoomSettings_event_"+i}>{event_type}</label> { user } is a
<input type="text" defaultValue={events_levels[event_type]} size="3" id={"mx_RoomSettings_event_"+i} disabled/> <PowerSelector value={ user_levels[user] } disabled={true}/>
</div> </div>
); );
})} })}
</div> </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; var banned_users_section;
if (banned.length) { if (banned.length) {
banned_users_section = banned_users_section =
@ -288,79 +637,120 @@ module.exports = React.createClass({
</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 ( return (
<div className="mx_RoomSettings"> <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="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="share_history" defaultChecked={history_visibility == "shared"}/> Share message history with new users</label> <br/>
<label> <label><input type="checkbox" ref="guests_read" defaultChecked={history_visibility === "world_readable"}/> Allow guests to read messages in this room</label> <br/>
<input type="checkbox" ref="guests_read" defaultChecked={history_visibility === "world_readable"}/> <label><input type="checkbox" ref="guests_join" defaultChecked={guest_access === "can_join"}/> Allow guests to join this room</label> <br/>
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> <label className="mx_RoomSettings_encrypt"><input type="checkbox" /> Encrypt room</label>
{ tags_section }
{ room_colors_section } { room_colors_section }
{ aliases_section }
<h3>Power levels</h3> <h3>Notifications</h3>
<div className="mx_RoomSettings_powerLevels mx_RoomSettings_settings"> <div className="mx_RoomSettings_settings">
<div> <label><input type="checkbox" ref="are_notifications_muted" defaultChecked={are_notifications_muted}/> Mute notifications for this room</label>
<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}/>
</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>
<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>
<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>
<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>
<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>
<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>
</div> </div>
<h3>User levels</h3> <h3>Permissions</h3>
<div className="mx_RoomSettings_userLevels mx_RoomSettings_settings"> <div className="mx_RoomSettings_powerLevels mx_RoomSettings_settings">
{Object.keys(user_levels).map(function(user, i) { <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 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 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 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 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 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 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>
{Object.keys(events_levels).map(function(event_type, i) {
return ( return (
<div key={user}> <div className="mx_RoomSettings_powerLevel" key={event_type}>
<label htmlFor={"mx_RoomSettings_user_"+i}>{user}</label> <span className="mx_RoomSettings_powerLevelKey">To send events of type <code>{ event_type }</code>, you must be a </span>
<input type="text" defaultValue={user_levels[user]} size="3" id={"mx_RoomSettings_user_"+i} disabled/> <PowerSelector value={ events_levels[event_type] } disabled={true} onChange={self.onPowerLevelsChanged}/>
</div> </div>
); );
})} })}
{ unfederatable_section }
</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> </div>
{ events_levels_section }
{ banned_users_section } { banned_users_section }
{ change_avatar }
<h3>Advanced</h3>
<div className="mx_RoomSettings_settings">
This room's internal ID is <code>{ this.props.room.roomId }</code>
</div>
</div> </div>
); );
} }

View File

@ -25,6 +25,8 @@ module.exports = React.createClass({
room: React.PropTypes.object, room: React.PropTypes.object,
// if false, you need to call changeAvatar.onFileSelected yourself. // if false, you need to call changeAvatar.onFileSelected yourself.
showUploadSection: React.PropTypes.bool, showUploadSection: React.PropTypes.bool,
width: React.PropTypes.number,
height: React.PropTypes.number,
className: React.PropTypes.string className: React.PropTypes.string
}, },
@ -37,7 +39,9 @@ module.exports = React.createClass({
getDefaultProps: function() { getDefaultProps: function() {
return { return {
showUploadSection: true, showUploadSection: true,
className: "mx_Dialog_content" // FIXME - shouldn't be this by default className: "",
width: 80,
height: 80,
}; };
}, },
@ -111,13 +115,14 @@ module.exports = React.createClass({
// Having just set an avatar we just display that since it will take a little // Having just set an avatar we just display that since it will take a little
// time to propagate through to the RoomAvatar. // time to propagate through to the RoomAvatar.
if (this.props.room && !this.avatarSet) { if (this.props.room && !this.avatarSet) {
avatarImg = <RoomAvatar room={this.props.room} width={240} height={240} resizeMethod='crop' />; avatarImg = <RoomAvatar room={this.props.room} width={ this.props.width } height={ this.props.height } resizeMethod='crop' />;
} else { } else {
var style = { var style = {
maxWidth: 240, width: this.props.width,
maxHeight: 240, height: this.props.height,
objectFit: 'cover', objectFit: 'cover',
}; };
// FIXME: surely we should be using MemberAvatar or UserAvatar or something here...
avatarImg = <img className="mx_RoomAvatar" src={this.state.avatarUrl} style={style} />; avatarImg = <img className="mx_RoomAvatar" src={this.state.avatarUrl} style={style} />;
} }

View File

@ -99,7 +99,9 @@ module.exports = React.createClass({
var EditableText = sdk.getComponent('elements.EditableText'); var EditableText = sdk.getComponent('elements.EditableText');
return ( return (
<EditableText ref="displayname_edit" initialValue={this.state.displayName} <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} /> onValueChanged={this.onValueChanged} />
); );
} }