Merge remote-tracking branch 'origin/release-v3.43.0'

# Conflicts:
#	CHANGELOG.md
#	package.json
#	src/components/views/messages/MessageActionBar.tsx
This commit is contained in:
Michael Telatynski 2022-04-26 11:49:16 +01:00
commit 6b0156d876
680 changed files with 12433 additions and 5108 deletions

View file

@ -17,7 +17,6 @@ limitations under the License.
import React, { ReactNode } from "react";
import PlayPauseButton from "./PlayPauseButton";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { formatBytes } from "../../../utils/FormattingUtils";
import DurationClock from "./DurationClock";
import { _t } from "../../../languageHandler";
@ -25,7 +24,6 @@ import SeekBar from "./SeekBar";
import PlaybackClock from "./PlaybackClock";
import AudioPlayerBase from "./AudioPlayerBase";
@replaceableComponent("views.audio_messages.AudioPlayer")
export default class AudioPlayer extends AudioPlayerBase {
protected renderFileSize(): string {
const bytes = this.props.playback.sizeBytes;

View file

@ -19,7 +19,6 @@ import { logger } from "matrix-js-sdk/src/logger";
import { Playback, PlaybackState } from "../../../audio/Playback";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { _t } from "../../../languageHandler";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
@ -39,7 +38,6 @@ interface IState {
error?: boolean;
}
@replaceableComponent("views.audio_messages.AudioPlayerBase")
export default abstract class AudioPlayerBase<T extends IProps = IProps> extends React.PureComponent<T, IState> {
protected seekRef: RefObject<SeekBar> = createRef();
protected playPauseRef: RefObject<PlayPauseButton> = createRef();

View file

@ -17,7 +17,6 @@ limitations under the License.
import React, { HTMLProps } from "react";
import { formatSeconds } from "../../../DateUtils";
import { replaceableComponent } from "../../../utils/replaceableComponent";
export interface IProps extends Pick<HTMLProps<HTMLSpanElement>, "aria-live"> {
seconds: number;
@ -27,7 +26,6 @@ export interface IProps extends Pick<HTMLProps<HTMLSpanElement>, "aria-live"> {
* Simply converts seconds into minutes and seconds. Note that hours will not be
* displayed, making it possible to see "82:29".
*/
@replaceableComponent("views.audio_messages.Clock")
export default class Clock extends React.Component<IProps> {
public constructor(props) {
super(props);

View file

@ -16,7 +16,6 @@ limitations under the License.
import React from "react";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import Clock from "./Clock";
import { Playback } from "../../../audio/Playback";
@ -31,7 +30,6 @@ interface IState {
/**
* A clock which shows a clip's maximum duration.
*/
@replaceableComponent("views.audio_messages.DurationClock")
export default class DurationClock extends React.PureComponent<IProps, IState> {
public constructor(props) {
super(props);

View file

@ -17,7 +17,6 @@ limitations under the License.
import React from "react";
import { IRecordingUpdate, VoiceRecording } from "../../../audio/VoiceRecording";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import Clock from "./Clock";
import { MarkedExecution } from "../../../utils/MarkedExecution";
@ -32,7 +31,6 @@ interface IState {
/**
* A clock for a live recording.
*/
@replaceableComponent("views.audio_messages.LiveRecordingClock")
export default class LiveRecordingClock extends React.PureComponent<IProps, IState> {
private seconds = 0;
private scheduledUpdate = new MarkedExecution(

View file

@ -17,7 +17,6 @@ limitations under the License.
import React from "react";
import { IRecordingUpdate, RECORDING_PLAYBACK_SAMPLES, VoiceRecording } from "../../../audio/VoiceRecording";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { arrayFastResample, arraySeed } from "../../../utils/arrays";
import Waveform from "./Waveform";
import { MarkedExecution } from "../../../utils/MarkedExecution";
@ -33,7 +32,6 @@ interface IState {
/**
* A waveform which shows the waveform of a live recording
*/
@replaceableComponent("views.audio_messages.LiveRecordingWaveform")
export default class LiveRecordingWaveform extends React.PureComponent<IProps, IState> {
public static defaultProps = {
progress: 1,

View file

@ -17,7 +17,6 @@ limitations under the License.
import React, { ReactNode } from "react";
import classNames from "classnames";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { _t } from "../../../languageHandler";
import { Playback, PlaybackState } from "../../../audio/Playback";
@ -35,7 +34,6 @@ interface IProps extends Omit<React.ComponentProps<typeof AccessibleTooltipButto
* Displays a play/pause button (activating the play/pause function of the recorder)
* to be displayed in reference to a recording.
*/
@replaceableComponent("views.audio_messages.PlayPauseButton")
export default class PlayPauseButton extends React.PureComponent<IProps> {
public constructor(props) {
super(props);

View file

@ -16,7 +16,6 @@ limitations under the License.
import React from "react";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import Clock from "./Clock";
import { Playback, PlaybackState } from "../../../audio/Playback";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
@ -39,7 +38,6 @@ interface IState {
/**
* A clock for a playback of a recording.
*/
@replaceableComponent("views.audio_messages.PlaybackClock")
export default class PlaybackClock extends React.PureComponent<IProps, IState> {
public constructor(props) {
super(props);

View file

@ -16,7 +16,6 @@ limitations under the License.
import React from "react";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { arraySeed, arrayTrimFill } from "../../../utils/arrays";
import Waveform from "./Waveform";
import { Playback, PLAYBACK_WAVEFORM_SAMPLES } from "../../../audio/Playback";
@ -34,7 +33,6 @@ interface IState {
/**
* A waveform which shows the waveform of a previously recorded recording
*/
@replaceableComponent("views.audio_messages.PlaybackWaveform")
export default class PlaybackWaveform extends React.PureComponent<IProps, IState> {
public constructor(props) {
super(props);

View file

@ -18,7 +18,6 @@ import React, { ReactNode } from "react";
import PlayPauseButton from "./PlayPauseButton";
import PlaybackClock from "./PlaybackClock";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import AudioPlayerBase, { IProps as IAudioPlayerBaseProps } from "./AudioPlayerBase";
import SeekBar from "./SeekBar";
import PlaybackWaveform from "./PlaybackWaveform";
@ -30,7 +29,6 @@ interface IProps extends IAudioPlayerBaseProps {
withWaveform?: boolean;
}
@replaceableComponent("views.audio_messages.RecordingPlayback")
export default class RecordingPlayback extends AudioPlayerBase<IProps> {
// This component is rendered in two ways: the composer and timeline. They have different
// rendering properties (specifically the difference of a waveform or not).

View file

@ -17,7 +17,6 @@ limitations under the License.
import React, { ChangeEvent, CSSProperties, ReactNode } from "react";
import { Playback, PlaybackState } from "../../../audio/Playback";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { MarkedExecution } from "../../../utils/MarkedExecution";
import { percentageOf } from "../../../utils/numbers";
@ -43,7 +42,6 @@ interface ISeekCSS extends CSSProperties {
const ARROW_SKIP_SECONDS = 5; // arbitrary
@replaceableComponent("views.audio_messages.SeekBar")
export default class SeekBar extends React.PureComponent<IProps, IState> {
// We use an animation frame request to avoid overly spamming prop updates, even if we aren't
// really using anything demanding on the CSS front.

View file

@ -17,8 +17,6 @@ limitations under the License.
import React, { CSSProperties } from "react";
import classNames from "classnames";
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface WaveformCSSProperties extends CSSProperties {
'--barHeight': number;
}
@ -39,7 +37,6 @@ interface IState {
* For CSS purposes, a mx_Waveform_bar_100pct class is added when the bar should be
* "filled", as a demonstration of the progress property.
*/
@replaceableComponent("views.audio_messages.Waveform")
export default class Waveform extends React.PureComponent<IProps, IState> {
public static defaultProps = {
progress: 1,

View file

@ -16,9 +16,6 @@ limitations under the License.
import React from 'react';
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.auth.AuthBody")
export default class AuthBody extends React.PureComponent {
public render(): React.ReactNode {
return <div className="mx_AuthBody">

View file

@ -19,9 +19,7 @@ limitations under the License.
import React from 'react';
import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.auth.AuthFooter")
export default class AuthFooter extends React.Component {
public render(): React.ReactNode {
return (

View file

@ -17,7 +17,6 @@ limitations under the License.
import React from 'react';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import AuthHeaderLogo from "./AuthHeaderLogo";
import LanguageSelector from "./LanguageSelector";
@ -25,7 +24,6 @@ interface IProps {
disableLanguageSelector?: boolean;
}
@replaceableComponent("views.auth.AuthHeader")
export default class AuthHeader extends React.Component<IProps> {
public render(): React.ReactNode {
return (

View file

@ -16,9 +16,6 @@ limitations under the License.
import React from 'react';
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.auth.AuthHeaderLogo")
export default class AuthHeaderLogo extends React.PureComponent {
public render(): React.ReactNode {
return <div className="mx_AuthHeaderLogo">

View file

@ -18,10 +18,8 @@ limitations under the License.
import React from 'react';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import AuthFooter from "./AuthFooter";
@replaceableComponent("views.auth.AuthPage")
export default class AuthPage extends React.PureComponent {
public render(): React.ReactNode {
return (

View file

@ -18,7 +18,6 @@ import React, { createRef } from 'react';
import { logger } from "matrix-js-sdk/src/logger";
import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent";
const DIV_ID = 'mx_recaptcha';
@ -35,7 +34,6 @@ interface ICaptchaFormState {
/**
* A pure UI component which displays a captcha form.
*/
@replaceableComponent("views.auth.CaptchaForm")
export default class CaptchaForm extends React.Component<ICaptchaFormProps, ICaptchaFormState> {
static defaultProps = {
onCaptchaResponse: () => {},

View file

@ -16,9 +16,6 @@ limitations under the License.
import React from 'react';
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.auth.CompleteSecurityBody")
export default class CompleteSecurityBody extends React.PureComponent {
public render(): React.ReactNode {
return <div className="mx_CompleteSecurityBody">

View file

@ -19,7 +19,6 @@ import React from 'react';
import { COUNTRIES, getEmojiFlag, PhoneNumberCountryDefinition } from '../../../phonenumber';
import SdkConfig from "../../../SdkConfig";
import { _t } from "../../../languageHandler";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import Dropdown from "../elements/Dropdown";
const COUNTRIES_BY_ISO2 = {};
@ -53,7 +52,6 @@ interface IState {
defaultCountry: PhoneNumberCountryDefinition;
}
@replaceableComponent("views.auth.CountryDropdown")
export default class CountryDropdown extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);

View file

@ -16,7 +16,6 @@ limitations under the License.
import React, { PureComponent, RefCallback, RefObject } from "react";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import Field, { IInputProps } from "../elements/Field";
import { _t, _td } from "../../../languageHandler";
import withValidation, { IFieldState, IValidationResult } from "../elements/Validation";
@ -39,7 +38,6 @@ interface IProps extends Omit<IInputProps, "onValidate"> {
onValidate?(result: IValidationResult): void;
}
@replaceableComponent("views.auth.EmailField")
class EmailField extends PureComponent<IProps> {
static defaultProps = {
label: _td("Email"),

View file

@ -24,7 +24,6 @@ import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from "../elements/AccessibleButton";
import Spinner from "../elements/Spinner";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { LocalisedPolicy, Policies } from '../../../Terms';
import Field from '../elements/Field';
import CaptchaForm from "./CaptchaForm";
@ -93,7 +92,6 @@ interface IPasswordAuthEntryState {
password: string;
}
@replaceableComponent("views.auth.PasswordAuthEntry")
export class PasswordAuthEntry extends React.Component<IAuthEntryProps, IPasswordAuthEntryState> {
static LOGIN_TYPE = AuthType.Password;
@ -191,7 +189,6 @@ interface IRecaptchaAuthEntryProps extends IAuthEntryProps {
}
/* eslint-enable camelcase */
@replaceableComponent("views.auth.RecaptchaAuthEntry")
export class RecaptchaAuthEntry extends React.Component<IRecaptchaAuthEntryProps> {
static LOGIN_TYPE = AuthType.Recaptcha;
@ -262,7 +259,6 @@ interface ITermsAuthEntryState {
errorText?: string;
}
@replaceableComponent("views.auth.TermsAuthEntry")
export class TermsAuthEntry extends React.Component<ITermsAuthEntryProps, ITermsAuthEntryState> {
static LOGIN_TYPE = AuthType.Terms;
@ -409,7 +405,6 @@ interface IEmailIdentityAuthEntryProps extends IAuthEntryProps {
};
}
@replaceableComponent("views.auth.EmailIdentityAuthEntry")
export class EmailIdentityAuthEntry extends React.Component<IEmailIdentityAuthEntryProps> {
static LOGIN_TYPE = AuthType.Email;
@ -472,7 +467,6 @@ interface IMsisdnAuthEntryState {
errorText: string;
}
@replaceableComponent("views.auth.MsisdnAuthEntry")
export class MsisdnAuthEntry extends React.Component<IMsisdnAuthEntryProps, IMsisdnAuthEntryState> {
static LOGIN_TYPE = AuthType.Msisdn;
@ -623,7 +617,6 @@ interface ISSOAuthEntryState {
attemptFailed: boolean;
}
@replaceableComponent("views.auth.SSOAuthEntry")
export class SSOAuthEntry extends React.Component<ISSOAuthEntryProps, ISSOAuthEntryState> {
static LOGIN_TYPE = AuthType.Sso;
static UNSTABLE_LOGIN_TYPE = AuthType.SsoUnstable;
@ -743,7 +736,6 @@ export class SSOAuthEntry extends React.Component<ISSOAuthEntryProps, ISSOAuthEn
}
}
@replaceableComponent("views.auth.FallbackAuthEntry")
export class FallbackAuthEntry extends React.Component<IAuthEntryProps> {
private popupWindow: Window;
private fallbackButton = createRef<HTMLButtonElement>();

View file

@ -16,7 +16,6 @@ limitations under the License.
import React, { PureComponent, RefCallback, RefObject } from "react";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import Field, { IInputProps } from "../elements/Field";
import withValidation, { IFieldState, IValidationResult } from "../elements/Validation";
import { _t, _td } from "../../../languageHandler";
@ -35,7 +34,6 @@ interface IProps extends Omit<IInputProps, "onValidate"> {
onValidate?(result: IValidationResult);
}
@replaceableComponent("views.auth.EmailField")
class PassphraseConfirmField extends PureComponent<IProps> {
static defaultProps = {
label: _td("Confirm password"),

View file

@ -22,7 +22,6 @@ import SdkConfig from "../../../SdkConfig";
import withValidation, { IFieldState, IValidationResult } from "../elements/Validation";
import { _t, _td } from "../../../languageHandler";
import Field, { IInputProps } from "../elements/Field";
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps extends Omit<IInputProps, "onValidate"> {
autoFocus?: boolean;
@ -41,7 +40,6 @@ interface IProps extends Omit<IInputProps, "onValidate"> {
onValidate?(result: IValidationResult);
}
@replaceableComponent("views.auth.PassphraseField")
class PassphraseField extends PureComponent<IProps> {
static defaultProps = {
label: _td("Password"),

View file

@ -24,7 +24,6 @@ import AccessibleButton from "../elements/AccessibleButton";
import withValidation, { IValidationResult } from "../elements/Validation";
import Field from "../elements/Field";
import CountryDropdown from "./CountryDropdown";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import EmailField from "./EmailField";
// For validating phone numbers without country codes
@ -66,7 +65,6 @@ enum LoginField {
* A pure UI component which displays a username/password form.
* The email/username/phone fields are fully-controlled, the password field is not.
*/
@replaceableComponent("views.auth.PasswordLogin")
export default class PasswordLogin extends React.PureComponent<IProps, IState> {
static defaultProps = {
onUsernameChanged: function() {},

View file

@ -31,7 +31,6 @@ import EmailField from "./EmailField";
import PassphraseField from "./PassphraseField";
import Field from '../elements/Field';
import RegistrationEmailPromptDialog from '../dialogs/RegistrationEmailPromptDialog';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import CountryDropdown from "./CountryDropdown";
import PassphraseConfirmField from "./PassphraseConfirmField";
@ -92,7 +91,6 @@ interface IState {
/*
* A pure UI component which displays a registration form.
*/
@replaceableComponent("views.auth.RegistrationForm")
export default class RegistrationForm extends React.PureComponent<IProps, IState> {
static defaultProps = {
onValidationChange: logger.error,

View file

@ -17,14 +17,14 @@ limitations under the License.
import React from 'react';
import classNames from "classnames";
import * as sdk from "../../../index";
import SdkConfig from '../../../SdkConfig';
import AuthPage from "./AuthPage";
import { _td } from "../../../languageHandler";
import SettingsStore from "../../../settings/SettingsStore";
import { UIFeature } from "../../../settings/UIFeature";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import LanguageSelector from "./LanguageSelector";
import EmbeddedPage from "../../structures/EmbeddedPage";
import { MATRIX_LOGO_HTML } from "../../structures/static-page-vars";
// translatable strings for Welcome pages
_td("Sign in with SSO");
@ -33,12 +33,8 @@ interface IProps {
}
@replaceableComponent("views.auth.Welcome")
export default class Welcome extends React.PureComponent<IProps> {
public render(): React.ReactNode {
// FIXME: Using an import will result in wrench-element-tests failures
const EmbeddedPage = sdk.getComponent("structures.EmbeddedPage");
const pagesConfig = SdkConfig.getObject("embedded_pages");
let pageUrl = null;
if (pagesConfig) {
@ -59,6 +55,8 @@ export default class Welcome extends React.PureComponent<IProps> {
replaceMap={{
"$riot:ssoUrl": "#/start_sso",
"$riot:casUrl": "#/start_cas",
"$matrixLogo": MATRIX_LOGO_HTML,
"[matrix]": MATRIX_LOGO_HTML,
}}
/>
<LanguageSelector />

View file

@ -32,7 +32,6 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { _t } from "../../../languageHandler";
import TextWithTooltip from "../elements/TextWithTooltip";
import DMRoomMap from "../../../utils/DMRoomMap";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { IOOBData } from "../../../stores/ThreepidInviteStore";
import TooltipTarget from "../elements/TooltipTarget";
@ -78,7 +77,6 @@ function tooltipText(variant: Icon) {
}
}
@replaceableComponent("views.avatars.DecoratedRoomAvatar")
export default class DecoratedRoomAvatar extends React.PureComponent<IProps, IState> {
private _dmUser: User;
private isUnmounted = false;

View file

@ -23,9 +23,8 @@ import { logger } from "matrix-js-sdk/src/logger";
import dis from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
import BaseAvatar from "./BaseAvatar";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media";
import { CardContext } from '../right_panel/BaseCard';
import { CardContext } from '../right_panel/context';
import UserIdentifierCustomisations from '../../../customisations/UserIdentifier';
import SettingsStore from "../../../settings/SettingsStore";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
@ -52,7 +51,6 @@ interface IState {
imageUrl?: string;
}
@replaceableComponent("views.avatars.MemberAvatar")
export default class MemberAvatar extends React.PureComponent<IProps, IState> {
public static defaultProps = {
width: 40,

View file

@ -28,7 +28,6 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg';
import Modal from '../../../Modal';
import * as Avatar from '../../../Avatar';
import DMRoomMap from "../../../utils/DMRoomMap";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media";
import { IOOBData } from '../../../stores/ThreepidInviteStore';
@ -52,7 +51,6 @@ interface IState {
urls: string[];
}
@replaceableComponent("views.avatars.RoomAvatar")
export default class RoomAvatar extends React.Component<IProps, IState> {
public static defaultProps = {
width: 36,

View file

@ -0,0 +1,65 @@
/*
Copyright 2022 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, { useContext } from 'react';
import maplibregl from 'maplibre-gl';
import {
Beacon,
BeaconEvent,
} from 'matrix-js-sdk/src/matrix';
import { LocationAssetType } from 'matrix-js-sdk/src/@types/location';
import MatrixClientContext from '../../../contexts/MatrixClientContext';
import { useEventEmitterState } from '../../../hooks/useEventEmitter';
import SmartMarker from '../location/SmartMarker';
interface Props {
map: maplibregl.Map;
beacon: Beacon;
}
/**
* Updates a map SmartMarker with latest location from given beacon
*/
const BeaconMarker: React.FC<Props> = ({ map, beacon }) => {
const latestLocationState = useEventEmitterState(
beacon,
BeaconEvent.LocationUpdate,
() => beacon.latestLocationState,
);
const matrixClient = useContext(MatrixClientContext);
const room = matrixClient.getRoom(beacon.roomId);
if (!latestLocationState || !beacon.isLive) {
return null;
}
const geoUri = latestLocationState?.uri;
const markerRoomMember = beacon.beaconInfo.assetType === LocationAssetType.Self ?
room.getMember(beacon.beaconInfoOwner) :
undefined;
return <SmartMarker
map={map}
id={beacon.identifier}
geoUri={geoUri}
roomMember={markerRoomMember}
useMemberColor
/>;
};
export default BeaconMarker;

View file

@ -0,0 +1,84 @@
/*
Copyright 2022 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, { HTMLProps } from 'react';
import classNames from 'classnames';
import { Beacon } from 'matrix-js-sdk/src/matrix';
import StyledLiveBeaconIcon from './StyledLiveBeaconIcon';
import { _t } from '../../../languageHandler';
import LiveTimeRemaining from './LiveTimeRemaining';
import { BeaconDisplayStatus } from './displayStatus';
import { getBeaconExpiryTimestamp } from '../../../utils/beacon';
import { formatTime } from '../../../DateUtils';
interface Props {
displayStatus: BeaconDisplayStatus;
displayLiveTimeRemaining?: boolean;
beacon?: Beacon;
label?: string;
}
const BeaconExpiryTime: React.FC<{ beacon: Beacon }> = ({ beacon }) => {
const expiryTime = formatTime(new Date(getBeaconExpiryTimestamp(beacon)));
return <span className='mx_BeaconStatus_expiryTime'>{ _t('Live until %(expiryTime)s', { expiryTime }) }</span>;
};
const BeaconStatus: React.FC<Props & HTMLProps<HTMLDivElement>> =
({
beacon,
displayStatus,
displayLiveTimeRemaining,
label,
className,
children,
...rest
}) => {
const isIdle = displayStatus === BeaconDisplayStatus.Loading ||
displayStatus === BeaconDisplayStatus.Stopped;
return <div
{...rest}
className={classNames('mx_BeaconStatus', `mx_BeaconStatus_${displayStatus}`, className)}
>
<StyledLiveBeaconIcon
className='mx_BeaconStatus_icon'
withError={displayStatus === BeaconDisplayStatus.Error}
isIdle={isIdle}
/>
<div className='mx_BeaconStatus_description'>
{ displayStatus === BeaconDisplayStatus.Loading && <span>{ _t('Loading live location...') }</span> }
{ displayStatus === BeaconDisplayStatus.Stopped && <span>{ _t('Live location ended') }</span> }
{ displayStatus === BeaconDisplayStatus.Error && <span>{ _t('Live location error') }</span> }
{ displayStatus === BeaconDisplayStatus.Active && beacon && <>
<>
{ label }
{ displayLiveTimeRemaining ?
<LiveTimeRemaining beacon={beacon} /> :
<BeaconExpiryTime beacon={beacon} />
}
</>
</>
}
</div>
{ children }
</div>;
};
export default BeaconStatus;

View file

@ -0,0 +1,116 @@
/*
Copyright 2022 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 { MatrixClient } from 'matrix-js-sdk/src/client';
import {
Beacon,
Room,
} from 'matrix-js-sdk/src/matrix';
import maplibregl from 'maplibre-gl';
import { useLiveBeacons } from '../../../utils/beacon/useLiveBeacons';
import MatrixClientContext from '../../../contexts/MatrixClientContext';
import BaseDialog from "../dialogs/BaseDialog";
import { IDialogProps } from "../dialogs/IDialogProps";
import Map from '../location/Map';
import ZoomButtons from '../location/ZoomButtons';
import BeaconMarker from './BeaconMarker';
import { Bounds, getBeaconBounds } from '../../../utils/beacon/bounds';
import { getGeoUri } from '../../../utils/beacon';
import { Icon as LocationIcon } from '../../../../res/img/element-icons/location.svg';
import { _t } from '../../../languageHandler';
import AccessibleButton from '../elements/AccessibleButton';
interface IProps extends IDialogProps {
roomId: Room['roomId'];
matrixClient: MatrixClient;
// open the map centered on this beacon's location
focusBeacon?: Beacon;
}
const getBoundsCenter = (bounds: Bounds): string | undefined => {
if (!bounds) {
return;
}
return getGeoUri({
latitude: (bounds.north + bounds.south) / 2,
longitude: (bounds.east + bounds.west) / 2,
timestamp: Date.now(),
});
};
/**
* Dialog to view live beacons maximised
*/
const BeaconViewDialog: React.FC<IProps> = ({
focusBeacon,
roomId,
matrixClient,
onFinished,
}) => {
const liveBeacons = useLiveBeacons(roomId, matrixClient);
const bounds = getBeaconBounds(liveBeacons);
const centerGeoUri = focusBeacon?.latestLocationState?.uri || getBoundsCenter(bounds);
return (
<BaseDialog
className='mx_BeaconViewDialog'
onFinished={onFinished}
fixedWidth={false}
>
<MatrixClientContext.Provider value={matrixClient}>
{ !!bounds ? <Map
id='mx_BeaconViewDialog'
bounds={bounds}
centerGeoUri={centerGeoUri}
interactive
className="mx_BeaconViewDialog_map"
>
{
({ map }: { map: maplibregl.Map}) =>
<>
{ liveBeacons.map(beacon => <BeaconMarker
key={beacon.identifier}
map={map}
beacon={beacon}
/>) }
<ZoomButtons map={map} />
</>
}
</Map> :
<div
data-test-id='beacon-view-dialog-map-fallback'
className='mx_BeaconViewDialog_map mx_BeaconViewDialog_mapFallback'
>
<LocationIcon className='mx_BeaconViewDialog_mapFallbackIcon' />
<span className='mx_BeaconViewDialog_mapFallbackMessage'>{ _t('No live locations') }</span>
<AccessibleButton
kind='primary'
onClick={onFinished}
data-test-id='beacon-view-dialog-fallback-close'
>
{ _t('Close') }
</AccessibleButton>
</div>
}
</MatrixClientContext.Provider>
</BaseDialog>
);
};
export default BeaconViewDialog;

View file

@ -0,0 +1,75 @@
/*
Copyright 2022 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, { useCallback, useEffect, useState } from 'react';
import { BeaconEvent, Beacon } from 'matrix-js-sdk/src/matrix';
import { formatDuration } from '../../../DateUtils';
import { useEventEmitterState } from '../../../hooks/useEventEmitter';
import { useInterval } from '../../../hooks/useTimeout';
import { _t } from '../../../languageHandler';
import { getBeaconMsUntilExpiry } from '../../../utils/beacon';
const MINUTE_MS = 60000;
const HOUR_MS = MINUTE_MS * 60;
const getUpdateInterval = (ms: number) => {
// every 10 mins when more than an hour
if (ms > HOUR_MS) {
return MINUTE_MS * 10;
}
// every minute when more than a minute
if (ms > MINUTE_MS) {
return MINUTE_MS;
}
// otherwise every second
return 1000;
};
const useMsRemaining = (beacon: Beacon): number => {
const beaconInfo = useEventEmitterState(
beacon,
BeaconEvent.Update,
() => beacon.beaconInfo,
);
const [msRemaining, setMsRemaining] = useState(() => getBeaconMsUntilExpiry(beaconInfo));
useEffect(() => {
setMsRemaining(getBeaconMsUntilExpiry(beaconInfo));
}, [beaconInfo]);
const updateMsRemaining = useCallback(() => {
const ms = getBeaconMsUntilExpiry(beaconInfo);
setMsRemaining(ms);
}, [beaconInfo]);
useInterval(updateMsRemaining, getUpdateInterval(msRemaining));
return msRemaining;
};
const LiveTimeRemaining: React.FC<{ beacon: Beacon }> = ({ beacon }) => {
const msRemaining = useMsRemaining(beacon);
const timeRemaining = formatDuration(msRemaining);
const liveTimeRemaining = _t(`%(timeRemaining)s left`, { timeRemaining });
return <span
data-test-id='room-live-share-expiry'
className="mx_LiveTimeRemaining"
>{ liveTimeRemaining }</span>;
};
export default LiveTimeRemaining;

View file

@ -0,0 +1,89 @@
/*
Copyright 2022 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 { Beacon } from 'matrix-js-sdk/src/matrix';
import React, { HTMLProps } from 'react';
import { _t } from '../../../languageHandler';
import { useOwnLiveBeacons } from '../../../utils/beacon';
import BeaconStatus from './BeaconStatus';
import { BeaconDisplayStatus } from './displayStatus';
import AccessibleButton from '../elements/AccessibleButton';
interface Props {
displayStatus: BeaconDisplayStatus;
beacon?: Beacon;
}
/**
* Wraps BeaconStatus with more capabilities
* for errors and actions available for users own live beacons
*/
const OwnBeaconStatus: React.FC<Props & HTMLProps<HTMLDivElement>> = ({
beacon, displayStatus, className, ...rest
}) => {
const {
hasWireError,
hasStopSharingError,
stoppingInProgress,
onStopSharing,
onResetWireError,
} = useOwnLiveBeacons([beacon?.identifier]);
// combine display status with errors that only occur for user's own beacons
const ownDisplayStatus = hasWireError || hasStopSharingError ?
BeaconDisplayStatus.Error :
displayStatus;
return <BeaconStatus
className='mx_MBeaconBody_chin'
beacon={beacon}
displayStatus={ownDisplayStatus}
label={_t('Live location enabled')}
displayLiveTimeRemaining
{...rest}
>
{ ownDisplayStatus === BeaconDisplayStatus.Active && <AccessibleButton
data-test-id='beacon-status-stop-beacon'
kind='link'
onClick={onStopSharing}
className='mx_OwnBeaconStatus_button mx_OwnBeaconStatus_destructiveButton'
disabled={stoppingInProgress}
>
{ _t('Stop') }
</AccessibleButton>
}
{ hasWireError && <AccessibleButton
data-test-id='beacon-status-reset-wire-error'
kind='link'
onClick={onResetWireError}
className='mx_OwnBeaconStatus_button mx_OwnBeaconStatus_destructiveButton'
>
{ _t('Retry') }
</AccessibleButton>
}
{ hasStopSharingError && <AccessibleButton
data-test-id='beacon-status-stop-beacon-retry'
kind='link'
onClick={onStopSharing}
className='mx_OwnBeaconStatus_button mx_OwnBeaconStatus_destructiveButton'
>
{ _t('Retry') }
</AccessibleButton> }
</BeaconStatus>;
};
export default OwnBeaconStatus;

View file

@ -14,126 +14,19 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useCallback, useEffect, useState } from 'react';
import React from 'react';
import classNames from 'classnames';
import { Room, Beacon } from 'matrix-js-sdk/src/matrix';
import { Room } from 'matrix-js-sdk/src/matrix';
import { formatDuration } from '../../../DateUtils';
import { _t } from '../../../languageHandler';
import { useEventEmitterState } from '../../../hooks/useEventEmitter';
import { useInterval } from '../../../hooks/useTimeout';
import { OwnBeaconStore, OwnBeaconStoreEvent } from '../../../stores/OwnBeaconStore';
import { getBeaconMsUntilExpiry, sortBeaconsByLatestExpiry } from '../../../utils/beacon';
import { useOwnLiveBeacons } from '../../../utils/beacon';
import AccessibleButton from '../elements/AccessibleButton';
import Spinner from '../elements/Spinner';
import StyledLiveBeaconIcon from './StyledLiveBeaconIcon';
import { Icon as CloseIcon } from '../../../../res/img/image-view/close.svg';
const MINUTE_MS = 60000;
const HOUR_MS = MINUTE_MS * 60;
const getUpdateInterval = (ms: number) => {
// every 10 mins when more than an hour
if (ms > HOUR_MS) {
return MINUTE_MS * 10;
}
// every minute when more than a minute
if (ms > MINUTE_MS) {
return MINUTE_MS;
}
// otherwise every second
return 1000;
};
const useMsRemaining = (beacon: Beacon): number => {
const [msRemaining, setMsRemaining] = useState(() => getBeaconMsUntilExpiry(beacon));
useEffect(() => {
setMsRemaining(getBeaconMsUntilExpiry(beacon));
}, [beacon]);
const updateMsRemaining = useCallback(() => {
const ms = getBeaconMsUntilExpiry(beacon);
setMsRemaining(ms);
}, [beacon]);
useInterval(updateMsRemaining, getUpdateInterval(msRemaining));
return msRemaining;
};
/**
* It's technically possible to have multiple live beacons in one room
* Select the latest expiry to display,
* and kill all beacons on stop sharing
*/
type LiveBeaconsState = {
beacon?: Beacon;
onStopSharing?: () => void;
onResetWireError?: () => void;
stoppingInProgress?: boolean;
hasStopSharingError?: boolean;
hasWireError?: boolean;
};
const useLiveBeacons = (liveBeaconIds: string[], roomId: string): LiveBeaconsState => {
const [stoppingInProgress, setStoppingInProgress] = useState(false);
const [error, setError] = useState<Error>();
const hasWireError = useEventEmitterState(
OwnBeaconStore.instance,
OwnBeaconStoreEvent.WireError,
() =>
OwnBeaconStore.instance.hasWireErrors(roomId),
);
// reset stopping in progress on change in live ids
useEffect(() => {
setStoppingInProgress(false);
setError(undefined);
}, [liveBeaconIds]);
// select the beacon with latest expiry to display expiry time
const beacon = liveBeaconIds.map(beaconId => OwnBeaconStore.instance.getBeaconById(beaconId))
.sort(sortBeaconsByLatestExpiry)
.shift();
const onStopSharing = async () => {
setStoppingInProgress(true);
try {
await Promise.all(liveBeaconIds.map(beaconId => OwnBeaconStore.instance.stopBeacon(beaconId)));
} catch (error) {
// only clear loading in case of error
// to avoid flash of not-loading state
// after beacons have been stopped but we wait for sync
setError(error);
setStoppingInProgress(false);
}
};
const onResetWireError = () => {
liveBeaconIds.map(beaconId => OwnBeaconStore.instance.resetWireError(beaconId));
};
return {
onStopSharing,
onResetWireError,
beacon,
stoppingInProgress,
hasWireError,
hasStopSharingError: !!error,
};
};
const LiveTimeRemaining: React.FC<{ beacon: Beacon }> = ({ beacon }) => {
const msRemaining = useMsRemaining(beacon);
const timeRemaining = formatDuration(msRemaining);
const liveTimeRemaining = _t(`%(timeRemaining)s left`, { timeRemaining });
return <span
data-test-id='room-live-share-expiry'
className="mx_RoomLiveShareWarning_expiry"
>{ liveTimeRemaining }</span>;
};
import LiveTimeRemaining from './LiveTimeRemaining';
const getLabel = (hasWireError: boolean, hasStopSharingError: boolean): string => {
if (hasWireError) {
@ -157,7 +50,7 @@ const RoomLiveShareWarningInner: React.FC<RoomLiveShareWarningInnerProps> = ({ l
stoppingInProgress,
hasStopSharingError,
hasWireError,
} = useLiveBeacons(liveBeaconIds, roomId);
} = useOwnLiveBeacons(liveBeaconIds);
if (!beacon) {
return null;
@ -188,6 +81,7 @@ const RoomLiveShareWarningInner: React.FC<RoomLiveShareWarningInnerProps> = ({ l
{ !stoppingInProgress && !hasError && <LiveTimeRemaining beacon={beacon} /> }
<AccessibleButton
className='mx_RoomLiveShareWarning_stopButton'
data-test-id='room-live-share-primary-button'
onClick={onButtonClick}
kind='danger'

View file

@ -22,11 +22,19 @@ import { Icon as LiveLocationIcon } from '../../../../res/img/location/live-loca
interface Props extends React.SVGProps<SVGSVGElement> {
// use error styling when true
withError?: boolean;
isIdle?: boolean;
}
const StyledLiveBeaconIcon: React.FC<Props> = ({ className, withError, ...props }) =>
const StyledLiveBeaconIcon: React.FC<Props> = ({ className, withError, isIdle, ...props }) =>
<LiveLocationIcon
{...props}
className={classNames('mx_StyledLiveBeaconIcon', className, { 'mx_StyledLiveBeaconIcon_error': withError })}
className={classNames(
'mx_StyledLiveBeaconIcon',
className,
{
'mx_StyledLiveBeaconIcon_error': withError,
'mx_StyledLiveBeaconIcon_idle': isIdle,
})}
/>;
export default StyledLiveBeaconIcon;

View file

@ -0,0 +1,42 @@
/*
Copyright 2022 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 { BeaconLocationState } from "matrix-js-sdk/src/content-helpers";
export enum BeaconDisplayStatus {
Loading = 'Loading',
Error = 'Error',
Stopped = 'Stopped',
Active = 'Active',
}
export const getBeaconDisplayStatus = (
isLive: boolean,
latestLocationState?: BeaconLocationState,
error?: Error): BeaconDisplayStatus => {
if (error) {
return BeaconDisplayStatus.Error;
}
if (!isLive) {
return BeaconDisplayStatus.Stopped;
}
if (!latestLocationState) {
return BeaconDisplayStatus.Loading;
}
if (latestLocationState) {
return BeaconDisplayStatus.Active;
}
};

View file

@ -21,13 +21,11 @@ import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import { _t } from '../../../languageHandler';
import ContextMenu, { IProps as IContextMenuProps, MenuItem } from '../../structures/ContextMenu';
import CallHandler from '../../../CallHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps extends IContextMenuProps {
call: MatrixCall;
}
@replaceableComponent("views.context_menus.CallContextMenu")
export default class CallContextMenu extends React.Component<IProps> {
static propTypes = {
// js-sdk User object. Not required because it might not exist.

View file

@ -22,7 +22,6 @@ import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
import ContextMenu, { IProps as IContextMenuProps } from '../../structures/ContextMenu';
import Field from "../elements/Field";
import DialPad from '../voip/DialPad';
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps extends IContextMenuProps {
call: MatrixCall;
@ -32,7 +31,6 @@ interface IState {
value: string;
}
@replaceableComponent("views.context_menus.DialpadContextMenu")
export default class DialpadContextMenu extends React.Component<IProps, IState> {
private numberEntryFieldRef: React.RefObject<Field> = createRef();

View file

@ -16,8 +16,6 @@ limitations under the License.
import React from 'react';
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
element: React.ReactNode;
// Function to be called when the parent window is resized
@ -30,7 +28,6 @@ interface IProps {
* This component can be used to display generic HTML content in a contextual
* menu.
*/
@replaceableComponent("views.context_menus.GenericElementContextMenu")
export default class GenericElementContextMenu extends React.Component<IProps> {
constructor(props: IProps) {
super(props);

View file

@ -16,13 +16,10 @@ limitations under the License.
import React from 'react';
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
message: string;
}
@replaceableComponent("views.context_menus.GenericTextContextMenu")
export default class GenericTextContextMenu extends React.Component<IProps> {
public render(): JSX.Element {
return <div className="mx_Tooltip mx_Tooltip_visible" style={{ display: "block" }}>

View file

@ -1,6 +1,7 @@
/*
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
Copyright 2021 - 2022 Šimon Brandner <simon.bra.ag@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -15,12 +16,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ReactElement } from 'react';
import React, { createRef } from 'react';
import { EventStatus, MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { EventType, RelationType } from "matrix-js-sdk/src/@types/event";
import { Relations } from 'matrix-js-sdk/src/models/relations';
import { RoomMemberEvent } from "matrix-js-sdk/src/models/room-member";
import { M_LOCATION } from 'matrix-js-sdk/src/@types/location';
import { M_POLL_START } from "matrix-events-sdk";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
@ -30,79 +30,81 @@ import Modal from '../../../Modal';
import Resend from '../../../Resend';
import SettingsStore from '../../../settings/SettingsStore';
import { isUrlPermitted } from '../../../HtmlUtils';
import { isContentActionable } from '../../../utils/EventUtils';
import { canEditContent, canForward, editEvent, isContentActionable, isLocationEvent } from '../../../utils/EventUtils';
import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from './IconizedContextMenu';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard";
import ForwardDialog from "../dialogs/ForwardDialog";
import { ReadPinsEventId } from "../right_panel/types";
import { Action } from "../../../dispatcher/actions";
import ReportEventDialog from '../dialogs/ReportEventDialog';
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
import { ButtonEvent } from '../elements/AccessibleButton';
import { copyPlaintext, getSelectedText } from '../../../utils/strings';
import ContextMenu, { toRightOf } from '../../structures/ContextMenu';
import ReactionPicker from '../emojipicker/ReactionPicker';
import ViewSource from '../../structures/ViewSource';
import { createRedactEventDialog } from '../dialogs/ConfirmRedactDialog';
import ShareDialog from '../dialogs/ShareDialog';
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import { ChevronFace, IPosition } from '../../structures/ContextMenu';
import { IPosition, ChevronFace } from '../../structures/ContextMenu';
import RoomContext, { TimelineRenderingType } from '../../../contexts/RoomContext';
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
import EndPollDialog from '../dialogs/EndPollDialog';
import { isPollEnded } from '../messages/MPollBody';
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { GetRelationsForEvent, IEventTileOps } from "../rooms/EventTile";
import { OpenForwardDialogPayload } from "../../../dispatcher/payloads/OpenForwardDialogPayload";
import { OpenReportEventDialogPayload } from "../../../dispatcher/payloads/OpenReportEventDialogPayload";
import { createMapSiteLink } from '../../../utils/location';
export function canCancel(status: EventStatus): boolean {
return status === EventStatus.QUEUED || status === EventStatus.NOT_SENT || status === EventStatus.ENCRYPTING;
}
export interface IEventTileOps {
isWidgetHidden(): boolean;
unhideWidget(): void;
}
export interface IOperableEventTile {
getEventTileOps(): IEventTileOps;
}
interface IProps extends IPosition {
chevronFace: ChevronFace;
/* the MatrixEvent associated with the context menu */
mxEvent: MatrixEvent;
/* an optional EventTileOps implementation that can be used to unhide preview widgets */
// An optional EventTileOps implementation that can be used to unhide preview widgets
eventTileOps?: IEventTileOps;
// Callback called when the menu is dismissed
permalinkCreator?: RoomPermalinkCreator;
/* an optional function to be called when the user clicks collapse thread, if not provided hide button */
collapseReplyChain?(): void;
/* callback called when the menu is dismissed */
onFinished(): void;
/* if the menu is inside a dialog, we sometimes need to close that dialog after click (forwarding) */
// If the menu is inside a dialog, we sometimes need to close that dialog after click (forwarding)
onCloseDialog?(): void;
getRelationsForEvent?: (
eventId: string,
relationType: string,
eventType: string
) => Relations;
// True if the menu is being used as a right click menu
rightClick?: boolean;
// The Relations model from the JS SDK for reactions to `mxEvent`
reactions?: Relations;
// A permalink to the event
showPermalink?: boolean;
getRelationsForEvent?: GetRelationsForEvent;
}
interface IState {
canRedact: boolean;
canPin: boolean;
reactionPickerDisplayed: boolean;
}
@replaceableComponent("views.context_menus.MessageContextMenu")
export default class MessageContextMenu extends React.Component<IProps, IState> {
static contextType = RoomContext;
public context!: React.ContextType<typeof RoomContext>;
state = {
canRedact: false,
canPin: false,
};
private reactButtonRef = createRef<any>(); // XXX Ref to a functional component
componentDidMount() {
constructor(props: IProps) {
super(props);
this.state = {
canRedact: false,
canPin: false,
reactionPickerDisplayed: false,
};
}
public componentDidMount() {
MatrixClientPeg.get().on(RoomMemberEvent.PowerLevel, this.checkPermissions);
this.checkPermissions();
}
componentWillUnmount() {
public componentWillUnmount(): void {
const cli = MatrixClientPeg.get();
if (cli) {
cli.removeListener(RoomMemberEvent.PowerLevel, this.checkPermissions);
@ -155,9 +157,10 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
};
private onReportEventClick = (): void => {
Modal.createTrackedDialog('Report Event', '', ReportEventDialog, {
mxEvent: this.props.mxEvent,
}, 'mx_Dialog_reportEvent');
dis.dispatch<OpenReportEventDialogPayload>({
action: Action.OpenReportEventDialog,
event: this.props.mxEvent,
});
this.closeMenu();
};
@ -178,8 +181,8 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
};
private onForwardClick = (): void => {
Modal.createTrackedDialog('Forward Message', '', ForwardDialog, {
matrixClient: MatrixClientPeg.get(),
dis.dispatch<OpenForwardDialogPayload>({
action: Action.OpenForwardDialog,
event: this.props.mxEvent,
permalinkCreator: this.props.permalinkCreator,
});
@ -234,11 +237,45 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
this.closeMenu();
};
private onCopyPermalinkClick = (e: ButtonEvent): void => {
e.preventDefault(); // So that we don't open the permalink
copyPlaintext(this.getPermalink());
this.closeMenu();
};
private onCollapseReplyChainClick = (): void => {
this.props.collapseReplyChain();
this.closeMenu();
};
private onCopyClick = (): void => {
copyPlaintext(getSelectedText());
this.closeMenu();
};
private onEditClick = (): void => {
editEvent(this.props.mxEvent, this.context.timelineRenderingType, this.props.getRelationsForEvent);
this.closeMenu();
};
private onReplyClick = (): void => {
dis.dispatch({
action: 'reply_to_event',
event: this.props.mxEvent,
context: this.context.timelineRenderingType,
});
this.closeMenu();
};
private onReactClick = (): void => {
this.setState({ reactionPickerDisplayed: true });
};
private onCloseReactionPicker = (): void => {
this.setState({ reactionPickerDisplayed: false });
this.closeMenu();
};
private onEndPollClick = (): void => {
const matrixClient = MatrixClientPeg.get();
Modal.createTrackedDialog('End Poll', '', EndPollDialog, {
@ -259,11 +296,16 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
});
}
private getPermalink(): string {
if (!this.props.permalinkCreator) return;
return this.props.permalinkCreator.forEvent(this.props.mxEvent.getId());
}
private getUnsentReactions(): MatrixEvent[] {
return this.getReactions(e => e.status === EventStatus.NOT_SENT);
}
private viewInRoom = () => {
private viewInRoom = (): void => {
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
event_id: this.props.mxEvent.getId(),
@ -274,39 +316,35 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
this.closeMenu();
};
render() {
public render(): JSX.Element {
const cli = MatrixClientPeg.get();
const me = cli.getUserId();
const mxEvent = this.props.mxEvent;
const { mxEvent, rightClick, showPermalink, eventTileOps, reactions, collapseReplyChain } = this.props;
const eventStatus = mxEvent.status;
const unsentReactionsCount = this.getUnsentReactions().length;
let openInMapSiteButton: JSX.Element;
let endPollButton: JSX.Element;
let resendReactionsButton: JSX.Element;
let redactButton: JSX.Element;
let forwardButton: JSX.Element;
let pinButton: JSX.Element;
let unhidePreviewButton: JSX.Element;
let externalURLButton: JSX.Element;
let quoteButton: JSX.Element;
let collapseReplyChain: JSX.Element;
let redactItemList: JSX.Element;
const contentActionable = isContentActionable(mxEvent);
const permalink = this.getPermalink();
// status is SENT before remote-echo, null after
const isSent = !eventStatus || eventStatus === EventStatus.SENT;
if (!mxEvent.isRedacted()) {
if (unsentReactionsCount !== 0) {
resendReactionsButton = (
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconResend"
label={_t('Resend %(unsentCount)s reaction(s)', { unsentCount: unsentReactionsCount })}
onClick={this.onResendReactionsClick}
/>
);
}
const { timelineRenderingType, canReact, canSendMessages } = this.context;
const isThread = (
timelineRenderingType === TimelineRenderingType.Thread ||
timelineRenderingType === TimelineRenderingType.ThreadsList
);
const isThreadRootEvent = isThread && mxEvent?.getThread()?.rootEvent === mxEvent;
let resendReactionsButton: JSX.Element;
if (!mxEvent.isRedacted() && unsentReactionsCount !== 0) {
resendReactionsButton = (
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconResend"
label={_t('Resend %(unsentCount)s reaction(s)', { unsentCount: unsentReactionsCount })}
onClick={this.onResendReactionsClick}
/>
);
}
let redactButton: JSX.Element;
if (isSent && this.state.canRedact) {
redactButton = (
<IconizedContextMenuOption
@ -317,6 +355,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
);
}
let openInMapSiteButton: JSX.Element;
if (this.canOpenInMapSite(mxEvent)) {
const mapSiteLink = createMapSiteLink(mxEvent);
openInMapSiteButton = (
@ -336,26 +375,26 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
);
}
if (isContentActionable(mxEvent)) {
if (canForward(mxEvent)) {
forwardButton = (
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconForward"
label={_t("Forward")}
onClick={this.onForwardClick}
/>
);
}
let forwardButton: JSX.Element;
if (contentActionable && canForward(mxEvent)) {
forwardButton = (
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconForward"
label={_t("Forward")}
onClick={this.onForwardClick}
/>
);
}
if (this.state.canPin) {
pinButton = (
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconPin"
label={this.isPinned() ? _t('Unpin') : _t('Pin')}
onClick={this.onPinClick}
/>
);
}
let pinButton: JSX.Element;
if (contentActionable && this.state.canPin) {
pinButton = (
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconPin"
label={this.isPinned() ? _t('Unpin') : _t('Pin')}
onClick={this.onPinClick}
/>
);
}
let viewSourceButton: JSX.Element;
@ -369,40 +408,42 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
);
}
if (this.props.eventTileOps) {
if (this.props.eventTileOps.isWidgetHidden()) {
unhidePreviewButton = (
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconUnhidePreview"
label={_t("Show preview")}
onClick={this.onUnhidePreviewClick}
/>
);
}
let unhidePreviewButton: JSX.Element;
if (eventTileOps?.isWidgetHidden()) {
unhidePreviewButton = (
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconUnhidePreview"
label={_t("Show preview")}
onClick={this.onUnhidePreviewClick}
/>
);
}
let permalink: string | null = null;
let permalinkButton: ReactElement | null = null;
if (this.props.permalinkCreator) {
permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId());
}
permalinkButton = (
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconPermalink"
onClick={this.onPermalinkClick}
label={_t('Share')}
element="a"
{
// XXX: Typescript signature for AccessibleButton doesn't work properly for non-inputs like `a`
...{
href: permalink,
target: "_blank",
rel: "noreferrer noopener",
let permalinkButton: JSX.Element;
if (permalink) {
permalinkButton = (
<IconizedContextMenuOption
iconClassName={showPermalink
? "mx_MessageContextMenu_iconCopy"
: "mx_MessageContextMenu_iconPermalink"
}
}
/>
);
onClick={showPermalink ? this.onCopyPermalinkClick : this.onPermalinkClick}
label={showPermalink ? _t('Copy link') : _t('Share')}
element="a"
{
// XXX: Typescript signature for AccessibleButton doesn't work properly for non-inputs like `a`
...{
href: permalink,
target: "_blank",
rel: "noreferrer noopener",
}
}
/>
);
}
let endPollButton: JSX.Element;
if (this.canEndPoll(mxEvent)) {
endPollButton = (
<IconizedContextMenuOption
@ -413,7 +454,8 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
);
}
if (this.props.eventTileOps) { // this event is rendered using TextualBody
let quoteButton: JSX.Element;
if (eventTileOps) { // this event is rendered using TextualBody
quoteButton = (
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconQuote"
@ -424,7 +466,9 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
}
// Bridges can provide a 'external_url' to link back to the source.
if (typeof (mxEvent.getContent().external_url) === "string" &&
let externalURLButton: JSX.Element;
if (
typeof (mxEvent.getContent().external_url) === "string" &&
isUrlPermitted(mxEvent.getContent().external_url)
) {
externalURLButton = (
@ -445,8 +489,9 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
);
}
if (this.props.collapseReplyChain) {
collapseReplyChain = (
let collapseReplyChainButton: JSX.Element;
if (collapseReplyChain) {
collapseReplyChainButton = (
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconCollapse"
label={_t("Collapse reply thread")}
@ -466,20 +511,86 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
);
}
const { timelineRenderingType } = this.context;
const isThread = (
timelineRenderingType === TimelineRenderingType.Thread ||
timelineRenderingType === TimelineRenderingType.ThreadsList
);
const isThreadRootEvent = isThread && this.props.mxEvent.isThreadRoot;
let copyButton: JSX.Element;
if (rightClick && getSelectedText()) {
copyButton = (
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconCopy"
label={_t("Copy")}
triggerOnMouseDown={true} // We use onMouseDown so that the selection isn't cleared when we click
onClick={this.onCopyClick}
/>
);
}
const commonItemsList = (
<IconizedContextMenuOptionList>
{ isThreadRootEvent && <IconizedContextMenuOption
let editButton: JSX.Element;
if (rightClick && canEditContent(mxEvent)) {
editButton = (
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconEdit"
label={_t("Edit")}
onClick={this.onEditClick}
/>
);
}
let replyButton: JSX.Element;
if (rightClick && contentActionable && canSendMessages) {
replyButton = (
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconReply"
label={_t("Reply")}
onClick={this.onReplyClick}
/>
);
}
let reactButton;
if (rightClick && contentActionable && canReact) {
reactButton = (
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconReact"
label={_t("React")}
onClick={this.onReactClick}
inputRef={this.reactButtonRef}
/>
);
}
let viewInRoomButton: JSX.Element;
if (isThreadRootEvent) {
viewInRoomButton = (
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconViewInRoom"
label={_t("View in room")}
onClick={this.viewInRoom}
/> }
/>
);
}
let nativeItemsList: JSX.Element;
if (copyButton) {
nativeItemsList = (
<IconizedContextMenuOptionList>
{ copyButton }
</IconizedContextMenuOptionList>
);
}
let quickItemsList: JSX.Element;
if (editButton || replyButton || reactButton) {
quickItemsList = (
<IconizedContextMenuOptionList>
{ reactButton }
{ replyButton }
{ editButton }
</IconizedContextMenuOptionList>
);
}
const commonItemsList = (
<IconizedContextMenuOptionList>
{ viewInRoomButton }
{ openInMapSiteButton }
{ endPollButton }
{ quoteButton }
@ -491,10 +602,11 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
{ unhidePreviewButton }
{ viewSourceButton }
{ resendReactionsButton }
{ collapseReplyChain }
{ collapseReplyChainButton }
</IconizedContextMenuOptionList>
);
let redactItemList: JSX.Element;
if (redactButton) {
redactItemList = (
<IconizedContextMenuOptionList red>
@ -502,33 +614,40 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
</IconizedContextMenuOptionList>
);
}
let reactionPicker: JSX.Element;
if (this.state.reactionPickerDisplayed) {
const buttonRect = (this.reactButtonRef.current as HTMLElement)?.getBoundingClientRect();
reactionPicker = (
<ContextMenu
{...toRightOf(buttonRect)}
onFinished={this.closeMenu}
managed={false}
>
<ReactionPicker
mxEvent={mxEvent}
onFinished={this.onCloseReactionPicker}
reactions={reactions}
/>
</ContextMenu>
);
}
return (
<IconizedContextMenu
{...this.props}
className="mx_MessageContextMenu"
compact={true}
>
{ commonItemsList }
{ redactItemList }
</IconizedContextMenu>
<React.Fragment>
<IconizedContextMenu
{...this.props}
className="mx_MessageContextMenu"
compact={true}
>
{ nativeItemsList }
{ quickItemsList }
{ commonItemsList }
{ redactItemList }
</IconizedContextMenu>
{ reactionPicker }
</React.Fragment>
);
}
}
function canForward(event: MatrixEvent): boolean {
return !(
isLocationEvent(event) ||
M_POLL_START.matches(event.getType())
);
}
function isLocationEvent(event: MatrixEvent): boolean {
const eventType = event.getType();
return (
M_LOCATION.matches(eventType) ||
(
eventType === EventType.RoomMessage &&
M_LOCATION.matches(event.getContent().msgtype)
)
);
}

View file

@ -37,7 +37,7 @@ import Modal from "../../../Modal";
import ExportDialog from "../dialogs/ExportDialog";
import { useFeatureEnabled } from "../../../hooks/useSettings";
import { usePinnedEvents } from "../right_panel/PinnedMessagesCard";
import RoomViewStore from "../../../stores/RoomViewStore";
import { RoomViewStore } from "../../../stores/RoomViewStore";
import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases';
import { ROOM_NOTIFICATIONS_TAB } from "../dialogs/RoomSettingsDialog";
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
@ -328,7 +328,7 @@ const RoomContextMenu = ({ room, onFinished, ...props }: IProps) => {
};
const ensureViewingRoom = (ev: ButtonEvent) => {
if (RoomViewStore.getRoomId() === room.roomId) return;
if (RoomViewStore.instance.getRoomId() === room.roomId) return;
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: room.roomId,
@ -373,7 +373,7 @@ const RoomContextMenu = ({ room, onFinished, ...props }: IProps) => {
ev.stopPropagation();
Modal.createDialog(DevtoolsDialog, {
roomId: RoomViewStore.getRoomId(),
roomId: RoomViewStore.instance.getRoomId(),
}, "mx_DevtoolsDialog_wrapper");
onFinished();
}}

View file

@ -22,7 +22,6 @@ import { IProps as IContextMenuProps } from "../../structures/ContextMenu";
import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "./IconizedContextMenu";
import { _t } from "../../../languageHandler";
import {
leaveSpace,
shouldShowSpaceSettings,
showCreateNewRoom,
showCreateNewSubspace,
@ -30,6 +29,7 @@ import {
showSpacePreferences,
showSpaceSettings,
} from "../../../utils/space";
import { leaveSpace } from "../../../utils/leave-behaviour";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { ButtonEvent } from "../elements/AccessibleButton";
import defaultDispatcher from "../../../dispatcher/dispatcher";

View file

@ -19,7 +19,6 @@ import React from 'react';
import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
import { SettingLevel } from "../../../settings/SettingLevel";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseDialog from "./BaseDialog";
interface IProps {
@ -32,7 +31,6 @@ interface IProps {
onFinished: (success: boolean) => void;
}
@replaceableComponent("views.dialogs.AskInviteAnywayDialog")
export default class AskInviteAnywayDialog extends React.Component<IProps> {
private onInviteClicked = (): void => {
this.props.onInviteAnyways();

View file

@ -25,7 +25,6 @@ import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { _t } from "../../../languageHandler";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import Heading from '../typography/Heading';
import { IDialogProps } from "./IDialogProps";
import { PosthogScreenTracker, ScreenName } from "../../../PosthogTrackers";
@ -81,7 +80,6 @@ interface IProps extends IDialogProps {
* Includes a div for the title, and a keypress handler which cancels the
* dialog on escape.
*/
@replaceableComponent("views.dialogs.BaseDialog")
export default class BaseDialog extends React.Component<IProps> {
private matrixClient: MatrixClient;

View file

@ -22,7 +22,7 @@ import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from "../elements/AccessibleButton";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
import { UserTab } from "./UserSettingsDialog";
import { UserTab } from "./UserTab";
import GenericFeatureFeedbackDialog from "./GenericFeatureFeedbackDialog";
// XXX: Keep this around for re-use in future Betas

View file

@ -24,7 +24,6 @@ import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
import sendBugReport, { downloadBugReport } from '../../../rageshake/submit-rageshake';
import AccessibleButton from "../elements/AccessibleButton";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import QuestionDialog from "./QuestionDialog";
import BaseDialog from "./BaseDialog";
import Field from '../elements/Field';
@ -50,7 +49,6 @@ interface IState {
downloadProgress: string;
}
@replaceableComponent("views.dialogs.BugReportDialog")
export default class BugReportDialog extends React.Component<IProps, IState> {
private unmounted: boolean;

View file

@ -17,7 +17,6 @@ limitations under the License.
import React from 'react';
import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import ConfirmRedactDialog from './ConfirmRedactDialog';
import ErrorDialog from './ErrorDialog';
import BaseDialog from "./BaseDialog";
@ -45,7 +44,6 @@ interface IState {
*
* To avoid this, we keep the dialog open as long as /redact is in progress.
*/
@replaceableComponent("views.dialogs.ConfirmAndWaitRedactDialog")
export default class ConfirmAndWaitRedactDialog extends React.PureComponent<IProps, IState> {
constructor(props) {
super(props);

View file

@ -20,7 +20,6 @@ import React from 'react';
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import Modal from '../../../Modal';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import ErrorDialog from './ErrorDialog';
import TextInputDialog from "./TextInputDialog";
@ -31,7 +30,6 @@ interface IProps {
/*
* A dialog for confirming a redaction.
*/
@replaceableComponent("views.dialogs.ConfirmRedactDialog")
export default class ConfirmRedactDialog extends React.Component<IProps> {
render() {
return (

View file

@ -19,7 +19,6 @@ import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import classNames from "classnames";
import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import MemberAvatar from '../avatars/MemberAvatar';
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
@ -55,7 +54,6 @@ interface IState {
* to make it obvious what is going to happen.
* Also tweaks the style for 'dangerous' actions (albeit only with colour)
*/
@replaceableComponent("views.dialogs.ConfirmUserActionDialog")
export default class ConfirmUserActionDialog extends React.Component<IProps, IState> {
static defaultProps = {
danger: false,

View file

@ -17,7 +17,6 @@ limitations under the License.
import React from 'react';
import { _t } from "../../../languageHandler";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
@ -25,7 +24,6 @@ interface IProps {
onFinished: (success: boolean) => void;
}
@replaceableComponent("views.dialogs.ConfirmWipeDeviceDialog")
export default class ConfirmWipeDeviceDialog extends React.Component<IProps> {
private onConfirm = (): void => {
this.props.onFinished(true);

View file

@ -24,8 +24,7 @@ import SdkConfig from '../../../SdkConfig';
import withValidation, { IFieldState } from '../elements/Validation';
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { IOpts, privateShouldBeEncrypted } from "../../../createRoom";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { IOpts } from "../../../createRoom";
import Field from "../elements/Field";
import RoomAliasField from "../elements/RoomAliasField";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
@ -35,6 +34,7 @@ import SpaceStore from "../../../stores/spaces/SpaceStore";
import JoinRuleDropdown from "../elements/JoinRuleDropdown";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { privateShouldBeEncrypted } from "../../../utils/rooms";
interface IProps {
type?: RoomType;
@ -58,7 +58,6 @@ interface IState {
canChangeEncryption: boolean;
}
@replaceableComponent("views.dialogs.CreateRoomDialog")
export default class CreateRoomDialog extends React.Component<IProps, IState> {
private readonly supportsRestricted: boolean;
private nameField = createRef<Field>();
@ -104,7 +103,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
createOpts.preset = Preset.PublicChat;
opts.guestAccess = false;
const { alias } = this.state;
createOpts.room_alias_name = alias.substr(1, alias.indexOf(":") - 1);
createOpts.room_alias_name = alias.substring(1, alias.indexOf(":"));
} else {
// If we cannot change encryption we pass `true` for safety, the server should automatically do this for us.
opts.encryption = this.state.canChangeEncryption ? this.state.isEncrypted : true;

View file

@ -21,13 +21,13 @@ import { logger } from "matrix-js-sdk/src/logger";
import Analytics from '../../../Analytics';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import * as Lifecycle from '../../../Lifecycle';
import { _t } from '../../../languageHandler';
import InteractiveAuth, { ERROR_USER_CANCELLED } from "../../structures/InteractiveAuth";
import { DEFAULT_PHASE, PasswordAuthEntry, SSOAuthEntry } from "../auth/InteractiveAuthEntryComponents";
import StyledCheckbox from "../elements/StyledCheckbox";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseDialog from "./BaseDialog";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
interface IProps {
onFinished: (success: boolean) => void;
@ -46,7 +46,6 @@ interface IState {
continueKind: string;
}
@replaceableComponent("views.dialogs.DeactivateAccountDialog")
export default class DeactivateAccountDialog extends React.Component<IProps, IState> {
constructor(props) {
super(props);
@ -124,7 +123,7 @@ export default class DeactivateAccountDialog extends React.Component<IProps, ISt
MatrixClientPeg.get().deactivateAccount(auth, this.state.shouldErase).then(r => {
// Deactivation worked - logout & close this dialog
Analytics.trackEvent('Account', 'Deactivate Account');
Lifecycle.onLoggedOut();
defaultDispatcher.fire(Action.TriggerLogout);
this.props.onFinished(true);
}).catch(e => {
logger.error(e);

View file

@ -28,7 +28,6 @@ limitations under the License.
import React from 'react';
import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseDialog from "./BaseDialog";
interface IProps {
@ -44,7 +43,6 @@ interface IState {
onFinished: (success: boolean) => void;
}
@replaceableComponent("views.dialogs.ErrorDialog")
export default class ErrorDialog extends React.Component<IProps, IState> {
public static defaultProps = {
focus: true,

View file

@ -43,10 +43,10 @@ import QueryMatcher from "../../../autocomplete/QueryMatcher";
import TruncatedList from "../elements/TruncatedList";
import EntityTile from "../rooms/EntityTile";
import BaseAvatar from "../avatars/BaseAvatar";
import { roomContextDetailsText } from "../../../Rooms";
import { Action } from "../../../dispatcher/actions";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { ButtonEvent } from "../elements/AccessibleButton";
import { roomContextDetailsText } from "../../../utils/i18n-helpers";
const AVATAR_SIZE = 30;

View file

@ -32,7 +32,6 @@ import {
IPostmessageResponseData,
PostmessageAction,
} from "./HostSignupDialogTypes";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { IConfigOptions } from "../../../IConfigOptions";
import { SnakedObject } from "../../../utils/SnakedObject";
@ -46,7 +45,6 @@ interface IState {
minimized: boolean;
}
@replaceableComponent("views.dialogs.HostSignupDialog")
export default class HostSignupDialog extends React.PureComponent<IProps, IState> {
private iframeRef: React.RefObject<HTMLIFrameElement> = React.createRef();
private readonly config: SnakedObject<IConfigOptions["host_signup"]>;

View file

@ -21,7 +21,6 @@ import { logger } from "matrix-js-sdk/src/logger";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media";
import VerificationComplete from "../verification/VerificationComplete";
import VerificationCancelled from "../verification/VerificationCancelled";
@ -54,7 +53,6 @@ interface IState {
sas: IGeneratedSas;
}
@replaceableComponent("views.dialogs.IncomingSasDialog")
export default class IncomingSasDialog extends React.Component<IProps, IState> {
private showSasEvent: ISasEvent;

View file

@ -19,8 +19,9 @@ import React, { ReactNode, KeyboardEvent } from 'react';
import classNames from "classnames";
import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import { IDialogProps } from "./IDialogProps";
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
interface IProps extends IDialogProps {
title?: string;
@ -44,9 +45,6 @@ export default class InfoDialog extends React.Component<IProps> {
};
render() {
// FIXME: Using a regular import will break the app
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return (
<BaseDialog
className="mx_InfoDialog"

View file

@ -19,14 +19,12 @@ import React from 'react';
import { _t } from "../../../languageHandler";
import dis from '../../../dispatcher/dispatcher';
import { Action } from "../../../dispatcher/actions";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
import { IDialogProps } from "./IDialogProps";
interface IProps extends IDialogProps {}
@replaceableComponent("views.dialogs.IntegrationsDisabledDialog")
export default class IntegrationsDisabledDialog extends React.Component<IProps> {
private onAcknowledgeClick = (): void => {
this.props.onFinished();

View file

@ -18,14 +18,12 @@ import React from 'react';
import { _t } from "../../../languageHandler";
import SdkConfig from "../../../SdkConfig";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { IDialogProps } from "./IDialogProps";
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
interface IProps extends IDialogProps {}
@replaceableComponent("views.dialogs.IntegrationsImpossibleDialog")
export default class IntegrationsImpossibleDialog extends React.Component<IProps> {
private onAcknowledgeClick = (): void => {
this.props.onFinished();

View file

@ -24,7 +24,6 @@ import { _t } from '../../../languageHandler';
import AccessibleButton from '../elements/AccessibleButton';
import InteractiveAuth, { ERROR_USER_CANCELLED } from "../../structures/InteractiveAuth";
import { SSOAuthEntry } from "../auth/InteractiveAuthEntryComponents";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseDialog from "./BaseDialog";
import { IDialogProps } from "./IDialogProps";
@ -83,7 +82,6 @@ interface IState {
uiaStagePhase: number | string;
}
@replaceableComponent("views.dialogs.InteractiveAuthDialog")
export default class InteractiveAuthDialog extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);

View file

@ -1,5 +1,5 @@
/*
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Copyright 2019 - 2022 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.
@ -19,7 +19,6 @@ import classNames from 'classnames';
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import { IInvite3PID } from "matrix-js-sdk/src/@types/requests";
import { logger } from "matrix-js-sdk/src/logger";
import { _t, _td } from "../../../languageHandler";
@ -30,15 +29,8 @@ import SdkConfig from "../../../SdkConfig";
import * as Email from "../../../email";
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from "../../../utils/IdentityServerUtils";
import { abbreviateUrl } from "../../../utils/UrlUtils";
import dis from "../../../dispatcher/dispatcher";
import IdentityAuthClient from "../../../IdentityAuthClient";
import Modal from "../../../Modal";
import { humanizeTime } from "../../../utils/humanize";
import createRoom, {
canEncryptToAllUsers,
findDMForUser,
privateShouldBeEncrypted,
} from "../../../createRoom";
import {
IInviteResult,
inviteMultipleToRoom,
@ -49,9 +41,7 @@ import { DefaultTagID } from "../../../stores/room-list/models";
import RoomListStore from "../../../stores/room-list/RoomListStore";
import SettingsStore from "../../../settings/SettingsStore";
import { UIFeature } from "../../../settings/UIFeature";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media";
import { getAddressType } from "../../../UserAddress";
import BaseAvatar from '../avatars/BaseAvatar';
import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton';
import { compare, selectText } from '../../../utils/strings';
@ -66,9 +56,12 @@ import CallHandler from "../../../CallHandler";
import UserIdentifierCustomisations from '../../../customisations/UserIdentifier';
import CopyableText from "../elements/CopyableText";
import { ScreenName } from '../../../PosthogTrackers';
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { DirectoryMember, IDMUserTileProps, Member, startDm, ThreepidMember } from "../../../utils/direct-messages";
import { AnyInviteKind, KIND_CALL_TRANSFER, KIND_DM, KIND_INVITE } from './InviteDialogTypes';
import Modal from '../../../Modal';
import dis from "../../../dispatcher/dispatcher";
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
/* eslint-disable camelcase */
@ -79,13 +72,6 @@ interface IRecentUser {
lastActive: number;
}
export const KIND_DM = "dm";
export const KIND_INVITE = "invite";
// NB. This dialog needs the 'mx_InviteDialog_transferWrapper' wrapper class to have the correct
// padding on the bottom (because all modals have 24px padding on all sides), so this needs to
// be passed when creating the modal
export const KIND_CALL_TRANSFER = "call_transfer";
const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first
const INCREMENT_ROOMS_SHOWN = 5; // Number of rooms to add when 'show more' is clicked
@ -94,90 +80,6 @@ enum TabId {
DialPad = 'dialpad',
}
// This is the interface that is expected by various components in the Invite Dialog and RoomInvite.
// It is a bit awkward because it also matches the RoomMember class from the js-sdk with some extra support
// for 3PIDs/email addresses.
export abstract class Member {
/**
* The display name of this Member. For users this should be their profile's display
* name or user ID if none set. For 3PIDs this should be the 3PID address (email).
*/
public abstract get name(): string;
/**
* The ID of this Member. For users this should be their user ID. For 3PIDs this should
* be the 3PID address (email).
*/
public abstract get userId(): string;
/**
* Gets the MXC URL of this Member's avatar. For users this should be their profile's
* avatar MXC URL or null if none set. For 3PIDs this should always be null.
*/
public abstract getMxcAvatarUrl(): string;
}
class DirectoryMember extends Member {
private readonly _userId: string;
private readonly displayName?: string;
private readonly avatarUrl?: string;
// eslint-disable-next-line camelcase
constructor(userDirResult: { user_id: string, display_name?: string, avatar_url?: string }) {
super();
this._userId = userDirResult.user_id;
this.displayName = userDirResult.display_name;
this.avatarUrl = userDirResult.avatar_url;
}
// These next class members are for the Member interface
get name(): string {
return this.displayName || this._userId;
}
get userId(): string {
return this._userId;
}
getMxcAvatarUrl(): string {
return this.avatarUrl;
}
}
class ThreepidMember extends Member {
private readonly id: string;
constructor(id: string) {
super();
this.id = id;
}
// This is a getter that would be falsey on all other implementations. Until we have
// better type support in the react-sdk we can use this trick to determine the kind
// of 3PID we're dealing with, if any.
get isEmail(): boolean {
return this.id.includes('@');
}
// These next class members are for the Member interface
get name(): string {
return this.id;
}
get userId(): string {
return this.id;
}
getMxcAvatarUrl(): string {
return null;
}
}
interface IDMUserTileProps {
member: Member;
onRemove(member: Member): void;
}
class DMUserTile extends React.PureComponent<IDMUserTileProps> {
private onRemove = (e) => {
// Stop the browser from highlighting text
@ -355,7 +257,7 @@ interface IInviteDialogProps {
// The kind of invite being performed. Assumed to be KIND_DM if
// not provided.
kind: string;
kind: AnyInviteKind;
// The room ID this dialog is for. Only required for KIND_INVITE.
roomId: string;
@ -387,7 +289,6 @@ interface IInviteDialogState {
errorText: string;
}
@replaceableComponent("views.dialogs.InviteDialog")
export default class InviteDialog extends React.PureComponent<IInviteDialogProps, IInviteDialogState> {
static defaultProps = {
kind: KIND_DM,
@ -663,72 +564,10 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
private startDm = async () => {
this.setState({ busy: true });
const client = MatrixClientPeg.get();
const targets = this.convertFilter();
const targetIds = targets.map(t => t.userId);
// Check if there is already a DM with these people and reuse it if possible.
let existingRoom: Room;
if (targetIds.length === 1) {
existingRoom = findDMForUser(client, targetIds[0]);
} else {
existingRoom = DMRoomMap.shared().getDMRoomForIdentifiers(targetIds);
}
if (existingRoom) {
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: existingRoom.roomId,
should_peek: false,
joining: false,
metricsTrigger: "MessageUser",
});
this.props.onFinished(true);
return;
}
const createRoomOptions = { inlineErrors: true } as any; // XXX: Type out `createRoomOptions`
if (privateShouldBeEncrypted()) {
// Check whether all users have uploaded device keys before.
// If so, enable encryption in the new room.
const has3PidMembers = targets.some(t => t instanceof ThreepidMember);
if (!has3PidMembers) {
const allHaveDeviceKeys = await canEncryptToAllUsers(client, targetIds);
if (allHaveDeviceKeys) {
createRoomOptions.encryption = true;
}
}
}
// Check if it's a traditional DM and create the room if required.
// TODO: [Canonical DMs] Remove this check and instead just create the multi-person DM
try {
const isSelf = targetIds.length === 1 && targetIds[0] === client.getUserId();
if (targetIds.length === 1 && !isSelf) {
createRoomOptions.dmUserId = targetIds[0];
}
if (targetIds.length > 1) {
createRoomOptions.createOpts = targetIds.reduce(
(roomOptions, address) => {
const type = getAddressType(address);
if (type === 'email') {
const invite: IInvite3PID = {
id_server: client.getIdentityServerUrl(true),
medium: 'email',
address,
};
roomOptions.invite_3pid.push(invite);
} else if (type === 'mx-user-id') {
roomOptions.invite.push(address);
}
return roomOptions;
},
{ invite: [], invite_3pid: [] },
);
}
await createRoom(createRoomOptions);
const cli = MatrixClientPeg.get();
const targets = this.convertFilter();
await startDm(cli, targets);
this.props.onFinished(true);
} catch (err) {
logger.error(err);
@ -736,6 +575,8 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
busy: false,
errorText: _t("We couldn't create your DM."),
});
} finally {
this.setState({ busy: false });
}
};

View file

@ -0,0 +1,24 @@
/*
Copyright 2022 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.
*/
export const KIND_DM = "dm";
export const KIND_INVITE = "invite";
// NB. This dialog needs the 'mx_InviteDialog_transferWrapper' wrapper class to have the correct
// padding on the bottom (because all modals have 24px padding on all sides), so this needs to
// be passed when creating the modal
export const KIND_CALL_TRANSFER = "call_transfer";
export type AnyInviteKind = typeof KIND_INVITE | typeof KIND_DM | typeof KIND_CALL_TRANSFER;

View file

@ -24,7 +24,6 @@ import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import RestoreKeyBackupDialog from './security/RestoreKeyBackupDialog';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import QuestionDialog from "./QuestionDialog";
import BaseDialog from "./BaseDialog";
import Spinner from "../elements/Spinner";
@ -41,7 +40,6 @@ interface IState {
error?: string;
}
@replaceableComponent("views.dialogs.LogoutDialog")
export default class LogoutDialog extends React.Component<IProps, IState> {
static defaultProps = {
onFinished: function() {},

View file

@ -24,7 +24,6 @@ import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import * as FormattingUtils from '../../../utils/FormattingUtils';
import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import QuestionDialog from "./QuestionDialog";
import { IDialogProps } from "./IDialogProps";
@ -33,7 +32,6 @@ interface IProps extends IDialogProps {
device: DeviceInfo;
}
@replaceableComponent("views.dialogs.ManualDeviceKeyVerificationDialog")
export default class ManualDeviceKeyVerificationDialog extends React.Component<IProps> {
private onLegacyFinished = (confirm: boolean): void => {
if (confirm) {

View file

@ -25,7 +25,6 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { _t } from '../../../languageHandler';
import { wantsDateSeparator } from '../../../DateUtils';
import SettingsStore from '../../../settings/SettingsStore';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseDialog from "./BaseDialog";
import ScrollPanel from "../../structures/ScrollPanel";
import Spinner from "../elements/Spinner";
@ -48,7 +47,6 @@ interface IState {
isTwelveHour: boolean;
}
@replaceableComponent("views.dialogs.MessageEditHistoryDialog")
export default class MessageEditHistoryDialog extends React.PureComponent<IProps, IState> {
constructor(props: IProps) {
super(props);

View file

@ -39,7 +39,6 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { OwnProfileStore } from "../../../stores/OwnProfileStore";
import { arrayFastClone } from "../../../utils/arrays";
import { ElementWidget } from "../../../stores/widgets/StopGapWidget";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { ELEMENT_CLIENT_ID } from "../../../identifiers";
import SettingsStore from "../../../settings/SettingsStore";
@ -57,7 +56,6 @@ interface IState {
const MAX_BUTTONS = 3;
@replaceableComponent("views.dialogs.ModalWidgetDialog")
export default class ModalWidgetDialog extends React.PureComponent<IProps, IState> {
private readonly widget: Widget;
private readonly possibleButtons: ModalButtonID[];

View file

@ -18,9 +18,10 @@ limitations under the License.
import React from 'react';
import classNames from "classnames";
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import { IDialogProps } from "./IDialogProps";
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
interface IProps extends IDialogProps {
title?: string;
@ -58,10 +59,6 @@ export default class QuestionDialog extends React.Component<IProps> {
};
public render(): JSX.Element {
// Converting these to imports breaks wrench tests
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
let primaryButtonClass = "";
if (this.props.danger) {
primaryButtonClass = "danger";

View file

@ -16,6 +16,7 @@ limitations under the License.
import React from 'react';
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { logger } from "matrix-js-sdk/src/logger";
import { _t } from '../../../languageHandler';
import { ensureDMExists } from "../../../createRoom";
@ -23,7 +24,6 @@ import { IDialogProps } from "./IDialogProps";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import SdkConfig from '../../../SdkConfig';
import Markdown from '../../../Markdown';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import SettingsStore from "../../../settings/SettingsStore";
import StyledRadioButton from "../elements/StyledRadioButton";
import BaseDialog from "./BaseDialog";
@ -89,7 +89,6 @@ type Moderation = {
* a well-formed state event `m.room.moderation.moderated_by`
* /`org.matrix.msc3215.room.moderation.moderated_by`?
*/
@replaceableComponent("views.dialogs.ReportEventDialog")
export default class ReportEventDialog extends React.Component<IProps, IState> {
// If the room supports moderation, the moderation information.
private moderation?: Moderation;
@ -215,7 +214,7 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
try {
const client = MatrixClientPeg.get();
const ev = this.props.mxEvent;
if (this.moderation && this.state.nature != NonStandardValue.Admin) {
if (this.moderation && this.state.nature !== NonStandardValue.Admin) {
const nature: Nature = this.state.nature;
// Report to moderators through to the dedicated bot,
@ -235,6 +234,7 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
}
this.props.onFinished(true);
} catch (e) {
logger.error(e);
this.setState({
busy: false,
err: e.message,

View file

@ -30,7 +30,6 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg";
import dis from "../../../dispatcher/dispatcher";
import SettingsStore from "../../../settings/SettingsStore";
import { UIFeature } from "../../../settings/UIFeature";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseDialog from "./BaseDialog";
import { Action } from '../../../dispatcher/actions';
@ -51,7 +50,6 @@ interface IState {
roomName: string;
}
@replaceableComponent("views.dialogs.RoomSettingsDialog")
export default class RoomSettingsDialog extends React.Component<IProps, IState> {
private dispatcherRef: string;

View file

@ -19,7 +19,6 @@ import { Room } from "matrix-js-sdk/src/models/room";
import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { upgradeRoom } from "../../../utils/RoomUpgrade";
import { IDialogProps } from "./IDialogProps";
import BaseDialog from "./BaseDialog";
@ -35,7 +34,6 @@ interface IState {
busy: boolean;
}
@replaceableComponent("views.dialogs.RoomUpgradeDialog")
export default class RoomUpgradeDialog extends React.Component<IProps, IState> {
private targetVersion: string;

View file

@ -23,7 +23,6 @@ import SdkConfig from "../../../SdkConfig";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import Modal from "../../../Modal";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { IDialogProps } from "./IDialogProps";
import BugReportDialog from './BugReportDialog';
import BaseDialog from "./BaseDialog";
@ -50,7 +49,6 @@ interface IState {
total?: number;
}
@replaceableComponent("views.dialogs.RoomUpgradeWarningDialog")
export default class RoomUpgradeWarningDialog extends React.Component<IProps, IState> {
private readonly isPrivate: boolean;
private readonly currentVersion: string;

View file

@ -29,12 +29,10 @@ import AccessibleButton from "../elements/AccessibleButton";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { IDialogProps } from "./IDialogProps";
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps extends IDialogProps {
}
@replaceableComponent("views.dialogs.ServerOfflineDialog")
export default class ServerOfflineDialog extends React.PureComponent<IProps> {
public componentDidMount() {
EchoStore.instance.on(UPDATE_EVENT, this.onEchosUpdated);

View file

@ -27,7 +27,6 @@ import Field from "../elements/Field";
import StyledRadioButton from "../elements/StyledRadioButton";
import TextWithTooltip from "../elements/TextWithTooltip";
import withValidation, { IFieldState } from "../elements/Validation";
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
title?: string;
@ -40,7 +39,6 @@ interface IState {
otherHomeserver: string;
}
@replaceableComponent("views.dialogs.ServerPickerDialog")
export default class ServerPickerDialog extends React.PureComponent<IProps, IState> {
private readonly defaultServer: ValidatedServerConfig;
private readonly fieldRef = createRef<Field>();

View file

@ -17,12 +17,10 @@ limitations under the License.
import React from 'react';
import { _t } from "../../../languageHandler";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
import { IDialogProps } from "./IDialogProps";
@replaceableComponent("views.dialogs.SeshatResetDialog")
export default class SeshatResetDialog extends React.PureComponent<IDialogProps> {
render() {
return (

View file

@ -21,7 +21,6 @@ import React from 'react';
import SdkConfig from '../../../SdkConfig';
import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import QuestionDialog from "./QuestionDialog";
import BugReportDialog from "./BugReportDialog";
import BaseDialog from "./BaseDialog";
@ -32,7 +31,6 @@ interface IProps extends IDialogProps {
error: Error;
}
@replaceableComponent("views.dialogs.SessionRestoreErrorDialog")
export default class SessionRestoreErrorDialog extends React.Component<IProps> {
private sendBugReport = (): void => {
Modal.createTrackedDialog('Session Restore Error', 'Send Bug Report Dialog', BugReportDialog, {

View file

@ -22,7 +22,6 @@ import * as Email from '../../../email';
import AddThreepid from '../../../AddThreepid';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import Spinner from "../elements/Spinner";
import ErrorDialog from "./ErrorDialog";
import QuestionDialog from "./QuestionDialog";
@ -44,7 +43,6 @@ interface IState {
*
* On success, `onFinished(true)` is called.
*/
@replaceableComponent("views.dialogs.SetEmailDialog")
export default class SetEmailDialog extends React.Component<IProps, IState> {
private addThreepid: AddThreepid;

View file

@ -29,7 +29,6 @@ import StyledCheckbox from '../elements/StyledCheckbox';
import { IDialogProps } from "./IDialogProps";
import SettingsStore from "../../../settings/SettingsStore";
import { UIFeature } from "../../../settings/UIFeature";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseDialog from "./BaseDialog";
import CopyableText from "../elements/CopyableText";
@ -71,7 +70,6 @@ interface IState {
permalinkCreator: RoomPermalinkCreator;
}
@replaceableComponent("views.dialogs.ShareDialog")
export default class ShareDialog extends React.PureComponent<IProps, IState> {
protected closeCopiedTooltip: () => void;

View file

@ -26,10 +26,7 @@ import { useSettingValue } from "../../../hooks/useSettings";
import SettingsStore from "../../../settings/SettingsStore";
import { SettingLevel } from "../../../settings/SettingLevel";
import RoomName from "../elements/RoomName";
export enum SpacePreferenceTab {
Appearance = "SPACE_PREFERENCE_APPEARANCE_TAB",
}
import { SpacePreferenceTab } from "../../../dispatcher/payloads/OpenSpacePreferencesPayload";
interface IProps extends IDialogProps {
space: Room;

View file

@ -53,19 +53,18 @@ import DMRoomMap from "../../../utils/DMRoomMap";
import { mediaFromMxc } from "../../../customisations/Media";
import BaseAvatar from "../avatars/BaseAvatar";
import Spinner from "../elements/Spinner";
import { roomContextDetailsText, spaceContextDetailsText } from "../../../Rooms";
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import { Action } from "../../../dispatcher/actions";
import Modal from "../../../Modal";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import RoomViewStore from "../../../stores/RoomViewStore";
import { RoomViewStore } from "../../../stores/RoomViewStore";
import { showStartChatInviteDialog } from "../../../RoomInvite";
import SettingsStore from "../../../settings/SettingsStore";
import { SettingLevel } from "../../../settings/SettingLevel";
import NotificationBadge from "../rooms/NotificationBadge";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import { BetaPill } from "../beta/BetaCard";
import { UserTab } from "./UserSettingsDialog";
import { UserTab } from "./UserTab";
import BetaFeedbackDialog from "./BetaFeedbackDialog";
import SdkConfig from "../../../SdkConfig";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
@ -74,6 +73,7 @@ import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { PosthogAnalytics } from "../../../PosthogAnalytics";
import { getCachedRoomIDForAlias } from "../../../RoomAliasCache";
import { roomContextDetailsText, spaceContextDetailsText } from "../../../utils/i18n-helpers";
const MAX_RECENT_SEARCHES = 10;
const SECTION_LIMIT = 50; // only show 50 results per section for performance reasons
@ -559,7 +559,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", onFinished }) =>
<h4>{ _t("Recently viewed") }</h4>
<div>
{ BreadcrumbsStore.instance.rooms
.filter(r => r.roomId !== RoomViewStore.getRoomId())
.filter(r => r.roomId !== RoomViewStore.instance.getRoomId())
.map(room => (
<TooltipOption
id={`mx_SpotlightDialog_button_recentlyViewed_${room.roomId}`}

View file

@ -19,7 +19,6 @@ import React from 'react';
import SdkConfig from '../../../SdkConfig';
import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
import BugReportDialog from "./BugReportDialog";
@ -28,7 +27,6 @@ import AccessibleButton from '../elements/AccessibleButton';
interface IProps extends IDialogProps { }
@replaceableComponent("views.dialogs.StorageEvictedDialog")
export default class StorageEvictedDialog extends React.Component<IProps> {
private sendBugReport = (ev: React.MouseEvent): void => {
ev.preventDefault();

View file

@ -22,7 +22,6 @@ import { logger } from "matrix-js-sdk/src/logger";
import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
import { dialogTermsInteractionCallback, TermsNotSignedError } from "../../../Terms";
import * as ScalarMessaging from "../../../ScalarMessaging";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { IntegrationManagerInstance } from "../../../integrations/IntegrationManagerInstance";
import ScalarAuthClient from "../../../ScalarAuthClient";
import AccessibleButton from "../elements/AccessibleButton";
@ -55,7 +54,6 @@ interface IState {
currentScalarClient: ScalarAuthClient;
}
@replaceableComponent("views.dialogs.TabbedIntegrationManagerDialog")
export default class TabbedIntegrationManagerDialog extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);

View file

@ -19,7 +19,6 @@ import React from 'react';
import { SERVICE_TYPES } from "matrix-js-sdk/src/service-types";
import { _t, pickBestLanguage } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import DialogButtons from "../elements/DialogButtons";
import BaseDialog from "./BaseDialog";
@ -66,7 +65,6 @@ interface IState {
agreedUrls: any;
}
@replaceableComponent("views.dialogs.TermsDialog")
export default class TermsDialog extends React.PureComponent<ITermsDialogProps, IState> {
constructor(props) {
super(props);

View file

@ -18,7 +18,6 @@ import React, { ChangeEvent, createRef } from 'react';
import Field from "../elements/Field";
import { _t, _td } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { IFieldState, IValidationResult } from "../elements/Validation";
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
@ -43,7 +42,6 @@ interface IState {
valid: boolean;
}
@replaceableComponent("views.dialogs.TextInputDialog")
export default class TextInputDialog extends React.Component<IProps, IState> {
private field = createRef<Field>();

View file

@ -19,7 +19,6 @@ import React from 'react';
import filesize from "filesize";
import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { getBlobSafeMimeType } from '../../../utils/blobs';
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
@ -31,7 +30,6 @@ interface IProps {
onFinished: (uploadConfirmed: boolean, uploadAll?: boolean) => void;
}
@replaceableComponent("views.dialogs.UploadConfirmDialog")
export default class UploadConfirmDialog extends React.Component<IProps> {
private readonly objectUrl: string;
private readonly mimeType: string;

View file

@ -19,7 +19,6 @@ import React from 'react';
import { _t } from '../../../languageHandler';
import ContentMessages from '../../../ContentMessages';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
import { IDialogProps } from "./IDialogProps";
@ -35,7 +34,6 @@ interface IProps extends IDialogProps {
* them. This is named fairly generically but the only thing we check right now is
* the size of the file.
*/
@replaceableComponent("views.dialogs.UploadFailureDialog")
export default class UploadFailureDialog extends React.Component<IProps> {
private onCancelClick = (): void => {
this.props.onFinished(false);

View file

@ -31,25 +31,11 @@ import HelpUserSettingsTab from "../settings/tabs/user/HelpUserSettingsTab";
import SdkConfig from "../../../SdkConfig";
import MjolnirUserSettingsTab from "../settings/tabs/user/MjolnirUserSettingsTab";
import { UIFeature } from "../../../settings/UIFeature";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseDialog from "./BaseDialog";
import { IDialogProps } from "./IDialogProps";
import SidebarUserSettingsTab from "../settings/tabs/user/SidebarUserSettingsTab";
import KeyboardUserSettingsTab from "../settings/tabs/user/KeyboardUserSettingsTab";
export enum UserTab {
General = "USER_GENERAL_TAB",
Appearance = "USER_APPEARANCE_TAB",
Notifications = "USER_NOTIFICATIONS_TAB",
Preferences = "USER_PREFERENCES_TAB",
Keyboard = "USER_KEYBOARD_TAB",
Sidebar = "USER_SIDEBAR_TAB",
Voice = "USER_VOICE_TAB",
Security = "USER_SECURITY_TAB",
Labs = "USER_LABS_TAB",
Mjolnir = "USER_MJOLNIR_TAB",
Help = "USER_HELP_TAB",
}
import { UserTab } from "./UserTab";
interface IProps extends IDialogProps {
initialTabId?: UserTab;
@ -59,7 +45,6 @@ interface IState {
mjolnirEnabled: boolean;
}
@replaceableComponent("views.dialogs.UserSettingsDialog")
export default class UserSettingsDialog extends React.Component<IProps, IState> {
private mjolnirWatcher: string;

View file

@ -0,0 +1,29 @@
/*
Copyright 2022 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.
*/
export enum UserTab {
General = "USER_GENERAL_TAB",
Appearance = "USER_APPEARANCE_TAB",
Notifications = "USER_NOTIFICATIONS_TAB",
Preferences = "USER_PREFERENCES_TAB",
Keyboard = "USER_KEYBOARD_TAB",
Sidebar = "USER_SIDEBAR_TAB",
Voice = "USER_VOICE_TAB",
Security = "USER_SECURITY_TAB",
Labs = "USER_LABS_TAB",
Mjolnir = "USER_MJOLNIR_TAB",
Help = "USER_HELP_TAB",
}

View file

@ -20,7 +20,6 @@ import { User } from 'matrix-js-sdk/src/models/user';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseDialog from "./BaseDialog";
import EncryptionPanel from "../right_panel/EncryptionPanel";
@ -35,7 +34,6 @@ interface IState {
verificationRequest: VerificationRequest;
}
@replaceableComponent("views.dialogs.VerificationRequestDialog")
export default class VerificationRequestDialog extends React.Component<IProps, IState> {
constructor(props) {
super(props);

View file

@ -32,7 +32,6 @@ import StyledCheckbox from "../elements/StyledCheckbox";
import DialogButtons from "../elements/DialogButtons";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
import { CapabilityText } from "../../../widgets/CapabilityText";
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps extends IDialogProps {
requestedCapabilities: Set<Capability>;
@ -50,7 +49,6 @@ interface IState {
rememberSelection: boolean;
}
@replaceableComponent("views.dialogs.WidgetCapabilitiesPromptDialog")
export default class WidgetCapabilitiesPromptDialog extends React.PureComponent<IProps, IState> {
private eventPermissionsMap = new Map<Capability, WidgetEventCapability>();

View file

@ -22,7 +22,6 @@ import { logger } from "matrix-js-sdk/src/logger";
import { _t } from "../../../languageHandler";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
import { OIDCState, WidgetPermissionStore } from "../../../stores/widgets/WidgetPermissionStore";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { IDialogProps } from "./IDialogProps";
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
@ -37,7 +36,6 @@ interface IState {
rememberSelection: boolean;
}
@replaceableComponent("views.dialogs.WidgetOpenIDPermissionsDialog")
export default class WidgetOpenIDPermissionsDialog extends React.PureComponent<IProps, IState> {
constructor(props: IProps) {
super(props);

View file

@ -20,7 +20,6 @@ import React, { ChangeEvent, FormEvent } from 'react';
import { ISecretStorageKeyInfo } from "matrix-js-sdk/src/crypto/api";
import { logger } from "matrix-js-sdk/src/logger";
import * as sdk from '../../../../index';
import { MatrixClientPeg } from '../../../../MatrixClientPeg';
import Field from '../../elements/Field';
import AccessibleButton from '../../elements/AccessibleButton';
@ -28,6 +27,10 @@ import { _t } from '../../../../languageHandler';
import { IDialogProps } from "../IDialogProps";
import { accessSecretStorage } from "../../../../SecurityManager";
import Modal from "../../../../Modal";
import InteractiveAuthDialog from "../InteractiveAuthDialog";
import DialogButtons from "../../elements/DialogButtons";
import BaseDialog from "../BaseDialog";
import { chromeFileInputFix } from "../../../../utils/BrowserWorkarounds";
// Maximum acceptable size of a key file. It's 59 characters including the spaces we encode,
// so this should be plenty and allow for people putting extra whitespace in the file because
@ -172,7 +175,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
this.fileUpload.current.click();
};
private onPassPhraseNext = async (ev: FormEvent<HTMLFormElement>) => {
private onPassPhraseNext = async (ev: FormEvent<HTMLFormElement> | React.MouseEvent) => {
ev.preventDefault();
if (this.state.passPhrase.length <= 0) return;
@ -187,7 +190,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
}
};
private onRecoveryKeyNext = async (ev: FormEvent<HTMLFormElement>) => {
private onRecoveryKeyNext = async (ev: FormEvent<HTMLFormElement> | React.MouseEvent) => {
ev.preventDefault();
if (!this.state.recoveryKeyValid) return;
@ -230,8 +233,6 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
const cli = MatrixClientPeg.get();
await cli.bootstrapCrossSigning({
authUploadDeviceSigningKeys: async (makeRequest) => {
// XXX: Making this an import breaks the app.
const InteractiveAuthDialog = sdk.getComponent("views.dialogs.InteractiveAuthDialog");
const { finished } = Modal.createTrackedDialog(
'Cross-signing keys dialog', '', InteractiveAuthDialog,
{
@ -273,10 +274,6 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
}
render() {
// Caution: Making these an import will break tests.
const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
const DialogButtons = sdk.getComponent("views.elements.DialogButtons");
const hasPassphrase = (
this.props.keyInfo &&
this.props.keyInfo.passphrase &&
@ -315,7 +312,6 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
/>
</div>;
} else if (hasPassphrase && !this.state.forceRecoveryKey) {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
title = _t("Security Phrase");
titleClass = ['mx_AccessSecretStorageDialog_titleWithIcon mx_AccessSecretStorageDialog_securePhraseTitle'];
@ -408,6 +404,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
<input type="file"
className="mx_AccessSecretStorageDialog_recoveryKeyEntry_fileInput"
ref={this.fileUpload}
onClick={chromeFileInputFix}
onChange={this.onRecoveryKeyFileChange}
/>
<AccessibleButton kind="primary" onClick={this.onRecoveryKeyFileUploadClick}>

View file

@ -17,7 +17,6 @@ limitations under the License.
import React from 'react';
import { _t } from "../../../../languageHandler";
import { replaceableComponent } from "../../../../utils/replaceableComponent";
import BaseDialog from "../BaseDialog";
import DialogButtons from "../../elements/DialogButtons";
@ -25,7 +24,6 @@ interface IProps {
onFinished: (success: boolean) => void;
}
@replaceableComponent("views.dialogs.security.ConfirmDestroyCrossSigningDialog")
export default class ConfirmDestroyCrossSigningDialog extends React.Component<IProps> {
private onConfirm = (): void => {
this.props.onFinished(true);

View file

@ -27,7 +27,6 @@ import DialogButtons from '../../elements/DialogButtons';
import BaseDialog from '../BaseDialog';
import Spinner from '../../elements/Spinner';
import InteractiveAuthDialog from '../InteractiveAuthDialog';
import { replaceableComponent } from "../../../../utils/replaceableComponent";
interface IProps {
accountPassword?: string;
@ -46,7 +45,6 @@ interface IState {
* cases, only a spinner is shown, but for more complex auth like SSO, the user
* may need to complete some steps to proceed.
*/
@replaceableComponent("views.dialogs.security.CreateCrossSigningDialog")
export default class CreateCrossSigningDialog extends React.PureComponent<IProps, IState> {
constructor(props: IProps) {
super(props);

View file

@ -24,8 +24,11 @@ import { logger } from "matrix-js-sdk/src/logger";
import { MatrixClientPeg } from '../../../../MatrixClientPeg';
import { _t } from '../../../../languageHandler';
import { accessSecretStorage } from '../../../../SecurityManager';
import * as sdk from '../../../../index';
import { IDialogProps } from "../IDialogProps";
import Spinner from '../../elements/Spinner';
import DialogButtons from "../../elements/DialogButtons";
import AccessibleButton from "../../elements/AccessibleButton";
import BaseDialog from "../BaseDialog";
enum RestoreType {
Passphrase = "passphrase",
@ -298,12 +301,6 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps,
}
public render(): JSX.Element {
// FIXME: Making these into imports will break tests
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const Spinner = sdk.getComponent("elements.Spinner");
const backupHasPassphrase = (
this.state.backupInfo &&
this.state.backupInfo.auth_data &&

View file

@ -20,7 +20,6 @@ import SetupEncryptionBody from '../../../structures/auth/SetupEncryptionBody';
import BaseDialog from '../BaseDialog';
import { _t } from '../../../../languageHandler';
import { SetupEncryptionStore, Phase } from '../../../../stores/SetupEncryptionStore';
import { replaceableComponent } from "../../../../utils/replaceableComponent";
import { IDialogProps } from "../IDialogProps";
function iconFromPhase(phase: Phase) {
@ -36,7 +35,6 @@ interface IState {
icon: string;
}
@replaceableComponent("views.dialogs.security.SetupEncryptionDialog")
export default class SetupEncryptionDialog extends React.Component<IProps, IState> {
private store: SetupEncryptionStore;

View file

@ -53,6 +53,7 @@ interface IProps extends React.InputHTMLAttributes<Element> {
tabIndex?: number;
disabled?: boolean;
className?: string;
triggerOnMouseDown?: boolean;
onClick(e?: ButtonEvent): void | Promise<void>;
}
@ -78,13 +79,18 @@ export default function AccessibleButton({
className,
onKeyDown,
onKeyUp,
triggerOnMouseDown,
...restProps
}: IProps) {
const newProps: IAccessibleButtonProps = restProps;
if (disabled) {
newProps["aria-disabled"] = true;
} else {
newProps.onClick = onClick;
if (triggerOnMouseDown) {
newProps.onMouseDown = onClick;
} else {
newProps.onClick = onClick;
}
// We need to consume enter onKeyDown and space onKeyUp
// otherwise we are risking also activating other keyboard focusable elements
// that might receive focus as a result of the AccessibleButtonClick action

View file

@ -19,12 +19,11 @@ import React, { SyntheticEvent } from 'react';
import AccessibleButton from "./AccessibleButton";
import Tooltip, { Alignment } from './Tooltip';
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps extends React.ComponentProps<typeof AccessibleButton> {
title: string;
tooltip?: React.ReactNode;
label?: React.ReactNode;
label?: string;
tooltipClassName?: string;
forceHide?: boolean;
yOffset?: number;
@ -36,7 +35,6 @@ interface IState {
hover: boolean;
}
@replaceableComponent("views.elements.AccessibleTooltipButton")
export default class AccessibleTooltipButton extends React.PureComponent<IProps, IState> {
constructor(props: IProps) {
super(props);

Some files were not shown because too many files have changed in this diff Show more