From fc7707971eae173f4fb1ec627de98e9cb82665ae Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 26 Nov 2015 17:10:36 +0000 Subject: [PATCH 01/10] Move and merge Change Avatar|DisplayName|Password components --- .../views/settings}/ChangeAvatar.js | 57 +++++++- .../views/settings}/ChangeDisplayName.js | 36 ++++- .../views/settings/ChangePassword.js | 135 ++++++++++++++++++ src/controllers/molecules/ChangePassword.js | 76 ---------- 4 files changed, 222 insertions(+), 82 deletions(-) rename src/{controllers/molecules => components/views/settings}/ChangeAvatar.js (54%) rename src/{controllers/molecules => components/views/settings}/ChangeDisplayName.js (65%) create mode 100644 src/components/views/settings/ChangePassword.js delete mode 100644 src/controllers/molecules/ChangePassword.js diff --git a/src/controllers/molecules/ChangeAvatar.js b/src/components/views/settings/ChangeAvatar.js similarity index 54% rename from src/controllers/molecules/ChangeAvatar.js rename to src/components/views/settings/ChangeAvatar.js index 7e8f959ebf..2ae50a0cae 100644 --- a/src/controllers/molecules/ChangeAvatar.js +++ b/src/components/views/settings/ChangeAvatar.js @@ -15,9 +15,11 @@ limitations under the License. */ var React = require('react'); -var MatrixClientPeg = require("../../MatrixClientPeg"); +var MatrixClientPeg = require("../../../MatrixClientPeg"); +var sdk = require('../../../index'); -module.exports = { +module.exports = React.createClass({ + displayName: 'ChangeAvatar', propTypes: { initialAvatarUrl: React.PropTypes.string, room: React.PropTypes.object, @@ -77,4 +79,53 @@ module.exports = { self.onError(error); }); }, -} + + onFileSelected: function(ev) { + this.avatarSet = true; + this.setAvatarFromFile(ev.target.files[0]); + }, + + onError: function(error) { + this.setState({ + errorText: "Failed to upload profile picture!" + }); + }, + + render: function() { + var RoomAvatar = sdk.getComponent('avatars.RoomAvatar'); + var avatarImg; + // Having just set an avatar we just display that since it will take a little + // time to propagate through to the RoomAvatar. + if (this.props.room && !this.avatarSet) { + avatarImg = ; + } else { + var style = { + maxWidth: 320, + maxHeight: 240, + }; + avatarImg = ; + } + + switch (this.state.phase) { + case this.Phases.Display: + case this.Phases.Error: + return ( +
+
+ {avatarImg} +
+
+ Upload new: + + {this.state.errorText} +
+
+ ); + case this.Phases.Uploading: + var Loader = sdk.getComponent("elements.Spinner"); + return ( + + ); + } + } +}); diff --git a/src/controllers/molecules/ChangeDisplayName.js b/src/components/views/settings/ChangeDisplayName.js similarity index 65% rename from src/controllers/molecules/ChangeDisplayName.js rename to src/components/views/settings/ChangeDisplayName.js index afef82772c..4af413cfbe 100644 --- a/src/controllers/molecules/ChangeDisplayName.js +++ b/src/components/views/settings/ChangeDisplayName.js @@ -16,9 +16,11 @@ limitations under the License. 'use strict'; var React = require('react'); -var MatrixClientPeg = require("../../MatrixClientPeg"); +var sdk = require('../../../index'); +var MatrixClientPeg = require("../../../MatrixClientPeg"); -module.exports = { +module.exports = React.createClass({ + displayName: 'ChangeDisplayName', propTypes: { onFinished: React.PropTypes.func }, @@ -72,4 +74,32 @@ module.exports = { }); }); }, -} + + edit: function() { + this.refs.displayname_edit.edit() + }, + + onValueChanged: function(new_value, shouldSubmit) { + if (shouldSubmit) { + this.changeDisplayname(new_value); + } + }, + + render: function() { + if (this.state.busy) { + var Loader = sdk.getComponent("elements.Spinner"); + return ( + + ); + } else if (this.state.errorString) { + return ( +
{this.state.errorString}
+ ); + } else { + var EditableText = sdk.getComponent('elements.EditableText'); + return ( + + ); + } + } +}); diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js new file mode 100644 index 0000000000..a6666b7ed1 --- /dev/null +++ b/src/components/views/settings/ChangePassword.js @@ -0,0 +1,135 @@ +/* +Copyright 2015 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +'use strict'; + +var React = require('react'); +var MatrixClientPeg = require("../../../MatrixClientPeg"); + +module.exports = React.createClass({ + displayName: 'ChangePassword', + propTypes: { + onFinished: React.PropTypes.func, + }, + + Phases: { + Edit: "edit", + Uploading: "uploading", + Error: "error", + Success: "Success" + }, + + getDefaultProps: function() { + return { + onFinished: function() {}, + }; + }, + + getInitialState: function() { + return { + phase: this.Phases.Edit, + errorString: '' + } + }, + + changePassword: function(old_password, new_password) { + var cli = MatrixClientPeg.get(); + + var authDict = { + type: 'm.login.password', + user: cli.credentials.userId, + password: old_password + }; + + this.setState({ + phase: this.Phases.Uploading, + errorString: '', + }) + + var d = cli.setPassword(authDict, new_password); + + var self = this; + d.then(function() { + self.setState({ + phase: self.Phases.Success, + errorString: '', + }) + }, function(err) { + self.setState({ + phase: self.Phases.Error, + errorString: err.toString() + }) + }); + }, + + onClickChange: function() { + var old_password = this.refs.old_input.value; + var new_password = this.refs.new_input.value; + var confirm_password = this.refs.confirm_input.value; + if (new_password != confirm_password) { + this.setState({ + state: this.Phases.Error, + errorString: "Passwords don't match" + }); + } else if (new_password == '' || old_password == '') { + this.setState({ + state: this.Phases.Error, + errorString: "Passwords can't be empty" + }); + } else { + this.changePassword(old_password, new_password); + } + }, + + render: function() { + switch (this.state.phase) { + case this.Phases.Edit: + case this.Phases.Error: + return ( +
+
+
{this.state.errorString}
+
+
+
+
+
+ + +
+
+ ); + case this.Phases.Uploading: + var Loader = sdk.getComponent("elements.Spinner"); + return ( +
+ +
+ ); + case this.Phases.Success: + return ( +
+
+ Success! +
+
+ +
+
+ ) + } + } +}); diff --git a/src/controllers/molecules/ChangePassword.js b/src/controllers/molecules/ChangePassword.js deleted file mode 100644 index 637e133a79..0000000000 --- a/src/controllers/molecules/ChangePassword.js +++ /dev/null @@ -1,76 +0,0 @@ -/* -Copyright 2015 OpenMarket Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -'use strict'; - -var React = require('react'); -var MatrixClientPeg = require("../../MatrixClientPeg"); - -module.exports = { - propTypes: { - onFinished: React.PropTypes.func, - }, - - Phases: { - Edit: "edit", - Uploading: "uploading", - Error: "error", - Success: "Success" - }, - - getDefaultProps: function() { - return { - onFinished: function() {}, - }; - }, - - getInitialState: function() { - return { - phase: this.Phases.Edit, - errorString: '' - } - }, - - changePassword: function(old_password, new_password) { - var cli = MatrixClientPeg.get(); - - var authDict = { - type: 'm.login.password', - user: cli.credentials.userId, - password: old_password - }; - - this.setState({ - phase: this.Phases.Uploading, - errorString: '', - }) - - var d = cli.setPassword(authDict, new_password); - - var self = this; - d.then(function() { - self.setState({ - phase: self.Phases.Success, - errorString: '', - }) - }, function(err) { - self.setState({ - phase: self.Phases.Error, - errorString: err.toString() - }) - }); - }, -} From 75afc3a7dee0312a3484fbfd45327803cde4550a Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 26 Nov 2015 17:21:08 +0000 Subject: [PATCH 02/10] Move and merge ProgressBar --- .../views/elements}/ProgressBar.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) rename src/{controllers/molecules => components/views/elements}/ProgressBar.js (59%) diff --git a/src/controllers/molecules/ProgressBar.js b/src/components/views/elements/ProgressBar.js similarity index 59% rename from src/controllers/molecules/ProgressBar.js rename to src/components/views/elements/ProgressBar.js index c711650a25..bab6a701dd 100644 --- a/src/controllers/molecules/ProgressBar.js +++ b/src/components/views/elements/ProgressBar.js @@ -18,9 +18,21 @@ limitations under the License. var React = require('react'); -module.exports = { +module.exports = React.createClass({ + displayName: 'ProgressBar', propTypes: { value: React.PropTypes.number, max: React.PropTypes.number }, -}; + + render: function() { + // Would use an HTML5 progress tag but if that doesn't animate if you + // use the HTML attributes rather than styles + var progressStyle = { + width: ((this.props.value / this.props.max) * 100)+"%" + }; + return ( +
+ ); + } +}); \ No newline at end of file From 206c45e703de64f4d6f550600d53a0d3c1e04138 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 26 Nov 2015 17:31:10 +0000 Subject: [PATCH 03/10] Move and merge MessageComposer --- .../views/messages}/MessageComposer.js | 81 ++++++++++++++++--- 1 file changed, 72 insertions(+), 9 deletions(-) rename src/{controllers/molecules => components/views/messages}/MessageComposer.js (85%) diff --git a/src/controllers/molecules/MessageComposer.js b/src/components/views/messages/MessageComposer.js similarity index 85% rename from src/controllers/molecules/MessageComposer.js rename to src/components/views/messages/MessageComposer.js index 237c710395..869e9f7614 100644 --- a/src/controllers/molecules/MessageComposer.js +++ b/src/components/views/messages/MessageComposer.js @@ -13,7 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ - +var React = require("react"); var marked = require("marked"); marked.setOptions({ renderer: new marked.Renderer(), @@ -25,12 +25,12 @@ marked.setOptions({ smartLists: true, smartypants: false }); -var MatrixClientPeg = require("../../MatrixClientPeg"); -var SlashCommands = require("../../SlashCommands"); -var Modal = require("../../Modal"); -var sdk = require('../../index'); +var MatrixClientPeg = require("../../../MatrixClientPeg"); +var SlashCommands = require("../../../SlashCommands"); +var Modal = require("../../../Modal"); +var sdk = require('../../../index'); -var dis = require("../../dispatcher"); +var dis = require("../../../dispatcher"); var KeyCode = { ENTER: 13, BACKSPACE: 8, @@ -58,10 +58,11 @@ function mdownToHtml(mdown) { return html; } -module.exports = { - oldScrollHeight: 0, +module.exports = React.createClass({ + displayName: 'MessageComposer', componentWillMount: function() { + this.oldScrollHeight = 0; this.markdownEnabled = MARKDOWN_ENABLED; this.tabStruct = { completing: false, @@ -501,7 +502,69 @@ module.exports = { clearTimeout(this.typingTimeout); this.typingTimeout = null; } + }, + onInputClick: function(ev) { + this.refs.textarea.focus(); + }, + + onUploadClick: function(ev) { + this.refs.uploadInput.click(); + }, + + onUploadFileSelected: function(ev) { + var files = ev.target.files; + // MessageComposer shouldn't have to rely on it's parent passing in a callback to upload a file + if (files && files.length > 0) { + this.props.uploadFile(files[0]); + } + this.refs.uploadInput.value = null; + }, + + onCallClick: function(ev) { + dis.dispatch({ + action: 'place_call', + type: ev.shiftKey ? "screensharing" : "video", + room_id: this.props.room.roomId + }); + }, + + onVoiceCallClick: function(ev) { + dis.dispatch({ + action: 'place_call', + type: 'voice', + room_id: this.props.room.roomId + }); + }, + + render: function() { + var me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId); + var uploadInputStyle = {display: 'none'}; + var MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); + return ( +
+
+
+
+ +
+
+
+ cancel_button =
Cancel
+ save_button =
Save Changes
+ } else { + // + name = +
+
{ this.props.room.name }
+
+ +
+
+ if (topic) topic_el =
{ topic.getContent().topic }
; + } + + var roomAvatar = null; + if (this.props.room) { + roomAvatar = ( + + ); + } + + var zoom_button, video_button, voice_button; + if (activeCall) { + if (activeCall.type == "video") { + zoom_button = ( +
+ Fullscreen +
+ ); + } + video_button = +
+ Video call +
; + voice_button = +
+ VoIP call +
; + } + + header = +
+
+
+ { roomAvatar } +
+
+ { name } + { topic_el } +
+
+ {call_buttons} + {cancel_button} + {save_button} +
+ { video_button } + { voice_button } + { zoom_button } +
+ Search +
+
+
+ } + + return ( +
+ { header } +
+ ); + }, +}); diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js new file mode 100644 index 0000000000..eb9bfd90c8 --- /dev/null +++ b/src/components/views/rooms/RoomSettings.js @@ -0,0 +1,237 @@ +/* +Copyright 2015 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +var React = require('react'); +var MatrixClientPeg = require('../../../MatrixClientPeg'); +var sdk = require('../../../index'); + +module.exports = React.createClass({ + displayName: 'RoomSettings', + + propTypes: { + room: React.PropTypes.object.isRequired, + }, + + getInitialState: function() { + return { + power_levels_changed: false + }; + }, + + getTopic: function() { + return this.refs.topic.value; + }, + + getJoinRules: function() { + return this.refs.is_private.checked ? "invite" : "public"; + }, + + getHistoryVisibility: function() { + return this.refs.share_history.checked ? "shared" : "invited"; + }, + + getPowerLevels: function() { + if (!this.state.power_levels_changed) return undefined; + + var power_levels = this.props.room.currentState.getStateEvents('m.room.power_levels', ''); + power_levels = power_levels.getContent(); + + var new_power_levels = { + ban: parseInt(this.refs.ban.value), + kick: parseInt(this.refs.kick.value), + redact: parseInt(this.refs.redact.value), + invite: parseInt(this.refs.invite.value), + events_default: parseInt(this.refs.events_default.value), + state_default: parseInt(this.refs.state_default.value), + users_default: parseInt(this.refs.users_default.value), + users: power_levels.users, + events: power_levels.events, + }; + + return new_power_levels; + }, + + onPowerLevelsChanged: function() { + this.setState({ + power_levels_changed: true + }); + }, + + render: function() { + var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar'); + + var topic = this.props.room.currentState.getStateEvents('m.room.topic', ''); + if (topic) topic = topic.getContent().topic; + + var join_rule = this.props.room.currentState.getStateEvents('m.room.join_rules', ''); + if (join_rule) join_rule = join_rule.getContent().join_rule; + + var history_visibility = this.props.room.currentState.getStateEvents('m.room.history_visibility', ''); + if (history_visibility) history_visibility = history_visibility.getContent().history_visibility; + + var power_levels = this.props.room.currentState.getStateEvents('m.room.power_levels', ''); + + var events_levels = power_levels.events || {}; + + if (power_levels) { + power_levels = power_levels.getContent(); + + var ban_level = parseInt(power_levels.ban); + var kick_level = parseInt(power_levels.kick); + var redact_level = parseInt(power_levels.redact); + var invite_level = parseInt(power_levels.invite || 0); + var send_level = parseInt(power_levels.events_default || 0); + var state_level = parseInt(power_levels.state_default || 0); + var default_user_level = parseInt(power_levels.users_default || 0); + + if (power_levels.ban == undefined) ban_level = 50; + if (power_levels.kick == undefined) kick_level = 50; + if (power_levels.redact == undefined) redact_level = 50; + + var user_levels = power_levels.users || {}; + + var user_id = MatrixClientPeg.get().credentials.userId; + + var current_user_level = user_levels[user_id]; + if (current_user_level == undefined) current_user_level = default_user_level; + + var power_level_level = events_levels["m.room.power_levels"]; + if (power_level_level == undefined) { + power_level_level = state_level; + } + + var can_change_levels = current_user_level >= power_level_level; + } else { + var ban_level = 50; + var kick_level = 50; + var redact_level = 50; + var invite_level = 0; + var send_level = 0; + var state_level = 0; + var default_user_level = 0; + + var user_levels = []; + var events_levels = []; + + var current_user_level = 0; + + var power_level_level = 0; + + var can_change_levels = false; + } + + var room_avatar_level = parseInt(power_levels.state_default || 0); + if (events_levels['m.room.avatar'] !== undefined) { + room_avatar_level = events_levels['m.room.avatar']; + } + var can_set_room_avatar = current_user_level >= room_avatar_level; + + var change_avatar; + if (can_set_room_avatar) { + change_avatar =
+

Room Icon

+ +
; + } + + var banned = this.props.room.getMembersWithMembership("ban"); + + return ( +
+