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

@ -21,7 +21,6 @@ import { logger } from "matrix-js-sdk/src/logger";
import { _t } from '../../../languageHandler';
import { formatFullDateNoTime } from '../../../DateUtils';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import dis from '../../../dispatcher/dispatcher';
import { Action } from '../../../dispatcher/actions';
@ -61,7 +60,6 @@ interface IState {
jumpToDateEnabled: boolean;
}
@replaceableComponent("views.messages.DateSeparator")
export default class DateSeparator extends React.Component<IProps, IState> {
private settingWatcherRef = null;

View file

@ -58,8 +58,8 @@ export default class DisambiguatedProfile extends React.Component<IProps> {
});
return (
<div className="mx_DisambiguatedProfile" dir="auto" onClick={onClick}>
<span className={displayNameClasses}>
<div className="mx_DisambiguatedProfile" onClick={onClick}>
<span className={displayNameClasses} dir="auto">
{ rawDisplayName }
</span>
{ mxidElement }

View file

@ -22,7 +22,6 @@ import { MediaEventHelper } from "../../../utils/MediaEventHelper";
import { RovingAccessibleTooltipButton } from "../../../accessibility/RovingTabIndex";
import Spinner from "../elements/Spinner";
import { _t, _td } from "../../../languageHandler";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { FileDownloader } from "../../../utils/FileDownloader";
interface IProps {
@ -40,7 +39,6 @@ interface IState {
tooltip: string;
}
@replaceableComponent("views.messages.DownloadActionButton")
export default class DownloadActionButton extends React.PureComponent<IProps, IState> {
private downloader = new FileDownloader();

View file

@ -26,7 +26,6 @@ import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import Modal from '../../../Modal';
import RedactedBody from "./RedactedBody";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import AccessibleButton from "../elements/AccessibleButton";
import ConfirmAndWaitRedactDialog from "../dialogs/ConfirmAndWaitRedactDialog";
import ViewSource from "../../structures/ViewSource";
@ -50,7 +49,6 @@ interface IState {
sendStatus: EventStatus;
}
@replaceableComponent("views.messages.EditHistoryMessage")
export default class EditHistoryMessage extends React.PureComponent<IProps, IState> {
private content = createRef<HTMLDivElement>();
private pills: Element[] = [];

View file

@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { LegacyRef } from "react";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Relations } from "matrix-js-sdk/src/models/relations";
@ -52,4 +53,6 @@ export interface IBodyProps {
// helper function to access relations for this event
getRelationsForEvent?: (eventId: string, relationType: string, eventType: string) => Relations;
ref?: React.RefObject<any> | LegacyRef<any>;
}

View file

@ -17,7 +17,6 @@ limitations under the License.
import React from "react";
import { logger } from "matrix-js-sdk/src/logger";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { Playback } from "../../../audio/Playback";
import InlineSpinner from '../elements/InlineSpinner';
import { _t } from "../../../languageHandler";
@ -35,7 +34,6 @@ interface IState {
playback?: Playback;
}
@replaceableComponent("views.messages.MAudioBody")
export default class MAudioBody extends React.PureComponent<IBodyProps, IState> {
static contextType = RoomContext;
public context!: React.ContextType<typeof RoomContext>;

View file

@ -0,0 +1,162 @@
/*
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, useEffect, useState } from 'react';
import { Beacon, BeaconEvent, MatrixEvent } from 'matrix-js-sdk/src/matrix';
import { BeaconLocationState } from 'matrix-js-sdk/src/content-helpers';
import { randomString } from 'matrix-js-sdk/src/randomstring';
import { Icon as LocationMarkerIcon } from '../../../../res/img/element-icons/location.svg';
import MatrixClientContext from '../../../contexts/MatrixClientContext';
import { useEventEmitterState } from '../../../hooks/useEventEmitter';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
import { useBeacon } from '../../../utils/beacon';
import { isSelfLocation } from '../../../utils/location';
import { BeaconDisplayStatus, getBeaconDisplayStatus } from '../beacon/displayStatus';
import BeaconStatus from '../beacon/BeaconStatus';
import Spinner from '../elements/Spinner';
import Map from '../location/Map';
import SmartMarker from '../location/SmartMarker';
import OwnBeaconStatus from '../beacon/OwnBeaconStatus';
import BeaconViewDialog from '../beacon/BeaconViewDialog';
import { IBodyProps } from "./IBodyProps";
const useBeaconState = (beaconInfoEvent: MatrixEvent): {
beacon?: Beacon;
description?: string;
latestLocationState?: BeaconLocationState;
isLive?: boolean;
} => {
const beacon = useBeacon(beaconInfoEvent);
const isLive = useEventEmitterState(
beacon,
BeaconEvent.LivenessChange,
() => beacon?.isLive);
const latestLocationState = useEventEmitterState(
beacon,
BeaconEvent.LocationUpdate,
() => beacon?.latestLocationState);
if (!beacon) {
return {};
}
const { description } = beacon.beaconInfo;
return {
beacon,
description,
isLive,
latestLocationState,
};
};
// multiple instances of same map might be in document
// eg thread and main timeline, reply
// maplibregl needs a unique id to attach the map instance to
const useUniqueId = (eventId: string): string => {
const [id, setId] = useState(`${eventId}_${randomString(8)}`);
useEffect(() => {
setId(`${eventId}_${randomString(8)}`);
}, [eventId]);
return id;
};
const MBeaconBody: React.FC<IBodyProps> = React.forwardRef(({ mxEvent }, ref) => {
const {
beacon,
isLive,
latestLocationState,
} = useBeaconState(mxEvent);
const mapId = useUniqueId(mxEvent.getId());
const matrixClient = useContext(MatrixClientContext);
const [error, setError] = useState<Error>();
const displayStatus = getBeaconDisplayStatus(isLive, latestLocationState, error);
const markerRoomMember = isSelfLocation(mxEvent.getContent()) ? mxEvent.sender : undefined;
const isOwnBeacon = beacon?.beaconInfoOwner === matrixClient.getUserId();
const onClick = () => {
if (displayStatus !== BeaconDisplayStatus.Active) {
return;
}
Modal.createTrackedDialog(
'Beacon View',
'',
BeaconViewDialog,
{
roomId: mxEvent.getRoomId(),
matrixClient,
focusBeacon: beacon,
},
"mx_BeaconViewDialog_wrapper",
false, // isPriority
true, // isStatic
);
};
return (
<div className='mx_MBeaconBody' ref={ref}>
{ displayStatus === BeaconDisplayStatus.Active ?
<Map
id={mapId}
centerGeoUri={latestLocationState.uri}
onError={setError}
onClick={onClick}
className="mx_MBeaconBody_map"
>
{
({ map }) =>
<SmartMarker
map={map}
id={`${mapId}-marker`}
geoUri={latestLocationState.uri}
roomMember={markerRoomMember}
useMemberColor
/>
}
</Map>
: <div className='mx_MBeaconBody_map mx_MBeaconBody_mapFallback'>
{ displayStatus === BeaconDisplayStatus.Loading ?
<Spinner h={32} w={32} /> :
<LocationMarkerIcon className='mx_MBeaconBody_mapFallbackIcon' />
}
</div>
}
{ isOwnBeacon ?
<OwnBeaconStatus
className='mx_MBeaconBody_chin'
beacon={beacon}
displayStatus={displayStatus}
/> :
<BeaconStatus
className='mx_MBeaconBody_chin'
beacon={beacon}
displayStatus={displayStatus}
label={_t('View live location')}
/>
}
</div>
);
});
export default MBeaconBody;

View file

@ -21,7 +21,6 @@ import { logger } from "matrix-js-sdk/src/logger";
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
import AccessibleButton from "../elements/AccessibleButton";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromContent } from "../../../customisations/Media";
import ErrorDialog from "../dialogs/ErrorDialog";
import { presentableTextForFile } from "../../../utils/FileUtils";
@ -106,7 +105,6 @@ interface IState {
decryptedBlob?: Blob;
}
@replaceableComponent("views.messages.MFileBody")
export default class MFileBody extends React.Component<IProps, IState> {
static contextType = RoomContext;
public context!: React.ContextType<typeof RoomContext>;

View file

@ -28,9 +28,8 @@ import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
import Spinner from '../elements/Spinner';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { Media, mediaFromContent } from "../../../customisations/Media";
import { BLURHASH_FIELD, createThumbnail } from "../../../ContentMessages";
import { BLURHASH_FIELD, createThumbnail } from "../../../utils/image-media";
import { IMediaEventContent } from '../../../customisations/models/IMediaEventContent';
import ImageView from '../elements/ImageView';
import { IBodyProps } from "./IBodyProps";
@ -60,7 +59,6 @@ interface IState {
placeholder: Placeholder;
}
@replaceableComponent("views.messages.MImageBody")
export default class MImageBody extends React.Component<IBodyProps, IState> {
static contextType = RoomContext;
public context!: React.ContextType<typeof RoomContext>;
@ -366,10 +364,12 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
let infoWidth: number;
let infoHeight: number;
let infoSvg = false;
if (content.info?.w && content.info?.h) {
infoWidth = content.info.w;
infoHeight = content.info.h;
infoSvg = content.info.mimetype.startsWith("image/svg");
} else {
// Whilst the image loads, display nothing. We also don't display a blurhash image
// because we don't really know what size of image we'll end up with.
@ -451,6 +451,11 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
'mx_MImageBody_placeholder--blurhash': this.props.mxEvent.getContent().info?.[BLURHASH_FIELD],
});
// many SVGs don't have an intrinsic size if used in <img> elements.
// due to this we have to set our desired width directly.
// this way if the image is forced to shrink, the height adapts appropriately.
const sizing = infoSvg ? { maxHeight, maxWidth, width: maxWidth } : { maxHeight, maxWidth };
const thumbnail = (
<div className="mx_MImageBody_thumbnail_container" style={{ maxHeight, maxWidth, aspectRatio: `${infoWidth}/${infoHeight}` }}>
<SwitchTransition mode="out-in">
@ -465,7 +470,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
</CSSTransition>
</SwitchTransition>
<div style={{ maxHeight, maxWidth }}>
<div style={sizing}>
{ img }
{ gifLabel }
</div>

View file

@ -22,14 +22,12 @@ import WidgetStore from "../../../stores/WidgetStore";
import EventTileBubble from "./EventTileBubble";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
mxEvent: MatrixEvent;
timestamp?: JSX.Element;
}
@replaceableComponent("views.messages.MJitsiWidgetEvent")
export default class MJitsiWidgetEvent extends React.PureComponent<IProps> {
constructor(props) {
super(props);

View file

@ -28,7 +28,6 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
import { getNameForEventRoom, userLabelForEventRoom } from '../../../utils/KeyVerificationStateObserver';
import EventTileBubble from "./EventTileBubble";
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
/* the MatrixEvent to show */
@ -36,7 +35,6 @@ interface IProps {
timestamp?: JSX.Element;
}
@replaceableComponent("views.messages.MKeyVerificationConclusion")
export default class MKeyVerificationConclusion extends React.Component<IProps> {
constructor(props: IProps) {
super(props);

View file

@ -24,7 +24,6 @@ import { _t } from '../../../languageHandler';
import { getNameForEventRoom, userLabelForEventRoom } from '../../../utils/KeyVerificationStateObserver';
import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases';
import EventTileBubble from "./EventTileBubble";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import AccessibleButton from '../elements/AccessibleButton';
import RightPanelStore from '../../../stores/right-panel/RightPanelStore';
@ -33,7 +32,6 @@ interface IProps {
timestamp?: JSX.Element;
}
@replaceableComponent("views.messages.MKeyVerificationRequest")
export default class MKeyVerificationRequest extends React.Component<IProps> {
public componentDidMount() {
const request = this.props.mxEvent.verificationRequest;

View file

@ -15,97 +15,48 @@ limitations under the License.
*/
import React from 'react';
import maplibregl from 'maplibre-gl';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import {
M_ASSET,
LocationAssetType,
ILocationContent,
} from 'matrix-js-sdk/src/@types/location';
import { ClientEvent, IClientWellKnown } from 'matrix-js-sdk/src/client';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { IBodyProps } from "./IBodyProps";
import { _t } from '../../../languageHandler';
import MemberAvatar from '../avatars/MemberAvatar';
import Modal from '../../../Modal';
import {
parseGeoUri,
locationEventGeoUri,
createMap,
getLocationShareErrorMessage,
LocationShareError,
isSelfLocation,
} from '../../../utils/location';
import LocationViewDialog from '../location/LocationViewDialog';
import MatrixClientContext from '../../../contexts/MatrixClientContext';
import TooltipTarget from '../elements/TooltipTarget';
import { Alignment } from '../elements/Tooltip';
import AccessibleButton from '../elements/AccessibleButton';
import { tileServerFromWellKnown } from '../../../utils/WellKnownUtils';
import MatrixClientContext from '../../../contexts/MatrixClientContext';
import LocationViewDialog from '../location/LocationViewDialog';
import Map from '../location/Map';
import SmartMarker from '../location/SmartMarker';
import { IBodyProps } from "./IBodyProps";
interface IState {
error: Error;
}
@replaceableComponent("views.messages.MLocationBody")
export default class MLocationBody extends React.Component<IBodyProps, IState> {
public static contextType = MatrixClientContext;
public context!: React.ContextType<typeof MatrixClientContext>;
private coords: GeolocationCoordinates;
private bodyId: string;
private markerId: string;
private map?: maplibregl.Map = null;
private mapId: string;
constructor(props: IBodyProps) {
super(props);
const randomString = Math.random().toString(16).slice(2, 10);
// multiple instances of same map might be in document
// eg thread and main timeline, reply
const idSuffix = `${props.mxEvent.getId()}_${randomString}`;
this.bodyId = `mx_MLocationBody_${idSuffix}`;
this.markerId = `mx_MLocationBody_marker_${idSuffix}`;
this.coords = parseGeoUri(locationEventGeoUri(this.props.mxEvent));
this.mapId = `mx_MLocationBody_${idSuffix}`;
this.state = {
error: undefined,
};
}
componentDidMount() {
if (this.state.error) {
return;
}
this.context.on(ClientEvent.ClientWellKnown, this.updateStyleUrl);
this.map = createMap(
this.coords,
false,
this.bodyId,
this.markerId,
(e: Error) => this.setState({ error: e }),
);
}
componentWillUnmount() {
this.context.off(ClientEvent.ClientWellKnown, this.updateStyleUrl);
}
private updateStyleUrl = (clientWellKnown: IClientWellKnown) => {
const style = tileServerFromWellKnown(clientWellKnown)?.["map_style_url"];
if (style) {
this.map?.setStyle(style);
}
};
private onClick = (
event: React.MouseEvent<HTMLDivElement, MouseEvent>,
) => {
// Don't open map if we clicked the attribution button
const target = event.target as Element;
if (target.classList.contains("maplibregl-ctrl-attrib-button")) {
return;
}
private onClick = () => {
Modal.createTrackedDialog(
'Location View',
'',
@ -120,38 +71,23 @@ export default class MLocationBody extends React.Component<IBodyProps, IState> {
);
};
private onError = (error) => {
this.setState({ error });
};
render(): React.ReactElement<HTMLDivElement> {
return this.state.error ?
<LocationBodyFallbackContent error={this.state.error} event={this.props.mxEvent} /> :
<LocationBodyContent
mxEvent={this.props.mxEvent}
bodyId={this.bodyId}
markerId={this.markerId}
error={this.state.error}
mapId={this.mapId}
onError={this.onError}
tooltip={_t("Expand map")}
onClick={this.onClick}
/>;
}
}
export function isSelfLocation(locationContent: ILocationContent): boolean {
const asset = M_ASSET.findIn(locationContent) as { type: string };
const assetType = asset?.type ?? LocationAssetType.Self;
return assetType == LocationAssetType.Self;
}
interface ILocationBodyContentProps {
mxEvent: MatrixEvent;
bodyId: string;
markerId: string;
error: Error;
tooltip?: string;
onClick?: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
zoomButtons?: boolean;
onZoomIn?: () => void;
onZoomOut?: () => void;
}
export const LocationBodyFallbackContent: React.FC<{ event: MatrixEvent, error: Error }> = ({ error, event }) => {
const errorType = error?.message as LocationShareError;
const message = `${_t('Unable to load map')}: ${getLocationShareErrorMessage(errorType)}`;
@ -169,75 +105,54 @@ export const LocationBodyFallbackContent: React.FC<{ event: MatrixEvent, error:
</div>;
};
export function LocationBodyContent(props: ILocationBodyContentProps):
React.ReactElement<HTMLDivElement> {
const mapDiv = <div
id={props.bodyId}
onClick={props.onClick}
className="mx_MLocationBody_map"
/>;
interface LocationBodyContentProps {
mxEvent: MatrixEvent;
mapId: string;
tooltip?: string;
onError: (error: Error) => void;
onClick?: () => void;
}
export const LocationBodyContent: React.FC<LocationBodyContentProps> = ({
mxEvent,
mapId,
tooltip,
onError,
onClick,
}) => {
// only pass member to marker when should render avatar marker
const markerRoomMember = isSelfLocation(mxEvent.getContent()) ? mxEvent.sender : undefined;
const geoUri = locationEventGeoUri(mxEvent);
const markerContents = (
isSelfLocation(props.mxEvent.getContent())
? <MemberAvatar
member={props.mxEvent.sender}
width={27}
height={27}
viewUserOnClick={false}
/>
: <div className="mx_MLocationBody_markerContents" />
);
const mapElement = (<Map
id={mapId}
centerGeoUri={geoUri}
onClick={onClick}
onError={onError}
className="mx_MLocationBody_map"
>
{
({ map }) =>
<SmartMarker
map={map}
id={`${mapId}-marker`}
geoUri={geoUri}
roomMember={markerRoomMember}
/>
}
</Map>);
return <div className="mx_MLocationBody">
{
props.tooltip
tooltip
? <TooltipTarget
label={props.tooltip}
label={tooltip}
alignment={Alignment.InnerBottom}
maxParentWidth={450}
>
{ mapDiv }
{ mapElement }
</TooltipTarget>
: mapDiv
}
<div className="mx_MLocationBody_marker" id={props.markerId}>
<div className="mx_MLocationBody_markerBorder">
{ markerContents }
</div>
<div
className="mx_MLocationBody_pointer"
/>
</div>
{
props.zoomButtons
? <ZoomButtons
onZoomIn={props.onZoomIn}
onZoomOut={props.onZoomOut}
/>
: null
: mapElement
}
</div>;
}
interface IZoomButtonsProps {
onZoomIn: () => void;
onZoomOut: () => void;
}
function ZoomButtons(props: IZoomButtonsProps): React.ReactElement<HTMLDivElement> {
return <div className="mx_MLocationBody_zoomButtons">
<AccessibleButton
onClick={props.onZoomIn}
title={_t("Zoom in")}
>
<div className="mx_MLocationBody_zoomButton mx_MLocationBody_plusButton" />
</AccessibleButton>
<AccessibleButton
onClick={props.onZoomOut}
title={_t("Zoom out")}
>
<div className="mx_MLocationBody_zoomButton mx_MLocationBody_minusButton" />
</AccessibleButton>
</div>;
}
};

View file

@ -32,7 +32,6 @@ import {
import { RelatedRelations } from "matrix-js-sdk/src/models/related-relations";
import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import Modal from '../../../Modal';
import { IBodyProps } from "./IBodyProps";
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
@ -210,7 +209,6 @@ export function launchPollEditor(mxEvent: MatrixEvent, getRelationsForEvent?: Ge
}
}
@replaceableComponent("views.messages.MPollBody")
export default class MPollBody extends React.Component<IBodyProps, IState> {
public static contextType = MatrixClientContext;
public context!: React.ContextType<typeof MatrixClientContext>;

View file

@ -17,11 +17,9 @@ limitations under the License.
import React from 'react';
import MImageBody from './MImageBody';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { BLURHASH_FIELD } from "../../../ContentMessages";
import { BLURHASH_FIELD } from "../../../utils/image-media";
import Tooltip from "../elements/Tooltip";
@replaceableComponent("views.messages.MStickerBody")
export default class MStickerBody extends MImageBody {
// Mostly empty to prevent default behaviour of MImageBody
protected onClick = (ev: React.MouseEvent) => {

View file

@ -21,9 +21,8 @@ import { logger } from "matrix-js-sdk/src/logger";
import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
import InlineSpinner from '../elements/InlineSpinner';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromContent } from "../../../customisations/Media";
import { BLURHASH_FIELD } from "../../../ContentMessages";
import { BLURHASH_FIELD } from "../../../utils/image-media";
import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent";
import { IBodyProps } from "./IBodyProps";
import MFileBody from "./MFileBody";
@ -40,7 +39,6 @@ interface IState {
blurhashUrl: string;
}
@replaceableComponent("views.messages.MVideoBody")
export default class MVideoBody extends React.PureComponent<IBodyProps, IState> {
static contextType = RoomContext;
public context!: React.ContextType<typeof RoomContext>;

View file

@ -16,14 +16,12 @@ limitations under the License.
import React from "react";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import InlineSpinner from '../elements/InlineSpinner';
import { _t } from "../../../languageHandler";
import RecordingPlayback from "../audio_messages/RecordingPlayback";
import MAudioBody from "./MAudioBody";
import MFileBody from "./MFileBody";
@replaceableComponent("views.messages.MVoiceMessageBody")
export default class MVoiceMessageBody extends MAudioBody {
// A voice message is an audio file but rendered in a special way.
public render() {

View file

@ -17,12 +17,10 @@ limitations under the License.
import React from "react";
import MAudioBody from "./MAudioBody";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import MVoiceMessageBody from "./MVoiceMessageBody";
import { IBodyProps } from "./IBodyProps";
import { isVoiceMessage } from "../../../utils/EventUtils";
@replaceableComponent("views.messages.MVoiceOrAudioBody")
export default class MVoiceOrAudioBody extends React.PureComponent<IBodyProps> {
public render() {
if (!this.props.forExport && isVoiceMessage(this.props.mxEvent)) {

View file

@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ReactElement, useEffect } from 'react';
import React, { ReactElement, useContext, useEffect } from 'react';
import { EventStatus, MatrixEvent, MatrixEventEvent } from 'matrix-js-sdk/src/models/event';
import classNames from 'classnames';
import { MsgType, RelationType } from 'matrix-js-sdk/src/@types/event';
@ -26,12 +26,11 @@ import type { Relations } from 'matrix-js-sdk/src/models/relations';
import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher/dispatcher';
import ContextMenu, { aboveLeftOf, ContextMenuTooltipButton, useContextMenu } from '../../structures/ContextMenu';
import { isContentActionable, canEditContent, editEvent } from '../../../utils/EventUtils';
import { isContentActionable, canEditContent, editEvent, canCancel } from '../../../utils/EventUtils';
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
import Toolbar from "../../../accessibility/Toolbar";
import { RovingAccessibleTooltipButton, useRovingTabIndex } from "../../../accessibility/RovingTabIndex";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import MessageContextMenu, { canCancel } from "../context_menus/MessageContextMenu";
import MessageContextMenu from "../context_menus/MessageContextMenu";
import Resend from "../../../Resend";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
@ -40,13 +39,14 @@ import SettingsStore from '../../../settings/SettingsStore';
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
import ReplyChain from '../elements/ReplyChain';
import ReactionPicker from "../emojipicker/ReactionPicker";
import { CardContext } from '../right_panel/BaseCard';
import { CardContext } from '../right_panel/context';
import { showThread } from "../../../dispatcher/dispatch-actions/threads";
import { shouldDisplayReply } from '../../../utils/Reply';
import { Key } from "../../../Keyboard";
import { ALTERNATE_KEY_NAME } from "../../../accessibility/KeyboardShortcuts";
import { UserTab } from '../dialogs/UserSettingsDialog';
import { UserTab } from '../dialogs/UserTab';
import { Action } from '../../../dispatcher/actions';
import SdkConfig from "../../../SdkConfig";
interface IOptionsButtonProps {
mxEvent: MatrixEvent;
@ -156,6 +156,81 @@ const ReactButton: React.FC<IReactButtonProps> = ({ mxEvent, reactions, onFocusC
</React.Fragment>;
};
interface IReplyInThreadButton {
mxEvent: MatrixEvent;
}
const ReplyInThreadButton = ({ mxEvent }: IReplyInThreadButton) => {
const context = useContext(CardContext);
const relationType = mxEvent?.getRelation()?.rel_type;
const hasARelation = !!relationType && relationType !== RelationType.Thread;
const firstTimeSeeingThreads = !localStorage.getItem("mx_seen_feature_thread");
const threadsEnabled = SettingsStore.getValue("feature_thread");
if (!threadsEnabled && !Thread.hasServerSideSupport) {
// hide the prompt if the user would only have degraded mode
return null;
}
const onClick = (): void => {
if (firstTimeSeeingThreads) {
localStorage.setItem("mx_seen_feature_thread", "true");
}
if (!SettingsStore.getValue("feature_thread")) {
dis.dispatch({
action: Action.ViewUserSettings,
initialTabId: UserTab.Labs,
});
} else if (mxEvent.isThreadRelation) {
showThread({
rootEvent: mxEvent.getThread().rootEvent,
initialEvent: mxEvent,
scroll_into_view: true,
highlighted: true,
push: context.isCard,
});
} else {
showThread({
rootEvent: mxEvent,
push: context.isCard,
});
}
};
return <RovingAccessibleTooltipButton
className="mx_MessageActionBar_maskButton mx_MessageActionBar_threadButton"
disabled={hasARelation}
tooltip={<>
<div className="mx_Tooltip_title">
{ !hasARelation
? _t("Reply in thread")
: _t("Can't create a thread from an event with an existing relation") }
</div>
{ !hasARelation && (
<div className="mx_Tooltip_sub">
{ SettingsStore.getValue("feature_thread")
? _t("Beta feature")
: _t("Beta feature. Click to learn more.")
}
</div>
) }
</>}
title={!hasARelation
? _t("Reply in thread")
: _t("Can't create a thread from an event with an existing relation")}
onClick={onClick}
>
{ firstTimeSeeingThreads && !threadsEnabled && (
<div className="mx_Indicator" />
) }
</RovingAccessibleTooltipButton>;
};
interface IMessageActionBarProps {
mxEvent: MatrixEvent;
reactions?: Relations;
@ -173,7 +248,6 @@ interface IMessageActionBarProps {
) => Relations;
}
@replaceableComponent("views.messages.MessageActionBar")
export default class MessageActionBar extends React.PureComponent<IMessageActionBarProps> {
public static contextType = RoomContext;
@ -225,21 +299,6 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
});
};
private onThreadClick = (isCard: boolean): void => {
if (localStorage.getItem("mx_seen_feature_thread") === null) {
localStorage.setItem("mx_seen_feature_thread", "true");
}
if (!SettingsStore.getValue("feature_thread")) {
dis.dispatch({
action: Action.ViewUserSettings,
initialTabId: UserTab.Labs,
});
} else {
showThread({ rootEvent: this.props.mxEvent, push: isCard });
}
};
private onEditClick = (): void => {
editEvent(this.props.mxEvent, this.context.timelineRenderingType, this.props.getRelationsForEvent);
};
@ -254,6 +313,15 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
return null;
}
if (!SettingsStore.getBetaInfo("feature_thread") &&
!SettingsStore.getValue("feature_thread") &&
!SdkConfig.get("show_labs_settings")
) {
// Hide the beta prompt if there is no UI to enable it,
// e.g if config.json disables it and doesn't enable show labs flags
return false;
}
const inNotThreadTimeline = this.context.timelineRenderingType !== TimelineRenderingType.Thread;
const isAllowedMessageType = !this.forbiddenThreadHeadMsgType.includes(
@ -316,44 +384,7 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
key="cancel"
/>;
const relationType = this.props.mxEvent?.getRelation()?.rel_type;
const hasARelation = !!relationType && relationType !== RelationType.Thread;
const firstTimeSeeingThreads = localStorage.getItem("mx_seen_feature_thread") === null &&
!SettingsStore.getValue("feature_thread");
const threadTooltipButton = <CardContext.Consumer key="thread">
{ context =>
<RovingAccessibleTooltipButton
className="mx_MessageActionBar_maskButton mx_MessageActionBar_threadButton"
disabled={hasARelation}
tooltip={<>
<div className="mx_Tooltip_title">
{ !hasARelation
? _t("Reply in thread")
: _t("Can't create a thread from an event with an existing relation") }
</div>
{ !hasARelation && (
<div className="mx_Tooltip_sub">
{ SettingsStore.getValue("feature_thread")
? _t("Beta feature")
: _t("Beta feature. Click to learn more.")
}
</div>
) }
</>}
title={!hasARelation
? _t("Reply in thread")
: _t("Can't create a thread from an event with an existing relation")}
onClick={this.onThreadClick.bind(null, context.isCard)}
>
{ firstTimeSeeingThreads && (
<div className="mx_Indicator" />
) }
</RovingAccessibleTooltipButton>
}
</CardContext.Consumer>;
const threadTooltipButton = <ReplyInThreadButton mxEvent={this.props.mxEvent} />;
// We show a different toolbar for failed events, so detect that first.
const mxEvent = this.props.mxEvent;

View file

@ -17,27 +17,36 @@ limitations under the License.
import React, { createRef } from 'react';
import { EventType, MsgType } from "matrix-js-sdk/src/@types/event";
import { Relations } from 'matrix-js-sdk/src/models/relations';
import { M_BEACON_INFO } from 'matrix-js-sdk/src/@types/beacon';
import { M_LOCATION } from 'matrix-js-sdk/src/@types/location';
import { M_POLL_START } from "matrix-events-sdk";
import * as sdk from '../../../index';
import SettingsStore from "../../../settings/SettingsStore";
import { Mjolnir } from "../../../mjolnir/Mjolnir";
import RedactedBody from "./RedactedBody";
import UnknownBody from "./UnknownBody";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { IMediaBody } from "./IMediaBody";
import { IOperableEventTile } from "../context_menus/MessageContextMenu";
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
import { ReactAnyComponent } from "../../../@types/common";
import { IBodyProps } from "./IBodyProps";
import MatrixClientContext from '../../../contexts/MatrixClientContext';
import TextualBody from "./TextualBody";
import MImageBody from "./MImageBody";
import MFileBody from "./MFileBody";
import MVoiceOrAudioBody from "./MVoiceOrAudioBody";
import MVideoBody from "./MVideoBody";
import MStickerBody from "./MStickerBody";
import MPollBody from "./MPollBody";
import MLocationBody from "./MLocationBody";
import MjolnirBody from "./MjolnirBody";
import MBeaconBody from "./MBeaconBody";
import { IEventTileOps } from "../rooms/EventTile";
// onMessageAllowed is handled internally
interface IProps extends Omit<IBodyProps, "onMessageAllowed"> {
interface IProps extends Omit<IBodyProps, "onMessageAllowed" | "mediaEventHelper"> {
/* overrides for the msgtype-specific components, used by ReplyTile to override file rendering */
overrideBodyTypes?: Record<string, React.Component>;
overrideEventTypes?: Record<string, React.Component>;
overrideBodyTypes?: Record<string, typeof React.Component>;
overrideEventTypes?: Record<string, typeof React.Component>;
// helper function to access relations for this event
getRelationsForEvent?: (eventId: string, relationType: string, eventType: string) => Relations;
@ -45,7 +54,10 @@ interface IProps extends Omit<IBodyProps, "onMessageAllowed"> {
isSeeingThroughMessageHiddenForModeration?: boolean;
}
@replaceableComponent("views.messages.MessageEvent")
export interface IOperableEventTile {
getEventTileOps(): IEventTileOps;
}
export default class MessageEvent extends React.Component<IProps> implements IMediaBody, IOperableEventTile {
private body: React.RefObject<React.Component | IOperableEventTile> = createRef();
private mediaHelper: MediaEventHelper;
@ -72,23 +84,27 @@ export default class MessageEvent extends React.Component<IProps> implements IMe
}
}
private get bodyTypes(): Record<string, React.Component> {
private get bodyTypes(): Record<string, typeof React.Component> {
return {
[MsgType.Text]: sdk.getComponent('messages.TextualBody'),
[MsgType.Notice]: sdk.getComponent('messages.TextualBody'),
[MsgType.Emote]: sdk.getComponent('messages.TextualBody'),
[MsgType.Image]: sdk.getComponent('messages.MImageBody'),
[MsgType.File]: sdk.getComponent('messages.MFileBody'),
[MsgType.Audio]: sdk.getComponent('messages.MVoiceOrAudioBody'),
[MsgType.Video]: sdk.getComponent('messages.MVideoBody'),
[MsgType.Text]: TextualBody,
[MsgType.Notice]: TextualBody,
[MsgType.Emote]: TextualBody,
[MsgType.Image]: MImageBody,
[MsgType.File]: MFileBody,
[MsgType.Audio]: MVoiceOrAudioBody,
[MsgType.Video]: MVideoBody,
...(this.props.overrideBodyTypes || {}),
};
}
private get evTypes(): Record<string, React.Component> {
private get evTypes(): Record<string, React.ComponentType<Partial<IBodyProps>>> {
return {
[EventType.Sticker]: sdk.getComponent('messages.MStickerBody'),
[EventType.Sticker]: MStickerBody,
[M_POLL_START.name]: MPollBody,
[M_POLL_START.altName]: MPollBody,
[M_BEACON_INFO.name]: MBeaconBody,
[M_BEACON_INFO.altName]: MBeaconBody,
...(this.props.overrideEventTypes || {}),
};
@ -110,7 +126,7 @@ export default class MessageEvent extends React.Component<IProps> implements IMe
const content = this.props.mxEvent.getContent();
const type = this.props.mxEvent.getType();
const msgtype = content.msgtype;
let BodyType: ReactAnyComponent = RedactedBody;
let BodyType: React.ComponentType<Partial<IBodyProps>> | ReactAnyComponent = RedactedBody;
if (!this.props.mxEvent.isRedacted()) {
// only resolve BodyType if event is not redacted
if (type && this.evTypes[type]) {
@ -125,17 +141,12 @@ export default class MessageEvent extends React.Component<IProps> implements IMe
BodyType = UnknownBody;
}
// TODO: move to eventTypes when Polls spec stabilises
if (M_POLL_START.matches(type)) {
BodyType = sdk.getComponent('messages.MPollBody');
}
// TODO: move to eventTypes when location sharing spec stabilises
if (
M_LOCATION.matches(type) ||
(type === EventType.RoomMessage && msgtype === MsgType.Location)
) {
BodyType = sdk.getComponent('messages.MLocationBody');
BodyType = MLocationBody;
}
}
@ -149,7 +160,7 @@ export default class MessageEvent extends React.Component<IProps> implements IMe
const serverBanned = Mjolnir.sharedInstance().isServerBanned(userDomain);
if (userBanned || serverBanned) {
BodyType = sdk.getComponent('messages.MjolnirBody');
BodyType = MjolnirBody;
}
}
}

View file

@ -18,7 +18,6 @@ limitations under the License.
import React from 'react';
import { formatFullDate, formatTime, formatFullTime, formatRelativeTime } from '../../../DateUtils';
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
ts: number;
@ -28,7 +27,6 @@ interface IProps {
showRelative?: boolean;
}
@replaceableComponent("views.messages.MessageTimestamp")
export default class MessageTimestamp extends React.Component<IProps> {
public render() {
const date = new Date(this.props.ts);

View file

@ -18,7 +18,6 @@ import React from 'react';
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import AccessibleButton from '../elements/AccessibleButton';
interface IProps {
@ -26,7 +25,6 @@ interface IProps {
onMessageAllowed: () => void;
}
@replaceableComponent("views.messages.MjolnirBody")
export default class MjolnirBody extends React.Component<IProps> {
private onAllowClick = (e: React.MouseEvent): void => {
e.preventDefault();

View file

@ -21,7 +21,6 @@ import { Relations, RelationsEvent } from "matrix-js-sdk/src/models/relations";
import { _t } from '../../../languageHandler';
import { isContentActionable } from '../../../utils/EventUtils';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton";
import ContextMenu, { aboveLeftOf, useContextMenu } from "../../structures/ContextMenu";
import ReactionPicker from "../emojipicker/ReactionPicker";
@ -74,7 +73,6 @@ interface IState {
showAll: boolean;
}
@replaceableComponent("views.messages.ReactionsRow")
export default class ReactionsRow extends React.PureComponent<IProps, IState> {
static contextType = RoomContext;
public context!: React.ContextType<typeof RoomContext>;

View file

@ -21,7 +21,6 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { _t } from '../../../languageHandler';
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
import dis from "../../../dispatcher/dispatcher";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import ReactionsRowButtonTooltip from "./ReactionsRowButtonTooltip";
import AccessibleButton from "../elements/AccessibleButton";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
@ -46,7 +45,6 @@ interface IState {
tooltipVisible: boolean;
}
@replaceableComponent("views.messages.ReactionsRowButton")
export default class ReactionsRowButton extends React.PureComponent<IProps, IState> {
static contextType = MatrixClientContext;

View file

@ -20,7 +20,6 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { unicodeToShortcode } from '../../../HtmlUtils';
import { _t } from '../../../languageHandler';
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import Tooltip from "../elements/Tooltip";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
@ -34,7 +33,6 @@ interface IProps {
visible: boolean;
}
@replaceableComponent("views.messages.ReactionsRowButtonTooltip")
export default class ReactionsRowButtonTooltip extends React.PureComponent<IProps> {
static contextType = MatrixClientContext;

View file

@ -23,7 +23,6 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
import AccessibleButton from '../elements/AccessibleButton';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media";
import RoomAvatar from "../avatars/RoomAvatar";
import ImageView from "../elements/ImageView";
@ -33,7 +32,6 @@ interface IProps {
mxEvent: MatrixEvent;
}
@replaceableComponent("views.messages.RoomAvatarEvent")
export default class RoomAvatarEvent extends React.Component<IProps> {
private onAvatarClick = (): void => {
const cli = MatrixClientPeg.get();

View file

@ -24,7 +24,6 @@ import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import EventTileBubble from "./EventTileBubble";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
interface IProps {
@ -33,7 +32,6 @@ interface IProps {
timestamp?: JSX.Element;
}
@replaceableComponent("views.messages.RoomCreate")
export default class RoomCreate extends React.Component<IProps> {
private onLinkClicked = (e: React.MouseEvent): void => {
e.preventDefault();

View file

@ -19,7 +19,6 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { MsgType } from "matrix-js-sdk/src/@types/event";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import DisambiguatedProfile from "./DisambiguatedProfile";
import RoomContext, { TimelineRenderingType } from '../../../contexts/RoomContext';
import SettingsStore from "../../../settings/SettingsStore";
@ -30,7 +29,6 @@ interface IProps {
onClick?(): void;
}
@replaceableComponent("views.messages.SenderProfile")
export default class SenderProfile extends React.PureComponent<IProps> {
public static contextType = MatrixClientContext;
public context!: React.ContextType<typeof MatrixClientContext>;

View file

@ -33,7 +33,6 @@ import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
import { isPermalinkHost, tryTransformPermalinkToLocalHref } from "../../../utils/permalinks/Permalinks";
import { copyPlaintext } from "../../../utils/strings";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import UIStore from "../../../stores/UIStore";
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
import { Action } from "../../../dispatcher/actions";
@ -59,7 +58,6 @@ interface IState {
widgetHidden: boolean;
}
@replaceableComponent("views.messages.TextualBody")
export default class TextualBody extends React.Component<IBodyProps, IState> {
private readonly contentRef = createRef<HTMLSpanElement>();

View file

@ -19,18 +19,16 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import RoomContext from "../../../contexts/RoomContext";
import * as TextForEvent from "../../../TextForEvent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
mxEvent: MatrixEvent;
}
@replaceableComponent("views.messages.TextualEvent")
export default class TextualEvent extends React.Component<IProps> {
static contextType = RoomContext;
public render() {
const text = TextForEvent.textForEvent(this.props.mxEvent, true, this.context?.showHiddenEventsInTimeline);
const text = TextForEvent.textForEvent(this.props.mxEvent, true, this.context?.showHiddenEvents);
if (!text) return null;
return <div className="mx_TextualEvent">{ text }</div>;
}

View file

@ -21,7 +21,6 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
import SdkConfig from "../../../SdkConfig";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import BugReportDialog from '../dialogs/BugReportDialog';
import AccessibleButton from '../elements/AccessibleButton';
import SettingsStore from "../../../settings/SettingsStore";
@ -37,7 +36,6 @@ interface IState {
error: Error;
}
@replaceableComponent("views.messages.TileErrorBoundary")
export default class TileErrorBoundary extends React.Component<IProps, IState> {
constructor(props) {
super(props);

View file

@ -18,7 +18,6 @@ import React from 'react';
import { MatrixEvent, MatrixEventEvent } from 'matrix-js-sdk/src/matrix';
import classNames from 'classnames';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { _t } from '../../../languageHandler';
import AccessibleButton from '../elements/AccessibleButton';
@ -31,7 +30,6 @@ interface IState {
expanded: boolean;
}
@replaceableComponent("views.messages.ViewSourceEvent")
export default class ViewSourceEvent extends React.PureComponent<IProps, IState> {
constructor(props) {
super(props);