Merge branch 'experimental' into bwindels/redesign-smallfixes

This commit is contained in:
Bruno Windels 2019-01-29 18:30:08 +01:00
commit 15a74cac15
19 changed files with 656 additions and 258 deletions

View file

@ -139,7 +139,8 @@
@import "./views/settings/_Notifications.scss";
@import "./views/settings/_PhoneNumbers.scss";
@import "./views/settings/_ProfileSettings.scss";
@import "./views/settings/tabs/_GeneralSettingsTab.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";
@import "./views/settings/tabs/_SecuritySettingsTab.scss";

View file

@ -16,10 +16,9 @@ limitations under the License.
.mx_AuthPage {
width: 100%;
height: 100%;
min-height: 100%;
display: flex;
flex-direction: column;
overflow: auto;
background-color: $authpage-bg-color;
}

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,23 @@
/*
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;
}
.mx_GeneralRoomSettingsTab .mx_AliasSettings .mx_Field select {
width: 100%;
}

View file

@ -14,33 +14,33 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_GeneralSettingsTab_changePassword,
.mx_GeneralSettingsTab_themeSection {
.mx_GeneralUserSettingsTab_changePassword,
.mx_GeneralUserSettingsTab_themeSection {
display: block;
}
.mx_GeneralSettingsTab_changePassword .mx_Field,
.mx_GeneralSettingsTab_themeSection .mx_Field {
.mx_GeneralUserSettingsTab_changePassword .mx_Field,
.mx_GeneralUserSettingsTab_themeSection .mx_Field {
display: block;
margin-right: 100px; // Align with the other fields on the page
}
.mx_GeneralSettingsTab_changePassword .mx_Field input {
.mx_GeneralUserSettingsTab_changePassword .mx_Field input {
display: block;
width: calc(100% - 20px); // subtract 10px padding on left and right
}
.mx_GeneralSettingsTab_changePassword .mx_Field:first-child {
.mx_GeneralUserSettingsTab_changePassword .mx_Field:first-child {
margin-top: 0;
}
.mx_GeneralSettingsTab_themeSection .mx_Field select {
.mx_GeneralUserSettingsTab_themeSection .mx_Field select {
display: block;
width: 100%;
}
.mx_GeneralSettingsTab_accountSection > .mx_EmailAddresses,
.mx_GeneralSettingsTab_accountSection > .mx_PhoneNumbers,
.mx_GeneralSettingsTab_languageInput {
.mx_GeneralUserSettingsTab_accountSection > .mx_EmailAddresses,
.mx_GeneralUserSettingsTab_accountSection > .mx_PhoneNumbers,
.mx_GeneralUserSettingsTab_languageInput {
margin-right: 100px; // Align with the other fields on the page
}

View file

@ -1054,6 +1054,7 @@ export default React.createClass({
modal.close();
if (this.state.currentRoomId === roomId) {
dis.dispatch({action: 'view_next_room'});
dis.dispatch({action: 'close_room_settings'});
}
}, (err) => {
modal.close();

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,18 +38,32 @@ 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,
};
componentWillMount(): void {
this.dispatcherRef = dis.register(this._onAction);
}
componentWillUnmount(): void {
dis.unregister(this.dispatcherRef);
}
_onAction = (payload) => {
if (payload.action !== 'close_room_settings') return;
this.props.onFinished();
};
_getTabs() {
const tabs = [];
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

@ -19,7 +19,7 @@ import PropTypes from 'prop-types';
import {Tab, TabbedView} from "../../structures/TabbedView";
import {_t, _td} from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton";
import GeneralSettingsTab from "../settings/tabs/GeneralSettingsTab";
import GeneralUserSettingsTab from "../settings/tabs/GeneralUserSettingsTab";
import dis from '../../../dispatcher';
import SettingsStore from "../../../settings/SettingsStore";
import LabsSettingsTab from "../settings/tabs/LabsSettingsTab";
@ -57,7 +57,7 @@ export default class UserSettingsDialog extends React.Component {
tabs.push(new Tab(
_td("General"),
"mx_UserSettingsDialog_settingsIcon",
<GeneralSettingsTab />,
<GeneralUserSettingsTab />,
));
tabs.push(new Tab(
_td("Flair"),

View file

@ -76,6 +76,7 @@ const EditableItem = React.createClass({
},
});
// TODO: Make this use the new Field element
module.exports = React.createClass({
displayName: 'EditableItemList',

View file

@ -22,6 +22,7 @@ const ObjectUtils = require("../../../ObjectUtils");
const MatrixClientPeg = require('../../../MatrixClientPeg');
const sdk = require("../../../index");
import { _t } from '../../../languageHandler';
import Field from "../elements/Field";
const Modal = require("../../../Modal");
module.exports = React.createClass({
@ -222,7 +223,8 @@ module.exports = React.createClass({
let found = false;
const canonicalValue = this.state.canonicalAlias || "";
canonical_alias_section = (
<select onChange={this.onCanonicalAliasChange} value={canonicalValue}>
<Field onChange={this.onCanonicalAliasChange} value={canonicalValue}
element='select' id='canonicalAlias' label={_t('Main address')}>
<option value="" key="unset">{ _t('not specified') }</option>
{
Object.keys(self.state.domainToAliases).map((domain, i) => {
@ -242,7 +244,7 @@ module.exports = React.createClass({
{ this.state.canonicalAlias }
</option>
}
</select>
</Field>
);
} else {
canonical_alias_section = (
@ -277,11 +279,8 @@ module.exports = React.createClass({
}
return (
<div>
<h3>{ _t('Addresses') }</h3>
<div className="mx_RoomSettings_aliasLabel">
{ _t('The main address for this room is') }: { canonical_alias_section }
</div>
<div className='mx_AliasSettings'>
{canonical_alias_section}
<EditableItemList
className={"mx_RoomSettings_localAliases"}
items={this.state.domainToAliases[localDomain] || []}

View file

@ -119,7 +119,6 @@ module.exports = React.createClass({
const localDomain = this.context.matrixClient.getDomain();
const EditableItemList = sdk.getComponent('elements.EditableItemList');
return <div>
<h3>{ _t('Flair') }</h3>
<EditableItemList
items={this.state.newGroupsList}
className={"mx_RelatedGroupSettings"}

View file

@ -0,0 +1,199 @@
/*
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);
const avatarEvent = room.currentState.getStateEvents("m.room.avatar", "");
let avatarUrl = avatarEvent && avatarEvent.getContent() ? avatarEvent.getContent()["url"] : null;
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'] : '';
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

@ -1,7 +1,7 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Travis Ralston
Copyright 2018 New Vector Ltd
Copyright 2018-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.
@ -16,12 +16,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {MatrixClient} from "matrix-js-sdk";
const React = require('react');
import PropTypes from 'prop-types';
const sdk = require("../../../index");
import { _t, _td } from '../../../languageHandler';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import dis from "../../../dispatcher";
import MatrixClientPeg from "../../../MatrixClientPeg";
module.exports = React.createClass({
@ -31,21 +32,16 @@ module.exports = React.createClass({
room: PropTypes.object,
},
contextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
},
saveSettings: function() {
const promises = [];
if (this.refs.urlPreviewsRoom) promises.push(this.refs.urlPreviewsRoom.save());
if (this.refs.urlPreviewsSelf) promises.push(this.refs.urlPreviewsSelf.save());
return promises;
_onClickUserSettings: (e) => {
e.preventDefault();
e.stopPropagation();
dis.dispatch({action: 'view_user_settings'});
},
render: function() {
const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
const roomId = this.props.room.roomId;
const isEncrypted = this.context.matrixClient.isRoomEncrypted(roomId);
const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(roomId);
let previewsForAccount = null;
let previewsForRoom = null;
@ -56,13 +52,13 @@ module.exports = React.createClass({
if (accountEnabled) {
previewsForAccount = (
_t("You have <a>enabled</a> URL previews by default.", {}, {
'a': (sub)=><a href="#/settings">{ sub }</a>,
'a': (sub)=><a onClick={this._onClickUserSettings} href=''>{ sub }</a>,
})
);
} else if (accountEnabled) {
previewsForAccount = (
_t("You have <a>disabled</a> URL previews by default.", {}, {
'a': (sub)=><a href="#/settings">{ sub }</a>,
'a': (sub)=><a onClick={this._onClickUserSettings} href=''>{ sub }</a>,
})
);
}
@ -73,9 +69,7 @@ module.exports = React.createClass({
<SettingsFlag name="urlPreviewsEnabled"
level={SettingLevel.ROOM}
roomId={roomId}
isExplicit={true}
manualSave={true}
ref="urlPreviewsRoom" />
isExplicit={true} />
</label>
);
} else {
@ -96,20 +90,16 @@ module.exports = React.createClass({
const previewsForRoomAccount = ( // in an e2ee room we use a special key to enforce per-room opt-in
<SettingsFlag name={isEncrypted ? 'urlPreviewsEnabled_e2ee' : 'urlPreviewsEnabled'}
level={SettingLevel.ROOM_ACCOUNT}
roomId={roomId}
manualSave={true}
ref="urlPreviewsSelf"
/>
roomId={roomId} />
);
return (
<div className="mx_RoomSettings_toggles">
<h3>{ _t("URL Previews") }</h3>
<div>
<div>
<div className='mx_SettingsTab_subsectionText'>
{ _t('When someone puts a URL in their message, a URL preview can be shown to give more ' +
'information about that link such as the title, description, and an image from the website.') }
</div>
<div>
<div className='mx_SettingsTab_subsectionText'>
{ previewsForAccount }
</div>
{ previewsForRoom }

View file

@ -0,0 +1,118 @@
/*
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 "../../room_settings/RoomProfileSettings";
import MatrixClientPeg from "../../../../MatrixClientPeg";
import sdk from "../../../../index";
import AccessibleButton from "../../elements/AccessibleButton";
import {MatrixClient} from "matrix-js-sdk";
import dis from "../../../../dispatcher";
export default class GeneralRoomSettingsTab extends React.Component {
static childContextTypes = {
matrixClient: PropTypes.instanceOf(MatrixClient),
};
static propTypes = {
roomId: PropTypes.string.isRequired,
};
getChildContext() {
return {
matrixClient: MatrixClientPeg.get(),
};
}
_saveAliases = (e) => {
// TODO: Live modification?
if (!this.refs.aliasSettings) return;
this.refs.aliasSettings.saveSettings();
};
_saveGroups = (e) => {
// TODO: Live modification?
if (!this.refs.flairSettings) return;
this.refs.flairSettings.saveSettings();
};
_onLeaveClick = () => {
dis.dispatch({
action: 'leave_room',
room_id: this.props.roomId,
});
};
render() {
const AliasSettings = sdk.getComponent("room_settings.AliasSettings");
const RelatedGroupSettings = sdk.getComponent("room_settings.RelatedGroupSettings");
const UrlPreviewSettings = sdk.getComponent("room_settings.UrlPreviewSettings");
const client = MatrixClientPeg.get();
const room = client.getRoom(this.props.roomId);
const canSetAliases = true; // Previously, we arbitrarily only allowed admins to do this
const canSetCanonical = room.currentState.mayClientSendStateEvent("m.room.canonical_alias", client);
const canonicalAliasEv = room.currentState.getStateEvents("m.room.canonical_alias", '');
const aliasEvents = room.currentState.getStateEvents("m.room.aliases");
const canChangeGroups = room.currentState.mayClientSendStateEvent("m.room.related_groups", client);
const groupsEvent = room.currentState.getStateEvents("m.room.related_groups", "");
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>
<span className='mx_SettingsTab_subheading'>{_t("Room Addresses")}</span>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<AliasSettings ref="aliasSettings" roomId={this.props.roomId}
canSetCanonicalAlias={canSetCanonical} canSetAliases={canSetAliases}
canonicalAliasEvent={canonicalAliasEv} aliasEvents={aliasEvents} />
<AccessibleButton onClick={this._saveAliases} kind='primary'>
{_t("Save")}
</AccessibleButton>
</div>
<span className='mx_SettingsTab_subheading'>{_t("Flair")}</span>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<RelatedGroupSettings ref="flairSettings" roomId={room.roomId}
canSetRelatedGroups={canChangeGroups}
relatedGroupsEvent={groupsEvent} />
<AccessibleButton onClick={this._saveGroups} kind='primary'>
{_t("Save")}
</AccessibleButton>
</div>
<span className='mx_SettingsTab_subheading'>{_t("URL Previews")}</span>
<div className='mx_SettingsTab_section'>
<UrlPreviewSettings room={room} />
</div>
<span className='mx_SettingsTab_subheading'>{_t("Leave room")}</span>
<div className='mx_SettingsTab_section'>
<AccessibleButton kind='danger' onClick={this._onLeaveClick}>
{ _t('Leave room') }
</AccessibleButton>
</div>
</div>
);
}
}

View file

@ -16,6 +16,11 @@ limitations under the License.
import React from 'react';
import {_t} from "../../../../languageHandler";
import MatrixClientPeg from "../../../../MatrixClientPeg";
import GroupUserSettings from "../../groups/GroupUserSettings";
import PropTypes from "prop-types";
import {MatrixClient} from "matrix-js-sdk";
import { DragDropContext } from 'react-beautiful-dnd';
import ProfileSettings from "../ProfileSettings";
import EmailAddresses from "../EmailAddresses";
import PhoneNumbers from "../PhoneNumbers";
@ -31,7 +36,11 @@ const sdk = require('../../../../index');
const Modal = require("../../../../Modal");
const dis = require("../../../../dispatcher");
export default class GeneralSettingsTab extends React.Component {
export default class GeneralUserSettingsTab extends React.Component {
static childContextTypes = {
matrixClient: PropTypes.instanceOf(MatrixClient),
};
constructor() {
super();
@ -41,6 +50,12 @@ export default class GeneralSettingsTab extends React.Component {
};
}
getChildContext() {
return {
matrixClient: MatrixClientPeg.get(),
};
}
_onLanguageChange = (newLanguage) => {
if (this.state.language === newLanguage) return;
@ -95,6 +110,11 @@ export default class GeneralSettingsTab extends React.Component {
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Profile")}</span>
<ProfileSettings />
<span className="mx_SettingsTab_subheading">{_t("Flair")}</span>
<DragDropContext>
<GroupUserSettings />
</DragDropContext>
</div>
);
}
@ -103,7 +123,7 @@ export default class GeneralSettingsTab extends React.Component {
const ChangePassword = sdk.getComponent("views.settings.ChangePassword");
const passwordChangeForm = (
<ChangePassword
className="mx_GeneralSettingsTab_changePassword"
className="mx_GeneralUserSettingsTab_changePassword"
rowClassName=""
buttonKind="primary"
onError={this._onPasswordChangeError}
@ -111,7 +131,7 @@ export default class GeneralSettingsTab extends React.Component {
);
return (
<div className="mx_SettingsTab_section mx_GeneralSettingsTab_accountSection">
<div className="mx_SettingsTab_section mx_GeneralUserSettingsTab_accountSection">
<span className="mx_SettingsTab_subheading">{_t("Account")}</span>
<p className="mx_SettingsTab_subsectionText">
{_t("Set a new account password...")}
@ -132,7 +152,7 @@ export default class GeneralSettingsTab extends React.Component {
return (
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Language and region")}</span>
<LanguageDropdown className="mx_GeneralSettingsTab_languageInput"
<LanguageDropdown className="mx_GeneralUserSettingsTab_languageInput"
onOptionChange={this._onLanguageChange} value={this.state.language} />
</div>
);
@ -142,7 +162,7 @@ export default class GeneralSettingsTab extends React.Component {
// TODO: Re-enable theme selection once the themes actually work
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
return (
<div className="mx_SettingsTab_section mx_GeneralSettingsTab_themeSection">
<div className="mx_SettingsTab_section mx_GeneralUserSettingsTab_themeSection">
<span className="mx_SettingsTab_subheading">{_t("Theme")}</span>
<Field id="theme" label={_t("Theme")} element="select" disabled={true}
value={this.state.theme} onChange={this._onThemeChange}>

View file

@ -431,6 +431,9 @@
"Display Name": "Display Name",
"Save": "Save",
"Flair": "Flair",
"General": "General",
"Room Addresses": "Room Addresses",
"URL Previews": "URL Previews",
"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 +451,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.",
@ -760,11 +762,10 @@
"'%(alias)s' is not a valid format for an alias": "'%(alias)s' is not a valid format for an alias",
"Invalid address format": "Invalid address format",
"'%(alias)s' is not a valid format for an address": "'%(alias)s' is not a valid format for an address",
"Main address": "Main address",
"not specified": "not specified",
"not set": "not set",
"Remote addresses for this room:": "Remote addresses for this room:",
"Addresses": "Addresses",
"The main address for this room is": "The main address for this room is",
"Local addresses for this room:": "Local addresses for this room:",
"This room has no local addresses": "This room has no local addresses",
"New address (e.g. #foo:%(localDomain)s)": "New address (e.g. #foo:%(localDomain)s)",
@ -773,12 +774,16 @@
"Showing flair for these communities:": "Showing flair for these communities:",
"This room is not showing flair for any communities": "This room is not showing flair for any communities",
"New community ID (e.g. +foo:%(localDomain)s)": "New community ID (e.g. +foo:%(localDomain)s)",
"Room avatar": "Room avatar",
"Upload room avatar": "Upload room avatar",
"No room avatar": "No room avatar",
"Room Name": "Room Name",
"Room Topic": "Room Topic",
"You have <a>enabled</a> URL previews by default.": "You have <a>enabled</a> URL previews by default.",
"You have <a>disabled</a> URL previews by default.": "You have <a>disabled</a> URL previews by default.",
"URL previews are enabled by default for participants in this room.": "URL previews are enabled by default for participants in this room.",
"URL previews are disabled by default for participants in this room.": "URL previews are disabled by default for participants in this room.",
"In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.",
"URL Previews": "URL Previews",
"When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.",
"Members": "Members",
"Files": "Files",

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,

View file

@ -1,191 +1,192 @@
import React from 'react';
import ReactDOM from 'react-dom';
import expect from 'expect';
import jest from 'jest-mock';
import Promise from 'bluebird';
import * as testUtils from '../../../test-utils';
import sdk from 'matrix-react-sdk';
const WrappedRoomSettings = testUtils.wrapInMatrixClientContext(sdk.getComponent('views.rooms.RoomSettings'));
import MatrixClientPeg from '../../../../src/MatrixClientPeg';
import SettingsStore from '../../../../src/settings/SettingsStore';
describe('RoomSettings', () => {
let parentDiv = null;
let sandbox = null;
let client = null;
let roomSettings = null;
const room = testUtils.mkStubRoom('!DdJkzRliezrwpNebLk:matrix.org');
function expectSentStateEvent(roomId, eventType, expectedEventContent) {
let found = false;
for (const call of client.sendStateEvent.mock.calls) {
const [
actualRoomId,
actualEventType,
actualEventContent,
] = call.slice(0, 3);
if (roomId === actualRoomId && actualEventType === eventType) {
expect(actualEventContent).toEqual(expectedEventContent);
found = true;
break;
}
}
expect(found).toBe(true);
}
beforeEach(function(done) {
testUtils.beforeEach(this);
sandbox = testUtils.stubClient();
client = MatrixClientPeg.get();
client.credentials = {userId: '@me:domain.com'};
client.setRoomName = jest.fn().mockReturnValue(Promise.resolve());
client.setRoomTopic = jest.fn().mockReturnValue(Promise.resolve());
client.setRoomDirectoryVisibility = jest.fn().mockReturnValue(Promise.resolve());
// Covers any room state event (e.g. name, avatar, topic)
client.sendStateEvent = jest.fn().mockReturnValue(Promise.resolve());
// Covers room tagging
client.setRoomTag = jest.fn().mockReturnValue(Promise.resolve());
client.deleteRoomTag = jest.fn().mockReturnValue(Promise.resolve());
// Covers any setting in the SettingsStore
// (including local client settings not stored via matrix)
SettingsStore.setValue = jest.fn().mockReturnValue(Promise.resolve());
parentDiv = document.createElement('div');
document.body.appendChild(parentDiv);
const gatherWrappedRef = (r) => {roomSettings = r;};
// get use wrappedRef because we're using wrapInMatrixClientContext
ReactDOM.render(
<WrappedRoomSettings
wrappedRef={gatherWrappedRef}
room={room}
/>,
parentDiv,
done,
);
});
afterEach((done) => {
if (parentDiv) {
ReactDOM.unmountComponentAtNode(parentDiv);
parentDiv.remove();
parentDiv = null;
}
sandbox.restore();
done();
});
it('should not set when no setting is changed', (done) => {
roomSettings.save().then(() => {
expect(client.sendStateEvent).not.toHaveBeenCalled();
expect(client.setRoomTag).not.toHaveBeenCalled();
expect(client.deleteRoomTag).not.toHaveBeenCalled();
done();
});
});
// XXX: Apparently we do call SettingsStore.setValue
xit('should not settings via the SettingsStore when no setting is changed', (done) => {
roomSettings.save().then(() => {
expect(SettingsStore.setValue).not.toHaveBeenCalled();
done();
});
});
it('should set room name when it has changed', (done) => {
const name = "My Room Name";
roomSettings.setName(name);
roomSettings.save().then(() => {
expect(client.setRoomName.mock.calls[0].slice(0, 2))
.toEqual(['!DdJkzRliezrwpNebLk:matrix.org', name]);
done();
});
});
it('should set room topic when it has changed', (done) => {
const topic = "this is a topic";
roomSettings.setTopic(topic);
roomSettings.save().then(() => {
expect(client.setRoomTopic.mock.calls[0].slice(0, 2))
.toEqual(['!DdJkzRliezrwpNebLk:matrix.org', topic]);
done();
});
});
it('should set history visibility when it has changed', (done) => {
const historyVisibility = "translucent";
roomSettings.setState({
history_visibility: historyVisibility,
});
roomSettings.save().then(() => {
expectSentStateEvent(
"!DdJkzRliezrwpNebLk:matrix.org",
"m.room.history_visibility", {history_visibility: historyVisibility},
);
done();
});
});
// XXX: Can't test this because we `getRoomDirectoryVisibility` in `componentWillMount`
xit('should set room directory publicity when set to true', (done) => {
const isRoomPublished = true;
roomSettings.setState({
isRoomPublished,
}, () => {
roomSettings.save().then(() => {
expect(client.setRoomDirectoryVisibility.calls[0].arguments.slice(0, 2))
.toEqual("!DdJkzRliezrwpNebLk:matrix.org", isRoomPublished ? "public" : "private");
done();
});
});
});
it('should set power levels when changed', (done) => {
roomSettings.onPowerLevelsChanged(42, "invite");
roomSettings.save().then(() => {
expectSentStateEvent(
"!DdJkzRliezrwpNebLk:matrix.org",
"m.room.power_levels", { invite: 42 },
);
done();
});
});
it('should set event power levels when changed', (done) => {
roomSettings.onPowerLevelsChanged(42, "event_levels_m.room.message");
roomSettings.save().then(() => {
// We expect all state events to be set to the state_default (50)
// See powerLevelDescriptors in RoomSettings
expectSentStateEvent(
"!DdJkzRliezrwpNebLk:matrix.org",
"m.room.power_levels", {
events: {
'm.room.message': 42,
'm.room.avatar': 50,
'm.room.name': 50,
'm.room.canonical_alias': 50,
'm.room.history_visibility': 50,
'm.room.power_levels': 50,
'm.room.topic': 50,
'im.vector.modular.widgets': 50,
},
},
);
done();
});
});
});
// TODO: Rewrite room settings tests for dialog support
// import React from 'react';
// import ReactDOM from 'react-dom';
// import expect from 'expect';
// import jest from 'jest-mock';
// import Promise from 'bluebird';
// import * as testUtils from '../../../test-utils';
// import sdk from 'matrix-react-sdk';
// const WrappedRoomSettings = testUtils.wrapInMatrixClientContext(sdk.getComponent('views.rooms.RoomSettings'));
// import MatrixClientPeg from '../../../../src/MatrixClientPeg';
// import SettingsStore from '../../../../src/settings/SettingsStore';
//
//
// describe('RoomSettings', () => {
// let parentDiv = null;
// let sandbox = null;
// let client = null;
// let roomSettings = null;
// const room = testUtils.mkStubRoom('!DdJkzRliezrwpNebLk:matrix.org');
//
// function expectSentStateEvent(roomId, eventType, expectedEventContent) {
// let found = false;
// for (const call of client.sendStateEvent.mock.calls) {
// const [
// actualRoomId,
// actualEventType,
// actualEventContent,
// ] = call.slice(0, 3);
//
// if (roomId === actualRoomId && actualEventType === eventType) {
// expect(actualEventContent).toEqual(expectedEventContent);
// found = true;
// break;
// }
// }
// expect(found).toBe(true);
// }
//
// beforeEach(function(done) {
// testUtils.beforeEach(this);
// sandbox = testUtils.stubClient();
// client = MatrixClientPeg.get();
// client.credentials = {userId: '@me:domain.com'};
//
// client.setRoomName = jest.fn().mockReturnValue(Promise.resolve());
// client.setRoomTopic = jest.fn().mockReturnValue(Promise.resolve());
// client.setRoomDirectoryVisibility = jest.fn().mockReturnValue(Promise.resolve());
//
// // Covers any room state event (e.g. name, avatar, topic)
// client.sendStateEvent = jest.fn().mockReturnValue(Promise.resolve());
//
// // Covers room tagging
// client.setRoomTag = jest.fn().mockReturnValue(Promise.resolve());
// client.deleteRoomTag = jest.fn().mockReturnValue(Promise.resolve());
//
// // Covers any setting in the SettingsStore
// // (including local client settings not stored via matrix)
// SettingsStore.setValue = jest.fn().mockReturnValue(Promise.resolve());
//
// parentDiv = document.createElement('div');
// document.body.appendChild(parentDiv);
//
// const gatherWrappedRef = (r) => {roomSettings = r;};
//
// // get use wrappedRef because we're using wrapInMatrixClientContext
// ReactDOM.render(
// <WrappedRoomSettings
// wrappedRef={gatherWrappedRef}
// room={room}
// />,
// parentDiv,
// done,
// );
// });
//
// afterEach((done) => {
// if (parentDiv) {
// ReactDOM.unmountComponentAtNode(parentDiv);
// parentDiv.remove();
// parentDiv = null;
// }
// sandbox.restore();
// done();
// });
//
// it('should not set when no setting is changed', (done) => {
// roomSettings.save().then(() => {
// expect(client.sendStateEvent).not.toHaveBeenCalled();
// expect(client.setRoomTag).not.toHaveBeenCalled();
// expect(client.deleteRoomTag).not.toHaveBeenCalled();
// done();
// });
// });
//
// // XXX: Apparently we do call SettingsStore.setValue
// xit('should not settings via the SettingsStore when no setting is changed', (done) => {
// roomSettings.save().then(() => {
// expect(SettingsStore.setValue).not.toHaveBeenCalled();
// done();
// });
// });
//
// it('should set room name when it has changed', (done) => {
// const name = "My Room Name";
// roomSettings.setName(name);
//
// roomSettings.save().then(() => {
// expect(client.setRoomName.mock.calls[0].slice(0, 2))
// .toEqual(['!DdJkzRliezrwpNebLk:matrix.org', name]);
//
// done();
// });
// });
//
// it('should set room topic when it has changed', (done) => {
// const topic = "this is a topic";
// roomSettings.setTopic(topic);
//
// roomSettings.save().then(() => {
// expect(client.setRoomTopic.mock.calls[0].slice(0, 2))
// .toEqual(['!DdJkzRliezrwpNebLk:matrix.org', topic]);
//
// done();
// });
// });
//
// it('should set history visibility when it has changed', (done) => {
// const historyVisibility = "translucent";
// roomSettings.setState({
// history_visibility: historyVisibility,
// });
//
// roomSettings.save().then(() => {
// expectSentStateEvent(
// "!DdJkzRliezrwpNebLk:matrix.org",
// "m.room.history_visibility", {history_visibility: historyVisibility},
// );
// done();
// });
// });
//
// // XXX: Can't test this because we `getRoomDirectoryVisibility` in `componentWillMount`
// xit('should set room directory publicity when set to true', (done) => {
// const isRoomPublished = true;
// roomSettings.setState({
// isRoomPublished,
// }, () => {
// roomSettings.save().then(() => {
// expect(client.setRoomDirectoryVisibility.calls[0].arguments.slice(0, 2))
// .toEqual("!DdJkzRliezrwpNebLk:matrix.org", isRoomPublished ? "public" : "private");
// done();
// });
// });
// });
//
// it('should set power levels when changed', (done) => {
// roomSettings.onPowerLevelsChanged(42, "invite");
//
// roomSettings.save().then(() => {
// expectSentStateEvent(
// "!DdJkzRliezrwpNebLk:matrix.org",
// "m.room.power_levels", { invite: 42 },
// );
// done();
// });
// });
//
// it('should set event power levels when changed', (done) => {
// roomSettings.onPowerLevelsChanged(42, "event_levels_m.room.message");
//
// roomSettings.save().then(() => {
// // We expect all state events to be set to the state_default (50)
// // See powerLevelDescriptors in RoomSettings
// expectSentStateEvent(
// "!DdJkzRliezrwpNebLk:matrix.org",
// "m.room.power_levels", {
// events: {
// 'm.room.message': 42,
// 'm.room.avatar': 50,
// 'm.room.name': 50,
// 'm.room.canonical_alias': 50,
// 'm.room.history_visibility': 50,
// 'm.room.power_levels': 50,
// 'm.room.topic': 50,
// 'im.vector.modular.widgets': 50,
// },
// },
// );
// done();
// });
// });
// });