Early tab structure and profile changes (name/avatar/topic)

This commit is contained in:
Travis Ralston 2019-01-25 20:38:35 -07:00
parent b7d0dd4408
commit 243feb9b13
9 changed files with 300 additions and 11 deletions

View file

@ -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";

View file

@ -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;
}

View file

@ -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;

View file

@ -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;
}

View file

@ -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",
<div>General Test</div>,
<GeneralRoomSettingsTab roomId={this.props.roomId}/>,
));
tabs.push(new Tab(
_td("Security & Privacy"),

View file

@ -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 = <div className="mx_ProfileSettings_avatarPlaceholder" />;
if (this.state.avatarUrl) {
showOverlayAnyways = false;
avatarElement = <img src={this.state.avatarUrl}
alt={_t("Room avatar")} />;
}
const avatarOverlayClasses = classNames({
"mx_ProfileSettings_avatarOverlay": true,
"mx_ProfileSettings_avatarOverlay_show": showOverlayAnyways,
});
let avatarHoverElement = (
<div className={avatarOverlayClasses} onClick={this._uploadAvatar}>
<span className="mx_ProfileSettings_avatarOverlayText">{_t("Upload room avatar")}</span>
<div className="mx_ProfileSettings_avatarOverlayImgContainer">
<div className="mx_ProfileSettings_avatarOverlayImg" />
</div>
</div>
);
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 = (
<div className={disabledOverlayClasses}>
<span className="mx_ProfileSettings_noAvatarText">{_t("No room avatar")}</span>
</div>
);
}
}
return (
<form onSubmit={this._saveProfile} autoComplete={false} noValidate={true}>
<input type="file" ref="avatarUpload" className="mx_ProfileSettings_avatarUpload"
onChange={this._onAvatarChanged} accept="image/*" />
<div className="mx_ProfileSettings_profile">
<div className="mx_ProfileSettings_controls">
<Field id="profileDisplayName" label={_t("Room Name")}
type="text" value={this.state.displayName} autoComplete="off"
onChange={this._onDisplayNameChanged} disabled={!this.state.canSetName} />
<Field id="profileTopic" label={_t("Room Topic")} disabled={!this.state.canSetTopic}
type="text" value={this.state.topic} autoComplete="off"
onChange={this._onTopicChanged} element="textarea" />
</div>
<div className="mx_ProfileSettings_avatar">
{avatarElement}
{avatarHoverElement}
</div>
</div>
<AccessibleButton onClick={this._saveProfile} kind="primary"
disabled={!this.state.enableProfileSave}>
{_t("Save")}
</AccessibleButton>
</form>
);
}
}

View file

@ -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 (
<div className="mx_SettingsTab mx_GeneralRoomSettingsTab">
<div className="mx_SettingsTab_heading">{_t("General")}</div>
<div className='mx_SettingsTab_section mx_GeneralRoomSettingsTab_profileSection'>
<RoomProfileSettings roomId={this.props.roomId} />
</div>
</div>
);
}
}

View file

@ -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 <a>here</a>.": "For help with using Riot, click <a>here</a>.",
"For help with using Riot, click <a>here</a> or start a chat with our bot using the button below.": "For help with using Riot, click <a>here</a> or start a chat with our bot using the button below.",

View file

@ -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,