Merge pull request #6151 from matrix-org/t3chguy/fix/17244
This commit is contained in:
commit
50a5c03730
24 changed files with 912 additions and 448 deletions
|
@ -108,7 +108,10 @@ export default class RoomSettingsDialog extends React.Component<IProps> {
|
|||
ROOM_ADVANCED_TAB,
|
||||
_td("Advanced"),
|
||||
"mx_RoomSettingsDialog_warningIcon",
|
||||
<AdvancedRoomSettingsTab roomId={this.props.roomId} closeSettingsFn={this.props.onFinished} />,
|
||||
<AdvancedRoomSettingsTab
|
||||
roomId={this.props.roomId}
|
||||
closeSettingsFn={() => this.props.onFinished(true)}
|
||||
/>,
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
@ -14,24 +14,27 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useState} from 'react';
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||
import {EventType} from "matrix-js-sdk/src/@types/event";
|
||||
import React, { useMemo } from 'react';
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
|
||||
import {_t} from '../../../languageHandler';
|
||||
import {IDialogProps} from "./IDialogProps";
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import DevtoolsDialog from "./DevtoolsDialog";
|
||||
import SpaceBasicSettings from '../spaces/SpaceBasicSettings';
|
||||
import {getTopic} from "../elements/RoomTopic";
|
||||
import {avatarUrlForRoom} from "../../../Avatar";
|
||||
import ToggleSwitch from "../elements/ToggleSwitch";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import Modal from "../../../Modal";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import {useDispatcher} from "../../../hooks/useDispatcher";
|
||||
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
|
||||
import { useDispatcher } from "../../../hooks/useDispatcher";
|
||||
import TabbedView, { Tab } from "../../structures/TabbedView";
|
||||
import SpaceSettingsGeneralTab from '../spaces/SpaceSettingsGeneralTab';
|
||||
import SpaceSettingsVisibilityTab from "../spaces/SpaceSettingsVisibilityTab";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
import AdvancedRoomSettingsTab from "../settings/tabs/room/AdvancedRoomSettingsTab";
|
||||
|
||||
export enum SpaceSettingsTab {
|
||||
General = "SPACE_GENERAL_TAB",
|
||||
Visibility = "SPACE_VISIBILITY_TAB",
|
||||
Advanced = "SPACE_ADVANCED_TAB",
|
||||
}
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
matrixClient: MatrixClient;
|
||||
|
@ -45,63 +48,30 @@ const SpaceSettingsDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFin
|
|||
}
|
||||
});
|
||||
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const userId = cli.getUserId();
|
||||
|
||||
const [newAvatar, setNewAvatar] = useState<File>(null); // undefined means to remove avatar
|
||||
const canSetAvatar = space.currentState.maySendStateEvent(EventType.RoomAvatar, userId);
|
||||
const avatarChanged = newAvatar !== null;
|
||||
|
||||
const [name, setName] = useState<string>(space.name);
|
||||
const canSetName = space.currentState.maySendStateEvent(EventType.RoomName, userId);
|
||||
const nameChanged = name !== space.name;
|
||||
|
||||
const currentTopic = getTopic(space);
|
||||
const [topic, setTopic] = useState<string>(currentTopic);
|
||||
const canSetTopic = space.currentState.maySendStateEvent(EventType.RoomTopic, userId);
|
||||
const topicChanged = topic !== currentTopic;
|
||||
|
||||
const currentJoinRule = space.getJoinRule();
|
||||
const [joinRule, setJoinRule] = useState(currentJoinRule);
|
||||
const canSetJoinRule = space.currentState.maySendStateEvent(EventType.RoomJoinRules, userId);
|
||||
const joinRuleChanged = joinRule !== currentJoinRule;
|
||||
|
||||
const onSave = async () => {
|
||||
setBusy(true);
|
||||
const promises = [];
|
||||
|
||||
if (avatarChanged) {
|
||||
if (newAvatar) {
|
||||
promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, {
|
||||
url: await cli.uploadContent(newAvatar),
|
||||
}, ""));
|
||||
} else {
|
||||
promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, {}, ""));
|
||||
}
|
||||
}
|
||||
|
||||
if (nameChanged) {
|
||||
promises.push(cli.setRoomName(space.roomId, name));
|
||||
}
|
||||
|
||||
if (topicChanged) {
|
||||
promises.push(cli.setRoomTopic(space.roomId, topic));
|
||||
}
|
||||
|
||||
if (joinRuleChanged) {
|
||||
promises.push(cli.sendStateEvent(space.roomId, EventType.RoomJoinRules, { join_rule: joinRule }, ""));
|
||||
}
|
||||
|
||||
const results = await Promise.allSettled(promises);
|
||||
setBusy(false);
|
||||
const failures = results.filter(r => r.status === "rejected");
|
||||
if (failures.length > 0) {
|
||||
console.error("Failed to save space settings: ", failures);
|
||||
setError(_t("Failed to save space settings."));
|
||||
}
|
||||
};
|
||||
const tabs = useMemo(() => {
|
||||
return [
|
||||
new Tab(
|
||||
SpaceSettingsTab.General,
|
||||
_td("General"),
|
||||
"mx_SpaceSettingsDialog_generalIcon",
|
||||
<SpaceSettingsGeneralTab matrixClient={cli} space={space} onFinished={onFinished} />,
|
||||
),
|
||||
new Tab(
|
||||
SpaceSettingsTab.Visibility,
|
||||
_td("Visibility"),
|
||||
"mx_SpaceSettingsDialog_visibilityIcon",
|
||||
<SpaceSettingsVisibilityTab matrixClient={cli} space={space} />,
|
||||
),
|
||||
SettingsStore.getValue(UIFeature.AdvancedSettings)
|
||||
? new Tab(
|
||||
SpaceSettingsTab.Advanced,
|
||||
_td("Advanced"),
|
||||
"mx_RoomSettingsDialog_warningIcon",
|
||||
<AdvancedRoomSettingsTab roomId={space.roomId} closeSettingsFn={onFinished} />,
|
||||
)
|
||||
: null,
|
||||
].filter(Boolean);
|
||||
}, [cli, space, onFinished]);
|
||||
|
||||
return <BaseDialog
|
||||
title={_t("Space settings")}
|
||||
|
@ -110,61 +80,14 @@ const SpaceSettingsDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFin
|
|||
onFinished={onFinished}
|
||||
fixedWidth={false}
|
||||
>
|
||||
<div className="mx_SpaceSettingsDialog_content" id="mx_SpaceSettingsDialog">
|
||||
<div>{ _t("Edit settings relating to your space.") }</div>
|
||||
|
||||
{ error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
|
||||
|
||||
<SpaceFeedbackPrompt onClick={() => onFinished(false)} />
|
||||
|
||||
<SpaceBasicSettings
|
||||
avatarUrl={avatarUrlForRoom(space, 80, 80, "crop")}
|
||||
avatarDisabled={busy || !canSetAvatar}
|
||||
setAvatar={setNewAvatar}
|
||||
name={name}
|
||||
nameDisabled={busy || !canSetName}
|
||||
setName={setName}
|
||||
topic={topic}
|
||||
topicDisabled={busy || !canSetTopic}
|
||||
setTopic={setTopic}
|
||||
/>
|
||||
|
||||
<div>
|
||||
{ _t("Make this space private") }
|
||||
<ToggleSwitch
|
||||
checked={joinRule !== "public"}
|
||||
onChange={checked => setJoinRule(checked ? "invite" : "public")}
|
||||
disabled={!canSetJoinRule}
|
||||
aria-label={_t("Make this space private")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AccessibleButton
|
||||
kind="danger"
|
||||
onClick={() => {
|
||||
defaultDispatcher.dispatch({
|
||||
action: "leave_room",
|
||||
room_id: space.roomId,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{ _t("Leave Space") }
|
||||
</AccessibleButton>
|
||||
|
||||
<div className="mx_SpaceSettingsDialog_buttons">
|
||||
<AccessibleButton onClick={() => Modal.createDialog(DevtoolsDialog, {roomId: space.roomId})}>
|
||||
{ _t("View dev tools") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton onClick={onFinished} disabled={busy} kind="link">
|
||||
{ _t("Cancel") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton onClick={onSave} disabled={busy} kind="primary">
|
||||
{ busy ? _t("Saving...") : _t("Save Changes") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<div
|
||||
className="mx_SpaceSettingsDialog_content"
|
||||
id="mx_SpaceSettingsDialog"
|
||||
title={_t("Settings - %(spaceName)s", { spaceName: space.name })}
|
||||
>
|
||||
<TabbedView tabs={tabs} />
|
||||
</div>
|
||||
</BaseDialog>;
|
||||
};
|
||||
|
||||
export default SpaceSettingsDialog;
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2017, 2019 New Vector Ltd.
|
||||
Copyright 2017-2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,48 +14,48 @@ 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 React from "react";
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Field from "./Field";
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
export class EditableItem extends React.Component {
|
||||
static propTypes = {
|
||||
index: PropTypes.number,
|
||||
value: PropTypes.string,
|
||||
onRemove: PropTypes.func,
|
||||
interface IItemProps {
|
||||
index?: number;
|
||||
value?: string;
|
||||
onRemove?(index: number): void;
|
||||
}
|
||||
|
||||
interface IItemState {
|
||||
verifyRemove: boolean;
|
||||
}
|
||||
|
||||
export class EditableItem extends React.Component<IItemProps, IItemState> {
|
||||
public state = {
|
||||
verifyRemove: false,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
verifyRemove: false,
|
||||
};
|
||||
}
|
||||
|
||||
_onRemove = (e) => {
|
||||
private onRemove = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
this.setState({verifyRemove: true});
|
||||
this.setState({ verifyRemove: true });
|
||||
};
|
||||
|
||||
_onDontRemove = (e) => {
|
||||
private onDontRemove = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
this.setState({verifyRemove: false});
|
||||
this.setState({ verifyRemove: false });
|
||||
};
|
||||
|
||||
_onActuallyRemove = (e) => {
|
||||
private onActuallyRemove = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (this.props.onRemove) this.props.onRemove(this.props.index);
|
||||
this.setState({verifyRemove: false});
|
||||
this.setState({ verifyRemove: false });
|
||||
};
|
||||
|
||||
render() {
|
||||
|
@ -66,14 +66,14 @@ export class EditableItem extends React.Component {
|
|||
{_t("Are you sure?")}
|
||||
</span>
|
||||
<AccessibleButton
|
||||
onClick={this._onActuallyRemove}
|
||||
onClick={this.onActuallyRemove}
|
||||
kind="primary_sm"
|
||||
className="mx_EditableItem_confirmBtn"
|
||||
>
|
||||
{_t("Yes")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
onClick={this._onDontRemove}
|
||||
onClick={this.onDontRemove}
|
||||
kind="danger_sm"
|
||||
className="mx_EditableItem_confirmBtn"
|
||||
>
|
||||
|
@ -85,59 +85,68 @@ export class EditableItem extends React.Component {
|
|||
|
||||
return (
|
||||
<div className="mx_EditableItem">
|
||||
<div onClick={this._onRemove} className="mx_EditableItem_delete" title={_t("Remove")} role="button" />
|
||||
<div onClick={this.onRemove} className="mx_EditableItem_delete" title={_t("Remove")} role="button" />
|
||||
<span className="mx_EditableItem_item">{this.props.value}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
id: string;
|
||||
items: string[];
|
||||
itemsLabel?: string;
|
||||
noItemsLabel?: string;
|
||||
placeholder?: string;
|
||||
newItem?: string;
|
||||
canEdit?: boolean;
|
||||
canRemove?: boolean;
|
||||
suggestionsListId?: string;
|
||||
onItemAdded?(item: string): void;
|
||||
onItemRemoved?(index: number): void;
|
||||
onNewItemChanged?(item: string): void;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.elements.EditableItemList")
|
||||
export default class EditableItemList extends React.Component {
|
||||
static propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
itemsLabel: PropTypes.string,
|
||||
noItemsLabel: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
newItem: PropTypes.string,
|
||||
|
||||
onItemAdded: PropTypes.func,
|
||||
onItemRemoved: PropTypes.func,
|
||||
onNewItemChanged: PropTypes.func,
|
||||
|
||||
canEdit: PropTypes.bool,
|
||||
canRemove: PropTypes.bool,
|
||||
};
|
||||
|
||||
_onItemAdded = (e) => {
|
||||
export default class EditableItemList<P = {}> extends React.PureComponent<IProps & P> {
|
||||
protected onItemAdded = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (this.props.onItemAdded) this.props.onItemAdded(this.props.newItem);
|
||||
};
|
||||
|
||||
_onItemRemoved = (index) => {
|
||||
protected onItemRemoved = (index) => {
|
||||
if (this.props.onItemRemoved) this.props.onItemRemoved(index);
|
||||
};
|
||||
|
||||
_onNewItemChanged = (e) => {
|
||||
protected onNewItemChanged = (e) => {
|
||||
if (this.props.onNewItemChanged) this.props.onNewItemChanged(e.target.value);
|
||||
};
|
||||
|
||||
_renderNewItemField() {
|
||||
protected renderNewItemField() {
|
||||
return (
|
||||
<form
|
||||
onSubmit={this._onItemAdded}
|
||||
onSubmit={this.onItemAdded}
|
||||
autoComplete="off"
|
||||
noValidate={true}
|
||||
className="mx_EditableItemList_newItem"
|
||||
>
|
||||
<Field label={this.props.placeholder} type="text"
|
||||
autoComplete="off" value={this.props.newItem || ""} onChange={this._onNewItemChanged}
|
||||
list={this.props.suggestionsListId} />
|
||||
<AccessibleButton onClick={this._onItemAdded} kind="primary" type="submit" disabled={!this.props.newItem}>
|
||||
{_t("Add")}
|
||||
<Field
|
||||
label={this.props.placeholder}
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
value={this.props.newItem || ""}
|
||||
onChange={this.onNewItemChanged}
|
||||
list={this.props.suggestionsListId}
|
||||
/>
|
||||
<AccessibleButton
|
||||
onClick={this.onItemAdded}
|
||||
kind="primary"
|
||||
type="submit"
|
||||
disabled={!this.props.newItem}
|
||||
>
|
||||
{ _t("Add") }
|
||||
</AccessibleButton>
|
||||
</form>
|
||||
);
|
||||
|
@ -153,19 +162,21 @@ export default class EditableItemList extends React.Component {
|
|||
key={item}
|
||||
index={index}
|
||||
value={item}
|
||||
onRemove={this._onItemRemoved}
|
||||
onRemove={this.onItemRemoved}
|
||||
/>;
|
||||
});
|
||||
|
||||
const editableItemsSection = this.props.canRemove ? editableItems : <ul>{editableItems}</ul>;
|
||||
const label = this.props.items.length > 0 ? this.props.itemsLabel : this.props.noItemsLabel;
|
||||
|
||||
return (<div className="mx_EditableItemList">
|
||||
<div className="mx_EditableItemList_label">
|
||||
{ label }
|
||||
return (
|
||||
<div className="mx_EditableItemList">
|
||||
<div className="mx_EditableItemList_label">
|
||||
{ label }
|
||||
</div>
|
||||
{ editableItemsSection }
|
||||
{ this.props.canEdit ? this.renderNewItemField() : <div /> }
|
||||
</div>
|
||||
{ editableItemsSection }
|
||||
{ this.props.canEdit ? this._renderNewItemField() : <div /> }
|
||||
</div>);
|
||||
);
|
||||
}
|
||||
}
|
|
@ -29,6 +29,11 @@ function getId() {
|
|||
return `${BASE_ID}_${count++}`;
|
||||
}
|
||||
|
||||
export interface IValidateOpts {
|
||||
focused?: boolean;
|
||||
allowEmpty?: boolean;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
// The field's ID, which binds the input and label together. Immutable.
|
||||
id?: string;
|
||||
|
@ -180,7 +185,7 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
public async validate({ focused, allowEmpty = true }: {focused?: boolean, allowEmpty?: boolean}) {
|
||||
public async validate({ focused, allowEmpty = true }: IValidateOpts) {
|
||||
if (!this.props.onValidate) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,38 +14,33 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from "prop-types";
|
||||
import React from "react";
|
||||
|
||||
import ToggleSwitch from "./ToggleSwitch";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
// The value for the toggle switch
|
||||
value: boolean;
|
||||
// The translated label for the switch
|
||||
label: string;
|
||||
// Whether or not to disable the toggle switch
|
||||
disabled?: boolean;
|
||||
// True to put the toggle in front of the label
|
||||
// Default false.
|
||||
toggleInFront?: boolean;
|
||||
// Additional class names to append to the switch. Optional.
|
||||
className?: string;
|
||||
// The function to call when the value changes
|
||||
onChange(checked: boolean): void;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.elements.LabelledToggleSwitch")
|
||||
export default class LabelledToggleSwitch extends React.Component {
|
||||
static propTypes = {
|
||||
// The value for the toggle switch
|
||||
value: PropTypes.bool.isRequired,
|
||||
|
||||
// The function to call when the value changes
|
||||
onChange: PropTypes.func.isRequired,
|
||||
|
||||
// The translated label for the switch
|
||||
label: PropTypes.string.isRequired,
|
||||
|
||||
// Whether or not to disable the toggle switch
|
||||
disabled: PropTypes.bool,
|
||||
|
||||
// True to put the toggle in front of the label
|
||||
// Default false.
|
||||
toggleInFront: PropTypes.bool,
|
||||
|
||||
// Additional class names to append to the switch. Optional.
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default class LabelledToggleSwitch extends React.PureComponent<IProps> {
|
||||
render() {
|
||||
// This is a minimal version of a SettingsFlag
|
||||
|
||||
let firstPart = <span className="mx_SettingsFlag_label">{this.props.label}</span>;
|
||||
let firstPart = <span className="mx_SettingsFlag_label">{ this.props.label }</span>;
|
||||
let secondPart = <ToggleSwitch
|
||||
checked={this.props.value}
|
||||
disabled={this.props.disabled}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -13,67 +13,78 @@ 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, { createRef } from "react";
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../index';
|
||||
import withValidation from './Validation';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import Field, { IValidateOpts } from "./Field";
|
||||
|
||||
interface IProps {
|
||||
domain: string;
|
||||
value: string;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
onChange?(value: string): void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
isValid: boolean;
|
||||
}
|
||||
|
||||
// Controlled form component wrapping Field for inputting a room alias scoped to a given domain
|
||||
@replaceableComponent("views.elements.RoomAliasField")
|
||||
export default class RoomAliasField extends React.PureComponent {
|
||||
static propTypes = {
|
||||
domain: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.string.isRequired,
|
||||
};
|
||||
export default class RoomAliasField extends React.PureComponent<IProps, IState> {
|
||||
private fieldRef = createRef<Field>();
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {isValid: true};
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isValid: true,
|
||||
};
|
||||
}
|
||||
|
||||
_asFullAlias(localpart) {
|
||||
private asFullAlias(localpart: string): string {
|
||||
return `#${localpart}:${this.props.domain}`;
|
||||
}
|
||||
|
||||
render() {
|
||||
const Field = sdk.getComponent('views.elements.Field');
|
||||
const poundSign = (<span>#</span>);
|
||||
const aliasPostfix = ":" + this.props.domain;
|
||||
const domain = (<span title={aliasPostfix}>{aliasPostfix}</span>);
|
||||
const maxlength = 255 - this.props.domain.length - 2; // 2 for # and :
|
||||
return (
|
||||
<Field
|
||||
label={_t("Room address")}
|
||||
label={this.props.label || _t("Room address")}
|
||||
className="mx_RoomAliasField"
|
||||
prefixComponent={poundSign}
|
||||
postfixComponent={domain}
|
||||
ref={ref => this._fieldRef = ref}
|
||||
onValidate={this._onValidate}
|
||||
placeholder={_t("e.g. my-room")}
|
||||
onChange={this._onChange}
|
||||
ref={this.fieldRef}
|
||||
onValidate={this.onValidate}
|
||||
placeholder={this.props.placeholder || _t("e.g. my-room")}
|
||||
onChange={this.onChange}
|
||||
value={this.props.value.substring(1, this.props.value.length - this.props.domain.length - 1)}
|
||||
maxLength={maxlength}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
_onChange = (ev) => {
|
||||
private onChange = (ev) => {
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(this._asFullAlias(ev.target.value));
|
||||
this.props.onChange(this.asFullAlias(ev.target.value));
|
||||
}
|
||||
};
|
||||
|
||||
_onValidate = async (fieldState) => {
|
||||
const result = await this._validationRules(fieldState);
|
||||
private onValidate = async (fieldState) => {
|
||||
const result = await this.validationRules(fieldState);
|
||||
this.setState({isValid: result.valid});
|
||||
return result;
|
||||
};
|
||||
|
||||
_validationRules = withValidation({
|
||||
private validationRules = withValidation({
|
||||
rules: [
|
||||
{
|
||||
key: "safeLocalpart",
|
||||
|
@ -81,7 +92,7 @@ export default class RoomAliasField extends React.PureComponent {
|
|||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
const fullAlias = this._asFullAlias(value);
|
||||
const fullAlias = this.asFullAlias(value);
|
||||
// XXX: FIXME https://github.com/matrix-org/matrix-doc/issues/668
|
||||
return !value.includes("#") && !value.includes(":") && !value.includes(",") &&
|
||||
encodeURI(fullAlias) === fullAlias;
|
||||
|
@ -90,7 +101,7 @@ export default class RoomAliasField extends React.PureComponent {
|
|||
}, {
|
||||
key: "required",
|
||||
test: async ({ value, allowEmpty }) => allowEmpty || !!value,
|
||||
invalid: () => _t("Please provide a room address"),
|
||||
invalid: () => _t("Please provide an address"),
|
||||
}, {
|
||||
key: "taken",
|
||||
final: true,
|
||||
|
@ -100,7 +111,7 @@ export default class RoomAliasField extends React.PureComponent {
|
|||
}
|
||||
const client = MatrixClientPeg.get();
|
||||
try {
|
||||
await client.getRoomIdForAlias(this._asFullAlias(value));
|
||||
await client.getRoomIdForAlias(this.asFullAlias(value));
|
||||
// we got a room id, so the alias is taken
|
||||
return false;
|
||||
} catch (err) {
|
||||
|
@ -116,15 +127,15 @@ export default class RoomAliasField extends React.PureComponent {
|
|||
],
|
||||
});
|
||||
|
||||
get isValid() {
|
||||
public get isValid() {
|
||||
return this.state.isValid;
|
||||
}
|
||||
|
||||
validate(options) {
|
||||
return this._fieldRef.validate(options);
|
||||
public validate(options: IValidateOpts) {
|
||||
return this.fieldRef.current?.validate(options);
|
||||
}
|
||||
|
||||
focus() {
|
||||
this._fieldRef.focus();
|
||||
public focus() {
|
||||
this.fieldRef.current?.focus();
|
||||
}
|
||||
}
|
|
@ -34,10 +34,19 @@ interface IProps<T extends string> {
|
|||
definitions: IDefinition<T>[];
|
||||
value?: T; // if not provided no options will be selected
|
||||
outlined?: boolean;
|
||||
disabled?: boolean;
|
||||
onChange(newValue: T): void;
|
||||
}
|
||||
|
||||
function StyledRadioGroup<T extends string>({name, definitions, value, className, outlined, onChange}: IProps<T>) {
|
||||
function StyledRadioGroup<T extends string>({
|
||||
name,
|
||||
definitions,
|
||||
value,
|
||||
className,
|
||||
outlined,
|
||||
disabled,
|
||||
onChange,
|
||||
}: IProps<T>) {
|
||||
const _onChange = e => {
|
||||
onChange(e.target.value);
|
||||
};
|
||||
|
@ -50,12 +59,12 @@ function StyledRadioGroup<T extends string>({name, definitions, value, className
|
|||
checked={d.checked !== undefined ? d.checked : d.value === value}
|
||||
name={name}
|
||||
value={d.value}
|
||||
disabled={d.disabled}
|
||||
disabled={disabled || d.disabled}
|
||||
outlined={outlined}
|
||||
>
|
||||
{d.label}
|
||||
{ d.label }
|
||||
</StyledRadioButton>
|
||||
{d.description}
|
||||
{ d.description ? <span>{ d.description }</span> : null }
|
||||
</React.Fragment>)}
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
|||
|
||||
import React, {useCallback, useContext, useEffect, useState} from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { RoomState } from "matrix-js-sdk/src/models/room-state";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { EventType } from 'matrix-js-sdk/src/@types/event';
|
||||
|
||||
|
@ -28,6 +27,7 @@ import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
|||
import PinningUtils from "../../../utils/PinningUtils";
|
||||
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
|
||||
import PinnedEventTile from "../rooms/PinnedEventTile";
|
||||
import { useRoomState } from "../../../hooks/useRoomState";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -75,24 +75,6 @@ export const useReadPinnedEvents = (room: Room): Set<string> => {
|
|||
return readPinnedEvents;
|
||||
};
|
||||
|
||||
const useRoomState = <T extends any>(room: Room, mapper: (state: RoomState) => T): T => {
|
||||
const [value, setValue] = useState<T>(room ? mapper(room.currentState) : undefined);
|
||||
|
||||
const update = useCallback(() => {
|
||||
if (!room) return;
|
||||
setValue(mapper(room.currentState));
|
||||
}, [room, mapper]);
|
||||
|
||||
useEventEmitter(room?.currentState, "RoomState.events", update);
|
||||
useEffect(() => {
|
||||
update();
|
||||
return () => {
|
||||
setValue(undefined);
|
||||
};
|
||||
}, [update]);
|
||||
return value;
|
||||
};
|
||||
|
||||
const PinnedMessagesCard = ({ room, onClose }: IProps) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const canUnpin = useRoomState(room, state => state.mayClientSendStateEvent(EventType.RoomPinnedEvents, cli));
|
||||
|
|
|
@ -503,19 +503,15 @@ const isMuted = (member: RoomMember, powerLevelContent: IPowerLevelsContent) =>
|
|||
return member.powerLevel < levelToSend;
|
||||
};
|
||||
|
||||
const getPowerLevels = room => room.currentState.getStateEvents(EventType.RoomPowerLevels, "")?.getContent() || {};
|
||||
|
||||
export const useRoomPowerLevels = (cli: MatrixClient, room: Room) => {
|
||||
const [powerLevels, setPowerLevels] = useState<IPowerLevelsContent>({});
|
||||
const [powerLevels, setPowerLevels] = useState<IPowerLevelsContent>(getPowerLevels(room));
|
||||
|
||||
const update = useCallback((ev?: MatrixEvent) => {
|
||||
if (!room) return;
|
||||
if (ev && ev.getType() !== EventType.RoomPowerLevels) return;
|
||||
|
||||
const event = room.currentState.getStateEvents(EventType.RoomPowerLevels, "");
|
||||
if (event) {
|
||||
setPowerLevels(event.getContent());
|
||||
} else {
|
||||
setPowerLevels({});
|
||||
}
|
||||
setPowerLevels(getPowerLevels(room));
|
||||
}, [room]);
|
||||
|
||||
useEventEmitter(cli, "RoomState.events", update);
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2018, 2019 New Vector Ltd
|
||||
Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,59 +14,60 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ChangeEvent, createRef } from "react";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
import EditableItemList from "../elements/EditableItemList";
|
||||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import * as sdk from "../../../index";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Field from "../elements/Field";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import Modal from "../../../Modal";
|
||||
import RoomPublishSetting from "./RoomPublishSetting";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import RoomAliasField from "../elements/RoomAliasField";
|
||||
|
||||
class EditableAliasesList extends EditableItemList {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
interface IEditableAliasesListProps {
|
||||
domain?: string;
|
||||
}
|
||||
|
||||
this._aliasField = createRef();
|
||||
}
|
||||
class EditableAliasesList extends EditableItemList<IEditableAliasesListProps> {
|
||||
private aliasField = createRef<RoomAliasField>();
|
||||
|
||||
_onAliasAdded = async () => {
|
||||
await this._aliasField.current.validate({ allowEmpty: false });
|
||||
private onAliasAdded = async () => {
|
||||
await this.aliasField.current.validate({ allowEmpty: false });
|
||||
|
||||
if (this._aliasField.current.isValid) {
|
||||
if (this.aliasField.current.isValid) {
|
||||
if (this.props.onItemAdded) this.props.onItemAdded(this.props.newItem);
|
||||
return;
|
||||
}
|
||||
|
||||
this._aliasField.current.focus();
|
||||
this._aliasField.current.validate({ allowEmpty: false, focused: true });
|
||||
this.aliasField.current.focus();
|
||||
this.aliasField.current.validate({ allowEmpty: false, focused: true });
|
||||
};
|
||||
|
||||
_renderNewItemField() {
|
||||
protected renderNewItemField() {
|
||||
// if we don't need the RoomAliasField,
|
||||
// we don't need to overriden version of _renderNewItemField
|
||||
// we don't need to overriden version of renderNewItemField
|
||||
if (!this.props.domain) {
|
||||
return super._renderNewItemField();
|
||||
return super.renderNewItemField();
|
||||
}
|
||||
const RoomAliasField = sdk.getComponent('views.elements.RoomAliasField');
|
||||
const onChange = (alias) => this._onNewItemChanged({target: {value: alias}});
|
||||
const onChange = (alias) => this.onNewItemChanged({target: {value: alias}});
|
||||
return (
|
||||
<form
|
||||
onSubmit={this._onAliasAdded}
|
||||
onSubmit={this.onAliasAdded}
|
||||
autoComplete="off"
|
||||
noValidate={true}
|
||||
className="mx_EditableItemList_newItem"
|
||||
>
|
||||
<RoomAliasField
|
||||
ref={this._aliasField}
|
||||
ref={this.aliasField}
|
||||
onChange={onChange}
|
||||
value={this.props.newItem || ""}
|
||||
domain={this.props.domain} />
|
||||
<AccessibleButton onClick={this._onAliasAdded} kind="primary">
|
||||
<AccessibleButton onClick={this.onAliasAdded} kind="primary">
|
||||
{ _t("Add") }
|
||||
</AccessibleButton>
|
||||
</form>
|
||||
|
@ -75,19 +75,30 @@ class EditableAliasesList extends EditableItemList {
|
|||
}
|
||||
}
|
||||
|
||||
@replaceableComponent("views.room_settings.AliasSettings")
|
||||
export default class AliasSettings extends React.Component {
|
||||
static propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
canSetCanonicalAlias: PropTypes.bool.isRequired,
|
||||
canSetAliases: PropTypes.bool.isRequired,
|
||||
canonicalAliasEvent: PropTypes.object, // MatrixEvent
|
||||
};
|
||||
interface IProps {
|
||||
roomId: string;
|
||||
canSetCanonicalAlias: boolean;
|
||||
canSetAliases: boolean;
|
||||
canonicalAliasEvent?: MatrixEvent;
|
||||
hidePublishSetting?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
altAliases: string[];
|
||||
localAliases: string[];
|
||||
canonicalAlias?: string;
|
||||
updatingCanonicalAlias: boolean;
|
||||
localAliasesLoading: boolean;
|
||||
detailsOpen: boolean;
|
||||
newAlias?: string;
|
||||
newAltAlias?: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.room_settings.AliasSettings")
|
||||
export default class AliasSettings extends React.Component<IProps, IState> {
|
||||
static defaultProps = {
|
||||
canSetAliases: false,
|
||||
canSetCanonicalAlias: false,
|
||||
aliasEvents: [],
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
|
@ -122,7 +133,7 @@ export default class AliasSettings extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
async loadLocalAliases() {
|
||||
private async loadLocalAliases() {
|
||||
this.setState({ localAliasesLoading: true });
|
||||
try {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
@ -143,7 +154,7 @@ export default class AliasSettings extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
changeCanonicalAlias(alias) {
|
||||
private changeCanonicalAlias(alias: string) {
|
||||
if (!this.props.canSetCanonicalAlias) return;
|
||||
|
||||
const oldAlias = this.state.canonicalAlias;
|
||||
|
@ -174,7 +185,7 @@ export default class AliasSettings extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
changeAltAliases(altAliases) {
|
||||
private changeAltAliases(altAliases: string[]) {
|
||||
if (!this.props.canSetCanonicalAlias) return;
|
||||
|
||||
this.setState({
|
||||
|
@ -185,7 +196,7 @@ export default class AliasSettings extends React.Component {
|
|||
const eventContent = {};
|
||||
|
||||
if (this.state.canonicalAlias) {
|
||||
eventContent.alias = this.state.canonicalAlias;
|
||||
eventContent["alias"] = this.state.canonicalAlias;
|
||||
}
|
||||
if (altAliases) {
|
||||
eventContent["alt_aliases"] = altAliases;
|
||||
|
@ -206,11 +217,11 @@ export default class AliasSettings extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
onNewAliasChanged = (value) => {
|
||||
this.setState({newAlias: value});
|
||||
private onNewAliasChanged = (value: string) => {
|
||||
this.setState({ newAlias: value });
|
||||
};
|
||||
|
||||
onLocalAliasAdded = (alias) => {
|
||||
private onLocalAliasAdded = (alias: string) => {
|
||||
if (!alias || alias.length === 0) return; // ignore attempts to create blank aliases
|
||||
|
||||
const localDomain = MatrixClientPeg.get().getDomain();
|
||||
|
@ -236,7 +247,7 @@ export default class AliasSettings extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
onLocalAliasDeleted = (index) => {
|
||||
private onLocalAliasDeleted = (index: number) => {
|
||||
const alias = this.state.localAliases[index];
|
||||
// TODO: In future, we should probably be making sure that the alias actually belongs
|
||||
// to this room. See https://github.com/vector-im/element-web/issues/7353
|
||||
|
@ -265,7 +276,7 @@ export default class AliasSettings extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
onLocalAliasesToggled = (event) => {
|
||||
private onLocalAliasesToggled = (event: ChangeEvent<HTMLDetailsElement>) => {
|
||||
// expanded
|
||||
if (event.target.open) {
|
||||
// if local aliases haven't been preloaded yet at component mount
|
||||
|
@ -273,43 +284,45 @@ export default class AliasSettings extends React.Component {
|
|||
this.loadLocalAliases();
|
||||
}
|
||||
}
|
||||
this.setState({detailsOpen: event.target.open});
|
||||
this.setState({ detailsOpen: event.currentTarget.open });
|
||||
};
|
||||
|
||||
onCanonicalAliasChange = (event) => {
|
||||
private onCanonicalAliasChange = (event: ChangeEvent<HTMLSelectElement>) => {
|
||||
this.changeCanonicalAlias(event.target.value);
|
||||
};
|
||||
|
||||
onNewAltAliasChanged = (value) => {
|
||||
this.setState({newAltAlias: value});
|
||||
private onNewAltAliasChanged = (value: string) => {
|
||||
this.setState({ newAltAlias: value });
|
||||
}
|
||||
|
||||
onAltAliasAdded = (alias) => {
|
||||
private onAltAliasAdded = (alias: string) => {
|
||||
const altAliases = this.state.altAliases.slice();
|
||||
if (!altAliases.some(a => a.trim() === alias.trim())) {
|
||||
altAliases.push(alias.trim());
|
||||
this.changeAltAliases(altAliases);
|
||||
this.setState({newAltAlias: ""});
|
||||
this.setState({ newAltAlias: "" });
|
||||
}
|
||||
}
|
||||
|
||||
onAltAliasDeleted = (index) => {
|
||||
private onAltAliasDeleted = (index: number) => {
|
||||
const altAliases = this.state.altAliases.slice();
|
||||
altAliases.splice(index, 1);
|
||||
this.changeAltAliases(altAliases);
|
||||
}
|
||||
|
||||
_getAliases() {
|
||||
return this.state.altAliases.concat(this._getLocalNonAltAliases());
|
||||
private getAliases() {
|
||||
return this.state.altAliases.concat(this.getLocalNonAltAliases());
|
||||
}
|
||||
|
||||
_getLocalNonAltAliases() {
|
||||
private getLocalNonAltAliases() {
|
||||
const {altAliases} = this.state;
|
||||
return this.state.localAliases.filter(alias => !altAliases.includes(alias));
|
||||
}
|
||||
|
||||
render() {
|
||||
const localDomain = MatrixClientPeg.get().getDomain();
|
||||
const cli = MatrixClientPeg.get();
|
||||
const localDomain = cli.getDomain();
|
||||
const isSpaceRoom = cli.getRoom(this.props.roomId)?.isSpaceRoom();
|
||||
|
||||
let found = false;
|
||||
const canonicalValue = this.state.canonicalAlias || "";
|
||||
|
@ -324,7 +337,7 @@ export default class AliasSettings extends React.Component {
|
|||
>
|
||||
<option value="" key="unset">{ _t('not specified') }</option>
|
||||
{
|
||||
this._getAliases().map((alias, i) => {
|
||||
this.getAliases().map((alias, i) => {
|
||||
if (alias === this.state.canonicalAlias) found = true;
|
||||
return (
|
||||
<option value={alias} key={i}>
|
||||
|
@ -344,12 +357,10 @@ export default class AliasSettings extends React.Component {
|
|||
|
||||
let localAliasesList;
|
||||
if (this.state.localAliasesLoading) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
localAliasesList = <Spinner />;
|
||||
} else {
|
||||
localAliasesList = (<EditableAliasesList
|
||||
id="roomAliases"
|
||||
className={"mx_RoomSettings_localAliases"}
|
||||
items={this.state.localAliases}
|
||||
newItem={this.state.newAlias}
|
||||
onNewItemChanged={this.onNewAliasChanged}
|
||||
|
@ -357,7 +368,9 @@ export default class AliasSettings extends React.Component {
|
|||
canEdit={this.props.canSetAliases}
|
||||
onItemAdded={this.onLocalAliasAdded}
|
||||
onItemRemoved={this.onLocalAliasDeleted}
|
||||
noItemsLabel={_t('This room has no local addresses')}
|
||||
noItemsLabel={isSpaceRoom
|
||||
? _t("This space has no local addresses")
|
||||
: _t("This room has no local addresses")}
|
||||
placeholder={_t('Local address')}
|
||||
domain={localDomain}
|
||||
/>);
|
||||
|
@ -366,18 +379,27 @@ export default class AliasSettings extends React.Component {
|
|||
return (
|
||||
<div className='mx_AliasSettings'>
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Published Addresses")}</span>
|
||||
<p>{_t("Published addresses can be used by anyone on any server to join your room. " +
|
||||
"To publish an address, it needs to be set as a local address first.")}</p>
|
||||
{canonicalAliasSection}
|
||||
<RoomPublishSetting roomId={this.props.roomId} canSetCanonicalAlias={this.props.canSetCanonicalAlias} />
|
||||
<p>
|
||||
{ isSpaceRoom
|
||||
? _t("Published addresses can be used by anyone on any server to join your space.")
|
||||
: _t("Published addresses can be used by anyone on any server to join your room.")}
|
||||
|
||||
{ _t("To publish an address, it needs to be set as a local address first.") }
|
||||
</p>
|
||||
{ canonicalAliasSection }
|
||||
{ this.props.hidePublishSetting
|
||||
? null
|
||||
: <RoomPublishSetting
|
||||
roomId={this.props.roomId}
|
||||
canSetCanonicalAlias={this.props.canSetCanonicalAlias}
|
||||
/> }
|
||||
<datalist id="mx_AliasSettings_altRecommendations">
|
||||
{this._getLocalNonAltAliases().map(alias => {
|
||||
{this.getLocalNonAltAliases().map(alias => {
|
||||
return <option value={alias} key={alias} />;
|
||||
})};
|
||||
</datalist>
|
||||
<EditableAliasesList
|
||||
id="roomAltAliases"
|
||||
className={"mx_RoomSettings_altAliases"}
|
||||
items={this.state.altAliases}
|
||||
newItem={this.state.newAltAlias}
|
||||
onNewItemChanged={this.onNewAltAliasChanged}
|
||||
|
@ -390,11 +412,19 @@ export default class AliasSettings extends React.Component {
|
|||
noItemsLabel={_t('No other published addresses yet, add one below')}
|
||||
placeholder={_t('New published address (e.g. #alias:server)')}
|
||||
/>
|
||||
<span className='mx_SettingsTab_subheading mx_AliasSettings_localAliasHeader'>{_t("Local Addresses")}</span>
|
||||
<p>{_t("Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)", {localDomain})}</p>
|
||||
<span className='mx_SettingsTab_subheading mx_AliasSettings_localAliasHeader'>
|
||||
{ _t("Local Addresses") }
|
||||
</span>
|
||||
<p>
|
||||
{ isSpaceRoom
|
||||
? _t("Set addresses for this space so users can find this space " +
|
||||
"through your homeserver (%(localDomain)s)", { localDomain })
|
||||
: _t("Set addresses for this room so users can find this room " +
|
||||
"through your homeserver (%(localDomain)s)", { localDomain }) }
|
||||
</p>
|
||||
<details onToggle={this.onLocalAliasesToggled} open={this.state.detailsOpen}>
|
||||
<summary>{ this.state.detailsOpen ? _t('Show less') : _t("Show more")}</summary>
|
||||
{localAliasesList}
|
||||
{ localAliasesList }
|
||||
</details>
|
||||
</div>
|
||||
);
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2020 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,20 +14,34 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
|
||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
roomId: string;
|
||||
label?: string;
|
||||
canSetCanonicalAlias?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
isRoomPublished: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.room_settings.RoomPublishSetting")
|
||||
export default class RoomPublishSetting extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {isRoomPublished: false};
|
||||
export default class RoomPublishSetting extends React.PureComponent<IProps, IState> {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isRoomPublished: false,
|
||||
};
|
||||
}
|
||||
|
||||
onRoomPublishChange = (e) => {
|
||||
private onRoomPublishChange = (e) => {
|
||||
const valueBefore = this.state.isRoomPublished;
|
||||
const newValue = !valueBefore;
|
||||
this.setState({isRoomPublished: newValue});
|
||||
|
@ -52,11 +66,14 @@ export default class RoomPublishSetting extends React.PureComponent {
|
|||
render() {
|
||||
const client = MatrixClientPeg.get();
|
||||
|
||||
return (<LabelledToggleSwitch value={this.state.isRoomPublished}
|
||||
onChange={this.onRoomPublishChange}
|
||||
disabled={!this.props.canSetCanonicalAlias}
|
||||
label={_t("Publish this room to the public in %(domain)s's room directory?", {
|
||||
domain: client.getDomain(),
|
||||
})} />);
|
||||
return (
|
||||
<LabelledToggleSwitch value={this.state.isRoomPublished}
|
||||
onChange={this.onRoomPublishChange}
|
||||
disabled={!this.props.canSetCanonicalAlias}
|
||||
label={_t("Publish this room to the public in %(domain)s's room directory?", {
|
||||
domain: client.getDomain(),
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,68 +15,76 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {_t} from "../../../../../languageHandler";
|
||||
import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
|
||||
import * as sdk from "../../../../..";
|
||||
import { EventType } from 'matrix-js-sdk/src/@types/event';
|
||||
|
||||
import { _t } from "../../../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
|
||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
import RoomUpgradeDialog from "../../../dialogs/RoomUpgradeDialog";
|
||||
import DevtoolsDialog from "../../../dialogs/DevtoolsDialog";
|
||||
import Modal from "../../../../../Modal";
|
||||
import dis from "../../../../../dispatcher/dispatcher";
|
||||
import {replaceableComponent} from "../../../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
roomId: string;
|
||||
closeSettingsFn(): void;
|
||||
}
|
||||
|
||||
interface IRecommendedVersion {
|
||||
version: string;
|
||||
needsUpgrade: boolean;
|
||||
urgent: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
upgradeRecommendation?: IRecommendedVersion;
|
||||
oldRoomId?: string;
|
||||
oldEventId?: string;
|
||||
upgraded?: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.settings.tabs.room.AdvancedRoomSettingsTab")
|
||||
export default class AdvancedRoomSettingsTab extends React.Component {
|
||||
static propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
closeSettingsFn: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
export default class AdvancedRoomSettingsTab extends React.Component<IProps, IState> {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
// This is eventually set to the value of room.getRecommendedVersion()
|
||||
upgradeRecommendation: null,
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: [REACT-WARNING] Move this to constructor
|
||||
UNSAFE_componentWillMount() { // eslint-disable-line camelcase
|
||||
// we handle lack of this object gracefully later, so don't worry about it failing here.
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||
room.getRecommendedVersion().then((v) => {
|
||||
const tombstone = room.currentState.getStateEvents("m.room.tombstone", "");
|
||||
const tombstone = room.currentState.getStateEvents(EventType.RoomTombstone, "");
|
||||
|
||||
const additionalStateChanges = {};
|
||||
const createEvent = room.currentState.getStateEvents("m.room.create", "");
|
||||
const additionalStateChanges: Partial<IState> = {};
|
||||
const createEvent = room.currentState.getStateEvents(EventType.RoomCreate, "");
|
||||
const predecessor = createEvent ? createEvent.getContent().predecessor : null;
|
||||
if (predecessor && predecessor.room_id) {
|
||||
additionalStateChanges['oldRoomId'] = predecessor.room_id;
|
||||
additionalStateChanges['oldEventId'] = predecessor.event_id;
|
||||
additionalStateChanges['hasPreviousRoom'] = true;
|
||||
additionalStateChanges.oldRoomId = predecessor.room_id;
|
||||
additionalStateChanges.oldEventId = predecessor.event_id;
|
||||
}
|
||||
|
||||
|
||||
this.setState({
|
||||
upgraded: tombstone && tombstone.getContent().replacement_room,
|
||||
upgraded: !!tombstone?.getContent().replacement_room,
|
||||
upgradeRecommendation: v,
|
||||
...additionalStateChanges,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_upgradeRoom = (e) => {
|
||||
const RoomUpgradeDialog = sdk.getComponent('dialogs.RoomUpgradeDialog');
|
||||
private upgradeRoom = (e) => {
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||
Modal.createTrackedDialog('Upgrade Room Version', '', RoomUpgradeDialog, {room: room});
|
||||
Modal.createTrackedDialog('Upgrade Room Version', '', RoomUpgradeDialog, { room });
|
||||
};
|
||||
|
||||
_openDevtools = (e) => {
|
||||
const DevtoolsDialog = sdk.getComponent('dialogs.DevtoolsDialog');
|
||||
private openDevtools = (e) => {
|
||||
Modal.createDialog(DevtoolsDialog, {roomId: this.props.roomId});
|
||||
};
|
||||
|
||||
_onOldRoomClicked = (e) => {
|
||||
private onOldRoomClicked = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
|
@ -93,9 +101,9 @@ export default class AdvancedRoomSettingsTab extends React.Component {
|
|||
const room = client.getRoom(this.props.roomId);
|
||||
|
||||
let unfederatableSection;
|
||||
const createEvent = room.currentState.getStateEvents('m.room.create', '');
|
||||
const createEvent = room.currentState.getStateEvents(EventType.RoomCreate, '');
|
||||
if (createEvent && createEvent.getContent()['m.federate'] === false) {
|
||||
unfederatableSection = <div>{_t('This room is not accessible by remote Matrix servers')}</div>;
|
||||
unfederatableSection = <div>{ _t('This room is not accessible by remote Matrix servers') }</div>;
|
||||
}
|
||||
|
||||
let roomUpgradeButton;
|
||||
|
@ -103,7 +111,7 @@ export default class AdvancedRoomSettingsTab extends React.Component {
|
|||
roomUpgradeButton = (
|
||||
<div>
|
||||
<p className='mx_SettingsTab_warningText'>
|
||||
{_t(
|
||||
{ _t(
|
||||
"<b>Warning</b>: Upgrading a room will <i>not automatically migrate room members " +
|
||||
"to the new version of the room.</i> We'll post a link to the new room in the old " +
|
||||
"version of the room - room members will have to click this link to join the new room.",
|
||||
|
@ -111,51 +119,53 @@ export default class AdvancedRoomSettingsTab extends React.Component {
|
|||
"b": (sub) => <b>{sub}</b>,
|
||||
"i": (sub) => <i>{sub}</i>,
|
||||
},
|
||||
)}
|
||||
) }
|
||||
</p>
|
||||
<AccessibleButton onClick={this._upgradeRoom} kind='primary'>
|
||||
{_t("Upgrade this room to the recommended room version")}
|
||||
<AccessibleButton onClick={this.upgradeRoom} kind='primary'>
|
||||
{ _t("Upgrade this room to the recommended room version") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let oldRoomLink;
|
||||
if (this.state.hasPreviousRoom) {
|
||||
if (this.state.oldRoomId) {
|
||||
let name = _t("this room");
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||
if (room && room.name) name = room.name;
|
||||
oldRoomLink = (
|
||||
<AccessibleButton element='a' onClick={this._onOldRoomClicked}>
|
||||
{_t("View older messages in %(roomName)s.", {roomName: name})}
|
||||
<AccessibleButton element='a' onClick={this.onOldRoomClicked}>
|
||||
{ _t("View older messages in %(roomName)s.", { roomName: name }) }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_SettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{_t("Advanced")}</div>
|
||||
<div className="mx_SettingsTab_heading">{ _t("Advanced") }</div>
|
||||
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Room information")}</span>
|
||||
<span className='mx_SettingsTab_subheading'>
|
||||
{ room?.isSpaceRoom() ? _t("Space information") : _t("Room information") }
|
||||
</span>
|
||||
<div>
|
||||
<span>{_t("Internal room ID:")}</span>
|
||||
{this.props.roomId}
|
||||
<span>{ _t("Internal room ID:") }</span>
|
||||
{ this.props.roomId }
|
||||
</div>
|
||||
{unfederatableSection}
|
||||
{ unfederatableSection }
|
||||
</div>
|
||||
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Room version")}</span>
|
||||
<span className='mx_SettingsTab_subheading'>{ _t("Room version") }</span>
|
||||
<div>
|
||||
<span>{_t("Room version:")}</span>
|
||||
{room.getVersion()}
|
||||
<span>{ _t("Room version:") }</span>
|
||||
{ room.getVersion() }
|
||||
</div>
|
||||
{oldRoomLink}
|
||||
{roomUpgradeButton}
|
||||
{ oldRoomLink }
|
||||
{ roomUpgradeButton }
|
||||
</div>
|
||||
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Developer options")}</span>
|
||||
<AccessibleButton onClick={this._openDevtools} kind='primary'>
|
||||
{_t("Open Devtools")}
|
||||
<span className='mx_SettingsTab_subheading'>{ _t("Developer options") }</span>
|
||||
<AccessibleButton onClick={this.openDevtools} kind='primary'>
|
||||
{ _t("Open Devtools") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
|
@ -60,7 +60,6 @@ export default class GeneralRoomSettingsTab extends React.Component {
|
|||
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", "");
|
||||
|
@ -100,7 +99,7 @@ export default class GeneralRoomSettingsTab extends React.Component {
|
|||
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
||||
<AliasSettings roomId={this.props.roomId}
|
||||
canSetCanonicalAlias={canSetCanonical} canSetAliases={canSetAliases}
|
||||
canonicalAliasEvent={canonicalAliasEv} aliasEvents={aliasEvents} />
|
||||
canonicalAliasEvent={canonicalAliasEv} />
|
||||
</div>
|
||||
<div className="mx_SettingsTab_heading">{_t("Other")}</div>
|
||||
{ flairSection }
|
||||
|
|
|
@ -29,19 +29,19 @@ import {UIFeature} from "../../../../../settings/UIFeature";
|
|||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||
|
||||
// Knock and private are reserved keywords which are not yet implemented.
|
||||
enum JoinRule {
|
||||
export enum JoinRule {
|
||||
Public = "public",
|
||||
Knock = "knock",
|
||||
Invite = "invite",
|
||||
Private = "private",
|
||||
}
|
||||
|
||||
enum GuestAccess {
|
||||
export enum GuestAccess {
|
||||
CanJoin = "can_join",
|
||||
Forbidden = "forbidden",
|
||||
}
|
||||
|
||||
enum HistoryVisibility {
|
||||
export enum HistoryVisibility {
|
||||
Invited = "invited",
|
||||
Joined = "joined",
|
||||
Shared = "shared",
|
||||
|
@ -121,7 +121,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||
if (refreshWhenTypes.includes(e.getType())) this.forceUpdate();
|
||||
};
|
||||
|
||||
private onEncryptionChange = (e: React.ChangeEvent) => {
|
||||
private onEncryptionChange = () => {
|
||||
Modal.createTrackedDialog('Enable encryption', '', QuestionDialog, {
|
||||
title: _t('Enable encryption?'),
|
||||
description: _t(
|
||||
|
|
|
@ -35,6 +35,7 @@ import withValidation from "../elements/Validation";
|
|||
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
|
||||
import { Preset } from "matrix-js-sdk/src/@types/partials";
|
||||
import { ICreateRoomStateEvent } from "matrix-js-sdk/src/@types/requests";
|
||||
import RoomAliasField from "../elements/RoomAliasField";
|
||||
|
||||
const SpaceCreateMenuType = ({ title, description, className, onClick }) => {
|
||||
return (
|
||||
|
@ -60,6 +61,11 @@ const spaceNameValidator = withValidation({
|
|||
],
|
||||
});
|
||||
|
||||
const nameToAlias = (name: string, domain: string): string => {
|
||||
const localpart = name.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9_-]+/gi, "");
|
||||
return `#${localpart}:${domain}`;
|
||||
};
|
||||
|
||||
const SpaceCreateMenu = ({ onFinished }) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const [visibility, setVisibility] = useState<Visibility>(null);
|
||||
|
@ -67,6 +73,8 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
|||
|
||||
const [name, setName] = useState("");
|
||||
const spaceNameField = useRef<Field>();
|
||||
const [alias, setAlias] = useState("");
|
||||
const spaceAliasField = useRef<RoomAliasField>();
|
||||
const [avatar, setAvatar] = useState<File>(null);
|
||||
const [topic, setTopic] = useState<string>("");
|
||||
|
||||
|
@ -82,6 +90,13 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
|||
setBusy(false);
|
||||
return;
|
||||
}
|
||||
// validate the space name alias field but do not require it
|
||||
if (visibility === Visibility.Public && !await spaceAliasField.current.validate({ allowEmpty: true })) {
|
||||
spaceAliasField.current.focus();
|
||||
spaceAliasField.current.validate({ allowEmpty: true, focused: true });
|
||||
setBusy(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const initialState: ICreateRoomStateEvent[] = [
|
||||
{
|
||||
|
@ -99,12 +114,6 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
|||
content: { url },
|
||||
});
|
||||
}
|
||||
if (topic) {
|
||||
initialState.push({
|
||||
type: EventType.RoomTopic,
|
||||
content: { topic },
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await createRoom({
|
||||
|
@ -112,7 +121,6 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
|||
preset: visibility === Visibility.Public ? Preset.PublicChat : Preset.PrivateChat,
|
||||
name,
|
||||
creation_content: {
|
||||
// Based on MSC1840
|
||||
[RoomCreateTypeField]: RoomType.Space,
|
||||
},
|
||||
initial_state: initialState,
|
||||
|
@ -121,6 +129,8 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
|||
events_default: 100,
|
||||
...Visibility.Public ? { invite: 0 } : {},
|
||||
},
|
||||
room_alias_name: alias ? alias.substr(1, alias.indexOf(":") - 1) : undefined,
|
||||
topic,
|
||||
},
|
||||
spinner: false,
|
||||
encryption: false,
|
||||
|
@ -159,6 +169,7 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
|||
<SpaceFeedbackPrompt onClick={onFinished} />
|
||||
</React.Fragment>;
|
||||
} else {
|
||||
const domain = cli.getDomain();
|
||||
body = <React.Fragment>
|
||||
<AccessibleTooltipButton
|
||||
className="mx_SpaceCreateMenu_back"
|
||||
|
@ -187,12 +198,30 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
|||
label={_t("Name")}
|
||||
autoFocus={true}
|
||||
value={name}
|
||||
onChange={ev => setName(ev.target.value)}
|
||||
onChange={ev => {
|
||||
const newName = ev.target.value;
|
||||
if (!alias || alias === nameToAlias(name, domain)) {
|
||||
setAlias(nameToAlias(newName, domain));
|
||||
}
|
||||
setName(newName);
|
||||
}}
|
||||
ref={spaceNameField}
|
||||
onValidate={spaceNameValidator}
|
||||
disabled={busy}
|
||||
/>
|
||||
|
||||
{ visibility === Visibility.Public
|
||||
? <RoomAliasField
|
||||
ref={spaceAliasField}
|
||||
onChange={setAlias}
|
||||
domain={domain}
|
||||
value={alias}
|
||||
placeholder={name ? nameToAlias(name, domain) : _t("e.g. my-space")}
|
||||
label={_t("Address")}
|
||||
/>
|
||||
: null
|
||||
}
|
||||
|
||||
<Field
|
||||
name="spaceTopic"
|
||||
element="textarea"
|
||||
|
|
143
src/components/views/spaces/SpaceSettingsGeneralTab.tsx
Normal file
143
src/components/views/spaces/SpaceSettingsGeneralTab.tsx
Normal file
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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, { useState } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { SpaceFeedbackPrompt } from "../../structures/SpaceRoomView";
|
||||
import SpaceBasicSettings from "./SpaceBasicSettings";
|
||||
import { avatarUrlForRoom } from "../../../Avatar";
|
||||
import { IDialogProps } from "../dialogs/IDialogProps";
|
||||
import { getTopic } from "../elements/RoomTopic";
|
||||
import { defaultDispatcher } from "../../../dispatcher/dispatcher";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
matrixClient: MatrixClient;
|
||||
space: Room;
|
||||
}
|
||||
|
||||
const SpaceSettingsGeneralTab = ({ matrixClient: cli, space, onFinished }: IProps) => {
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const userId = cli.getUserId();
|
||||
|
||||
const [newAvatar, setNewAvatar] = useState<File>(null); // undefined means to remove avatar
|
||||
const canSetAvatar = space.currentState.maySendStateEvent(EventType.RoomAvatar, userId);
|
||||
const avatarChanged = newAvatar !== null;
|
||||
|
||||
const [name, setName] = useState<string>(space.name);
|
||||
const canSetName = space.currentState.maySendStateEvent(EventType.RoomName, userId);
|
||||
const nameChanged = name !== space.name;
|
||||
|
||||
const currentTopic = getTopic(space);
|
||||
const [topic, setTopic] = useState<string>(currentTopic);
|
||||
const canSetTopic = space.currentState.maySendStateEvent(EventType.RoomTopic, userId);
|
||||
const topicChanged = topic !== currentTopic;
|
||||
|
||||
const onCancel = () => {
|
||||
setNewAvatar(null);
|
||||
setName(space.name);
|
||||
setTopic(currentTopic);
|
||||
};
|
||||
|
||||
const onSave = async () => {
|
||||
setBusy(true);
|
||||
const promises = [];
|
||||
|
||||
if (avatarChanged) {
|
||||
if (newAvatar) {
|
||||
promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, {
|
||||
url: await cli.uploadContent(newAvatar),
|
||||
}, ""));
|
||||
} else {
|
||||
promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, {}, ""));
|
||||
}
|
||||
}
|
||||
|
||||
if (nameChanged) {
|
||||
promises.push(cli.setRoomName(space.roomId, name));
|
||||
}
|
||||
|
||||
if (topicChanged) {
|
||||
promises.push(cli.setRoomTopic(space.roomId, topic));
|
||||
}
|
||||
|
||||
const results = await Promise.allSettled(promises);
|
||||
setBusy(false);
|
||||
const failures = results.filter(r => r.status === "rejected");
|
||||
if (failures.length > 0) {
|
||||
console.error("Failed to save space settings: ", failures);
|
||||
setError(_t("Failed to save space settings."));
|
||||
}
|
||||
};
|
||||
|
||||
return <div className="mx_SettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{ _t("General") }</div>
|
||||
|
||||
<div>{ _t("Edit settings relating to your space.") }</div>
|
||||
|
||||
{ error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
|
||||
|
||||
<SpaceFeedbackPrompt onClick={() => onFinished(false)} />
|
||||
|
||||
<div className="mx_SettingsTab_section">
|
||||
<SpaceBasicSettings
|
||||
avatarUrl={avatarUrlForRoom(space, 80, 80, "crop")}
|
||||
avatarDisabled={busy || !canSetAvatar}
|
||||
setAvatar={setNewAvatar}
|
||||
name={name}
|
||||
nameDisabled={busy || !canSetName}
|
||||
setName={setName}
|
||||
topic={topic}
|
||||
topicDisabled={busy || !canSetTopic}
|
||||
setTopic={setTopic}
|
||||
/>
|
||||
|
||||
<AccessibleButton
|
||||
onClick={onCancel}
|
||||
disabled={busy || !(avatarChanged || nameChanged || topicChanged)}
|
||||
kind="link"
|
||||
>
|
||||
{ _t("Cancel") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton onClick={onSave} disabled={busy} kind="primary">
|
||||
{ busy ? _t("Saving...") : _t("Save Changes") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
|
||||
<span className="mx_SettingsTab_subheading">{_t("Leave Space")}</span>
|
||||
<div className="mx_SettingsTab_section mx_SettingsTab_subsectionText">
|
||||
<AccessibleButton
|
||||
kind="danger"
|
||||
onClick={() => {
|
||||
defaultDispatcher.dispatch({
|
||||
action: "leave_room",
|
||||
room_id: space.roomId,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{ _t("Leave Space") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default SpaceSettingsGeneralTab;
|
187
src/components/views/spaces/SpaceSettingsVisibilityTab.tsx
Normal file
187
src/components/views/spaces/SpaceSettingsVisibilityTab.tsx
Normal file
|
@ -0,0 +1,187 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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, { useState } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import AliasSettings from "../room_settings/AliasSettings";
|
||||
import { useStateToggle } from "../../../hooks/useStateToggle";
|
||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
||||
import { GuestAccess, HistoryVisibility, JoinRule } from "../settings/tabs/room/SecurityRoomSettingsTab";
|
||||
import StyledRadioGroup from "../elements/StyledRadioGroup";
|
||||
|
||||
interface IProps {
|
||||
matrixClient: MatrixClient;
|
||||
space: Room;
|
||||
}
|
||||
|
||||
enum SpaceVisibility {
|
||||
Unlisted = "unlisted",
|
||||
Private = "private",
|
||||
}
|
||||
|
||||
const useLocalEcho = <T extends any>(
|
||||
currentFactory: () => T,
|
||||
setterFn: (value: T) => Promise<void>,
|
||||
errorFn: (error: Error) => void,
|
||||
): [value: T, handler: (value: T) => void] => {
|
||||
const [value, setValue] = useState(currentFactory);
|
||||
const handler = async (value: T) => {
|
||||
setValue(value);
|
||||
try {
|
||||
await setterFn(value);
|
||||
} catch (e) {
|
||||
setValue(currentFactory());
|
||||
errorFn(e);
|
||||
}
|
||||
};
|
||||
|
||||
return [value, handler];
|
||||
};
|
||||
|
||||
const SpaceSettingsVisibilityTab = ({ matrixClient: cli, space }: IProps) => {
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const userId = cli.getUserId();
|
||||
|
||||
const [visibility, setVisibility] = useLocalEcho<SpaceVisibility>(
|
||||
() => space.getJoinRule() === JoinRule.Private ? SpaceVisibility.Private : SpaceVisibility.Unlisted,
|
||||
visibility => cli.sendStateEvent(space.roomId, EventType.RoomJoinRules, {
|
||||
join_rule: visibility === SpaceVisibility.Unlisted ? JoinRule.Public : JoinRule.Private,
|
||||
}, ""),
|
||||
() => setError(_t("Failed to update the visibility of this space")),
|
||||
);
|
||||
const [guestAccessEnabled, setGuestAccessEnabled] = useLocalEcho<boolean>(
|
||||
() => space.currentState.getStateEvents(EventType.RoomGuestAccess, "")
|
||||
?.getContent()?.guest_access === GuestAccess.CanJoin,
|
||||
guestAccessEnabled => cli.sendStateEvent(space.roomId, EventType.RoomGuestAccess, {
|
||||
guest_access: guestAccessEnabled ? GuestAccess.CanJoin : GuestAccess.Forbidden,
|
||||
}, ""),
|
||||
() => setError(_t("Failed to update the guest access of this space")),
|
||||
);
|
||||
const [historyVisibility, setHistoryVisibility] = useLocalEcho<HistoryVisibility>(
|
||||
() => space.currentState.getStateEvents(EventType.RoomHistoryVisibility, "")
|
||||
?.getContent()?.history_visibility || HistoryVisibility.Shared,
|
||||
historyVisibility => cli.sendStateEvent(space.roomId, EventType.RoomHistoryVisibility, {
|
||||
history_visibility: historyVisibility,
|
||||
}, ""),
|
||||
() => setError(_t("Failed to update the history visibility of this space")),
|
||||
);
|
||||
|
||||
const [showAdvancedSection, toggleAdvancedSection] = useStateToggle();
|
||||
|
||||
const canSetJoinRule = space.currentState.maySendStateEvent(EventType.RoomJoinRules, userId);
|
||||
const canSetGuestAccess = space.currentState.maySendStateEvent(EventType.RoomGuestAccess, userId);
|
||||
const canSetHistoryVisibility = space.currentState.maySendStateEvent(EventType.RoomHistoryVisibility, userId);
|
||||
const canSetCanonical = space.currentState.mayClientSendStateEvent(EventType.RoomCanonicalAlias, cli);
|
||||
const canonicalAliasEv = space.currentState.getStateEvents(EventType.RoomCanonicalAlias, "");
|
||||
|
||||
let advancedSection;
|
||||
if (showAdvancedSection) {
|
||||
advancedSection = <>
|
||||
<AccessibleButton onClick={toggleAdvancedSection} kind="link" className="mx_SettingsTab_showAdvanced">
|
||||
{ _t("Hide advanced") }
|
||||
</AccessibleButton>
|
||||
|
||||
<LabelledToggleSwitch
|
||||
value={guestAccessEnabled}
|
||||
onChange={setGuestAccessEnabled}
|
||||
disabled={!canSetGuestAccess}
|
||||
label={_t("Enable guest access")}
|
||||
/>
|
||||
<p>
|
||||
{ _t("Guests can join a space without having an account.") }
|
||||
<br />
|
||||
{ _t("This may be useful for public spaces.") }
|
||||
</p>
|
||||
</>;
|
||||
} else {
|
||||
advancedSection = <>
|
||||
<AccessibleButton onClick={toggleAdvancedSection} kind="link" className="mx_SettingsTab_showAdvanced">
|
||||
{ _t("Show advanced") }
|
||||
</AccessibleButton>
|
||||
</>;
|
||||
}
|
||||
|
||||
let addressesSection;
|
||||
if (visibility !== SpaceVisibility.Private) {
|
||||
addressesSection = <>
|
||||
<span className="mx_SettingsTab_subheading">{_t("Address")}</span>
|
||||
<div className="mx_SettingsTab_section mx_SettingsTab_subsectionText">
|
||||
<AliasSettings
|
||||
roomId={space.roomId}
|
||||
canSetCanonicalAlias={canSetCanonical}
|
||||
canSetAliases={true}
|
||||
canonicalAliasEvent={canonicalAliasEv}
|
||||
hidePublishSetting={true}
|
||||
/>
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
|
||||
return <div className="mx_SettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{ _t("Visibility") }</div>
|
||||
|
||||
{ error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
|
||||
|
||||
<div className="mx_SettingsTab_section">
|
||||
<div className="mx_SettingsTab_section_caption">
|
||||
{ _t("Decide who can view and join %(spaceName)s.", { spaceName: space.name }) }
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<StyledRadioGroup
|
||||
name="spaceVisibility"
|
||||
value={visibility}
|
||||
onChange={setVisibility}
|
||||
disabled={!canSetJoinRule}
|
||||
definitions={[
|
||||
{
|
||||
value: SpaceVisibility.Unlisted,
|
||||
label: _t("Public"),
|
||||
description: _t("anyone with the link can view and join"),
|
||||
}, {
|
||||
value: SpaceVisibility.Private,
|
||||
label: _t("Invite only"),
|
||||
description: _t("only invited people can view and join"),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{ advancedSection }
|
||||
|
||||
<LabelledToggleSwitch
|
||||
value={historyVisibility === HistoryVisibility.WorldReadable}
|
||||
onChange={(checked: boolean) => {
|
||||
setHistoryVisibility(checked ? HistoryVisibility.WorldReadable : HistoryVisibility.Shared);
|
||||
}}
|
||||
disabled={!canSetHistoryVisibility}
|
||||
label={_t("Preview Space")}
|
||||
/>
|
||||
<div>{ _t("Allow people to preview your space before they join.") }</div>
|
||||
<b>{ _t("Recommended for public spaces.") }</b>
|
||||
</div>
|
||||
|
||||
{ addressesSection }
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default SpaceSettingsVisibilityTab;
|
Loading…
Add table
Add a link
Reference in a new issue