Apply prettier formatting
This commit is contained in:
parent
1cac306093
commit
526645c791
1576 changed files with 65385 additions and 62478 deletions
|
@ -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 we’re 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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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.")} {encryptionWarning}
|
||||
</div>
|
||||
<div className='mx_AppPermissionWarning_row mx_AppPermissionWarning_smallText'>
|
||||
{ _t("This widget may use cookies.") } { 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>
|
||||
|
|
|
@ -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> - </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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
}}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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"> </div>
|
||||
<ol className="mx_GenericEventListSummary_unstyledList">
|
||||
{ children }
|
||||
</ol>
|
||||
</React.Fragment>;
|
||||
body = (
|
||||
<React.Fragment>
|
||||
<div className="mx_GenericEventListSummary_line"> </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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -34,7 +34,7 @@ const RoomName = ({ room, children }: IProps): JSX.Element => {
|
|||
}, [room]);
|
||||
|
||||
if (children) return children(name);
|
||||
return <>{ name || "" }</>;
|
||||
return <>{name || ""}</>;
|
||||
};
|
||||
|
||||
export default RoomName;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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> </React.Fragment> }
|
||||
{message && (
|
||||
<React.Fragment>
|
||||
<div className="mx_Spinner_Msg">{message}</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
<div
|
||||
className="mx_Spinner_icon"
|
||||
style={{ width: w, height: h }}
|
||||
|
|
|
@ -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}
|
||||
|
||||
<span className="mx_EventTile_spoiler_content" dangerouslySetInnerHTML={{ __html: this.props.contentHtml }} />
|
||||
<span
|
||||
className="mx_EventTile_spoiler_content"
|
||||
dangerouslySetInnerHTML={{ __html: this.props.contentHtml }}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>;
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue