Merge remote-tracking branch 'upstream/develop' into feature/image-view-load-anim/18186
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
This commit is contained in:
commit
1a128c3a00
567 changed files with 23292 additions and 9067 deletions
|
@ -19,7 +19,7 @@ import React, { ReactHTML } from 'react';
|
|||
import { Key } from '../../../Keyboard';
|
||||
import classnames from 'classnames';
|
||||
|
||||
export type ButtonEvent = React.MouseEvent<Element> | React.KeyboardEvent<Element>;
|
||||
export type ButtonEvent = React.MouseEvent<Element> | React.KeyboardEvent<Element> | React.FormEvent<Element>;
|
||||
|
||||
/**
|
||||
* children: React's magic prop. Represents all children given to the element.
|
||||
|
@ -39,7 +39,7 @@ interface IProps extends React.InputHTMLAttributes<Element> {
|
|||
tabIndex?: number;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
onClick(e?: ButtonEvent): void;
|
||||
onClick(e?: ButtonEvent): void | Promise<void>;
|
||||
}
|
||||
|
||||
interface IAccessibleButtonProps extends React.InputHTMLAttributes<Element> {
|
||||
|
@ -67,7 +67,9 @@ export default function AccessibleButton({
|
|||
...restProps
|
||||
}: IProps) {
|
||||
const newProps: IAccessibleButtonProps = restProps;
|
||||
if (!disabled) {
|
||||
if (disabled) {
|
||||
newProps["aria-disabled"] = true;
|
||||
} else {
|
||||
newProps.onClick = onClick;
|
||||
// We need to consume enter onKeyDown and space onKeyUp
|
||||
// otherwise we are risking also activating other keyboard focusable elements
|
||||
|
@ -118,7 +120,7 @@ export default function AccessibleButton({
|
|||
);
|
||||
|
||||
// React.createElement expects InputHTMLAttributes
|
||||
return React.createElement(element, restProps, children);
|
||||
return React.createElement(element, newProps, children);
|
||||
}
|
||||
|
||||
AccessibleButton.defaultProps = {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -122,7 +122,7 @@ export default class AddressTile extends React.Component<IProps> {
|
|||
let dismiss;
|
||||
if (this.props.canDismiss) {
|
||||
dismiss = (
|
||||
<div className="mx_AddressTile_dismiss" onClick={this.props.onDismissed} >
|
||||
<div className="mx_AddressTile_dismiss" onClick={this.props.onDismissed}>
|
||||
<img src={require("../../../../res/img/icon-address-delete.svg")} width="9" height="9" />
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -62,10 +62,10 @@ export default class AppPermission extends React.Component<IProps, IState> {
|
|||
|
||||
// Set all this into the initial state
|
||||
this.state = {
|
||||
...urlInfo,
|
||||
roomMember,
|
||||
isWrapped: null,
|
||||
widgetDomain: null,
|
||||
isWrapped: null,
|
||||
roomMember,
|
||||
...urlInfo,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -218,6 +218,7 @@ export default class AppTile extends React.Component {
|
|||
|
||||
// Delete the widget from the persisted store for good measure.
|
||||
PersistedElement.destroyElement(this._persistKey);
|
||||
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
|
||||
|
||||
if (this._sgWidget) this._sgWidget.stop({ forceDestroy: true });
|
||||
}
|
||||
|
@ -307,7 +308,6 @@ export default class AppTile extends React.Component {
|
|||
if (this.iframe) {
|
||||
// Reload iframe
|
||||
this.iframe.src = this._sgWidget.embedUrl;
|
||||
this.setState({});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -333,7 +333,7 @@ export default class AppTile extends React.Component {
|
|||
// this would only be for content hosted on the same origin as the element client: anything
|
||||
// hosted on the same origin as the client will get the same access as if you clicked
|
||||
// a link to it.
|
||||
const sandboxFlags = "allow-forms allow-popups allow-popups-to-escape-sandbox "+
|
||||
const sandboxFlags = "allow-forms allow-popups allow-popups-to-escape-sandbox " +
|
||||
"allow-same-origin allow-scripts allow-presentation";
|
||||
|
||||
// Additional iframe feature pemissions
|
||||
|
@ -443,25 +443,25 @@ export default class AppTile extends React.Component {
|
|||
return <React.Fragment>
|
||||
<div className={appTileClasses} id={this.props.app.id}>
|
||||
{ this.props.showMenubar &&
|
||||
<div className="mx_AppTileMenuBar">
|
||||
<span className="mx_AppTileMenuBarTitle" style={{ pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : false) }}>
|
||||
{ this.props.showTitle && this._getTileTitle() }
|
||||
</span>
|
||||
<span className="mx_AppTileMenuBarWidgets">
|
||||
{ this.props.showPopout && <AccessibleButton
|
||||
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_popout"
|
||||
title={_t('Popout widget')}
|
||||
onClick={this._onPopoutWidgetClick}
|
||||
/> }
|
||||
<ContextMenuButton
|
||||
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_menu"
|
||||
label={_t("Options")}
|
||||
isExpanded={this.state.menuDisplayed}
|
||||
inputRef={this._contextMenuButton}
|
||||
onClick={this._onContextMenuClick}
|
||||
/>
|
||||
</span>
|
||||
</div> }
|
||||
<div className="mx_AppTileMenuBar">
|
||||
<span className="mx_AppTileMenuBarTitle" style={{ pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : false) }}>
|
||||
{ this.props.showTitle && this._getTileTitle() }
|
||||
</span>
|
||||
<span className="mx_AppTileMenuBarWidgets">
|
||||
{ this.props.showPopout && <AccessibleButton
|
||||
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_popout"
|
||||
title={_t('Popout widget')}
|
||||
onClick={this._onPopoutWidgetClick}
|
||||
/> }
|
||||
<ContextMenuButton
|
||||
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_menu"
|
||||
label={_t("Options")}
|
||||
isExpanded={this.state.menuDisplayed}
|
||||
inputRef={this._contextMenuButton}
|
||||
onClick={this._onContextMenuClick}
|
||||
/>
|
||||
</span>
|
||||
</div> }
|
||||
{ appTileBody }
|
||||
</div>
|
||||
|
||||
|
|
|
@ -17,72 +17,91 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import BaseDialog from "..//dialogs/BaseDialog";
|
||||
import DialogButtons from "./DialogButtons";
|
||||
import classNames from 'classnames';
|
||||
import AccessibleButton from './AccessibleButton';
|
||||
import { getDesktopCapturerSources } from "matrix-js-sdk/src/webrtc/call";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import TabbedView, { Tab, TabLocation } from '../../structures/TabbedView';
|
||||
|
||||
export interface DesktopCapturerSource {
|
||||
id: string;
|
||||
name: string;
|
||||
thumbnailURL;
|
||||
export function getDesktopCapturerSources(): Promise<Array<DesktopCapturerSource>> {
|
||||
const options: GetSourcesOptions = {
|
||||
thumbnailSize: {
|
||||
height: 176,
|
||||
width: 312,
|
||||
},
|
||||
types: [
|
||||
"screen",
|
||||
"window",
|
||||
],
|
||||
};
|
||||
return window.electron.getDesktopCapturerSources(options);
|
||||
}
|
||||
|
||||
export enum Tabs {
|
||||
Screens = "screens",
|
||||
Windows = "windows",
|
||||
Screens = "screen",
|
||||
Windows = "window",
|
||||
}
|
||||
|
||||
export interface DesktopCapturerSourceIProps {
|
||||
export interface ExistingSourceIProps {
|
||||
source: DesktopCapturerSource;
|
||||
onSelect(source: DesktopCapturerSource): void;
|
||||
selected: boolean;
|
||||
}
|
||||
|
||||
export class ExistingSource extends React.Component<DesktopCapturerSourceIProps> {
|
||||
constructor(props) {
|
||||
export class ExistingSource extends React.Component<ExistingSourceIProps> {
|
||||
constructor(props: ExistingSourceIProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
onClick = (ev) => {
|
||||
private onClick = (): void => {
|
||||
this.props.onSelect(this.props.source);
|
||||
};
|
||||
|
||||
render() {
|
||||
const thumbnailClasses = classNames({
|
||||
mx_desktopCapturerSourcePicker_source_thumbnail: true,
|
||||
mx_desktopCapturerSourcePicker_source_thumbnail_selected: this.props.selected,
|
||||
});
|
||||
|
||||
return (
|
||||
<AccessibleButton
|
||||
className="mx_desktopCapturerSourcePicker_stream_button"
|
||||
className="mx_desktopCapturerSourcePicker_source"
|
||||
title={this.props.source.name}
|
||||
onClick={this.onClick} >
|
||||
onClick={this.onClick}
|
||||
>
|
||||
<img
|
||||
className="mx_desktopCapturerSourcePicker_stream_thumbnail"
|
||||
className={thumbnailClasses}
|
||||
src={this.props.source.thumbnailURL}
|
||||
/>
|
||||
<span className="mx_desktopCapturerSourcePicker_stream_name">{ this.props.source.name }</span>
|
||||
<span className="mx_desktopCapturerSourcePicker_source_name">{ this.props.source.name }</span>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export interface DesktopCapturerSourcePickerIState {
|
||||
export interface PickerIState {
|
||||
selectedTab: Tabs;
|
||||
sources: Array<DesktopCapturerSource>;
|
||||
selectedSource: DesktopCapturerSource | null;
|
||||
}
|
||||
export interface DesktopCapturerSourcePickerIProps {
|
||||
onFinished(source: DesktopCapturerSource): void;
|
||||
export interface PickerIProps {
|
||||
onFinished(sourceId: string): void;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.elements.DesktopCapturerSourcePicker")
|
||||
export default class DesktopCapturerSourcePicker extends React.Component<
|
||||
DesktopCapturerSourcePickerIProps,
|
||||
DesktopCapturerSourcePickerIState
|
||||
> {
|
||||
interval;
|
||||
PickerIProps,
|
||||
PickerIState
|
||||
> {
|
||||
interval: number;
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: PickerIProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
selectedTab: Tabs.Screens,
|
||||
sources: [],
|
||||
selectedSource: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -106,69 +125,61 @@ export default class DesktopCapturerSourcePicker extends React.Component<
|
|||
clearInterval(this.interval);
|
||||
}
|
||||
|
||||
onSelect = (source) => {
|
||||
this.props.onFinished(source);
|
||||
private onSelect = (source: DesktopCapturerSource): void => {
|
||||
this.setState({ selectedSource: source });
|
||||
};
|
||||
|
||||
onScreensClick = (ev) => {
|
||||
this.setState({ selectedTab: Tabs.Screens });
|
||||
private onShare = (): void => {
|
||||
this.props.onFinished(this.state.selectedSource.id);
|
||||
};
|
||||
|
||||
onWindowsClick = (ev) => {
|
||||
this.setState({ selectedTab: Tabs.Windows });
|
||||
private onTabChange = (): void => {
|
||||
this.setState({ selectedSource: null });
|
||||
};
|
||||
|
||||
onCloseClick = (ev) => {
|
||||
private onCloseClick = (): void => {
|
||||
this.props.onFinished(null);
|
||||
};
|
||||
|
||||
render() {
|
||||
let sources;
|
||||
if (this.state.selectedTab === Tabs.Screens) {
|
||||
sources = this.state.sources
|
||||
.filter((source) => {
|
||||
return source.id.startsWith("screen");
|
||||
})
|
||||
.map((source) => {
|
||||
return <ExistingSource source={source} onSelect={this.onSelect} key={source.id} />;
|
||||
});
|
||||
} else {
|
||||
sources = this.state.sources
|
||||
.filter((source) => {
|
||||
return source.id.startsWith("window");
|
||||
})
|
||||
.map((source) => {
|
||||
return <ExistingSource source={source} onSelect={this.onSelect} key={source.id} />;
|
||||
});
|
||||
}
|
||||
private getTab(type: "screen" | "window", label: string): Tab {
|
||||
const sources = this.state.sources.filter((source) => source.id.startsWith(type)).map((source) => {
|
||||
return (
|
||||
<ExistingSource
|
||||
selected={this.state.selectedSource?.id === source.id}
|
||||
source={source}
|
||||
onSelect={this.onSelect}
|
||||
key={source.id}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const buttonStyle = "mx_desktopCapturerSourcePicker_tabLabel";
|
||||
const screensButtonStyle = buttonStyle + ((this.state.selectedTab === Tabs.Screens) ? "_selected" : "");
|
||||
const windowsButtonStyle = buttonStyle + ((this.state.selectedTab === Tabs.Windows) ? "_selected" : "");
|
||||
return new Tab(type, label, null, (
|
||||
<div className="mx_desktopCapturerSourcePicker_tab">
|
||||
{ sources }
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
render() {
|
||||
const tabs = [
|
||||
this.getTab("screen", _t("Share entire screen")),
|
||||
this.getTab("window", _t("Application window")),
|
||||
];
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className="mx_desktopCapturerSourcePicker"
|
||||
onFinished={this.onCloseClick}
|
||||
title={_t("Share your screen")}
|
||||
title={_t("Share content")}
|
||||
>
|
||||
<div className="mx_desktopCapturerSourcePicker_tabLabels">
|
||||
<AccessibleButton
|
||||
className={screensButtonStyle}
|
||||
onClick={this.onScreensClick}
|
||||
>
|
||||
{ _t("Screens") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
className={windowsButtonStyle}
|
||||
onClick={this.onWindowsClick}
|
||||
>
|
||||
{ _t("Windows") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<div className="mx_desktopCapturerSourcePicker_panel">
|
||||
{ sources }
|
||||
</div>
|
||||
<TabbedView tabs={tabs} tabLocation={TabLocation.TOP} onChange={this.onTabChange} />
|
||||
<DialogButtons
|
||||
primaryButton={_t("Share")}
|
||||
hasCancel={true}
|
||||
onCancel={this.onCloseClick}
|
||||
onPrimaryButtonClick={this.onShare}
|
||||
primaryDisabled={!this.state.selectedSource}
|
||||
/>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -15,11 +15,11 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
import AccessibleButton, { ButtonEvent } from "./AccessibleButton";
|
||||
|
||||
interface IProps {
|
||||
// Callback for when the button is pressed
|
||||
onBackspacePress: () => void;
|
||||
onBackspacePress: (ev: ButtonEvent) => void;
|
||||
}
|
||||
|
||||
export default class DialPadBackspaceButton extends React.PureComponent<IProps> {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2017 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -16,34 +15,38 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { createRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { ChangeEvent, createRef, CSSProperties, ReactElement, ReactNode, Ref } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import AccessibleButton from './AccessibleButton';
|
||||
|
||||
import AccessibleButton, { ButtonEvent } from './AccessibleButton';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { Key } from "../../../Keyboard";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
class MenuOption extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._onMouseEnter = this._onMouseEnter.bind(this);
|
||||
this._onClick = this._onClick.bind(this);
|
||||
}
|
||||
interface IMenuOptionProps {
|
||||
children: ReactElement;
|
||||
highlighted?: boolean;
|
||||
dropdownKey: string;
|
||||
id?: string;
|
||||
inputRef?: Ref<HTMLDivElement>;
|
||||
onClick(dropdownKey: string): void;
|
||||
onMouseEnter(dropdownKey: string): void;
|
||||
}
|
||||
|
||||
class MenuOption extends React.Component<IMenuOptionProps> {
|
||||
static defaultProps = {
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
_onMouseEnter() {
|
||||
private onMouseEnter = () => {
|
||||
this.props.onMouseEnter(this.props.dropdownKey);
|
||||
}
|
||||
};
|
||||
|
||||
_onClick(e) {
|
||||
private onClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.props.onClick(this.props.dropdownKey);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const optClasses = classnames({
|
||||
|
@ -54,8 +57,8 @@ class MenuOption extends React.Component {
|
|||
return <div
|
||||
id={this.props.id}
|
||||
className={optClasses}
|
||||
onClick={this._onClick}
|
||||
onMouseEnter={this._onMouseEnter}
|
||||
onClick={this.onClick}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
role="option"
|
||||
aria-selected={this.props.highlighted}
|
||||
ref={this.props.inputRef}
|
||||
|
@ -65,91 +68,97 @@ class MenuOption extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
MenuOption.propTypes = {
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node,
|
||||
]),
|
||||
highlighted: PropTypes.bool,
|
||||
dropdownKey: PropTypes.string,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
onMouseEnter: PropTypes.func.isRequired,
|
||||
inputRef: PropTypes.any,
|
||||
};
|
||||
interface IProps {
|
||||
id: string;
|
||||
// ARIA label
|
||||
label: string;
|
||||
value?: string;
|
||||
className?: string;
|
||||
children: ReactElement[];
|
||||
// negative for consistency with HTML
|
||||
disabled?: boolean;
|
||||
// The width that the dropdown should be. If specified,
|
||||
// the dropped-down part of the menu will be set to this
|
||||
// width.
|
||||
menuWidth?: number;
|
||||
searchEnabled?: boolean;
|
||||
// Called when the selected option changes
|
||||
onOptionChange(dropdownKey: string): void;
|
||||
// Called when the value of the search field changes
|
||||
onSearchChange?(query: string): void;
|
||||
// Function that, given the key of an option, returns
|
||||
// a node representing that option to be displayed in the
|
||||
// box itself as the currently-selected option (ie. as
|
||||
// opposed to in the actual dropped-down part). If
|
||||
// unspecified, the appropriate child element is used as
|
||||
// in the dropped-down menu.
|
||||
getShortOption?(value: string): ReactNode;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
expanded: boolean;
|
||||
highlightedOption: string | null;
|
||||
searchQuery: string;
|
||||
}
|
||||
|
||||
/*
|
||||
* Reusable dropdown select control, akin to react-select,
|
||||
* but somewhat simpler as react-select is 79KB of minified
|
||||
* javascript.
|
||||
*
|
||||
* TODO: Port NetworkDropdown to use this.
|
||||
*/
|
||||
@replaceableComponent("views.elements.Dropdown")
|
||||
export default class Dropdown extends React.Component {
|
||||
constructor(props) {
|
||||
export default class Dropdown extends React.Component<IProps, IState> {
|
||||
private readonly buttonRef = createRef<HTMLDivElement>();
|
||||
private dropdownRootElement: HTMLDivElement = null;
|
||||
private ignoreEvent: MouseEvent = null;
|
||||
private childrenByKey: Record<string, ReactNode> = {};
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.dropdownRootElement = null;
|
||||
this.ignoreEvent = null;
|
||||
this.reindexChildren(this.props.children);
|
||||
|
||||
this._onInputClick = this._onInputClick.bind(this);
|
||||
this._onRootClick = this._onRootClick.bind(this);
|
||||
this._onDocumentClick = this._onDocumentClick.bind(this);
|
||||
this._onMenuOptionClick = this._onMenuOptionClick.bind(this);
|
||||
this._onInputChange = this._onInputChange.bind(this);
|
||||
this._collectRoot = this._collectRoot.bind(this);
|
||||
this._collectInputTextBox = this._collectInputTextBox.bind(this);
|
||||
this._setHighlightedOption = this._setHighlightedOption.bind(this);
|
||||
|
||||
this.inputTextBox = null;
|
||||
|
||||
this._reindexChildren(this.props.children);
|
||||
|
||||
const firstChild = React.Children.toArray(props.children)[0];
|
||||
const firstChild = React.Children.toArray(props.children)[0] as ReactElement;
|
||||
|
||||
this.state = {
|
||||
// True if the menu is dropped-down
|
||||
expanded: false,
|
||||
// The key of the highlighted option
|
||||
// (the option that would become selected if you pressed enter)
|
||||
highlightedOption: firstChild ? firstChild.key : null,
|
||||
highlightedOption: firstChild ? firstChild.key as string : null,
|
||||
// the current search query
|
||||
searchQuery: '',
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
|
||||
UNSAFE_componentWillMount() { // eslint-disable-line camelcase
|
||||
this._button = createRef();
|
||||
// Listen for all clicks on the document so we can close the
|
||||
// menu when the user clicks somewhere else
|
||||
document.addEventListener('click', this._onDocumentClick, false);
|
||||
document.addEventListener('click', this.onDocumentClick, false);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('click', this._onDocumentClick, false);
|
||||
document.removeEventListener('click', this.onDocumentClick, false);
|
||||
}
|
||||
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase
|
||||
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line
|
||||
if (!nextProps.children || nextProps.children.length === 0) {
|
||||
return;
|
||||
}
|
||||
this._reindexChildren(nextProps.children);
|
||||
this.reindexChildren(nextProps.children);
|
||||
const firstChild = nextProps.children[0];
|
||||
this.setState({
|
||||
highlightedOption: firstChild ? firstChild.key : null,
|
||||
});
|
||||
}
|
||||
|
||||
_reindexChildren(children) {
|
||||
private reindexChildren(children: ReactElement[]): void {
|
||||
this.childrenByKey = {};
|
||||
React.Children.forEach(children, (child) => {
|
||||
this.childrenByKey[child.key] = child;
|
||||
});
|
||||
}
|
||||
|
||||
_onDocumentClick(ev) {
|
||||
private onDocumentClick = (ev: MouseEvent) => {
|
||||
// Close the dropdown if the user clicks anywhere that isn't
|
||||
// within our root element
|
||||
if (ev !== this.ignoreEvent) {
|
||||
|
@ -157,9 +166,9 @@ export default class Dropdown extends React.Component {
|
|||
expanded: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_onRootClick(ev) {
|
||||
private onRootClick = (ev: MouseEvent) => {
|
||||
// This captures any clicks that happen within our elements,
|
||||
// such that we can then ignore them when they're seen by the
|
||||
// click listener on the document handler, ie. not close the
|
||||
|
@ -167,9 +176,9 @@ export default class Dropdown extends React.Component {
|
|||
// NB. We can't just stopPropagation() because then the event
|
||||
// doesn't reach the React onClick().
|
||||
this.ignoreEvent = ev;
|
||||
}
|
||||
};
|
||||
|
||||
_onInputClick(ev) {
|
||||
private onAccessibleButtonClick = (ev: ButtonEvent) => {
|
||||
if (this.props.disabled) return;
|
||||
|
||||
if (!this.state.expanded) {
|
||||
|
@ -177,25 +186,29 @@ export default class Dropdown extends React.Component {
|
|||
expanded: true,
|
||||
});
|
||||
ev.preventDefault();
|
||||
} else if ((ev as React.KeyboardEvent).key === Key.ENTER) {
|
||||
// the accessible button consumes enter onKeyDown for firing onClick, so handle it here
|
||||
this.props.onOptionChange(this.state.highlightedOption);
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_close() {
|
||||
private close() {
|
||||
this.setState({
|
||||
expanded: false,
|
||||
});
|
||||
// their focus was on the input, its getting unmounted, move it to the button
|
||||
if (this._button.current) {
|
||||
this._button.current.focus();
|
||||
if (this.buttonRef.current) {
|
||||
this.buttonRef.current.focus();
|
||||
}
|
||||
}
|
||||
|
||||
_onMenuOptionClick(dropdownKey) {
|
||||
this._close();
|
||||
private onMenuOptionClick = (dropdownKey: string) => {
|
||||
this.close();
|
||||
this.props.onOptionChange(dropdownKey);
|
||||
}
|
||||
};
|
||||
|
||||
_onInputKeyDown = (e) => {
|
||||
private onKeyDown = (e: React.KeyboardEvent) => {
|
||||
let handled = true;
|
||||
|
||||
// These keys don't generate keypress events and so needs to be on keyup
|
||||
|
@ -204,16 +217,16 @@ export default class Dropdown extends React.Component {
|
|||
this.props.onOptionChange(this.state.highlightedOption);
|
||||
// fallthrough
|
||||
case Key.ESCAPE:
|
||||
this._close();
|
||||
this.close();
|
||||
break;
|
||||
case Key.ARROW_DOWN:
|
||||
this.setState({
|
||||
highlightedOption: this._nextOption(this.state.highlightedOption),
|
||||
highlightedOption: this.nextOption(this.state.highlightedOption),
|
||||
});
|
||||
break;
|
||||
case Key.ARROW_UP:
|
||||
this.setState({
|
||||
highlightedOption: this._prevOption(this.state.highlightedOption),
|
||||
highlightedOption: this.prevOption(this.state.highlightedOption),
|
||||
});
|
||||
break;
|
||||
default:
|
||||
|
@ -224,53 +237,46 @@ export default class Dropdown extends React.Component {
|
|||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_onInputChange(e) {
|
||||
private onInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({
|
||||
searchQuery: e.target.value,
|
||||
searchQuery: e.currentTarget.value,
|
||||
});
|
||||
if (this.props.onSearchChange) {
|
||||
this.props.onSearchChange(e.target.value);
|
||||
this.props.onSearchChange(e.currentTarget.value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_collectRoot(e) {
|
||||
private collectRoot = (e: HTMLDivElement) => {
|
||||
if (this.dropdownRootElement) {
|
||||
this.dropdownRootElement.removeEventListener(
|
||||
'click', this._onRootClick, false,
|
||||
);
|
||||
this.dropdownRootElement.removeEventListener('click', this.onRootClick, false);
|
||||
}
|
||||
if (e) {
|
||||
e.addEventListener('click', this._onRootClick, false);
|
||||
e.addEventListener('click', this.onRootClick, false);
|
||||
}
|
||||
this.dropdownRootElement = e;
|
||||
}
|
||||
};
|
||||
|
||||
_collectInputTextBox(e) {
|
||||
this.inputTextBox = e;
|
||||
if (e) e.focus();
|
||||
}
|
||||
|
||||
_setHighlightedOption(optionKey) {
|
||||
private setHighlightedOption = (optionKey: string) => {
|
||||
this.setState({
|
||||
highlightedOption: optionKey,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_nextOption(optionKey) {
|
||||
private nextOption(optionKey: string): string {
|
||||
const keys = Object.keys(this.childrenByKey);
|
||||
const index = keys.indexOf(optionKey);
|
||||
return keys[(index + 1) % keys.length];
|
||||
}
|
||||
|
||||
_prevOption(optionKey) {
|
||||
private prevOption(optionKey: string): string {
|
||||
const keys = Object.keys(this.childrenByKey);
|
||||
const index = keys.indexOf(optionKey);
|
||||
return keys[(index - 1) % keys.length];
|
||||
return keys[index <= 0 ? keys.length - 1 : (index - 1) % keys.length];
|
||||
}
|
||||
|
||||
_scrollIntoView(node) {
|
||||
private scrollIntoView(node: Element) {
|
||||
if (node) {
|
||||
node.scrollIntoView({
|
||||
block: "nearest",
|
||||
|
@ -279,18 +285,18 @@ export default class Dropdown extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_getMenuOptions() {
|
||||
private getMenuOptions() {
|
||||
const options = React.Children.map(this.props.children, (child) => {
|
||||
const highlighted = this.state.highlightedOption === child.key;
|
||||
return (
|
||||
<MenuOption
|
||||
id={`${this.props.id}__${child.key}`}
|
||||
key={child.key}
|
||||
dropdownKey={child.key}
|
||||
dropdownKey={child.key as string}
|
||||
highlighted={highlighted}
|
||||
onMouseEnter={this._setHighlightedOption}
|
||||
onClick={this._onMenuOptionClick}
|
||||
inputRef={highlighted ? this._scrollIntoView : undefined}
|
||||
onMouseEnter={this.setHighlightedOption}
|
||||
onClick={this.onMenuOptionClick}
|
||||
inputRef={highlighted ? this.scrollIntoView : undefined}
|
||||
>
|
||||
{ child }
|
||||
</MenuOption>
|
||||
|
@ -307,7 +313,7 @@ export default class Dropdown extends React.Component {
|
|||
render() {
|
||||
let currentValue;
|
||||
|
||||
const menuStyle = {};
|
||||
const menuStyle: CSSProperties = {};
|
||||
if (this.props.menuWidth) menuStyle.width = this.props.menuWidth;
|
||||
|
||||
let menu;
|
||||
|
@ -316,10 +322,9 @@ export default class Dropdown extends React.Component {
|
|||
currentValue = (
|
||||
<input
|
||||
type="text"
|
||||
autoFocus={true}
|
||||
className="mx_Dropdown_option"
|
||||
ref={this._collectInputTextBox}
|
||||
onKeyDown={this._onInputKeyDown}
|
||||
onChange={this._onInputChange}
|
||||
onChange={this.onInputChange}
|
||||
value={this.state.searchQuery}
|
||||
role="combobox"
|
||||
aria-autocomplete="list"
|
||||
|
@ -327,12 +332,13 @@ export default class Dropdown extends React.Component {
|
|||
aria-owns={`${this.props.id}_listbox`}
|
||||
aria-disabled={this.props.disabled}
|
||||
aria-label={this.props.label}
|
||||
onKeyDown={this.onKeyDown}
|
||||
/>
|
||||
);
|
||||
}
|
||||
menu = (
|
||||
<div className="mx_Dropdown_menu" style={menuStyle} role="listbox" id={`${this.props.id}_listbox`}>
|
||||
{ this._getMenuOptions() }
|
||||
{ this.getMenuOptions() }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -356,16 +362,17 @@ export default class Dropdown extends React.Component {
|
|||
|
||||
// Note the menu sits inside the AccessibleButton div so it's anchored
|
||||
// to the input, but overflows below it. The root contains both.
|
||||
return <div className={classnames(dropdownClasses)} ref={this._collectRoot}>
|
||||
return <div className={classnames(dropdownClasses)} ref={this.collectRoot}>
|
||||
<AccessibleButton
|
||||
className="mx_Dropdown_input mx_no_textinput"
|
||||
onClick={this._onInputClick}
|
||||
onClick={this.onAccessibleButtonClick}
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={this.state.expanded}
|
||||
disabled={this.props.disabled}
|
||||
inputRef={this._button}
|
||||
inputRef={this.buttonRef}
|
||||
aria-label={this.props.label}
|
||||
aria-describedby={`${this.props.id}_value`}
|
||||
onKeyDown={this.onKeyDown}
|
||||
>
|
||||
{ currentValue }
|
||||
<span className="mx_Dropdown_arrow" />
|
||||
|
@ -374,28 +381,3 @@ export default class Dropdown extends React.Component {
|
|||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
Dropdown.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
// The width that the dropdown should be. If specified,
|
||||
// the dropped-down part of the menu will be set to this
|
||||
// width.
|
||||
menuWidth: PropTypes.number,
|
||||
// Called when the selected option changes
|
||||
onOptionChange: PropTypes.func.isRequired,
|
||||
// Called when the value of the search field changes
|
||||
onSearchChange: PropTypes.func,
|
||||
searchEnabled: PropTypes.bool,
|
||||
// Function that, given the key of an option, returns
|
||||
// a node representing that option to be displayed in the
|
||||
// box itself as the currently-selected option (ie. as
|
||||
// opposed to in the actual dropped-down part). If
|
||||
// unspecified, the appropriate child element is used as
|
||||
// in the dropped-down menu.
|
||||
getShortOption: PropTypes.func,
|
||||
value: PropTypes.string,
|
||||
// negative for consistency with HTML
|
||||
disabled: PropTypes.bool,
|
||||
// ARIA label
|
||||
label: PropTypes.string.isRequired,
|
||||
};
|
|
@ -71,12 +71,13 @@ export default class ErrorBoundary extends React.PureComponent<{}, IState> {
|
|||
private onBugReport = (): void => {
|
||||
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {
|
||||
label: 'react-soft-crash',
|
||||
error: this.state.error,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.error) {
|
||||
const newIssueUrl = "https://github.com/vector-im/element-web/issues/new";
|
||||
const newIssueUrl = "https://github.com/vector-im/element-web/issues/new/choose";
|
||||
|
||||
let bugReportSection;
|
||||
if (SdkConfig.get().bug_report_endpoint_url) {
|
||||
|
@ -93,8 +94,9 @@ export default class ErrorBoundary extends React.PureComponent<{}, IState> {
|
|||
"If you've submitted a bug via GitHub, debug logs can help " +
|
||||
"us track down the problem. Debug logs contain application " +
|
||||
"usage data including your username, the IDs or aliases of " +
|
||||
"the rooms or groups you have visited and the usernames of " +
|
||||
"other users. They do not contain messages.",
|
||||
"the rooms or groups you have visited, which UI elements you " +
|
||||
"last interacted with, and the usernames of other users. " +
|
||||
"They do not contain messages.",
|
||||
) }</p>
|
||||
<AccessibleButton onClick={this.onBugReport} kind='primary'>
|
||||
{ _t("Submit debug logs") }
|
||||
|
|
|
@ -22,6 +22,7 @@ import MemberAvatar from '../avatars/MemberAvatar';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import { useStateToggle } from "../../../hooks/useStateToggle";
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
import { Layout } from '../../../settings/Layout';
|
||||
|
||||
interface IProps {
|
||||
// An array of member events to summarise
|
||||
|
@ -33,11 +34,13 @@ interface IProps {
|
|||
// The list of room members for which to show avatars next to the summary
|
||||
summaryMembers?: RoomMember[];
|
||||
// The text to show as the summary of this event list
|
||||
summaryText?: string;
|
||||
summaryText?: string | JSX.Element;
|
||||
// An array of EventTiles to render when expanded
|
||||
children: ReactNode[];
|
||||
// Called when the event list expansion is toggled
|
||||
onToggle?(): void;
|
||||
// The layout currently used
|
||||
layout?: Layout;
|
||||
}
|
||||
|
||||
const EventListSummary: React.FC<IProps> = ({
|
||||
|
@ -48,6 +51,7 @@ const EventListSummary: React.FC<IProps> = ({
|
|||
startExpanded,
|
||||
summaryMembers = [],
|
||||
summaryText,
|
||||
layout,
|
||||
}) => {
|
||||
const [expanded, toggleExpanded] = useStateToggle(startExpanded);
|
||||
|
||||
|
@ -63,7 +67,7 @@ const EventListSummary: React.FC<IProps> = ({
|
|||
// If we are only given few events then just pass them through
|
||||
if (events.length < threshold) {
|
||||
return (
|
||||
<li className="mx_EventListSummary" data-scroll-tokens={eventIds} data-expanded={true}>
|
||||
<li className="mx_EventListSummary" data-scroll-tokens={eventIds} data-expanded={true} data-layout={layout}>
|
||||
{ children }
|
||||
</li>
|
||||
);
|
||||
|
@ -92,7 +96,7 @@ const EventListSummary: React.FC<IProps> = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<li className="mx_EventListSummary" data-scroll-tokens={eventIds} data-expanded={expanded + ""}>
|
||||
<li className="mx_EventListSummary" data-scroll-tokens={eventIds} data-expanded={expanded + ""} data-layout={layout}>
|
||||
<AccessibleButton className="mx_EventListSummary_toggle" onClick={toggleExpanded} aria-expanded={expanded}>
|
||||
{ expanded ? _t('collapse') : _t('expand') }
|
||||
</AccessibleButton>
|
||||
|
@ -103,6 +107,7 @@ const EventListSummary: React.FC<IProps> = ({
|
|||
|
||||
EventListSummary.defaultProps = {
|
||||
startExpanded: false,
|
||||
layout: Layout.Group,
|
||||
};
|
||||
|
||||
export default EventListSummary;
|
||||
|
|
|
@ -25,6 +25,7 @@ import SettingsStore from "../../../settings/SettingsStore";
|
|||
import { Layout } from "../../../settings/Layout";
|
||||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import Spinner from './Spinner';
|
||||
|
||||
interface IProps {
|
||||
/**
|
||||
|
@ -45,7 +46,7 @@ interface IProps {
|
|||
/**
|
||||
* The ID of the displayed user
|
||||
*/
|
||||
userId: string;
|
||||
userId?: string;
|
||||
|
||||
/**
|
||||
* The display name of the displayed user
|
||||
|
@ -118,13 +119,16 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
const event = this.fakeEvent(this.state);
|
||||
|
||||
const className = classnames(this.props.className, {
|
||||
"mx_IRCLayout": this.props.layout == Layout.IRC,
|
||||
"mx_GroupLayout": this.props.layout == Layout.Group,
|
||||
"mx_EventTilePreview_loader": !this.props.userId,
|
||||
});
|
||||
|
||||
if (!this.props.userId) return <div className={className}><Spinner /></div>;
|
||||
|
||||
const event = this.fakeEvent(this.state);
|
||||
|
||||
return <div className={className}>
|
||||
<EventTile
|
||||
mxEvent={event}
|
||||
|
|
|
@ -467,7 +467,9 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
const avatar = (
|
||||
<MemberAvatar
|
||||
member={mxEvent.sender}
|
||||
width={32} height={32}
|
||||
fallbackUserId={mxEvent.getSender()}
|
||||
width={32}
|
||||
height={32}
|
||||
viewUserOnClick={true}
|
||||
/>
|
||||
);
|
||||
|
@ -486,7 +488,7 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
// an empty div here, since the panel uses space-between
|
||||
// and we want the same placement of elements
|
||||
info = (
|
||||
<div></div>
|
||||
<div />
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -510,15 +512,15 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
<AccessibleTooltipButton
|
||||
className="mx_ImageView_button mx_ImageView_button_zoomOut"
|
||||
title={_t("Zoom out")}
|
||||
onClick={this.onZoomOutClick}>
|
||||
</AccessibleTooltipButton>
|
||||
onClick={this.onZoomOutClick}
|
||||
/>
|
||||
);
|
||||
zoomInButton = (
|
||||
<AccessibleTooltipButton
|
||||
className="mx_ImageView_button mx_ImageView_button_zoomIn"
|
||||
title={_t("Zoom in")}
|
||||
onClick={this.onZoomInClick}>
|
||||
</AccessibleTooltipButton>
|
||||
onClick={this.onZoomInClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -540,24 +542,24 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
<AccessibleTooltipButton
|
||||
className="mx_ImageView_button mx_ImageView_button_rotateCCW"
|
||||
title={_t("Rotate Left")}
|
||||
onClick={this.onRotateCounterClockwiseClick}>
|
||||
</AccessibleTooltipButton>
|
||||
onClick={this.onRotateCounterClockwiseClick}
|
||||
/>
|
||||
<AccessibleTooltipButton
|
||||
className="mx_ImageView_button mx_ImageView_button_rotateCW"
|
||||
title={_t("Rotate Right")}
|
||||
onClick={this.onRotateClockwiseClick}>
|
||||
</AccessibleTooltipButton>
|
||||
onClick={this.onRotateClockwiseClick}
|
||||
/>
|
||||
<AccessibleTooltipButton
|
||||
className="mx_ImageView_button mx_ImageView_button_download"
|
||||
title={_t("Download")}
|
||||
onClick={this.onDownloadClick}>
|
||||
</AccessibleTooltipButton>
|
||||
onClick={this.onDownloadClick}
|
||||
/>
|
||||
{ contextMenuButton }
|
||||
<AccessibleTooltipButton
|
||||
className="mx_ImageView_button mx_ImageView_button_close"
|
||||
title={_t("Close")}
|
||||
onClick={this.props.onFinished}>
|
||||
</AccessibleTooltipButton>
|
||||
onClick={this.props.onFinished}
|
||||
/>
|
||||
{ this.renderContextMenu() }
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -16,11 +16,13 @@ limitations under the License.
|
|||
|
||||
import classNames from "classnames";
|
||||
import React from "react";
|
||||
import { sanitizedHtmlNode } from "../../../HtmlUtils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
reason: string;
|
||||
htmlReason?: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -51,7 +53,7 @@ export default class InviteReason extends React.PureComponent<IProps, IState> {
|
|||
});
|
||||
|
||||
return <div className={classes}>
|
||||
<div className="mx_InviteReason_reason">{ this.props.reason }</div>
|
||||
<div className="mx_InviteReason_reason">{ this.props.htmlReason ? sanitizedHtmlNode(this.props.htmlReason) : this.props.reason }</div>
|
||||
<div className="mx_InviteReason_view"
|
||||
onClick={this.onViewClick}
|
||||
>
|
||||
|
|
68
src/components/views/elements/JoinRuleDropdown.tsx
Normal file
68
src/components/views/elements/JoinRuleDropdown.tsx
Normal file
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
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 { JoinRule } from 'matrix-js-sdk/src/@types/partials';
|
||||
|
||||
import Dropdown from "./Dropdown";
|
||||
|
||||
interface IProps {
|
||||
value: JoinRule;
|
||||
label: string;
|
||||
width?: number;
|
||||
labelInvite: string;
|
||||
labelPublic: string;
|
||||
labelRestricted?: string; // if omitted then this option will be hidden, e.g if unsupported
|
||||
onChange(value: JoinRule): void;
|
||||
}
|
||||
|
||||
const JoinRuleDropdown = ({
|
||||
label,
|
||||
labelInvite,
|
||||
labelPublic,
|
||||
labelRestricted,
|
||||
value,
|
||||
width = 448,
|
||||
onChange,
|
||||
}: IProps) => {
|
||||
const options = [
|
||||
<div key={JoinRule.Invite} className="mx_JoinRuleDropdown_invite">
|
||||
{ labelInvite }
|
||||
</div>,
|
||||
<div key={JoinRule.Public} className="mx_JoinRuleDropdown_public">
|
||||
{ labelPublic }
|
||||
</div>,
|
||||
];
|
||||
|
||||
if (labelRestricted) {
|
||||
options.unshift(<div key={JoinRule.Restricted} className="mx_JoinRuleDropdown_restricted">
|
||||
{ labelRestricted }
|
||||
</div>);
|
||||
}
|
||||
|
||||
return <Dropdown
|
||||
id="mx_JoinRuleDropdown"
|
||||
className="mx_JoinRuleDropdown"
|
||||
onOptionChange={onChange}
|
||||
menuWidth={width}
|
||||
value={value}
|
||||
label={label}
|
||||
>
|
||||
{ options }
|
||||
</Dropdown>;
|
||||
};
|
||||
|
||||
export default JoinRuleDropdown;
|
|
@ -25,12 +25,31 @@ import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
|
|||
import { isValid3pidInvite } from "../../../RoomInvite";
|
||||
import EventListSummary from "./EventListSummary";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import defaultDispatcher from '../../../dispatcher/dispatcher';
|
||||
import { RightPanelPhases } from '../../../stores/RightPanelStorePhases';
|
||||
import { Action } from '../../../dispatcher/actions';
|
||||
import { SetRightPanelPhasePayload } from '../../../dispatcher/payloads/SetRightPanelPhasePayload';
|
||||
import { jsxJoin } from '../../../utils/ReactUtils';
|
||||
import { EventType } from 'matrix-js-sdk/src/@types/event';
|
||||
import { Layout } from '../../../settings/Layout';
|
||||
|
||||
const onPinnedMessagesClick = (): void => {
|
||||
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
|
||||
action: Action.SetRightPanelPhase,
|
||||
phase: RightPanelPhases.PinnedMessages,
|
||||
allowClose: false,
|
||||
});
|
||||
};
|
||||
|
||||
const SENDER_AS_DISPLAY_NAME_EVENTS = [EventType.RoomServerAcl, EventType.RoomPinnedEvents];
|
||||
|
||||
interface IProps extends Omit<ComponentProps<typeof EventListSummary>, "summaryText" | "summaryMembers"> {
|
||||
// The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left"
|
||||
summaryLength?: number;
|
||||
// The maximum number of avatars to display in the summary
|
||||
avatarsMaxLength?: number;
|
||||
// The currently selected layout
|
||||
layout: Layout;
|
||||
}
|
||||
|
||||
interface IUserEvents {
|
||||
|
@ -57,6 +76,7 @@ enum TransitionType {
|
|||
ChangedAvatar = "changed_avatar",
|
||||
NoChange = "no_change",
|
||||
ServerAcl = "server_acl",
|
||||
ChangedPins = "pinned_messages"
|
||||
}
|
||||
|
||||
const SEP = ",";
|
||||
|
@ -67,6 +87,7 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
|||
summaryLength: 1,
|
||||
threshold: 3,
|
||||
avatarsMaxLength: 5,
|
||||
layout: Layout.Group,
|
||||
};
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
|
@ -89,7 +110,10 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
|||
* `Object.keys(eventAggregates)`.
|
||||
* @returns {string} the textual summary of the aggregated events that occurred.
|
||||
*/
|
||||
private generateSummary(eventAggregates: Record<string, string[]>, orderedTransitionSequences: string[]) {
|
||||
private generateSummary(
|
||||
eventAggregates: Record<string, string[]>,
|
||||
orderedTransitionSequences: string[],
|
||||
): string | JSX.Element {
|
||||
const summaries = orderedTransitionSequences.map((transitions) => {
|
||||
const userNames = eventAggregates[transitions];
|
||||
const nameList = this.renderNameList(userNames);
|
||||
|
@ -118,7 +142,7 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
|||
return null;
|
||||
}
|
||||
|
||||
return summaries.join(", ");
|
||||
return jsxJoin(summaries, ", ");
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -212,7 +236,11 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
|||
* @param {number} repeats the number of times the transition was repeated in a row.
|
||||
* @returns {string} the written Human Readable equivalent of the transition.
|
||||
*/
|
||||
private static getDescriptionForTransition(t: TransitionType, userCount: number, repeats: number) {
|
||||
private static getDescriptionForTransition(
|
||||
t: TransitionType,
|
||||
userCount: number,
|
||||
repeats: number,
|
||||
): string | JSX.Element {
|
||||
// The empty interpolations 'severalUsers' and 'oneUser'
|
||||
// are there only to show translators to non-English languages
|
||||
// that the verb is conjugated to plural or singular Subject.
|
||||
|
@ -295,6 +323,15 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
|||
{ severalUsers: "", count: repeats })
|
||||
: _t("%(oneUser)schanged the server ACLs %(count)s times", { oneUser: "", count: repeats });
|
||||
break;
|
||||
case "pinned_messages":
|
||||
res = (userCount > 1)
|
||||
? _t("%(severalUsers)schanged the <a>pinned messages</a> for the room %(count)s times.",
|
||||
{ severalUsers: "", count: repeats },
|
||||
{ "a": (sub) => <a onClick={onPinnedMessagesClick}> { sub } </a> })
|
||||
: _t("%(oneUser)schanged the <a>pinned messages</a> for the room %(count)s times.",
|
||||
{ oneUser: "", count: repeats },
|
||||
{ "a": (sub) => <a onClick={onPinnedMessagesClick}> { sub } </a> });
|
||||
break;
|
||||
}
|
||||
|
||||
return res;
|
||||
|
@ -313,16 +350,18 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
|||
* if a transition is not recognised.
|
||||
*/
|
||||
private static getTransition(e: IUserEvents): TransitionType {
|
||||
if (e.mxEvent.getType() === 'm.room.third_party_invite') {
|
||||
const type = e.mxEvent.getType();
|
||||
|
||||
if (type === EventType.RoomThirdPartyInvite) {
|
||||
// Handle 3pid invites the same as invites so they get bundled together
|
||||
if (!isValid3pidInvite(e.mxEvent)) {
|
||||
return TransitionType.InviteWithdrawal;
|
||||
}
|
||||
return TransitionType.Invited;
|
||||
}
|
||||
|
||||
if (e.mxEvent.getType() === 'm.room.server_acl') {
|
||||
} else if (type === EventType.RoomServerAcl) {
|
||||
return TransitionType.ServerAcl;
|
||||
} else if (type === EventType.RoomPinnedEvents) {
|
||||
return TransitionType.ChangedPins;
|
||||
}
|
||||
|
||||
switch (e.mxEvent.getContent().membership) {
|
||||
|
@ -411,22 +450,23 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
|||
// Object mapping user IDs to an array of IUserEvents
|
||||
const userEvents: Record<string, IUserEvents[]> = {};
|
||||
eventsToRender.forEach((e, index) => {
|
||||
const userId = e.getType() === 'm.room.server_acl' ? e.getSender() : e.getStateKey();
|
||||
const type = e.getType();
|
||||
const userId = type === EventType.RoomServerAcl ? e.getSender() : e.getStateKey();
|
||||
// Initialise a user's events
|
||||
if (!userEvents[userId]) {
|
||||
userEvents[userId] = [];
|
||||
}
|
||||
|
||||
if (e.getType() === 'm.room.server_acl') {
|
||||
if (SENDER_AS_DISPLAY_NAME_EVENTS.includes(type as EventType)) {
|
||||
latestUserAvatarMember.set(userId, e.sender);
|
||||
} else if (e.target) {
|
||||
latestUserAvatarMember.set(userId, e.target);
|
||||
}
|
||||
|
||||
let displayName = userId;
|
||||
if (e.getType() === 'm.room.third_party_invite') {
|
||||
if (type === EventType.RoomThirdPartyInvite) {
|
||||
displayName = e.getContent().display_name;
|
||||
} else if (e.getType() === 'm.room.server_acl') {
|
||||
} else if (SENDER_AS_DISPLAY_NAME_EVENTS.includes(type as EventType)) {
|
||||
displayName = e.sender.name;
|
||||
} else if (e.target) {
|
||||
displayName = e.target.name;
|
||||
|
@ -453,6 +493,7 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
|||
startExpanded={this.props.startExpanded}
|
||||
children={this.props.children}
|
||||
summaryMembers={[...latestUserAvatarMember.values()]}
|
||||
layout={this.props.layout}
|
||||
summaryText={this.generateSummary(aggregate.names, orderedTransitionSequences)} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -92,7 +92,7 @@ const MiniAvatarUploader: React.FC<IProps> = ({ hasAvatar, hasAvatarLabel, noAva
|
|||
<div className="mx_MiniAvatarUploader_indicator">
|
||||
{ busy ?
|
||||
<Spinner w={20} h={20} /> :
|
||||
<div className="mx_MiniAvatarUploader_cameraIcon"></div> }
|
||||
<div className="mx_MiniAvatarUploader_cameraIcon" /> }
|
||||
</div>
|
||||
|
||||
<div className={classNames("mx_Tooltip", {
|
||||
|
|
|
@ -192,7 +192,8 @@ class Pill extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
onUserPillClicked = () => {
|
||||
onUserPillClicked = (e) => {
|
||||
e.preventDefault();
|
||||
dis.dispatch({
|
||||
action: Action.ViewUser,
|
||||
member: this.state.member,
|
||||
|
@ -258,7 +259,10 @@ class Pill extends React.Component {
|
|||
linkText = groupId;
|
||||
if (this.props.shouldShowPillAvatar) {
|
||||
avatar = <BaseAvatar
|
||||
name={name || groupId} width={16} height={16} aria-hidden="true"
|
||||
name={name || groupId}
|
||||
width={16}
|
||||
height={16}
|
||||
aria-hidden="true"
|
||||
url={avatarUrl ? mediaFromMxc(avatarUrl).getSquareThumbnailHttp(16) : null} />;
|
||||
}
|
||||
pillClass = 'mx_GroupPill';
|
||||
|
|
|
@ -134,8 +134,10 @@ export default class PowerSelector extends React.Component {
|
|||
const label = typeof this.props.label === "undefined" ? _t("Power level") : this.props.label;
|
||||
if (this.state.custom) {
|
||||
picker = (
|
||||
<Field type="number"
|
||||
label={label} max={this.props.maxValue}
|
||||
<Field
|
||||
type="number"
|
||||
label={label}
|
||||
max={this.props.maxValue}
|
||||
onBlur={this.onCustomBlur}
|
||||
onKeyDown={this.onCustomKeyDown}
|
||||
onChange={this.onCustomChange}
|
||||
|
@ -157,9 +159,12 @@ export default class PowerSelector extends React.Component {
|
|||
});
|
||||
|
||||
picker = (
|
||||
<Field element="select"
|
||||
label={label} onChange={this.onSelectChange}
|
||||
value={String(this.state.selectValue)} disabled={this.props.disabled}
|
||||
<Field
|
||||
element="select"
|
||||
label={label}
|
||||
onChange={this.onSelectChange}
|
||||
value={String(this.state.selectValue)}
|
||||
disabled={this.props.disabled}
|
||||
>
|
||||
{ options }
|
||||
</Field>
|
||||
|
|
|
@ -19,6 +19,7 @@ import React from 'react';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import { UNSTABLE_ELEMENT_REPLY_IN_THREAD } from "matrix-js-sdk/src/@types/event";
|
||||
import { makeUserPermalink, RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { Layout } from "../../../settings/Layout";
|
||||
|
@ -206,15 +207,28 @@ export default class ReplyThread extends React.Component<IProps, IState> {
|
|||
return { body, html };
|
||||
}
|
||||
|
||||
public static makeReplyMixIn(ev: MatrixEvent) {
|
||||
public static makeReplyMixIn(ev: MatrixEvent, replyInThread: boolean) {
|
||||
if (!ev) return {};
|
||||
return {
|
||||
|
||||
const replyMixin = {
|
||||
'm.relates_to': {
|
||||
'm.in_reply_to': {
|
||||
'event_id': ev.getId(),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* Rendering hint for threads, only attached if true to make
|
||||
* sure that Element does not start sending that property for all events
|
||||
*/
|
||||
if (replyInThread) {
|
||||
const inReplyTo = replyMixin['m.relates_to']['m.in_reply_to'];
|
||||
inReplyTo[UNSTABLE_ELEMENT_REPLY_IN_THREAD.name] = replyInThread;
|
||||
}
|
||||
|
||||
return replyMixin;
|
||||
}
|
||||
|
||||
public static makeThread(
|
||||
|
|
|
@ -1,27 +1,27 @@
|
|||
|
||||
import React from 'react'; // eslint-disable-line no-unused-vars
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
//see src/resizer for the actual resizing code, this is just the DOM for the resize handle
|
||||
const ResizeHandle = (props) => {
|
||||
interface IResizeHandleProps {
|
||||
vertical?: boolean;
|
||||
reverse?: boolean;
|
||||
id?: string;
|
||||
passRef?: React.RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
const ResizeHandle: React.FC<IResizeHandleProps> = ({ vertical, reverse, id, passRef }) => {
|
||||
const classNames = ['mx_ResizeHandle'];
|
||||
if (props.vertical) {
|
||||
if (vertical) {
|
||||
classNames.push('mx_ResizeHandle_vertical');
|
||||
} else {
|
||||
classNames.push('mx_ResizeHandle_horizontal');
|
||||
}
|
||||
if (props.reverse) {
|
||||
if (reverse) {
|
||||
classNames.push('mx_ResizeHandle_reverse');
|
||||
}
|
||||
return (
|
||||
<div className={classNames.join(' ')} data-id={props.id}><div /></div>
|
||||
<div ref={passRef} className={classNames.join(' ')} data-id={id}><div /></div>
|
||||
);
|
||||
};
|
||||
|
||||
ResizeHandle.propTypes = {
|
||||
vertical: PropTypes.bool,
|
||||
reverse: PropTypes.bool,
|
||||
id: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ResizeHandle;
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { createRef } from "react";
|
||||
import React, { createRef, KeyboardEventHandler } from "react";
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import withValidation from './Validation';
|
||||
|
@ -28,6 +28,7 @@ interface IProps {
|
|||
label?: string;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
onKeyDown?: KeyboardEventHandler;
|
||||
onChange?(value: string): void;
|
||||
}
|
||||
|
||||
|
@ -70,6 +71,8 @@ export default class RoomAliasField extends React.PureComponent<IProps, IState>
|
|||
value={this.props.value.substring(1, this.props.value.length - this.props.domain.length - 1)}
|
||||
maxLength={maxlength}
|
||||
disabled={this.props.disabled}
|
||||
autoComplete="off"
|
||||
onKeyDown={this.props.onKeyDown}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -14,17 +14,17 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import React, { ReactNode } from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import StyledRadioButton from "./StyledRadioButton";
|
||||
|
||||
interface IDefinition<T extends string> {
|
||||
export interface IDefinition<T extends string> {
|
||||
value: T;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
label: React.ReactChild;
|
||||
description?: React.ReactChild;
|
||||
label: ReactNode;
|
||||
description?: ReactNode;
|
||||
checked?: boolean; // If provided it will override the value comparison done in the group
|
||||
}
|
||||
|
||||
|
@ -59,7 +59,7 @@ function StyledRadioGroup<T extends string>({
|
|||
checked={d.checked !== undefined ? d.checked : d.value === value}
|
||||
name={name}
|
||||
value={d.value}
|
||||
disabled={disabled || d.disabled}
|
||||
disabled={d.disabled ?? disabled}
|
||||
outlined={outlined}
|
||||
>
|
||||
{ d.label }
|
||||
|
|
|
@ -166,8 +166,7 @@ export default class Tooltip extends React.Component<IProps> {
|
|||
public render() {
|
||||
// Render a placeholder
|
||||
return (
|
||||
<div className={this.props.className}>
|
||||
</div>
|
||||
<div className={this.props.className} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue