Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/fix/18891
This commit is contained in:
commit
41676b4a9b
24 changed files with 834 additions and 527 deletions
|
@ -60,7 +60,7 @@ const SpaceSettingsDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFin
|
|||
SpaceSettingsTab.Visibility,
|
||||
_td("Visibility"),
|
||||
"mx_SpaceSettingsDialog_visibilityIcon",
|
||||
<SpaceSettingsVisibilityTab matrixClient={cli} space={space} />,
|
||||
<SpaceSettingsVisibilityTab matrixClient={cli} space={space} closeSettingsFn={onFinished} />,
|
||||
),
|
||||
SettingsStore.getValue(UIFeature.AdvancedSettings)
|
||||
? new Tab(
|
||||
|
|
|
@ -25,6 +25,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
|||
interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> {
|
||||
title: string;
|
||||
tooltip?: React.ReactNode;
|
||||
label?: React.ReactNode;
|
||||
tooltipClassName?: string;
|
||||
forceHide?: boolean;
|
||||
yOffset?: number;
|
||||
|
@ -84,7 +85,8 @@ export default class AccessibleTooltipButton extends React.PureComponent<IToolti
|
|||
aria-label={title}
|
||||
>
|
||||
{ children }
|
||||
{ tip }
|
||||
{ this.props.label }
|
||||
{ (tooltip || title) && tip }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ 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 React, { createRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
|
@ -27,7 +27,12 @@ import { makeRoomPermalink, RoomPermalinkCreator } from '../../../utils/permalin
|
|||
import ContentMessages from '../../../ContentMessages';
|
||||
import E2EIcon from './E2EIcon';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu";
|
||||
import {
|
||||
aboveLeftOf,
|
||||
ContextMenu,
|
||||
useContextMenu,
|
||||
MenuItem,
|
||||
} from "../../structures/ContextMenu";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import ReplyPreview from "./ReplyPreview";
|
||||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
|
@ -45,6 +50,10 @@ import { Action } from "../../../dispatcher/actions";
|
|||
import EditorModel from "../../../editor/model";
|
||||
import EmojiPicker from '../emojipicker/EmojiPicker';
|
||||
import MemberStatusMessageAvatar from "../avatars/MemberStatusMessageAvatar";
|
||||
import UIStore, { UI_EVENTS } from '../../../stores/UIStore';
|
||||
|
||||
let instanceCount = 0;
|
||||
const NARROW_MODE_BREAKPOINT = 500;
|
||||
|
||||
interface IComposerAvatarProps {
|
||||
me: object;
|
||||
|
@ -71,13 +80,19 @@ function SendButton(props: ISendButtonProps) {
|
|||
);
|
||||
}
|
||||
|
||||
const EmojiButton = ({ addEmoji }) => {
|
||||
interface IEmojiButtonProps {
|
||||
addEmoji: (unicode: string) => boolean;
|
||||
menuPosition: any; // TODO: Types
|
||||
narrowMode: boolean;
|
||||
}
|
||||
|
||||
const EmojiButton: React.FC<IEmojiButtonProps> = ({ addEmoji, menuPosition, narrowMode }) => {
|
||||
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
|
||||
|
||||
let contextMenu;
|
||||
if (menuDisplayed) {
|
||||
const buttonRect = button.current.getBoundingClientRect();
|
||||
contextMenu = <ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu} managed={false}>
|
||||
const position = menuPosition ?? aboveLeftOf(button.current.getBoundingClientRect());
|
||||
contextMenu = <ContextMenu {...position} onFinished={closeMenu} managed={false}>
|
||||
<EmojiPicker onChoose={addEmoji} showQuickReactions={true} />
|
||||
</ContextMenu>;
|
||||
}
|
||||
|
@ -93,12 +108,11 @@ const EmojiButton = ({ addEmoji }) => {
|
|||
// TODO: replace ContextMenuTooltipButton with a unified representation of
|
||||
// the header buttons and the right panel buttons
|
||||
return <React.Fragment>
|
||||
<ContextMenuTooltipButton
|
||||
<AccessibleTooltipButton
|
||||
className={className}
|
||||
onClick={openMenu}
|
||||
isExpanded={menuDisplayed}
|
||||
title={_t('Emoji picker')}
|
||||
inputRef={button}
|
||||
title={!narrowMode && _t('Emoji picker')}
|
||||
label={narrowMode && _t("Add emoji")}
|
||||
/>
|
||||
|
||||
{ contextMenu }
|
||||
|
@ -196,6 +210,9 @@ interface IState {
|
|||
haveRecording: boolean;
|
||||
recordingTimeLeftSeconds?: number;
|
||||
me?: RoomMember;
|
||||
narrowMode?: boolean;
|
||||
isMenuOpen: boolean;
|
||||
showStickers: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.rooms.MessageComposer")
|
||||
|
@ -203,6 +220,8 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
private dispatcherRef: string;
|
||||
private messageComposerInput: SendMessageComposer;
|
||||
private voiceRecordingButton: VoiceRecordComposerTile;
|
||||
private ref: React.RefObject<HTMLDivElement> = createRef();
|
||||
private instanceId: number;
|
||||
|
||||
static defaultProps = {
|
||||
replyInThread: false,
|
||||
|
@ -220,15 +239,32 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
isComposerEmpty: true,
|
||||
haveRecording: false,
|
||||
recordingTimeLeftSeconds: null, // when set to a number, shows a toast
|
||||
isMenuOpen: false,
|
||||
showStickers: false,
|
||||
};
|
||||
|
||||
this.instanceId = instanceCount++;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents);
|
||||
this.waitForOwnMember();
|
||||
UIStore.instance.trackElementDimensions(`MessageComposer${this.instanceId}`, this.ref.current);
|
||||
UIStore.instance.on(`MessageComposer${this.instanceId}`, this.onResize);
|
||||
}
|
||||
|
||||
private onResize = (type: UI_EVENTS, entry: ResizeObserverEntry) => {
|
||||
if (type === UI_EVENTS.Resize) {
|
||||
const narrowMode = entry.contentRect.width <= NARROW_MODE_BREAKPOINT;
|
||||
this.setState({
|
||||
narrowMode,
|
||||
isMenuOpen: !narrowMode ? false : this.state.isMenuOpen,
|
||||
showStickers: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
if (payload.action === 'reply_to_event') {
|
||||
// add a timeout for the reply preview to be rendered, so
|
||||
|
@ -263,6 +299,8 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
}
|
||||
VoiceRecordingStore.instance.off(UPDATE_EVENT, this.onVoiceStoreUpdate);
|
||||
dis.unregister(this.dispatcherRef);
|
||||
UIStore.instance.stopTrackingElementDimensions(`MessageComposer${this.instanceId}`);
|
||||
UIStore.instance.removeListener(`MessageComposer${this.instanceId}`, this.onResize);
|
||||
}
|
||||
|
||||
private onRoomStateEvents = (ev, state) => {
|
||||
|
@ -312,7 +350,11 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
|
||||
private renderPlaceholderText = () => {
|
||||
if (this.props.replyToEvent) {
|
||||
if (this.props.e2eStatus) {
|
||||
if (this.props.replyInThread && this.props.e2eStatus) {
|
||||
return _t('Reply to encrypted thread…');
|
||||
} else if (this.props.replyInThread) {
|
||||
return _t('Reply to thread…');
|
||||
} else if (this.props.e2eStatus) {
|
||||
return _t('Send an encrypted reply…');
|
||||
} else {
|
||||
return _t('Send a reply…');
|
||||
|
@ -326,11 +368,12 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private addEmoji(emoji: string) {
|
||||
private addEmoji(emoji: string): boolean {
|
||||
dis.dispatch<ComposerInsertPayload>({
|
||||
action: Action.ComposerInsert,
|
||||
text: emoji,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private sendMessage = async () => {
|
||||
|
@ -369,6 +412,97 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private shouldShowStickerPicker = (): boolean => {
|
||||
return SettingsStore.getValue(UIFeature.Widgets)
|
||||
&& SettingsStore.getValue("MessageComposerInput.showStickersButton")
|
||||
&& !this.state.haveRecording;
|
||||
};
|
||||
|
||||
private showStickers = (showStickers: boolean) => {
|
||||
this.setState({ showStickers });
|
||||
};
|
||||
|
||||
private toggleButtonMenu = (): void => {
|
||||
this.setState({
|
||||
isMenuOpen: !this.state.isMenuOpen,
|
||||
});
|
||||
};
|
||||
|
||||
private renderButtons(menuPosition): JSX.Element | JSX.Element[] {
|
||||
const buttons: JSX.Element[] = [];
|
||||
if (!this.state.haveRecording) {
|
||||
buttons.push(
|
||||
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
|
||||
);
|
||||
buttons.push(
|
||||
<EmojiButton key="emoji_button" addEmoji={this.addEmoji} menuPosition={menuPosition} narrowMode={this.state.narrowMode} />,
|
||||
);
|
||||
}
|
||||
if (this.shouldShowStickerPicker()) {
|
||||
let title;
|
||||
if (!this.state.narrowMode) {
|
||||
title = this.state.showStickers ? _t("Hide Stickers") : _t("Show Stickers");
|
||||
}
|
||||
|
||||
buttons.push(
|
||||
<AccessibleTooltipButton
|
||||
id='stickersButton'
|
||||
key="controls_stickers"
|
||||
className="mx_MessageComposer_button mx_MessageComposer_stickers"
|
||||
onClick={() => this.showStickers(!this.state.showStickers)}
|
||||
title={title}
|
||||
label={this.state.narrowMode && _t("Send a sticker")}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
if (!this.state.haveRecording && !this.state.narrowMode) {
|
||||
buttons.push(
|
||||
<AccessibleTooltipButton
|
||||
className="mx_MessageComposer_button mx_MessageComposer_voiceMessage"
|
||||
onClick={() => this.voiceRecordingButton?.onRecordStartEndClick()}
|
||||
title={_t("Send voice message")}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.state.narrowMode) {
|
||||
return buttons;
|
||||
} else {
|
||||
const classnames = classNames({
|
||||
mx_MessageComposer_button: true,
|
||||
mx_MessageComposer_buttonMenu: true,
|
||||
mx_MessageComposer_closeButtonMenu: this.state.isMenuOpen,
|
||||
});
|
||||
|
||||
return <>
|
||||
{ buttons[0] }
|
||||
<AccessibleTooltipButton
|
||||
className={classnames}
|
||||
onClick={this.toggleButtonMenu}
|
||||
title={_t("More options")}
|
||||
tooltip={false}
|
||||
/>
|
||||
{ this.state.isMenuOpen && (
|
||||
<ContextMenu
|
||||
onFinished={this.toggleButtonMenu}
|
||||
{...menuPosition}
|
||||
menuPaddingRight={10}
|
||||
menuPaddingTop={5}
|
||||
menuPaddingBottom={5}
|
||||
menuWidth={150}
|
||||
wrapperClassName="mx_MessageComposer_Menu"
|
||||
>
|
||||
{ buttons.slice(1).map((button, index) => (
|
||||
<MenuItem className="mx_CallContextMenu_item" key={index} onClick={this.toggleButtonMenu}>
|
||||
{ button }
|
||||
</MenuItem>
|
||||
)) }
|
||||
</ContextMenu>
|
||||
) }
|
||||
</>;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const controls = [
|
||||
this.state.me && !this.props.compact ? <ComposerAvatar key="controls_avatar" me={this.state.me} /> : null,
|
||||
|
@ -377,6 +511,12 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
null,
|
||||
];
|
||||
|
||||
let menuPosition;
|
||||
if (this.ref.current) {
|
||||
const contentRect = this.ref.current.getBoundingClientRect();
|
||||
menuPosition = aboveLeftOf(contentRect);
|
||||
}
|
||||
|
||||
if (!this.state.tombstone && this.state.canSendMessages) {
|
||||
controls.push(
|
||||
<SendMessageComposer
|
||||
|
@ -392,33 +532,10 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
/>,
|
||||
);
|
||||
|
||||
if (!this.state.haveRecording) {
|
||||
controls.push(
|
||||
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
|
||||
<EmojiButton key="emoji_button" addEmoji={this.addEmoji} />,
|
||||
);
|
||||
}
|
||||
|
||||
if (SettingsStore.getValue(UIFeature.Widgets) &&
|
||||
SettingsStore.getValue("MessageComposerInput.showStickersButton") &&
|
||||
!this.state.haveRecording) {
|
||||
controls.push(<Stickerpicker key="stickerpicker_controls_button" room={this.props.room} />);
|
||||
}
|
||||
|
||||
controls.push(<VoiceRecordComposerTile
|
||||
key="controls_voice_record"
|
||||
ref={c => this.voiceRecordingButton = c}
|
||||
room={this.props.room} />);
|
||||
|
||||
if (!this.state.isComposerEmpty || this.state.haveRecording) {
|
||||
controls.push(
|
||||
<SendButton
|
||||
key="controls_send"
|
||||
onClick={this.sendMessage}
|
||||
title={this.state.haveRecording ? _t("Send voice message") : undefined}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
} else if (this.state.tombstone) {
|
||||
const replacementRoomId = this.state.tombstone.getContent()['replacement_room'];
|
||||
|
||||
|
@ -459,6 +576,15 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
yOffset={-50}
|
||||
/>;
|
||||
}
|
||||
controls.push(
|
||||
<Stickerpicker
|
||||
room={this.props.room}
|
||||
showStickers={this.state.showStickers}
|
||||
setShowStickers={this.showStickers}
|
||||
menuPosition={menuPosition} />,
|
||||
);
|
||||
|
||||
const showSendButton = !this.state.isComposerEmpty || this.state.haveRecording;
|
||||
|
||||
const classes = classNames({
|
||||
"mx_MessageComposer": true,
|
||||
|
@ -467,7 +593,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
});
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className={classes} ref={this.ref}>
|
||||
{ recordingTooltip }
|
||||
<div className="mx_MessageComposer_wrapper">
|
||||
{ this.props.showReplyPreview && (
|
||||
|
@ -475,6 +601,14 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
) }
|
||||
<div className="mx_MessageComposer_row">
|
||||
{ controls }
|
||||
{ this.renderButtons(menuPosition) }
|
||||
{ showSendButton && (
|
||||
<SendButton
|
||||
key="controls_send"
|
||||
onClick={this.sendMessage}
|
||||
title={this.state.haveRecording ? _t("Send voice message") : undefined}
|
||||
/>
|
||||
) }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -54,6 +54,7 @@ import { Room } from 'matrix-js-sdk/src/models/room';
|
|||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
import { decorateStartSendingTime, sendRoundTripMetric } from "../../../sendTimePerformanceMetrics";
|
||||
|
||||
function addReplyToMessageContent(
|
||||
content: IContent,
|
||||
|
@ -418,6 +419,10 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
|||
// don't bother sending an empty message
|
||||
if (!content.body.trim()) return;
|
||||
|
||||
if (SettingsStore.getValue("Performance.addSendMessageTimingMetadata")) {
|
||||
decorateStartSendingTime(content);
|
||||
}
|
||||
|
||||
const prom = this.context.sendMessage(roomId, content);
|
||||
if (replyToEvent) {
|
||||
// Clear reply_to_event as we put the message into the queue
|
||||
|
@ -433,6 +438,11 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
|||
dis.dispatch({ action: `effects.${effect.command}` });
|
||||
}
|
||||
});
|
||||
if (SettingsStore.getValue("Performance.addSendMessageTimingMetadata")) {
|
||||
prom.then(resp => {
|
||||
sendRoundTripMetric(this.context, roomId, resp.event_id);
|
||||
});
|
||||
}
|
||||
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, false, !!replyToEvent, content);
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import AppTile from '../elements/AppTile';
|
||||
|
@ -27,7 +26,6 @@ import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
|
|||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { ChevronFace, ContextMenu } from "../../structures/ContextMenu";
|
||||
import { WidgetType } from "../../../widgets/WidgetType";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
@ -44,10 +42,12 @@ const PERSISTED_ELEMENT_KEY = "stickerPicker";
|
|||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
showStickers: boolean;
|
||||
menuPosition?: any;
|
||||
setShowStickers: (showStickers: boolean) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
showStickers: boolean;
|
||||
imError: string;
|
||||
stickerpickerX: number;
|
||||
stickerpickerY: number;
|
||||
|
@ -72,7 +72,6 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
|||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
showStickers: false,
|
||||
imError: null,
|
||||
stickerpickerX: null,
|
||||
stickerpickerY: null,
|
||||
|
@ -114,7 +113,7 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
|||
console.warn('No widget ID specified, not disabling assets');
|
||||
}
|
||||
|
||||
this.setState({ showStickers: false });
|
||||
this.props.setShowStickers(false);
|
||||
WidgetUtils.removeStickerpickerWidgets().then(() => {
|
||||
this.forceUpdate();
|
||||
}).catch((e) => {
|
||||
|
@ -146,15 +145,15 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
public componentDidUpdate(prevProps: IProps, prevState: IState): void {
|
||||
this.sendVisibilityToWidget(this.state.showStickers);
|
||||
this.sendVisibilityToWidget(this.props.showStickers);
|
||||
}
|
||||
|
||||
private imError(errorMsg: string, e: Error): void {
|
||||
console.error(errorMsg, e);
|
||||
this.setState({
|
||||
showStickers: false,
|
||||
imError: _t(errorMsg),
|
||||
});
|
||||
this.props.setShowStickers(false);
|
||||
}
|
||||
|
||||
private updateWidget = (): void => {
|
||||
|
@ -194,12 +193,12 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
|||
this.forceUpdate();
|
||||
break;
|
||||
case "stickerpicker_close":
|
||||
this.setState({ showStickers: false });
|
||||
this.props.setShowStickers(false);
|
||||
break;
|
||||
case Action.AfterRightPanelPhaseChange:
|
||||
case "show_left_panel":
|
||||
case "hide_left_panel":
|
||||
this.setState({ showStickers: false });
|
||||
this.props.setShowStickers(false);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
@ -338,8 +337,8 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
|||
|
||||
const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19;
|
||||
|
||||
this.props.setShowStickers(true);
|
||||
this.setState({
|
||||
showStickers: true,
|
||||
stickerpickerX: x,
|
||||
stickerpickerY: y,
|
||||
stickerpickerChevronOffset,
|
||||
|
@ -351,8 +350,8 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
|||
* @param {Event} ev Event that triggered the function call
|
||||
*/
|
||||
private onHideStickersClick = (ev: React.MouseEvent): void => {
|
||||
if (this.state.showStickers) {
|
||||
this.setState({ showStickers: false });
|
||||
if (this.props.showStickers) {
|
||||
this.props.setShowStickers(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -360,8 +359,8 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
|||
* Called when the window is resized
|
||||
*/
|
||||
private onResize = (): void => {
|
||||
if (this.state.showStickers) {
|
||||
this.setState({ showStickers: false });
|
||||
if (this.props.showStickers) {
|
||||
this.props.setShowStickers(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -369,8 +368,8 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
|||
* The stickers picker was hidden
|
||||
*/
|
||||
private onFinished = (): void => {
|
||||
if (this.state.showStickers) {
|
||||
this.setState({ showStickers: false });
|
||||
if (this.props.showStickers) {
|
||||
this.props.setShowStickers(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -395,54 +394,23 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
|||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
let stickerPicker;
|
||||
let stickersButton;
|
||||
const className = classNames(
|
||||
"mx_MessageComposer_button",
|
||||
"mx_MessageComposer_stickers",
|
||||
"mx_Stickers_hideStickers",
|
||||
"mx_MessageComposer_button_highlight",
|
||||
);
|
||||
if (this.state.showStickers) {
|
||||
// Show hide-stickers button
|
||||
stickersButton =
|
||||
<AccessibleButton
|
||||
id='stickersButton'
|
||||
key="controls_hide_stickers"
|
||||
className={className}
|
||||
onClick={this.onHideStickersClick}
|
||||
title={_t("Hide Stickers")}
|
||||
/>;
|
||||
if (!this.props.showStickers) return null;
|
||||
|
||||
stickerPicker = <ContextMenu
|
||||
chevronOffset={this.state.stickerpickerChevronOffset}
|
||||
chevronFace={ChevronFace.Bottom}
|
||||
left={this.state.stickerpickerX}
|
||||
top={this.state.stickerpickerY}
|
||||
menuWidth={this.popoverWidth}
|
||||
menuHeight={this.popoverHeight}
|
||||
onFinished={this.onFinished}
|
||||
menuPaddingTop={0}
|
||||
menuPaddingLeft={0}
|
||||
menuPaddingRight={0}
|
||||
zIndex={STICKERPICKER_Z_INDEX}
|
||||
>
|
||||
<GenericElementContextMenu element={this.getStickerpickerContent()} onResize={this.onFinished} />
|
||||
</ContextMenu>;
|
||||
} else {
|
||||
// Show show-stickers button
|
||||
stickersButton =
|
||||
<AccessibleTooltipButton
|
||||
id='stickersButton'
|
||||
key="controls_show_stickers"
|
||||
className="mx_MessageComposer_button mx_MessageComposer_stickers"
|
||||
onClick={this.onShowStickersClick}
|
||||
title={_t("Show Stickers")}
|
||||
/>;
|
||||
}
|
||||
return <React.Fragment>
|
||||
{ stickersButton }
|
||||
{ stickerPicker }
|
||||
</React.Fragment>;
|
||||
return <ContextMenu
|
||||
chevronOffset={this.state.stickerpickerChevronOffset}
|
||||
chevronFace={ChevronFace.Bottom}
|
||||
left={this.state.stickerpickerX}
|
||||
top={this.state.stickerpickerY}
|
||||
menuWidth={this.popoverWidth}
|
||||
menuHeight={this.popoverHeight}
|
||||
onFinished={this.onFinished}
|
||||
menuPaddingTop={0}
|
||||
menuPaddingLeft={0}
|
||||
menuPaddingRight={0}
|
||||
zIndex={STICKERPICKER_Z_INDEX}
|
||||
{...this.props.menuPosition}
|
||||
>
|
||||
<GenericElementContextMenu element={this.getStickerpickerContent()} onResize={this.onFinished} />
|
||||
</ContextMenu>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,6 @@ import React, { ReactNode } from "react";
|
|||
import { IUpload, RecordingState, VoiceRecording } from "../../../audio/VoiceRecording";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import classNames from "classnames";
|
||||
import LiveRecordingWaveform from "../audio_messages/LiveRecordingWaveform";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import LiveRecordingClock from "../audio_messages/LiveRecordingClock";
|
||||
|
@ -137,7 +136,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
await this.disposeRecording();
|
||||
};
|
||||
|
||||
private onRecordStartEndClick = async () => {
|
||||
public onRecordStartEndClick = async () => {
|
||||
if (this.state.recorder) {
|
||||
await this.state.recorder.stop();
|
||||
return;
|
||||
|
@ -215,27 +214,23 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
}
|
||||
|
||||
public render(): ReactNode {
|
||||
let stopOrRecordBtn;
|
||||
let deleteButton;
|
||||
if (!this.state.recordingPhase || this.state.recordingPhase === RecordingState.Started) {
|
||||
const classes = classNames({
|
||||
'mx_MessageComposer_button': !this.state.recorder,
|
||||
'mx_MessageComposer_voiceMessage': !this.state.recorder,
|
||||
'mx_VoiceRecordComposerTile_stop': this.state.recorder?.isRecording,
|
||||
});
|
||||
if (!this.state.recordingPhase) return null;
|
||||
|
||||
let stopBtn;
|
||||
let deleteButton;
|
||||
if (this.state.recordingPhase === RecordingState.Started) {
|
||||
let tooltip = _t("Send voice message");
|
||||
if (!!this.state.recorder) {
|
||||
tooltip = _t("Stop recording");
|
||||
}
|
||||
|
||||
stopOrRecordBtn = <AccessibleTooltipButton
|
||||
className={classes}
|
||||
stopBtn = <AccessibleTooltipButton
|
||||
className="mx_VoiceRecordComposerTile_stop"
|
||||
onClick={this.onRecordStartEndClick}
|
||||
title={tooltip}
|
||||
/>;
|
||||
if (this.state.recorder && !this.state.recorder?.isRecording) {
|
||||
stopOrRecordBtn = null;
|
||||
stopBtn = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -264,13 +259,10 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
</span>;
|
||||
}
|
||||
|
||||
// The record button (mic icon) is meant to be on the right edge, but we also want the
|
||||
// stop button to be left of the waveform area. Luckily, none of the surrounding UI is
|
||||
// rendered when we're not recording, so the record button ends up in the correct spot.
|
||||
return (<>
|
||||
{ uploadIndicator }
|
||||
{ deleteButton }
|
||||
{ stopOrRecordBtn }
|
||||
{ stopBtn }
|
||||
{ this.renderWaveformArea() }
|
||||
</>);
|
||||
}
|
||||
|
|
269
src/components/views/settings/JoinRuleSettings.tsx
Normal file
269
src/components/views/settings/JoinRuleSettings.tsx
Normal file
|
@ -0,0 +1,269 @@
|
|||
/*
|
||||
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 from "react";
|
||||
import { IJoinRuleEventContent, JoinRule, RestrictedAllowType } from "matrix-js-sdk/src/@types/partials";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
|
||||
import StyledRadioGroup, { IDefinition } from "../elements/StyledRadioGroup";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import SpaceStore from "../../../stores/SpaceStore";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import Modal from "../../../Modal";
|
||||
import ManageRestrictedJoinRuleDialog from "../dialogs/ManageRestrictedJoinRuleDialog";
|
||||
import RoomUpgradeWarningDialog from "../dialogs/RoomUpgradeWarningDialog";
|
||||
import { upgradeRoom } from "../../../utils/RoomUpgrade";
|
||||
import { arrayHasDiff } from "../../../utils/arrays";
|
||||
import { useLocalEcho } from "../../../hooks/useLocalEcho";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import { ROOM_SECURITY_TAB } from "../dialogs/RoomSettingsDialog";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
promptUpgrade?: boolean;
|
||||
closeSettingsFn(): void;
|
||||
onError(error: Error): void;
|
||||
beforeChange?(joinRule: JoinRule): Promise<boolean>; // if returns false then aborts the change
|
||||
}
|
||||
|
||||
const JoinRuleSettings = ({ room, promptUpgrade, onError, beforeChange, closeSettingsFn }: IProps) => {
|
||||
const cli = room.client;
|
||||
|
||||
const restrictedRoomCapabilities = SpaceStore.instance.restrictedJoinRuleSupport;
|
||||
const roomSupportsRestricted = Array.isArray(restrictedRoomCapabilities?.support)
|
||||
&& restrictedRoomCapabilities.support.includes(room.getVersion());
|
||||
const preferredRestrictionVersion = !roomSupportsRestricted && promptUpgrade
|
||||
? restrictedRoomCapabilities?.preferred
|
||||
: undefined;
|
||||
|
||||
const disabled = !room.currentState.mayClientSendStateEvent(EventType.RoomJoinRules, cli);
|
||||
|
||||
const [content, setContent] = useLocalEcho<IJoinRuleEventContent>(
|
||||
() => room.currentState.getStateEvents(EventType.RoomJoinRules, "")?.getContent(),
|
||||
content => cli.sendStateEvent(room.roomId, EventType.RoomJoinRules, content, ""),
|
||||
onError,
|
||||
);
|
||||
|
||||
const { join_rule: joinRule } = content;
|
||||
const restrictedAllowRoomIds = joinRule === JoinRule.Restricted
|
||||
? content.allow.filter(o => o.type === RestrictedAllowType.RoomMembership).map(o => o.room_id)
|
||||
: undefined;
|
||||
|
||||
const editRestrictedRoomIds = async (): Promise<string[] | undefined> => {
|
||||
let selected = restrictedAllowRoomIds;
|
||||
if (!selected?.length && SpaceStore.instance.activeSpace) {
|
||||
selected = [SpaceStore.instance.activeSpace.roomId];
|
||||
}
|
||||
|
||||
const matrixClient = MatrixClientPeg.get();
|
||||
const { finished } = Modal.createTrackedDialog('Edit restricted', '', ManageRestrictedJoinRuleDialog, {
|
||||
matrixClient,
|
||||
room,
|
||||
selected,
|
||||
}, "mx_ManageRestrictedJoinRuleDialog_wrapper");
|
||||
|
||||
const [roomIds] = await finished;
|
||||
return roomIds;
|
||||
};
|
||||
|
||||
const definitions: IDefinition<JoinRule>[] = [{
|
||||
value: JoinRule.Invite,
|
||||
label: _t("Private (invite only)"),
|
||||
description: _t("Only invited people can join."),
|
||||
checked: joinRule === JoinRule.Invite || (joinRule === JoinRule.Restricted && !restrictedAllowRoomIds?.length),
|
||||
}, {
|
||||
value: JoinRule.Public,
|
||||
label: _t("Public"),
|
||||
description: _t("Anyone can find and join."),
|
||||
}];
|
||||
|
||||
if (roomSupportsRestricted || preferredRestrictionVersion || joinRule === JoinRule.Restricted) {
|
||||
let upgradeRequiredPill;
|
||||
if (preferredRestrictionVersion) {
|
||||
upgradeRequiredPill = <span className="mx_SecurityRoomSettingsTab_upgradeRequired">
|
||||
{ _t("Upgrade required") }
|
||||
</span>;
|
||||
}
|
||||
|
||||
let description;
|
||||
if (joinRule === JoinRule.Restricted && restrictedAllowRoomIds?.length) {
|
||||
// only show the first 4 spaces we know about, so that the UI doesn't grow out of proportion there are lots.
|
||||
const shownSpaces = restrictedAllowRoomIds
|
||||
.map(roomId => cli.getRoom(roomId))
|
||||
.filter(room => room?.isSpaceRoom())
|
||||
.slice(0, 4);
|
||||
|
||||
let moreText;
|
||||
if (shownSpaces.length < restrictedAllowRoomIds.length) {
|
||||
if (shownSpaces.length > 0) {
|
||||
moreText = _t("& %(count)s more", {
|
||||
count: restrictedAllowRoomIds.length - shownSpaces.length,
|
||||
});
|
||||
} else {
|
||||
moreText = _t("Currently, %(count)s spaces have access", {
|
||||
count: restrictedAllowRoomIds.length,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const onRestrictedRoomIdsChange = (newAllowRoomIds: string[]) => {
|
||||
if (!arrayHasDiff(restrictedAllowRoomIds || [], newAllowRoomIds)) return;
|
||||
|
||||
if (!newAllowRoomIds.length) {
|
||||
setContent({
|
||||
join_rule: JoinRule.Invite,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setContent({
|
||||
join_rule: JoinRule.Restricted,
|
||||
allow: newAllowRoomIds.map(roomId => ({
|
||||
"type": RestrictedAllowType.RoomMembership,
|
||||
"room_id": roomId,
|
||||
})),
|
||||
});
|
||||
};
|
||||
|
||||
const onEditRestrictedClick = async () => {
|
||||
const restrictedAllowRoomIds = await editRestrictedRoomIds();
|
||||
if (!Array.isArray(restrictedAllowRoomIds)) return;
|
||||
if (restrictedAllowRoomIds.length > 0) {
|
||||
onRestrictedRoomIdsChange(restrictedAllowRoomIds);
|
||||
} else {
|
||||
onChange(JoinRule.Invite);
|
||||
}
|
||||
};
|
||||
|
||||
description = <div>
|
||||
<span>
|
||||
{ _t("Anyone in a space can find and join. <a>Edit which spaces can access here.</a>", {}, {
|
||||
a: sub => <AccessibleButton
|
||||
disabled={disabled}
|
||||
onClick={onEditRestrictedClick}
|
||||
kind="link"
|
||||
>
|
||||
{ sub }
|
||||
</AccessibleButton>,
|
||||
}) }
|
||||
</span>
|
||||
|
||||
<div className="mx_SecurityRoomSettingsTab_spacesWithAccess">
|
||||
<h4>{ _t("Spaces with access") }</h4>
|
||||
{ shownSpaces.map(room => {
|
||||
return <span key={room.roomId}>
|
||||
<RoomAvatar room={room} height={32} width={32} />
|
||||
{ room.name }
|
||||
</span>;
|
||||
}) }
|
||||
{ moreText && <span>{ moreText }</span> }
|
||||
</div>
|
||||
</div>;
|
||||
} else if (SpaceStore.instance.activeSpace) {
|
||||
description = _t("Anyone in <spaceName/> can find and join. You can select other spaces too.", {}, {
|
||||
spaceName: () => <b>{ SpaceStore.instance.activeSpace.name }</b>,
|
||||
});
|
||||
} else {
|
||||
description = _t("Anyone in a space can find and join. You can select multiple spaces.");
|
||||
}
|
||||
|
||||
definitions.splice(1, 0, {
|
||||
value: JoinRule.Restricted,
|
||||
label: <>
|
||||
{ _t("Space members") }
|
||||
{ upgradeRequiredPill }
|
||||
</>,
|
||||
description,
|
||||
// if there are 0 allowed spaces then render it as invite only instead
|
||||
checked: joinRule === JoinRule.Restricted && !!restrictedAllowRoomIds?.length,
|
||||
});
|
||||
}
|
||||
|
||||
const onChange = async (joinRule: JoinRule) => {
|
||||
const beforeJoinRule = content.join_rule;
|
||||
|
||||
let restrictedAllowRoomIds: string[];
|
||||
if (joinRule === JoinRule.Restricted) {
|
||||
if (beforeJoinRule === JoinRule.Restricted || roomSupportsRestricted) {
|
||||
// Have the user pick which spaces to allow joins from
|
||||
restrictedAllowRoomIds = await editRestrictedRoomIds();
|
||||
if (!Array.isArray(restrictedAllowRoomIds)) return;
|
||||
} else if (preferredRestrictionVersion) {
|
||||
// Block this action on a room upgrade otherwise it'd make their room unjoinable
|
||||
const targetVersion = preferredRestrictionVersion;
|
||||
Modal.createTrackedDialog('Restricted join rule upgrade', '', RoomUpgradeWarningDialog, {
|
||||
roomId: room.roomId,
|
||||
targetVersion,
|
||||
description: _t("This upgrade will allow members of selected spaces " +
|
||||
"access to this room without an invite."),
|
||||
onFinished: async (resp) => {
|
||||
if (!resp?.continue) return;
|
||||
const roomId = await upgradeRoom(room, targetVersion, resp.invite, true, true, true);
|
||||
closeSettingsFn();
|
||||
// switch to the new room in the background
|
||||
dis.dispatch({
|
||||
action: "view_room",
|
||||
room_id: roomId,
|
||||
});
|
||||
// open new settings on this tab
|
||||
dis.dispatch({
|
||||
action: "open_room_settings",
|
||||
initial_tab_id: ROOM_SECURITY_TAB,
|
||||
});
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// when setting to 0 allowed rooms/spaces set to invite only instead as per the note
|
||||
if (!restrictedAllowRoomIds.length) {
|
||||
joinRule = JoinRule.Invite;
|
||||
}
|
||||
}
|
||||
|
||||
if (beforeJoinRule === joinRule && !restrictedAllowRoomIds) return;
|
||||
if (beforeChange && !await beforeChange(joinRule)) return;
|
||||
|
||||
const newContent: IJoinRuleEventContent = {
|
||||
join_rule: joinRule,
|
||||
};
|
||||
|
||||
// pre-set the accepted spaces with the currently viewed one as per the microcopy
|
||||
if (joinRule === JoinRule.Restricted) {
|
||||
newContent.allow = restrictedAllowRoomIds.map(roomId => ({
|
||||
"type": RestrictedAllowType.RoomMembership,
|
||||
"room_id": roomId,
|
||||
}));
|
||||
}
|
||||
|
||||
setContent(newContent);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledRadioGroup
|
||||
name="joinRule"
|
||||
value={joinRule}
|
||||
onChange={onChange}
|
||||
definitions={definitions}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default JoinRuleSettings;
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import { GuestAccess, HistoryVisibility, JoinRule, RestrictedAllowType } from "matrix-js-sdk/src/@types/partials";
|
||||
import { IContent, MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { EventType } from 'matrix-js-sdk/src/@types/event';
|
||||
|
||||
import { _t } from "../../../../../languageHandler";
|
||||
|
@ -24,23 +24,17 @@ import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
|
|||
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
||||
import Modal from "../../../../../Modal";
|
||||
import QuestionDialog from "../../../dialogs/QuestionDialog";
|
||||
import StyledRadioGroup, { IDefinition } from '../../../elements/StyledRadioGroup';
|
||||
import StyledRadioGroup from '../../../elements/StyledRadioGroup';
|
||||
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||
import { UIFeature } from "../../../../../settings/UIFeature";
|
||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
import SpaceStore from "../../../../../stores/SpaceStore";
|
||||
import RoomAvatar from "../../../avatars/RoomAvatar";
|
||||
import ManageRestrictedJoinRuleDialog from '../../../dialogs/ManageRestrictedJoinRuleDialog';
|
||||
import RoomUpgradeWarningDialog from '../../../dialogs/RoomUpgradeWarningDialog';
|
||||
import { upgradeRoom } from "../../../../../utils/RoomUpgrade";
|
||||
import { arrayHasDiff } from "../../../../../utils/arrays";
|
||||
import SettingsFlag from '../../../elements/SettingsFlag';
|
||||
import createRoom, { IOpts } from '../../../../../createRoom';
|
||||
import CreateRoomDialog from '../../../dialogs/CreateRoomDialog';
|
||||
import dis from "../../../../../dispatcher/dispatcher";
|
||||
import { ROOM_SECURITY_TAB } from "../../../dialogs/RoomSettingsDialog";
|
||||
import JoinRuleSettings from "../../JoinRuleSettings";
|
||||
import ErrorDialog from "../../../dialogs/ErrorDialog";
|
||||
|
||||
interface IProps {
|
||||
roomId: string;
|
||||
|
@ -48,14 +42,11 @@ interface IProps {
|
|||
}
|
||||
|
||||
interface IState {
|
||||
joinRule: JoinRule;
|
||||
restrictedAllowRoomIds?: string[];
|
||||
guestAccess: GuestAccess;
|
||||
history: HistoryVisibility;
|
||||
hasAliases: boolean;
|
||||
encrypted: boolean;
|
||||
roomSupportsRestricted?: boolean;
|
||||
preferredRestrictionVersion?: string;
|
||||
showAdvancedSection: boolean;
|
||||
}
|
||||
|
||||
|
@ -65,7 +56,6 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||
super(props);
|
||||
|
||||
this.state = {
|
||||
joinRule: JoinRule.Invite,
|
||||
guestAccess: GuestAccess.Forbidden,
|
||||
history: HistoryVisibility.Shared,
|
||||
hasAliases: false,
|
||||
|
@ -106,12 +96,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||
);
|
||||
|
||||
const encrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.roomId);
|
||||
const restrictedRoomCapabilities = SpaceStore.instance.restrictedJoinRuleSupport;
|
||||
const roomSupportsRestricted = Array.isArray(restrictedRoomCapabilities?.support)
|
||||
&& restrictedRoomCapabilities.support.includes(room.getVersion());
|
||||
const preferredRestrictionVersion = roomSupportsRestricted ? undefined : restrictedRoomCapabilities?.preferred;
|
||||
this.setState({ joinRule, restrictedAllowRoomIds, guestAccess, history, encrypted,
|
||||
roomSupportsRestricted, preferredRestrictionVersion });
|
||||
this.setState({ restrictedAllowRoomIds, guestAccess, history, encrypted });
|
||||
|
||||
this.hasAliases().then(hasAliases => this.setState({ hasAliases }));
|
||||
}
|
||||
|
@ -135,7 +120,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||
};
|
||||
|
||||
private onEncryptionChange = async () => {
|
||||
if (this.state.joinRule == "public") {
|
||||
if (MatrixClientPeg.get().getRoom(this.props.roomId)?.getJoinRule() === JoinRule.Public) {
|
||||
const dialog = Modal.createTrackedDialog('Confirm Public Encrypted Room', '', QuestionDialog, {
|
||||
title: _t('Are you sure you want to add encryption to this public room?'),
|
||||
description: <div>
|
||||
|
@ -202,128 +187,6 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||
});
|
||||
};
|
||||
|
||||
private onJoinRuleChange = async (joinRule: JoinRule) => {
|
||||
const beforeJoinRule = this.state.joinRule;
|
||||
|
||||
let restrictedAllowRoomIds: string[];
|
||||
if (joinRule === JoinRule.Restricted) {
|
||||
const matrixClient = MatrixClientPeg.get();
|
||||
const roomId = this.props.roomId;
|
||||
const room = matrixClient.getRoom(roomId);
|
||||
|
||||
if (beforeJoinRule === JoinRule.Restricted || this.state.roomSupportsRestricted) {
|
||||
// Have the user pick which spaces to allow joins from
|
||||
restrictedAllowRoomIds = await this.editRestrictedRoomIds();
|
||||
if (!Array.isArray(restrictedAllowRoomIds)) return;
|
||||
} else if (this.state.preferredRestrictionVersion) {
|
||||
// Block this action on a room upgrade otherwise it'd make their room unjoinable
|
||||
const targetVersion = this.state.preferredRestrictionVersion;
|
||||
Modal.createTrackedDialog('Restricted join rule upgrade', '', RoomUpgradeWarningDialog, {
|
||||
roomId,
|
||||
targetVersion,
|
||||
description: _t("This upgrade will allow members of selected spaces " +
|
||||
"access to this room without an invite."),
|
||||
onFinished: async (resp) => {
|
||||
if (!resp?.continue) return;
|
||||
const roomId = await upgradeRoom(room, targetVersion, resp.invite, true, true, true);
|
||||
this.props.closeSettingsFn();
|
||||
// switch to the new room in the background
|
||||
dis.dispatch({
|
||||
action: "view_room",
|
||||
room_id: roomId,
|
||||
});
|
||||
// open new settings on this tab
|
||||
dis.dispatch({
|
||||
action: "open_room_settings",
|
||||
initial_tab_id: ROOM_SECURITY_TAB,
|
||||
});
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
this.state.encrypted &&
|
||||
this.state.joinRule !== JoinRule.Public &&
|
||||
joinRule === JoinRule.Public
|
||||
) {
|
||||
const dialog = Modal.createTrackedDialog('Confirm Public Encrypted Room', '', QuestionDialog, {
|
||||
title: _t("Are you sure you want to make this encrypted room public?"),
|
||||
description: <div>
|
||||
<p> { _t(
|
||||
"<b>It's not recommended to make encrypted rooms public.</b> " +
|
||||
"It will mean anyone can find and join the room, so anyone can read messages. " +
|
||||
"You'll get none of the benefits of encryption. Encrypting messages in a public " +
|
||||
"room will make receiving and sending messages slower.",
|
||||
null,
|
||||
{ "b": (sub) => <b>{ sub }</b> },
|
||||
) } </p>
|
||||
<p> { _t(
|
||||
"To avoid these issues, create a <a>new public room</a> for the conversation " +
|
||||
"you plan to have.",
|
||||
null,
|
||||
{
|
||||
"a": (sub) => <a
|
||||
className="mx_linkButton"
|
||||
onClick={() => {
|
||||
dialog.close();
|
||||
this.createNewRoom(true, false);
|
||||
}}> { sub } </a>,
|
||||
},
|
||||
) } </p>
|
||||
</div>,
|
||||
});
|
||||
|
||||
const { finished } = dialog;
|
||||
const [confirm] = await finished;
|
||||
if (!confirm) return;
|
||||
}
|
||||
|
||||
if (beforeJoinRule === joinRule && !restrictedAllowRoomIds) return;
|
||||
|
||||
const content: IContent = {
|
||||
join_rule: joinRule,
|
||||
};
|
||||
|
||||
// pre-set the accepted spaces with the currently viewed one as per the microcopy
|
||||
if (joinRule === JoinRule.Restricted) {
|
||||
content.allow = restrictedAllowRoomIds.map(roomId => ({
|
||||
"type": RestrictedAllowType.RoomMembership,
|
||||
"room_id": roomId,
|
||||
}));
|
||||
}
|
||||
|
||||
this.setState({ joinRule, restrictedAllowRoomIds });
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
client.sendStateEvent(this.props.roomId, EventType.RoomJoinRules, content, "").catch((e) => {
|
||||
console.error(e);
|
||||
this.setState({
|
||||
joinRule: beforeJoinRule,
|
||||
restrictedAllowRoomIds: undefined,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
private onRestrictedRoomIdsChange = (restrictedAllowRoomIds: string[]) => {
|
||||
const beforeRestrictedAllowRoomIds = this.state.restrictedAllowRoomIds;
|
||||
if (!arrayHasDiff(beforeRestrictedAllowRoomIds || [], restrictedAllowRoomIds)) return;
|
||||
this.setState({ restrictedAllowRoomIds });
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
client.sendStateEvent(this.props.roomId, EventType.RoomJoinRules, {
|
||||
join_rule: JoinRule.Restricted,
|
||||
allow: restrictedAllowRoomIds.map(roomId => ({
|
||||
"type": RestrictedAllowType.RoomMembership,
|
||||
"room_id": roomId,
|
||||
})),
|
||||
}, "").catch((e) => {
|
||||
console.error(e);
|
||||
this.setState({ restrictedAllowRoomIds: beforeRestrictedAllowRoomIds });
|
||||
});
|
||||
};
|
||||
|
||||
private onGuestAccessChange = (allowed: boolean) => {
|
||||
const guestAccess = allowed ? GuestAccess.CanJoin : GuestAccess.Forbidden;
|
||||
const beforeGuestAccess = this.state.guestAccess;
|
||||
|
@ -385,42 +248,12 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||
}
|
||||
}
|
||||
|
||||
private editRestrictedRoomIds = async (): Promise<string[] | undefined> => {
|
||||
let selected = this.state.restrictedAllowRoomIds;
|
||||
if (!selected?.length && SpaceStore.instance.activeSpace) {
|
||||
selected = [SpaceStore.instance.activeSpace.roomId];
|
||||
}
|
||||
|
||||
const matrixClient = MatrixClientPeg.get();
|
||||
const { finished } = Modal.createTrackedDialog('Edit restricted', '', ManageRestrictedJoinRuleDialog, {
|
||||
matrixClient,
|
||||
room: matrixClient.getRoom(this.props.roomId),
|
||||
selected,
|
||||
}, "mx_ManageRestrictedJoinRuleDialog_wrapper");
|
||||
|
||||
const [restrictedAllowRoomIds] = await finished;
|
||||
return restrictedAllowRoomIds;
|
||||
};
|
||||
|
||||
private onEditRestrictedClick = async () => {
|
||||
const restrictedAllowRoomIds = await this.editRestrictedRoomIds();
|
||||
if (!Array.isArray(restrictedAllowRoomIds)) return;
|
||||
if (restrictedAllowRoomIds.length > 0) {
|
||||
this.onRestrictedRoomIdsChange(restrictedAllowRoomIds);
|
||||
} else {
|
||||
this.onJoinRuleChange(JoinRule.Invite);
|
||||
}
|
||||
};
|
||||
|
||||
private renderJoinRule() {
|
||||
const client = MatrixClientPeg.get();
|
||||
const room = client.getRoom(this.props.roomId);
|
||||
const joinRule = this.state.joinRule;
|
||||
|
||||
const canChangeJoinRule = room.currentState.mayClientSendStateEvent(EventType.RoomJoinRules, client);
|
||||
|
||||
let aliasWarning = null;
|
||||
if (joinRule === JoinRule.Public && !this.state.hasAliases) {
|
||||
if (room.getJoinRule() === JoinRule.Public && !this.state.hasAliases) {
|
||||
aliasWarning = (
|
||||
<div className='mx_SecurityRoomSettingsTab_warning'>
|
||||
<img src={require("../../../../../../res/img/warning.svg")} width={15} height={15} />
|
||||
|
@ -431,111 +264,68 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||
);
|
||||
}
|
||||
|
||||
const radioDefinitions: IDefinition<JoinRule>[] = [{
|
||||
value: JoinRule.Invite,
|
||||
label: _t("Private (invite only)"),
|
||||
description: _t("Only invited people can join."),
|
||||
checked: this.state.joinRule === JoinRule.Invite
|
||||
|| (this.state.joinRule === JoinRule.Restricted && !this.state.restrictedAllowRoomIds?.length),
|
||||
}, {
|
||||
value: JoinRule.Public,
|
||||
label: _t("Public"),
|
||||
description: _t("Anyone can find and join."),
|
||||
}];
|
||||
return <div className="mx_SecurityRoomSettingsTab_joinRule">
|
||||
<div className="mx_SettingsTab_subsectionText">
|
||||
<span>{ _t("Decide who can join %(roomName)s.", {
|
||||
roomName: room?.name,
|
||||
}) }</span>
|
||||
</div>
|
||||
|
||||
if (this.state.roomSupportsRestricted ||
|
||||
this.state.preferredRestrictionVersion ||
|
||||
joinRule === JoinRule.Restricted
|
||||
) {
|
||||
let upgradeRequiredPill;
|
||||
if (this.state.preferredRestrictionVersion) {
|
||||
upgradeRequiredPill = <span className="mx_SecurityRoomSettingsTab_upgradeRequired">
|
||||
{ _t("Upgrade required") }
|
||||
</span>;
|
||||
}
|
||||
{ aliasWarning }
|
||||
|
||||
let description;
|
||||
if (joinRule === JoinRule.Restricted && this.state.restrictedAllowRoomIds?.length) {
|
||||
const shownSpaces = this.state.restrictedAllowRoomIds
|
||||
.map(roomId => client.getRoom(roomId))
|
||||
.filter(room => room?.isSpaceRoom())
|
||||
.slice(0, 4);
|
||||
<JoinRuleSettings
|
||||
room={room}
|
||||
beforeChange={this.onBeforeJoinRuleChange}
|
||||
onError={this.onJoinRuleChangeError}
|
||||
closeSettingsFn={this.props.closeSettingsFn}
|
||||
promptUpgrade={true}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
let moreText;
|
||||
if (shownSpaces.length < this.state.restrictedAllowRoomIds.length) {
|
||||
if (shownSpaces.length > 0) {
|
||||
moreText = _t("& %(count)s more", {
|
||||
count: this.state.restrictedAllowRoomIds.length - shownSpaces.length,
|
||||
});
|
||||
} else {
|
||||
moreText = _t("Currently, %(count)s spaces have access", {
|
||||
count: this.state.restrictedAllowRoomIds.length,
|
||||
});
|
||||
}
|
||||
}
|
||||
private onJoinRuleChangeError = (error: Error) => {
|
||||
Modal.createTrackedDialog('Room not found', '', ErrorDialog, {
|
||||
title: _t("Failed to update the join rules"),
|
||||
description: error.message ?? _t("Unknown failure"),
|
||||
});
|
||||
};
|
||||
|
||||
description = <div>
|
||||
<span>
|
||||
{ _t("Anyone in a space can find and join. <a>Edit which spaces can access here.</a>", {}, {
|
||||
a: sub => <AccessibleButton
|
||||
disabled={!canChangeJoinRule}
|
||||
onClick={this.onEditRestrictedClick}
|
||||
kind="link"
|
||||
>
|
||||
{ sub }
|
||||
</AccessibleButton>,
|
||||
}) }
|
||||
</span>
|
||||
|
||||
<div className="mx_SecurityRoomSettingsTab_spacesWithAccess">
|
||||
<h4>{ _t("Spaces with access") }</h4>
|
||||
{ shownSpaces.map(room => {
|
||||
return <span key={room.roomId}>
|
||||
<RoomAvatar room={room} height={32} width={32} />
|
||||
{ room.name }
|
||||
</span>;
|
||||
}) }
|
||||
{ moreText && <span>{ moreText }</span> }
|
||||
</div>
|
||||
</div>;
|
||||
} else if (SpaceStore.instance.activeSpace) {
|
||||
description = _t("Anyone in %(spaceName)s can find and join. You can select other spaces too.", {
|
||||
spaceName: SpaceStore.instance.activeSpace.name,
|
||||
});
|
||||
} else {
|
||||
description = _t("Anyone in a space can find and join. You can select multiple spaces.");
|
||||
}
|
||||
|
||||
radioDefinitions.splice(1, 0, {
|
||||
value: JoinRule.Restricted,
|
||||
label: <>
|
||||
{ _t("Space members") }
|
||||
{ upgradeRequiredPill }
|
||||
</>,
|
||||
description,
|
||||
// if there are 0 allowed spaces then render it as invite only instead
|
||||
checked: this.state.joinRule === JoinRule.Restricted && !!this.state.restrictedAllowRoomIds?.length,
|
||||
private onBeforeJoinRuleChange = async (joinRule: JoinRule): Promise<boolean> => {
|
||||
if (this.state.encrypted && joinRule === JoinRule.Public) {
|
||||
const dialog = Modal.createTrackedDialog('Confirm Public Encrypted Room', '', QuestionDialog, {
|
||||
title: _t("Are you sure you want to make this encrypted room public?"),
|
||||
description: <div>
|
||||
<p> { _t(
|
||||
"<b>It's not recommended to make encrypted rooms public.</b> " +
|
||||
"It will mean anyone can find and join the room, so anyone can read messages. " +
|
||||
"You'll get none of the benefits of encryption. Encrypting messages in a public " +
|
||||
"room will make receiving and sending messages slower.",
|
||||
null,
|
||||
{ "b": (sub) => <b>{ sub }</b> },
|
||||
) } </p>
|
||||
<p> { _t(
|
||||
"To avoid these issues, create a <a>new public room</a> for the conversation " +
|
||||
"you plan to have.",
|
||||
null,
|
||||
{
|
||||
"a": (sub) => <a
|
||||
className="mx_linkButton"
|
||||
onClick={() => {
|
||||
dialog.close();
|
||||
this.createNewRoom(true, false);
|
||||
}}> { sub } </a>,
|
||||
},
|
||||
) } </p>
|
||||
</div>,
|
||||
});
|
||||
|
||||
const { finished } = dialog;
|
||||
const [confirm] = await finished;
|
||||
if (!confirm) return false;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_SecurityRoomSettingsTab_joinRule">
|
||||
<div className="mx_SettingsTab_subsectionText">
|
||||
<span>{ _t("Decide who can join %(roomName)s.", {
|
||||
roomName: client.getRoom(this.props.roomId)?.name,
|
||||
}) }</span>
|
||||
</div>
|
||||
{ aliasWarning }
|
||||
<StyledRadioGroup
|
||||
name="joinRule"
|
||||
value={joinRule}
|
||||
onChange={this.onJoinRuleChange}
|
||||
definitions={radioDefinitions}
|
||||
disabled={!canChangeJoinRule}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
private renderHistory() {
|
||||
const client = MatrixClientPeg.get();
|
||||
|
@ -634,6 +424,22 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||
historySection = null;
|
||||
}
|
||||
|
||||
let advanced;
|
||||
if (this.state.joinRule === JoinRule.Public) {
|
||||
advanced = (
|
||||
<>
|
||||
<AccessibleButton
|
||||
onClick={this.toggleAdvancedSection}
|
||||
kind="link"
|
||||
className="mx_SettingsTab_showAdvanced"
|
||||
>
|
||||
{ this.state.showAdvancedSection ? _t("Hide advanced") : _t("Show advanced") }
|
||||
</AccessibleButton>
|
||||
{ this.state.showAdvancedSection && this.renderAdvanced() }
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_SettingsTab mx_SecurityRoomSettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{ _t("Security & Privacy") }</div>
|
||||
|
@ -659,15 +465,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||
{ this.renderJoinRule() }
|
||||
</div>
|
||||
|
||||
<AccessibleButton
|
||||
onClick={this.toggleAdvancedSection}
|
||||
kind="link"
|
||||
className="mx_SettingsTab_showAdvanced"
|
||||
>
|
||||
{ this.state.showAdvancedSection ? _t("Hide advanced") : _t("Show advanced") }
|
||||
</AccessibleButton>
|
||||
{ this.state.showAdvancedSection && this.renderAdvanced() }
|
||||
|
||||
{ advanced }
|
||||
{ historySection }
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -25,49 +25,20 @@ import AccessibleButton from "../elements/AccessibleButton";
|
|||
import AliasSettings from "../room_settings/AliasSettings";
|
||||
import { useStateToggle } from "../../../hooks/useStateToggle";
|
||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
||||
import StyledRadioGroup from "../elements/StyledRadioGroup";
|
||||
import { useLocalEcho } from "../../../hooks/useLocalEcho";
|
||||
import JoinRuleSettings from "../settings/JoinRuleSettings";
|
||||
|
||||
interface IProps {
|
||||
matrixClient: MatrixClient;
|
||||
space: Room;
|
||||
closeSettingsFn(): void;
|
||||
}
|
||||
|
||||
enum SpaceVisibility {
|
||||
Unlisted = "unlisted",
|
||||
Private = "private",
|
||||
}
|
||||
|
||||
const useLocalEcho = <T extends any>(
|
||||
currentFactory: () => T,
|
||||
setterFn: (value: T) => Promise<unknown>,
|
||||
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 SpaceSettingsVisibilityTab = ({ matrixClient: cli, space, closeSettingsFn }: IProps) => {
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const userId = cli.getUserId();
|
||||
|
||||
const [visibility, setVisibility] = useLocalEcho<SpaceVisibility>(
|
||||
() => space.getJoinRule() === JoinRule.Invite ? SpaceVisibility.Private : SpaceVisibility.Unlisted,
|
||||
visibility => cli.sendStateEvent(space.roomId, EventType.RoomJoinRules, {
|
||||
join_rule: visibility === SpaceVisibility.Unlisted ? JoinRule.Public : JoinRule.Invite,
|
||||
}, ""),
|
||||
() => 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,
|
||||
|
@ -87,41 +58,42 @@ const SpaceSettingsVisibilityTab = ({ matrixClient: cli, space }: IProps) => {
|
|||
|
||||
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>
|
||||
if (visibility === SpaceVisibility.Unlisted) {
|
||||
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>
|
||||
</>;
|
||||
<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) {
|
||||
if (space.getJoinRule() === JoinRule.Public) {
|
||||
addressesSection = <>
|
||||
<span className="mx_SettingsTab_subheading">{ _t("Address") }</span>
|
||||
<div className="mx_SettingsTab_section mx_SettingsTab_subsectionText">
|
||||
|
@ -147,22 +119,10 @@ const SpaceSettingsVisibilityTab = ({ matrixClient: cli, space }: IProps) => {
|
|||
</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"),
|
||||
},
|
||||
]}
|
||||
<JoinRuleSettings
|
||||
room={space}
|
||||
onError={() => setError(_t("Failed to update the visibility of this space"))}
|
||||
closeSettingsFn={closeSettingsFn}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -261,7 +261,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
|||
|
||||
render() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { space, activeSpaces, isNested, isPanelCollapsed, onExpand, parents, innerRef,
|
||||
const { space, activeSpaces, isNested, isPanelCollapsed, onExpand, parents, innerRef, dragHandleProps,
|
||||
...otherProps } = this.props;
|
||||
|
||||
const collapsed = this.isCollapsed;
|
||||
|
@ -300,7 +300,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
|||
/> : null;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { tabIndex, ...dragHandleProps } = this.props.dragHandleProps || {};
|
||||
const { tabIndex, ...restDragHandleProps } = dragHandleProps || {};
|
||||
|
||||
return (
|
||||
<li
|
||||
|
@ -311,7 +311,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
|||
role="treeitem"
|
||||
>
|
||||
<SpaceButton
|
||||
{...dragHandleProps}
|
||||
{...restDragHandleProps}
|
||||
space={space}
|
||||
className={isInvite ? "mx_SpaceButton_invite" : undefined}
|
||||
selected={activeSpaces.includes(space)}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue