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 = ;
+ }
+
+ 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 (
+
+ );
+ }
+}
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,