diff --git a/res/css/_components.scss b/res/css/_components.scss index 076516b7af..c6c8cace27 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -138,6 +138,7 @@ @import "./views/settings/_Notifications.scss"; @import "./views/settings/_PhoneNumbers.scss"; @import "./views/settings/_ProfileSettings.scss"; +@import "./views/settings/tabs/_GeneralRoomSettingsTab.scss"; @import "./views/settings/tabs/_GeneralUserSettingsTab.scss"; @import "./views/settings/tabs/_HelpSettingsTab.scss"; @import "./views/settings/tabs/_PreferencesSettingsTab.scss"; diff --git a/res/css/views/elements/_Field.scss b/res/css/views/elements/_Field.scss index 128e7dbfde..d2877ca741 100644 --- a/res/css/views/elements/_Field.scss +++ b/res/css/views/elements/_Field.scss @@ -22,7 +22,8 @@ limitations under the License. } .mx_Field input, -.mx_Field select { +.mx_Field select, +.mx_Field textarea { font-weight: normal; font-family: $font-family; border-radius: 4px; @@ -32,17 +33,20 @@ limitations under the License. } .mx_Field input:focus, -.mx_Field select:focus { +.mx_Field select:focus, +.mx_Field textarea:focus { outline: 0; border-color: $input-focused-border-color; } -.mx_Field input::placeholder { +.mx_Field input::placeholder, +.mx_Field textarea::placeholder { transition: color 0.25s ease-in 0s; color: transparent; } -.mx_Field input:placeholder-shown:focus::placeholder { +.mx_Field input:placeholder-shown:focus::placeholder, +.mx_Field textarea:placeholder-shown:focus::placeholder { transition: color 0.25s ease-in 0.1s; color: $greyed-fg-color; } @@ -65,6 +69,8 @@ limitations under the License. .mx_Field input:focus + label, .mx_Field input:not(:placeholder-shown) + label, +.mx_Field textarea:focus + label, +.mx_Field textarea:not(:placeholder-shown) + label, .mx_Field select + label /* Always show a select's label on top to not collide with the value */ { transition: font-size 0.25s ease-out 0s, @@ -77,7 +83,8 @@ limitations under the License. } .mx_Field input:focus + label, -.mx_Field select:focus + label { +.mx_Field select:focus + label, +.mx_Field textarea:focus + label { color: $input-focused-border-color; } diff --git a/res/css/views/settings/_ProfileSettings.scss b/res/css/views/settings/_ProfileSettings.scss index 4bec429d55..9fe2ff4b89 100644 --- a/res/css/views/settings/_ProfileSettings.scss +++ b/res/css/views/settings/_ProfileSettings.scss @@ -22,10 +22,19 @@ limitations under the License. flex-grow: 1; } -.mx_ProfileSettings_controls .mx_Field #profileDisplayName { +.mx_ProfileSettings_controls .mx_Field #profileDisplayName, +.mx_ProfileSettings_controls .mx_Field #profileTopic { width: calc(100% - 20px); // subtract 10px padding on left and right } +.mx_ProfileSettings_controls .mx_Field #profileTopic { + height: 4em; +} + +.mx_ProfileSettings_controls .mx_Field:first-child { + margin-top: 0; +} + .mx_ProfileSettings_avatar { width: 88px; height: 88px; @@ -41,6 +50,10 @@ limitations under the License. border-radius: 4px; } +.mx_ProfileSettings_avatar .mx_ProfileSettings_avatarOverlay_disabled { + cursor: default; +} + .mx_ProfileSettings_avatar .mx_ProfileSettings_avatarPlaceholder { background-color: $settings-profile-placeholder-bg-color; } @@ -57,7 +70,7 @@ limitations under the License. font-size: 10px; } -.mx_ProfileSettings_avatar:hover .mx_ProfileSettings_avatarOverlay { +.mx_ProfileSettings_avatar:hover .mx_ProfileSettings_avatarOverlay:not(.mx_ProfileSettings_avatarOverlay_disabled) { display: inline-block; opacity: 0.5 !important; color: $settings-profile-overlay-fg-color !important; @@ -77,6 +90,11 @@ limitations under the License. margin-bottom: 8px; } +.mx_ProfileSettings_noAvatarText { + display: block; + margin: 34px auto auto; +} + .mx_ProfileSettings_avatarOverlayImgContainer { position: relative; width: 14px; diff --git a/res/css/views/settings/tabs/_GeneralRoomSettingsTab.scss b/res/css/views/settings/tabs/_GeneralRoomSettingsTab.scss new file mode 100644 index 0000000000..d1274973d5 --- /dev/null +++ b/res/css/views/settings/tabs/_GeneralRoomSettingsTab.scss @@ -0,0 +1,19 @@ +/* +Copyright 2019 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_GeneralRoomSettingsTab_profileSection { + margin-top: 10px; +} diff --git a/src/components/views/dialogs/RoomSettingsDialog.js b/src/components/views/dialogs/RoomSettingsDialog.js index ce834d564e..7336373e32 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.js +++ b/src/components/views/dialogs/RoomSettingsDialog.js @@ -20,6 +20,7 @@ import {Tab, TabbedView} from "../../structures/TabbedView"; import {_t, _td} from "../../../languageHandler"; import AccessibleButton from "../elements/AccessibleButton"; import dis from '../../../dispatcher'; +import GeneralRoomSettingsTab from "../settings/tabs/GeneralRoomSettingsTab"; // TODO: Ditch this whole component export class TempTab extends React.Component { @@ -37,8 +38,9 @@ export class TempTab extends React.Component { } } -export default class UserSettingsDialog extends React.Component { +export default class RoomSettingsDialog extends React.Component { static propTypes = { + roomId: PropTypes.string.isRequired, onFinished: PropTypes.func.isRequired, }; @@ -48,7 +50,7 @@ export default class UserSettingsDialog extends React.Component { tabs.push(new Tab( _td("General"), "mx_RoomSettingsDialog_settingsIcon", -
General Test
, + , )); tabs.push(new Tab( _td("Security & Privacy"), diff --git a/src/components/views/settings/RoomProfileSettings.js b/src/components/views/settings/RoomProfileSettings.js new file mode 100644 index 0000000000..4d925967b1 --- /dev/null +++ b/src/components/views/settings/RoomProfileSettings.js @@ -0,0 +1,198 @@ +/* +Copyright 2019 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import {_t} from "../../../languageHandler"; +import MatrixClientPeg from "../../../MatrixClientPeg"; +import Field from "../elements/Field"; +import AccessibleButton from "../elements/AccessibleButton"; +import classNames from 'classnames'; + +// TODO: Merge with ProfileSettings? +export default class RoomProfileSettings extends React.Component { + static propTypes = { + roomId: PropTypes.string.isRequired, + }; + + constructor(props) { + super(props); + + const client = MatrixClientPeg.get(); + const room = client.getRoom(props.roomId); + if (!room) throw new Error("Expected a room for ID: ", props.roomId); + let avatarUrl = room.avatarUrl; + if (avatarUrl) avatarUrl = client.mxcUrlToHttp(avatarUrl, 96, 96, 'crop', false); + const topicEvent = room.currentState.getStateEvents("m.room.topic", ""); + const topic = topicEvent && topicEvent.getContent() ? topicEvent.getContent()['topic'] : null; + this.state = { + originalDisplayName: room.name, + displayName: room.name, + originalAvatarUrl: avatarUrl, + avatarUrl: avatarUrl, + avatarFile: null, + originalTopic: topic, + topic: topic, + enableProfileSave: false, + canSetName: room.currentState.maySendStateEvent('m.room.name', client.getUserId()), + canSetTopic: room.currentState.maySendStateEvent('m.room.topic', client.getUserId()), + canSetAvatar: room.currentState.maySendStateEvent('m.room.avatar', client.getUserId()), + }; + } + + _uploadAvatar = (e) => { + e.stopPropagation(); + e.preventDefault(); + + this.refs.avatarUpload.click(); + }; + + _saveProfile = async (e) => { + e.stopPropagation(); + e.preventDefault(); + + if (!this.state.enableProfileSave) return; + this.setState({enableProfileSave: false}); + + const client = MatrixClientPeg.get(); + const newState = {}; + + // TODO: What do we do about errors? + + if (this.state.originalDisplayName !== this.state.displayName) { + await client.setRoomName(this.props.roomId, this.state.displayName); + newState.originalDisplayName = this.state.displayName; + } + + if (this.state.avatarFile) { + const uri = await client.uploadContent(this.state.avatarFile); + await client.sendStateEvent(this.props.roomId, 'm.room.avatar', {url: uri}, ''); + newState.avatarUrl = client.mxcUrlToHttp(uri, 96, 96, 'crop', false); + newState.originalAvatarUrl = newState.avatarUrl; + newState.avatarFile = null; + } + + if (this.state.originalTopic !== this.state.topic) { + await client.setRoomTopic(this.props.roomId, this.state.topic); + newState.originalTopic = this.state.topic; + } + + newState.enableProfileSave = true; + this.setState(newState); + }; + + _onDisplayNameChanged = (e) => { + this.setState({ + displayName: e.target.value, + enableProfileSave: true, + }); + }; + + _onTopicChanged = (e) => { + this.setState({ + topic: e.target.value, + enableProfileSave: true, + }); + }; + + _onAvatarChanged = (e) => { + if (!e.target.files || !e.target.files.length) { + this.setState({ + avatarUrl: this.state.originalAvatarUrl, + avatarFile: null, + enableProfileSave: false, + }); + return; + } + + const file = e.target.files[0]; + const reader = new FileReader(); + reader.onload = (ev) => { + this.setState({ + avatarUrl: ev.target.result, + avatarFile: file, + enableProfileSave: true, + }); + }; + reader.readAsDataURL(file); + }; + + render() { + // TODO: Why is rendering a box with an overlay so complicated? Can the DOM be reduced? + + let showOverlayAnyways = true; + let avatarElement =
; + if (this.state.avatarUrl) { + showOverlayAnyways = false; + avatarElement = {_t("Room; + } + + const avatarOverlayClasses = classNames({ + "mx_ProfileSettings_avatarOverlay": true, + "mx_ProfileSettings_avatarOverlay_show": showOverlayAnyways, + }); + let avatarHoverElement = ( +
+ {_t("Upload room avatar")} +
+
+
+
+ ); + if (!this.state.canSetAvatar) { + if (!showOverlayAnyways) { + avatarHoverElement = null; + } else { + const disabledOverlayClasses = classNames({ + "mx_ProfileSettings_avatarOverlay": true, + "mx_ProfileSettings_avatarOverlay_show": true, + "mx_ProfileSettings_avatarOverlay_disabled": true, + }); + avatarHoverElement = ( +
+ {_t("No room avatar")} +
+ ); + } + } + + return ( +
+ +
+
+ + +
+
+ {avatarElement} + {avatarHoverElement} +
+
+ + {_t("Save")} + +
+ ); + } +} diff --git a/src/components/views/settings/tabs/GeneralRoomSettingsTab.js b/src/components/views/settings/tabs/GeneralRoomSettingsTab.js new file mode 100644 index 0000000000..c78decd2cb --- /dev/null +++ b/src/components/views/settings/tabs/GeneralRoomSettingsTab.js @@ -0,0 +1,37 @@ +/* +Copyright 2019 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import {_t} from "../../../../languageHandler"; +import RoomProfileSettings from "../RoomProfileSettings"; + +export default class GeneralRoomSettingsTab extends React.Component { + static propTypes = { + roomId: PropTypes.string.isRequired, + }; + + render() { + return ( +
+
{_t("General")}
+
+ +
+
+ ); + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f04c62cbbb..b2abba403f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -430,6 +430,12 @@ "Upload profile picture": "Upload profile picture", "Display Name": "Display Name", "Save": "Save", + "Room avatar": "Room avatar", + "Upload room avatar": "Upload room avatar", + "No room avatar": "No room avatar", + "Room Name": "Room Name", + "Room Topic": "Room Topic", + "General": "General", "Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?", "Success": "Success", "Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them": "Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them", @@ -448,7 +454,6 @@ "Account management": "Account management", "Deactivating your account is a permanent action - be careful!": "Deactivating your account is a permanent action - be careful!", "Close Account": "Close Account", - "General": "General", "Legal": "Legal", "For help with using Riot, click here.": "For help with using Riot, click here.", "For help with using Riot, click here or start a chat with our bot using the button below.": "For help with using Riot, click here or start a chat with our bot using the button below.", diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js index 036a7c04fc..ba78e7687f 100644 --- a/src/stores/RoomViewStore.js +++ b/src/stores/RoomViewStore.js @@ -122,7 +122,9 @@ class RoomViewStore extends Store { case 'open_room_settings': if (SettingsStore.isFeatureEnabled("feature_tabbed_settings")) { const RoomSettingsDialog = sdk.getComponent("dialogs.RoomSettingsDialog"); - Modal.createTrackedDialog('Room settings', '', RoomSettingsDialog, {}, 'mx_SettingsDialog'); + Modal.createTrackedDialog('Room settings', '', RoomSettingsDialog, { + roomId: this._state.roomId, + }, 'mx_SettingsDialog'); } else { this._setState({ isEditingSettings: true,