Merge branch 'develop' into export-conversations

This commit is contained in:
Jaiwanth 2021-09-22 18:07:01 +05:30
commit 94e4fb71c1
498 changed files with 13790 additions and 23008 deletions

View file

@ -17,7 +17,7 @@ limitations under the License.
import React, { createRef } from 'react';
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { _t, _td } from '../../../languageHandler';
import { _t } from '../../../languageHandler';
import MemberAvatar from '../avatars/MemberAvatar';
import CallEventGrouper, { CallEventGrouperEvent, CustomCallState } from '../../structures/CallEventGrouper';
import AccessibleButton from '../elements/AccessibleButton';
@ -26,6 +26,7 @@ import InfoTooltip, { InfoTooltipKind } from '../elements/InfoTooltip';
import classNames from 'classnames';
import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
import { formatCallTime } from "../../../DateUtils";
import Clock from "../audio_messages/Clock";
const MAX_NON_NARROW_WIDTH = 450 / 70 * 100;
@ -38,13 +39,9 @@ interface IState {
callState: CallState | CustomCallState;
silenced: boolean;
narrow: boolean;
length: number;
}
const TEXTUAL_STATES: Map<CallState | CustomCallState, string> = new Map([
[CallState.Connected, _td("Connected")],
[CallState.Connecting, _td("Connecting")],
]);
export default class CallEvent extends React.PureComponent<IProps, IState> {
private wrapperElement = createRef<HTMLDivElement>();
private resizeObserver: ResizeObserver;
@ -56,12 +53,14 @@ export default class CallEvent extends React.PureComponent<IProps, IState> {
callState: this.props.callEventGrouper.state,
silenced: false,
narrow: false,
length: 0,
};
}
componentDidMount() {
this.props.callEventGrouper.addListener(CallEventGrouperEvent.StateChanged, this.onStateChanged);
this.props.callEventGrouper.addListener(CallEventGrouperEvent.SilencedChanged, this.onSilencedChanged);
this.props.callEventGrouper.addListener(CallEventGrouperEvent.LengthChanged, this.onLengthChanged);
this.resizeObserver = new ResizeObserver(this.resizeObserverCallback);
this.resizeObserver.observe(this.wrapperElement.current);
@ -70,10 +69,15 @@ export default class CallEvent extends React.PureComponent<IProps, IState> {
componentWillUnmount() {
this.props.callEventGrouper.removeListener(CallEventGrouperEvent.StateChanged, this.onStateChanged);
this.props.callEventGrouper.removeListener(CallEventGrouperEvent.SilencedChanged, this.onSilencedChanged);
this.props.callEventGrouper.removeListener(CallEventGrouperEvent.LengthChanged, this.onLengthChanged);
this.resizeObserver.disconnect();
}
private onLengthChanged = (length: number): void => {
this.setState({ length });
};
private resizeObserverCallback = (entries: ResizeObserverEntry[]): void => {
const wrapperElementEntry = entries.find((entry) => entry.target === this.wrapperElement.current);
if (!wrapperElementEntry) return;
@ -214,10 +218,17 @@ export default class CallEvent extends React.PureComponent<IProps, IState> {
</div>
);
}
if (Array.from(TEXTUAL_STATES.keys()).includes(state)) {
if (state === CallState.Connected) {
return (
<div className="mx_CallEvent_content">
{ TEXTUAL_STATES.get(state) }
<Clock seconds={this.state.length} />
</div>
);
}
if (state === CallState.Connecting) {
return (
<div className="mx_CallEvent_content">
{ _t("Connecting") }
</div>
);
}

View file

@ -16,26 +16,38 @@ limitations under the License.
import React, { forwardRef, useContext } from 'react';
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { IRoomEncryption } from "matrix-js-sdk/src/crypto/RoomList";
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import EventTileBubble from "./EventTileBubble";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import DMRoomMap from "../../../utils/DMRoomMap";
import { objectHasDiff } from "../../../utils/objects";
interface IProps {
mxEvent: MatrixEvent;
}
const ALGORITHM = "m.megolm.v1.aes-sha2";
const EncryptionEvent = forwardRef<HTMLDivElement, IProps>(({ mxEvent }, ref) => {
const cli = useContext(MatrixClientContext);
const roomId = mxEvent.getRoomId();
const isRoomEncrypted = MatrixClientPeg.get().isRoomEncrypted(roomId);
if (mxEvent.getContent().algorithm === 'm.megolm.v1.aes-sha2' && isRoomEncrypted) {
const prevContent = mxEvent.getPrevContent() as IRoomEncryption;
const content = mxEvent.getContent<IRoomEncryption>();
// if no change happened then skip rendering this, a shallow check is enough as all known fields are top-level.
if (!objectHasDiff(prevContent, content)) return null; // nop
if (content.algorithm === ALGORITHM && isRoomEncrypted) {
let subtitle: string;
const dmPartner = DMRoomMap.shared().getUserIdForRoomId(roomId);
if (dmPartner) {
if (prevContent.algorithm === ALGORITHM) {
subtitle = _t("Some encryption parameters have been changed.");
} else if (dmPartner) {
const displayName = cli?.getRoom(roomId)?.getMember(dmPartner)?.rawDisplayName || dmPartner;
subtitle = _t("Messages here are end-to-end encrypted. " +
"Verify %(displayName)s in their profile - tap on their avatar.", { displayName });
@ -49,7 +61,9 @@ const EncryptionEvent = forwardRef<HTMLDivElement, IProps>(({ mxEvent }, ref) =>
title={_t("Encryption enabled")}
subtitle={subtitle}
/>;
} else if (isRoomEncrypted) {
}
if (isRoomEncrypted) {
return <EventTileBubble
className="mx_cryptoEvent mx_cryptoEvent_icon"
title={_t("Encryption enabled")}

View file

@ -24,6 +24,8 @@ import { IMediaEventContent } from "../../../customisations/models/IMediaEventCo
import MFileBody from "./MFileBody";
import { IBodyProps } from "./IBodyProps";
import { PlaybackManager } from "../../../audio/PlaybackManager";
import { isVoiceMessage } from "../../../utils/EventUtils";
import { PlaybackQueue } from "../../../audio/PlaybackQueue";
interface IState {
error?: Error;
@ -67,6 +69,10 @@ export default class MAudioBody extends React.PureComponent<IBodyProps, IState>
playback.clockInfo.populatePlaceholdersFrom(this.props.mxEvent);
this.setState({ playback });
if (isVoiceMessage(this.props.mxEvent)) {
PlaybackQueue.forRoom(this.props.mxEvent.getRoomId()).unsortedEnqueue(this.props.mxEvent, playback);
}
// Note: the components later on will handle preparing the Playback class for us.
}

View file

@ -29,6 +29,8 @@ import { IBodyProps } from "./IBodyProps";
import { FileDownloader } from "../../../utils/FileDownloader";
import TextWithTooltip from "../elements/TextWithTooltip";
import { logger } from "matrix-js-sdk/src/logger";
export let DOWNLOAD_ICON_URL; // cached copy of the download.svg asset for the sandboxed iframe later on
async function cacheDownloadIcon() {
@ -293,7 +295,7 @@ export default class MFileBody extends React.Component<IProps, IState> {
if (["application/pdf"].includes(fileType) && !fileTooBig) {
// We want to force a download on this type, so use an onClick handler.
downloadProps["onClick"] = (e) => {
console.log(`Downloading ${fileType} as blob (unencrypted)`);
logger.log(`Downloading ${fileType} as blob (unencrypted)`);
// Avoid letting the <a> do its thing
e.preventDefault();

View file

@ -55,6 +55,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
static contextType = MatrixClientContext;
private unmounted = true;
private image = createRef<HTMLImageElement>();
private timeout?: number;
constructor(props: IBodyProps) {
super(props);
@ -116,6 +117,17 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
params.fileSize = content.info.size;
}
if (this.image.current) {
const clientRect = this.image.current.getBoundingClientRect();
params.thumbnailInfo = {
width: clientRect.width,
height: clientRect.height,
positionX: clientRect.x,
positionY: clientRect.y,
};
}
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true);
}
};
@ -128,7 +140,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
private onImageEnter = (e: React.MouseEvent<HTMLImageElement>): void => {
this.setState({ hover: true });
if (!this.state.showImage || !this.isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
if (!this.state.showImage || !this.isGif() || SettingsStore.getValue("autoplayGifs")) {
return;
}
const imgElement = e.currentTarget;
@ -138,7 +150,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
private onImageLeave = (e: React.MouseEvent<HTMLImageElement>): void => {
this.setState({ hover: false });
if (!this.state.showImage || !this.isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
if (!this.state.showImage || !this.isGif() || SettingsStore.getValue("autoplayGifs")) {
return;
}
const imgElement = e.currentTarget;
@ -146,12 +158,14 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
};
private onImageError = (): void => {
this.clearBlurhashTimeout();
this.setState({
imgError: true,
});
};
private onImageLoad = (): void => {
this.clearBlurhashTimeout();
this.props.onHeightChanged();
let loadedImageDimensions;
@ -270,6 +284,13 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
}
}
private clearBlurhashTimeout() {
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = undefined;
}
}
componentDidMount() {
this.unmounted = false;
this.context.on('sync', this.onClientSync);
@ -284,8 +305,9 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
} // else don't download anything because we don't want to display anything.
// Add a 150ms timer for blurhash to first appear.
if (this.media.isEncrypted) {
setTimeout(() => {
if (this.props.mxEvent.getContent().info?.[BLURHASH_FIELD]) {
this.clearBlurhashTimeout();
this.timeout = setTimeout(() => {
if (!this.state.imgLoaded || !this.state.imgError) {
this.setState({
placeholder: 'blurhash',
@ -298,6 +320,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
componentWillUnmount() {
this.unmounted = true;
this.context.removeListener('sync', this.onClientSync);
this.clearBlurhashTimeout();
}
protected messageContent(
@ -390,13 +413,13 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
showPlaceholder = false; // because we're hiding the image, so don't show the placeholder.
}
if (this.isGif() && !SettingsStore.getValue("autoplayGifsAndVideos") && !this.state.hover) {
if (this.isGif() && !SettingsStore.getValue("autoplayGifs") && !this.state.hover) {
gifLabel = <p className="mx_MImageBody_gifLabel">GIF</p>;
}
const classes = classNames({
'mx_MImageBody_thumbnail': true,
'mx_MImageBody_thumbnail--blurhash': this.props.mxEvent.getContent().info[BLURHASH_FIELD],
'mx_MImageBody_thumbnail--blurhash': this.props.mxEvent.getContent().info?.[BLURHASH_FIELD],
});
// This has incredibly broken types.
@ -449,7 +472,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
// Overidden by MStickerBody
protected getPlaceholder(width: number, height: number): JSX.Element {
const blurhash = this.props.mxEvent.getContent().info[BLURHASH_FIELD];
const blurhash = this.props.mxEvent.getContent().info?.[BLURHASH_FIELD];
if (blurhash) {
if (this.state.placeholder === 'no-image') {
@ -491,7 +514,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
const contentUrl = this.getContentUrl();
let thumbUrl;
if (this.props.forExport || (this.isGif() && SettingsStore.getValue("autoplayGifsAndVideos"))) {
if (this.props.forExport || (this.isGif() && SettingsStore.getValue("autoplayGifs"))) {
thumbUrl = contentUrl;
} else {
thumbUrl = this.getThumbUrl();

View file

@ -43,7 +43,7 @@ export default class MStickerBody extends MImageBody {
// Placeholder to show in place of the sticker image if
// img onLoad hasn't fired yet.
protected getPlaceholder(width: number, height: number): JSX.Element {
if (this.props.mxEvent.getContent().info[BLURHASH_FIELD]) return super.getPlaceholder(width, height);
if (this.props.mxEvent.getContent().info?.[BLURHASH_FIELD]) return super.getPlaceholder(width, height);
return <img src={require("../../../../res/img/icons-show-stickers.svg")} width="75" height="75" />;
}

View file

@ -27,6 +27,8 @@ import { IMediaEventContent } from "../../../customisations/models/IMediaEventCo
import { IBodyProps } from "./IBodyProps";
import MFileBody from "./MFileBody";
import { logger } from "matrix-js-sdk/src/logger";
interface IState {
decryptedUrl?: string;
decryptedThumbnailUrl?: string;
@ -151,14 +153,14 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
}
async componentDidMount() {
const autoplay = SettingsStore.getValue("autoplayGifsAndVideos") as boolean;
const autoplay = SettingsStore.getValue("autoplayVideo") as boolean;
this.loadBlurhash();
if (this.props.mediaEventHelper.media.isEncrypted && this.state.decryptedUrl === null) {
try {
const thumbnailUrl = await this.props.mediaEventHelper.thumbnailUrl.value;
if (autoplay) {
console.log("Preloading video");
logger.log("Preloading video");
this.setState({
decryptedUrl: await this.props.mediaEventHelper.sourceUrl.value,
decryptedThumbnailUrl: thumbnailUrl,
@ -166,7 +168,7 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
});
this.props.onHeightChanged();
} else {
console.log("NOT preloading video");
logger.log("NOT preloading video");
const content = this.props.mxEvent.getContent<IMediaEventContent>();
this.setState({
// For Chrome and Electron, we need to set some non-empty `src` to
@ -220,7 +222,7 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
render() {
const content = this.props.mxEvent.getContent();
const autoplay = SettingsStore.getValue("autoplayGifsAndVideos");
const autoplay = SettingsStore.getValue("autoplayVideo");
if (this.state.error !== null) {
return (

View file

@ -19,14 +19,12 @@ 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() {
// MSC2516 is a legacy identifier. See https://github.com/matrix-org/matrix-doc/pull/3245
const isVoiceMessage = !!this.props.mxEvent.getContent()['org.matrix.msc2516.voice']
|| !!this.props.mxEvent.getContent()['org.matrix.msc3245.voice'];
if (!this.props.forExport && isVoiceMessage) {
if (!this.props.forExport && isVoiceMessage(this.props.mxEvent)) {
return <MVoiceMessageBody {...this.props} />;
} else {
return <MAudioBody {...this.props} />;

View file

@ -17,12 +17,13 @@ limitations under the License.
*/
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { EventStatus } from 'matrix-js-sdk/src/models/event';
import { MatrixEvent, EventStatus } from 'matrix-js-sdk/src/models/event';
import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher';
import { Action } from '../../../dispatcher/actions';
import { RightPanelPhases } from '../../../stores/RightPanelStorePhases';
import { aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu } from '../../structures/ContextMenu';
import { isContentActionable, canEditContent } from '../../../utils/EventUtils';
import RoomContext from "../../../contexts/RoomContext";
@ -34,48 +35,66 @@ import Resend from "../../../Resend";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
import DownloadActionButton from "./DownloadActionButton";
import SettingsStore from '../../../settings/SettingsStore';
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
import ReplyThread from '../elements/ReplyThread';
const OptionsButton = ({ mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange }) => {
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
const [onFocus, isActive, ref] = useRovingTabIndex(button);
useEffect(() => {
onFocusChange(menuDisplayed);
}, [onFocusChange, menuDisplayed]);
interface IOptionsButtonProps {
mxEvent: MatrixEvent;
getTile: () => any; // TODO: FIXME, haven't figured out what the return type is here
getReplyThread: () => ReplyThread;
permalinkCreator: RoomPermalinkCreator;
onFocusChange: (menuDisplayed: boolean) => void;
}
let contextMenu;
if (menuDisplayed) {
const MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu');
const OptionsButton: React.FC<IOptionsButtonProps> =
({ mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange }) => {
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
const [onFocus, isActive, ref] = useRovingTabIndex(button);
useEffect(() => {
onFocusChange(menuDisplayed);
}, [onFocusChange, menuDisplayed]);
const tile = getTile && getTile();
const replyThread = getReplyThread && getReplyThread();
let contextMenu;
if (menuDisplayed) {
const MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu');
const buttonRect = button.current.getBoundingClientRect();
contextMenu = <MessageContextMenu
{...aboveLeftOf(buttonRect)}
mxEvent={mxEvent}
permalinkCreator={permalinkCreator}
eventTileOps={tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined}
collapseReplyThread={replyThread && replyThread.canCollapse() ? replyThread.collapse : undefined}
onFinished={closeMenu}
/>;
}
const tile = getTile && getTile();
const replyThread = getReplyThread && getReplyThread();
return <React.Fragment>
<ContextMenuTooltipButton
className="mx_MessageActionBar_maskButton mx_MessageActionBar_optionsButton"
title={_t("Options")}
onClick={openMenu}
isExpanded={menuDisplayed}
inputRef={ref}
onFocus={onFocus}
tabIndex={isActive ? 0 : -1}
/>
const buttonRect = button.current.getBoundingClientRect();
contextMenu = <MessageContextMenu
{...aboveLeftOf(buttonRect)}
mxEvent={mxEvent}
permalinkCreator={permalinkCreator}
eventTileOps={tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined}
collapseReplyThread={replyThread && replyThread.canCollapse() ? replyThread.collapse : undefined}
onFinished={closeMenu}
/>;
}
{ contextMenu }
</React.Fragment>;
};
return <React.Fragment>
<ContextMenuTooltipButton
className="mx_MessageActionBar_maskButton mx_MessageActionBar_optionsButton"
title={_t("Options")}
onClick={openMenu}
isExpanded={menuDisplayed}
inputRef={ref}
onFocus={onFocus}
tabIndex={isActive ? 0 : -1}
/>
const ReactButton = ({ mxEvent, reactions, onFocusChange }) => {
{ contextMenu }
</React.Fragment>;
};
interface IReactButtonProps {
mxEvent: MatrixEvent;
reactions: any; // TODO: types
onFocusChange: (menuDisplayed: boolean) => void;
}
const ReactButton: React.FC<IReactButtonProps> = ({ mxEvent, reactions, onFocusChange }) => {
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
const [onFocus, isActive, ref] = useRovingTabIndex(button);
useEffect(() => {
@ -106,21 +125,21 @@ const ReactButton = ({ mxEvent, reactions, onFocusChange }) => {
</React.Fragment>;
};
interface IMessageActionBarProps {
mxEvent: MatrixEvent;
// The Relations model from the JS SDK for reactions to `mxEvent`
reactions?: any; // TODO: types
permalinkCreator?: RoomPermalinkCreator;
getTile: () => any; // TODO: FIXME, haven't figured out what the return type is here
getReplyThread?: () => ReplyThread;
onFocusChange?: (menuDisplayed: boolean) => void;
}
@replaceableComponent("views.messages.MessageActionBar")
export default class MessageActionBar extends React.PureComponent {
static propTypes = {
mxEvent: PropTypes.object.isRequired,
// The Relations model from the JS SDK for reactions to `mxEvent`
reactions: PropTypes.object,
permalinkCreator: PropTypes.object,
getTile: PropTypes.func,
getReplyThread: PropTypes.func,
onFocusChange: PropTypes.func,
};
export default class MessageActionBar extends React.PureComponent<IMessageActionBarProps> {
public static contextType = RoomContext;
static contextType = RoomContext;
componentDidMount() {
public componentDidMount(): void {
if (this.props.mxEvent.status && this.props.mxEvent.status !== EventStatus.SENT) {
this.props.mxEvent.on("Event.status", this.onSent);
}
@ -134,43 +153,54 @@ export default class MessageActionBar extends React.PureComponent {
this.props.mxEvent.on("Event.beforeRedaction", this.onBeforeRedaction);
}
componentWillUnmount() {
public componentWillUnmount(): void {
this.props.mxEvent.off("Event.status", this.onSent);
this.props.mxEvent.off("Event.decrypted", this.onDecrypted);
this.props.mxEvent.off("Event.beforeRedaction", this.onBeforeRedaction);
}
onDecrypted = () => {
private onDecrypted = (): void => {
// When an event decrypts, it is likely to change the set of available
// actions, so we force an update to check again.
this.forceUpdate();
};
onBeforeRedaction = () => {
private onBeforeRedaction = (): void => {
// When an event is redacted, we can't edit it so update the available actions.
this.forceUpdate();
};
onSent = () => {
private onSent = (): void => {
// When an event is sent and echoed the possible actions change.
this.forceUpdate();
};
onFocusChange = (focused) => {
private onFocusChange = (focused: boolean): void => {
if (!this.props.onFocusChange) {
return;
}
this.props.onFocusChange(focused);
};
onReplyClick = (ev) => {
private onReplyClick = (ev: React.MouseEvent): void => {
dis.dispatch({
action: 'reply_to_event',
event: this.props.mxEvent,
});
};
onEditClick = (ev) => {
private onThreadClick = (): void => {
dis.dispatch({
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.ThreadView,
allowClose: false,
refireParams: {
event: this.props.mxEvent,
},
});
};
private onEditClick = (ev: React.MouseEvent): void => {
dis.dispatch({
action: 'edit_event',
event: this.props.mxEvent,
@ -186,7 +216,7 @@ export default class MessageActionBar extends React.PureComponent {
* @param {Function} fn The execution function.
* @param {Function} checkFn The test function.
*/
runActionOnFailedEv(fn, checkFn) {
private runActionOnFailedEv(fn: (ev: MatrixEvent) => void, checkFn?: (ev: MatrixEvent) => boolean): void {
if (!checkFn) checkFn = () => true;
const mxEvent = this.props.mxEvent;
@ -201,18 +231,18 @@ export default class MessageActionBar extends React.PureComponent {
}
}
onResendClick = (ev) => {
private onResendClick = (ev: React.MouseEvent): void => {
this.runActionOnFailedEv((tarEv) => Resend.resend(tarEv));
};
onCancelClick = (ev) => {
private onCancelClick = (ev: React.MouseEvent): void => {
this.runActionOnFailedEv(
(tarEv) => Resend.removeFromQueue(tarEv),
(testEv) => canCancel(testEv.status),
);
};
render() {
public render(): JSX.Element {
const toolbarOpts = [];
if (canEditContent(this.props.mxEvent)) {
toolbarOpts.push(<RovingAccessibleTooltipButton
@ -235,7 +265,7 @@ export default class MessageActionBar extends React.PureComponent {
const editStatus = mxEvent.replacingEvent() && mxEvent.replacingEvent().status;
const redactStatus = mxEvent.localRedactionEvent() && mxEvent.localRedactionEvent().status;
const allowCancel = canCancel(mxEvent.status) || canCancel(editStatus) || canCancel(redactStatus);
const isFailed = [mxEvent.status, editStatus, redactStatus].includes("not_sent");
const isFailed = [mxEvent.status, editStatus, redactStatus].includes(EventStatus.NOT_SENT);
if (allowCancel && isFailed) {
// The resend button needs to appear ahead of the edit button, so insert to the
// start of the opts
@ -254,12 +284,22 @@ export default class MessageActionBar extends React.PureComponent {
// The only catch is we do the reply button first so that we can make sure the react
// button is the very first button without having to do length checks for `splice()`.
if (this.context.canReply) {
toolbarOpts.splice(0, 0, <RovingAccessibleTooltipButton
className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton"
title={_t("Reply")}
onClick={this.onReplyClick}
key="reply"
/>);
toolbarOpts.splice(0, 0, <>
<RovingAccessibleTooltipButton
className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton"
title={_t("Reply")}
onClick={this.onReplyClick}
key="reply"
/>
{ SettingsStore.getValue("feature_thread") && (
<RovingAccessibleTooltipButton
className="mx_MessageActionBar_maskButton mx_MessageActionBar_threadButton"
title={_t("Thread")}
onClick={this.onThreadClick}
key="thread"
/>
) }
</>);
}
if (this.context.canReact) {
toolbarOpts.splice(0, 0, <ReactButton

View file

@ -106,31 +106,20 @@ export default class ReactionsRowButton extends React.PureComponent<IProps, ISta
}
const room = this.context.getRoom(mxEvent.getRoomId());
let label;
let label: string;
if (room) {
const senders = [];
for (const reactionEvent of reactionEvents) {
const member = room.getMember(reactionEvent.getSender());
const name = member ? member.name : reactionEvent.getSender();
senders.push(name);
senders.push(member?.name || reactionEvent.getSender());
}
const reactors = formatCommaSeparatedList(senders, 6);
if (content) {
label = _t("%(reactors)s reacted with %(content)s", { reactors, content });
} else {
label = reactors;
}
label = _t(
"<reactors/><reactedWith> reacted with %(content)s</reactedWith>",
{
content,
},
{
reactors: () => {
return formatCommaSeparatedList(senders, 6);
},
reactedWith: (sub) => {
if (!content) {
return null;
}
return sub;
},
},
);
}
const isPeeking = room.getMyMembership() !== "join";
return <AccessibleButton