Apply prettier formatting

This commit is contained in:
Michael Weimann 2022-12-12 12:24:14 +01:00
parent 1cac306093
commit 526645c791
No known key found for this signature in database
GPG key ID: 53F535A266BB9584
1576 changed files with 65385 additions and 62478 deletions

View file

@ -14,30 +14,31 @@
limitations under the License.
*/
import React, { HTMLAttributes, InputHTMLAttributes, ReactHTML, ReactNode } from 'react';
import classnames from 'classnames';
import React, { HTMLAttributes, InputHTMLAttributes, ReactHTML, ReactNode } from "react";
import classnames from "classnames";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
export type ButtonEvent = React.MouseEvent<Element> | React.KeyboardEvent<Element> | React.FormEvent<Element>;
type AccessibleButtonKind = | 'primary'
| 'primary_outline'
| 'primary_sm'
| 'secondary'
| 'secondary_content'
| 'content_inline'
| 'danger'
| 'danger_outline'
| 'danger_sm'
| 'danger_inline'
| 'link'
| 'link_inline'
| 'link_sm'
| 'confirm_sm'
| 'cancel_sm'
| 'icon';
type AccessibleButtonKind =
| "primary"
| "primary_outline"
| "primary_sm"
| "secondary"
| "secondary_content"
| "content_inline"
| "danger"
| "danger_outline"
| "danger_sm"
| "danger_inline"
| "link"
| "link_inline"
| "link_sm"
| "confirm_sm"
| "cancel_sm"
| "icon";
/**
* This type construct allows us to specifically pass those props down to the element were creating that the element
@ -49,9 +50,10 @@ type AccessibleButtonKind = | 'primary'
*/
type DynamicHtmlElementProps<T extends keyof JSX.IntrinsicElements> =
JSX.IntrinsicElements[T] extends HTMLAttributes<{}> ? DynamicElementProps<T> : DynamicElementProps<"div">;
type DynamicElementProps<T extends keyof JSX.IntrinsicElements> =
Partial<Omit<JSX.IntrinsicElements[T], 'ref' | 'onClick' | 'onMouseDown' | 'onKeyUp' | 'onKeyDown'>>
& Omit<InputHTMLAttributes<Element>, 'onClick'>;
type DynamicElementProps<T extends keyof JSX.IntrinsicElements> = Partial<
Omit<JSX.IntrinsicElements[T], "ref" | "onClick" | "onMouseDown" | "onKeyUp" | "onKeyDown">
> &
Omit<InputHTMLAttributes<Element>, "onClick">;
/**
* children: React's magic prop. Represents all children given to the element.
@ -156,23 +158,19 @@ export default function AccessibleButton<T extends keyof JSX.IntrinsicElements>(
// Pass through the ref - used for keyboard shortcut access to some buttons
newProps.ref = inputRef;
newProps.className = classnames(
"mx_AccessibleButton",
className,
{
"mx_AccessibleButton_hasKind": kind,
[`mx_AccessibleButton_kind_${kind}`]: kind,
"mx_AccessibleButton_disabled": disabled,
},
);
newProps.className = classnames("mx_AccessibleButton", className, {
mx_AccessibleButton_hasKind: kind,
[`mx_AccessibleButton_kind_${kind}`]: kind,
mx_AccessibleButton_disabled: disabled,
});
// React.createElement expects InputHTMLAttributes
return React.createElement(element, newProps, children);
}
AccessibleButton.defaultProps = {
element: 'div' as keyof ReactHTML,
role: 'button',
element: "div" as keyof ReactHTML,
role: "button",
tabIndex: 0,
};

View file

@ -15,10 +15,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { SyntheticEvent, FocusEvent } from 'react';
import React, { SyntheticEvent, FocusEvent } from "react";
import AccessibleButton from "./AccessibleButton";
import Tooltip, { Alignment } from './Tooltip';
import Tooltip, { Alignment } from "./Tooltip";
interface IProps extends React.ComponentProps<typeof AccessibleButton> {
title?: string;
@ -75,14 +75,12 @@ export default class AccessibleTooltipButton extends React.PureComponent<IProps,
render() {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { title, tooltip, children, tooltipClassName, forceHide, alignment, onHideTooltip,
...props } = this.props;
const { title, tooltip, children, tooltipClassName, forceHide, alignment, onHideTooltip, ...props } =
this.props;
const tip = this.state.hover && (title || tooltip) && <Tooltip
tooltipClassName={tooltipClassName}
label={tooltip || title}
alignment={alignment}
/>;
const tip = this.state.hover && (title || tooltip) && (
<Tooltip tooltipClassName={tooltipClassName} label={tooltip || title} alignment={alignment} />
);
return (
<AccessibleButton
{...props}
@ -92,9 +90,9 @@ export default class AccessibleTooltipButton extends React.PureComponent<IProps,
onBlur={this.hideTooltip || props.onBlur}
aria-label={title || props["aria-label"]}
>
{ children }
{ this.props.label }
{ (tooltip || title) && tip }
{children}
{this.props.label}
{(tooltip || title) && tip}
</AccessibleButton>
);
}

View file

@ -16,17 +16,17 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import url from 'url';
import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
import React from "react";
import url from "url";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
import { _t } from "../../../languageHandler";
import SdkConfig from "../../../SdkConfig";
import WidgetUtils from "../../../utils/WidgetUtils";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import MemberAvatar from '../avatars/MemberAvatar';
import BaseAvatar from '../avatars/BaseAvatar';
import AccessibleButton from './AccessibleButton';
import MemberAvatar from "../avatars/MemberAvatar";
import BaseAvatar from "../avatars/BaseAvatar";
import AccessibleButton from "./AccessibleButton";
import TextWithTooltip from "./TextWithTooltip";
interface IProps {
@ -68,14 +68,14 @@ export default class AppPermission extends React.Component<IProps, IState> {
};
}
private parseWidgetUrl(): { isWrapped: boolean, widgetDomain: string } {
private parseWidgetUrl(): { isWrapped: boolean; widgetDomain: string } {
const widgetUrl = url.parse(this.props.url);
const params = new URLSearchParams(widgetUrl.search);
// HACK: We're relying on the query params when we should be relying on the widget's `data`.
// This is a workaround for Scalar.
if (WidgetUtils.isScalarUrl(this.props.url) && params && params.get('url')) {
const unwrappedUrl = url.parse(params.get('url'));
if (WidgetUtils.isScalarUrl(this.props.url) && params && params.get("url")) {
const unwrappedUrl = url.parse(params.get("url"));
return {
widgetDomain: unwrappedUrl.host || unwrappedUrl.hostname,
isWrapped: true,
@ -94,58 +94,67 @@ export default class AppPermission extends React.Component<IProps, IState> {
const displayName = this.state.roomMember ? this.state.roomMember.name : this.props.creatorUserId;
const userId = displayName === this.props.creatorUserId ? null : this.props.creatorUserId;
const avatar = this.state.roomMember
? <MemberAvatar member={this.state.roomMember} width={38} height={38} />
: <BaseAvatar name={this.props.creatorUserId} width={38} height={38} />;
const avatar = this.state.roomMember ? (
<MemberAvatar member={this.state.roomMember} width={38} height={38} />
) : (
<BaseAvatar name={this.props.creatorUserId} width={38} height={38} />
);
const warningTooltipText = (
<div>
{ _t("Any of the following data may be shared:") }
{_t("Any of the following data may be shared:")}
<ul>
<li>{ _t("Your display name") }</li>
<li>{ _t("Your avatar URL") }</li>
<li>{ _t("Your user ID") }</li>
<li>{ _t("Your theme") }</li>
<li>{ _t("%(brand)s URL", { brand }) }</li>
<li>{ _t("Room ID") }</li>
<li>{ _t("Widget ID") }</li>
<li>{_t("Your display name")}</li>
<li>{_t("Your avatar URL")}</li>
<li>{_t("Your user ID")}</li>
<li>{_t("Your theme")}</li>
<li>{_t("%(brand)s URL", { brand })}</li>
<li>{_t("Room ID")}</li>
<li>{_t("Widget ID")}</li>
</ul>
</div>
);
const warningTooltip = (
<TextWithTooltip tooltip={warningTooltipText} tooltipClass='mx_AppPermissionWarning_tooltip mx_Tooltip_dark'>
<span className='mx_AppPermissionWarning_helpIcon' />
<TextWithTooltip
tooltip={warningTooltipText}
tooltipClass="mx_AppPermissionWarning_tooltip mx_Tooltip_dark"
>
<span className="mx_AppPermissionWarning_helpIcon" />
</TextWithTooltip>
);
// Due to i18n limitations, we can't dedupe the code for variables in these two messages.
const warning = this.state.isWrapped
? _t("Using this widget may share data <helpIcon /> with %(widgetDomain)s & your integration manager.",
{ widgetDomain: this.state.widgetDomain }, { helpIcon: () => warningTooltip })
: _t("Using this widget may share data <helpIcon /> with %(widgetDomain)s.",
{ widgetDomain: this.state.widgetDomain }, { helpIcon: () => warningTooltip });
? _t(
"Using this widget may share data <helpIcon /> with %(widgetDomain)s & your integration manager.",
{ widgetDomain: this.state.widgetDomain },
{ helpIcon: () => warningTooltip },
)
: _t(
"Using this widget may share data <helpIcon /> with %(widgetDomain)s.",
{ widgetDomain: this.state.widgetDomain },
{ helpIcon: () => warningTooltip },
);
const encryptionWarning = this.props.isRoomEncrypted ? _t("Widgets do not use message encryption.") : null;
return (
<div className='mx_AppPermissionWarning'>
<div className='mx_AppPermissionWarning_row mx_AppPermissionWarning_bolder mx_AppPermissionWarning_smallText'>
{ _t("Widget added by") }
<div className="mx_AppPermissionWarning">
<div className="mx_AppPermissionWarning_row mx_AppPermissionWarning_bolder mx_AppPermissionWarning_smallText">
{_t("Widget added by")}
</div>
<div className='mx_AppPermissionWarning_row'>
{ avatar }
<h4 className='mx_AppPermissionWarning_bolder'>{ displayName }</h4>
<div className='mx_AppPermissionWarning_smallText'>{ userId }</div>
<div className="mx_AppPermissionWarning_row">
{avatar}
<h4 className="mx_AppPermissionWarning_bolder">{displayName}</h4>
<div className="mx_AppPermissionWarning_smallText">{userId}</div>
</div>
<div className='mx_AppPermissionWarning_row mx_AppPermissionWarning_smallText'>
{ warning }
<div className="mx_AppPermissionWarning_row mx_AppPermissionWarning_smallText">{warning}</div>
<div className="mx_AppPermissionWarning_row mx_AppPermissionWarning_smallText">
{_t("This widget may use cookies.")}&nbsp;{encryptionWarning}
</div>
<div className='mx_AppPermissionWarning_row mx_AppPermissionWarning_smallText'>
{ _t("This widget may use cookies.") }&nbsp;{ encryptionWarning }
</div>
<div className='mx_AppPermissionWarning_row'>
<AccessibleButton kind='primary_sm' onClick={this.props.onPermissionGranted}>
{ _t("Continue") }
<div className="mx_AppPermissionWarning_row">
<AccessibleButton kind="primary_sm" onClick={this.props.onPermissionGranted}>
{_t("Continue")}
</AccessibleButton>
</div>
</div>

View file

@ -17,20 +17,20 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import url from 'url';
import React, { ContextType, createRef, MutableRefObject, ReactNode } from 'react';
import classNames from 'classnames';
import url from "url";
import React, { ContextType, createRef, MutableRefObject, ReactNode } from "react";
import classNames from "classnames";
import { MatrixCapabilities } from "matrix-widget-api";
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
import { logger } from "matrix-js-sdk/src/logger";
import AccessibleButton from './AccessibleButton';
import { _t } from '../../../languageHandler';
import AppPermission from './AppPermission';
import AppWarning from './AppWarning';
import Spinner from './Spinner';
import dis from '../../../dispatcher/dispatcher';
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
import AccessibleButton from "./AccessibleButton";
import { _t } from "../../../languageHandler";
import AppPermission from "./AppPermission";
import AppWarning from "./AppWarning";
import Spinner from "./Spinner";
import dis from "../../../dispatcher/dispatcher";
import ActiveWidgetStore from "../../../stores/ActiveWidgetStore";
import SettingsStore from "../../../settings/SettingsStore";
import { aboveLeftOf, ContextMenuButton } from "../../structures/ContextMenu";
import PersistedElement, { getPersistKey } from "./PersistedElement";
@ -38,18 +38,18 @@ import { WidgetType } from "../../../widgets/WidgetType";
import { ElementWidget, StopGapWidget } from "../../../stores/widgets/StopGapWidget";
import WidgetContextMenu from "../context_menus/WidgetContextMenu";
import WidgetAvatar from "../avatars/WidgetAvatar";
import LegacyCallHandler from '../../../LegacyCallHandler';
import LegacyCallHandler from "../../../LegacyCallHandler";
import { IApp } from "../../../stores/WidgetStore";
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
import { OwnProfileStore } from '../../../stores/OwnProfileStore';
import { UPDATE_EVENT } from '../../../stores/AsyncStore';
import WidgetUtils from '../../../utils/WidgetUtils';
import { OwnProfileStore } from "../../../stores/OwnProfileStore";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import WidgetUtils from "../../../utils/WidgetUtils";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { ActionPayload } from "../../../dispatcher/payloads";
import { Action } from '../../../dispatcher/actions';
import { ElementWidgetCapabilities } from '../../../stores/widgets/ElementWidgetCapabilities';
import { WidgetMessagingStore } from '../../../stores/widgets/WidgetMessagingStore';
import { SdkContextClass } from '../../../contexts/SDKContext';
import { Action } from "../../../dispatcher/actions";
import { ElementWidgetCapabilities } from "../../../stores/widgets/ElementWidgetCapabilities";
import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore";
import { SdkContextClass } from "../../../contexts/SDKContext";
interface IProps {
app: IApp;
@ -170,7 +170,8 @@ export default class AppTile extends React.Component<IProps, IState> {
private onUserLeftRoom() {
const isActiveWidget = ActiveWidgetStore.instance.getWidgetPersistence(
this.props.app.id, this.props.app.roomId,
this.props.app.id,
this.props.app.roomId,
);
if (isActiveWidget) {
// We just left the room that the active widget was from.
@ -249,9 +250,14 @@ export default class AppTile extends React.Component<IProps, IState> {
const parentContentProtocol = window.location.protocol;
const u = url.parse(this.props.app.url);
const childContentProtocol = u.protocol;
if (parentContentProtocol === 'https:' && childContentProtocol !== 'https:') {
logger.warn("Refusing to load mixed-content app:",
parentContentProtocol, childContentProtocol, window.location, this.props.app.url);
if (parentContentProtocol === "https:" && childContentProtocol !== "https:") {
logger.warn(
"Refusing to load mixed-content app:",
parentContentProtocol,
childContentProtocol,
window.location,
this.props.app.url,
);
return true;
}
return false;
@ -363,7 +369,8 @@ export default class AppTile extends React.Component<IProps, IState> {
* @private
* @returns {Promise<*>} Resolves when the widget is terminated, or timeout passed.
*/
private async endWidgetActions(): Promise<void> { // widget migration dev note: async to maintain signature
private async endWidgetActions(): Promise<void> {
// widget migration dev note: async to maintain signature
// HACK: This is a really dirty way to ensure that Jitsi cleans up
// its hold on the webcam. Without this, the widget holds a media
// stream open, even after death. See https://github.com/vector-im/element-web/issues/7351
@ -374,7 +381,7 @@ export default class AppTile extends React.Component<IProps, IState> {
// the iframe at a page that is reasonably safe to use in the
// event the iframe doesn't wink away.
// This is relative to where the Element instance is located.
this.iframe.src = 'about:blank';
this.iframe.src = "about:blank";
}
if (WidgetType.JITSI.matches(this.props.app.type) && this.props.room) {
@ -400,20 +407,21 @@ export default class AppTile extends React.Component<IProps, IState> {
private onAction = (payload: ActionPayload): void => {
switch (payload.action) {
case 'm.sticker':
if (payload.widgetId === this.props.app.id &&
case "m.sticker":
if (
payload.widgetId === this.props.app.id &&
this.sgWidget.widgetApi.hasCapability(MatrixCapabilities.StickerSending)
) {
dis.dispatch({
action: 'post_sticker_message',
action: "post_sticker_message",
data: {
...payload.data,
threadId: this.props.threadId,
},
});
dis.dispatch({ action: 'stickerpicker_close' });
dis.dispatch({ action: "stickerpicker_close" });
} else {
logger.warn('Ignoring sticker message. Invalid capability');
logger.warn("Ignoring sticker message. Invalid capability");
}
break;
@ -432,15 +440,17 @@ export default class AppTile extends React.Component<IProps, IState> {
const current = SettingsStore.getValue("allowedWidgets", roomId);
if (this.props.app.eventId !== undefined) current[this.props.app.eventId] = true;
const level = SettingsStore.firstSupportedLevel("allowedWidgets");
SettingsStore.setValue("allowedWidgets", roomId, level, current).then(() => {
this.setState({ hasPermissionToLoad: true });
SettingsStore.setValue("allowedWidgets", roomId, level, current)
.then(() => {
this.setState({ hasPermissionToLoad: true });
// Fetch a token for the integration manager, now that we're allowed to
this.startWidget();
}).catch(err => {
logger.error(err);
// We don't really need to do anything about this - the user will just hit the button again.
});
// Fetch a token for the integration manager, now that we're allowed to
this.startWidget();
})
.catch((err) => {
logger.error(err);
// We don't really need to do anything about this - the user will just hit the button again.
});
};
private formatAppTileName(): string {
@ -463,7 +473,7 @@ export default class AppTile extends React.Component<IProps, IState> {
private getTileTitle(): JSX.Element {
const name = this.formatAppTileName();
const titleSpacer = <span>&nbsp;-&nbsp;</span>;
let title = '';
let title = "";
if (this.props.widgetPageTitle && this.props.widgetPageTitle !== this.formatAppTileName()) {
title = this.props.widgetPageTitle;
}
@ -471,8 +481,11 @@ export default class AppTile extends React.Component<IProps, IState> {
return (
<span>
<WidgetAvatar app={this.props.app} />
<b>{ name }</b>
<span>{ title ? titleSpacer : '' }{ title }</span>
<b>{name}</b>
<span>
{title ? titleSpacer : ""}
{title}
</span>
</span>
);
}
@ -499,16 +512,22 @@ export default class AppTile extends React.Component<IProps, IState> {
}
// Using Object.assign workaround as the following opens in a new window instead of a new tab.
// window.open(this._getPopoutUrl(), '_blank', 'noopener=yes');
Object.assign(document.createElement('a'),
{ target: '_blank', href: this.sgWidget.popoutUrl, rel: 'noreferrer noopener' }).click();
Object.assign(document.createElement("a"), {
target: "_blank",
href: this.sgWidget.popoutUrl,
rel: "noreferrer noopener",
}).click();
};
private onToggleMaximisedClick = (): void => {
if (!this.props.room) return; // ignore action - it shouldn't even be visible
const targetContainer =
WidgetLayoutStore.instance.isInContainer(this.props.room, this.props.app, Container.Center)
? Container.Top
: Container.Center;
const targetContainer = WidgetLayoutStore.instance.isInContainer(
this.props.room,
this.props.app,
Container.Center,
)
? Container.Top
: Container.Center;
WidgetLayoutStore.instance.moveToContainer(this.props.room, this.props.app, targetContainer);
};
@ -533,18 +552,19 @@ export default class AppTile extends React.Component<IProps, IState> {
// 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 allow-downloads";
// Additional iframe feature permissions
// (see - https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-permissions-in-cross-origin-iframes and https://wicg.github.io/feature-policy/)
const iframeFeatures = "microphone; camera; encrypted-media; autoplay; display-capture; clipboard-write; " +
"clipboard-read;";
const iframeFeatures =
"microphone; camera; encrypted-media; autoplay; display-capture; clipboard-write; " + "clipboard-read;";
const appTileBodyClass = 'mx_AppTileBody' + (this.props.miniMode ? '_mini ' : ' ');
const appTileBodyClass = "mx_AppTileBody" + (this.props.miniMode ? "_mini " : " ");
const appTileBodyStyles = {};
if (this.props.pointerEvents) {
appTileBodyStyles['pointerEvents'] = this.props.pointerEvents;
appTileBodyStyles["pointerEvents"] = this.props.pointerEvents;
}
const loadingElement = (
@ -577,8 +597,11 @@ export default class AppTile extends React.Component<IProps, IState> {
);
} else if (this.state.initialising || !this.state.isUserProfileReady) {
appTileBody = (
<div className={appTileBodyClass + (this.state.loading ? 'mx_AppLoading' : '')} style={appTileBodyStyles}>
{ loadingElement }
<div
className={appTileBodyClass + (this.state.loading ? "mx_AppLoading" : "")}
style={appTileBodyStyles}
>
{loadingElement}
</div>
);
} else {
@ -590,8 +613,11 @@ export default class AppTile extends React.Component<IProps, IState> {
);
} else {
appTileBody = (
<div className={appTileBodyClass + (this.state.loading ? 'mx_AppLoading' : '')} style={appTileBodyStyles}>
{ this.state.loading && loadingElement }
<div
className={appTileBodyClass + (this.state.loading ? "mx_AppLoading" : "")}
style={appTileBodyStyles}
>
{this.state.loading && loadingElement}
<iframe
title={widgetTitle}
allow={iframeFeatures}
@ -616,15 +642,17 @@ export default class AppTile extends React.Component<IProps, IState> {
// otherwise there are issues that the PiP view is drawn UNDER another widget (Persistent app) when dragged around.
const zIndexAboveOtherPersistentElements = 101;
appTileBody = <div className="mx_AppTile_persistedWrapper">
<PersistedElement
zIndex={this.props.miniMode ? zIndexAboveOtherPersistentElements : 9}
persistKey={this.persistKey}
moveRef={this.props.movePersistedElement}
>
{ appTileBody }
</PersistedElement>
</div>;
appTileBody = (
<div className="mx_AppTile_persistedWrapper">
<PersistedElement
zIndex={this.props.miniMode ? zIndexAboveOtherPersistentElements : 9}
persistKey={this.persistKey}
moveRef={this.props.movePersistedElement}
>
{appTileBody}
</PersistedElement>
</div>
);
}
}
}
@ -656,57 +684,70 @@ export default class AppTile extends React.Component<IProps, IState> {
const layoutButtons: ReactNode[] = [];
if (this.props.showLayoutButtons) {
const isMaximised = WidgetLayoutStore.instance.
isInContainer(this.props.room, this.props.app, Container.Center);
const isMaximised = WidgetLayoutStore.instance.isInContainer(
this.props.room,
this.props.app,
Container.Center,
);
const maximisedClasses = classNames({
"mx_AppTileMenuBar_iconButton": true,
"mx_AppTileMenuBar_iconButton_collapse": isMaximised,
"mx_AppTileMenuBar_iconButton_maximise": !isMaximised,
mx_AppTileMenuBar_iconButton: true,
mx_AppTileMenuBar_iconButton_collapse: isMaximised,
mx_AppTileMenuBar_iconButton_maximise: !isMaximised,
});
layoutButtons.push(<AccessibleButton
key="toggleMaximised"
className={maximisedClasses}
title={
isMaximised ? _t("Un-maximise") : _t("Maximise")
}
onClick={this.onToggleMaximisedClick}
/>);
layoutButtons.push(
<AccessibleButton
key="toggleMaximised"
className={maximisedClasses}
title={isMaximised ? _t("Un-maximise") : _t("Maximise")}
onClick={this.onToggleMaximisedClick}
/>,
);
layoutButtons.push(<AccessibleButton
key="minimise"
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_minimise"
title={_t("Minimise")}
onClick={this.onMinimiseClicked}
/>);
layoutButtons.push(
<AccessibleButton
key="minimise"
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_minimise"
title={_t("Minimise")}
onClick={this.onMinimiseClicked}
/>,
);
}
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' : "none") }}>
{ this.props.showTitle && this.getTileTitle() }
</span>
<span className="mx_AppTileMenuBarWidgets">
{ layoutButtons }
{ (this.props.showPopout && !this.state.requiresClient) && <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>
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" : "none" }}
>
{this.props.showTitle && this.getTileTitle()}
</span>
<span className="mx_AppTileMenuBarWidgets">
{layoutButtons}
{this.props.showPopout && !this.state.requiresClient && (
<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>
{ contextMenu }
</React.Fragment>;
{contextMenu}
</React.Fragment>
);
}
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
interface IProps {
errorMsg?: string;
@ -22,12 +22,12 @@ interface IProps {
const AppWarning: React.FC<IProps> = (props) => {
return (
<div className='mx_AppPermissionWarning'>
<div className='mx_AppPermissionWarningImage'>
<img src={require("../../../../res/img/warning.svg").default} alt='' />
<div className="mx_AppPermissionWarning">
<div className="mx_AppPermissionWarningImage">
<img src={require("../../../../res/img/warning.svg").default} alt="" />
</div>
<div className='mx_AppPermissionWarningText'>
<span className='mx_AppPermissionWarningTextLabel'>{ props.errorMsg || "Error" }</span>
<div className="mx_AppPermissionWarningText">
<span className="mx_AppPermissionWarningTextLabel">{props.errorMsg || "Error"}</span>
</div>
</div>
);

View file

@ -30,13 +30,13 @@ interface IProps {
className?: string;
}
const CopyableText: React.FC<IProps> = ({ children, getTextToCopy, border=true, className }) => {
const CopyableText: React.FC<IProps> = ({ children, getTextToCopy, border = true, className }) => {
const [tooltip, setTooltip] = useState<string | undefined>(undefined);
const onCopyClickInternal = async (e: ButtonEvent) => {
e.preventDefault();
const successful = await copyPlaintext(getTextToCopy());
setTooltip(successful ? _t('Copied!') : _t('Failed to copy'));
setTooltip(successful ? _t("Copied!") : _t("Failed to copy"));
};
const onHideTooltip = () => {
@ -49,15 +49,17 @@ const CopyableText: React.FC<IProps> = ({ children, getTextToCopy, border=true,
mx_CopyableText_border: border,
});
return <div className={combinedClassName}>
{ children }
<AccessibleTooltipButton
title={tooltip ?? _t("Copy")}
onClick={onCopyClickInternal}
className="mx_CopyableText_copyButton"
onHideTooltip={onHideTooltip}
/>
</div>;
return (
<div className={combinedClassName}>
{children}
<AccessibleTooltipButton
title={tooltip ?? _t("Copy")}
onClick={onCopyClickInternal}
className="mx_CopyableText_copyButton"
onHideTooltip={onHideTooltip}
/>
</div>
);
};
export default CopyableText;

View file

@ -14,14 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import classNames from 'classnames';
import React from "react";
import classNames from "classnames";
import { _t } from '../../../languageHandler';
import { _t } from "../../../languageHandler";
import BaseDialog from "..//dialogs/BaseDialog";
import DialogButtons from "./DialogButtons";
import AccessibleButton from './AccessibleButton';
import TabbedView, { Tab, TabLocation } from '../../structures/TabbedView';
import AccessibleButton from "./AccessibleButton";
import TabbedView, { Tab, TabLocation } from "../../structures/TabbedView";
import PlatformPeg from "../../../PlatformPeg";
export function getDesktopCapturerSources(): Promise<Array<DesktopCapturerSource>> {
@ -30,10 +30,7 @@ export function getDesktopCapturerSources(): Promise<Array<DesktopCapturerSource
height: 176,
width: 312,
},
types: [
"screen",
"window",
],
types: ["screen", "window"],
};
return PlatformPeg.get().getDesktopCapturerSources(options);
}
@ -70,11 +67,8 @@ export class ExistingSource extends React.Component<ExistingSourceIProps> {
title={this.props.source.name}
onClick={this.onClick}
>
<img
className={thumbnailClasses}
src={this.props.source.thumbnailURL}
/>
<span className="mx_desktopCapturerSourcePicker_source_name">{ this.props.source.name }</span>
<img className={thumbnailClasses} src={this.props.source.thumbnailURL} />
<span className="mx_desktopCapturerSourcePicker_source_name">{this.props.source.name}</span>
</AccessibleButton>
);
}
@ -89,10 +83,7 @@ export interface PickerIProps {
onFinished(sourceId: string): void;
}
export default class DesktopCapturerSourcePicker extends React.Component<
PickerIProps,
PickerIState
> {
export default class DesktopCapturerSourcePicker extends React.Component<PickerIProps, PickerIState> {
interval: number;
constructor(props: PickerIProps) {
@ -142,22 +133,20 @@ export default class DesktopCapturerSourcePicker extends React.Component<
};
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 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}
/>
);
});
return new Tab(type, label, null, (
<div className="mx_desktopCapturerSourcePicker_tab">
{ sources }
</div>
));
return new Tab(type, label, null, <div className="mx_desktopCapturerSourcePicker_tab">{sources}</div>);
}
render() {

View file

@ -26,12 +26,14 @@ interface IProps {
export default class DialPadBackspaceButton extends React.PureComponent<IProps> {
render() {
return <div className="mx_DialPadBackspaceButtonWrapper">
<AccessibleButton
className="mx_DialPadBackspaceButton"
onClick={this.props.onBackspacePress}
aria-label={_t("Backspace")}
/>
</div>;
return (
<div className="mx_DialPadBackspaceButtonWrapper">
<AccessibleButton
className="mx_DialPadBackspaceButton"
onClick={this.props.onBackspacePress}
aria-label={_t("Backspace")}
/>
</div>
);
}
}

View file

@ -18,7 +18,7 @@ limitations under the License.
import React from "react";
import { _t } from '../../../languageHandler';
import { _t } from "../../../languageHandler";
interface IProps {
// The primary button which is styled differently and has default focus.
@ -32,7 +32,7 @@ interface IProps {
// onClick handler for the primary button. Note that the returned promise, if
// returning a promise, is not used.
onPrimaryButtonClick?: (ev: React.MouseEvent) => (void | Promise<void>);
onPrimaryButtonClick?: (ev: React.MouseEvent) => void | Promise<void>;
// should there be a cancel button? default: true
hasCancel?: boolean;
@ -79,38 +79,41 @@ export default class DialogButtons extends React.Component<IProps> {
let cancelButton;
if (this.props.cancelButton || this.props.hasCancel) {
cancelButton = <button
// important: the default type is 'submit' and this button comes before the
// primary in the DOM so will get form submissions unless we make it not a submit.
data-testid="dialog-cancel-button"
type="button"
onClick={this.onCancelClick}
className={this.props.cancelButtonClass}
disabled={this.props.disabled}
>
{ this.props.cancelButton || _t("Cancel") }
</button>;
cancelButton = (
<button
// important: the default type is 'submit' and this button comes before the
// primary in the DOM so will get form submissions unless we make it not a submit.
data-testid="dialog-cancel-button"
type="button"
onClick={this.onCancelClick}
className={this.props.cancelButtonClass}
disabled={this.props.disabled}
>
{this.props.cancelButton || _t("Cancel")}
</button>
);
}
let additive = null;
if (this.props.additive) {
additive = <div className="mx_Dialog_buttons_additive">{ this.props.additive }</div>;
additive = <div className="mx_Dialog_buttons_additive">{this.props.additive}</div>;
}
return (
<div className="mx_Dialog_buttons">
{ additive }
{additive}
<span className="mx_Dialog_buttons_row">
{ cancelButton }
{ this.props.children }
<button type={this.props.primaryIsSubmit ? 'submit' : 'button'}
{cancelButton}
{this.props.children}
<button
type={this.props.primaryIsSubmit ? "submit" : "button"}
data-testid="dialog-primary-button"
className={primaryButtonClassName}
onClick={this.props.onPrimaryButtonClick}
autoFocus={this.props.focus}
disabled={this.props.disabled || this.props.primaryDisabled}
>
{ this.props.primaryButton }
{this.props.primaryButton}
</button>
</span>
</div>

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
interface IProps {
className: string;

View file

@ -15,11 +15,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ChangeEvent, createRef, CSSProperties, ReactElement, ReactNode, Ref } from 'react';
import classnames from 'classnames';
import React, { ChangeEvent, createRef, CSSProperties, ReactElement, ReactNode, Ref } from "react";
import classnames from "classnames";
import AccessibleButton, { ButtonEvent } from './AccessibleButton';
import { _t } from '../../../languageHandler';
import AccessibleButton, { ButtonEvent } from "./AccessibleButton";
import { _t } from "../../../languageHandler";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { objectHasDiff } from "../../../utils/objects";
@ -55,17 +55,19 @@ class MenuOption extends React.Component<IMenuOptionProps> {
mx_Dropdown_option_highlight: this.props.highlighted,
});
return <div
id={this.props.id}
className={optClasses}
onClick={this.onClick}
onMouseEnter={this.onMouseEnter}
role="option"
aria-selected={this.props.highlighted}
ref={this.props.inputRef}
>
{ this.props.children }
</div>;
return (
<div
id={this.props.id}
className={optClasses}
onClick={this.onClick}
onMouseEnter={this.onMouseEnter}
role="option"
aria-selected={this.props.highlighted}
ref={this.props.inputRef}
>
{this.props.children}
</div>
);
}
}
@ -127,14 +129,14 @@ export default class Dropdown extends React.Component<DropdownProps, IState> {
expanded: false,
// The key of the highlighted option
// (the option that would become selected if you pressed enter)
highlightedOption: firstChild ? firstChild.key as string : null,
highlightedOption: firstChild ? (firstChild.key as string) : null,
// the current search query
searchQuery: '',
searchQuery: "",
};
// 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);
}
public componentDidUpdate(prevProps: Readonly<DropdownProps>) {
@ -148,7 +150,7 @@ export default class Dropdown extends React.Component<DropdownProps, IState> {
}
componentWillUnmount() {
document.removeEventListener('click', this.onDocumentClick, false);
document.removeEventListener("click", this.onDocumentClick, false);
}
private reindexChildren(children: ReactElement[]): void {
@ -220,7 +222,7 @@ export default class Dropdown extends React.Component<DropdownProps, IState> {
switch (action) {
case KeyBindingAction.Enter:
this.props.onOptionChange(this.state.highlightedOption);
// fallthrough
// fallthrough
case KeyBindingAction.Escape:
this.close();
break;
@ -263,10 +265,10 @@ export default class Dropdown extends React.Component<DropdownProps, IState> {
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;
};
@ -311,14 +313,16 @@ export default class Dropdown extends React.Component<DropdownProps, IState> {
onClick={this.onMenuOptionClick}
inputRef={highlighted ? this.scrollIntoView : undefined}
>
{ child }
{child}
</MenuOption>
);
});
if (options.length === 0) {
return [<div key="0" className="mx_Dropdown_option" role="option" aria-selected={false}>
{ _t("No results") }
</div>];
return [
<div key="0" className="mx_Dropdown_option" role="option" aria-selected={false}>
{_t("No results")}
</div>,
];
}
return options;
}
@ -353,18 +357,20 @@ export default class Dropdown extends React.Component<DropdownProps, IState> {
}
menu = (
<div className="mx_Dropdown_menu" style={menuStyle} role="listbox" id={`${this.props.id}_listbox`}>
{ this.getMenuOptions() }
{this.getMenuOptions()}
</div>
);
}
if (!currentValue) {
const selectedChild = this.props.getShortOption ?
this.props.getShortOption(this.props.value) :
this.childrenByKey[this.props.value];
currentValue = <div className="mx_Dropdown_option" id={`${this.props.id}_value`}>
{ selectedChild || this.props.placeholder }
</div>;
const selectedChild = this.props.getShortOption
? this.props.getShortOption(this.props.value)
: this.childrenByKey[this.props.value];
currentValue = (
<div className="mx_Dropdown_option" id={`${this.props.id}_value`}>
{selectedChild || this.props.placeholder}
</div>
);
}
const dropdownClasses = {
@ -377,23 +383,25 @@ export default class Dropdown extends React.Component<DropdownProps, IState> {
// 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}>
<AccessibleButton
className="mx_Dropdown_input mx_no_textinput"
onClick={this.onAccessibleButtonClick}
aria-haspopup="listbox"
aria-expanded={this.state.expanded}
disabled={this.props.disabled}
inputRef={this.buttonRef}
aria-label={this.props.label}
aria-describedby={`${this.props.id}_value`}
aria-owns={`${this.props.id}_input`}
onKeyDown={this.onKeyDown}
>
{ currentValue }
<span className="mx_Dropdown_arrow" />
{ menu }
</AccessibleButton>
</div>;
return (
<div className={classnames(dropdownClasses)} ref={this.collectRoot}>
<AccessibleButton
className="mx_Dropdown_input mx_no_textinput"
onClick={this.onAccessibleButtonClick}
aria-haspopup="listbox"
aria-expanded={this.state.expanded}
disabled={this.props.disabled}
inputRef={this.buttonRef}
aria-label={this.props.label}
aria-describedby={`${this.props.id}_value`}
aria-owns={`${this.props.id}_input`}
onKeyDown={this.onKeyDown}
>
{currentValue}
<span className="mx_Dropdown_arrow" />
{menu}
</AccessibleButton>
</div>
);
}
}

View file

@ -16,7 +16,7 @@ limitations under the License.
import React from "react";
import { _t } from '../../../languageHandler';
import { _t } from "../../../languageHandler";
import Field from "./Field";
import AccessibleButton from "./AccessibleButton";
@ -61,22 +61,20 @@ export class EditableItem extends React.Component<IItemProps, IItemState> {
if (this.state.verifyRemove) {
return (
<div className="mx_EditableItem">
<span className="mx_EditableItem_promptText">
{ _t("Are you sure?") }
</span>
<span className="mx_EditableItem_promptText">{_t("Are you sure?")}</span>
<AccessibleButton
onClick={this.onActuallyRemove}
kind="primary_sm"
className="mx_EditableItem_confirmBtn"
>
{ _t("Yes") }
{_t("Yes")}
</AccessibleButton>
<AccessibleButton
onClick={this.onDontRemove}
kind="danger_sm"
className="mx_EditableItem_confirmBtn"
>
{ _t("No") }
{_t("No")}
</AccessibleButton>
</div>
);
@ -85,7 +83,7 @@ export class EditableItem extends React.Component<IItemProps, IItemState> {
return (
<div className="mx_EditableItem">
<div onClick={this.onRemove} className="mx_EditableItem_delete" title={_t("Remove")} role="button" />
<span className="mx_EditableItem_item">{ this.props.value }</span>
<span className="mx_EditableItem_item">{this.props.value}</span>
</div>
);
}
@ -144,7 +142,7 @@ export default class EditableItemList<P = {}> extends React.PureComponent<IProps
type="submit"
disabled={!this.props.newItem}
>
{ _t("Add") }
{_t("Add")}
</AccessibleButton>
</form>
);
@ -153,27 +151,20 @@ export default class EditableItemList<P = {}> extends React.PureComponent<IProps
render() {
const editableItems = this.props.items.map((item, index) => {
if (!this.props.canRemove) {
return <li key={item}>{ item }</li>;
return <li key={item}>{item}</li>;
}
return <EditableItem
key={item}
index={index}
value={item}
onRemove={this.onItemRemoved}
/>;
return <EditableItem key={item} index={index} value={item} onRemove={this.onItemRemoved} />;
});
const editableItemsSection = this.props.canRemove ? editableItems : <ul>{ editableItems }</ul>;
const editableItemsSection = this.props.canRemove ? editableItems : <ul>{editableItems}</ul>;
const label = this.props.items.length > 0 ? this.props.itemsLabel : this.props.noItemsLabel;
return (
<div className="mx_EditableItemList" id={this.props.id}>
<div className="mx_EditableItemList_label">
{ label }
</div>
{ editableItemsSection }
{ this.props.canEdit ? this.renderNewItemField() : <div /> }
<div className="mx_EditableItemList_label">{label}</div>
{editableItemsSection}
{this.props.canEdit ? this.renderNewItemField() : <div />}
</div>
);
}

View file

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { createRef } from 'react';
import React, { createRef } from "react";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
@ -47,15 +47,15 @@ interface IState {
export default class EditableText extends React.Component<IProps, IState> {
// we track value as an JS object field rather than in React state
// as React doesn't play nice with contentEditable.
public value = '';
public value = "";
private placeholder = false;
private editableDiv = createRef<HTMLDivElement>();
public static defaultProps: Partial<IProps> = {
onValueChanged() {},
initialValue: '',
label: '',
placeholder: '',
initialValue: "",
label: "",
placeholder: "",
editable: true,
className: "mx_EditableText",
placeholderClassName: "mx_EditableText_placeholder",
@ -89,10 +89,12 @@ export default class EditableText extends React.Component<IProps, IState> {
private showPlaceholder = (show: boolean): void => {
if (show) {
this.editableDiv.current.textContent = this.props.placeholder;
this.editableDiv.current.setAttribute("class", this.props.className
+ " " + this.props.placeholderClassName);
this.editableDiv.current.setAttribute(
"class",
this.props.className + " " + this.props.placeholderClassName,
);
this.placeholder = true;
this.value = '';
this.value = "";
} else {
this.editableDiv.current.textContent = this.value;
this.editableDiv.current.setAttribute("class", this.props.className);
@ -175,13 +177,16 @@ export default class EditableText extends React.Component<IProps, IState> {
const self = this;
const action = getKeyBindingsManager().getAccessibilityAction(ev as React.KeyboardEvent);
const submit = action === KeyBindingAction.Enter || shouldSubmit;
this.setState({
phase: Phases.Display,
}, () => {
if (this.value !== this.props.initialValue) {
self.onValueChanged(submit);
}
});
this.setState(
{
phase: Phases.Display,
},
() => {
if (this.value !== this.props.initialValue) {
self.onValueChanged(submit);
}
},
);
};
private onBlur = (ev: React.FocusEvent<HTMLDivElement>): void => {
@ -201,24 +206,26 @@ export default class EditableText extends React.Component<IProps, IState> {
const { className, editable, initialValue, label, labelClassName } = this.props;
let editableEl;
if (!editable || (this.state.phase === Phases.Display &&
(label || labelClassName) && !this.value)
) {
if (!editable || (this.state.phase === Phases.Display && (label || labelClassName) && !this.value)) {
// show the label
editableEl = <div className={className + " " + labelClassName} onClick={this.onClickDiv}>
{ label || initialValue }
</div>;
editableEl = (
<div className={className + " " + labelClassName} onClick={this.onClickDiv}>
{label || initialValue}
</div>
);
} else {
// show the content editable div, but manually manage its contents as react and contentEditable don't play nice together
editableEl = <div
ref={this.editableDiv}
contentEditable={true}
className={className}
onKeyDown={this.onKeyDown}
onKeyUp={this.onKeyUp}
onFocus={this.onFocus}
onBlur={this.onBlur}
/>;
editableEl = (
<div
ref={this.editableDiv}
contentEditable={true}
className={className}
onKeyDown={this.onKeyDown}
onKeyUp={this.onKeyUp}
onFocus={this.onFocus}
onBlur={this.onBlur}
/>
);
}
return editableEl;

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import Spinner from "./Spinner";
import EditableText from "./EditableText";
@ -61,7 +61,9 @@ export default class EditableTextContainer extends React.Component<IProps, IStat
initialValue: "",
placeholder: "",
blurToSubmit: false,
onSubmit: () => { return Promise.resolve(); },
onSubmit: () => {
return Promise.resolve();
},
};
constructor(props: IProps) {
@ -111,14 +113,18 @@ export default class EditableTextContainer extends React.Component<IProps, IStat
this.props.onSubmit(value).then(
() => {
if (this.unmounted) { return; }
if (this.unmounted) {
return;
}
this.setState({
busy: false,
value: value,
});
},
(error) => {
if (this.unmounted) { return; }
if (this.unmounted) {
return;
}
this.setState({
errorString: error.toString(),
busy: false,
@ -129,16 +135,13 @@ export default class EditableTextContainer extends React.Component<IProps, IStat
public render(): JSX.Element {
if (this.state.busy) {
return (
<Spinner />
);
return <Spinner />;
} else if (this.state.errorString) {
return (
<div className="error">{ this.state.errorString }</div>
);
return <div className="error">{this.state.errorString}</div>;
} else {
return (
<EditableText initialValue={this.state.value}
<EditableText
initialValue={this.state.value}
placeholder={this.props.placeholder}
onValueChanged={this.onValueChanged}
blurToSubmit={this.props.blurToSubmit}
@ -147,4 +150,3 @@ export default class EditableTextContainer extends React.Component<IProps, IStat
}
}
}

View file

@ -14,12 +14,12 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { FunctionComponent, useEffect, useRef } from 'react';
import React, { FunctionComponent, useEffect, useRef } from "react";
import { logger } from "matrix-js-sdk/src/logger";
import dis from '../../../dispatcher/dispatcher';
import ICanvasEffect from '../../../effects/ICanvasEffect';
import { CHAT_EFFECTS } from '../../../effects';
import dis from "../../../dispatcher/dispatcher";
import ICanvasEffect from "../../../effects/ICanvasEffect";
import { CHAT_EFFECTS } from "../../../effects";
import UIStore, { UI_EVENTS } from "../../../stores/UIStore";
interface IProps {
@ -53,7 +53,7 @@ const EffectsOverlay: FunctionComponent<IProps> = ({ roomWidth }) => {
}
};
const onAction = (payload: { action: string }) => {
const actionPrefix = 'effects.';
const actionPrefix = "effects.";
if (payload.action.indexOf(actionPrefix) === 0) {
const effect = payload.action.slice(actionPrefix.length);
lazyLoadEffectModule(effect).then((module) => module?.start(canvasRef.current));
@ -83,10 +83,10 @@ const EffectsOverlay: FunctionComponent<IProps> = ({ roomWidth }) => {
ref={canvasRef}
width={roomWidth}
style={{
display: 'block',
display: "block",
zIndex: 999999,
pointerEvents: 'none',
position: 'fixed',
pointerEvents: "none",
position: "fixed",
top: 0,
right: 0,
}}

View file

@ -14,16 +14,16 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ErrorInfo } from 'react';
import React, { ErrorInfo } from "react";
import { logger } from "matrix-js-sdk/src/logger";
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import PlatformPeg from '../../../PlatformPeg';
import Modal from '../../../Modal';
import { _t } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import PlatformPeg from "../../../PlatformPeg";
import Modal from "../../../Modal";
import SdkConfig from "../../../SdkConfig";
import BugReportDialog from '../dialogs/BugReportDialog';
import AccessibleButton from './AccessibleButton';
import BugReportDialog from "../dialogs/BugReportDialog";
import AccessibleButton from "./AccessibleButton";
interface IState {
error: Error;
@ -52,24 +52,23 @@ export default class ErrorBoundary extends React.PureComponent<{}, IState> {
// Browser consoles are better at formatting output when native errors are passed
// in their own `console.error` invocation.
logger.error(error);
logger.error(
"The above error occurred while React was rendering the following components:",
componentStack,
);
logger.error("The above error occurred while React was rendering the following components:", componentStack);
}
private onClearCacheAndReload = (): void => {
if (!PlatformPeg.get()) return;
MatrixClientPeg.get().stopClient();
MatrixClientPeg.get().store.deleteAllData().then(() => {
PlatformPeg.get().reload();
});
MatrixClientPeg.get()
.store.deleteAllData()
.then(() => {
PlatformPeg.get().reload();
});
};
private onBugReport = (): void => {
Modal.createDialog(BugReportDialog, {
label: 'react-soft-crash',
label: "react-soft-crash",
error: this.state.error,
});
};
@ -80,47 +79,63 @@ export default class ErrorBoundary extends React.PureComponent<{}, IState> {
let bugReportSection;
if (SdkConfig.get().bug_report_endpoint_url) {
bugReportSection = <React.Fragment>
<p>{ _t(
"Please <newIssueLink>create a new issue</newIssueLink> " +
"on GitHub so that we can investigate this bug.", {}, {
newIssueLink: (sub) => {
return <a target="_blank" rel="noreferrer noopener" href={newIssueUrl}>{ sub }</a>;
},
},
) }</p>
<p>{ _t(
"If you've submitted a bug via GitHub, debug logs can help " +
"us track down the problem. ")
}
{ _t(
"Debug logs contain application " +
"usage data including your username, the IDs or aliases of " +
"the rooms 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") }
</AccessibleButton>
</React.Fragment>;
bugReportSection = (
<React.Fragment>
<p>
{_t(
"Please <newIssueLink>create a new issue</newIssueLink> " +
"on GitHub so that we can investigate this bug.",
{},
{
newIssueLink: (sub) => {
return (
<a target="_blank" rel="noreferrer noopener" href={newIssueUrl}>
{sub}
</a>
);
},
},
)}
</p>
<p>
{_t(
"If you've submitted a bug via GitHub, debug logs can help " +
"us track down the problem. ",
)}
{_t(
"Debug logs contain application " +
"usage data including your username, the IDs or aliases of " +
"the rooms 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")}
</AccessibleButton>
</React.Fragment>
);
}
let clearCacheButton: JSX.Element;
// we only show this button if there is an initialised MatrixClient otherwise we can't clear the cache
if (MatrixClientPeg.get()) {
clearCacheButton = <AccessibleButton onClick={this.onClearCacheAndReload} kind='danger'>
{ _t("Clear cache and reload") }
</AccessibleButton>;
clearCacheButton = (
<AccessibleButton onClick={this.onClearCacheAndReload} kind="danger">
{_t("Clear cache and reload")}
</AccessibleButton>
);
}
return <div className="mx_ErrorBoundary">
<div className="mx_ErrorBoundary_body">
<h1>{ _t("Something went wrong!") }</h1>
{ bugReportSection }
{ clearCacheButton }
return (
<div className="mx_ErrorBoundary">
<div className="mx_ErrorBoundary_body">
<h1>{_t("Something went wrong!")}</h1>
{bugReportSection}
{clearCacheButton}
</div>
</div>
</div>;
);
}
return this.props.children;

View file

@ -16,20 +16,20 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ComponentProps } from 'react';
import React, { ComponentProps } from "react";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { EventType } from 'matrix-js-sdk/src/@types/event';
import { EventType } from "matrix-js-sdk/src/@types/event";
import { _t } from '../../../languageHandler';
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
import { _t } from "../../../languageHandler";
import { formatCommaSeparatedList } from "../../../utils/FormattingUtils";
import { isValid3pidInvite } from "../../../RoomInvite";
import GenericEventListSummary from "./GenericEventListSummary";
import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases';
import { jsxJoin } from '../../../utils/ReactUtils';
import { Layout } from '../../../settings/enums/Layout';
import RightPanelStore from '../../../stores/right-panel/RightPanelStore';
import AccessibleButton from './AccessibleButton';
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
import { jsxJoin } from "../../../utils/ReactUtils";
import { Layout } from "../../../settings/enums/Layout";
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
import AccessibleButton from "./AccessibleButton";
import RoomContext from "../../../contexts/RoomContext";
const onPinnedMessagesClick = (): void => {
@ -128,14 +128,12 @@ export default class EventListSummary extends React.Component<IProps> {
const coalescedTransitions = EventListSummary.coalesceRepeatedTransitions(canonicalTransitions);
const descs = coalescedTransitions.map((t) => {
return EventListSummary.getDescriptionForTransition(
t.transitionType, userNames.length, t.repeats,
);
return EventListSummary.getDescriptionForTransition(t.transitionType, userNames.length, t.repeats);
});
const desc = formatCommaSeparatedList(descs);
return _t('%(nameList)s %(transitionList)s', { nameList, transitionList: desc });
return _t("%(nameList)s %(transitionList)s", { nameList, transitionList: desc });
});
if (!summaries) {
@ -247,110 +245,132 @@ export default class EventListSummary extends React.Component<IProps> {
let res = null;
switch (t) {
case TransitionType.Joined:
res = (userCount > 1)
? _t("%(severalUsers)sjoined %(count)s times", { severalUsers: "", count })
: _t("%(oneUser)sjoined %(count)s times", { oneUser: "", count });
res =
userCount > 1
? _t("%(severalUsers)sjoined %(count)s times", { severalUsers: "", count })
: _t("%(oneUser)sjoined %(count)s times", { oneUser: "", count });
break;
case TransitionType.Left:
res = (userCount > 1)
? _t("%(severalUsers)sleft %(count)s times", { severalUsers: "", count })
: _t("%(oneUser)sleft %(count)s times", { oneUser: "", count });
res =
userCount > 1
? _t("%(severalUsers)sleft %(count)s times", { severalUsers: "", count })
: _t("%(oneUser)sleft %(count)s times", { oneUser: "", count });
break;
case TransitionType.JoinedAndLeft:
res = (userCount > 1)
? _t("%(severalUsers)sjoined and left %(count)s times", { severalUsers: "", count })
: _t("%(oneUser)sjoined and left %(count)s times", { oneUser: "", count });
res =
userCount > 1
? _t("%(severalUsers)sjoined and left %(count)s times", { severalUsers: "", count })
: _t("%(oneUser)sjoined and left %(count)s times", { oneUser: "", count });
break;
case TransitionType.LeftAndJoined:
res = (userCount > 1)
? _t("%(severalUsers)sleft and rejoined %(count)s times", { severalUsers: "", count })
: _t("%(oneUser)sleft and rejoined %(count)s times", { oneUser: "", count });
res =
userCount > 1
? _t("%(severalUsers)sleft and rejoined %(count)s times", { severalUsers: "", count })
: _t("%(oneUser)sleft and rejoined %(count)s times", { oneUser: "", count });
break;
case TransitionType.InviteReject:
res = (userCount > 1)
? _t("%(severalUsers)srejected their invitations %(count)s times", {
severalUsers: "",
count,
})
: _t("%(oneUser)srejected their invitation %(count)s times", { oneUser: "", count });
res =
userCount > 1
? _t("%(severalUsers)srejected their invitations %(count)s times", {
severalUsers: "",
count,
})
: _t("%(oneUser)srejected their invitation %(count)s times", { oneUser: "", count });
break;
case TransitionType.InviteWithdrawal:
res = (userCount > 1)
? _t("%(severalUsers)shad their invitations withdrawn %(count)s times", {
severalUsers: "",
count,
})
: _t("%(oneUser)shad their invitation withdrawn %(count)s times", { oneUser: "", count });
res =
userCount > 1
? _t("%(severalUsers)shad their invitations withdrawn %(count)s times", {
severalUsers: "",
count,
})
: _t("%(oneUser)shad their invitation withdrawn %(count)s times", { oneUser: "", count });
break;
case TransitionType.Invited:
res = (userCount > 1)
? _t("were invited %(count)s times", { count })
: _t("was invited %(count)s times", { count });
res =
userCount > 1
? _t("were invited %(count)s times", { count })
: _t("was invited %(count)s times", { count });
break;
case TransitionType.Banned:
res = (userCount > 1)
? _t("were banned %(count)s times", { count })
: _t("was banned %(count)s times", { count });
res =
userCount > 1
? _t("were banned %(count)s times", { count })
: _t("was banned %(count)s times", { count });
break;
case TransitionType.Unbanned:
res = (userCount > 1)
? _t("were unbanned %(count)s times", { count })
: _t("was unbanned %(count)s times", { count });
res =
userCount > 1
? _t("were unbanned %(count)s times", { count })
: _t("was unbanned %(count)s times", { count });
break;
case TransitionType.Kicked:
res = (userCount > 1)
? _t("were removed %(count)s times", { count })
: _t("was removed %(count)s times", { count });
res =
userCount > 1
? _t("were removed %(count)s times", { count })
: _t("was removed %(count)s times", { count });
break;
case TransitionType.ChangedName:
res = (userCount > 1)
? _t("%(severalUsers)schanged their name %(count)s times", { severalUsers: "", count })
: _t("%(oneUser)schanged their name %(count)s times", { oneUser: "", count });
res =
userCount > 1
? _t("%(severalUsers)schanged their name %(count)s times", { severalUsers: "", count })
: _t("%(oneUser)schanged their name %(count)s times", { oneUser: "", count });
break;
case TransitionType.ChangedAvatar:
res = (userCount > 1)
? _t("%(severalUsers)schanged their avatar %(count)s times", { severalUsers: "", count })
: _t("%(oneUser)schanged their avatar %(count)s times", { oneUser: "", count });
res =
userCount > 1
? _t("%(severalUsers)schanged their avatar %(count)s times", { severalUsers: "", count })
: _t("%(oneUser)schanged their avatar %(count)s times", { oneUser: "", count });
break;
case TransitionType.NoChange:
res = (userCount > 1)
? _t("%(severalUsers)smade no changes %(count)s times", { severalUsers: "", count })
: _t("%(oneUser)smade no changes %(count)s times", { oneUser: "", count });
res =
userCount > 1
? _t("%(severalUsers)smade no changes %(count)s times", { severalUsers: "", count })
: _t("%(oneUser)smade no changes %(count)s times", { oneUser: "", count });
break;
case TransitionType.ServerAcl:
res = (userCount > 1)
? _t("%(severalUsers)schanged the server ACLs %(count)s times",
{ severalUsers: "", count })
: _t("%(oneUser)schanged the server ACLs %(count)s times", { oneUser: "", count });
res =
userCount > 1
? _t("%(severalUsers)schanged the server ACLs %(count)s times", { severalUsers: "", count })
: _t("%(oneUser)schanged the server ACLs %(count)s times", { oneUser: "", count });
break;
case TransitionType.ChangedPins:
res = (userCount > 1)
? _t("%(severalUsers)schanged the <a>pinned messages</a> for the room %(count)s times",
{ severalUsers: "", count },
{
"a": (sub) => <AccessibleButton kind='link_inline' onClick={onPinnedMessagesClick}>
{ sub }
</AccessibleButton>,
})
: _t("%(oneUser)schanged the <a>pinned messages</a> for the room %(count)s times",
{ oneUser: "", count },
{
"a": (sub) => <AccessibleButton kind='link_inline' onClick={onPinnedMessagesClick}>
{ sub }
</AccessibleButton>,
});
res =
userCount > 1
? _t(
"%(severalUsers)schanged the <a>pinned messages</a> for the room %(count)s times",
{ severalUsers: "", count },
{
a: (sub) => (
<AccessibleButton kind="link_inline" onClick={onPinnedMessagesClick}>
{sub}
</AccessibleButton>
),
},
)
: _t(
"%(oneUser)schanged the <a>pinned messages</a> for the room %(count)s times",
{ oneUser: "", count },
{
a: (sub) => (
<AccessibleButton kind="link_inline" onClick={onPinnedMessagesClick}>
{sub}
</AccessibleButton>
),
},
);
break;
case TransitionType.MessageRemoved:
res = (userCount > 1)
? _t("%(severalUsers)sremoved a message %(count)s times",
{ severalUsers: "", count })
: _t("%(oneUser)sremoved a message %(count)s times", { oneUser: "", count });
res =
userCount > 1
? _t("%(severalUsers)sremoved a message %(count)s times", { severalUsers: "", count })
: _t("%(oneUser)sremoved a message %(count)s times", { oneUser: "", count });
break;
case TransitionType.HiddenEvent:
res = (userCount > 1)
? _t("%(severalUsers)ssent %(count)s hidden messages",
{ severalUsers: "", count })
: _t("%(oneUser)ssent %(count)s hidden messages", { oneUser: "", count });
res =
userCount > 1
? _t("%(severalUsers)ssent %(count)s hidden messages", { severalUsers: "", count })
: _t("%(oneUser)ssent %(count)s hidden messages", { oneUser: "", count });
break;
}
@ -390,10 +410,12 @@ export default class EventListSummary extends React.Component<IProps> {
case EventType.RoomMember:
switch (e.mxEvent.getContent().membership) {
case 'invite': return TransitionType.Invited;
case 'ban': return TransitionType.Banned;
case 'join':
if (e.mxEvent.getPrevContent().membership === 'join') {
case "invite":
return TransitionType.Invited;
case "ban":
return TransitionType.Banned;
case "join":
if (e.mxEvent.getPrevContent().membership === "join") {
if (e.mxEvent.getContent().displayname !== e.mxEvent.getPrevContent().displayname) {
return TransitionType.ChangedName;
} else if (e.mxEvent.getContent().avatar_url !== e.mxEvent.getPrevContent().avatar_url) {
@ -403,7 +425,7 @@ export default class EventListSummary extends React.Component<IProps> {
} else {
return TransitionType.Joined;
}
case 'leave':
case "leave":
if (e.mxEvent.getSender() === e.mxEvent.getStateKey()) {
if (e.mxEvent.getPrevContent().membership === "invite") {
return TransitionType.InviteReject;
@ -411,12 +433,16 @@ export default class EventListSummary extends React.Component<IProps> {
return TransitionType.Left;
}
switch (e.mxEvent.getPrevContent().membership) {
case 'invite': return TransitionType.InviteWithdrawal;
case 'ban': return TransitionType.Unbanned;
case "invite":
return TransitionType.InviteWithdrawal;
case "ban":
return TransitionType.Unbanned;
// sender is not target and made the target leave, if not from invite/ban then this is a kick
default: return TransitionType.Kicked;
default:
return TransitionType.Kicked;
}
default: return null;
default:
return null;
}
default:
@ -440,26 +466,22 @@ export default class EventListSummary extends React.Component<IProps> {
};
const users = Object.keys(userEvents);
users.forEach(
(userId) => {
const firstEvent = userEvents[userId][0];
const displayName = firstEvent.displayName;
users.forEach((userId) => {
const firstEvent = userEvents[userId][0];
const displayName = firstEvent.displayName;
const seq = EventListSummary.getTransitionSequence(userEvents[userId]).join(SEP);
if (!aggregate[seq]) {
aggregate[seq] = [];
aggregateIndices[seq] = -1;
}
const seq = EventListSummary.getTransitionSequence(userEvents[userId]).join(SEP);
if (!aggregate[seq]) {
aggregate[seq] = [];
aggregateIndices[seq] = -1;
}
aggregate[seq].push(displayName);
aggregate[seq].push(displayName);
if (aggregateIndices[seq] === -1 ||
firstEvent.index < aggregateIndices[seq]
) {
aggregateIndices[seq] = firstEvent.index;
}
},
);
if (aggregateIndices[seq] === -1 || firstEvent.index < aggregateIndices[seq]) {
aggregateIndices[seq] = firstEvent.index;
}
});
return {
names: aggregate,
@ -525,15 +547,18 @@ export default class EventListSummary extends React.Component<IProps> {
(seq1, seq2) => aggregate.indices[seq1] - aggregate.indices[seq2],
);
return <GenericEventListSummary
data-testid={this.props['data-testid']}
events={this.props.events}
threshold={this.props.threshold}
onToggle={this.props.onToggle}
startExpanded={this.props.startExpanded}
children={this.props.children}
summaryMembers={[...latestUserAvatarMember.values()]}
layout={this.props.layout}
summaryText={this.generateSummary(aggregate.names, orderedTransitionSequences)} />;
return (
<GenericEventListSummary
data-testid={this.props["data-testid"]}
events={this.props.events}
threshold={this.props.threshold}
onToggle={this.props.onToggle}
startExpanded={this.props.startExpanded}
children={this.props.children}
summaryMembers={[...latestUserAvatarMember.values()]}
layout={this.props.layout}
summaryText={this.generateSummary(aggregate.names, orderedTransitionSequences)}
/>
);
}
}

View file

@ -14,15 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import classnames from 'classnames';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
import React from "react";
import classnames from "classnames";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import * as Avatar from '../../../Avatar';
import EventTile from '../rooms/EventTile';
import * as Avatar from "../../../Avatar";
import EventTile from "../rooms/EventTile";
import { Layout } from "../../../settings/enums/Layout";
import Spinner from './Spinner';
import Spinner from "./Spinner";
interface IProps {
/**
@ -83,10 +83,10 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
displayname: this.props.displayName,
avatar_url: this.props.avatarUrl,
},
msgtype: "m.text",
body: message,
displayname: this.props.displayName,
avatar_url: this.props.avatarUrl,
"msgtype": "m.text",
"body": message,
"displayname": this.props.displayName,
"avatar_url": this.props.avatarUrl,
},
unsigned: {
age: 97,
@ -103,10 +103,7 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
rawDisplayName: this.props.displayName,
userId: this.props.userId,
getAvatarUrl: (..._) => {
return Avatar.avatarUrlForUser(
{ avatarUrl: this.props.avatarUrl },
AVATAR_SIZE, AVATAR_SIZE, "crop",
);
return Avatar.avatarUrlForUser({ avatarUrl: this.props.avatarUrl }, AVATAR_SIZE, AVATAR_SIZE, "crop");
},
getMxcAvatarUrl: () => this.props.avatarUrl,
} as RoomMember;
@ -116,20 +113,23 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
public render() {
const className = classnames(this.props.className, {
"mx_IRCLayout": this.props.layout == Layout.IRC,
"mx_EventTilePreview_loader": !this.props.userId,
mx_IRCLayout: this.props.layout == Layout.IRC,
mx_EventTilePreview_loader: !this.props.userId,
});
if (!this.props.userId) return <div className={className}><Spinner /></div>;
if (!this.props.userId)
return (
<div className={className}>
<Spinner />
</div>
);
const event = this.fakeEvent(this.state);
return <div className={className}>
<EventTile
mxEvent={event}
layout={this.props.layout}
as="div"
/>
</div>;
return (
<div className={className}>
<EventTile mxEvent={event} layout={this.props.layout} as="div" />
</div>
);
}
}

View file

@ -14,23 +14,19 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { DetailedHTMLProps, AnchorHTMLAttributes } from 'react';
import classNames from 'classnames';
import React, { DetailedHTMLProps, AnchorHTMLAttributes } from "react";
import classNames from "classnames";
interface Props extends DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement> {}
/**
* Simple link component that adds external link icon after link children
*/
const ExternalLink: React.FC<Props> = ({ children, className, ...rest }) =>
<a
target="_blank"
rel="noreferrer noopener"
{...rest}
className={classNames('mx_ExternalLink', className)}
>
{ children }
<i className='mx_ExternalLink_icon' />
</a>;
const ExternalLink: React.FC<Props> = ({ children, className, ...rest }) => (
<a target="_blank" rel="noreferrer noopener" {...rest} className={classNames("mx_ExternalLink", className)}>
{children}
<i className="mx_ExternalLink_icon" />
</a>
);
export default ExternalLink;

View file

@ -32,35 +32,39 @@ interface IProps extends HTMLAttributes<HTMLSpanElement> {
const FacePile: FC<IProps> = ({ members, faceSize, overflow, tooltip, children, ...props }) => {
const faces = members.map(
tooltip
? m => <MemberAvatar key={m.userId} member={m} width={faceSize} height={faceSize} hideTitle />
: m => <TooltipTarget key={m.userId} label={m.name}>
<MemberAvatar
member={m}
width={faceSize}
height={faceSize}
viewUserOnClick={!props.onClick}
hideTitle
/>
</TooltipTarget>,
? (m) => <MemberAvatar key={m.userId} member={m} width={faceSize} height={faceSize} hideTitle />
: (m) => (
<TooltipTarget key={m.userId} label={m.name}>
<MemberAvatar
member={m}
width={faceSize}
height={faceSize}
viewUserOnClick={!props.onClick}
hideTitle
/>
</TooltipTarget>
),
);
const pileContents = <>
{ overflow ? <span className="mx_FacePile_more" /> : null }
{ faces }
</>;
const pileContents = (
<>
{overflow ? <span className="mx_FacePile_more" /> : null}
{faces}
</>
);
return <div {...props} className="mx_FacePile">
{ tooltip ? (
<TextWithTooltip class="mx_FacePile_faces" tooltip={tooltip}>
{ pileContents }
</TextWithTooltip>
) : (
<div className="mx_FacePile_faces">
{ pileContents }
</div>
) }
{ children }
</div>;
return (
<div {...props} className="mx_FacePile">
{tooltip ? (
<TextWithTooltip class="mx_FacePile_faces" tooltip={tooltip}>
{pileContents}
</TextWithTooltip>
) : (
<div className="mx_FacePile_faces">{pileContents}</div>
)}
{children}
</div>
);
};
export default FacePile;

View file

@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { InputHTMLAttributes, SelectHTMLAttributes, TextareaHTMLAttributes, RefObject } from 'react';
import classNames from 'classnames';
import React, { InputHTMLAttributes, SelectHTMLAttributes, TextareaHTMLAttributes, RefObject } from "react";
import classNames from "classnames";
import { debounce } from "lodash";
import { IFieldState, IValidationResult } from "./Validation";
@ -240,10 +240,25 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
public render() {
/* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */
const { element, inputRef, prefixComponent, postfixComponent, className, onValidate, children,
tooltipContent, forceValidity, tooltipClassName, list, validateOnBlur, validateOnChange, validateOnFocus,
usePlaceholderAsHint, forceTooltipVisible,
...inputProps } = this.props;
const {
element,
inputRef,
prefixComponent,
postfixComponent,
className,
onValidate,
children,
tooltipContent,
forceValidity,
tooltipClassName,
list,
validateOnBlur,
validateOnChange,
validateOnFocus,
usePlaceholderAsHint,
forceTooltipVisible,
...inputProps
} = this.props;
this.inputRef = inputRef || React.createRef();
@ -261,48 +276,45 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
let prefixContainer = null;
if (prefixComponent) {
prefixContainer = <span className="mx_Field_prefix">{ prefixComponent }</span>;
prefixContainer = <span className="mx_Field_prefix">{prefixComponent}</span>;
}
let postfixContainer = null;
if (postfixComponent) {
postfixContainer = <span className="mx_Field_postfix">{ postfixComponent }</span>;
postfixContainer = <span className="mx_Field_postfix">{postfixComponent}</span>;
}
const hasValidationFlag = forceValidity !== null && forceValidity !== undefined;
const fieldClasses = classNames(
"mx_Field",
`mx_Field_${this.props.element}`,
className,
{
// If we have a prefix element, leave the label always at the top left and
// don't animate it, as it looks a bit clunky and would add complexity to do
// properly.
mx_Field_labelAlwaysTopLeft: prefixComponent || usePlaceholderAsHint,
mx_Field_placeholderIsHint: usePlaceholderAsHint,
mx_Field_valid: hasValidationFlag ? forceValidity : onValidate && this.state.valid === true,
mx_Field_invalid: hasValidationFlag
? !forceValidity
: onValidate && this.state.valid === false,
},
);
const fieldClasses = classNames("mx_Field", `mx_Field_${this.props.element}`, className, {
// If we have a prefix element, leave the label always at the top left and
// don't animate it, as it looks a bit clunky and would add complexity to do
// properly.
mx_Field_labelAlwaysTopLeft: prefixComponent || usePlaceholderAsHint,
mx_Field_placeholderIsHint: usePlaceholderAsHint,
mx_Field_valid: hasValidationFlag ? forceValidity : onValidate && this.state.valid === true,
mx_Field_invalid: hasValidationFlag ? !forceValidity : onValidate && this.state.valid === false,
});
// Handle displaying feedback on validity
let fieldTooltip;
if (tooltipContent || this.state.feedback) {
fieldTooltip = <Tooltip
tooltipClassName={classNames("mx_Field_tooltip", "mx_Tooltip_noMargin", tooltipClassName)}
visible={(this.state.focused && forceTooltipVisible) || this.state.feedbackVisible}
label={tooltipContent || this.state.feedback}
alignment={Tooltip.Alignment.Right}
/>;
fieldTooltip = (
<Tooltip
tooltipClassName={classNames("mx_Field_tooltip", "mx_Tooltip_noMargin", tooltipClassName)}
visible={(this.state.focused && forceTooltipVisible) || this.state.feedbackVisible}
label={tooltipContent || this.state.feedback}
alignment={Tooltip.Alignment.Right}
/>
);
}
return <div className={fieldClasses}>
{ prefixContainer }
{ fieldInput }
<label htmlFor={this.id}>{ this.props.label }</label>
{ postfixContainer }
{ fieldTooltip }
</div>;
return (
<div className={fieldClasses}>
{prefixContainer}
{fieldInput}
<label htmlFor={this.id}>{this.props.label}</label>
{postfixContainer}
{fieldTooltip}
</div>
);
}
}

View file

@ -14,18 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import classNames from 'classnames';
import React from "react";
import classNames from "classnames";
import { Icon as CheckmarkIcon } from '../../../../res/img/element-icons/roomlist/checkmark.svg';
import Dropdown, { DropdownProps } from './Dropdown';
import { Icon as CheckmarkIcon } from "../../../../res/img/element-icons/roomlist/checkmark.svg";
import Dropdown, { DropdownProps } from "./Dropdown";
export type FilterDropdownOption<FilterKeysType extends string> = {
id: FilterKeysType;
label: string;
description?: string;
};
type FilterDropdownProps<FilterKeysType extends string> = Omit<DropdownProps, 'children'> & {
type FilterDropdownProps<FilterKeysType extends string> = Omit<DropdownProps, "children"> & {
value: FilterKeysType;
options: FilterDropdownOption<FilterKeysType>[];
// A label displayed before the selected value
@ -33,9 +33,9 @@ type FilterDropdownProps<FilterKeysType extends string> = Omit<DropdownProps, 'c
selectedLabel?: string;
};
const getSelectedFilterOptionComponent = <FilterKeysType extends string>(
options: FilterDropdownOption<FilterKeysType>[], selectedLabel?: string,
) => (filterKey: FilterKeysType) => {
const getSelectedFilterOptionComponent =
<FilterKeysType extends string>(options: FilterDropdownOption<FilterKeysType>[], selectedLabel?: string) =>
(filterKey: FilterKeysType) => {
const option = options.find(({ id }) => id === filterKey);
if (!option) {
return null;
@ -49,38 +49,27 @@ const getSelectedFilterOptionComponent = <FilterKeysType extends string>(
/**
* Dropdown styled for list filtering
*/
export const FilterDropdown = <FilterKeysType extends string = string>(
{
value,
options,
selectedLabel,
className,
...restProps
}: FilterDropdownProps<FilterKeysType>,
): React.ReactElement<FilterDropdownProps<FilterKeysType>> => {
return <Dropdown
{...restProps}
value={value}
className={classNames('mx_FilterDropdown', className)}
getShortOption={getSelectedFilterOptionComponent<FilterKeysType>(options, selectedLabel)}
>
{ options.map(({ id, label, description }) =>
<div
className='mx_FilterDropdown_option'
data-testid={`filter-option-${id}`}
key={id}
>
{ id === value && <CheckmarkIcon className='mx_FilterDropdown_optionSelectedIcon' /> }
<span className='mx_FilterDropdown_optionLabel'>
{ label }
</span>
{
!!description
&& <span
className='mx_FilterDropdown_optionDescription'
>{ description }</span>
}
</div>,
) }
</Dropdown>;
export const FilterDropdown = <FilterKeysType extends string = string>({
value,
options,
selectedLabel,
className,
...restProps
}: FilterDropdownProps<FilterKeysType>): React.ReactElement<FilterDropdownProps<FilterKeysType>> => {
return (
<Dropdown
{...restProps}
value={value}
className={classNames("mx_FilterDropdown", className)}
getShortOption={getSelectedFilterOptionComponent<FilterKeysType>(options, selectedLabel)}
>
{options.map(({ id, label, description }) => (
<div className="mx_FilterDropdown_option" data-testid={`filter-option-${id}`} key={id}>
{id === value && <CheckmarkIcon className="mx_FilterDropdown_optionSelectedIcon" />}
<span className="mx_FilterDropdown_optionLabel">{label}</span>
{!!description && <span className="mx_FilterDropdown_optionDescription">{description}</span>}
</div>
))}
</Dropdown>
);
};

View file

@ -20,30 +20,30 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { logger } from "matrix-js-sdk/src/logger";
import MemberAvatar from '../avatars/MemberAvatar';
import { _t } from '../../../languageHandler';
import MemberAvatar from "../avatars/MemberAvatar";
import { _t } from "../../../languageHandler";
import { useStateToggle } from "../../../hooks/useStateToggle";
import AccessibleButton from "./AccessibleButton";
import { Layout } from '../../../settings/enums/Layout';
import { Layout } from "../../../settings/enums/Layout";
interface IProps {
// An array of member events to summarise
events: MatrixEvent[];
"events": MatrixEvent[];
// The minimum number of events needed to trigger summarisation
threshold?: number;
"threshold"?: number;
// Whether or not to begin with state.expanded=true
startExpanded?: boolean;
"startExpanded"?: boolean;
// The list of room members for which to show avatars next to the summary
summaryMembers?: RoomMember[];
"summaryMembers"?: RoomMember[];
// The text to show as the summary of this event list
summaryText?: string | JSX.Element;
"summaryText"?: string | JSX.Element;
// An array of EventTiles to render when expanded
children: ReactNode[];
"children": ReactNode[];
// Called when the event list expansion is toggled
onToggle?(): void;
// The layout currently used
layout?: Layout;
'data-testid'?: string;
"layout"?: Layout;
"data-testid"?: string;
}
const GenericEventListSummary: React.FC<IProps> = ({
@ -55,7 +55,7 @@ const GenericEventListSummary: React.FC<IProps> = ({
summaryMembers = [],
summaryText,
layout = Layout.Group,
'data-testid': testId,
"data-testid": testId,
}) => {
const [expanded, toggleExpanded] = useStateToggle(startExpanded);
@ -66,46 +66,52 @@ const GenericEventListSummary: React.FC<IProps> = ({
}
}, [expanded]); // eslint-disable-line react-hooks/exhaustive-deps
const eventIds = events.map((e) => e.getId()).join(',');
const eventIds = events.map((e) => e.getId()).join(",");
// If we are only given few events then just pass them through
if (events.length < threshold) {
return (
<li className="mx_GenericEventListSummary" data-scroll-tokens={eventIds} data-expanded={true} data-layout={layout}>
<ol className="mx_GenericEventListSummary_unstyledList">
{ children }
</ol>
<li
className="mx_GenericEventListSummary"
data-scroll-tokens={eventIds}
data-expanded={true}
data-layout={layout}
>
<ol className="mx_GenericEventListSummary_unstyledList">{children}</ol>
</li>
);
}
let body;
if (expanded) {
body = <React.Fragment>
<div className="mx_GenericEventListSummary_line">&nbsp;</div>
<ol className="mx_GenericEventListSummary_unstyledList">
{ children }
</ol>
</React.Fragment>;
body = (
<React.Fragment>
<div className="mx_GenericEventListSummary_line">&nbsp;</div>
<ol className="mx_GenericEventListSummary_unstyledList">{children}</ol>
</React.Fragment>
);
} else {
const uniqueMembers = uniqBy(summaryMembers.filter(member => {
if (!member?.getMxcAvatarUrl) {
logger.error("EventListSummary given null summaryMember, termites may be afoot eating event senders",
summaryMembers);
return false;
}
return true;
}), member => member.getMxcAvatarUrl());
const uniqueMembers = uniqBy(
summaryMembers.filter((member) => {
if (!member?.getMxcAvatarUrl) {
logger.error(
"EventListSummary given null summaryMember, termites may be afoot eating event senders",
summaryMembers,
);
return false;
}
return true;
}),
(member) => member.getMxcAvatarUrl(),
);
const avatars = uniqueMembers.map((m) => <MemberAvatar key={m.userId} member={m} width={14} height={14} />);
body = (
<div className="mx_EventTile_line">
<div className="mx_EventTile_info">
<span className="mx_GenericEventListSummary_avatars" onClick={toggleExpanded}>
{ avatars }
</span>
<span className="mx_TextualEvent mx_GenericEventListSummary_summary">
{ summaryText }
{avatars}
</span>
<span className="mx_TextualEvent mx_GenericEventListSummary_summary">{summaryText}</span>
</div>
</div>
);
@ -125,9 +131,9 @@ const GenericEventListSummary: React.FC<IProps> = ({
onClick={toggleExpanded}
aria-expanded={expanded}
>
{ expanded ? _t('collapse') : _t('expand') }
{expanded ? _t("collapse") : _t("expand")}
</AccessibleButton>
{ body }
{body}
</li>
);
};

View file

@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import SettingsStore from "../../../settings/SettingsStore";
import Draggable, { ILocationState } from './Draggable';
import Draggable, { ILocationState } from "./Draggable";
import { SettingLevel } from "../../../settings/SettingLevel";
interface IProps {
@ -43,9 +43,12 @@ export default class IRCTimelineProfileResizer extends React.Component<IProps, I
}
componentDidMount() {
this.setState({
IRCLayoutRoot: document.querySelector(".mx_IRCLayout"),
}, () => this.updateCSSWidth(this.state.width));
this.setState(
{
IRCLayoutRoot: document.querySelector(".mx_IRCLayout"),
},
() => this.updateCSSWidth(this.state.width),
);
}
private dragFunc = (location: ILocationState, event: MouseEvent): ILocationState => {
@ -89,10 +92,6 @@ export default class IRCTimelineProfileResizer extends React.Component<IProps, I
};
render() {
return <Draggable
className="mx_ProfileResizer"
dragFunc={this.dragFunc}
onMouseUp={this.onMoueUp}
/>;
return <Draggable className="mx_ProfileResizer" dragFunc={this.dragFunc} onMouseUp={this.onMoueUp} />;
}
}

View file

@ -16,25 +16,25 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { createRef } from 'react';
import React, { createRef } from "react";
import FocusLock from "react-focus-lock";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { _t } from '../../../languageHandler';
import { _t } from "../../../languageHandler";
import AccessibleTooltipButton from "./AccessibleTooltipButton";
import MemberAvatar from "../avatars/MemberAvatar";
import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton";
import MessageContextMenu from "../context_menus/MessageContextMenu";
import { aboveLeftOf } from '../../structures/ContextMenu';
import { aboveLeftOf } from "../../structures/ContextMenu";
import MessageTimestamp from "../messages/MessageTimestamp";
import SettingsStore from "../../../settings/SettingsStore";
import { formatFullDate } from "../../../DateUtils";
import dis from '../../../dispatcher/dispatcher';
import { Action } from '../../../dispatcher/actions';
import dis from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import { normalizeWheelEvent } from "../../../utils/Mouse";
import { IDialogProps } from '../dialogs/IDialogProps';
import UIStore from '../../../stores/UIStore';
import { IDialogProps } from "../dialogs/IDialogProps";
import UIStore from "../../../stores/UIStore";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
@ -43,7 +43,7 @@ import { presentableTextForFile } from "../../../utils/FileUtils";
// Max scale to keep gaps around the image
const MAX_SCALE = 0.95;
// This is used for the buttons
const ZOOM_STEP = 0.10;
const ZOOM_STEP = 0.1;
// This is used for mouse wheel events
const ZOOM_COEFFICIENT = 0.0025;
// If we have moved only this much we can zoom
@ -101,17 +101,12 @@ export default class ImageView extends React.Component<IProps, IState> {
minZoom: MAX_SCALE,
maxZoom: MAX_SCALE,
rotation: 0,
translationX: (
thumbnailInfo?.positionX +
(thumbnailInfo?.width / 2) -
(UIStore.instance.windowWidth / 2)
) ?? 0,
translationY: (
translationX: thumbnailInfo?.positionX + thumbnailInfo?.width / 2 - UIStore.instance.windowWidth / 2 ?? 0,
translationY:
thumbnailInfo?.positionY +
(thumbnailInfo?.height / 2) -
(UIStore.instance.windowHeight / 2) -
(getPanelHeight() / 2)
) ?? 0,
thumbnailInfo?.height / 2 -
UIStore.instance.windowHeight / 2 -
getPanelHeight() / 2 ?? 0,
moving: false,
contextMenuDisplayed: false,
};
@ -134,7 +129,7 @@ export default class ImageView extends React.Component<IProps, IState> {
componentDidMount() {
// We have to use addEventListener() because the listener
// needs to be passive in order to work with Chromium
this.focusLock.current.addEventListener('wheel', this.onWheel, { passive: false });
this.focusLock.current.addEventListener("wheel", this.onWheel, { passive: false });
// We want to recalculate zoom whenever the window's size changes
window.addEventListener("resize", this.recalculateZoom);
// After the image loads for the first time we want to calculate the zoom
@ -142,7 +137,7 @@ export default class ImageView extends React.Component<IProps, IState> {
}
componentWillUnmount() {
this.focusLock.current.removeEventListener('wheel', this.onWheel);
this.focusLock.current.removeEventListener("wheel", this.onWheel);
window.removeEventListener("resize", this.recalculateZoom);
this.image.current.removeEventListener("load", this.imageLoaded);
}
@ -205,7 +200,7 @@ export default class ImageView extends React.Component<IProps, IState> {
const minZoom = Math.min(zoomX, zoomY) * MAX_SCALE;
// If zoom is smaller than minZoom don't go below that value
const zoom = (this.state.zoom <= this.state.minZoom) ? minZoom : this.state.zoom;
const zoom = this.state.zoom <= this.state.minZoom ? minZoom : this.state.zoom;
this.setState({
minZoom: minZoom,
@ -234,8 +229,8 @@ export default class ImageView extends React.Component<IProps, IState> {
// Zoom relative to the center of the view
this.setState({
zoom: newZoom,
translationX: this.state.translationX * newZoom / oldZoom,
translationY: this.state.translationY * newZoom / oldZoom,
translationX: (this.state.translationX * newZoom) / oldZoom,
translationY: (this.state.translationY * newZoom) / oldZoom,
});
} else {
// Zoom relative to the given point on the image.
@ -358,9 +353,11 @@ export default class ImageView extends React.Component<IProps, IState> {
// Zoom in if we are completely zoomed out and increase the zoom factor for images
// smaller than the viewport size
if (this.state.zoom === this.state.minZoom) {
this.zoom(this.state.maxZoom === this.state.minZoom
? 2 * this.state.maxZoom
: this.state.maxZoom, ev.nativeEvent.offsetX, ev.nativeEvent.offsetY);
this.zoom(
this.state.maxZoom === this.state.minZoom ? 2 * this.state.maxZoom : this.state.maxZoom,
ev.nativeEvent.offsetX,
ev.nativeEvent.offsetY,
);
return;
}
@ -411,11 +408,7 @@ export default class ImageView extends React.Component<IProps, IState> {
);
}
return (
<React.Fragment>
{ contextMenu }
</React.Fragment>
);
return <React.Fragment>{contextMenu}</React.Fragment>;
}
render() {
@ -457,11 +450,7 @@ export default class ImageView extends React.Component<IProps, IState> {
}
const senderName = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
const sender = (
<div className="mx_ImageView_info_sender">
{ senderName }
</div>
);
const sender = <div className="mx_ImageView_info_sender">{senderName}</div>;
const messageTimestamp = (
<a
href={permalink}
@ -488,10 +477,10 @@ export default class ImageView extends React.Component<IProps, IState> {
info = (
<div className="mx_ImageView_info_wrapper">
{ avatar }
{avatar}
<div className="mx_ImageView_info">
{ sender }
{ messageTimestamp }
{sender}
{messageTimestamp}
</div>
</div>
);
@ -499,9 +488,7 @@ export default class ImageView extends React.Component<IProps, IState> {
// If there is no event - we're viewing an avatar, we set
// an empty div here, since the panel uses space-between
// and we want the same placement of elements
info = (
<div />
);
info = <div />;
}
let contextMenuButton;
@ -536,7 +523,7 @@ export default class ImageView extends React.Component<IProps, IState> {
if (this.props.mxEvent?.getContent()) {
title = (
<div className="mx_ImageView_title">
{ presentableTextForFile(this.props.mxEvent?.getContent(), _t("Image"), true) }
{presentableTextForFile(this.props.mxEvent?.getContent(), _t("Image"), true)}
</div>
);
}
@ -552,11 +539,11 @@ export default class ImageView extends React.Component<IProps, IState> {
ref={this.focusLock}
>
<div className="mx_ImageView_panel">
{ info }
{ title }
{info}
{title}
<div className="mx_ImageView_toolbar">
{ zoomOutButton }
{ zoomInButton }
{zoomOutButton}
{zoomInButton}
<AccessibleTooltipButton
className="mx_ImageView_button mx_ImageView_button_rotateCCW"
title={_t("Rotate Left")}
@ -572,13 +559,13 @@ export default class ImageView extends React.Component<IProps, IState> {
title={_t("Download")}
onClick={this.onDownloadClick}
/>
{ contextMenuButton }
{contextMenuButton}
<AccessibleTooltipButton
className="mx_ImageView_button mx_ImageView_button_close"
title={_t("Close")}
onClick={this.props.onFinished}
/>
{ this.renderContextMenu() }
{this.renderContextMenu()}
</div>
</div>
<div

View file

@ -15,12 +15,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import classNames from 'classnames';
import React from "react";
import classNames from "classnames";
import { Alignment } from './Tooltip';
import { Alignment } from "./Tooltip";
import { _t } from "../../../languageHandler";
import TooltipTarget from './TooltipTarget';
import TooltipTarget from "./TooltipTarget";
export enum InfoTooltipKind {
Info = "info",
@ -42,21 +42,20 @@ export default class InfoTooltip extends React.PureComponent<ITooltipProps> {
render() {
const { tooltip, children, tooltipClassName, className, kind } = this.props;
const title = _t("Information");
const iconClassName = (
(kind !== InfoTooltipKind.Warning) ?
"mx_InfoTooltip_icon_info" : "mx_InfoTooltip_icon_warning"
);
const iconClassName =
kind !== InfoTooltipKind.Warning ? "mx_InfoTooltip_icon_info" : "mx_InfoTooltip_icon_warning";
// Tooltip are forced on the right for a more natural feel to them on info icons
return (
<TooltipTarget tooltipTargetClassName={classNames("mx_InfoTooltip", className)}
<TooltipTarget
tooltipTargetClassName={classNames("mx_InfoTooltip", className)}
className="mx_InfoTooltip_container"
tooltipClassName={classNames("mx_InfoTooltip_tooltip", tooltipClassName)}
label={tooltip || title}
alignment={Alignment.Right}
>
<span className={classNames("mx_InfoTooltip_icon", iconClassName)} aria-label={title} />
{ children }
{children}
</TooltipTarget>
);
}

View file

@ -38,7 +38,7 @@ export default class InlineSpinner extends React.PureComponent<IProps> {
style={{ width: this.props.w, height: this.props.h }}
aria-label={_t("Loading...")}
>
{ this.props.children }
{this.props.children}
</div>
</div>
);

View file

@ -67,7 +67,7 @@ function isInUpperLeftHalf(x: number, y: number, rect: IRect): boolean {
// Negative slope because Y values grow downwards and for this case, the
// diagonal goes from larger to smaller Y values.
const diagonalSlope = getDiagonalSlope(rect) * -1;
return isInRect(x, y, rect) && (y <= bottom + diagonalSlope * (x - left));
return isInRect(x, y, rect) && y <= bottom + diagonalSlope * (x - left);
}
function isInLowerRightHalf(x: number, y: number, rect: IRect): boolean {
@ -75,7 +75,7 @@ function isInLowerRightHalf(x: number, y: number, rect: IRect): boolean {
// Negative slope because Y values grow downwards and for this case, the
// diagonal goes from larger to smaller Y values.
const diagonalSlope = getDiagonalSlope(rect) * -1;
return isInRect(x, y, rect) && (y >= bottom + diagonalSlope * (x - left));
return isInRect(x, y, rect) && y >= bottom + diagonalSlope * (x - left);
}
function isInUpperRightHalf(x: number, y: number, rect: IRect): boolean {
@ -83,7 +83,7 @@ function isInUpperRightHalf(x: number, y: number, rect: IRect): boolean {
// Positive slope because Y values grow downwards and for this case, the
// diagonal goes from smaller to larger Y values.
const diagonalSlope = getDiagonalSlope(rect) * 1;
return isInRect(x, y, rect) && (y <= top + diagonalSlope * (x - left));
return isInRect(x, y, rect) && y <= top + diagonalSlope * (x - left);
}
function isInLowerLeftHalf(x: number, y: number, rect: IRect): boolean {
@ -91,7 +91,7 @@ function isInLowerLeftHalf(x: number, y: number, rect: IRect): boolean {
// Positive slope because Y values grow downwards and for this case, the
// diagonal goes from smaller to larger Y values.
const diagonalSlope = getDiagonalSlope(rect) * 1;
return isInRect(x, y, rect) && (y >= top + diagonalSlope * (x - left));
return isInRect(x, y, rect) && y >= top + diagonalSlope * (x - left);
}
export enum Direction {
@ -284,10 +284,7 @@ export function mouseWithinRegion(
}
interface IProps {
children(props: {
ref: RefCallback<HTMLElement>;
onMouseOver: MouseEventHandler;
}): ReactNode;
children(props: { ref: RefCallback<HTMLElement>; onMouseOver: MouseEventHandler }): ReactNode;
// Content to show in the tooltip
content: ReactNode;
direction?: Direction;
@ -353,11 +350,11 @@ export default class InteractiveTooltip extends React.Component<IProps, IState>
if (this.props.direction === Direction.Left) {
const targetLeft = targetRect.left + window.scrollX;
return !contentRect || (targetLeft - contentRect.width > MIN_SAFE_DISTANCE_TO_WINDOW_EDGE);
return !contentRect || targetLeft - contentRect.width > MIN_SAFE_DISTANCE_TO_WINDOW_EDGE;
} else {
const targetRight = targetRect.right + window.scrollX;
const spaceOnRight = UIStore.instance.windowWidth - targetRight;
return contentRect && (spaceOnRight - contentRect.width < MIN_SAFE_DISTANCE_TO_WINDOW_EDGE);
return contentRect && spaceOnRight - contentRect.width < MIN_SAFE_DISTANCE_TO_WINDOW_EDGE;
}
}
@ -367,11 +364,11 @@ export default class InteractiveTooltip extends React.Component<IProps, IState>
if (this.props.direction === Direction.Top) {
const targetTop = targetRect.top + window.scrollY;
return !contentRect || (targetTop - contentRect.height > MIN_SAFE_DISTANCE_TO_WINDOW_EDGE);
return !contentRect || targetTop - contentRect.height > MIN_SAFE_DISTANCE_TO_WINDOW_EDGE;
} else {
const targetBottom = targetRect.bottom + window.scrollY;
const spaceBelow = UIStore.instance.windowHeight - targetBottom;
return contentRect && (spaceBelow - contentRect.height < MIN_SAFE_DISTANCE_TO_WINDOW_EDGE);
return contentRect && spaceBelow - contentRect.height < MIN_SAFE_DISTANCE_TO_WINDOW_EDGE;
}
}
@ -465,11 +462,11 @@ export default class InteractiveTooltip extends React.Component<IProps, IState>
const chevron = <div className={"mx_InteractiveTooltip_chevron_" + chevronFace} />;
const menuClasses = classNames({
'mx_InteractiveTooltip': true,
'mx_InteractiveTooltip_withChevron_top': chevronFace === ChevronFace.Top,
'mx_InteractiveTooltip_withChevron_left': chevronFace === ChevronFace.Left,
'mx_InteractiveTooltip_withChevron_right': chevronFace === ChevronFace.Right,
'mx_InteractiveTooltip_withChevron_bottom': chevronFace === ChevronFace.Bottom,
mx_InteractiveTooltip: true,
mx_InteractiveTooltip_withChevron_top: chevronFace === ChevronFace.Top,
mx_InteractiveTooltip_withChevron_left: chevronFace === ChevronFace.Left,
mx_InteractiveTooltip_withChevron_right: chevronFace === ChevronFace.Right,
mx_InteractiveTooltip_withChevron_bottom: chevronFace === ChevronFace.Bottom,
});
const menuStyle: CSSProperties = {};
@ -477,12 +474,14 @@ export default class InteractiveTooltip extends React.Component<IProps, IState>
menuStyle.left = `-${contentRect.width / 2}px`;
}
const tooltip = <div className="mx_InteractiveTooltip_wrapper" style={{ ...position }}>
<div className={menuClasses} style={menuStyle} ref={this.collectContentRect}>
{ chevron }
{ this.props.content }
const tooltip = (
<div className="mx_InteractiveTooltip_wrapper" style={{ ...position }}>
<div className={menuClasses} style={menuStyle} ref={this.collectContentRect}>
{chevron}
{this.props.content}
</div>
</div>
</div>;
);
ReactDOM.render(tooltip, getOrCreateContainer());
}

View file

@ -47,17 +47,19 @@ export default class InviteReason extends React.PureComponent<IProps, IState> {
render() {
const classes = classNames({
"mx_InviteReason": true,
"mx_InviteReason_hidden": this.state.hidden,
mx_InviteReason: true,
mx_InviteReason_hidden: this.state.hidden,
});
return <div className={classes}>
<div className="mx_InviteReason_reason">{ this.props.htmlReason ? sanitizedHtmlNode(this.props.htmlReason) : this.props.reason }</div>
<div className="mx_InviteReason_view"
onClick={this.onViewClick}
>
{ _t("View message") }
return (
<div className={classes}>
<div className="mx_InviteReason_reason">
{this.props.htmlReason ? sanitizedHtmlNode(this.props.htmlReason) : this.props.reason}
</div>
<div className="mx_InviteReason_view" onClick={this.onViewClick}>
{_t("View message")}
</div>
</div>
</div>;
);
}
}

View file

@ -14,8 +14,8 @@ 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 React from "react";
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
import Dropdown from "./Dropdown";
@ -40,29 +40,33 @@ const JoinRuleDropdown = ({
}: IProps) => {
const options = [
<div key={JoinRule.Invite} className="mx_JoinRuleDropdown_invite">
{ labelInvite }
{labelInvite}
</div>,
<div key={JoinRule.Public} className="mx_JoinRuleDropdown_public">
{ labelPublic }
{labelPublic}
</div>,
];
if (labelRestricted) {
options.unshift(<div key={JoinRule.Restricted} className="mx_JoinRuleDropdown_restricted">
{ labelRestricted }
</div>);
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>;
return (
<Dropdown
id="mx_JoinRuleDropdown"
className="mx_JoinRuleDropdown"
onOptionChange={onChange}
menuWidth={width}
value={value}
label={label}
>
{options}
</Dropdown>
);
};
export default JoinRuleDropdown;

View file

@ -32,13 +32,15 @@ interface IProps {
}
const LabelledCheckbox: React.FC<IProps> = ({ value, label, byline, disabled, onChange }) => {
return <label className="mx_LabelledCheckbox">
<StyledCheckbox disabled={disabled} checked={value} onChange={e => onChange(e.target.checked)} />
<div className="mx_LabelledCheckbox_labels">
<span className="mx_LabelledCheckbox_label">{ label }</span>
{ byline ? <span className="mx_LabelledCheckbox_byline">{ byline }</span> : null }
</div>
</label>;
return (
<label className="mx_LabelledCheckbox">
<StyledCheckbox disabled={disabled} checked={value} onChange={(e) => onChange(e.target.checked)} />
<div className="mx_LabelledCheckbox_labels">
<span className="mx_LabelledCheckbox_label">{label}</span>
{byline ? <span className="mx_LabelledCheckbox_byline">{byline}</span> : null}
</div>
</label>
);
};
export default LabelledCheckbox;

View file

@ -44,20 +44,26 @@ export default class LabelledToggleSwitch extends React.PureComponent<IProps> {
public render() {
// This is a minimal version of a SettingsFlag
const { label, caption } = this.props;
let firstPart = <span className="mx_SettingsFlag_label">
{ label }
{ caption && <>
<br />
<Caption>{ caption }</Caption>
</> }
</span>;
let secondPart = <ToggleSwitch
checked={this.props.value}
disabled={this.props.disabled}
onChange={this.props.onChange}
title={this.props.label}
tooltip={this.props.tooltip}
/>;
let firstPart = (
<span className="mx_SettingsFlag_label">
{label}
{caption && (
<>
<br />
<Caption>{caption}</Caption>
</>
)}
</span>
);
let secondPart = (
<ToggleSwitch
checked={this.props.value}
disabled={this.props.disabled}
onChange={this.props.onChange}
title={this.props.label}
tooltip={this.props.tooltip}
/>
);
if (this.props.toggleInFront) {
const temp = firstPart;
@ -66,12 +72,12 @@ export default class LabelledToggleSwitch extends React.PureComponent<IProps> {
}
const classes = classNames("mx_SettingsFlag", this.props.className, {
"mx_SettingsFlag_toggleInFront": this.props.toggleInFront,
mx_SettingsFlag_toggleInFront: this.props.toggleInFront,
});
return (
<div data-testid={this.props["data-testid"]} className={classes}>
{ firstPart }
{ secondPart }
{firstPart}
{secondPart}
</div>
);
}

View file

@ -15,9 +15,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import * as languageHandler from '../../../languageHandler';
import * as languageHandler from "../../../languageHandler";
import SettingsStore from "../../../settings/SettingsStore";
import { _t } from "../../../languageHandler";
import Spinner from "./Spinner";
@ -46,22 +46,25 @@ export default class LanguageDropdown extends React.Component<IProps, IState> {
super(props);
this.state = {
searchQuery: '',
searchQuery: "",
langs: null,
};
}
public componentDidMount(): void {
languageHandler.getAllLanguagesFromJson().then((langs) => {
langs.sort(function(a, b) {
if (a.label < b.label) return -1;
if (a.label > b.label) return 1;
return 0;
languageHandler
.getAllLanguagesFromJson()
.then((langs) => {
langs.sort(function (a, b) {
if (a.label < b.label) return -1;
if (a.label > b.label) return 1;
return 0;
});
this.setState({ langs });
})
.catch(() => {
this.setState({ langs: [{ value: "en", label: "English" }] });
});
this.setState({ langs });
}).catch(() => {
this.setState({ langs: [{ value: 'en', label: "English" }] });
});
if (!this.props.value) {
// If no value is given, we start with the first
@ -93,14 +96,12 @@ export default class LanguageDropdown extends React.Component<IProps, IState> {
}
const options = displayedLanguages.map((language) => {
return <div key={language.value}>
{ language.label }
</div>;
return <div key={language.value}>{language.label}</div>;
});
// default value here too, otherwise we need to handle null / undefined
// values between mounting and the initial value propagating
let language = SettingsStore.getValue("language", null, /*excludeDefault:*/true);
let language = SettingsStore.getValue("language", null, /*excludeDefault:*/ true);
let value = null;
if (language) {
value = this.props.value || language;
@ -109,18 +110,19 @@ export default class LanguageDropdown extends React.Component<IProps, IState> {
value = this.props.value || language;
}
return <Dropdown
id="mx_LanguageDropdown"
className={this.props.className}
onOptionChange={this.props.onOptionChange}
onSearchChange={this.onSearchChange}
searchEnabled={true}
value={value}
label={_t("Language Dropdown")}
disabled={this.props.disabled}
>
{ options }
</Dropdown>;
return (
<Dropdown
id="mx_LanguageDropdown"
className={this.props.className}
onOptionChange={this.props.onOptionChange}
onSearchChange={this.onSearchChange}
searchEnabled={true}
value={value}
label={_t("Language Dropdown")}
disabled={this.props.disabled}
>
{options}
</Dropdown>
);
}
}

View file

@ -17,11 +17,7 @@ limitations under the License.
import React from "react";
class ItemRange {
constructor(
public topCount: number,
public renderCount: number,
public bottomCount: number,
) { }
constructor(public topCount: number, public renderCount: number, public bottomCount: number) {}
public contains(range: ItemRange): boolean {
// don't contain empty ranges
@ -30,8 +26,9 @@ class ItemRange {
if (!range.renderCount && this.renderCount) {
return false;
}
return range.topCount >= this.topCount &&
(range.topCount + range.renderCount) <= (this.topCount + this.renderCount);
return (
range.topCount >= this.topCount && range.topCount + range.renderCount <= this.topCount + this.renderCount
);
}
public expand(amount: number): ItemRange {
@ -127,17 +124,13 @@ export default class LazyRenderList<T = any> extends React.Component<IProps<T>,
const paddingTop = topCount * itemHeight;
const paddingBottom = bottomCount * itemHeight;
const renderedItems = (items || []).slice(
topCount,
topCount + renderCount,
);
const renderedItems = (items || []).slice(topCount, topCount + renderCount);
const element = this.props.element || "div";
const elementProps = {
"style": { paddingTop: `${paddingTop}px`, paddingBottom: `${paddingBottom}px` },
"className": this.props.className,
style: { paddingTop: `${paddingTop}px`, paddingBottom: `${paddingBottom}px` },
className: this.props.className,
};
return React.createElement(element, elementProps, renderedItems.map(renderItem));
}
}

View file

@ -14,43 +14,33 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
import InfoDialog from '../dialogs/InfoDialog';
import AccessibleButton, { IAccessibleButtonProps } from './AccessibleButton';
import { _t } from "../../../languageHandler";
import Modal from "../../../Modal";
import InfoDialog from "../dialogs/InfoDialog";
import AccessibleButton, { IAccessibleButtonProps } from "./AccessibleButton";
export interface LearnMoreProps extends IAccessibleButtonProps {
title: string;
description: string | React.ReactNode;
}
const LearnMore: React.FC<LearnMoreProps> = ({
title,
description,
...rest
}) => {
const LearnMore: React.FC<LearnMoreProps> = ({ title, description, ...rest }) => {
const onClick = () => {
Modal.createDialog(
InfoDialog,
{
title,
description,
button: _t('Got it'),
hasCloseButton: true,
},
);
Modal.createDialog(InfoDialog, {
title,
description,
button: _t("Got it"),
hasCloseButton: true,
});
};
return <AccessibleButton
{...rest}
kind='link_inline'
onClick={onClick}
className='mx_LearnMore_button'
>
{ _t('Learn more') }
</AccessibleButton>;
return (
<AccessibleButton {...rest} kind="link_inline" onClick={onClick} className="mx_LearnMore_button">
{_t("Learn more")}
</AccessibleButton>
);
};
export default LearnMore;

View file

@ -14,11 +14,11 @@
limitations under the License.
*/
import React from 'react';
import React from "react";
import TextWithTooltip from './TextWithTooltip';
import TextWithTooltip from "./TextWithTooltip";
interface IProps extends Omit<React.ComponentProps<typeof TextWithTooltip>, "tabIndex" | "onClick" > {}
interface IProps extends Omit<React.ComponentProps<typeof TextWithTooltip>, "tabIndex" | "onClick"> {}
export default class LinkWithTooltip extends React.Component<IProps> {
constructor(props: IProps) {
@ -34,10 +34,10 @@ export default class LinkWithTooltip extends React.Component<IProps> {
// itself allows focusing which also triggers the tooltip.
tabIndex={-1}
tooltip={tooltip}
onClick={e => (e.target as HTMLElement).blur()} // Force tooltip to hide on clickout
onClick={(e) => (e.target as HTMLElement).blur()} // Force tooltip to hide on clickout
{...props}
>
{ children }
{children}
</TextWithTooltip>
);
}

View file

@ -24,11 +24,7 @@ interface Props {
onClick?: (ev: MouseEvent) => void;
}
export function Linkify({
as = "div",
children,
onClick,
}: Props): JSX.Element {
export function Linkify({ as = "div", children, onClick }: Props): JSX.Element {
const ref = useRef();
useLayoutEffect(() => {
@ -41,4 +37,3 @@ export function Linkify({
onClick,
});
}

View file

@ -50,8 +50,7 @@ export default class Measured extends React.PureComponent<IProps> {
UIStore.instance.stopTrackingElementDimensions(`Measured${this.instanceId}`);
}
if (current) {
UIStore.instance.trackElementDimensions(`Measured${this.instanceId}`,
this.props.sensor);
UIStore.instance.trackElementDimensions(`Measured${this.instanceId}`, this.props.sensor);
}
}

View file

@ -14,14 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import classNames from 'classnames';
import { EventType } from 'matrix-js-sdk/src/@types/event';
import React, { useContext, useRef, useState, MouseEvent } from 'react';
import classNames from "classnames";
import { EventType } from "matrix-js-sdk/src/@types/event";
import React, { useContext, useRef, useState, MouseEvent } from "react";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import RoomContext from "../../../contexts/RoomContext";
import { useTimeout } from "../../../hooks/useTimeout";
import { TranslatedString } from '../../../languageHandler';
import { TranslatedString } from "../../../languageHandler";
import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds";
import AccessibleButton from "./AccessibleButton";
import Spinner from "./Spinner";
@ -38,7 +38,13 @@ interface IProps {
}
const MiniAvatarUploader: React.FC<IProps> = ({
hasAvatar, hasAvatarLabel, noAvatarLabel, setAvatarUrl, isUserAvatar, children, onClick,
hasAvatar,
hasAvatarLabel,
noAvatarLabel,
setAvatarUrl,
isUserAvatar,
children,
onClick,
}) => {
const cli = useContext(MatrixClientContext);
const [busy, setBusy] = useState(false);
@ -54,62 +60,64 @@ const MiniAvatarUploader: React.FC<IProps> = ({
const uploadRef = useRef<HTMLInputElement>();
const label = (hasAvatar || busy) ? hasAvatarLabel : noAvatarLabel;
const label = hasAvatar || busy ? hasAvatarLabel : noAvatarLabel;
const { room } = useContext(RoomContext);
const canSetAvatar = isUserAvatar || room?.currentState?.maySendStateEvent(EventType.RoomAvatar, cli.getUserId());
if (!canSetAvatar) return <React.Fragment>{ children }</React.Fragment>;
if (!canSetAvatar) return <React.Fragment>{children}</React.Fragment>;
const visible = !!label && (hover || show);
return <React.Fragment>
<input
type="file"
ref={uploadRef}
className="mx_MiniAvatarUploader_input"
onClick={(ev) => {
chromeFileInputFix(ev);
onClick?.(ev);
}}
onChange={async (ev) => {
if (!ev.target.files?.length) return;
setBusy(true);
const file = ev.target.files[0];
const { content_uri: uri } = await cli.uploadContent(file);
await setAvatarUrl(uri);
setBusy(false);
}}
accept="image/*"
/>
return (
<React.Fragment>
<input
type="file"
ref={uploadRef}
className="mx_MiniAvatarUploader_input"
onClick={(ev) => {
chromeFileInputFix(ev);
onClick?.(ev);
}}
onChange={async (ev) => {
if (!ev.target.files?.length) return;
setBusy(true);
const file = ev.target.files[0];
const { content_uri: uri } = await cli.uploadContent(file);
await setAvatarUrl(uri);
setBusy(false);
}}
accept="image/*"
/>
<AccessibleButton
className={classNames("mx_MiniAvatarUploader", {
mx_MiniAvatarUploader_busy: busy,
mx_MiniAvatarUploader_hasAvatar: hasAvatar,
})}
disabled={busy}
onClick={() => {
uploadRef.current.click();
}}
onMouseOver={() => setHover(true)}
onMouseLeave={() => setHover(false)}
>
{ children }
<AccessibleButton
className={classNames("mx_MiniAvatarUploader", {
mx_MiniAvatarUploader_busy: busy,
mx_MiniAvatarUploader_hasAvatar: hasAvatar,
})}
disabled={busy}
onClick={() => {
uploadRef.current.click();
}}
onMouseOver={() => setHover(true)}
onMouseLeave={() => setHover(false)}
>
{children}
<div className="mx_MiniAvatarUploader_indicator">
{ busy ?
<Spinner w={20} h={20} /> :
<div className="mx_MiniAvatarUploader_cameraIcon" /> }
</div>
<div className="mx_MiniAvatarUploader_indicator">
{busy ? <Spinner w={20} h={20} /> : <div className="mx_MiniAvatarUploader_cameraIcon" />}
</div>
<div className={classNames("mx_Tooltip", {
"mx_Tooltip_visible": visible,
"mx_Tooltip_invisible": !visible,
})}>
<div className="mx_Tooltip_chevron" />
{ label }
</div>
</AccessibleButton>
</React.Fragment>;
<div
className={classNames("mx_Tooltip", {
mx_Tooltip_visible: visible,
mx_Tooltip_invisible: !visible,
})}
>
<div className="mx_Tooltip_chevron" />
{label}
</div>
</AccessibleButton>
</React.Fragment>
);
};
export default MiniAvatarUploader;

View file

@ -14,17 +14,17 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { MutableRefObject } from 'react';
import ReactDOM from 'react-dom';
import React, { MutableRefObject } from "react";
import ReactDOM from "react-dom";
import { throttle } from "lodash";
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
import dis from '../../../dispatcher/dispatcher';
import dis from "../../../dispatcher/dispatcher";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { ActionPayload } from "../../../dispatcher/payloads";
export const getPersistKey = (appId: string) => 'widget_' + appId;
export const getPersistKey = (appId: string) => "widget_" + appId;
// Shamelessly ripped off Modal.js. There's probably a better way
// of doing reusable widgets like dialog boxes & menus where we go and
@ -87,7 +87,7 @@ export default class PersistedElement extends React.Component<IProps> {
// dimensions. Doesn't look like there's a ResizeObserver equivalent
// for this, so we bodge it by listening for document resize and
// the timeline_resize action.
window.addEventListener('resize', this.repositionChild);
window.addEventListener("resize", this.repositionChild);
this.dispatcherRef = dis.register(this.onAction);
if (this.props.moveRef) this.props.moveRef.current = this.repositionChild;
@ -101,14 +101,14 @@ export default class PersistedElement extends React.Component<IProps> {
* @param {string} persistKey Key used to uniquely identify this PersistedElement
*/
public static destroyElement(persistKey: string): void {
const container = getContainer('mx_persistedElement_' + persistKey);
const container = getContainer("mx_persistedElement_" + persistKey);
if (container) {
container.remove();
}
}
static isMounted(persistKey) {
return Boolean(getContainer('mx_persistedElement_' + persistKey));
return Boolean(getContainer("mx_persistedElement_" + persistKey));
}
private collectChildContainer = (ref: HTMLDivElement): void => {
@ -139,14 +139,14 @@ export default class PersistedElement extends React.Component<IProps> {
public componentWillUnmount(): void {
this.updateChildVisibility(this.child, false);
this.resizeObserver.disconnect();
window.removeEventListener('resize', this.repositionChild);
window.removeEventListener("resize", this.repositionChild);
dis.unregister(this.dispatcherRef);
}
private onAction = (payload: ActionPayload): void => {
if (payload.action === 'timeline_resize') {
if (payload.action === "timeline_resize") {
this.repositionChild();
} else if (payload.action === 'logout') {
} else if (payload.action === "logout") {
PersistedElement.destroyElement(this.props.persistKey);
}
};
@ -161,37 +161,42 @@ export default class PersistedElement extends React.Component<IProps> {
}
private renderApp(): void {
const content = <MatrixClientContext.Provider value={MatrixClientPeg.get()}>
<div ref={this.collectChild} style={this.props.style}>
{ this.props.children }
</div>
</MatrixClientContext.Provider>;
const content = (
<MatrixClientContext.Provider value={MatrixClientPeg.get()}>
<div ref={this.collectChild} style={this.props.style}>
{this.props.children}
</div>
</MatrixClientContext.Provider>
);
ReactDOM.render(content, getOrCreateContainer('mx_persistedElement_'+this.props.persistKey));
ReactDOM.render(content, getOrCreateContainer("mx_persistedElement_" + this.props.persistKey));
}
private updateChildVisibility(child: HTMLDivElement, visible: boolean): void {
if (!child) return;
child.style.display = visible ? 'block' : 'none';
child.style.display = visible ? "block" : "none";
}
private updateChildPosition = throttle((child: HTMLDivElement, parent: HTMLDivElement): void => {
if (!child || !parent) return;
private updateChildPosition = throttle(
(child: HTMLDivElement, parent: HTMLDivElement): void => {
if (!child || !parent) return;
const parentRect = parent.getBoundingClientRect();
Object.assign(child.style, {
zIndex: isNullOrUndefined(this.props.zIndex) ? 9 : this.props.zIndex,
position: 'absolute',
top: '0',
left: '0',
transform: `translateX(${parentRect.left}px) translateY(${parentRect.top}px)`,
width: parentRect.width + 'px',
height: parentRect.height + 'px',
});
}, 16, { trailing: true, leading: true });
const parentRect = parent.getBoundingClientRect();
Object.assign(child.style, {
zIndex: isNullOrUndefined(this.props.zIndex) ? 9 : this.props.zIndex,
position: "absolute",
top: "0",
left: "0",
transform: `translateX(${parentRect.left}px) translateY(${parentRect.top}px)`,
width: parentRect.width + "px",
height: parentRect.height + "px",
});
},
16,
{ trailing: true, leading: true },
);
public render(): JSX.Element {
return <div ref={this.collectChildContainer} />;
}
}

View file

@ -15,12 +15,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ContextType, MutableRefObject } from 'react';
import React, { ContextType, MutableRefObject } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import WidgetUtils from '../../../utils/WidgetUtils';
import WidgetUtils from "../../../utils/WidgetUtils";
import AppTile from "./AppTile";
import WidgetStore from '../../../stores/WidgetStore';
import WidgetStore from "../../../stores/WidgetStore";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
interface IProps {
@ -44,20 +44,21 @@ export default class PersistentApp extends React.Component<IProps> {
const app = WidgetStore.instance.get(this.props.persistentWidgetId, this.props.persistentRoomId);
if (!app) return null;
return <AppTile
key={app.id}
app={app}
fullWidth={true}
room={this.room}
userId={this.context.credentials.userId}
creatorUserId={app.creatorUserId}
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
waitForIframeLoad={app.waitForIframeLoad}
miniMode={true}
showMenubar={false}
pointerEvents={this.props.pointerEvents}
movePersistedElement={this.props.movePersistedElement}
/>;
return (
<AppTile
key={app.id}
app={app}
fullWidth={true}
room={this.room}
userId={this.context.credentials.userId}
creatorUserId={app.creatorUserId}
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
waitForIframeLoad={app.waitForIframeLoad}
miniMode={true}
showMenubar={false}
pointerEvents={this.props.pointerEvents}
movePersistedElement={this.props.movePersistedElement}
/>
);
}
}

View file

@ -14,27 +14,27 @@ 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 { RoomMember } from 'matrix-js-sdk/src/models/room-member';
import React from "react";
import classNames from "classnames";
import { Room } from "matrix-js-sdk/src/models/room";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { logger } from "matrix-js-sdk/src/logger";
import { MatrixClient } from 'matrix-js-sdk/src/client';
import { MatrixClient } from "matrix-js-sdk/src/client";
import dis from '../../../dispatcher/dispatcher';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import dis from "../../../dispatcher/dispatcher";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { getPrimaryPermalinkEntity, parsePermalink } from "../../../utils/permalinks/Permalinks";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { Action } from "../../../dispatcher/actions";
import Tooltip, { Alignment } from './Tooltip';
import RoomAvatar from '../avatars/RoomAvatar';
import MemberAvatar from '../avatars/MemberAvatar';
import Tooltip, { Alignment } from "./Tooltip";
import RoomAvatar from "../avatars/RoomAvatar";
import MemberAvatar from "../avatars/MemberAvatar";
import { objectHasDiff } from "../../../utils/objects";
export enum PillType {
UserMention = 'TYPE_USER_MENTION',
RoomMention = 'TYPE_ROOM_MENTION',
AtRoomMention = 'TYPE_AT_ROOM_MENTION', // '@room' mention
UserMention = "TYPE_USER_MENTION",
RoomMention = "TYPE_ROOM_MENTION",
AtRoomMention = "TYPE_AT_ROOM_MENTION", // '@room' mention
}
interface IProps {
@ -102,41 +102,51 @@ export default class Pill extends React.Component<IProps, IState> {
}
}
const pillType = this.props.type || {
'@': PillType.UserMention,
'#': PillType.RoomMention,
'!': PillType.RoomMention,
}[prefix];
const pillType =
this.props.type ||
{
"@": PillType.UserMention,
"#": PillType.RoomMention,
"!": PillType.RoomMention,
}[prefix];
let member: RoomMember;
let room: Room;
switch (pillType) {
case PillType.AtRoomMention: {
room = this.props.room;
}
break;
case PillType.UserMention: {
const localMember = this.props.room?.getMember(resourceId);
member = localMember;
if (!localMember) {
member = new RoomMember(null, resourceId);
this.doProfileLookup(resourceId, member);
case PillType.AtRoomMention:
{
room = this.props.room;
}
}
break;
case PillType.RoomMention: {
const localRoom = resourceId[0] === '#' ?
MatrixClientPeg.get().getRooms().find((r) => {
return r.getCanonicalAlias() === resourceId ||
r.getAltAliases().includes(resourceId);
}) : MatrixClientPeg.get().getRoom(resourceId);
room = localRoom;
if (!localRoom) {
// TODO: This would require a new API to resolve a room alias to
// a room avatar and name.
// this.doRoomProfileLookup(resourceId, member);
case PillType.UserMention:
{
const localMember = this.props.room?.getMember(resourceId);
member = localMember;
if (!localMember) {
member = new RoomMember(null, resourceId);
this.doProfileLookup(resourceId, member);
}
}
break;
case PillType.RoomMention:
{
const localRoom =
resourceId[0] === "#"
? MatrixClientPeg.get()
.getRooms()
.find((r) => {
return (
r.getCanonicalAlias() === resourceId || r.getAltAliases().includes(resourceId)
);
})
: MatrixClientPeg.get().getRoom(resourceId);
room = localRoom;
if (!localRoom) {
// TODO: This would require a new API to resolve a room alias to
// a room avatar and name.
// this.doRoomProfileLookup(resourceId, member);
}
}
}
break;
}
this.setState({ resourceId, pillType, member, room });
@ -171,24 +181,27 @@ export default class Pill extends React.Component<IProps, IState> {
};
private doProfileLookup(userId: string, member): void {
MatrixClientPeg.get().getProfileInfo(userId).then((resp) => {
if (this.unmounted) {
return;
}
member.name = resp.displayname;
member.rawDisplayName = resp.displayname;
member.events.member = {
getContent: () => {
return { avatar_url: resp.avatar_url };
},
getDirectionalContent: function() {
return this.getContent();
},
};
this.setState({ member });
}).catch((err) => {
logger.error('Could not retrieve profile data for ' + userId + ':', err);
});
MatrixClientPeg.get()
.getProfileInfo(userId)
.then((resp) => {
if (this.unmounted) {
return;
}
member.name = resp.displayname;
member.rawDisplayName = resp.displayname;
member.events.member = {
getContent: () => {
return { avatar_url: resp.avatar_url };
},
getDirectionalContent: function () {
return this.getContent();
},
};
this.setState({ member });
})
.catch((err) => {
logger.error("Could not retrieve profile data for " + userId + ":", err);
});
}
private onUserPillClicked = (e): void => {
@ -209,48 +222,53 @@ export default class Pill extends React.Component<IProps, IState> {
let href = this.props.url;
let onClick;
switch (this.state.pillType) {
case PillType.AtRoomMention: {
const room = this.props.room;
if (room) {
linkText = "@room";
if (this.props.shouldShowPillAvatar) {
avatar = <RoomAvatar room={room} width={16} height={16} aria-hidden="true" />;
case PillType.AtRoomMention:
{
const room = this.props.room;
if (room) {
linkText = "@room";
if (this.props.shouldShowPillAvatar) {
avatar = <RoomAvatar room={room} width={16} height={16} aria-hidden="true" />;
}
pillClass = "mx_AtRoomPill";
}
pillClass = 'mx_AtRoomPill';
}
}
break;
case PillType.UserMention: {
// If this user is not a member of this room, default to the empty member
const member = this.state.member;
if (member) {
userId = member.userId;
member.rawDisplayName = member.rawDisplayName || '';
linkText = member.rawDisplayName;
if (this.props.shouldShowPillAvatar) {
avatar = <MemberAvatar member={member} width={16} height={16} aria-hidden="true" hideTitle />;
case PillType.UserMention:
{
// If this user is not a member of this room, default to the empty member
const member = this.state.member;
if (member) {
userId = member.userId;
member.rawDisplayName = member.rawDisplayName || "";
linkText = member.rawDisplayName;
if (this.props.shouldShowPillAvatar) {
avatar = (
<MemberAvatar member={member} width={16} height={16} aria-hidden="true" hideTitle />
);
}
pillClass = "mx_UserPill";
href = null;
onClick = this.onUserPillClicked;
}
pillClass = 'mx_UserPill';
href = null;
onClick = this.onUserPillClicked;
}
}
break;
case PillType.RoomMention: {
const room = this.state.room;
if (room) {
linkText = room.name || resource;
if (this.props.shouldShowPillAvatar) {
avatar = <RoomAvatar room={room} width={16} height={16} aria-hidden="true" />;
case PillType.RoomMention:
{
const room = this.state.room;
if (room) {
linkText = room.name || resource;
if (this.props.shouldShowPillAvatar) {
avatar = <RoomAvatar room={room} width={16} height={16} aria-hidden="true" />;
}
}
pillClass = room?.isSpaceRoom() ? "mx_SpacePill" : "mx_RoomPill";
}
pillClass = room?.isSpaceRoom() ? "mx_SpacePill" : "mx_RoomPill";
}
break;
}
const classes = classNames("mx_Pill", pillClass, {
"mx_UserPill_me": userId === MatrixClientPeg.get().getUserId(),
mx_UserPill_me: userId === MatrixClientPeg.get().getUserId(),
});
if (this.state.pillType) {
@ -259,29 +277,31 @@ export default class Pill extends React.Component<IProps, IState> {
tip = <Tooltip label={resource} alignment={Alignment.Right} />;
}
return <bdi><MatrixClientContext.Provider value={this.matrixClient}>
{ this.props.inMessage ?
<a
className={classes}
href={href}
onClick={onClick}
onMouseOver={this.onMouseOver}
onMouseLeave={this.onMouseLeave}
>
{ avatar }
<span className="mx_Pill_linkText">{ linkText }</span>
{ tip }
</a> :
<span
className={classes}
onMouseOver={this.onMouseOver}
onMouseLeave={this.onMouseLeave}
>
{ avatar }
<span className="mx_Pill_linkText">{ linkText }</span>
{ tip }
</span> }
</MatrixClientContext.Provider></bdi>;
return (
<bdi>
<MatrixClientContext.Provider value={this.matrixClient}>
{this.props.inMessage ? (
<a
className={classes}
href={href}
onClick={onClick}
onMouseOver={this.onMouseOver}
onMouseLeave={this.onMouseLeave}
>
{avatar}
<span className="mx_Pill_linkText">{linkText}</span>
{tip}
</a>
) : (
<span className={classes} onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave}>
{avatar}
<span className="mx_Pill_linkText">{linkText}</span>
{tip}
</span>
)}
</MatrixClientContext.Provider>
</bdi>
);
} else {
// Deliberately render nothing if the URL isn't recognised
return null;

View file

@ -29,7 +29,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import ScrollableBaseModal, { IScrollableBaseState } from "../dialogs/ScrollableBaseModal";
import { IDialogProps } from "../dialogs/IDialogProps";
import QuestionDialog from "../dialogs/QuestionDialog";
import Modal from '../../../Modal';
import Modal from "../../../Modal";
import { _t } from "../../../languageHandler";
import { arrayFastClone, arraySeed } from "../../../utils/arrays";
import Field from "./Field";
@ -40,7 +40,7 @@ import { doMaybeLocalRoomAction } from "../../../utils/local-room";
interface IProps extends IDialogProps {
room: Room;
threadId?: string;
editingMxEvent?: MatrixEvent; // Truthy if we are editing an existing poll
editingMxEvent?: MatrixEvent; // Truthy if we are editing an existing poll
}
enum FocusTarget {
@ -83,7 +83,7 @@ function editingInitialState(editingMxEvent: MatrixEvent): IState {
actionLabel: _t("Done"),
canSubmit: true,
question: poll.question.text,
options: poll.answers.map(ans => ans.text),
options: poll.answers.map((ans) => ans.text),
busy: false,
kind: poll.kind,
autoFocusTarget: FocusTarget.Topic,
@ -96,11 +96,7 @@ export default class PollCreateDialog extends ScrollableBaseModal<IProps, IState
public constructor(props: IProps) {
super(props);
this.state = (
props.editingMxEvent
? editingInitialState(props.editingMxEvent)
: creatingInitialState()
);
this.state = props.editingMxEvent ? editingInitialState(props.editingMxEvent) : creatingInitialState();
}
private checkCanSubmit() {
@ -108,7 +104,7 @@ export default class PollCreateDialog extends ScrollableBaseModal<IProps, IState
canSubmit:
!this.state.busy &&
this.state.question.trim().length > 0 &&
this.state.options.filter(op => op.trim().length > 0).length >= MIN_OPTIONS,
this.state.options.filter((op) => op.trim().length > 0).length >= MIN_OPTIONS,
});
}
@ -141,7 +137,7 @@ export default class PollCreateDialog extends ScrollableBaseModal<IProps, IState
private createEvent(): IPartialEvent<object> {
const pollStart = PollStartEvent.from(
this.state.question.trim(),
this.state.options.map(a => a.trim()).filter(a => !!a),
this.state.options.map((a) => a.trim()).filter((a) => !!a),
this.state.kind,
).serialize();
@ -149,14 +145,14 @@ export default class PollCreateDialog extends ScrollableBaseModal<IProps, IState
return pollStart;
} else {
return {
"content": {
content: {
"m.new_content": pollStart.content,
"m.relates_to": {
"rel_type": "m.replace",
"event_id": this.props.editingMxEvent.getId(),
rel_type: "m.replace",
event_id: this.props.editingMxEvent.getId(),
},
},
"type": pollStart.type,
type: pollStart.type,
};
}
}
@ -166,32 +162,27 @@ export default class PollCreateDialog extends ScrollableBaseModal<IProps, IState
const pollEvent = this.createEvent();
doMaybeLocalRoomAction(
this.props.room.roomId,
(actualRoomId: string) => this.matrixClient.sendEvent(
actualRoomId,
this.props.threadId,
pollEvent.type,
pollEvent.content,
),
(actualRoomId: string) =>
this.matrixClient.sendEvent(actualRoomId, this.props.threadId, pollEvent.type, pollEvent.content),
this.matrixClient,
).then(
() => this.props.onFinished(true),
).catch(e => {
console.error("Failed to post poll:", e);
Modal.createDialog(QuestionDialog, {
title: _t("Failed to post poll"),
description: _t(
"Sorry, the poll you tried to create was not posted."),
button: _t('Try again'),
cancelButton: _t('Cancel'),
onFinished: (tryAgain: boolean) => {
if (!tryAgain) {
this.cancel();
} else {
this.setState({ busy: false, canSubmit: true });
}
},
)
.then(() => this.props.onFinished(true))
.catch((e) => {
console.error("Failed to post poll:", e);
Modal.createDialog(QuestionDialog, {
title: _t("Failed to post poll"),
description: _t("Sorry, the poll you tried to create was not posted."),
button: _t("Try again"),
cancelButton: _t("Cancel"),
onFinished: (tryAgain: boolean) => {
if (!tryAgain) {
this.cancel();
} else {
this.setState({ busy: false, canSubmit: true });
}
},
});
});
});
}
protected cancel(): void {
@ -199,87 +190,75 @@ export default class PollCreateDialog extends ScrollableBaseModal<IProps, IState
}
protected renderContent(): React.ReactNode {
return <div className="mx_PollCreateDialog">
<h2>{ _t("Poll type") }</h2>
<Field
element="select"
value={this.state.kind.name}
onChange={this.onPollTypeChange}
>
<option
key={M_POLL_KIND_DISCLOSED.name}
value={M_POLL_KIND_DISCLOSED.name}
return (
<div className="mx_PollCreateDialog">
<h2>{_t("Poll type")}</h2>
<Field element="select" value={this.state.kind.name} onChange={this.onPollTypeChange}>
<option key={M_POLL_KIND_DISCLOSED.name} value={M_POLL_KIND_DISCLOSED.name}>
{_t("Open poll")}
</option>
<option key={M_POLL_KIND_UNDISCLOSED.name} value={M_POLL_KIND_UNDISCLOSED.name}>
{_t("Closed poll")}
</option>
</Field>
<p>{pollTypeNotes(this.state.kind)}</p>
<h2>{_t("What is your poll question or topic?")}</h2>
<Field
id="poll-topic-input"
value={this.state.question}
maxLength={MAX_QUESTION_LENGTH}
label={_t("Question or topic")}
placeholder={_t("Write something...")}
onChange={this.onQuestionChange}
usePlaceholderAsHint={true}
disabled={this.state.busy}
autoFocus={this.state.autoFocusTarget === FocusTarget.Topic}
/>
<h2>{_t("Create options")}</h2>
{this.state.options.map((op, i) => (
<div key={`option_${i}`} className="mx_PollCreateDialog_option">
<Field
id={`pollcreate_option_${i}`}
value={op}
maxLength={MAX_OPTION_LENGTH}
label={_t("Option %(number)s", { number: i + 1 })}
placeholder={_t("Write an option")}
onChange={(e: ChangeEvent<HTMLInputElement>) => this.onOptionChange(i, e)}
usePlaceholderAsHint={true}
disabled={this.state.busy}
autoFocus={
this.state.autoFocusTarget === FocusTarget.NewOption &&
i === this.state.options.length - 1
}
/>
<AccessibleButton
onClick={() => this.onOptionRemove(i)}
className="mx_PollCreateDialog_removeOption"
disabled={this.state.busy}
/>
</div>
))}
<AccessibleButton
onClick={this.onOptionAdd}
disabled={this.state.busy || this.state.options.length >= MAX_OPTIONS}
kind="secondary"
className="mx_PollCreateDialog_addOption"
inputRef={this.addOptionRef}
>
{ _t("Open poll") }
</option>
<option
key={M_POLL_KIND_UNDISCLOSED.name}
value={M_POLL_KIND_UNDISCLOSED.name}
>
{ _t("Closed poll") }
</option>
</Field>
<p>{ pollTypeNotes(this.state.kind) }</p>
<h2>{ _t("What is your poll question or topic?") }</h2>
<Field
id='poll-topic-input'
value={this.state.question}
maxLength={MAX_QUESTION_LENGTH}
label={_t("Question or topic")}
placeholder={_t("Write something...")}
onChange={this.onQuestionChange}
usePlaceholderAsHint={true}
disabled={this.state.busy}
autoFocus={this.state.autoFocusTarget === FocusTarget.Topic}
/>
<h2>{ _t("Create options") }</h2>
{
this.state.options.map((op, i) => <div key={`option_${i}`} className="mx_PollCreateDialog_option">
<Field
id={`pollcreate_option_${i}`}
value={op}
maxLength={MAX_OPTION_LENGTH}
label={_t("Option %(number)s", { number: i + 1 })}
placeholder={_t("Write an option")}
onChange={
(e: ChangeEvent<HTMLInputElement>) =>
this.onOptionChange(i, e)
}
usePlaceholderAsHint={true}
disabled={this.state.busy}
autoFocus={
this.state.autoFocusTarget === FocusTarget.NewOption &&
i === this.state.options.length - 1
}
/>
<AccessibleButton
onClick={() => this.onOptionRemove(i)}
className="mx_PollCreateDialog_removeOption"
disabled={this.state.busy}
/>
</div>)
}
<AccessibleButton
onClick={this.onOptionAdd}
disabled={this.state.busy || this.state.options.length >= MAX_OPTIONS}
kind="secondary"
className="mx_PollCreateDialog_addOption"
inputRef={this.addOptionRef}
>{ _t("Add option") }</AccessibleButton>
{
this.state.busy &&
<div className="mx_PollCreateDialog_busy"><Spinner /></div>
}
</div>;
{_t("Add option")}
</AccessibleButton>
{this.state.busy && (
<div className="mx_PollCreateDialog_busy">
<Spinner />
</div>
)}
</div>
);
}
onPollTypeChange = (e: ChangeEvent<HTMLSelectElement>) => {
this.setState({
kind: (
M_POLL_KIND_DISCLOSED.matches(e.target.value)
? M_POLL_KIND_DISCLOSED
: M_POLL_KIND_UNDISCLOSED
),
kind: M_POLL_KIND_DISCLOSED.matches(e.target.value) ? M_POLL_KIND_DISCLOSED : M_POLL_KIND_UNDISCLOSED,
});
};
}

View file

@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import * as Roles from '../../../Roles';
import { _t } from '../../../languageHandler';
import * as Roles from "../../../Roles";
import { _t } from "../../../languageHandler";
import Field from "./Field";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
@ -86,13 +86,13 @@ export default class PowerSelector extends React.Component<IProps, IState> {
private initStateFromProps(): void {
// This needs to be done now because levelRoleMap has translated strings
const levelRoleMap = Roles.levelRoleMap(this.props.usersDefault);
const options = Object.keys(levelRoleMap).filter(level => {
return (
level === undefined ||
parseInt(level) <= this.props.maxValue ||
parseInt(level) == this.props.value
);
}).map(level => parseInt(level));
const options = Object.keys(levelRoleMap)
.filter((level) => {
return (
level === undefined || parseInt(level) <= this.props.maxValue || parseInt(level) == this.props.value
);
})
.map((level) => parseInt(level));
const isCustom = levelRoleMap[this.props.value] === undefined;
@ -175,12 +175,8 @@ export default class PowerSelector extends React.Component<IProps, IState> {
options.push({ value: CUSTOM_VALUE, text: _t("Custom level") });
const optionsElements = options.map((op) => {
return (
<option
value={op.value}
key={op.value}
data-testid={`power-level-option-${op.value}`}
>
{ op.text }
<option value={op.value} key={op.value} data-testid={`power-level-option-${op.value}`}>
{op.text}
</option>
);
});
@ -192,17 +188,13 @@ export default class PowerSelector extends React.Component<IProps, IState> {
onChange={this.onSelectChange}
value={String(this.state.selectValue)}
disabled={this.props.disabled}
data-testid='power-level-select-element'
data-testid="power-level-select-element"
>
{ optionsElements }
{optionsElements}
</Field>
);
}
return (
<div className="mx_PowerSelector">
{ picker }
</div>
);
return <div className="mx_PowerSelector">{picker}</div>;
}
}

View file

@ -27,14 +27,14 @@ interface IProps extends QRCodeRenderersOptions {
}
const defaultOptions: QRCodeToDataURLOptions = {
errorCorrectionLevel: 'L', // we want it as trivial-looking as possible
errorCorrectionLevel: "L", // we want it as trivial-looking as possible
};
const QRCode: React.FC<IProps> = ({ data, className, ...options }) => {
const [dataUri, setUri] = React.useState<string>(null);
React.useEffect(() => {
let cancelled = false;
toDataURL(data, { ...defaultOptions, ...options }).then(uri => {
toDataURL(data, { ...defaultOptions, ...options }).then((uri) => {
if (cancelled) return;
setUri(uri);
});
@ -43,9 +43,11 @@ const QRCode: React.FC<IProps> = ({ data, className, ...options }) => {
};
}, [JSON.stringify(data), options]); // eslint-disable-line react-hooks/exhaustive-deps
return <div className={classNames("mx_QRCode", className)}>
{ dataUri ? <img src={dataUri} className="mx_VerificationQRCode" alt={_t("QR Code")} /> : <Spinner /> }
</div>;
return (
<div className={classNames("mx_QRCode", className)}>
{dataUri ? <img src={dataUri} className="mx_VerificationQRCode" alt={_t("QR Code")} /> : <Spinner />}
</div>
);
};
export default QRCode;

View file

@ -15,26 +15,26 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import classNames from 'classnames';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { Room } from 'matrix-js-sdk/src/models/room';
import { MatrixClient } from 'matrix-js-sdk/src/client';
import React from "react";
import classNames from "classnames";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher/dispatcher';
import { _t } from "../../../languageHandler";
import dis from "../../../dispatcher/dispatcher";
import { makeUserPermalink, RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import SettingsStore from "../../../settings/SettingsStore";
import { Layout } from "../../../settings/enums/Layout";
import { getUserNameColorClass } from "../../../utils/FormattingUtils";
import { Action } from "../../../dispatcher/actions";
import Spinner from './Spinner';
import Spinner from "./Spinner";
import ReplyTile from "../rooms/ReplyTile";
import Pill, { PillType } from './Pill';
import AccessibleButton, { ButtonEvent } from './AccessibleButton';
import { getParentEventId, shouldDisplayReply } from '../../../utils/Reply';
import Pill, { PillType } from "./Pill";
import AccessibleButton, { ButtonEvent } from "./AccessibleButton";
import { getParentEventId, shouldDisplayReply } from "../../../utils/Reply";
import RoomContext from "../../../contexts/RoomContext";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { GetRelationsForEvent } from "../rooms/EventTile";
/**
@ -114,9 +114,9 @@ export default class ReplyChain extends React.Component<IProps, IState> {
private trySetExpandableQuotes() {
if (this.props.isQuoteExpanded === undefined && this.blockquoteRef.current) {
const el: HTMLElement | null = this.blockquoteRef.current.querySelector('.mx_EventTile_body');
const el: HTMLElement | null = this.blockquoteRef.current.querySelector(".mx_EventTile_body");
if (el) {
const code: HTMLElement | null = el.querySelector('code');
const code: HTMLElement | null = el.querySelector("code");
const isCodeEllipsisShown = code ? code.offsetHeight >= SHOW_EXPAND_QUOTE_PIXELS : false;
const isElipsisShown = el.offsetHeight >= SHOW_EXPAND_QUOTE_PIXELS || isCodeEllipsisShown;
if (isElipsisShown) {
@ -202,49 +202,62 @@ export default class ReplyChain extends React.Component<IProps, IState> {
render() {
let header = null;
if (this.state.err) {
header = <blockquote className="mx_ReplyChain mx_ReplyChain_error">
{
_t('Unable to load event that was replied to, ' +
'it either does not exist or you do not have permission to view it.')
}
</blockquote>;
header = (
<blockquote className="mx_ReplyChain mx_ReplyChain_error">
{_t(
"Unable to load event that was replied to, " +
"it either does not exist or you do not have permission to view it.",
)}
</blockquote>
);
} else if (this.state.loadedEv && shouldDisplayReply(this.state.events[0])) {
const ev = this.state.loadedEv;
const room = this.matrixClient.getRoom(ev.getRoomId());
header = <blockquote className={`mx_ReplyChain ${this.getReplyChainColorClass(ev)}`}>
{
_t('<a>In reply to</a> <pill>', {}, {
'a': (sub) => (
<AccessibleButton
kind="link_inline"
className="mx_ReplyChain_show"
onClick={this.onQuoteClick}
>
{ sub }
</AccessibleButton>
),
'pill': (
<Pill
type={PillType.UserMention}
room={room}
url={makeUserPermalink(ev.getSender())}
shouldShowPillAvatar={SettingsStore.getValue("Pill.shouldShowPillAvatar")}
/>
),
})
}
</blockquote>;
header = (
<blockquote className={`mx_ReplyChain ${this.getReplyChainColorClass(ev)}`}>
{_t(
"<a>In reply to</a> <pill>",
{},
{
a: (sub) => (
<AccessibleButton
kind="link_inline"
className="mx_ReplyChain_show"
onClick={this.onQuoteClick}
>
{sub}
</AccessibleButton>
),
pill: (
<Pill
type={PillType.UserMention}
room={room}
url={makeUserPermalink(ev.getSender())}
shouldShowPillAvatar={SettingsStore.getValue("Pill.shouldShowPillAvatar")}
/>
),
},
)}
</blockquote>
);
} else if (this.props.forExport) {
const eventId = getParentEventId(this.props.parentEv);
header = <p className="mx_ReplyChain_Export">
{ _t("In reply to <a>this message</a>",
{},
{ a: (sub) => (
<a className="mx_reply_anchor" href={`#${eventId}`} data-scroll-to={eventId}> { sub } </a>
),
})
}
</p>;
header = (
<p className="mx_ReplyChain_Export">
{_t(
"In reply to <a>this message</a>",
{},
{
a: (sub) => (
<a className="mx_reply_anchor" href={`#${eventId}`} data-scroll-to={eventId}>
{" "}
{sub}{" "}
</a>
),
},
)}
</p>
);
} else if (this.state.loading) {
header = <Spinner w={16} h={16} />;
}
@ -252,12 +265,12 @@ export default class ReplyChain extends React.Component<IProps, IState> {
const { isQuoteExpanded } = this.props;
const evTiles = this.state.events.map((ev) => {
const classname = classNames({
'mx_ReplyChain': true,
"mx_ReplyChain": true,
[this.getReplyChainColorClass(ev)]: true,
// We don't want to add the class if it's undefined, it should only be expanded/collapsed when it's true/false
'mx_ReplyChain--expanded': isQuoteExpanded === true,
"mx_ReplyChain--expanded": isQuoteExpanded === true,
// We don't want to add the class if it's undefined, it should only be expanded/collapsed when it's true/false
'mx_ReplyChain--collapsed': isQuoteExpanded === false,
"mx_ReplyChain--collapsed": isQuoteExpanded === false,
});
return (
<blockquote ref={this.blockquoteRef} className={classname} key={ev.getId()}>
@ -272,9 +285,11 @@ export default class ReplyChain extends React.Component<IProps, IState> {
);
});
return <div className="mx_ReplyChain_wrapper">
<div>{ header }</div>
<div>{ evTiles }</div>
</div>;
return (
<div className="mx_ReplyChain_wrapper">
<div>{header}</div>
<div>{evTiles}</div>
</div>
);
}
}

View file

@ -1,4 +1,3 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
@ -15,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react'; // eslint-disable-line no-unused-vars
import React from "react"; // eslint-disable-line no-unused-vars
//see src/resizer for the actual resizing code, this is just the DOM for the resize handle
interface IResizeHandleProps {
@ -26,17 +25,19 @@ interface IResizeHandleProps {
}
const ResizeHandle: React.FC<IResizeHandleProps> = ({ vertical, reverse, id, passRef }) => {
const classNames = ['mx_ResizeHandle'];
const classNames = ["mx_ResizeHandle"];
if (vertical) {
classNames.push('mx_ResizeHandle_vertical');
classNames.push("mx_ResizeHandle_vertical");
} else {
classNames.push('mx_ResizeHandle_horizontal');
classNames.push("mx_ResizeHandle_horizontal");
}
if (reverse) {
classNames.push('mx_ResizeHandle_reverse');
classNames.push("mx_ResizeHandle_reverse");
}
return (
<div ref={passRef} className={classNames.join(' ')} data-id={id}><div /></div>
<div ref={passRef} className={classNames.join(" ")} data-id={id}>
<div />
</div>
);
};

View file

@ -16,8 +16,8 @@ limitations under the License.
import React, { createRef, KeyboardEventHandler } from "react";
import { _t } from '../../../languageHandler';
import withValidation from './Validation';
import { _t } from "../../../languageHandler";
import withValidation from "./Validation";
import Field, { IValidateOpts } from "./Field";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
@ -53,7 +53,7 @@ export default class RoomAliasField extends React.PureComponent<IProps, IState>
}
private asFullAlias(localpart: string): string {
const hashAlias = `#${ localpart }`;
const hashAlias = `#${localpart}`;
if (this.props.domain) {
return `${hashAlias}:${this.props.domain}`;
}
@ -63,11 +63,11 @@ export default class RoomAliasField extends React.PureComponent<IProps, IState>
private get domainProps() {
const { domain } = this.props;
const prefix = <span>#</span>;
const postfix = domain ? (<span title={`:${domain}`}>{ `:${domain}` }</span>) : <span />;
const maxlength = domain ? 255 - domain.length - 2 : 255 - 1; // 2 for # and :
const value = domain ?
this.props.value.substring(1, this.props.value.length - this.props.domain.length - 1) :
this.props.value.substring(1);
const postfix = domain ? <span title={`:${domain}`}>{`:${domain}`}</span> : <span />;
const maxlength = domain ? 255 - domain.length - 2 : 255 - 1; // 2 for # and :
const value = domain
? this.props.value.substring(1, this.props.value.length - this.props.domain.length - 1)
: this.props.value.substring(1);
return { prefix, postfix, value, maxlength };
}
@ -107,14 +107,15 @@ export default class RoomAliasField extends React.PureComponent<IProps, IState>
private validationRules = withValidation({
rules: [
{ key: "hasDomain",
{
key: "hasDomain",
test: async ({ value }) => {
// Ignore if we have passed domain
if (!value || this.props.domain) {
return true;
}
if (value.split(':').length < 2) {
if (value.split(":").length < 2) {
return false;
}
return true;
@ -128,7 +129,7 @@ export default class RoomAliasField extends React.PureComponent<IProps, IState>
return true;
}
const split = value.split(':');
const split = value.split(":");
if (split.length < 2) {
return true; // hasDomain check will fail here instead
}
@ -154,59 +155,66 @@ export default class RoomAliasField extends React.PureComponent<IProps, IState>
const hasColon = this.props.domain ? !value.includes(":") : true;
// XXX: FIXME https://github.com/matrix-org/matrix-doc/issues/668
// NOTE: We could probably use linkifyjs to parse those aliases here?
return !value.includes("#") &&
return (
!value.includes("#") &&
hasColon &&
!value.includes(",") &&
encodeURI(fullAlias) === fullAlias;
encodeURI(fullAlias) === fullAlias
);
}
},
invalid: () => _t("Some characters not allowed"),
}, {
},
{
key: "required",
test: async ({ value, allowEmpty }) => allowEmpty || !!value,
invalid: () => _t("Please provide an address"),
}, this.props.roomId ? {
key: "matches",
final: true,
test: async ({ value }) => {
if (!value) {
return true;
}
const client = this.context;
try {
const result = await client.getRoomIdForAlias(this.asFullAlias(value));
return result.room_id === this.props.roomId;
} catch (err) {
console.log(err);
return false;
}
},
invalid: () => _t("This address does not point at this room"),
} : {
key: "taken",
final: true,
test: async ({ value }) => {
if (!value) {
return true;
}
const client = this.context;
try {
await client.getRoomIdForAlias(this.asFullAlias(value));
// we got a room id, so the alias is taken
return false;
} catch (err) {
console.log(err);
// any server error code will do,
// either it M_NOT_FOUND or the alias is invalid somehow,
// in which case we don't want to show the invalid message
return !!err.errcode;
}
},
valid: () => _t("This address is available to use"),
invalid: () => this.props.domain ?
_t("This address is already in use") :
_t("This address had invalid server or is already in use"),
},
this.props.roomId
? {
key: "matches",
final: true,
test: async ({ value }) => {
if (!value) {
return true;
}
const client = this.context;
try {
const result = await client.getRoomIdForAlias(this.asFullAlias(value));
return result.room_id === this.props.roomId;
} catch (err) {
console.log(err);
return false;
}
},
invalid: () => _t("This address does not point at this room"),
}
: {
key: "taken",
final: true,
test: async ({ value }) => {
if (!value) {
return true;
}
const client = this.context;
try {
await client.getRoomIdForAlias(this.asFullAlias(value));
// we got a room id, so the alias is taken
return false;
} catch (err) {
console.log(err);
// any server error code will do,
// either it M_NOT_FOUND or the alias is invalid somehow,
// in which case we don't want to show the invalid message
return !!err.errcode;
}
},
valid: () => _t("This address is available to use"),
invalid: () =>
this.props.domain
? _t("This address is already in use")
: _t("This address had invalid server or is already in use"),
},
],
});

View file

@ -35,55 +35,63 @@ interface IProps extends HTMLAttributes<HTMLSpanElement> {
numShown?: number;
}
const RoomFacePile: FC<IProps> = (
{ room, onlyKnownUsers = true, numShown = DEFAULT_NUM_FACES, ...props },
) => {
const RoomFacePile: FC<IProps> = ({ room, onlyKnownUsers = true, numShown = DEFAULT_NUM_FACES, ...props }) => {
const cli = useContext(MatrixClientContext);
const isJoined = room.getMyMembership() === "join";
let members = useRoomMembers(room);
const count = members.length;
// sort users with an explicit avatar first
const iteratees = [member => member.getMxcAvatarUrl() ? 0 : 1];
const iteratees = [(member) => (member.getMxcAvatarUrl() ? 0 : 1)];
if (onlyKnownUsers) {
members = members.filter(isKnownMember);
} else {
// sort known users first
iteratees.unshift(member => isKnownMember(member) ? 0 : 1);
iteratees.unshift((member) => (isKnownMember(member) ? 0 : 1));
}
// exclude ourselves from the shown members list
const shownMembers = sortBy(members.filter(m => m.userId !== cli.getUserId()), iteratees).slice(0, numShown);
const shownMembers = sortBy(
members.filter((m) => m.userId !== cli.getUserId()),
iteratees,
).slice(0, numShown);
if (shownMembers.length < 1) return null;
// We reverse the order of the shown faces in CSS to simplify their visual overlap,
// reverse members in tooltip order to make the order between the two match up.
const commaSeparatedMembers = shownMembers.map(m => m.name).reverse().join(", ");
const commaSeparatedMembers = shownMembers
.map((m) => m.name)
.reverse()
.join(", ");
const tooltip = <div>
<div className="mx_Tooltip_title">
{ props.onClick
? _t("View all %(count)s members", { count })
: _t("%(count)s members", { count }) }
const tooltip = (
<div>
<div className="mx_Tooltip_title">
{props.onClick ? _t("View all %(count)s members", { count }) : _t("%(count)s members", { count })}
</div>
<div className="mx_Tooltip_sub">
{isJoined
? _t("Including you, %(commaSeparatedMembers)s", { commaSeparatedMembers })
: _t("Including %(commaSeparatedMembers)s", { commaSeparatedMembers })}
</div>
</div>
<div className="mx_Tooltip_sub">
{ isJoined
? _t("Including you, %(commaSeparatedMembers)s", { commaSeparatedMembers })
: _t("Including %(commaSeparatedMembers)s", { commaSeparatedMembers }) }
</div>
</div>;
);
return <FacePile
members={shownMembers}
faceSize={28}
overflow={members.length > numShown}
tooltip={tooltip}
{...props}
>
{ onlyKnownUsers && <span className="mx_FacePile_summary">
{ _t("%(count)s people you know have already joined", { count: members.length }) }
</span> }
</FacePile>;
return (
<FacePile
members={shownMembers}
faceSize={28}
overflow={members.length > numShown}
tooltip={tooltip}
{...props}
>
{onlyKnownUsers && (
<span className="mx_FacePile_summary">
{_t("%(count)s people you know have already joined", { count: members.length })}
</span>
)}
</FacePile>
);
};
export default RoomFacePile;

View file

@ -34,7 +34,7 @@ const RoomName = ({ room, children }: IProps): JSX.Element => {
}, [room]);
if (children) return children(name);
return <>{ name || "" }</>;
return <>{name || ""}</>;
};
export default RoomName;

View file

@ -37,25 +37,25 @@ interface IProps extends React.HTMLProps<HTMLDivElement> {
room?: Room;
}
export default function RoomTopic({
room,
...props
}: IProps) {
export default function RoomTopic({ room, ...props }: IProps) {
const client = useContext(MatrixClientContext);
const ref = useRef<HTMLDivElement>();
const topic = useTopic(room);
const body = topicToHtml(topic?.text, topic?.html, ref);
const onClick = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
props.onClick?.(e);
const target = e.target as HTMLElement;
if (target.tagName.toUpperCase() === "A") {
return;
}
const onClick = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
props.onClick?.(e);
const target = e.target as HTMLElement;
if (target.tagName.toUpperCase() === "A") {
return;
}
dis.fire(Action.ShowRoomTopic);
}, [props]);
dis.fire(Action.ShowRoomTopic);
},
[props],
);
const ignoreHover = (ev: React.MouseEvent): boolean => {
return (ev.target as HTMLElement).tagName.toUpperCase() === "A";
@ -68,26 +68,31 @@ export default function RoomTopic({
const modal = Modal.createDialog(InfoDialog, {
title: room.name,
description: <div>
<Linkify
as="p"
onClick={(ev: MouseEvent) => {
if ((ev.target as HTMLElement).tagName.toUpperCase() === "A") {
modal.close();
}
}}
>
{ body }
</Linkify>
{ canSetTopic && <AccessibleButton
kind="primary_outline"
onClick={() => {
modal.close();
dis.dispatch({ action: "open_room_settings" });
}}>
{ _t("Edit topic") }
</AccessibleButton> }
</div>,
description: (
<div>
<Linkify
as="p"
onClick={(ev: MouseEvent) => {
if ((ev.target as HTMLElement).tagName.toUpperCase() === "A") {
modal.close();
}
}}
>
{body}
</Linkify>
{canSetTopic && (
<AccessibleButton
kind="primary_outline"
onClick={() => {
modal.close();
dis.dispatch({ action: "open_room_settings" });
}}
>
{_t("Edit topic")}
</AccessibleButton>
)}
</div>
),
hasCloseButton: true,
button: false,
});
@ -96,16 +101,11 @@ export default function RoomTopic({
const className = classNames(props.className, "mx_RoomTopic");
return <div {...props}
ref={ref}
onClick={onClick}
dir="auto"
className={className}
>
<TooltipTarget label={_t("Click to read topic")} alignment={Alignment.Bottom} ignoreHover={ignoreHover}>
<Linkify>
{ body }
</Linkify>
</TooltipTarget>
</div>;
return (
<div {...props} ref={ref} onClick={onClick} dir="auto" className={className}>
<TooltipTarget label={_t("Click to read topic")} alignment={Alignment.Bottom} ignoreHover={ignoreHover}>
<Linkify>{body}</Linkify>
</TooltipTarget>
</div>
);
}

View file

@ -112,15 +112,15 @@ const SSOButton: React.FC<ISSOButtonProps> = ({
// TODO fallback icon
return (
<AccessibleTooltipButton {...props} title={label} className={classes} onClick={onClick}>
{ icon }
{icon}
</AccessibleTooltipButton>
);
}
return (
<AccessibleButton {...props} className={classes} onClick={onClick}>
{ icon }
{ label }
{icon}
{label}
</AccessibleButton>
);
};
@ -138,37 +138,41 @@ const MAX_PER_ROW = 6;
const SSOButtons: React.FC<IProps> = ({ matrixClient, flow, loginType, fragmentAfterLogin, primary }) => {
const providers = flow.identity_providers || [];
if (providers.length < 2) {
return <div className="mx_SSOButtons">
<SSOButton
matrixClient={matrixClient}
loginType={loginType}
fragmentAfterLogin={fragmentAfterLogin}
idp={providers[0]}
primary={primary}
/>
</div>;
return (
<div className="mx_SSOButtons">
<SSOButton
matrixClient={matrixClient}
loginType={loginType}
fragmentAfterLogin={fragmentAfterLogin}
idp={providers[0]}
primary={primary}
/>
</div>
);
}
const rows = Math.ceil(providers.length / MAX_PER_ROW);
const size = Math.ceil(providers.length / rows);
return <div className="mx_SSOButtons">
{ chunk(providers, size).map(chunk => (
<div key={chunk[0].id} className="mx_SSOButtons_row">
{ chunk.map(idp => (
<SSOButton
key={idp.id}
matrixClient={matrixClient}
loginType={loginType}
fragmentAfterLogin={fragmentAfterLogin}
idp={idp}
mini={true}
primary={primary}
/>
)) }
</div>
)) }
</div>;
return (
<div className="mx_SSOButtons">
{chunk(providers, size).map((chunk) => (
<div key={chunk[0].id} className="mx_SSOButtons_row">
{chunk.map((idp) => (
<SSOButton
key={idp.id}
matrixClient={matrixClient}
loginType={loginType}
fragmentAfterLogin={fragmentAfterLogin}
idp={idp}
mini={true}
primary={primary}
/>
))}
</div>
))}
</div>
);
};
export default SSOButtons;

View file

@ -42,21 +42,26 @@ export default function SearchWarning({ isRoomEncrypted, kind }: IProps) {
if (EventIndexPeg.error) {
return (
<div className="mx_SearchWarning">
{ _t("Message search initialisation failed, check <a>your settings</a> for more information", {}, {
a: sub => (
<AccessibleButton
kind="link_inline"
onClick={(evt) => {
evt.preventDefault();
dis.dispatch({
action: Action.ViewUserSettings,
initialTabId: UserTab.Security,
});
}}
>
{ sub }
</AccessibleButton>),
}) }
{_t(
"Message search initialisation failed, check <a>your settings</a> for more information",
{},
{
a: (sub) => (
<AccessibleButton
kind="link_inline"
onClick={(evt) => {
evt.preventDefault();
dis.dispatch({
action: Action.ViewUserSettings,
initialTabId: UserTab.Security,
});
}}
>
{sub}
</AccessibleButton>
),
},
)}
</div>
);
}
@ -71,14 +76,30 @@ export default function SearchWarning({ isRoomEncrypted, kind }: IProps) {
const buildUrl = desktopBuilds.get("url");
switch (kind) {
case WarningKind.Files:
text = _t("Use the <a>Desktop app</a> to see all encrypted files", {}, {
a: sub => (<a href={buildUrl} target="_blank" rel="noreferrer noopener">{ sub }</a>),
});
text = _t(
"Use the <a>Desktop app</a> to see all encrypted files",
{},
{
a: (sub) => (
<a href={buildUrl} target="_blank" rel="noreferrer noopener">
{sub}
</a>
),
},
);
break;
case WarningKind.Search:
text = _t("Use the <a>Desktop app</a> to search encrypted messages", {}, {
a: sub => (<a href={buildUrl} target="_blank" rel="noreferrer noopener">{ sub }</a>),
});
text = _t(
"Use the <a>Desktop app</a> to search encrypted messages",
{},
{
a: (sub) => (
<a href={buildUrl} target="_blank" rel="noreferrer noopener">
{sub}
</a>
),
},
);
break;
}
} else {
@ -100,8 +121,8 @@ export default function SearchWarning({ isRoomEncrypted, kind }: IProps) {
return (
<div className="mx_SearchWarning">
{ logo }
<span>{ text }</span>
{logo}
<span>{text}</span>
</div>
);
}

View file

@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import AccessibleButton from "./AccessibleButton";
import { ValidatedServerConfig } from '../../../utils/ValidatedServerConfig';
import { ValidatedServerConfig } from "../../../utils/ValidatedServerConfig";
import { _t } from "../../../languageHandler";
import TextWithTooltip from "./TextWithTooltip";
import SdkConfig from "../../../SdkConfig";
@ -42,15 +42,22 @@ const showPickerDialog = (
const onHelpClick = () => {
const brand = SdkConfig.get().brand;
Modal.createDialog(InfoDialog, {
title: _t("Server Options"),
description: _t("You can use the custom server options to sign into other Matrix servers by specifying " +
"a different homeserver URL. This allows you to use %(brand)s with an existing Matrix account on " +
"a different homeserver.", { brand }),
button: _t("Dismiss"),
hasCloseButton: false,
fixedWidth: false,
}, "mx_ServerPicker_helpDialog");
Modal.createDialog(
InfoDialog,
{
title: _t("Server Options"),
description: _t(
"You can use the custom server options to sign into other Matrix servers by specifying " +
"a different homeserver URL. This allows you to use %(brand)s with an existing Matrix account on " +
"a different homeserver.",
{ brand },
),
button: _t("Dismiss"),
hasCloseButton: false,
fixedWidth: false,
},
"mx_ServerPicker_helpDialog",
);
};
const ServerPicker = ({ title, dialogTitle, serverConfig, onServerConfigChange }: IProps) => {
@ -65,39 +72,42 @@ const ServerPicker = ({ title, dialogTitle, serverConfig, onServerConfigChange }
}
});
};
editBtn = <AccessibleButton className="mx_ServerPicker_change" kind="link" onClick={onClick}>
{ _t("Edit") }
</AccessibleButton>;
editBtn = (
<AccessibleButton className="mx_ServerPicker_change" kind="link" onClick={onClick}>
{_t("Edit")}
</AccessibleButton>
);
}
let serverName: React.ReactNode = serverConfig.isNameResolvable ? serverConfig.hsName : serverConfig.hsUrl;
if (serverConfig.hsNameIsDifferent) {
serverName = <TextWithTooltip class="mx_Login_underlinedServerName" tooltip={serverConfig.hsUrl}>
{ serverConfig.hsName }
</TextWithTooltip>;
serverName = (
<TextWithTooltip class="mx_Login_underlinedServerName" tooltip={serverConfig.hsUrl}>
{serverConfig.hsName}
</TextWithTooltip>
);
}
let desc;
if (serverConfig.hsName === "matrix.org") {
desc = <span className="mx_ServerPicker_desc">
{ _t("Join millions for free on the largest public server") }
</span>;
desc = (
<span className="mx_ServerPicker_desc">{_t("Join millions for free on the largest public server")}</span>
);
}
return <div className="mx_ServerPicker">
<h2>{ title || _t("Homeserver") }</h2>
{ !disableCustomUrls ? (
<AccessibleButton
className="mx_ServerPicker_help"
onClick={onHelpClick}
aria-label={_t("Help")}
/>): null }
<span className="mx_ServerPicker_server" title={typeof serverName === "string" ? serverName : undefined}>
{ serverName }
</span>
{ editBtn }
{ desc }
</div>;
return (
<div className="mx_ServerPicker">
<h2>{title || _t("Homeserver")}</h2>
{!disableCustomUrls ? (
<AccessibleButton className="mx_ServerPicker_help" onClick={onHelpClick} aria-label={_t("Help")} />
) : null}
<span className="mx_ServerPicker_server" title={typeof serverName === "string" ? serverName : undefined}>
{serverName}
</span>
{editBtn}
{desc}
</div>
);
};
export default ServerPicker;

View file

@ -18,7 +18,7 @@ limitations under the License.
import React from "react";
import SettingsStore from "../../../settings/SettingsStore";
import { _t } from '../../../languageHandler';
import { _t } from "../../../languageHandler";
import ToggleSwitch from "./ToggleSwitch";
import StyledCheckbox from "./StyledCheckbox";
import { SettingLevel } from "../../../settings/SettingLevel";
@ -80,49 +80,50 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
if (!canChange && this.props.hideIfCannotSet) return null;
const label = (this.props.label
? _t(this.props.label)
: SettingsStore.getDisplayName(this.props.name, this.props.level)) ?? undefined;
const label =
(this.props.label
? _t(this.props.label)
: SettingsStore.getDisplayName(this.props.name, this.props.level)) ?? undefined;
const description = SettingsStore.getDescription(this.props.name);
const shouldWarn = SettingsStore.shouldHaveWarning(this.props.name);
let disabledDescription: JSX.Element | null = null;
if (this.props.disabled && this.props.disabledDescription) {
disabledDescription = <div className="mx_SettingsFlag_microcopy">
{ this.props.disabledDescription }
</div>;
disabledDescription = <div className="mx_SettingsFlag_microcopy">{this.props.disabledDescription}</div>;
}
if (this.props.useCheckbox) {
return <StyledCheckbox
checked={this.state.value}
onChange={this.checkBoxOnChange}
disabled={this.props.disabled || !canChange}
>
{ label }
</StyledCheckbox>;
return (
<StyledCheckbox
checked={this.state.value}
onChange={this.checkBoxOnChange}
disabled={this.props.disabled || !canChange}
>
{label}
</StyledCheckbox>
);
} else {
return (
<div className="mx_SettingsFlag">
<label className="mx_SettingsFlag_label">
<span className="mx_SettingsFlag_labelText">{ label }</span>
{ description && <div className="mx_SettingsFlag_microcopy">
{ shouldWarn
? _t(
"<w>WARNING:</w> <description/>", {},
{
"w": (sub) => (
<span className="mx_SettingsTab_microcopy_warning">
{ sub }
</span>
),
"description": description,
},
)
: description
}
</div> }
{ disabledDescription }
<span className="mx_SettingsFlag_labelText">{label}</span>
{description && (
<div className="mx_SettingsFlag_microcopy">
{shouldWarn
? _t(
"<w>WARNING:</w> <description/>",
{},
{
w: (sub) => (
<span className="mx_SettingsTab_microcopy_warning">{sub}</span>
),
description: description,
},
)
: description}
</div>
)}
{disabledDescription}
</label>
<ToggleSwitch
checked={this.state.value}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import * as React from 'react';
import * as React from "react";
interface IProps {
// A callback for the selected value
@ -46,7 +46,7 @@ export default class Slider extends React.Component<IProps> {
private offset(values: number[], value: number): number {
// the index of the first number greater than value.
const closest = values.reduce((prev, curr) => {
return (value > curr ? prev + 1 : prev);
return value > curr ? prev + 1 : prev;
}, 0);
// Off the left
@ -71,44 +71,48 @@ export default class Slider extends React.Component<IProps> {
}
render(): React.ReactNode {
const dots = this.props.values.map(v => <Dot
active={v <= this.props.value}
label={this.props.displayFunc(v)}
onClick={this.props.disabled ? () => {} : () => this.props.onSelectionChange(v)}
key={v}
disabled={this.props.disabled}
/>);
const dots = this.props.values.map((v) => (
<Dot
active={v <= this.props.value}
label={this.props.displayFunc(v)}
onClick={this.props.disabled ? () => {} : () => this.props.onSelectionChange(v)}
key={v}
disabled={this.props.disabled}
/>
));
let selection = null;
if (!this.props.disabled) {
const offset = this.offset(this.props.values, this.props.value);
selection = <div className="mx_Slider_selection">
<div className="mx_Slider_selectionDot" style={{ left: "calc(-1.195em + " + offset + "%)" }}>
<div className="mx_Slider_selectionText">{ this.props.value }</div>
selection = (
<div className="mx_Slider_selection">
<div className="mx_Slider_selectionDot" style={{ left: "calc(-1.195em + " + offset + "%)" }}>
<div className="mx_Slider_selectionText">{this.props.value}</div>
</div>
<hr style={{ width: offset + "%" }} />
</div>
<hr style={{ width: offset + "%" }} />
</div>;
);
}
return <div className="mx_Slider">
<div>
<div className="mx_Slider_bar">
<hr onClick={this.props.disabled ? () => {} : this.onClick.bind(this)} />
{ selection }
</div>
<div className="mx_Slider_dotContainer">
{ dots }
return (
<div className="mx_Slider">
<div>
<div className="mx_Slider_bar">
<hr onClick={this.props.disabled ? () => {} : this.onClick.bind(this)} />
{selection}
</div>
<div className="mx_Slider_dotContainer">{dots}</div>
</div>
</div>
</div>;
);
}
onClick(event: React.MouseEvent) {
const width = (event.target as HTMLElement).clientWidth;
// nativeEvent is safe to use because https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/offsetX
// is supported by all modern browsers
const relativeClick = (event.nativeEvent.offsetX / width);
const relativeClick = event.nativeEvent.offsetX / width;
const nearestValue = this.props.values[Math.round(relativeClick * (this.props.values.length - 1))];
this.props.onSelectionChange(nearestValue);
}
@ -135,13 +139,13 @@ class Dot extends React.PureComponent<IDotProps> {
className += " mx_Slider_dotActive";
}
return <span onClick={this.props.onClick} className="mx_Slider_dotValue">
<div className={className} />
<div className="mx_Slider_labelContainer">
<div className="mx_Slider_label">
{ this.props.label }
return (
<span onClick={this.props.onClick} className="mx_Slider_dotValue">
<div className={className} />
<div className="mx_Slider_labelContainer">
<div className="mx_Slider_label">{this.props.label}</div>
</div>
</div>
</span>;
</span>
);
}
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import Dropdown from "../../views/elements/Dropdown";
import PlatformPeg from "../../../PlatformPeg";
@ -39,14 +39,16 @@ interface SpellCheckLanguagesDropdownIState {
languages: any;
}
export default class SpellCheckLanguagesDropdown extends React.Component<SpellCheckLanguagesDropdownIProps,
SpellCheckLanguagesDropdownIState> {
export default class SpellCheckLanguagesDropdown extends React.Component<
SpellCheckLanguagesDropdownIProps,
SpellCheckLanguagesDropdownIState
> {
constructor(props) {
super(props);
this.onSearchChange = this.onSearchChange.bind(this);
this.state = {
searchQuery: '',
searchQuery: "",
languages: null,
};
}
@ -54,23 +56,25 @@ export default class SpellCheckLanguagesDropdown extends React.Component<SpellCh
componentDidMount() {
const plaf = PlatformPeg.get();
if (plaf) {
plaf.getAvailableSpellCheckLanguages().then((languages) => {
languages.sort(function(a, b) {
if (a < b) return -1;
if (a > b) return 1;
return 0;
});
const langs = [];
languages.forEach((language) => {
langs.push({
label: language,
value: language,
plaf.getAvailableSpellCheckLanguages()
.then((languages) => {
languages.sort(function (a, b) {
if (a < b) return -1;
if (a > b) return 1;
return 0;
});
const langs = [];
languages.forEach((language) => {
langs.push({
label: language,
value: language,
});
});
this.setState({ languages: langs });
})
.catch((e) => {
this.setState({ languages: ["en"] });
});
this.setState({ languages: langs });
}).catch((e) => {
this.setState({ languages: ['en'] });
});
}
}
@ -93,14 +97,12 @@ export default class SpellCheckLanguagesDropdown extends React.Component<SpellCh
}
const options = displayedLanguages.map((language) => {
return <div key={language.value}>
{ language.label }
</div>;
return <div key={language.value}>{language.label}</div>;
});
// default value here too, otherwise we need to handle null / undefined;
// values between mounting and the initial value propagating
let language = SettingsStore.getValue("language", null, /*excludeDefault:*/true);
let language = SettingsStore.getValue("language", null, /*excludeDefault:*/ true);
let value = null;
if (language) {
value = this.props.value || language;
@ -109,17 +111,19 @@ export default class SpellCheckLanguagesDropdown extends React.Component<SpellCh
value = this.props.value || language;
}
return <Dropdown
id="mx_LanguageDropdown"
className={this.props.className}
onOptionChange={this.props.onOptionChange}
onSearchChange={this.onSearchChange}
searchEnabled={true}
value={value}
label={_t("Language Dropdown")}
placeholder={_t("Choose a locale")}
>
{ options }
</Dropdown>;
return (
<Dropdown
id="mx_LanguageDropdown"
className={this.props.className}
onOptionChange={this.props.onOptionChange}
onSearchChange={this.onSearchChange}
searchEnabled={true}
value={value}
label={_t("Language Dropdown")}
placeholder={_t("Choose a locale")}
>
{options}
</Dropdown>
);
}
}

View file

@ -34,7 +34,11 @@ export default class Spinner extends React.PureComponent<IProps> {
const { w, h, message } = this.props;
return (
<div className="mx_Spinner">
{ message && <React.Fragment><div className="mx_Spinner_Msg">{ message }</div>&nbsp;</React.Fragment> }
{message && (
<React.Fragment>
<div className="mx_Spinner_Msg">{message}</div>&nbsp;
</React.Fragment>
)}
<div
className="mx_Spinner_icon"
style={{ width: w, height: h }}

View file

@ -14,7 +14,7 @@
limitations under the License.
*/
import React from 'react';
import React from "react";
interface IProps {
reason?: string;
@ -44,16 +44,22 @@ export default class Spoiler extends React.Component<IProps, IState> {
public render(): JSX.Element {
const reason = this.props.reason ? (
<span className="mx_EventTile_spoiler_reason">{ "(" + this.props.reason + ")" }</span>
<span className="mx_EventTile_spoiler_reason">{"(" + this.props.reason + ")"}</span>
) : null;
// react doesn't allow appending a DOM node as child.
// as such, we pass the this.props.contentHtml instead and then set the raw
// HTML content. This is secure as the contents have already been parsed previously
return (
<span className={"mx_EventTile_spoiler" + (this.state.visible ? " visible" : "")} onClick={this.toggleVisible}>
{ reason }
<span
className={"mx_EventTile_spoiler" + (this.state.visible ? " visible" : "")}
onClick={this.toggleVisible}
>
{reason}
&nbsp;
<span className="mx_EventTile_spoiler_content" dangerouslySetInnerHTML={{ __html: this.props.contentHtml }} />
<span
className="mx_EventTile_spoiler_content"
dangerouslySetInnerHTML={{ __html: this.props.contentHtml }}
/>
</span>
);
}

View file

@ -16,7 +16,7 @@ limitations under the License.
import React from "react";
import { randomString } from "matrix-js-sdk/src/randomstring";
import classnames from 'classnames';
import classnames from "classnames";
export enum CheckboxStyle {
Solid = "solid",
@ -29,8 +29,7 @@ interface IProps extends React.InputHTMLAttributes<HTMLInputElement> {
id?: string;
}
interface IState {
}
interface IState {}
export default class StyledCheckbox extends React.PureComponent<IProps, IState> {
private id: string;
@ -49,33 +48,27 @@ export default class StyledCheckbox extends React.PureComponent<IProps, IState>
/* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */
const { children, className, kind = CheckboxStyle.Solid, inputRef, ...otherProps } = this.props;
const newClassName = classnames(
"mx_Checkbox",
className,
{
"mx_Checkbox_hasKind": kind,
[`mx_Checkbox_kind_${kind}`]: kind,
},
);
return <span className={newClassName}>
<input
// Pass through the ref - used for keyboard shortcut access to some buttons
ref={inputRef}
id={this.id}
{...otherProps}
type="checkbox"
/>
<label htmlFor={this.id}>
{ /* Using the div to center the image */ }
<div className="mx_Checkbox_background">
<div className="mx_Checkbox_checkmark" />
</div>
{ !!this.props.children &&
<div>
{ this.props.children }
const newClassName = classnames("mx_Checkbox", className, {
mx_Checkbox_hasKind: kind,
[`mx_Checkbox_kind_${kind}`]: kind,
});
return (
<span className={newClassName}>
<input
// Pass through the ref - used for keyboard shortcut access to some buttons
ref={inputRef}
id={this.id}
{...otherProps}
type="checkbox"
/>
<label htmlFor={this.id}>
{/* Using the div to center the image */}
<div className="mx_Checkbox_background">
<div className="mx_Checkbox_checkmark" />
</div>
}
</label>
</span>;
{!!this.props.children && <div>{this.props.children}</div>}
</label>
</span>
);
}
}

View file

@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import classnames from 'classnames';
import React from "react";
import classnames from "classnames";
interface IProps extends React.InputHTMLAttributes<HTMLInputElement> {
inputRef?: React.RefObject<HTMLInputElement>;
@ -26,53 +26,55 @@ interface IProps extends React.InputHTMLAttributes<HTMLInputElement> {
childrenInLabel?: boolean;
}
interface IState {
}
interface IState {}
export default class StyledRadioButton extends React.PureComponent<IProps, IState> {
public static readonly defaultProps = {
className: '',
className: "",
childrenInLabel: true,
};
public render() {
const { children, className, disabled, outlined, childrenInLabel, inputRef, ...otherProps } = this.props;
const _className = classnames(
'mx_StyledRadioButton',
className,
{
"mx_StyledRadioButton_disabled": disabled,
"mx_StyledRadioButton_enabled": !disabled,
"mx_StyledRadioButton_checked": this.props.checked,
"mx_StyledRadioButton_outlined": outlined,
});
const _className = classnames("mx_StyledRadioButton", className, {
mx_StyledRadioButton_disabled: disabled,
mx_StyledRadioButton_enabled: !disabled,
mx_StyledRadioButton_checked: this.props.checked,
mx_StyledRadioButton_outlined: outlined,
});
const radioButton = <React.Fragment>
<input
// Pass through the ref - used for keyboard shortcut access to some buttons
ref={inputRef}
type='radio'
disabled={disabled}
{...otherProps}
/>
{ /* Used to render the radio button circle */ }
<div><div /></div>
</React.Fragment>;
const radioButton = (
<React.Fragment>
<input
// Pass through the ref - used for keyboard shortcut access to some buttons
ref={inputRef}
type="radio"
disabled={disabled}
{...otherProps}
/>
{/* Used to render the radio button circle */}
<div>
<div />
</div>
</React.Fragment>
);
if (childrenInLabel) {
return <label className={_className}>
{ radioButton }
<div className="mx_StyledRadioButton_content">{ children }</div>
<div className="mx_StyledRadioButton_spacer" />
</label>;
} else {
return <div className={_className}>
<label className="mx_StyledRadioButton_innerLabel">
{ radioButton }
return (
<label className={_className}>
{radioButton}
<div className="mx_StyledRadioButton_content">{children}</div>
<div className="mx_StyledRadioButton_spacer" />
</label>
<div className="mx_StyledRadioButton_content">{ children }</div>
<div className="mx_StyledRadioButton_spacer" />
</div>;
);
} else {
return (
<div className={_className}>
<label className="mx_StyledRadioButton_innerLabel">{radioButton}</label>
<div className="mx_StyledRadioButton_content">{children}</div>
<div className="mx_StyledRadioButton_spacer" />
</div>
);
}
}
}

View file

@ -47,31 +47,35 @@ function StyledRadioGroup<T extends string>({
disabled,
onChange,
}: IProps<T>) {
const _onChange = e => {
const _onChange = (e) => {
onChange(e.target.value);
};
return <React.Fragment>
{ definitions.map(d => {
const id = `${name}-${d.value}`;
return (<React.Fragment key={d.value}>
<StyledRadioButton
id={id}
className={classNames(className, d.className)}
onChange={_onChange}
checked={d.checked !== undefined ? d.checked : d.value === value}
name={name}
value={d.value}
disabled={d.disabled ?? disabled}
outlined={outlined}
aria-describedby={d.description ? `${id}-description` : undefined}
>
{ d.label }
</StyledRadioButton>
{ d.description ? <span id={`${id}-description`}>{ d.description }</span> : null }
</React.Fragment>);
}) }
</React.Fragment>;
return (
<React.Fragment>
{definitions.map((d) => {
const id = `${name}-${d.value}`;
return (
<React.Fragment key={d.value}>
<StyledRadioButton
id={id}
className={classNames(className, d.className)}
onChange={_onChange}
checked={d.checked !== undefined ? d.checked : d.value === value}
name={name}
value={d.value}
disabled={d.disabled ?? disabled}
outlined={outlined}
aria-describedby={d.description ? `${id}-description` : undefined}
>
{d.label}
</StyledRadioButton>
{d.description ? <span id={`${id}-description`}>{d.description}</span> : null}
</React.Fragment>
);
})}
</React.Fragment>
);
}
export default StyledRadioGroup;

View file

@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import hljs from 'highlight.js';
import React from "react";
import hljs from "highlight.js";
interface IProps {
language?: string;
@ -34,4 +34,3 @@ export default class SyntaxHighlight extends React.PureComponent<IProps> {
);
}
}

View file

@ -26,19 +26,16 @@ interface IProps {
disabled?: boolean;
}
export const Tag = ({
icon,
label,
onDeleteClick,
disabled = false,
}: IProps) => {
return <div className='mx_Tag'>
{ icon?.() }
{ label }
{ onDeleteClick && (
<AccessibleButton className="mx_Tag_delete" onClick={onDeleteClick} disabled={disabled}>
<CancelRounded />
</AccessibleButton>
) }
</div>;
export const Tag = ({ icon, label, onDeleteClick, disabled = false }: IProps) => {
return (
<div className="mx_Tag">
{icon?.()}
{label}
{onDeleteClick && (
<AccessibleButton className="mx_Tag_delete" onClick={onDeleteClick} disabled={disabled}>
<CancelRounded />
</AccessibleButton>
)}
</div>
);
};

View file

@ -66,29 +66,32 @@ export default class TagComposer extends React.PureComponent<IProps, IState> {
}
public render() {
return <div className='mx_TagComposer'>
<form className='mx_TagComposer_input' onSubmit={this.onAdd}>
<Field
value={this.state.newTag}
onChange={this.onInputChange}
label={this.props.label || _t("Keyword")}
placeholder={this.props.placeholder || _t("New keyword")}
disabled={this.props.disabled}
autoComplete="off"
/>
<AccessibleButton onClick={this.onAdd} kind='primary' disabled={this.props.disabled}>
{ _t("Add") }
</AccessibleButton>
</form>
<div className='mx_TagComposer_tags'>
{ this.props.tags.map((t, i) => (
<Tag
label={t}
key={t}
onDeleteClick={this.onRemove.bind(this, t)}
disabled={this.props.disabled} />
)) }
return (
<div className="mx_TagComposer">
<form className="mx_TagComposer_input" onSubmit={this.onAdd}>
<Field
value={this.state.newTag}
onChange={this.onInputChange}
label={this.props.label || _t("Keyword")}
placeholder={this.props.placeholder || _t("New keyword")}
disabled={this.props.disabled}
autoComplete="off"
/>
<AccessibleButton onClick={this.onAdd} kind="primary" disabled={this.props.disabled}>
{_t("Add")}
</AccessibleButton>
</form>
<div className="mx_TagComposer_tags">
{this.props.tags.map((t, i) => (
<Tag
label={t}
key={t}
onDeleteClick={this.onRemove.bind(this, t)}
disabled={this.props.disabled}
/>
))}
</div>
</div>
</div>;
);
}
}

View file

@ -14,10 +14,10 @@
limitations under the License.
*/
import React, { HTMLAttributes } from 'react';
import classNames from 'classnames';
import React, { HTMLAttributes } from "react";
import classNames from "classnames";
import TooltipTarget from './TooltipTarget';
import TooltipTarget from "./TooltipTarget";
interface IProps extends HTMLAttributes<HTMLSpanElement> {
class?: string;
@ -45,7 +45,7 @@ export default class TextWithTooltip extends React.Component<IProps> {
className="mx_TextWithTooltip_tooltip"
{...props}
>
{ children }
{children}
</TooltipTarget>
);
}

View file

@ -45,13 +45,14 @@ export default ({ checked, disabled = false, title, tooltip, onChange, ...props
};
const classes = classNames({
"mx_ToggleSwitch": true,
"mx_ToggleSwitch_on": checked,
"mx_ToggleSwitch_enabled": !disabled,
mx_ToggleSwitch: true,
mx_ToggleSwitch_on: checked,
mx_ToggleSwitch_enabled: !disabled,
});
return (
<AccessibleTooltipButton {...props}
<AccessibleTooltipButton
{...props}
className={classes}
onClick={_onClick}
role="switch"

View file

@ -17,9 +17,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { CSSProperties } from 'react';
import ReactDOM from 'react-dom';
import classNames from 'classnames';
import React, { CSSProperties } from "react";
import ReactDOM from "react-dom";
import classNames from "classnames";
import UIStore from "../../../stores/UIStore";
import { objectHasDiff } from "../../../utils/objects";
@ -35,22 +35,22 @@ export enum Alignment {
}
export interface ITooltipProps {
// Class applied to the element used to position the tooltip
className?: string;
// Class applied to the tooltip itself
tooltipClassName?: string;
// Whether the tooltip is visible or hidden.
// The hidden state allows animating the tooltip away via CSS.
// Defaults to visible if unset.
visible?: boolean;
// the react element to put into the tooltip
label: React.ReactNode;
alignment?: Alignment; // defaults to Natural
// id describing tooltip
// used to associate tooltip with target for a11y
id?: string;
// If the parent is over this width, act as if it is only this wide
maxParentWidth?: number;
// Class applied to the element used to position the tooltip
className?: string;
// Class applied to the tooltip itself
tooltipClassName?: string;
// Whether the tooltip is visible or hidden.
// The hidden state allows animating the tooltip away via CSS.
// Defaults to visible if unset.
visible?: boolean;
// the react element to put into the tooltip
label: React.ReactNode;
alignment?: Alignment; // defaults to Natural
// id describing tooltip
// used to associate tooltip with target for a11y
id?: string;
// If the parent is over this width, act as if it is only this wide
maxParentWidth?: number;
}
type State = Partial<Pick<CSSProperties, "display" | "right" | "top" | "transform" | "left">>;
@ -82,12 +82,12 @@ export default class Tooltip extends React.PureComponent<ITooltipProps, State> {
}
public componentDidMount() {
window.addEventListener('scroll', this.updatePosition, {
window.addEventListener("scroll", this.updatePosition, {
passive: true,
capture: true,
});
this.parent = ReactDOM.findDOMNode(this)?.parentNode as Element ?? null;
this.parent = (ReactDOM.findDOMNode(this)?.parentNode as Element) ?? null;
this.updatePosition();
}
@ -100,7 +100,7 @@ export default class Tooltip extends React.PureComponent<ITooltipProps, State> {
// Remove the wrapper element, as the tooltip has finished using it
public componentWillUnmount() {
window.removeEventListener('scroll', this.updatePosition, {
window.removeEventListener("scroll", this.updatePosition, {
capture: true,
});
}
@ -114,18 +114,14 @@ export default class Tooltip extends React.PureComponent<ITooltipProps, State> {
const parentBox = this.parent.getBoundingClientRect();
const width = UIStore.instance.windowWidth;
const spacing = 6;
const parentWidth = (
this.props.maxParentWidth
? Math.min(parentBox.width, this.props.maxParentWidth)
: parentBox.width
);
const parentWidth = this.props.maxParentWidth
? Math.min(parentBox.width, this.props.maxParentWidth)
: parentBox.width;
const baseTop = parentBox.top + window.scrollY;
const centerTop = parentBox.top + window.scrollY + (parentBox.height / 2);
const centerTop = parentBox.top + window.scrollY + parentBox.height / 2;
const right = width - parentBox.left - window.scrollX;
const left = parentBox.right + window.scrollX;
const horizontalCenter = (
parentBox.left - window.scrollX + (parentWidth / 2)
);
const horizontalCenter = parentBox.left - window.scrollX + parentWidth / 2;
const style: State = {};
switch (this.props.alignment) {
@ -136,7 +132,7 @@ export default class Tooltip extends React.PureComponent<ITooltipProps, State> {
style.transform = "translateY(-50%)";
break;
}
// fall through to Right
// fall through to Right
case Alignment.Right:
style.left = left + spacing;
style.top = centerTop;
@ -180,8 +176,8 @@ export default class Tooltip extends React.PureComponent<ITooltipProps, State> {
public render() {
const tooltipClasses = classNames("mx_Tooltip", this.props.tooltipClassName, {
"mx_Tooltip_visible": this.props.visible,
"mx_Tooltip_invisible": !this.props.visible,
mx_Tooltip_visible: this.props.visible,
mx_Tooltip_invisible: !this.props.visible,
});
const style = { ...this.state };
@ -192,14 +188,10 @@ export default class Tooltip extends React.PureComponent<ITooltipProps, State> {
const tooltip = (
<div role="tooltip" className={tooltipClasses} style={style}>
<div className="mx_Tooltip_chevron" />
{ this.props.label }
{this.props.label}
</div>
);
return (
<div className={this.props.className}>
{ ReactDOM.createPortal(tooltip, Tooltip.container) }
</div>
);
return <div className={this.props.className}>{ReactDOM.createPortal(tooltip, Tooltip.container)}</div>;
}
}

View file

@ -15,9 +15,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import TooltipTarget from './TooltipTarget';
import TooltipTarget from "./TooltipTarget";
interface IProps {
helpText: React.ReactNode | string;

View file

@ -14,13 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { HTMLAttributes } from 'react';
import React, { HTMLAttributes } from "react";
import useFocus from "../../../hooks/useFocus";
import useHover from "../../../hooks/useHover";
import Tooltip, { ITooltipProps } from './Tooltip';
import Tooltip, { ITooltipProps } from "./Tooltip";
interface IProps extends HTMLAttributes<HTMLSpanElement>, Omit<ITooltipProps, 'visible'> {
interface IProps extends HTMLAttributes<HTMLSpanElement>, Omit<ITooltipProps, "visible"> {
tooltipTargetClassName?: string;
ignoreHover?: (ev: React.MouseEvent) => boolean;
}
@ -47,15 +47,17 @@ const TooltipTarget: React.FC<IProps> = ({
// No need to fill up the DOM with hidden tooltip elements. Only add the
// tooltip when we're hovering over the item (performance)
const tooltip = (isFocused || isHovering) && <Tooltip
id={id}
className={className}
tooltipClassName={tooltipClassName}
label={label}
alignment={alignment}
visible={isFocused || isHovering}
maxParentWidth={maxParentWidth}
/>;
const tooltip = (isFocused || isHovering) && (
<Tooltip
id={id}
className={className}
tooltipClassName={tooltipClassName}
label={label}
alignment={alignment}
visible={isFocused || isHovering}
maxParentWidth={maxParentWidth}
/>
);
return (
<div
@ -66,8 +68,8 @@ const TooltipTarget: React.FC<IProps> = ({
className={tooltipTargetClassName}
{...rest}
>
{ children }
{ tooltip }
{children}
{tooltip}
</div>
);
};

View file

@ -15,9 +15,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { _t } from '../../../languageHandler';
import { _t } from "../../../languageHandler";
interface IProps {
// The number of elements to show before truncating. If negative, no truncation is done.
@ -38,12 +38,10 @@ interface IProps {
}
export default class TruncatedList extends React.Component<IProps> {
static defaultProps ={
static defaultProps = {
truncateAt: 2,
createOverflowElement(overflowCount, totalCount) {
return (
<div>{ _t("And %(count)s more...", { count: overflowCount }) }</div>
);
return <div>{_t("And %(count)s more...", { count: overflowCount })}</div>;
},
};
@ -54,9 +52,11 @@ export default class TruncatedList extends React.Component<IProps> {
// XXX: I'm not sure why anything would pass null into this, it seems
// like a bizarre case to handle, but I'm preserving the behaviour.
// (see commit 38d5c7d5c5d5a34dc16ef5d46278315f5c57f542)
return React.Children.toArray(this.props.children).filter((c) => {
return c != null;
}).slice(start, end);
return React.Children.toArray(this.props.children)
.filter((c) => {
return c != null;
})
.slice(start, end);
}
}
@ -78,9 +78,7 @@ export default class TruncatedList extends React.Component<IProps> {
if (this.props.truncateAt >= 0) {
const overflowCount = totalChildren - this.props.truncateAt;
if (overflowCount > 1) {
overflowNode = this.props.createOverflowElement(
overflowCount, totalChildren,
);
overflowNode = this.props.createOverflowElement(overflowCount, totalChildren);
upperBound = this.props.truncateAt;
}
}
@ -88,8 +86,8 @@ export default class TruncatedList extends React.Component<IProps> {
return (
<div className={this.props.className}>
{ childNodes }
{ overflowNode }
{childNodes}
{overflowNode}
</div>
);
}

View file

@ -15,7 +15,7 @@ limitations under the License.
*/
import classNames from "classnames";
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState } from "react";
import { _t } from "../../../languageHandler";
import { UseCase } from "../../../settings/enums/UseCase";
@ -47,17 +47,17 @@ export function UseCaseSelection({ onFinished }: Props) {
}, [selection, onFinished]);
return (
<SplashPage className={classNames(
"mx_UseCaseSelection", {
"mx_UseCaseSelection_selected": selection !== null,
},
)}>
<SplashPage
className={classNames("mx_UseCaseSelection", {
mx_UseCaseSelection_selected: selection !== null,
})}
>
<div className="mx_UseCaseSelection_title mx_UseCaseSelection_slideIn">
<h1>{ _t("You're in") }</h1>
<h1>{_t("You're in")}</h1>
</div>
<div className="mx_UseCaseSelection_info mx_UseCaseSelection_slideInDelayed">
<h2>{ _t("Who will you chat to the most?") }</h2>
<h3>{ _t("We'll help you get connected.") }</h3>
<h2>{_t("Who will you chat to the most?")}</h2>
<h3>{_t("We'll help you get connected.")}</h3>
</div>
<div className="mx_UseCaseSelection_options mx_UseCaseSelection_slideInDelayed">
<UseCaseSelectionButton
@ -78,7 +78,7 @@ export function UseCaseSelection({ onFinished }: Props) {
</div>
<div className="mx_UseCaseSelection_skip mx_UseCaseSelection_slideInDelayed">
<AccessibleButton kind="link" onClick={async () => setSelected(UseCase.Skip)}>
{ _t("Skip") }
{_t("Skip")}
</AccessibleButton>
</div>
</SplashPage>

View file

@ -44,15 +44,18 @@ export function UseCaseSelectionButton({ useCase, onClick, selected }: Props) {
return (
<AccessibleButton
className={classNames("mx_UseCaseSelectionButton", {
"mx_UseCaseSelectionButton_selected": selected,
mx_UseCaseSelectionButton_selected: selected,
})}
onClick={async () => onClick(useCase)}>
<div className={classNames("mx_UseCaseSelectionButton_icon", {
"mx_UseCaseSelectionButton_messaging": useCase === UseCase.PersonalMessaging,
"mx_UseCaseSelectionButton_work": useCase === UseCase.WorkMessaging,
"mx_UseCaseSelectionButton_community": useCase === UseCase.CommunityMessaging,
})} />
<span>{ label }</span>
onClick={async () => onClick(useCase)}
>
<div
className={classNames("mx_UseCaseSelectionButton_icon", {
mx_UseCaseSelectionButton_messaging: useCase === UseCase.PersonalMessaging,
mx_UseCaseSelectionButton_work: useCase === UseCase.WorkMessaging,
mx_UseCaseSelectionButton_community: useCase === UseCase.CommunityMessaging,
})}
/>
<span>{label}</span>
<div className="mx_UseCaseSelectionButton_selectedIcon" />
</AccessibleButton>
);

View file

@ -80,7 +80,10 @@ export interface IValidationResult {
* the overall validity and a feedback UI that can be rendered for more detail.
*/
export default function withValidation<T = void, D = void>({
description, hideDescriptionIfValid, deriveData, rules,
description,
hideDescriptionIfValid,
deriveData,
rules,
}: IArgs<T, D>) {
return async function onValidate(
this: T,
@ -154,18 +157,22 @@ export default function withValidation<T = void, D = void>({
let details;
if (results && results.length) {
details = <ul className="mx_Validation_details">
{ results.map(result => {
const classes = classNames({
"mx_Validation_detail": true,
"mx_Validation_valid": result.valid,
"mx_Validation_invalid": !result.valid,
});
return <li key={result.key} className={classes}>
{ result.text }
</li>;
}) }
</ul>;
details = (
<ul className="mx_Validation_details">
{results.map((result) => {
const classes = classNames({
mx_Validation_detail: true,
mx_Validation_valid: result.valid,
mx_Validation_invalid: !result.valid,
});
return (
<li key={result.key} className={classes}>
{result.text}
</li>
);
})}
</ul>
);
}
let summary;
@ -173,15 +180,17 @@ export default function withValidation<T = void, D = void>({
// We're setting `this` to whichever component holds the validation
// function. That allows rules to access the state of the component.
const content = description.call(this, derivedData, results);
summary = content ? <div className="mx_Validation_description">{ content }</div> : undefined;
summary = content ? <div className="mx_Validation_description">{content}</div> : undefined;
}
let feedback;
if (summary || details) {
feedback = <div className="mx_Validation">
{ summary }
{ details }
</div>;
feedback = (
<div className="mx_Validation">
{summary}
{details}
</div>
);
}
return {

View file

@ -27,9 +27,10 @@ export default class VerificationQRCode extends React.PureComponent<IProps> {
public render(): JSX.Element {
return (
<QRCode
data={[{ data: this.props.qrCodeData.getBuffer(), mode: 'byte' }]}
data={[{ data: this.props.qrCodeData.getBuffer(), mode: "byte" }]}
className="mx_VerificationQRCode"
width={196} />
width={196}
/>
);
}
}