mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-16 13:14:58 +08:00
Early tab structure and profile changes (name/avatar/topic)
This commit is contained in:
parent
b7d0dd4408
commit
243feb9b13
@ -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";
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
19
res/css/views/settings/tabs/_GeneralRoomSettingsTab.scss
Normal file
19
res/css/views/settings/tabs/_GeneralRoomSettingsTab.scss
Normal 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;
|
||||
}
|
@ -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"),
|
||||
|
198
src/components/views/settings/RoomProfileSettings.js
Normal file
198
src/components/views/settings/RoomProfileSettings.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
37
src/components/views/settings/tabs/GeneralRoomSettingsTab.js
Normal file
37
src/components/views/settings/tabs/GeneralRoomSettingsTab.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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.",
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user