Merge remote-tracking branch 'upstream/develop' into feature/collapse-pinned-mels/17938
This commit is contained in:
commit
2df4f7b859
483 changed files with 11911 additions and 7811 deletions
|
@ -94,15 +94,15 @@ export default class AppPermission extends React.Component {
|
|||
|
||||
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>
|
||||
);
|
||||
|
@ -114,7 +114,7 @@ export default class AppPermission extends React.Component {
|
|||
|
||||
// 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.",
|
||||
? _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 });
|
||||
|
@ -124,22 +124,22 @@ export default class AppPermission extends React.Component {
|
|||
return (
|
||||
<div className='mx_AppPermissionWarning'>
|
||||
<div className='mx_AppPermissionWarning_row mx_AppPermissionWarning_bolder mx_AppPermissionWarning_smallText'>
|
||||
{_t("Widget added by")}
|
||||
{ _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>
|
||||
{ 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}
|
||||
{ warning }
|
||||
</div>
|
||||
<div className='mx_AppPermissionWarning_row mx_AppPermissionWarning_smallText'>
|
||||
{_t("This widget may use cookies.")} {encryptionWarning}
|
||||
{ _t("This widget may use cookies.") } { encryptionWarning }
|
||||
</div>
|
||||
<div className='mx_AppPermissionWarning_row'>
|
||||
<AccessibleButton kind='primary_sm' onClick={this.props.onPermissionGranted}>
|
||||
{_t("Continue")}
|
||||
{ _t("Continue") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -238,6 +238,7 @@ export default class AppTile extends React.Component {
|
|||
case 'm.sticker':
|
||||
if (this._sgWidget.widgetApi.hasCapability(MatrixCapabilities.StickerSending)) {
|
||||
dis.dispatch({ action: 'post_sticker_message', data: payload.data });
|
||||
dis.dispatch({ action: 'stickerpicker_close' });
|
||||
} else {
|
||||
console.warn('Ignoring sticker message. Invalid capability');
|
||||
}
|
||||
|
@ -407,7 +408,7 @@ export default class AppTile extends React.Component {
|
|||
// AppTile's border is in the wrong place
|
||||
appTileBody = <div className="mx_AppTile_persistedWrapper">
|
||||
<PersistedElement persistKey={this._persistKey}>
|
||||
{appTileBody}
|
||||
{ appTileBody }
|
||||
</PersistedElement>
|
||||
</div>;
|
||||
}
|
||||
|
@ -452,13 +453,13 @@ export default class AppTile extends React.Component {
|
|||
title={_t('Popout widget')}
|
||||
onClick={this._onPopoutWidgetClick}
|
||||
/> }
|
||||
{ <ContextMenuButton
|
||||
<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 }
|
||||
|
|
|
@ -41,6 +41,6 @@ export default function DNDTagTile(props) {
|
|||
menuDisplayed={menuDisplayed}
|
||||
openMenu={openMenu}
|
||||
/>
|
||||
{contextMenu}
|
||||
{ contextMenu }
|
||||
</>;
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ export default function DesktopBuildsNotice({ isRoomEncrypted, kind }: IProps) {
|
|||
|
||||
if (EventIndexPeg.error) {
|
||||
return <>
|
||||
{_t("Message search initialisation failed, check <a>your settings</a> for more information", {}, {
|
||||
{ _t("Message search initialisation failed, check <a>your settings</a> for more information", {}, {
|
||||
a: sub => (<a onClick={(evt) => {
|
||||
evt.preventDefault();
|
||||
dis.dispatch({
|
||||
|
@ -46,9 +46,9 @@ export default function DesktopBuildsNotice({ isRoomEncrypted, kind }: IProps) {
|
|||
initialTabId: UserTab.Security,
|
||||
});
|
||||
}}>
|
||||
{sub}
|
||||
{ sub }
|
||||
</a>),
|
||||
})}
|
||||
}) }
|
||||
</>;
|
||||
}
|
||||
|
||||
|
@ -61,12 +61,12 @@ export default function DesktopBuildsNotice({ isRoomEncrypted, kind }: IProps) {
|
|||
switch (kind) {
|
||||
case WarningKind.Files:
|
||||
text = _t("Use the <a>Desktop app</a> to see all encrypted files", {}, {
|
||||
a: sub => (<a href={desktopBuilds.url} target="_blank" rel="noreferrer noopener">{sub}</a>),
|
||||
a: sub => (<a href={desktopBuilds.url} 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={desktopBuilds.url} target="_blank" rel="noreferrer noopener">{sub}</a>),
|
||||
a: sub => (<a href={desktopBuilds.url} target="_blank" rel="noreferrer noopener">{ sub }</a>),
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
@ -89,8 +89,8 @@ export default function DesktopBuildsNotice({ isRoomEncrypted, kind }: IProps) {
|
|||
|
||||
return (
|
||||
<div className="mx_DesktopBuildsNotice">
|
||||
{logo}
|
||||
<span>{text}</span>
|
||||
{ logo }
|
||||
<span>{ text }</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -56,7 +56,7 @@ export class ExistingSource extends React.Component<DesktopCapturerSourceIProps>
|
|||
className="mx_desktopCapturerSourcePicker_stream_thumbnail"
|
||||
src={this.props.source.thumbnailURL}
|
||||
/>
|
||||
<span className="mx_desktopCapturerSourcePicker_stream_name">{this.props.source.name}</span>
|
||||
<span className="mx_desktopCapturerSourcePicker_stream_name">{ this.props.source.name }</span>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
@ -157,13 +157,13 @@ export default class DesktopCapturerSourcePicker extends React.Component<
|
|||
className={screensButtonStyle}
|
||||
onClick={this.onScreensClick}
|
||||
>
|
||||
{_t("Screens")}
|
||||
{ _t("Screens") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
className={windowsButtonStyle}
|
||||
onClick={this.onWindowsClick}
|
||||
>
|
||||
{_t("Windows")}
|
||||
{ _t("Windows") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<div className="mx_desktopCapturerSourcePicker_panel">
|
||||
|
|
31
src/components/views/elements/DialPadBackspaceButton.tsx
Normal file
31
src/components/views/elements/DialPadBackspaceButton.tsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
|
||||
interface IProps {
|
||||
// Callback for when the button is pressed
|
||||
onBackspacePress: () => void;
|
||||
}
|
||||
|
||||
export default class DialPadBackspaceButton extends React.PureComponent<IProps> {
|
||||
render() {
|
||||
return <div className="mx_DialPadBackspaceButtonWrapper">
|
||||
<AccessibleButton className="mx_DialPadBackspaceButton" onClick={this.props.onBackspacePress} />
|
||||
</div>;
|
||||
}
|
||||
}
|
|
@ -92,7 +92,7 @@ export default class DialogButtons extends React.Component {
|
|||
|
||||
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 (
|
||||
|
|
|
@ -88,7 +88,7 @@ export default class DirectorySearchBox extends React.Component {
|
|||
if (this.props.showJoinButton) {
|
||||
joinButton = <AccessibleButton className="mx_DirectorySearchBox_joinButton"
|
||||
onClick={this._onJoinButtonClick}
|
||||
>{_t("Join")}</AccessibleButton>;
|
||||
>{ _t("Join") }</AccessibleButton>;
|
||||
}
|
||||
|
||||
return <div className={`mx_DirectorySearchBox ${this.props.className} mx_textinput`}>
|
||||
|
|
|
@ -63,21 +63,21 @@ export class EditableItem extends React.Component<IItemProps, IItemState> {
|
|||
return (
|
||||
<div className="mx_EditableItem">
|
||||
<span className="mx_EditableItem_promptText">
|
||||
{_t("Are you sure?")}
|
||||
{ _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>
|
||||
);
|
||||
|
@ -86,7 +86,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>
|
||||
);
|
||||
}
|
||||
|
@ -155,7 +155,7 @@ 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
|
||||
|
@ -166,7 +166,7 @@ export default class EditableItemList<P = {}> extends React.PureComponent<IProps
|
|||
/>;
|
||||
});
|
||||
|
||||
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 (
|
||||
|
|
|
@ -81,33 +81,33 @@ export default class ErrorBoundary extends React.PureComponent<{}, IState> {
|
|||
let bugReportSection;
|
||||
if (SdkConfig.get().bug_report_endpoint_url) {
|
||||
bugReportSection = <React.Fragment>
|
||||
<p>{_t(
|
||||
<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(
|
||||
) }</p>
|
||||
<p>{ _t(
|
||||
"If you've submitted a bug via GitHub, debug logs can help " +
|
||||
"us track down the problem. Debug logs contain application " +
|
||||
"usage data including your username, the IDs or aliases of " +
|
||||
"the rooms or groups you have visited and the usernames of " +
|
||||
"other users. They do not contain messages.",
|
||||
)}</p>
|
||||
) }</p>
|
||||
<AccessibleButton onClick={this.onBugReport} kind='primary'>
|
||||
{_t("Submit debug logs")}
|
||||
{ _t("Submit debug logs") }
|
||||
</AccessibleButton>
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
return <div className="mx_ErrorBoundary">
|
||||
<div className="mx_ErrorBoundary_body">
|
||||
<h1>{_t("Something went wrong!")}</h1>
|
||||
<h1>{ _t("Something went wrong!") }</h1>
|
||||
{ bugReportSection }
|
||||
<AccessibleButton onClick={this.onClearCacheAndReload} kind='danger'>
|
||||
{_t("Clear cache and reload")}
|
||||
{ _t("Clear cache and reload") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>;
|
||||
|
|
|
@ -63,7 +63,7 @@ const EventListSummary: React.FC<IProps> = ({
|
|||
// If we are only given few events then just pass them through
|
||||
if (events.length < threshold) {
|
||||
return (
|
||||
<li className="mx_EventListSummary" data-scroll-tokens={eventIds}>
|
||||
<li className="mx_EventListSummary" data-scroll-tokens={eventIds} data-expanded={true}>
|
||||
{ children }
|
||||
</li>
|
||||
);
|
||||
|
@ -92,7 +92,7 @@ const EventListSummary: React.FC<IProps> = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<li className="mx_EventListSummary" data-scroll-tokens={eventIds}>
|
||||
<li className="mx_EventListSummary" data-scroll-tokens={eventIds} data-expanded={expanded + ""}>
|
||||
<AccessibleButton className="mx_EventListSummary_toggle" onClick={toggleExpanded} aria-expanded={expanded}>
|
||||
{ expanded ? _t('collapse') : _t('expand') }
|
||||
</AccessibleButton>
|
||||
|
@ -101,4 +101,8 @@ const EventListSummary: React.FC<IProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
EventListSummary.defaultProps = {
|
||||
startExpanded: false,
|
||||
};
|
||||
|
||||
export default EventListSummary;
|
||||
|
|
|
@ -78,7 +78,7 @@ const FacePile = ({ room, onlyKnownUsers = true, numShown = DEFAULT_NUM_FACES, .
|
|||
<TextWithTooltip class="mx_FacePile_faces" tooltip={tooltip} tooltipProps={{ yOffset: 32 }}>
|
||||
{ members.length > numShown ? <span className="mx_FacePile_face mx_FacePile_more" /> : null }
|
||||
{ shownMembers.map(m =>
|
||||
<MemberAvatar key={m.userId} member={m} width={28} height={28} className="mx_FacePile_face" /> )}
|
||||
<MemberAvatar key={m.userId} member={m} width={28} height={28} className="mx_FacePile_face" /> ) }
|
||||
</TextWithTooltip>
|
||||
{ onlyKnownUsers && <span className="mx_FacePile_summary">
|
||||
{ _t("%(count)s people you know have already joined", { count: members.length }) }
|
||||
|
|
|
@ -240,11 +240,11 @@ 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;
|
||||
|
@ -273,11 +273,11 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
|
|||
}
|
||||
|
||||
return <div className={fieldClasses}>
|
||||
{prefixContainer}
|
||||
{fieldInput}
|
||||
<label htmlFor={this.id}>{this.props.label}</label>
|
||||
{postfixContainer}
|
||||
{fieldTooltip}
|
||||
{ prefixContainer }
|
||||
{ fieldInput }
|
||||
<label htmlFor={this.id}>{ this.props.label }</label>
|
||||
{ postfixContainer }
|
||||
{ fieldTooltip }
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
|||
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { normalizeWheelEvent } from "../../../utils/Mouse";
|
||||
import { IDialogProps } from '../dialogs/IDialogProps';
|
||||
|
||||
// Max scale to keep gaps around the image
|
||||
const MAX_SCALE = 0.95;
|
||||
|
@ -43,14 +44,13 @@ const ZOOM_COEFFICIENT = 0.0025;
|
|||
// If we have moved only this much we can zoom
|
||||
const ZOOM_DISTANCE = 10;
|
||||
|
||||
interface IProps {
|
||||
interface IProps extends IDialogProps {
|
||||
src: string; // the source of the image being displayed
|
||||
name?: string; // the main title ('name') for the image
|
||||
link?: string; // the link (if any) applied to the name of the image
|
||||
width?: number; // width of the image src in pixels
|
||||
height?: number; // height of the image src in pixels
|
||||
fileSize?: number; // size of the image src in bytes
|
||||
onFinished(): void; // callback when the lightbox is dismissed
|
||||
|
||||
// the event (if any) that the Image is displaying. Used for event-specific stuff like
|
||||
// redactions, senders, timestamps etc. Other descriptors are taken from the explicit
|
||||
|
@ -452,28 +452,28 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
<div className="mx_ImageView_panel">
|
||||
{ info }
|
||||
<div className="mx_ImageView_toolbar">
|
||||
{ zoomOutButton }
|
||||
{ zoomInButton }
|
||||
<AccessibleTooltipButton
|
||||
className="mx_ImageView_button mx_ImageView_button_rotateCCW"
|
||||
title={_t("Rotate Left")}
|
||||
onClick={ this.onRotateCounterClockwiseClick }>
|
||||
onClick={this.onRotateCounterClockwiseClick}>
|
||||
</AccessibleTooltipButton>
|
||||
<AccessibleTooltipButton
|
||||
className="mx_ImageView_button mx_ImageView_button_rotateCW"
|
||||
title={_t("Rotate Right")}
|
||||
onClick={this.onRotateClockwiseClick}>
|
||||
</AccessibleTooltipButton>
|
||||
{ zoomOutButton }
|
||||
{ zoomInButton }
|
||||
<AccessibleTooltipButton
|
||||
className="mx_ImageView_button mx_ImageView_button_download"
|
||||
title={_t("Download")}
|
||||
onClick={ this.onDownloadClick }>
|
||||
onClick={this.onDownloadClick}>
|
||||
</AccessibleTooltipButton>
|
||||
{ contextMenuButton }
|
||||
<AccessibleTooltipButton
|
||||
className="mx_ImageView_button mx_ImageView_button_close"
|
||||
title={_t("Close")}
|
||||
onClick={ this.props.onFinished }>
|
||||
onClick={this.props.onFinished}>
|
||||
</AccessibleTooltipButton>
|
||||
{ this.renderContextMenu() }
|
||||
</div>
|
||||
|
@ -488,8 +488,8 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
>
|
||||
<img
|
||||
src={this.props.src}
|
||||
title={this.props.name}
|
||||
style={style}
|
||||
alt={this.props.name}
|
||||
ref={this.image}
|
||||
className="mx_ImageView_image"
|
||||
draggable={true}
|
||||
|
|
|
@ -22,9 +22,16 @@ import Tooltip, { Alignment } from './Tooltip';
|
|||
import { _t } from "../../../languageHandler";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
export enum InfoTooltipKind {
|
||||
Info = "info",
|
||||
Warning = "warning",
|
||||
}
|
||||
|
||||
interface ITooltipProps {
|
||||
tooltip?: React.ReactNode;
|
||||
className?: string;
|
||||
tooltipClassName?: string;
|
||||
kind?: InfoTooltipKind;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -53,8 +60,12 @@ export default class InfoTooltip extends React.PureComponent<ITooltipProps, ISta
|
|||
};
|
||||
|
||||
render() {
|
||||
const { tooltip, children, tooltipClassName } = this.props;
|
||||
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"
|
||||
);
|
||||
|
||||
// Tooltip are forced on the right for a more natural feel to them on info icons
|
||||
const tip = this.state.hover ? <Tooltip
|
||||
|
@ -64,10 +75,14 @@ export default class InfoTooltip extends React.PureComponent<ITooltipProps, ISta
|
|||
alignment={Alignment.Right}
|
||||
/> : <div />;
|
||||
return (
|
||||
<div onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} className="mx_InfoTooltip">
|
||||
<span className="mx_InfoTooltip_icon" aria-label={title} />
|
||||
{children}
|
||||
{tip}
|
||||
<div
|
||||
onMouseOver={this.onMouseOver}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
className={classNames("mx_InfoTooltip", className)}
|
||||
>
|
||||
<span className={classNames("mx_InfoTooltip_icon", iconClassName)} aria-label={title} />
|
||||
{ children }
|
||||
{ tip }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -39,7 +39,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>
|
||||
);
|
||||
|
|
|
@ -51,11 +51,11 @@ export default class InviteReason extends React.PureComponent<IProps, IState> {
|
|||
});
|
||||
|
||||
return <div className={classes}>
|
||||
<div className="mx_InviteReason_reason">{this.props.reason}</div>
|
||||
<div className="mx_InviteReason_reason">{ this.props.reason }</div>
|
||||
<div className="mx_InviteReason_view"
|
||||
onClick={this.onViewClick}
|
||||
>
|
||||
{_t("View message")}
|
||||
{ _t("View message") }
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -57,8 +57,8 @@ export default class LabelledToggleSwitch extends React.PureComponent<IProps> {
|
|||
const classes = `mx_SettingsFlag ${this.props.className || ""}`;
|
||||
return (
|
||||
<div className={classes}>
|
||||
{firstPart}
|
||||
{secondPart}
|
||||
{ firstPart }
|
||||
{ secondPart }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ interface IProps {
|
|||
hasAvatar: boolean;
|
||||
noAvatarLabel?: string;
|
||||
hasAvatarLabel?: string;
|
||||
setAvatarUrl(url: string): Promise<void>;
|
||||
setAvatarUrl(url: string): Promise<unknown>;
|
||||
}
|
||||
|
||||
const MiniAvatarUploader: React.FC<IProps> = ({ hasAvatar, hasAvatarLabel, noAvatarLabel, setAvatarUrl, children }) => {
|
||||
|
|
|
@ -156,7 +156,7 @@ export default class PersistedElement extends React.Component {
|
|||
renderApp() {
|
||||
const content = <MatrixClientContext.Provider value={MatrixClientPeg.get()}>
|
||||
<div ref={this.collectChild} style={this.props.style}>
|
||||
{this.props.children}
|
||||
{ this.props.children }
|
||||
</div>
|
||||
</MatrixClientContext.Provider>;
|
||||
|
||||
|
|
|
@ -161,7 +161,7 @@ export default class PowerSelector extends React.Component {
|
|||
label={label} onChange={this.onSelectChange}
|
||||
value={String(this.state.selectValue)} disabled={this.props.disabled}
|
||||
>
|
||||
{options}
|
||||
{ options }
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2017 - 2021 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,71 +14,72 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import PropTypes from 'prop-types';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { wantsDateSeparator } from '../../../DateUtils';
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import { makeUserPermalink, RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { LayoutPropType } from "../../../settings/Layout";
|
||||
import { Layout } from "../../../settings/Layout";
|
||||
import escapeHtml from "escape-html";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { getUserNameColorClass } from "../../../utils/FormattingUtils";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
import { PERMITTED_URL_SCHEMES } from "../../../HtmlUtils";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import Spinner from './Spinner';
|
||||
import ReplyTile from "../rooms/ReplyTile";
|
||||
import Pill from './Pill';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
|
||||
interface IProps {
|
||||
// the latest event in this chain of replies
|
||||
parentEv?: MatrixEvent;
|
||||
// called when the ReplyThread contents has changed, including EventTiles thereof
|
||||
onHeightChanged: () => void;
|
||||
permalinkCreator: RoomPermalinkCreator;
|
||||
// Specifies which layout to use.
|
||||
layout?: Layout;
|
||||
// Whether to always show a timestamp
|
||||
alwaysShowTimestamps?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
// The loaded events to be rendered as linear-replies
|
||||
events: MatrixEvent[];
|
||||
// The latest loaded event which has not yet been shown
|
||||
loadedEv: MatrixEvent;
|
||||
// Whether the component is still loading more events
|
||||
loading: boolean;
|
||||
// Whether as error was encountered fetching a replied to event.
|
||||
err: boolean;
|
||||
}
|
||||
|
||||
// This component does no cycle detection, simply because the only way to make such a cycle would be to
|
||||
// craft event_id's, using a homeserver that generates predictable event IDs; even then the impact would
|
||||
// be low as each event being loaded (after the first) is triggered by an explicit user action.
|
||||
@replaceableComponent("views.elements.ReplyThread")
|
||||
export default class ReplyThread extends React.Component {
|
||||
static propTypes = {
|
||||
// the latest event in this chain of replies
|
||||
parentEv: PropTypes.instanceOf(MatrixEvent),
|
||||
// called when the ReplyThread contents has changed, including EventTiles thereof
|
||||
onHeightChanged: PropTypes.func.isRequired,
|
||||
permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired,
|
||||
// Specifies which layout to use.
|
||||
layout: LayoutPropType,
|
||||
// Whether to always show a timestamp
|
||||
alwaysShowTimestamps: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default class ReplyThread extends React.Component<IProps, IState> {
|
||||
static contextType = MatrixClientContext;
|
||||
private unmounted = false;
|
||||
private room: Room;
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
// The loaded events to be rendered as linear-replies
|
||||
events: [],
|
||||
|
||||
// The latest loaded event which has not yet been shown
|
||||
loadedEv: null,
|
||||
// Whether the component is still loading more events
|
||||
loading: true,
|
||||
|
||||
// Whether as error was encountered fetching a replied to event.
|
||||
err: false,
|
||||
};
|
||||
|
||||
this.unmounted = false;
|
||||
this.context.on("Event.replaced", this.onEventReplaced);
|
||||
this.room = this.context.getRoom(this.props.parentEv.getRoomId());
|
||||
this.room.on("Room.redaction", this.onRoomRedaction);
|
||||
this.room.on("Room.redactionCancelled", this.onRoomRedaction);
|
||||
|
||||
this.onQuoteClick = this.onQuoteClick.bind(this);
|
||||
this.canCollapse = this.canCollapse.bind(this);
|
||||
this.collapse = this.collapse.bind(this);
|
||||
}
|
||||
|
||||
static getParentEventId(ev) {
|
||||
public static getParentEventId(ev: MatrixEvent): string {
|
||||
if (!ev || ev.isRedacted()) return;
|
||||
|
||||
// XXX: For newer relations (annotations, replacements, etc.), we now
|
||||
|
@ -95,7 +95,7 @@ export default class ReplyThread extends React.Component {
|
|||
}
|
||||
|
||||
// Part of Replies fallback support
|
||||
static stripPlainReply(body) {
|
||||
public static stripPlainReply(body: string): string {
|
||||
// Removes lines beginning with `> ` until you reach one that doesn't.
|
||||
const lines = body.split('\n');
|
||||
while (lines.length && lines[0].startsWith('> ')) lines.shift();
|
||||
|
@ -105,7 +105,7 @@ export default class ReplyThread extends React.Component {
|
|||
}
|
||||
|
||||
// Part of Replies fallback support
|
||||
static stripHTMLReply(html) {
|
||||
public static stripHTMLReply(html: string): string {
|
||||
// Sanitize the original HTML for inclusion in <mx-reply>. We allow
|
||||
// any HTML, since the original sender could use special tags that we
|
||||
// don't recognize, but want to pass along to any recipients who do
|
||||
|
@ -127,7 +127,10 @@ export default class ReplyThread extends React.Component {
|
|||
}
|
||||
|
||||
// Part of Replies fallback support
|
||||
static getNestedReplyText(ev, permalinkCreator) {
|
||||
public static getNestedReplyText(
|
||||
ev: MatrixEvent,
|
||||
permalinkCreator: RoomPermalinkCreator,
|
||||
): { body: string, html: string } {
|
||||
if (!ev) return null;
|
||||
|
||||
let { body, formatted_body: html } = ev.getContent();
|
||||
|
@ -203,7 +206,7 @@ export default class ReplyThread extends React.Component {
|
|||
return { body, html };
|
||||
}
|
||||
|
||||
static makeReplyMixIn(ev) {
|
||||
public static makeReplyMixIn(ev: MatrixEvent) {
|
||||
if (!ev) return {};
|
||||
return {
|
||||
'm.relates_to': {
|
||||
|
@ -214,10 +217,15 @@ export default class ReplyThread extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, layout, alwaysShowTimestamps) {
|
||||
if (!ReplyThread.getParentEventId(parentEv)) {
|
||||
return null;
|
||||
}
|
||||
public static makeThread(
|
||||
parentEv: MatrixEvent,
|
||||
onHeightChanged: () => void,
|
||||
permalinkCreator: RoomPermalinkCreator,
|
||||
ref: React.RefObject<ReplyThread>,
|
||||
layout: Layout,
|
||||
alwaysShowTimestamps: boolean,
|
||||
): JSX.Element {
|
||||
if (!ReplyThread.getParentEventId(parentEv)) return null;
|
||||
return <ReplyThread
|
||||
parentEv={parentEv}
|
||||
onHeightChanged={onHeightChanged}
|
||||
|
@ -238,37 +246,9 @@ export default class ReplyThread extends React.Component {
|
|||
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
this.context.removeListener("Event.replaced", this.onEventReplaced);
|
||||
if (this.room) {
|
||||
this.room.removeListener("Room.redaction", this.onRoomRedaction);
|
||||
this.room.removeListener("Room.redactionCancelled", this.onRoomRedaction);
|
||||
}
|
||||
}
|
||||
|
||||
updateForEventId = (eventId) => {
|
||||
if (this.state.events.some(event => event.getId() === eventId)) {
|
||||
this.forceUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
onEventReplaced = (ev) => {
|
||||
if (this.unmounted) return;
|
||||
|
||||
// If one of the events we are rendering gets replaced, force a re-render
|
||||
this.updateForEventId(ev.getId());
|
||||
};
|
||||
|
||||
onRoomRedaction = (ev) => {
|
||||
if (this.unmounted) return;
|
||||
|
||||
const eventId = ev.getAssociatedId();
|
||||
if (!eventId) return;
|
||||
|
||||
// If one of the events we are rendering gets redacted, force a re-render
|
||||
this.updateForEventId(eventId);
|
||||
};
|
||||
|
||||
async initialize() {
|
||||
private async initialize(): Promise<void> {
|
||||
const { parentEv } = this.props;
|
||||
// at time of making this component we checked that props.parentEv has a parentEventId
|
||||
const ev = await this.getEvent(ReplyThread.getParentEventId(parentEv));
|
||||
|
@ -287,7 +267,7 @@ export default class ReplyThread extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
async getNextEvent(ev) {
|
||||
private async getNextEvent(ev: MatrixEvent): Promise<MatrixEvent> {
|
||||
try {
|
||||
const inReplyToEventId = ReplyThread.getParentEventId(ev);
|
||||
return await this.getEvent(inReplyToEventId);
|
||||
|
@ -296,7 +276,7 @@ export default class ReplyThread extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
async getEvent(eventId) {
|
||||
private async getEvent(eventId: string): Promise<MatrixEvent> {
|
||||
if (!eventId) return null;
|
||||
const event = this.room.findEventById(eventId);
|
||||
if (event) return event;
|
||||
|
@ -313,15 +293,15 @@ export default class ReplyThread extends React.Component {
|
|||
return this.room.findEventById(eventId);
|
||||
}
|
||||
|
||||
canCollapse() {
|
||||
public canCollapse = (): boolean => {
|
||||
return this.state.events.length > 1;
|
||||
}
|
||||
};
|
||||
|
||||
collapse() {
|
||||
public collapse = (): void => {
|
||||
this.initialize();
|
||||
}
|
||||
};
|
||||
|
||||
async onQuoteClick() {
|
||||
private onQuoteClick = async (): Promise<void> => {
|
||||
const events = [this.state.loadedEv, ...this.state.events];
|
||||
|
||||
let loadedEv = null;
|
||||
|
@ -335,6 +315,10 @@ export default class ReplyThread extends React.Component {
|
|||
});
|
||||
|
||||
dis.fire(Action.FocusSendMessageComposer);
|
||||
};
|
||||
|
||||
private getReplyThreadColorClass(ev: MatrixEvent): string {
|
||||
return getUserNameColorClass(ev.getSender()).replace("Username", "ReplyThread");
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -349,9 +333,8 @@ export default class ReplyThread extends React.Component {
|
|||
</blockquote>;
|
||||
} else if (this.state.loadedEv) {
|
||||
const ev = this.state.loadedEv;
|
||||
const Pill = sdk.getComponent('elements.Pill');
|
||||
const room = this.context.getRoom(ev.getRoomId());
|
||||
header = <blockquote className="mx_ReplyThread">
|
||||
header = <blockquote className={`mx_ReplyThread ${this.getReplyThreadColorClass(ev)}`}>
|
||||
{
|
||||
_t('<a>In reply to</a> <pill>', {}, {
|
||||
'a': (sub) => <a onClick={this.onQuoteClick} className="mx_ReplyThread_show">{ sub }</a>,
|
||||
|
@ -367,33 +350,15 @@ export default class ReplyThread extends React.Component {
|
|||
}
|
||||
</blockquote>;
|
||||
} else if (this.state.loading) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
header = <Spinner w={16} h={16} />;
|
||||
}
|
||||
|
||||
const EventTile = sdk.getComponent('views.rooms.EventTile');
|
||||
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||
const evTiles = this.state.events.map((ev) => {
|
||||
let dateSep = null;
|
||||
|
||||
if (wantsDateSeparator(this.props.parentEv.getDate(), ev.getDate())) {
|
||||
dateSep = <a href={this.props.url}><DateSeparator ts={ev.getTs()} /></a>;
|
||||
}
|
||||
|
||||
return <blockquote className="mx_ReplyThread" key={ev.getId()}>
|
||||
{ dateSep }
|
||||
<EventTile
|
||||
return <blockquote className={`mx_ReplyThread ${this.getReplyThreadColorClass(ev)}`} key={ev.getId()}>
|
||||
<ReplyTile
|
||||
mxEvent={ev}
|
||||
tileShape="reply"
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
isRedacted={ev.isRedacted()}
|
||||
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
|
||||
layout={this.props.layout}
|
||||
alwaysShowTimestamps={this.props.alwaysShowTimestamps}
|
||||
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
|
||||
replacingEventId={ev.replacingEventId()}
|
||||
as="div"
|
||||
/>
|
||||
</blockquote>;
|
||||
});
|
|
@ -55,7 +55,7 @@ export default class RoomAliasField extends React.PureComponent<IProps, IState>
|
|||
render() {
|
||||
const poundSign = (<span>#</span>);
|
||||
const aliasPostfix = ":" + this.props.domain;
|
||||
const domain = (<span title={aliasPostfix}>{aliasPostfix}</span>);
|
||||
const domain = (<span title={aliasPostfix}>{ aliasPostfix }</span>);
|
||||
const maxlength = 255 - this.props.domain.length - 2; // 2 for # and :
|
||||
return (
|
||||
<Field
|
||||
|
|
|
@ -63,28 +63,28 @@ const ServerPicker = ({ title, dialogTitle, serverConfig, onServerConfigChange }
|
|||
});
|
||||
};
|
||||
editBtn = <AccessibleButton className="mx_ServerPicker_change" kind="link" onClick={onClick}>
|
||||
{_t("Edit")}
|
||||
{ _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}
|
||||
{ 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")}
|
||||
{ _t("Join millions for free on the largest public server") }
|
||||
</span>;
|
||||
}
|
||||
|
||||
return <div className="mx_ServerPicker">
|
||||
<h3>{title || _t("Homeserver")}</h3>
|
||||
<h3>{ title || _t("Homeserver") }</h3>
|
||||
<AccessibleButton className="mx_ServerPicker_help" onClick={onHelpClick} />
|
||||
<span className="mx_ServerPicker_server">{serverName}</span>
|
||||
<span className="mx_ServerPicker_server">{ serverName }</span>
|
||||
{ editBtn }
|
||||
{ desc }
|
||||
</div>;
|
||||
|
|
|
@ -88,12 +88,12 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
|
|||
onChange={this.checkBoxOnChange}
|
||||
disabled={this.props.disabled || !canChange}
|
||||
>
|
||||
{label}
|
||||
{ label }
|
||||
</StyledCheckbox>;
|
||||
} else {
|
||||
return (
|
||||
<div className="mx_SettingsFlag">
|
||||
<span className="mx_SettingsFlag_label">{label}</span>
|
||||
<span className="mx_SettingsFlag_label">{ label }</span>
|
||||
<ToggleSwitch
|
||||
checked={this.state.value}
|
||||
onChange={this.onChange}
|
||||
|
|
|
@ -98,7 +98,7 @@ export default class Slider extends React.Component<IProps> {
|
|||
{ selection }
|
||||
</div>
|
||||
<div className="mx_Slider_dotContainer">
|
||||
{dots}
|
||||
{ dots }
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
|
@ -139,7 +139,7 @@ class Dot extends React.PureComponent<IDotProps> {
|
|||
<div className={className} />
|
||||
<div className="mx_Slider_labelContainer">
|
||||
<div className="mx_Slider_label">
|
||||
{this.props.label}
|
||||
{ this.props.label }
|
||||
</div>
|
||||
</div>
|
||||
</span>;
|
||||
|
|
|
@ -45,7 +45,7 @@ export default class SpellCheckLanguagesDropdown extends React.Component<SpellCh
|
|||
SpellCheckLanguagesDropdownIState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._onSearchChange = this._onSearchChange.bind(this);
|
||||
this.onSearchChange = this.onSearchChange.bind(this);
|
||||
|
||||
this.state = {
|
||||
searchQuery: '',
|
||||
|
@ -76,10 +76,8 @@ export default class SpellCheckLanguagesDropdown extends React.Component<SpellCh
|
|||
}
|
||||
}
|
||||
|
||||
_onSearchChange(search) {
|
||||
this.setState({
|
||||
searchQuery: search,
|
||||
});
|
||||
private onSearchChange(searchQuery: string) {
|
||||
this.setState({ searchQuery });
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -117,7 +115,7 @@ export default class SpellCheckLanguagesDropdown extends React.Component<SpellCh
|
|||
id="mx_LanguageDropdown"
|
||||
className={this.props.className}
|
||||
onOptionChange={this.props.onOptionChange}
|
||||
onSearchChange={this._onSearchChange}
|
||||
onSearchChange={this.onSearchChange}
|
||||
searchEnabled={true}
|
||||
value={value}
|
||||
label={_t("Language Dropdown")}>
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
const Spinner = ({ w = 32, h = 32, message }) => (
|
||||
<div className="mx_Spinner">
|
||||
{ message && <React.Fragment><div className="mx_Spinner_Msg">{ message }</div> </React.Fragment> }
|
||||
<div
|
||||
className="mx_Spinner_icon"
|
||||
style={{ width: w, height: h }}
|
||||
aria-label={_t("Loading...")}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Spinner.propTypes = {
|
||||
w: PropTypes.number,
|
||||
h: PropTypes.number,
|
||||
message: PropTypes.node,
|
||||
};
|
||||
|
||||
export default Spinner;
|
45
src/components/views/elements/Spinner.tsx
Normal file
45
src/components/views/elements/Spinner.tsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
Copyright 2015-2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
interface IProps {
|
||||
w?: number;
|
||||
h?: number;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export default class Spinner extends React.PureComponent<IProps> {
|
||||
public static defaultProps: Partial<IProps> = {
|
||||
w: 32,
|
||||
h: 32,
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { w, h, message } = this.props;
|
||||
return (
|
||||
<div className="mx_Spinner">
|
||||
{ message && <React.Fragment><div className="mx_Spinner_Msg">{ message }</div> </React.Fragment> }
|
||||
<div
|
||||
className="mx_Spinner_icon"
|
||||
style={{ width: w, height: h }}
|
||||
aria-label={_t("Loading...")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -37,7 +37,7 @@ export default class Spoiler extends React.Component {
|
|||
|
||||
render() {
|
||||
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
|
||||
|
|
|
@ -44,7 +44,7 @@ export default class StyledCheckbox extends React.PureComponent<IProps, IState>
|
|||
return <span className={"mx_Checkbox " + className}>
|
||||
<input id={this.id} {...otherProps} type="checkbox" />
|
||||
<label htmlFor={this.id}>
|
||||
{/* Using the div to center the image */}
|
||||
{ /* Using the div to center the image */ }
|
||||
<div className="mx_Checkbox_background">
|
||||
<img src={require("../../../../res/img/feather-customised/check.svg")} />
|
||||
</div>
|
||||
|
|
|
@ -20,6 +20,10 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
|||
|
||||
interface IProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
outlined?: boolean;
|
||||
// If true (default), the children will be contained within a <label> element
|
||||
// If false, they'll be in a div. Putting interactive components that have labels
|
||||
// themselves in labels can cause strange bugs like https://github.com/vector-im/element-web/issues/18031
|
||||
childrenInLabel?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -29,10 +33,11 @@ interface IState {
|
|||
export default class StyledRadioButton extends React.PureComponent<IProps, IState> {
|
||||
public static readonly defaultProps = {
|
||||
className: '',
|
||||
childrenInLabel: true,
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { children, className, disabled, outlined, ...otherProps } = this.props;
|
||||
const { children, className, disabled, outlined, childrenInLabel, ...otherProps } = this.props;
|
||||
const _className = classnames(
|
||||
'mx_RadioButton',
|
||||
className,
|
||||
|
@ -42,12 +47,27 @@ export default class StyledRadioButton extends React.PureComponent<IProps, IStat
|
|||
"mx_RadioButton_checked": this.props.checked,
|
||||
"mx_RadioButton_outlined": outlined,
|
||||
});
|
||||
return <label className={_className}>
|
||||
|
||||
const radioButton = <React.Fragment>
|
||||
<input type='radio' disabled={disabled} {...otherProps} />
|
||||
{/* Used to render the radio button circle */}
|
||||
{ /* Used to render the radio button circle */ }
|
||||
<div><div /></div>
|
||||
<div className="mx_RadioButton_content">{children}</div>
|
||||
<div className="mx_RadioButton_spacer" />
|
||||
</label>;
|
||||
</React.Fragment>;
|
||||
|
||||
if (childrenInLabel) {
|
||||
return <label className={_className}>
|
||||
{ radioButton }
|
||||
<div className="mx_RadioButton_content">{ children }</div>
|
||||
<div className="mx_RadioButton_spacer" />
|
||||
</label>;
|
||||
} else {
|
||||
return <div className={_className}>
|
||||
<label className="mx_RadioButton_innerLabel">
|
||||
{ radioButton }
|
||||
</label>
|
||||
<div className="mx_RadioButton_content">{ children }</div>
|
||||
<div className="mx_RadioButton_spacer" />
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ function StyledRadioGroup<T extends string>({
|
|||
};
|
||||
|
||||
return <React.Fragment>
|
||||
{definitions.map(d => <React.Fragment key={d.value}>
|
||||
{ definitions.map(d => <React.Fragment key={d.value}>
|
||||
<StyledRadioButton
|
||||
className={classNames(className, d.className)}
|
||||
onChange={_onChange}
|
||||
|
@ -65,7 +65,7 @@ function StyledRadioGroup<T extends string>({
|
|||
{ d.label }
|
||||
</StyledRadioButton>
|
||||
{ d.description ? <span>{ d.description }</span> : null }
|
||||
</React.Fragment>)}
|
||||
</React.Fragment>) }
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
|
|
91
src/components/views/elements/TagComposer.tsx
Normal file
91
src/components/views/elements/TagComposer.tsx
Normal file
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ChangeEvent, FormEvent } from "react";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import Field from "./Field";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
|
||||
interface IProps {
|
||||
tags: string[];
|
||||
onAdd: (tag: string) => void;
|
||||
onRemove: (tag: string) => void;
|
||||
disabled?: boolean;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
newTag: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple, controlled, composer for entering string tags. Contains a simple
|
||||
* input, add button, and per-tag remove button.
|
||||
*/
|
||||
@replaceableComponent("views.elements.TagComposer")
|
||||
export default class TagComposer extends React.PureComponent<IProps, IState> {
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
newTag: "",
|
||||
};
|
||||
}
|
||||
|
||||
private onInputChange = (ev: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ newTag: ev.target.value });
|
||||
};
|
||||
|
||||
private onAdd = (ev: FormEvent) => {
|
||||
ev.preventDefault();
|
||||
if (!this.state.newTag) return;
|
||||
|
||||
this.props.onAdd(this.state.newTag);
|
||||
this.setState({ newTag: "" });
|
||||
};
|
||||
|
||||
private onRemove(tag: string) {
|
||||
// We probably don't need to proxy this, but for
|
||||
// sanity of `this` we'll do so anyways.
|
||||
this.props.onRemove(tag);
|
||||
}
|
||||
|
||||
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) => (<div className='mx_TagComposer_tag' key={i}>
|
||||
<span>{ t }</span>
|
||||
<AccessibleButton onClick={this.onRemove.bind(this, t)} disabled={this.props.disabled} />
|
||||
</div>)) }
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
|
@ -152,7 +152,7 @@ export default class TagTile extends React.Component {
|
|||
"mx_TagTile_badge": true,
|
||||
"mx_TagTile_badgeHighlight": badge.highlight,
|
||||
});
|
||||
badgeElement = (<div className={badgeClasses}>{FormattingUtils.formatCount(badge.count)}</div>);
|
||||
badgeElement = (<div className={badgeClasses}>{ FormattingUtils.formatCount(badge.count) }</div>);
|
||||
}
|
||||
|
||||
const contextButton = this.state.hover || this.props.menuDisplayed ?
|
||||
|
@ -161,7 +161,7 @@ export default class TagTile extends React.Component {
|
|||
onClick={this.openMenu}
|
||||
inputRef={this.props.contextMenuButtonRef}
|
||||
>
|
||||
{"\u00B7\u00B7\u00B7"}
|
||||
{ "\u00B7\u00B7\u00B7" }
|
||||
</AccessibleButton> : <div ref={this.props.contextMenuButtonRef} />;
|
||||
|
||||
const AccessibleTooltipButton = sdk.getComponent("elements.AccessibleTooltipButton");
|
||||
|
@ -184,8 +184,8 @@ export default class TagTile extends React.Component {
|
|||
width={avatarSize}
|
||||
height={avatarSize}
|
||||
/>
|
||||
{contextButton}
|
||||
{badgeElement}
|
||||
{ contextButton }
|
||||
{ badgeElement }
|
||||
</div>
|
||||
</AccessibleTooltipButton>;
|
||||
}
|
||||
|
|
|
@ -51,12 +51,12 @@ export default class TextWithTooltip extends React.Component {
|
|||
|
||||
return (
|
||||
<span {...props} onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} className={className}>
|
||||
{children}
|
||||
{this.state.hover && <Tooltip
|
||||
{ children }
|
||||
{ this.state.hover && <Tooltip
|
||||
{...tooltipProps}
|
||||
label={tooltip}
|
||||
tooltipClassName={tooltipClass}
|
||||
className={"mx_TextWithTooltip_tooltip"}
|
||||
className="mx_TextWithTooltip_tooltip"
|
||||
/> }
|
||||
</span>
|
||||
);
|
||||
|
|
|
@ -147,16 +147,16 @@ export default function withValidation<T = undefined, D = void>({
|
|||
let details;
|
||||
if (results && results.length) {
|
||||
details = <ul className="mx_Validation_details">
|
||||
{results.map(result => {
|
||||
{ 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}
|
||||
{ result.text }
|
||||
</li>;
|
||||
})}
|
||||
}) }
|
||||
</ul>;
|
||||
}
|
||||
|
||||
|
@ -165,14 +165,14 @@ export default function withValidation<T = undefined, 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);
|
||||
summary = <div className="mx_Validation_description">{content}</div>;
|
||||
summary = <div className="mx_Validation_description">{ content }</div>;
|
||||
}
|
||||
|
||||
let feedback;
|
||||
if (summary || details) {
|
||||
feedback = <div className="mx_Validation">
|
||||
{summary}
|
||||
{details}
|
||||
{ summary }
|
||||
{ details }
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue