Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/fix/18891
This commit is contained in:
@@ -705,9 +705,9 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||
|
||||
let willWantDateSeparator = false;
|
||||
let lastInSection = true;
|
||||
if (nextEvent) {
|
||||
willWantDateSeparator = this.wantsDateSeparator(mxEv, nextEvent.getDate() || new Date());
|
||||
lastInSection = willWantDateSeparator || mxEv.getSender() !== nextEvent.getSender();
|
||||
if (nextEventWithTile) {
|
||||
willWantDateSeparator = this.wantsDateSeparator(mxEv, nextEventWithTile.getDate() || new Date());
|
||||
lastInSection = willWantDateSeparator || mxEv.getSender() !== nextEventWithTile.getSender();
|
||||
}
|
||||
|
||||
// is this a continuation of the previous message?
|
||||
|
||||
@@ -53,6 +53,7 @@ import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard";
|
||||
import { throttle } from 'lodash';
|
||||
import SpaceStore from "../../stores/SpaceStore";
|
||||
import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks';
|
||||
import { E2EStatus } from '../../utils/ShieldUtils';
|
||||
|
||||
interface IProps {
|
||||
room?: Room; // if showing panels for a given room, this is set
|
||||
@@ -60,6 +61,7 @@ interface IProps {
|
||||
user?: User; // used if we know the user ahead of opening the panel
|
||||
resizeNotifier: ResizeNotifier;
|
||||
permalinkCreator?: RoomPermalinkCreator;
|
||||
e2eStatus?: E2EStatus;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
@@ -319,7 +321,8 @@ export default class RightPanel extends React.Component<IProps, IState> {
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
onClose={this.onClose}
|
||||
mxEvent={this.state.event}
|
||||
permalinkCreator={this.props.permalinkCreator} />;
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
e2eStatus={this.props.e2eStatus} />;
|
||||
break;
|
||||
|
||||
case RightPanelPhases.ThreadPanel:
|
||||
|
||||
@@ -2063,7 +2063,8 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||
? <RightPanel
|
||||
room={this.state.room}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
permalinkCreator={this.getPermalinkCreatorForRoom(this.state.room)} />
|
||||
permalinkCreator={this.getPermalinkCreatorForRoom(this.state.room)}
|
||||
e2eStatus={this.state.e2eStatus} />
|
||||
: null;
|
||||
|
||||
const timelineClasses = classNames("mx_RoomView_timeline", {
|
||||
|
||||
@@ -275,8 +275,8 @@ export default class ScrollPanel extends React.Component<IProps> {
|
||||
// fractional values (both too big and too small)
|
||||
// for scrollTop happen on certain browsers/platforms
|
||||
// when scrolled all the way down. E.g. Chrome 72 on debian.
|
||||
// so check difference < 1;
|
||||
return Math.abs(sn.scrollHeight - (sn.scrollTop + sn.clientHeight)) < 1;
|
||||
// so check difference <= 1;
|
||||
return Math.abs(sn.scrollHeight - (sn.scrollTop + sn.clientHeight)) <= 1;
|
||||
};
|
||||
|
||||
// returns the vertical height in the given direction that can be removed from
|
||||
|
||||
@@ -33,6 +33,7 @@ import { ActionPayload } from '../../dispatcher/payloads';
|
||||
import { SetRightPanelPhasePayload } from '../../dispatcher/payloads/SetRightPanelPhasePayload';
|
||||
import { Action } from '../../dispatcher/actions';
|
||||
import { MatrixClientPeg } from '../../MatrixClientPeg';
|
||||
import { E2EStatus } from '../../utils/ShieldUtils';
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
@@ -40,6 +41,7 @@ interface IProps {
|
||||
resizeNotifier: ResizeNotifier;
|
||||
mxEvent: MatrixEvent;
|
||||
permalinkCreator?: RoomPermalinkCreator;
|
||||
e2eStatus?: E2EStatus;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
@@ -50,6 +52,7 @@ interface IState {
|
||||
@replaceableComponent("structures.ThreadView")
|
||||
export default class ThreadView extends React.Component<IProps, IState> {
|
||||
private dispatcherRef: string;
|
||||
private timelinePanelRef: React.RefObject<TimelinePanel> = React.createRef();
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
@@ -110,10 +113,13 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
||||
|
||||
private updateThread = (thread?: Thread) => {
|
||||
if (thread) {
|
||||
this.setState({ thread });
|
||||
} else {
|
||||
this.forceUpdate();
|
||||
this.setState({
|
||||
thread,
|
||||
replyToEvent: thread.replyToEvent,
|
||||
});
|
||||
}
|
||||
|
||||
this.timelinePanelRef.current?.refreshTimeline();
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
@@ -126,6 +132,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
||||
>
|
||||
{ this.state.thread && (
|
||||
<TimelinePanel
|
||||
ref={this.timelinePanelRef}
|
||||
manageReadReceipts={false}
|
||||
manageReadMarkers={false}
|
||||
timelineSet={this.state?.thread?.timelineSet}
|
||||
@@ -144,6 +151,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
||||
replyToEvent={this.state?.thread?.replyToEvent}
|
||||
showReplyPreview={false}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
e2eStatus={this.props.e2eStatus}
|
||||
compact={true}
|
||||
/>
|
||||
</BaseCard>
|
||||
|
||||
@@ -47,11 +47,14 @@ import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
|
||||
import Spinner from "../views/elements/Spinner";
|
||||
import EditorStateTransfer from '../../utils/EditorStateTransfer';
|
||||
import ErrorDialog from '../views/dialogs/ErrorDialog';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
const PAGINATE_SIZE = 20;
|
||||
const INITIAL_SIZE = 20;
|
||||
const READ_RECEIPT_INTERVAL_MS = 500;
|
||||
|
||||
const READ_MARKER_DEBOUNCE_MS = 100;
|
||||
|
||||
const DEBUG = false;
|
||||
|
||||
let debuglog = function(...s: any[]) {};
|
||||
@@ -475,22 +478,35 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
if (this.props.manageReadMarkers) {
|
||||
const rmPosition = this.getReadMarkerPosition();
|
||||
// we hide the read marker when it first comes onto the screen, but if
|
||||
// it goes back off the top of the screen (presumably because the user
|
||||
// clicks on the 'jump to bottom' button), we need to re-enable it.
|
||||
if (rmPosition < 0) {
|
||||
this.setState({ readMarkerVisible: true });
|
||||
}
|
||||
|
||||
// if read marker position goes between 0 and -1/1,
|
||||
// (and user is active), switch timeout
|
||||
const timeout = this.readMarkerTimeout(rmPosition);
|
||||
// NO-OP when timeout already has set to the given value
|
||||
this.readMarkerActivityTimer.changeTimeout(timeout);
|
||||
this.doManageReadMarkers();
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Debounced function to manage read markers because we don't need to
|
||||
* do this on every tiny scroll update. It also sets state which causes
|
||||
* a component update, which can in turn reset the scroll position, so
|
||||
* it's important we allow the browser to scroll a bit before running this
|
||||
* (hence trailing edge only and debounce rather than throttle because
|
||||
* we really only need to update this once the user has finished scrolling,
|
||||
* not periodically while they scroll).
|
||||
*/
|
||||
private doManageReadMarkers = debounce(() => {
|
||||
const rmPosition = this.getReadMarkerPosition();
|
||||
// we hide the read marker when it first comes onto the screen, but if
|
||||
// it goes back off the top of the screen (presumably because the user
|
||||
// clicks on the 'jump to bottom' button), we need to re-enable it.
|
||||
if (rmPosition < 0) {
|
||||
this.setState({ readMarkerVisible: true });
|
||||
}
|
||||
|
||||
// if read marker position goes between 0 and -1/1,
|
||||
// (and user is active), switch timeout
|
||||
const timeout = this.readMarkerTimeout(rmPosition);
|
||||
// NO-OP when timeout already has set to the given value
|
||||
this.readMarkerActivityTimer.changeTimeout(timeout);
|
||||
}, READ_MARKER_DEBOUNCE_MS, { leading: false, trailing: true });
|
||||
|
||||
private onAction = (payload: ActionPayload): void => {
|
||||
switch (payload.action) {
|
||||
case "ignore_state_changed":
|
||||
@@ -1179,6 +1195,12 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||
this.setState(this.getEvents());
|
||||
}
|
||||
|
||||
// Force refresh the timeline before threads support pending events
|
||||
public refreshTimeline(): void {
|
||||
this.loadTimeline();
|
||||
this.reloadEvents();
|
||||
}
|
||||
|
||||
// get the list of events from the timeline window and the pending event list
|
||||
private getEvents(): Pick<IState, "events" | "liveEvents" | "firstVisibleEventIndex"> {
|
||||
const events: MatrixEvent[] = this.timelineWindow.getEvents();
|
||||
|
||||
@@ -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)}
|
||||
|
||||
Reference in New Issue
Block a user