Merge branch 'experimental' into bwindels/roomdirectory-makeover

pull/21833/head
Bruno Windels 2019-01-29 18:33:03 +01:00
commit 855dbd7d2b
29 changed files with 784 additions and 306 deletions

View File

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

View File

@ -43,10 +43,6 @@ limitations under the License.
right: 8px; right: 8px;
} }
.mx_ContextualMenu_noChevron {
border-radius: unset !important;
}
.mx_ContextualMenu_chevron_right { .mx_ContextualMenu_chevron_right {
position: absolute; position: absolute;
right: -8px; right: -8px;

View File

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

View File

@ -16,6 +16,7 @@ limitations under the License.
.mx_TopLeftMenu { .mx_TopLeftMenu {
min-width: 180px; min-width: 180px;
border-radius: 4px;
.mx_TopLeftMenu_section:not(:last-child) { .mx_TopLeftMenu_section:not(:last-child) {
border-bottom: 1px solid $menu-border-color; border-bottom: 1px solid $menu-border-color;
@ -26,10 +27,32 @@ limitations under the License.
margin: 5px 0; margin: 5px 0;
padding: 0; padding: 0;
li.mx_TopLeftMenu_icon_settings::after {
mask-image: url('$(res)/img/feather-icons/settings.svg');
}
li.mx_TopLeftMenu_icon_signout::after {
mask-image: url('$(res)/img/feather-icons/sign-out.svg');
}
li::after {
mask-repeat: no-repeat;
mask-position: 0 center;
mask-size: 16px;
position: absolute;
width: 16px;
height: 16px;
content: "";
top: 5px;
left: 14px;
background-color: $primary-fg-color;
}
li { li {
position: relative;
cursor: pointer; cursor: pointer;
white-space: nowrap; white-space: nowrap;
padding: 5px 20px; padding: 5px 20px 5px 43px;
} }
li:hover { li:hover {

View File

@ -22,7 +22,8 @@ limitations under the License.
} }
.mx_Field input, .mx_Field input,
.mx_Field select { .mx_Field select,
.mx_Field textarea {
font-weight: normal; font-weight: normal;
font-family: $font-family; font-family: $font-family;
border-radius: 4px; border-radius: 4px;
@ -32,17 +33,20 @@ limitations under the License.
} }
.mx_Field input:focus, .mx_Field input:focus,
.mx_Field select:focus { .mx_Field select:focus,
.mx_Field textarea:focus {
outline: 0; outline: 0;
border-color: $input-focused-border-color; 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; transition: color 0.25s ease-in 0s;
color: transparent; 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; transition: color 0.25s ease-in 0.1s;
color: $greyed-fg-color; color: $greyed-fg-color;
} }
@ -65,6 +69,8 @@ limitations under the License.
.mx_Field input:focus + label, .mx_Field input:focus + label,
.mx_Field input:not(:placeholder-shown) + 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 */ { .mx_Field select + label /* Always show a select's label on top to not collide with the value */ {
transition: transition:
font-size 0.25s ease-out 0s, font-size 0.25s ease-out 0s,
@ -77,7 +83,8 @@ limitations under the License.
} }
.mx_Field input:focus + label, .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; color: $input-focused-border-color;
} }

View File

@ -32,6 +32,11 @@ limitations under the License.
cursor: row-resize; cursor: row-resize;
} }
.mx_MatrixChat > .mx_ResizeHandle.mx_ResizeHandle_horizontal {
margin: 0 -10px 0 0;
padding: 0 10px 0 0;
}
.mx_ResizeHandle > div { .mx_ResizeHandle > div {
background: $panel-divider-color; background: $panel-divider-color;
} }

View File

@ -22,10 +22,19 @@ limitations under the License.
flex-grow: 1; 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 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 { .mx_ProfileSettings_avatar {
width: 88px; width: 88px;
height: 88px; height: 88px;
@ -41,6 +50,10 @@ limitations under the License.
border-radius: 4px; border-radius: 4px;
} }
.mx_ProfileSettings_avatar .mx_ProfileSettings_avatarOverlay_disabled {
cursor: default;
}
.mx_ProfileSettings_avatar .mx_ProfileSettings_avatarPlaceholder { .mx_ProfileSettings_avatar .mx_ProfileSettings_avatarPlaceholder {
background-color: $settings-profile-placeholder-bg-color; background-color: $settings-profile-placeholder-bg-color;
} }
@ -57,7 +70,7 @@ limitations under the License.
font-size: 10px; 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; display: inline-block;
opacity: 0.5 !important; opacity: 0.5 !important;
color: $settings-profile-overlay-fg-color !important; color: $settings-profile-overlay-fg-color !important;
@ -77,6 +90,11 @@ limitations under the License.
margin-bottom: 8px; margin-bottom: 8px;
} }
.mx_ProfileSettings_noAvatarText {
display: block;
margin: 34px auto auto;
}
.mx_ProfileSettings_avatarOverlayImgContainer { .mx_ProfileSettings_avatarOverlayImgContainer {
position: relative; position: relative;
width: 14px; 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. limitations under the License.
*/ */
.mx_GeneralSettingsTab_changePassword, .mx_GeneralUserSettingsTab_changePassword,
.mx_GeneralSettingsTab_themeSection { .mx_GeneralUserSettingsTab_themeSection {
display: block; display: block;
} }
.mx_GeneralSettingsTab_changePassword .mx_Field, .mx_GeneralUserSettingsTab_changePassword .mx_Field,
.mx_GeneralSettingsTab_themeSection .mx_Field { .mx_GeneralUserSettingsTab_themeSection .mx_Field {
display: block; display: block;
margin-right: 100px; // Align with the other fields on the page 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; display: block;
width: calc(100% - 20px); // subtract 10px padding on left and right 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; margin-top: 0;
} }
.mx_GeneralSettingsTab_themeSection .mx_Field select { .mx_GeneralUserSettingsTab_themeSection .mx_Field select {
display: block; display: block;
width: 100%; width: 100%;
} }
.mx_GeneralSettingsTab_accountSection > .mx_EmailAddresses, .mx_GeneralUserSettingsTab_accountSection > .mx_EmailAddresses,
.mx_GeneralSettingsTab_accountSection > .mx_PhoneNumbers, .mx_GeneralUserSettingsTab_accountSection > .mx_PhoneNumbers,
.mx_GeneralSettingsTab_languageInput { .mx_GeneralUserSettingsTab_languageInput {
margin-right: 100px; // Align with the other fields on the page margin-right: 100px; // Align with the other fields on the page
} }

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<g fill="none" fill-rule="evenodd" stroke="#454545" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2">
<path d="M5.667 15H2.556C1.696 15 1 14.304 1 13.444V2.556C1 1.696 1.696 1 2.556 1h3.11M11.111 11.889L15 8l-3.889-3.889M15 8H5.667"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 364 B

View File

@ -102,10 +102,6 @@ export default class AutoHideScrollbar extends React.Component {
installBodyClassesIfNeeded(); installBodyClassesIfNeeded();
this._needsOverflowListener = this._needsOverflowListener =
document.body.classList.contains("mx_scrollbar_nooverlay"); document.body.classList.contains("mx_scrollbar_nooverlay");
if (this._needsOverflowListener) {
this.containerRef.addEventListener("overflow", this.onOverflow);
this.containerRef.addEventListener("underflow", this.onUnderflow);
}
this.checkOverflow(); this.checkOverflow();
} }
@ -118,13 +114,6 @@ export default class AutoHideScrollbar extends React.Component {
} }
} }
componentWillUnmount() {
if (this._needsOverflowListener && this.containerRef) {
this.containerRef.removeEventListener("overflow", this.onOverflow);
this.containerRef.removeEventListener("underflow", this.onUnderflow);
}
}
render() { render() {
return (<div return (<div
ref={this._collectContainerRef} ref={this._collectContainerRef}

View File

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

View File

@ -84,7 +84,7 @@ const RoomSubList = React.createClass({
// The dataset elements are added in the RoomList _initAndPositionStickyHeaders method // The dataset elements are added in the RoomList _initAndPositionStickyHeaders method
isCollapsableOnClick: function() { isCollapsableOnClick: function() {
const stuck = this.refs.header.dataset.stuck; const stuck = this.refs.header.dataset.stuck;
if (this.state.hidden || stuck === undefined || stuck === "none") { if (!this.props.forceExpand && (this.state.hidden || stuck === undefined || stuck === "none")) {
return true; return true;
} else { } else {
return false; return false;
@ -238,7 +238,7 @@ const RoomSubList = React.createClass({
} }
}, },
_getHeaderJsx: function() { _getHeaderJsx: function(isCollapsed) {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const subListNotifications = this.roomNotificationCount(); const subListNotifications = this.roomNotificationCount();
const subListNotifCount = subListNotifications[0]; const subListNotifCount = subListNotifications[0];
@ -254,9 +254,9 @@ const RoomSubList = React.createClass({
badge = <div className={badgeClasses} onClick={this._onNotifBadgeClick}> badge = <div className={badgeClasses} onClick={this._onNotifBadgeClick}>
{ FormattingUtils.formatCount(subListNotifCount) } { FormattingUtils.formatCount(subListNotifCount) }
</div>; </div>;
} else if (this.props.isInvite) { } else if (this.props.isInvite && this.props.list.length) {
// no notifications but highlight anyway because this is an invite badge // no notifications but highlight anyway because this is an invite badge
badge = <div className={badgeClasses} onClick={this._onInviteBadgeClick}>!</div>; badge = <div className={badgeClasses} onClick={this._onInviteBadgeClick}>{this.props.list.length}</div>;
} }
} }
@ -287,8 +287,8 @@ const RoomSubList = React.createClass({
if (len) { if (len) {
const chevronClasses = classNames({ const chevronClasses = classNames({
'mx_RoomSubList_chevron': true, 'mx_RoomSubList_chevron': true,
'mx_RoomSubList_chevronRight': this.state.hidden, 'mx_RoomSubList_chevronRight': isCollapsed,
'mx_RoomSubList_chevronDown': !this.state.hidden, 'mx_RoomSubList_chevronDown': !isCollapsed,
}); });
chevron = (<div className={chevronClasses}></div>); chevron = (<div className={chevronClasses}></div>);
} }
@ -321,21 +321,23 @@ const RoomSubList = React.createClass({
render: function() { render: function() {
const len = this.props.list.length + this.props.extraTiles.length; const len = this.props.list.length + this.props.extraTiles.length;
const isCollapsed = this.state.hidden && !this.props.forceExpand;
if (len) { if (len) {
const subListClasses = classNames({ const subListClasses = classNames({
"mx_RoomSubList": true, "mx_RoomSubList": true,
"mx_RoomSubList_hidden": this.state.hidden, "mx_RoomSubList_hidden": isCollapsed,
"mx_RoomSubList_nonEmpty": len && !this.state.hidden, "mx_RoomSubList_nonEmpty": len && !isCollapsed,
}); });
if (this.state.hidden) {
if (isCollapsed) {
return <div ref="subList" className={subListClasses}> return <div ref="subList" className={subListClasses}>
{this._getHeaderJsx()} {this._getHeaderJsx(isCollapsed)}
</div>; </div>;
} else { } else {
const tiles = this.makeRoomTiles(); const tiles = this.makeRoomTiles();
tiles.push(...this.props.extraTiles); tiles.push(...this.props.extraTiles);
return <div ref="subList" className={subListClasses}> return <div ref="subList" className={subListClasses}>
{this._getHeaderJsx()} {this._getHeaderJsx(isCollapsed)}
<IndicatorScrollbar ref="scroller" className="mx_RoomSubList_scroll"> <IndicatorScrollbar ref="scroller" className="mx_RoomSubList_scroll">
{ tiles } { tiles }
</IndicatorScrollbar> </IndicatorScrollbar>
@ -344,13 +346,13 @@ const RoomSubList = React.createClass({
} else { } else {
const Loader = sdk.getComponent("elements.Spinner"); const Loader = sdk.getComponent("elements.Spinner");
let content; let content;
if (this.props.showSpinner && !this.state.hidden) { if (this.props.showSpinner && !isCollapsed) {
content = <Loader />; content = <Loader />;
} }
return ( return (
<div ref="subList" className="mx_RoomSubList"> <div ref="subList" className="mx_RoomSubList">
{ this._getHeaderJsx() } { this._getHeaderJsx(isCollapsed) }
{ content } { content }
</div> </div>
); );

View File

@ -22,6 +22,7 @@ import AccessibleButton from '../views/elements/AccessibleButton';
import BaseAvatar from '../views/avatars/BaseAvatar'; import BaseAvatar from '../views/avatars/BaseAvatar';
import MatrixClientPeg from '../../MatrixClientPeg'; import MatrixClientPeg from '../../MatrixClientPeg';
import Avatar from '../../Avatar'; import Avatar from '../../Avatar';
import { _t } from '../../languageHandler';
const AVATAR_SIZE = 28; const AVATAR_SIZE = 28;
@ -70,7 +71,14 @@ export default class TopLeftMenuButton extends React.Component {
render() { render() {
const fallbackUserId = MatrixClientPeg.get().getUserId(); const fallbackUserId = MatrixClientPeg.get().getUserId();
const profileInfo = this.state.profileInfo; const profileInfo = this.state.profileInfo;
const name = profileInfo ? profileInfo.name : fallbackUserId; let name;
if (MatrixClientPeg.get().isGuest()) {
name = _t("Guest");
} else if (profileInfo) {
name = profileInfo.name;
} else {
name = fallbackUserId;
}
let nameElement; let nameElement;
if (!this.props.collapsed) { if (!this.props.collapsed) {
nameElement = <div className="mx_TopLeftMenuButton_name"> nameElement = <div className="mx_TopLeftMenuButton_name">

View File

@ -30,10 +30,10 @@ export class TopLeftMenu extends React.Component {
render() { render() {
return <div className="mx_TopLeftMenu"> return <div className="mx_TopLeftMenu">
<ul className="mx_TopLeftMenu_section"> <ul className="mx_TopLeftMenu_section">
<li onClick={this.openSettings}>{_t("Settings")}</li> <li className="mx_TopLeftMenu_icon_settings" onClick={this.openSettings}>{_t("Settings")}</li>
</ul> </ul>
<ul className="mx_TopLeftMenu_section"> <ul className="mx_TopLeftMenu_section">
<li onClick={this.signOut}>{_t("Sign out")}</li> <li className="mx_TopLeftMenu_icon_signout" onClick={this.signOut}>{_t("Sign out")}</li>
</ul> </ul>
</div>; </div>;
} }

View File

@ -20,6 +20,7 @@ import {Tab, TabbedView} from "../../structures/TabbedView";
import {_t, _td} from "../../../languageHandler"; import {_t, _td} from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import GeneralRoomSettingsTab from "../settings/tabs/GeneralRoomSettingsTab";
// TODO: Ditch this whole component // TODO: Ditch this whole component
export class TempTab extends React.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 = { static propTypes = {
roomId: PropTypes.string.isRequired,
onFinished: PropTypes.func.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() { _getTabs() {
const tabs = []; const tabs = [];
tabs.push(new Tab( tabs.push(new Tab(
_td("General"), _td("General"),
"mx_RoomSettingsDialog_settingsIcon", "mx_RoomSettingsDialog_settingsIcon",
<div>General Test</div>, <GeneralRoomSettingsTab roomId={this.props.roomId} />,
)); ));
tabs.push(new Tab( tabs.push(new Tab(
_td("Security & Privacy"), _td("Security & Privacy"),

View File

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

View File

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

View File

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

View File

@ -119,7 +119,6 @@ module.exports = React.createClass({
const localDomain = this.context.matrixClient.getDomain(); const localDomain = this.context.matrixClient.getDomain();
const EditableItemList = sdk.getComponent('elements.EditableItemList'); const EditableItemList = sdk.getComponent('elements.EditableItemList');
return <div> return <div>
<h3>{ _t('Flair') }</h3>
<EditableItemList <EditableItemList
items={this.state.newGroupsList} items={this.state.newGroupsList}
className={"mx_RelatedGroupSettings"} 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 2016 OpenMarket Ltd
Copyright 2017 Travis Ralston 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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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. limitations under the License.
*/ */
import {MatrixClient} from "matrix-js-sdk";
const React = require('react'); const React = require('react');
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
const sdk = require("../../../index"); const sdk = require("../../../index");
import { _t, _td } from '../../../languageHandler'; import { _t, _td } from '../../../languageHandler';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import dis from "../../../dispatcher";
import MatrixClientPeg from "../../../MatrixClientPeg";
module.exports = React.createClass({ module.exports = React.createClass({
@ -31,21 +32,16 @@ module.exports = React.createClass({
room: PropTypes.object, room: PropTypes.object,
}, },
contextTypes: { _onClickUserSettings: (e) => {
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired, e.preventDefault();
}, e.stopPropagation();
dis.dispatch({action: 'view_user_settings'});
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;
}, },
render: function() { render: function() {
const SettingsFlag = sdk.getComponent("elements.SettingsFlag"); const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
const roomId = this.props.room.roomId; const roomId = this.props.room.roomId;
const isEncrypted = this.context.matrixClient.isRoomEncrypted(roomId); const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(roomId);
let previewsForAccount = null; let previewsForAccount = null;
let previewsForRoom = null; let previewsForRoom = null;
@ -56,13 +52,13 @@ module.exports = React.createClass({
if (accountEnabled) { if (accountEnabled) {
previewsForAccount = ( previewsForAccount = (
_t("You have <a>enabled</a> URL previews by default.", {}, { _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) { } else if (accountEnabled) {
previewsForAccount = ( previewsForAccount = (
_t("You have <a>disabled</a> URL previews by default.", {}, { _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" <SettingsFlag name="urlPreviewsEnabled"
level={SettingLevel.ROOM} level={SettingLevel.ROOM}
roomId={roomId} roomId={roomId}
isExplicit={true} isExplicit={true} />
manualSave={true}
ref="urlPreviewsRoom" />
</label> </label>
); );
} else { } 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 const previewsForRoomAccount = ( // in an e2ee room we use a special key to enforce per-room opt-in
<SettingsFlag name={isEncrypted ? 'urlPreviewsEnabled_e2ee' : 'urlPreviewsEnabled'} <SettingsFlag name={isEncrypted ? 'urlPreviewsEnabled_e2ee' : 'urlPreviewsEnabled'}
level={SettingLevel.ROOM_ACCOUNT} level={SettingLevel.ROOM_ACCOUNT}
roomId={roomId} roomId={roomId} />
manualSave={true}
ref="urlPreviewsSelf"
/>
); );
return ( return (
<div className="mx_RoomSettings_toggles"> <div>
<h3>{ _t("URL Previews") }</h3> <div className='mx_SettingsTab_subsectionText'>
<div>
{ _t('When someone puts a URL in their message, a URL preview can be shown to give more ' + { _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.') } 'information about that link such as the title, description, and an image from the website.') }
</div> </div>
<div> <div className='mx_SettingsTab_subsectionText'>
{ previewsForAccount } { previewsForAccount }
</div> </div>
{ previewsForRoom } { previewsForRoom }

View File

@ -82,7 +82,11 @@ module.exports = React.createClass({
this.collapsedState = collapsedJson ? JSON.parse(collapsedJson) : {}; this.collapsedState = collapsedJson ? JSON.parse(collapsedJson) : {};
this._layoutSections = []; this._layoutSections = [];
this._layout = new Layout((key, size) => { const unfilteredOptions = {
allowWhitespace: false,
handleHeight: 1,
};
this._unfilteredlayout = new Layout((key, size) => {
const subList = this._subListRefs[key]; const subList = this._subListRefs[key];
if (subList) { if (subList) {
subList.setHeight(size); subList.setHeight(size);
@ -95,7 +99,19 @@ module.exports = React.createClass({
window.localStorage.setItem("mx_roomlist_sizes", window.localStorage.setItem("mx_roomlist_sizes",
JSON.stringify(this.subListSizes)); JSON.stringify(this.subListSizes));
} }
}, this.subListSizes, this.collapsedState); }, this.subListSizes, this.collapsedState, unfilteredOptions);
this._filteredLayout = new Layout((key, size) => {
const subList = this._subListRefs[key];
if (subList) {
subList.setHeight(size);
}
}, null, null, {
allowWhitespace: false,
handleHeight: 0,
});
this._layout = this._unfilteredlayout;
return { return {
isLoadingLeftRooms: false, isLoadingLeftRooms: false,
@ -187,15 +203,21 @@ module.exports = React.createClass({
}, },
componentDidUpdate: function(prevProps) { componentDidUpdate: function(prevProps) {
let forceLayoutUpdate = false;
this._repositionIncomingCallBox(undefined, false); this._repositionIncomingCallBox(undefined, false);
// if (this.props.searchFilter !== prevProps.searchFilter) { if (!this.props.searchFilter && prevProps.searchFilter) {
// this._checkSubListsOverflow(); this._layout = this._unfilteredlayout;
// } forceLayoutUpdate = true;
} else if (this.props.searchFilter && !prevProps.searchFilter) {
this._layout = this._filteredLayout;
forceLayoutUpdate = true;
}
this._layout.update( this._layout.update(
this._layoutSections, this._layoutSections,
this.resizeContainer && this.resizeContainer.clientHeight, this.resizeContainer && this.resizeContainer.clientHeight,
forceLayoutUpdate,
); );
// TODO: call layout.setAvailableHeight, window height was changed when bannerShown prop was changed this._checkSubListsOverflow();
}, },
onAction: function(payload) { onAction: function(payload) {
@ -617,7 +639,7 @@ module.exports = React.createClass({
onHeaderClick(collapsed); onHeaderClick(collapsed);
} }
}; };
const startAsHidden = props.startAsHidden || this.collapsedState[chosenKey]; let startAsHidden = props.startAsHidden || this.collapsedState[chosenKey];
this._layoutSections.push({ this._layoutSections.push({
id: chosenKey, id: chosenKey,
count: len, count: len,
@ -625,6 +647,7 @@ module.exports = React.createClass({
let subList = (<RoomSubList let subList = (<RoomSubList
ref={this._subListRef.bind(this, chosenKey)} ref={this._subListRef.bind(this, chosenKey)}
startAsHidden={startAsHidden} startAsHidden={startAsHidden}
forceExpand={!!this.props.searchFilter}
onHeaderClick={onSubListHeaderClick} onHeaderClick={onSubListHeaderClick}
key={chosenKey} key={chosenKey}
label={label} label={label}

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

View File

@ -431,6 +431,9 @@
"Display Name": "Display Name", "Display Name": "Display Name",
"Save": "Save", "Save": "Save",
"Flair": "Flair", "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?", "Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?",
"Success": "Success", "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", "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", "Account management": "Account management",
"Deactivating your account is a permanent action - be careful!": "Deactivating your account is a permanent action - be careful!", "Deactivating your account is a permanent action - be careful!": "Deactivating your account is a permanent action - be careful!",
"Close Account": "Close Account", "Close Account": "Close Account",
"General": "General",
"Legal": "Legal", "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>.": "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.", "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", "'%(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", "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", "'%(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 specified": "not specified",
"not set": "not set", "not set": "not set",
"Remote addresses for this room:": "Remote addresses for this room:", "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:", "Local addresses for this room:": "Local addresses for this room:",
"This room has no local addresses": "This room has no local addresses", "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)", "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:", "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", "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)", "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>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.", "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 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.", "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.", "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.", "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", "Members": "Members",
"Files": "Files", "Files": "Files",
@ -1332,6 +1337,7 @@
"Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.", "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.",
"Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.", "Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.",
"Failed to load timeline position": "Failed to load timeline position", "Failed to load timeline position": "Failed to load timeline position",
"Guest": "Guest",
"Uploading %(filename)s and %(count)s others|other": "Uploading %(filename)s and %(count)s others", "Uploading %(filename)s and %(count)s others|other": "Uploading %(filename)s and %(count)s others",
"Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s", "Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s",
"Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other", "Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other",

View File

@ -16,9 +16,6 @@ limitations under the License.
import FixedDistributor from "./fixed"; import FixedDistributor from "./fixed";
// const allowWhitespace = true;
const handleHeight = 1;
function clamp(height, min, max) { function clamp(height, min, max) {
if (height > max) return max; if (height > max) return max;
if (height < min) return min; if (height < min) return min;
@ -26,7 +23,7 @@ function clamp(height, min, max) {
} }
export class Layout { export class Layout {
constructor(applyHeight, initialSizes, collapsedState) { constructor(applyHeight, initialSizes, collapsedState, options) {
// callback to set height of section // callback to set height of section
this._applyHeight = applyHeight; this._applyHeight = applyHeight;
// list of {id, count} objects, // list of {id, count} objects,
@ -41,6 +38,17 @@ export class Layout {
this._sectionHeights = Object.assign({}, initialSizes); this._sectionHeights = Object.assign({}, initialSizes);
// in-progress heights, while dragging. Committed on mouse-up. // in-progress heights, while dragging. Committed on mouse-up.
this._heights = []; this._heights = [];
// use while manually resizing to cancel
// the resize for a given mouse position
// when the previous resize made the layout
// constrained
this._clampedOffset = 0;
// used while manually resizing, to clear
// _clampedOffset when the direction of resizing changes
this._lastOffset = 0;
this._allowWhitespace = options && options.allowWhitespace;
this._handleHeight = (options && options.handleHeight) || 0;
} }
setAvailableHeight(newSize) { setAvailableHeight(newSize) {
@ -60,7 +68,7 @@ export class Layout {
this._applyNewSize(); this._applyNewSize();
} }
update(sections, availableHeight) { update(sections, availableHeight, force = false) {
let heightChanged = false; let heightChanged = false;
if (Number.isFinite(availableHeight) && availableHeight !== this._availableHeight) { if (Number.isFinite(availableHeight) && availableHeight !== this._availableHeight) {
@ -75,7 +83,7 @@ export class Layout {
return a.id !== b.id || a.count !== b.count; return a.id !== b.id || a.count !== b.count;
}); });
if (!heightChanged && !sectionsChanged) { if (!heightChanged && !sectionsChanged && !force) {
return; return;
} }
@ -104,7 +112,7 @@ export class Layout {
const collapsed = this._collapsedState[section.id]; const collapsed = this._collapsedState[section.id];
return count + (collapsed ? 0 : 1); return count + (collapsed ? 0 : 1);
}, 0); }, 0);
return this._availableHeight - ((nonCollapsedSectionCount - 1) * handleHeight); return this._availableHeight - ((nonCollapsedSectionCount - 1) * this._handleHeight);
} }
_applyNewSize() { _applyNewSize() {
@ -130,9 +138,10 @@ export class Layout {
if (collapsed) { if (collapsed) {
return this._sectionHeight(0); return this._sectionHeight(0);
} else if (!this._allowWhitespace) {
return this._sectionHeight(section.count);
} else { } else {
return 100000; return 100000;
// return this._sectionHeight(section.count);
} }
} }
@ -268,6 +277,22 @@ export class Layout {
this._sectionHeights[section.id] = this._heights[i]; this._sectionHeights[section.id] = this._heights[i];
}); });
} }
_setUncommittedSectionHeight(sectionIndex, offset) {
if (Math.sign(offset) != Math.sign(this._lastOffset)) {
this._clampedOffset = undefined;
}
if (this._clampedOffset !== undefined) {
if (offset < 0 && offset < this._clampedOffset) {
return;
}
if (offset > 0 && offset > this._clampedOffset) {
return;
}
}
this._clampedOffset = this._relayout(sectionIndex, offset);
this._lastOffset = offset;
}
} }
class Handle { class Handle {
@ -278,7 +303,10 @@ class Handle {
} }
setHeight(height) { setHeight(height) {
this._layout._relayout(this._sectionIndex, height - this._initialHeight); this._layout._setUncommittedSectionHeight(
this._sectionIndex,
height - this._initialHeight,
);
return this; return this;
} }

View File

@ -122,7 +122,9 @@ class RoomViewStore extends Store {
case 'open_room_settings': case 'open_room_settings':
if (SettingsStore.isFeatureEnabled("feature_tabbed_settings")) { if (SettingsStore.isFeatureEnabled("feature_tabbed_settings")) {
const RoomSettingsDialog = sdk.getComponent("dialogs.RoomSettingsDialog"); 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 { } else {
this._setState({ this._setState({
isEditingSettings: true, isEditingSettings: true,

View File

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