Apply prettier formatting

This commit is contained in:
Michael Weimann 2022-12-12 12:24:14 +01:00
parent 1cac306093
commit 526645c791
No known key found for this signature in database
GPG key ID: 53F535A266BB9584
1576 changed files with 65385 additions and 62478 deletions

View file

@ -50,56 +50,47 @@ interface ActiveCallEventProps {
}
const ActiveCallEvent = forwardRef<any, ActiveCallEventProps>(
(
{
mxEvent,
call,
participatingMembers,
buttonText,
buttonKind,
buttonDisabledTooltip,
onButtonClick,
},
ref,
) => {
({ mxEvent, call, participatingMembers, buttonText, buttonKind, buttonDisabledTooltip, onButtonClick }, ref) => {
const senderName = useMemo(() => mxEvent.sender?.name ?? mxEvent.getSender(), [mxEvent]);
const facePileMembers = useMemo(() => participatingMembers.slice(0, MAX_FACES), [participatingMembers]);
const facePileOverflow = participatingMembers.length > facePileMembers.length;
return <div className="mx_CallEvent_wrapper" ref={ref}>
<div className="mx_CallEvent mx_CallEvent_active">
<MemberAvatar
member={mxEvent.sender}
fallbackUserId={mxEvent.getSender()}
viewUserOnClick
width={24}
height={24}
/>
<div className="mx_CallEvent_infoRows">
<span className="mx_CallEvent_title">
{ _t("%(name)s started a video call", { name: senderName }) }
</span>
<LiveContentSummary
type={LiveContentType.Video}
text={_t("Video call")}
active={false}
participantCount={participatingMembers.length}
return (
<div className="mx_CallEvent_wrapper" ref={ref}>
<div className="mx_CallEvent mx_CallEvent_active">
<MemberAvatar
member={mxEvent.sender}
fallbackUserId={mxEvent.getSender()}
viewUserOnClick
width={24}
height={24}
/>
<FacePile members={facePileMembers} faceSize={24} overflow={facePileOverflow} />
<div className="mx_CallEvent_infoRows">
<span className="mx_CallEvent_title">
{_t("%(name)s started a video call", { name: senderName })}
</span>
<LiveContentSummary
type={LiveContentType.Video}
text={_t("Video call")}
active={false}
participantCount={participatingMembers.length}
/>
<FacePile members={facePileMembers} faceSize={24} overflow={facePileOverflow} />
</div>
{call && <GroupCallDuration groupCall={call.groupCall} />}
<AccessibleTooltipButton
className="mx_CallEvent_button"
kind={buttonKind}
disabled={onButtonClick === null || buttonDisabledTooltip !== undefined}
onClick={onButtonClick}
tooltip={buttonDisabledTooltip}
>
{buttonText}
</AccessibleTooltipButton>
</div>
{ call && <GroupCallDuration groupCall={call.groupCall} /> }
<AccessibleTooltipButton
className="mx_CallEvent_button"
kind={buttonKind}
disabled={onButtonClick === null || buttonDisabledTooltip !== undefined}
onClick={onButtonClick}
tooltip={buttonDisabledTooltip}
>
{ buttonText }
</AccessibleTooltipButton>
</div>
</div>;
);
},
);
@ -113,40 +104,52 @@ const ActiveLoadedCallEvent = forwardRef<any, ActiveLoadedCallEventProps>(({ mxE
const participatingMembers = useParticipatingMembers(call);
const joinCallButtonDisabledTooltip = useJoinCallButtonDisabledTooltip(call);
const connect = useCallback((ev: ButtonEvent) => {
ev.preventDefault();
defaultDispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: mxEvent.getRoomId()!,
view_call: true,
metricsTrigger: undefined,
});
}, [mxEvent]);
const connect = useCallback(
(ev: ButtonEvent) => {
ev.preventDefault();
defaultDispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: mxEvent.getRoomId()!,
view_call: true,
metricsTrigger: undefined,
});
},
[mxEvent],
);
const disconnect = useCallback((ev: ButtonEvent) => {
ev.preventDefault();
call.disconnect();
}, [call]);
const disconnect = useCallback(
(ev: ButtonEvent) => {
ev.preventDefault();
call.disconnect();
},
[call],
);
const [buttonText, buttonKind, onButtonClick] = useMemo(() => {
switch (connectionState) {
case ConnectionState.Disconnected: return [_t("Join"), "primary", connect];
case ConnectionState.Connecting: return [_t("Join"), "primary", null];
case ConnectionState.Connected: return [_t("Leave"), "danger", disconnect];
case ConnectionState.Disconnecting: return [_t("Leave"), "danger", null];
case ConnectionState.Disconnected:
return [_t("Join"), "primary", connect];
case ConnectionState.Connecting:
return [_t("Join"), "primary", null];
case ConnectionState.Connected:
return [_t("Leave"), "danger", disconnect];
case ConnectionState.Disconnecting:
return [_t("Leave"), "danger", null];
}
}, [connectionState, connect, disconnect]);
return <ActiveCallEvent
ref={ref}
mxEvent={mxEvent}
call={call}
participatingMembers={participatingMembers}
buttonText={buttonText}
buttonKind={buttonKind}
buttonDisabledTooltip={joinCallButtonDisabledTooltip ?? undefined}
onButtonClick={onButtonClick}
/>;
return (
<ActiveCallEvent
ref={ref}
mxEvent={mxEvent}
call={call}
participatingMembers={participatingMembers}
buttonText={buttonText}
buttonKind={buttonKind}
buttonDisabledTooltip={joinCallButtonDisabledTooltip ?? undefined}
onButtonClick={onButtonClick}
/>
);
});
interface CallEventProps {
@ -159,30 +162,35 @@ interface CallEventProps {
export const CallEvent = forwardRef<any, CallEventProps>(({ mxEvent }, ref) => {
const client = useContext(MatrixClientContext);
const call = useCall(mxEvent.getRoomId()!);
const latestEvent = client.getRoom(mxEvent.getRoomId())!.currentState
.getStateEvents(mxEvent.getType(), mxEvent.getStateKey()!);
const latestEvent = client
.getRoom(mxEvent.getRoomId())!
.currentState.getStateEvents(mxEvent.getType(), mxEvent.getStateKey()!);
if ("m.terminated" in latestEvent.getContent()) {
// The call is terminated
return <div className="mx_CallEvent_wrapper" ref={ref}>
<div className="mx_CallEvent mx_CallEvent_inactive">
<span className="mx_CallEvent_title">{ _t("Video call ended") }</span>
<CallDuration delta={latestEvent.getTs() - mxEvent.getTs()} />
return (
<div className="mx_CallEvent_wrapper" ref={ref}>
<div className="mx_CallEvent mx_CallEvent_inactive">
<span className="mx_CallEvent_title">{_t("Video call ended")}</span>
<CallDuration delta={latestEvent.getTs() - mxEvent.getTs()} />
</div>
</div>
</div>;
);
}
if (call === null) {
// There should be a call, but it hasn't loaded yet
return <ActiveCallEvent
ref={ref}
mxEvent={mxEvent}
call={null}
participatingMembers={[]}
buttonText={_t("Join")}
buttonKind="primary"
onButtonClick={null}
/>;
return (
<ActiveCallEvent
ref={ref}
mxEvent={mxEvent}
call={null}
participatingMembers={[]}
buttonText={_t("Join")}
buttonKind="primary"
onButtonClick={null}
/>
);
}
return <ActiveLoadedCallEvent mxEvent={mxEvent} call={call as ElementCall} ref={ref} />;

View file

@ -15,38 +15,30 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { Direction } from 'matrix-js-sdk/src/models/event-timeline';
import React from "react";
import { Direction } from "matrix-js-sdk/src/models/event-timeline";
import { logger } from "matrix-js-sdk/src/logger";
import { _t } from '../../../languageHandler';
import { formatFullDateNoTime } from '../../../DateUtils';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import dis from '../../../dispatcher/dispatcher';
import { Action } from '../../../dispatcher/actions';
import SettingsStore from '../../../settings/SettingsStore';
import { UIFeature } from '../../../settings/UIFeature';
import Modal from '../../../Modal';
import ErrorDialog from '../dialogs/ErrorDialog';
import { contextMenuBelow } from '../rooms/RoomTile';
import { _t } from "../../../languageHandler";
import { formatFullDateNoTime } from "../../../DateUtils";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import dis from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
import SettingsStore from "../../../settings/SettingsStore";
import { UIFeature } from "../../../settings/UIFeature";
import Modal from "../../../Modal";
import ErrorDialog from "../dialogs/ErrorDialog";
import { contextMenuBelow } from "../rooms/RoomTile";
import { ContextMenuTooltipButton } from "../../structures/ContextMenu";
import IconizedContextMenu, {
IconizedContextMenuOption,
IconizedContextMenuOptionList,
} from "../context_menus/IconizedContextMenu";
import JumpToDatePicker from './JumpToDatePicker';
import JumpToDatePicker from "./JumpToDatePicker";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
function getDaysArray(): string[] {
return [
_t('Sunday'),
_t('Monday'),
_t('Tuesday'),
_t('Wednesday'),
_t('Thursday'),
_t('Friday'),
_t('Saturday'),
];
return [_t("Sunday"), _t("Monday"), _t("Tuesday"), _t("Wednesday"), _t("Thursday"), _t("Friday"), _t("Saturday")];
}
interface IProps {
@ -114,9 +106,9 @@ export default class DateSeparator extends React.Component<IProps, IState> {
yesterday.setDate(today.getDate() - 1);
if (date.toDateString() === today.toDateString()) {
return _t('Today');
return _t("Today");
} else if (date.toDateString() === yesterday.toDateString()) {
return _t('Yesterday');
return _t("Yesterday");
} else if (today.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) {
return days[date.getDay()];
} else {
@ -137,7 +129,7 @@ export default class DateSeparator extends React.Component<IProps, IState> {
);
logger.log(
`/timestamp_to_event: ` +
`found ${eventId} (${originServerTs}) for timestamp=${unixTimestamp} (looking forward)`,
`found ${eventId} (${originServerTs}) for timestamp=${unixTimestamp} (looking forward)`,
);
dis.dispatch<ViewRoomPayload>({
@ -155,8 +147,8 @@ export default class DateSeparator extends React.Component<IProps, IState> {
if (typeof code !== "undefined") {
// display error message stating you couldn't delete this.
Modal.createDialog(ErrorDialog, {
title: _t('Error'),
description: _t('Unable to find event at that date. (%(code)s)', { code }),
title: _t("Error"),
description: _t("Unable to find event at that date. (%(code)s)", { code }),
});
}
}
@ -191,29 +183,25 @@ export default class DateSeparator extends React.Component<IProps, IState> {
private renderJumpToDateMenu(): React.ReactElement {
let contextMenu: JSX.Element;
if (this.state.contextMenuPosition) {
contextMenu = <IconizedContextMenu
{...contextMenuBelow(this.state.contextMenuPosition)}
onFinished={this.onContextMenuCloseClick}
>
<IconizedContextMenuOptionList first>
<IconizedContextMenuOption
label={_t("Last week")}
onClick={this.onLastWeekClicked}
/>
<IconizedContextMenuOption
label={_t("Last month")}
onClick={this.onLastMonthClicked}
/>
<IconizedContextMenuOption
label={_t("The beginning of the room")}
onClick={this.onTheBeginningClicked}
/>
</IconizedContextMenuOptionList>
contextMenu = (
<IconizedContextMenu
{...contextMenuBelow(this.state.contextMenuPosition)}
onFinished={this.onContextMenuCloseClick}
>
<IconizedContextMenuOptionList first>
<IconizedContextMenuOption label={_t("Last week")} onClick={this.onLastWeekClicked} />
<IconizedContextMenuOption label={_t("Last month")} onClick={this.onLastMonthClicked} />
<IconizedContextMenuOption
label={_t("The beginning of the room")}
onClick={this.onTheBeginningClicked}
/>
</IconizedContextMenuOptionList>
<IconizedContextMenuOptionList>
<JumpToDatePicker ts={this.props.ts} onDatePicked={this.onDatePicked} />
</IconizedContextMenuOptionList>
</IconizedContextMenu>;
<IconizedContextMenuOptionList>
<JumpToDatePicker ts={this.props.ts} onDatePicked={this.onDatePicked} />
</IconizedContextMenuOptionList>
</IconizedContextMenu>
);
}
return (
@ -223,9 +211,9 @@ export default class DateSeparator extends React.Component<IProps, IState> {
isExpanded={!!this.state.contextMenuPosition}
title={_t("Jump to date")}
>
<h2 aria-hidden="true">{ this.getLabel() }</h2>
<h2 aria-hidden="true">{this.getLabel()}</h2>
<div className="mx_DateSeparator_chevron" />
{ contextMenu }
{contextMenu}
</ContextMenuTooltipButton>
);
}
@ -237,15 +225,17 @@ export default class DateSeparator extends React.Component<IProps, IState> {
if (this.state.jumpToDateEnabled) {
dateHeaderContent = this.renderJumpToDateMenu();
} else {
dateHeaderContent = <h2 aria-hidden="true">{ label }</h2>;
dateHeaderContent = <h2 aria-hidden="true">{label}</h2>;
}
// ARIA treats <hr/>s as separators, here we abuse them slightly so manually treat this entire thing as one
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
return <div className="mx_DateSeparator" role="separator" tabIndex={-1} aria-label={label}>
<hr role="none" />
{ dateHeaderContent }
<hr role="none" />
</div>;
return (
<div className="mx_DateSeparator" role="separator" tabIndex={-1} aria-label={label}>
<hr role="none" />
{dateHeaderContent}
<hr role="none" />
</div>
);
}
}

View file

@ -15,11 +15,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
import classNames from 'classnames';
import React from "react";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import classNames from "classnames";
import { getUserNameColorClass } from '../../../utils/FormattingUtils';
import { getUserNameColorClass } from "../../../utils/FormattingUtils";
import UserIdentifier from "../../../customisations/UserIdentifier";
interface IProps {
@ -45,24 +45,22 @@ export default class DisambiguatedProfile extends React.Component<IProps> {
if (member?.disambiguate && mxid) {
mxidElement = (
<span className="mx_DisambiguatedProfile_mxid">
{ UserIdentifier.getDisplayUserIdentifier(
mxid, { withDisplayName: true, roomId: member.roomId },
) }
{UserIdentifier.getDisplayUserIdentifier(mxid, { withDisplayName: true, roomId: member.roomId })}
</span>
);
}
const displayNameClasses = classNames({
"mx_DisambiguatedProfile_displayName": emphasizeDisplayName,
mx_DisambiguatedProfile_displayName: emphasizeDisplayName,
[colorClass]: true,
});
return (
<div className="mx_DisambiguatedProfile" onClick={onClick}>
<span className={displayNameClasses} dir="auto">
{ rawDisplayName }
{rawDisplayName}
</span>
{ mxidElement }
{mxidElement}
</div>
);
}

View file

@ -86,19 +86,21 @@ export default class DownloadActionButton extends React.PureComponent<IProps, IS
}
const classes = classNames({
'mx_MessageActionBar_iconButton': true,
'mx_MessageActionBar_downloadButton': true,
'mx_MessageActionBar_downloadSpinnerButton': !!spinner,
mx_MessageActionBar_iconButton: true,
mx_MessageActionBar_downloadButton: true,
mx_MessageActionBar_downloadSpinnerButton: !!spinner,
});
return <RovingAccessibleTooltipButton
className={classes}
title={spinner ? _t(this.state.tooltip) : _t("Download")}
onClick={this.onDownloadClick}
disabled={!!spinner}
>
<DownloadIcon />
{ spinner }
</RovingAccessibleTooltipButton>;
return (
<RovingAccessibleTooltipButton
className={classes}
title={spinner ? _t(this.state.tooltip) : _t("Download")}
onClick={this.onDownloadClick}
disabled={!!spinner}
>
<DownloadIcon />
{spinner}
</RovingAccessibleTooltipButton>
);
}
}

View file

@ -14,18 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { createRef } from 'react';
import { EventStatus, MatrixEvent, MatrixEventEvent } from 'matrix-js-sdk/src/models/event';
import classNames from 'classnames';
import React, { createRef } from "react";
import { EventStatus, MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/event";
import classNames from "classnames";
import * as HtmlUtils from '../../../HtmlUtils';
import { editBodyDiffToHtml } from '../../../utils/MessageDiffUtils';
import { formatTime } from '../../../DateUtils';
import { pillifyLinks, unmountPills } from '../../../utils/pillify';
import { tooltipifyLinks, unmountTooltips } from '../../../utils/tooltipify';
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import Modal from '../../../Modal';
import * as HtmlUtils from "../../../HtmlUtils";
import { editBodyDiffToHtml } from "../../../utils/MessageDiffUtils";
import { formatTime } from "../../../DateUtils";
import { pillifyLinks, unmountPills } from "../../../utils/pillify";
import { tooltipifyLinks, unmountTooltips } from "../../../utils/tooltipify";
import { _t } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import Modal from "../../../Modal";
import RedactedBody from "./RedactedBody";
import AccessibleButton from "../elements/AccessibleButton";
import ConfirmAndWaitRedactDialog from "../dialogs/ConfirmAndWaitRedactDialog";
@ -77,15 +77,23 @@ export default class EditHistoryMessage extends React.PureComponent<IProps, ISta
const event = this.props.mxEvent;
const cli = MatrixClientPeg.get();
Modal.createDialog(ConfirmAndWaitRedactDialog, {
redact: () => cli.redactEvent(event.getRoomId(), event.getId()),
}, 'mx_Dialog_confirmredact');
Modal.createDialog(
ConfirmAndWaitRedactDialog,
{
redact: () => cli.redactEvent(event.getRoomId(), event.getId()),
},
"mx_Dialog_confirmredact",
);
};
private onViewSourceClick = (): void => {
Modal.createDialog(ViewSource, {
mxEvent: this.props.mxEvent,
}, 'mx_Dialog_viewsource');
Modal.createDialog(
ViewSource,
{
mxEvent: this.props.mxEvent,
},
"mx_Dialog_viewsource",
);
};
private pillifyLinks(): void {
@ -125,27 +133,21 @@ export default class EditHistoryMessage extends React.PureComponent<IProps, ISta
// hide the button when already redacted
let redactButton: JSX.Element;
if (!this.props.mxEvent.isRedacted() && !this.props.isBaseEvent && this.state.canRedact) {
redactButton = (
<AccessibleButton onClick={this.onRedactClick}>
{ _t("Remove") }
</AccessibleButton>
);
redactButton = <AccessibleButton onClick={this.onRedactClick}>{_t("Remove")}</AccessibleButton>;
}
let viewSourceButton: JSX.Element;
if (SettingsStore.getValue("developerMode")) {
viewSourceButton = (
<AccessibleButton onClick={this.onViewSourceClick}>
{ _t("View Source") }
</AccessibleButton>
<AccessibleButton onClick={this.onViewSourceClick}>{_t("View Source")}</AccessibleButton>
);
}
// disabled remove button when not allowed
return (
<div className="mx_MessageActionBar">
{ redactButton }
{ viewSourceButton }
{redactButton}
{viewSourceButton}
</div>
);
}
@ -161,39 +163,43 @@ export default class EditHistoryMessage extends React.PureComponent<IProps, ISta
if (this.props.previousEdit) {
contentElements = editBodyDiffToHtml(getReplacedContent(this.props.previousEdit), content);
} else {
contentElements = HtmlUtils.bodyToHtml(
content,
null,
{ stripReplyFallback: true, returnString: false },
);
contentElements = HtmlUtils.bodyToHtml(content, null, {
stripReplyFallback: true,
returnString: false,
});
}
if (mxEvent.getContent().msgtype === "m.emote") {
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
contentContainer = (
<div className="mx_EventTile_content" ref={this.content}>*&nbsp;
<span className="mx_MEmoteBody_sender">{ name }</span>
&nbsp;{ contentElements }
<div className="mx_EventTile_content" ref={this.content}>
*&nbsp;
<span className="mx_MEmoteBody_sender">{name}</span>
&nbsp;{contentElements}
</div>
);
} else {
contentContainer = <div className="mx_EventTile_content" ref={this.content}>{ contentElements }</div>;
contentContainer = (
<div className="mx_EventTile_content" ref={this.content}>
{contentElements}
</div>
);
}
}
const timestamp = formatTime(new Date(mxEvent.getTs()), this.props.isTwelveHour);
const isSending = (['sending', 'queued', 'encrypting'].indexOf(this.state.sendStatus) !== -1);
const isSending = ["sending", "queued", "encrypting"].indexOf(this.state.sendStatus) !== -1;
const classes = classNames({
"mx_EventTile": true,
mx_EventTile: true,
// Note: we keep the `sending` state class for tests, not for our styles
"mx_EventTile_sending": isSending,
mx_EventTile_sending: isSending,
});
return (
<li>
<div className={classes}>
<div className="mx_EventTile_line">
<span className="mx_MessageTimestamp">{ timestamp }</span>
{ contentContainer }
{ this.renderActionBar() }
<span className="mx_MessageTimestamp">{timestamp}</span>
{contentContainer}
{this.renderActionBar()}
</div>
</div>
</li>

View file

@ -14,17 +14,17 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { forwardRef, useContext } from 'react';
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 { _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";
import { isLocalRoom } from '../../../utils/localRoom/isLocalRoom';
import { isLocalRoom } from "../../../utils/localRoom/isLocalRoom";
interface IProps {
mxEvent: MatrixEvent;
@ -52,39 +52,50 @@ const EncryptionEvent = forwardRef<HTMLDivElement, IProps>(({ mxEvent, timestamp
subtitle = _t("Some encryption parameters have been changed.");
} else if (dmPartner) {
const displayName = room.getMember(dmPartner)?.rawDisplayName || dmPartner;
subtitle = _t("Messages here are end-to-end encrypted. " +
"Verify %(displayName)s in their profile - tap on their avatar.", { displayName });
subtitle = _t(
"Messages here are end-to-end encrypted. " +
"Verify %(displayName)s in their profile - tap on their avatar.",
{ displayName },
);
} else if (isLocalRoom(room)) {
subtitle = _t("Messages in this chat will be end-to-end encrypted.");
} else {
subtitle = _t("Messages in this room are end-to-end encrypted. " +
"When people join, you can verify them in their profile, just tap on their avatar.");
subtitle = _t(
"Messages in this room are end-to-end encrypted. " +
"When people join, you can verify them in their profile, just tap on their avatar.",
);
}
return <EventTileBubble
className="mx_cryptoEvent mx_cryptoEvent_icon"
title={_t("Encryption enabled")}
subtitle={subtitle}
timestamp={timestamp}
/>;
return (
<EventTileBubble
className="mx_cryptoEvent mx_cryptoEvent_icon"
title={_t("Encryption enabled")}
subtitle={subtitle}
timestamp={timestamp}
/>
);
}
if (isRoomEncrypted) {
return <EventTileBubble
className="mx_cryptoEvent mx_cryptoEvent_icon"
title={_t("Encryption enabled")}
subtitle={_t("Ignored attempt to disable encryption")}
timestamp={timestamp}
/>;
return (
<EventTileBubble
className="mx_cryptoEvent mx_cryptoEvent_icon"
title={_t("Encryption enabled")}
subtitle={_t("Ignored attempt to disable encryption")}
timestamp={timestamp}
/>
);
}
return <EventTileBubble
className="mx_cryptoEvent mx_cryptoEvent_icon mx_cryptoEvent_icon_warning"
title={_t("Encryption not enabled")}
subtitle={_t("The encryption used by this room isn't supported.")}
ref={ref}
timestamp={timestamp}
/>;
return (
<EventTileBubble
className="mx_cryptoEvent mx_cryptoEvent_icon mx_cryptoEvent_icon_warning"
title={_t("Encryption not enabled")}
subtitle={_t("The encryption used by this room isn't supported.")}
ref={ref}
timestamp={timestamp}
/>
);
});
export default EncryptionEvent;

View file

@ -25,19 +25,17 @@ interface IProps {
children?: ReactChildren;
}
const EventTileBubble = forwardRef<HTMLDivElement, IProps>(({
className,
title,
timestamp,
subtitle,
children,
}, ref) => {
return <div className={classNames("mx_EventTileBubble", className)} ref={ref}>
<div className="mx_EventTileBubble_title">{ title }</div>
{ subtitle && <div className="mx_EventTileBubble_subtitle">{ subtitle }</div> }
{ children }
{ timestamp }
</div>;
});
const EventTileBubble = forwardRef<HTMLDivElement, IProps>(
({ className, title, timestamp, subtitle, children }, ref) => {
return (
<div className={classNames("mx_EventTileBubble", className)} ref={ref}>
<div className="mx_EventTileBubble_title">{title}</div>
{subtitle && <div className="mx_EventTileBubble_subtitle">{subtitle}</div>}
{children}
{timestamp}
</div>
);
},
);
export default EventTileBubble;

View file

@ -47,7 +47,7 @@ const HiddenBody = React.forwardRef<any, IProps | IBodyProps>(({ mxEvent }, ref)
return (
<span className="mx_HiddenBody" ref={ref}>
{ text }
{text}
</span>
);
});

View file

@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useState, FormEvent } from 'react';
import React, { useState, FormEvent } from "react";
import { _t } from '../../../languageHandler';
import { _t } from "../../../languageHandler";
import Field from "../elements/Field";
import { RovingAccessibleButton, useRovingTabIndex } from "../../../accessibility/RovingTabIndex";
@ -42,11 +42,8 @@ const JumpToDatePicker: React.FC<IProps> = ({ ts, onDatePicked }: IProps) => {
};
return (
<form
className="mx_JumpToDatePicker_form"
onSubmit={onJumpToDateSubmit}
>
<span className="mx_JumpToDatePicker_label">{ _t("Jump to date") }</span>
<form className="mx_JumpToDatePicker_form" onSubmit={onJumpToDateSubmit}>
<span className="mx_JumpToDatePicker_label">{_t("Jump to date")}</span>
<Field
element="input"
type="date"
@ -65,7 +62,7 @@ const JumpToDatePicker: React.FC<IProps> = ({ ts, onDatePicked }: IProps) => {
className="mx_JumpToDatePicker_submitButton"
onClick={onJumpToDateSubmit}
>
{ _t("Go") }
{_t("Go")}
</RovingAccessibleButton>
</form>
);

View file

@ -14,24 +14,24 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { createRef } from 'react';
import React, { createRef } from "react";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { CallErrorCode, CallState } from 'matrix-js-sdk/src/webrtc/call';
import classNames from 'classnames';
import { CallErrorCode, CallState } from "matrix-js-sdk/src/webrtc/call";
import classNames from "classnames";
import { _t } from '../../../languageHandler';
import MemberAvatar from '../avatars/MemberAvatar';
import { _t } from "../../../languageHandler";
import MemberAvatar from "../avatars/MemberAvatar";
import LegacyCallEventGrouper, {
LegacyCallEventGrouperEvent,
CustomCallState,
} from '../../structures/LegacyCallEventGrouper';
import AccessibleButton from '../elements/AccessibleButton';
import InfoTooltip, { InfoTooltipKind } from '../elements/InfoTooltip';
import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
} from "../../structures/LegacyCallEventGrouper";
import AccessibleButton from "../elements/AccessibleButton";
import InfoTooltip, { InfoTooltipKind } from "../elements/InfoTooltip";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { formatPreciseDuration } from "../../../DateUtils";
import Clock from "../audio_messages/Clock";
const MAX_NON_NARROW_WIDTH = 450 / 70 * 100;
const MAX_NON_NARROW_WIDTH = (450 / 70) * 100;
interface IProps {
mxEvent: MatrixEvent;
@ -104,16 +104,16 @@ export default class LegacyCallEvent extends React.PureComponent<IProps, IState>
onClick={this.props.callEventGrouper.callBack}
kind="primary"
>
<span> { text } </span>
<span> {text} </span>
</AccessibleButton>
);
}
private renderSilenceIcon(): JSX.Element {
const silenceClass = classNames({
"mx_LegacyCallEvent_iconButton": true,
"mx_LegacyCallEvent_unSilence": this.state.silenced,
"mx_LegacyCallEvent_silence": !this.state.silenced,
mx_LegacyCallEvent_iconButton: true,
mx_LegacyCallEvent_unSilence: this.state.silenced,
mx_LegacyCallEvent_silence: !this.state.silenced,
});
return (
@ -134,22 +134,22 @@ export default class LegacyCallEvent extends React.PureComponent<IProps, IState>
return (
<div className="mx_LegacyCallEvent_content">
{ silenceIcon }
{silenceIcon}
<AccessibleButton
className="mx_LegacyCallEvent_content_button mx_LegacyCallEvent_content_button_reject"
onClick={this.props.callEventGrouper.rejectCall}
kind="danger"
>
<span> { _t("Decline") } </span>
<span> {_t("Decline")} </span>
</AccessibleButton>
<AccessibleButton
className="mx_LegacyCallEvent_content_button mx_LegacyCallEvent_content_button_answer"
onClick={this.props.callEventGrouper.answerCall}
kind="primary"
>
<span> { _t("Accept") } </span>
<span> {_t("Accept")} </span>
</AccessibleButton>
{ this.props.timestamp }
{this.props.timestamp}
</div>
);
}
@ -160,12 +160,12 @@ export default class LegacyCallEvent extends React.PureComponent<IProps, IState>
if (gotRejected) {
return (
<div className="mx_LegacyCallEvent_content">
{ _t("Call declined") }
{ this.renderCallBackButton(_t("Call back")) }
{ this.props.timestamp }
{_t("Call declined")}
{this.renderCallBackButton(_t("Call back"))}
{this.props.timestamp}
</div>
);
} else if (([CallErrorCode.UserHangup, "user hangup"].includes(hangupReason) || !hangupReason)) {
} else if ([CallErrorCode.UserHangup, "user hangup"].includes(hangupReason) || !hangupReason) {
// workaround for https://github.com/vector-im/element-web/issues/5178
// it seems Android randomly sets a reason of "user hangup" which is
// interpreted as an error code :(
@ -179,16 +179,16 @@ export default class LegacyCallEvent extends React.PureComponent<IProps, IState>
}
return (
<div className="mx_LegacyCallEvent_content">
{ text }
{ this.props.timestamp }
{text}
{this.props.timestamp}
</div>
);
} else if (hangupReason === CallErrorCode.InviteTimeout) {
return (
<div className="mx_LegacyCallEvent_content">
{ _t("No answer") }
{ this.renderCallBackButton(_t("Call back")) }
{ this.props.timestamp }
{_t("No answer")}
{this.renderCallBackButton(_t("Call back"))}
{this.props.timestamp}
</div>
);
}
@ -211,7 +211,7 @@ export default class LegacyCallEvent extends React.PureComponent<IProps, IState>
} else if (hangupReason === CallErrorCode.UserBusy) {
reason = _t("The user you called is busy.");
} else {
reason = _t('Unknown failure: %(reason)s', { reason: hangupReason });
reason = _t("Unknown failure: %(reason)s", { reason: hangupReason });
}
return (
@ -221,9 +221,9 @@ export default class LegacyCallEvent extends React.PureComponent<IProps, IState>
className="mx_LegacyCallEvent_content_tooltip"
kind={InfoTooltipKind.Warning}
/>
{ _t("Connection failed") }
{ this.renderCallBackButton(_t("Retry")) }
{ this.props.timestamp }
{_t("Connection failed")}
{this.renderCallBackButton(_t("Retry"))}
{this.props.timestamp}
</div>
);
}
@ -231,32 +231,32 @@ export default class LegacyCallEvent extends React.PureComponent<IProps, IState>
return (
<div className="mx_LegacyCallEvent_content">
<Clock seconds={this.state.length} aria-live="off" />
{ this.props.timestamp }
{this.props.timestamp}
</div>
);
}
if (state === CallState.Connecting) {
return (
<div className="mx_LegacyCallEvent_content">
{ _t("Connecting") }
{ this.props.timestamp }
{_t("Connecting")}
{this.props.timestamp}
</div>
);
}
if (state === CustomCallState.Missed) {
return (
<div className="mx_LegacyCallEvent_content">
{ _t("Missed call") }
{ this.renderCallBackButton(_t("Call back")) }
{ this.props.timestamp }
{_t("Missed call")}
{this.renderCallBackButton(_t("Call back"))}
{this.props.timestamp}
</div>
);
}
return (
<div className="mx_LegacyCallEvent_content">
{ _t("The call is in an unknown state!") }
{ this.props.timestamp }
{_t("The call is in an unknown state!")}
{this.props.timestamp}
</div>
);
}
@ -285,24 +285,18 @@ export default class LegacyCallEvent extends React.PureComponent<IProps, IState>
return (
<div className="mx_LegacyCallEvent_wrapper" ref={this.wrapperElement}>
<div className={className}>
{ silenceIcon }
{silenceIcon}
<div className="mx_LegacyCallEvent_info">
<MemberAvatar
member={event.sender}
width={32}
height={32}
/>
<MemberAvatar member={event.sender} width={32} height={32} />
<div className="mx_LegacyCallEvent_info_basic">
<div className="mx_LegacyCallEvent_sender">
{ sender }
</div>
<div className="mx_LegacyCallEvent_sender">{sender}</div>
<div className="mx_LegacyCallEvent_type">
<div className="mx_LegacyCallEvent_type_icon" />
{ callType }
{callType}
</div>
</div>
</div>
{ content }
{content}
</div>
</div>
);

View file

@ -18,7 +18,7 @@ import React from "react";
import { logger } from "matrix-js-sdk/src/logger";
import { Playback } from "../../../audio/Playback";
import InlineSpinner from '../elements/InlineSpinner';
import InlineSpinner from "../elements/InlineSpinner";
import { _t } from "../../../languageHandler";
import AudioPlayer from "../audio_messages/AudioPlayer";
import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent";
@ -67,7 +67,7 @@ export default class MAudioBody extends React.PureComponent<IBodyProps, IState>
// Note: we don't actually need a waveform to render an audio event, but voice messages do.
const content = this.props.mxEvent.getContent<IMediaEventContent>();
const waveform = content?.["org.matrix.msc1767.audio"]?.waveform?.map(p => p / 1024);
const waveform = content?.["org.matrix.msc1767.audio"]?.waveform?.map((p) => p / 1024);
// We should have a buffer to work with now: let's set it up
const playback = PlaybackManager.instance.createPlaybackInstance(buffer, waveform);
@ -86,16 +86,18 @@ export default class MAudioBody extends React.PureComponent<IBodyProps, IState>
}
protected get showFileBody(): boolean {
return this.context.timelineRenderingType !== TimelineRenderingType.Room &&
return (
this.context.timelineRenderingType !== TimelineRenderingType.Room &&
this.context.timelineRenderingType !== TimelineRenderingType.Pinned &&
this.context.timelineRenderingType !== TimelineRenderingType.Search;
this.context.timelineRenderingType !== TimelineRenderingType.Search
);
}
public render() {
if (this.state.error) {
return (
<MediaProcessingError className="mx_MAudioBody">
{ _t("Error processing audio message") }
{_t("Error processing audio message")}
</MediaProcessingError>
);
}
@ -123,7 +125,7 @@ export default class MAudioBody extends React.PureComponent<IBodyProps, IState>
return (
<span className="mx_MAudioBody">
<AudioPlayer playback={this.state.playback} mediaName={this.props.mxEvent.getContent().body} />
{ this.showFileBody && <MFileBody {...this.props} showGenericPlaceholder={false} /> }
{this.showFileBody && <MFileBody {...this.props} showGenericPlaceholder={false} />}
</span>
);
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useCallback, useContext, useEffect, useState } from 'react';
import React, { useCallback, useContext, useEffect, useState } from "react";
import {
Beacon,
BeaconEvent,
@ -22,30 +22,32 @@ import {
MatrixEventEvent,
MatrixClient,
RelationType,
} from 'matrix-js-sdk/src/matrix';
import { BeaconLocationState } from 'matrix-js-sdk/src/content-helpers';
import { randomString } from 'matrix-js-sdk/src/randomstring';
import { M_BEACON } from 'matrix-js-sdk/src/@types/beacon';
import classNames from 'classnames';
} from "matrix-js-sdk/src/matrix";
import { BeaconLocationState } from "matrix-js-sdk/src/content-helpers";
import { randomString } from "matrix-js-sdk/src/randomstring";
import { M_BEACON } from "matrix-js-sdk/src/@types/beacon";
import classNames from "classnames";
import MatrixClientContext from '../../../contexts/MatrixClientContext';
import { useEventEmitterState } from '../../../hooks/useEventEmitter';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
import { isBeaconWaitingToStart, useBeacon } from '../../../utils/beacon';
import { isSelfLocation, LocationShareError } from '../../../utils/location';
import { BeaconDisplayStatus, getBeaconDisplayStatus } from '../beacon/displayStatus';
import BeaconStatus from '../beacon/BeaconStatus';
import OwnBeaconStatus from '../beacon/OwnBeaconStatus';
import Map from '../location/Map';
import { MapError } from '../location/MapError';
import MapFallback from '../location/MapFallback';
import SmartMarker from '../location/SmartMarker';
import { GetRelationsForEvent } from '../rooms/EventTile';
import BeaconViewDialog from '../beacon/BeaconViewDialog';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
import { _t } from "../../../languageHandler";
import Modal from "../../../Modal";
import { isBeaconWaitingToStart, useBeacon } from "../../../utils/beacon";
import { isSelfLocation, LocationShareError } from "../../../utils/location";
import { BeaconDisplayStatus, getBeaconDisplayStatus } from "../beacon/displayStatus";
import BeaconStatus from "../beacon/BeaconStatus";
import OwnBeaconStatus from "../beacon/OwnBeaconStatus";
import Map from "../location/Map";
import { MapError } from "../location/MapError";
import MapFallback from "../location/MapFallback";
import SmartMarker from "../location/SmartMarker";
import { GetRelationsForEvent } from "../rooms/EventTile";
import BeaconViewDialog from "../beacon/BeaconViewDialog";
import { IBodyProps } from "./IBodyProps";
const useBeaconState = (beaconInfoEvent: MatrixEvent): {
const useBeaconState = (
beaconInfoEvent: MatrixEvent,
): {
beacon?: Beacon;
description?: string;
latestLocationState?: BeaconLocationState;
@ -54,15 +56,13 @@ const useBeaconState = (beaconInfoEvent: MatrixEvent): {
} => {
const beacon = useBeacon(beaconInfoEvent);
const isLive = useEventEmitterState(
beacon,
BeaconEvent.LivenessChange,
() => beacon?.isLive);
const isLive = useEventEmitterState(beacon, BeaconEvent.LivenessChange, () => beacon?.isLive);
const latestLocationState = useEventEmitterState(
beacon,
BeaconEvent.LocationUpdate,
() => beacon?.latestLocationState);
() => beacon?.latestLocationState,
);
if (!beacon) {
return {};
@ -104,20 +104,23 @@ const useHandleBeaconRedaction = (
matrixClient: MatrixClient,
getRelationsForEvent?: GetRelationsForEvent,
): void => {
const onBeforeBeaconInfoRedaction = useCallback((_event: MatrixEvent, redactionEvent: MatrixEvent) => {
const relations = getRelationsForEvent ?
getRelationsForEvent(event.getId(), RelationType.Reference, M_BEACON.name) :
undefined;
const onBeforeBeaconInfoRedaction = useCallback(
(_event: MatrixEvent, redactionEvent: MatrixEvent) => {
const relations = getRelationsForEvent
? getRelationsForEvent(event.getId(), RelationType.Reference, M_BEACON.name)
: undefined;
relations?.getRelations()?.forEach(locationEvent => {
matrixClient.redactEvent(
locationEvent.getRoomId(),
locationEvent.getId(),
undefined,
redactionEvent.getContent(),
);
});
}, [event, matrixClient, getRelationsForEvent]);
relations?.getRelations()?.forEach((locationEvent) => {
matrixClient.redactEvent(
locationEvent.getRoomId(),
locationEvent.getId(),
undefined,
redactionEvent.getContent(),
);
});
},
[event, matrixClient, getRelationsForEvent],
);
useEffect(() => {
event.addListener(MatrixEventEvent.BeforeRedaction, onBeforeBeaconInfoRedaction);
@ -128,17 +131,13 @@ const useHandleBeaconRedaction = (
};
const MBeaconBody: React.FC<IBodyProps> = React.forwardRef(({ mxEvent, getRelationsForEvent }, ref) => {
const {
beacon,
isLive,
latestLocationState,
waitingToStart,
} = useBeaconState(mxEvent);
const { beacon, isLive, latestLocationState, waitingToStart } = useBeaconState(mxEvent);
const mapId = useUniqueId(mxEvent.getId());
const matrixClient = useContext(MatrixClientContext);
const [error, setError] = useState<Error>();
const isMapDisplayError = error?.message === LocationShareError.MapStyleUrlNotConfigured ||
const isMapDisplayError =
error?.message === LocationShareError.MapStyleUrlNotConfigured ||
error?.message === LocationShareError.MapStyleUrlNotReachable;
const displayStatus = getBeaconDisplayStatus(
isLive,
@ -173,15 +172,15 @@ const MBeaconBody: React.FC<IBodyProps> = React.forwardRef(({ mxEvent, getRelati
let map: JSX.Element;
if (displayStatus === BeaconDisplayStatus.Active && !isMapDisplayError) {
map = <Map
id={mapId}
centerGeoUri={latestLocationState.uri}
onError={setError}
onClick={onClick}
className="mx_MBeaconBody_map"
>
{
({ map }) =>
map = (
<Map
id={mapId}
centerGeoUri={latestLocationState.uri}
onError={setError}
onClick={onClick}
className="mx_MBeaconBody_map"
>
{({ map }) => (
<SmartMarker
map={map}
id={`${mapId}-marker`}
@ -189,52 +188,52 @@ const MBeaconBody: React.FC<IBodyProps> = React.forwardRef(({ mxEvent, getRelati
roomMember={markerRoomMember}
useMemberColor
/>
}
</Map>;
)}
</Map>
);
} else if (isMapDisplayError) {
map = <MapError
error={error.message as LocationShareError}
onClick={onClick}
className={classNames(
'mx_MBeaconBody_mapError',
// set interactive class when maximised map can be opened
{ 'mx_MBeaconBody_mapErrorInteractive':
displayStatus === BeaconDisplayStatus.Active,
},
)}
isMinimised
/>;
map = (
<MapError
error={error.message as LocationShareError}
onClick={onClick}
className={classNames(
"mx_MBeaconBody_mapError",
// set interactive class when maximised map can be opened
{ mx_MBeaconBody_mapErrorInteractive: displayStatus === BeaconDisplayStatus.Active },
)}
isMinimised
/>
);
} else {
map = <MapFallback
isLoading={displayStatus === BeaconDisplayStatus.Loading}
className='mx_MBeaconBody_map mx_MBeaconBody_mapFallback'
/>;
map = (
<MapFallback
isLoading={displayStatus === BeaconDisplayStatus.Loading}
className="mx_MBeaconBody_map mx_MBeaconBody_mapFallback"
/>
);
}
return (
<div
className='mx_MBeaconBody'
ref={ref}
>
{ map }
{ isOwnBeacon ?
<div className="mx_MBeaconBody" ref={ref}>
{map}
{isOwnBeacon ? (
<OwnBeaconStatus
className='mx_MBeaconBody_chin'
className="mx_MBeaconBody_chin"
beacon={beacon}
displayStatus={displayStatus}
withIcon
/> :
<BeaconStatus
className='mx_MBeaconBody_chin'
beacon={beacon}
displayStatus={displayStatus}
label={_t('View live location')}
withIcon
/>
}
) : (
<BeaconStatus
className="mx_MBeaconBody_chin"
beacon={beacon}
displayStatus={displayStatus}
label={_t("View live location")}
withIcon
/>
)}
</div>
);
});
export default MBeaconBody;

View file

@ -14,12 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { createRef } from 'react';
import { filesize } from 'filesize';
import React, { createRef } from "react";
import { filesize } from "filesize";
import { logger } from "matrix-js-sdk/src/logger";
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
import { _t } from "../../../languageHandler";
import Modal from "../../../Modal";
import AccessibleButton from "../elements/AccessibleButton";
import { mediaFromContent } from "../../../customisations/Media";
import ErrorDialog from "../dialogs/ErrorDialog";
@ -35,7 +35,7 @@ export let DOWNLOAD_ICON_URL; // cached copy of the download.svg asset for the s
async function cacheDownloadIcon() {
if (DOWNLOAD_ICON_URL) return; // cached already
// eslint-disable-next-line @typescript-eslint/no-var-requires
const svg = await fetch(require('../../../../res/img/download.svg').default).then(r => r.text());
const svg = await fetch(require("../../../../res/img/download.svg").default).then((r) => r.text());
DOWNLOAD_ICON_URL = "data:image/svg+xml;base64," + window.btoa(svg);
}
@ -207,7 +207,7 @@ export default class MFileBody extends React.Component<IProps, IState> {
<span className="mx_MFileBody_info_icon" />
<TextWithTooltip tooltip={presentableTextForFile(this.content, _t("Attachment"), true)}>
<span className="mx_MFileBody_info_filename">
{ presentableTextForFile(this.content, _t("Attachment"), true, true) }
{presentableTextForFile(this.content, _t("Attachment"), true, true)}
</span>
</TextWithTooltip>
</AccessibleButton>
@ -217,18 +217,18 @@ export default class MFileBody extends React.Component<IProps, IState> {
if (this.props.forExport) {
const content = this.props.mxEvent.getContent();
// During export, the content url will point to the MSC, which will later point to a local url
return <span className="mx_MFileBody">
<a href={content.file?.url || content.url}>
{ placeholder }
</a>
</span>;
return (
<span className="mx_MFileBody">
<a href={content.file?.url || content.url}>{placeholder}</a>
</span>
);
}
let showDownloadLink = !this.props.showGenericPlaceholder || (
this.context.timelineRenderingType !== TimelineRenderingType.Room &&
this.context.timelineRenderingType !== TimelineRenderingType.Search &&
this.context.timelineRenderingType !== TimelineRenderingType.Pinned
);
let showDownloadLink =
!this.props.showGenericPlaceholder ||
(this.context.timelineRenderingType !== TimelineRenderingType.Room &&
this.context.timelineRenderingType !== TimelineRenderingType.Search &&
this.context.timelineRenderingType !== TimelineRenderingType.Pinned);
if (this.context.timelineRenderingType === TimelineRenderingType.Thread) {
showDownloadLink = false;
@ -244,12 +244,14 @@ export default class MFileBody extends React.Component<IProps, IState> {
// but it is not guaranteed between various browsers' settings.
return (
<span className="mx_MFileBody">
{ placeholder }
{ showDownloadLink && <div className="mx_MFileBody_download">
<AccessibleButton onClick={this.decryptFile}>
{ _t("Decrypt %(text)s", { text: this.linkText }) }
</AccessibleButton>
</div> }
{placeholder}
{showDownloadLink && (
<div className="mx_MFileBody_download">
<AccessibleButton onClick={this.decryptFile}>
{_t("Decrypt %(text)s", { text: this.linkText })}
</AccessibleButton>
</div>
)}
</span>
);
}
@ -259,34 +261,37 @@ export default class MFileBody extends React.Component<IProps, IState> {
// If the attachment is encrypted then put the link inside an iframe.
return (
<span className="mx_MFileBody">
{ placeholder }
{ showDownloadLink && <div className="mx_MFileBody_download">
<div aria-hidden style={{ display: "none" }}>
{ /*
* Add dummy copy of the "a" tag
* We'll use it to learn how the download link
* would have been styled if it was rendered inline.
*/ }
{ /* this violates multiple eslint rules
so ignore it completely */ }
{ /* eslint-disable-next-line */ }
<a ref={this.dummyLink} />
</div>
{ /*
{placeholder}
{showDownloadLink && (
<div className="mx_MFileBody_download">
<div aria-hidden style={{ display: "none" }}>
{/*
* Add dummy copy of the "a" tag
* We'll use it to learn how the download link
* would have been styled if it was rendered inline.
*/}
{/* this violates multiple eslint rules
so ignore it completely */}
{/* eslint-disable-next-line */}
<a ref={this.dummyLink} />
</div>
{/*
TODO: Move iframe (and dummy link) into FileDownloader.
We currently have it set up this way because of styles applied to the iframe
itself which cannot be easily handled/overridden by the FileDownloader. In
future, the download link may disappear entirely at which point it could also
be suitable to just remove this bit of code.
*/ }
<iframe
aria-hidden
title={presentableTextForFile(this.content, _t("Attachment"), true, true)}
src={url}
onLoad={() => this.downloadFile(this.fileName, this.linkText)}
ref={this.iframe}
sandbox="allow-scripts allow-downloads allow-downloads-without-user-activation" />
</div> }
*/}
<iframe
aria-hidden
title={presentableTextForFile(this.content, _t("Attachment"), true, true)}
src={url}
onLoad={() => this.downloadFile(this.fileName, this.linkText)}
ref={this.iframe}
sandbox="allow-scripts allow-downloads allow-downloads-without-user-activation"
/>
</div>
)}
</span>
);
} else if (contentUrl) {
@ -304,7 +309,7 @@ export default class MFileBody extends React.Component<IProps, IState> {
// we won't try and convert it. Likewise, if the file size is unknown then we'll assume
// it is too big. There is the risk of the reported file size and the actual file size
// being different, however the user shouldn't normally run into this problem.
const fileTooBig = typeof(fileSize) === 'number' ? fileSize > 524288000 : true;
const fileTooBig = typeof fileSize === "number" ? fileSize > 524288000 : true;
if (["application/pdf"].includes(fileType) && !fileTooBig) {
// We want to force a download on this type, so use an onClick handler.
@ -321,7 +326,7 @@ export default class MFileBody extends React.Component<IProps, IState> {
const blobUrl = URL.createObjectURL(blob);
// We have to create an anchor to download the file
const tempAnchor = document.createElement('a');
const tempAnchor = document.createElement("a");
tempAnchor.download = this.fileName;
tempAnchor.href = blobUrl;
document.body.appendChild(tempAnchor); // for firefox: https://stackoverflow.com/a/32226068
@ -336,26 +341,30 @@ export default class MFileBody extends React.Component<IProps, IState> {
return (
<span className="mx_MFileBody">
{ placeholder }
{ showDownloadLink && <div className="mx_MFileBody_download">
<a {...downloadProps}>
<span className="mx_MFileBody_download_icon" />
{ _t("Download %(text)s", { text: this.linkText }) }
</a>
{ this.context.timelineRenderingType === TimelineRenderingType.File && (
<div className="mx_MImageBody_size">
{ this.content.info?.size ? filesize(this.content.info.size) : "" }
</div>
) }
</div> }
{placeholder}
{showDownloadLink && (
<div className="mx_MFileBody_download">
<a {...downloadProps}>
<span className="mx_MFileBody_download_icon" />
{_t("Download %(text)s", { text: this.linkText })}
</a>
{this.context.timelineRenderingType === TimelineRenderingType.File && (
<div className="mx_MImageBody_size">
{this.content.info?.size ? filesize(this.content.info.size) : ""}
</div>
)}
</div>
)}
</span>
);
} else {
const extra = this.linkText ? (': ' + this.linkText) : '';
return <span className="mx_MFileBody">
{ placeholder }
{ _t("Invalid file%(extra)s", { extra: extra }) }
</span>;
const extra = this.linkText ? ": " + this.linkText : "";
return (
<span className="mx_MFileBody">
{placeholder}
{_t("Invalid file%(extra)s", { extra: extra })}
</span>
);
}
}
}

View file

@ -15,30 +15,30 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ComponentProps, createRef } from 'react';
import React, { ComponentProps, createRef } from "react";
import { Blurhash } from "react-blurhash";
import classNames from 'classnames';
import { CSSTransition, SwitchTransition } from 'react-transition-group';
import classNames from "classnames";
import { CSSTransition, SwitchTransition } from "react-transition-group";
import { logger } from "matrix-js-sdk/src/logger";
import { ClientEvent, ClientEventHandlerMap } from "matrix-js-sdk/src/client";
import MFileBody from './MFileBody';
import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
import MFileBody from "./MFileBody";
import Modal from "../../../Modal";
import { _t } from "../../../languageHandler";
import SettingsStore from "../../../settings/SettingsStore";
import Spinner from '../elements/Spinner';
import Spinner from "../elements/Spinner";
import { Media, mediaFromContent } from "../../../customisations/Media";
import { BLURHASH_FIELD, createThumbnail } from "../../../utils/image-media";
import { IMediaEventContent } from '../../../customisations/models/IMediaEventContent';
import ImageView from '../elements/ImageView';
import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent";
import ImageView from "../elements/ImageView";
import { IBodyProps } from "./IBodyProps";
import { ImageSize, suggestedSize as suggestedImageSize } from "../../../settings/enums/ImageSize";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
import { blobIsAnimated, mayBeAnimated } from '../../../utils/Image';
import { blobIsAnimated, mayBeAnimated } from "../../../utils/Image";
import { presentableTextForFile } from "../../../utils/FileUtils";
import { createReconnectedListener } from '../../../utils/connection';
import MediaProcessingError from './shared/MediaProcessingError';
import { createReconnectedListener } from "../../../utils/connection";
import MediaProcessingError from "./shared/MediaProcessingError";
import { DecryptError, DownloadError } from "../../../utils/DecryptFile";
enum Placeholder {
@ -104,7 +104,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
const httpUrl = this.state.contentUrl;
const params: Omit<ComponentProps<typeof ImageView>, "onFinished"> = {
src: httpUrl,
name: content.body?.length > 0 ? content.body : _t('Attachment'),
name: content.body?.length > 0 ? content.body : _t("Attachment"),
mxEvent: this.props.mxEvent,
permalinkCreator: this.props.permalinkCreator,
};
@ -202,7 +202,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
if (info?.mimetype === "image/svg+xml" && media.hasThumbnail) {
// Special-case to return clientside sender-generated thumbnails for SVGs, if any,
// given we deliberately don't thumbnail them serverside to prevent billion lol attacks and similar.
return media.getThumbnailHttp(thumbWidth, thumbHeight, 'scale');
return media.getThumbnailHttp(thumbWidth, thumbHeight, "scale");
}
// we try to download the correct resolution for hi-res images (like retina screenshots).
@ -214,11 +214,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
// thumbnailing to produce the static preview image)
// - On a low DPI device, always thumbnail to save bandwidth
// - If there's no sizing info in the event, default to thumbnail
if (
this.state.isAnimated ||
window.devicePixelRatio === 1.0 ||
(!info || !info.w || !info.h || !info.size)
) {
if (this.state.isAnimated || window.devicePixelRatio === 1.0 || !info || !info.w || !info.h || !info.size) {
return media.getThumbnailOfSourceHttp(thumbWidth, thumbHeight);
}
@ -229,10 +225,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
// As a compromise, let's switch to non-retina thumbnails only if the original image is both
// physically too large and going to be massive to load in the timeline (e.g. >1MB).
const isLargerThanThumbnail = (
info.w > thumbWidth ||
info.h > thumbHeight
);
const isLargerThanThumbnail = info.w > thumbWidth || info.h > thumbHeight;
const isLargeFileSize = info.size > 1 * 1024 * 1024; // 1mb
if (isLargeFileSize && isLargerThanThumbnail) {
@ -253,10 +246,10 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
let contentUrl: string;
if (this.props.mediaEventHelper.media.isEncrypted) {
try {
([contentUrl, thumbUrl] = await Promise.all([
[contentUrl, thumbUrl] = await Promise.all([
this.props.mediaEventHelper.sourceUrl.value,
this.props.mediaEventHelper.thumbnailUrl.value,
]));
]);
} catch (error) {
if (this.unmounted) return;
@ -302,7 +295,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
try {
const blob = await this.props.mediaEventHelper.sourceBlob.value;
if (!await blobIsAnimated(content.info?.mimetype, blob)) {
if (!(await blobIsAnimated(content.info?.mimetype, blob))) {
isAnimated = false;
}
@ -335,8 +328,8 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
componentDidMount() {
this.unmounted = false;
const showImage = this.state.showImage ||
localStorage.getItem("mx_ShowImage_" + this.props.mxEvent.getId()) === "true";
const showImage =
this.state.showImage || localStorage.getItem("mx_ShowImage_" + this.props.mxEvent.getId()) === "true";
if (showImage) {
// noinspection JSIgnoredPromiseFromCall
@ -373,18 +366,13 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
protected getBanner(content: IMediaEventContent): JSX.Element {
// Hide it for the threads list & the file panel where we show it as text anyway.
if ([
TimelineRenderingType.ThreadsList,
TimelineRenderingType.File,
].includes(this.context.timelineRenderingType)) {
if (
[TimelineRenderingType.ThreadsList, TimelineRenderingType.File].includes(this.context.timelineRenderingType)
) {
return null;
}
return (
<span className="mx_MImageBody_banner">
{ presentableTextForFile(content, _t("Image"), true, true) }
</span>
);
return <span className="mx_MImageBody_banner">{presentableTextForFile(content, _t("Image"), true, true)}</span>;
}
protected messageContent(
@ -418,7 +406,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
} else {
imageElement = (
<img
style={{ display: 'none' }}
style={{ display: "none" }}
src={thumbUrl}
ref={this.image}
alt={content.body}
@ -446,13 +434,11 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
let gifLabel: JSX.Element;
if (!this.props.forExport && !this.state.imgLoaded) {
const classes = classNames('mx_MImageBody_placeholder', {
'mx_MImageBody_placeholder--blurhash': this.props.mxEvent.getContent().info?.[BLURHASH_FIELD],
const classes = classNames("mx_MImageBody_placeholder", {
"mx_MImageBody_placeholder--blurhash": this.props.mxEvent.getContent().info?.[BLURHASH_FIELD],
});
placeholder = <div className={classes}>
{ this.getPlaceholder(maxWidth, maxHeight) }
</div>;
placeholder = <div className={classes}>{this.getPlaceholder(maxWidth, maxHeight)}</div>;
}
let showPlaceholder = Boolean(placeholder);
@ -496,33 +482,34 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
const sizing = infoSvg ? { maxHeight, maxWidth, width: maxWidth } : { maxHeight, maxWidth };
if (!this.props.forExport) {
placeholder = <SwitchTransition mode="out-in">
<CSSTransition
classNames="mx_rtg--fade"
key={`img-${showPlaceholder}`}
timeout={300}
>
{ showPlaceholder ? placeholder : <></> /* Transition always expects a child */ }
</CSSTransition>
</SwitchTransition>;
placeholder = (
<SwitchTransition mode="out-in">
<CSSTransition classNames="mx_rtg--fade" key={`img-${showPlaceholder}`} timeout={300}>
{showPlaceholder ? placeholder : <></> /* Transition always expects a child */}
</CSSTransition>
</SwitchTransition>
);
}
const thumbnail = (
<div className="mx_MImageBody_thumbnail_container" style={{ maxHeight, maxWidth, aspectRatio: `${infoWidth}/${infoHeight}` }}>
{ placeholder }
<div
className="mx_MImageBody_thumbnail_container"
style={{ maxHeight, maxWidth, aspectRatio: `${infoWidth}/${infoHeight}` }}
>
{placeholder}
<div style={sizing}>
{ img }
{ gifLabel }
{ banner }
{img}
{gifLabel}
{banner}
</div>
{ /* HACK: This div fills out space while the image loads, to prevent scroll jumps */ }
{ !this.props.forExport && !this.state.imgLoaded && (
{/* HACK: This div fills out space while the image loads, to prevent scroll jumps */}
{!this.props.forExport && !this.state.imgLoaded && (
<div style={{ height: maxHeight, width: maxWidth }} />
) }
)}
{ this.state.hover && this.getTooltip() }
{this.state.hover && this.getTooltip()}
</div>
);
@ -531,9 +518,11 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
// Overridden by MStickerBody
protected wrapImage(contentUrl: string, children: JSX.Element): JSX.Element {
return <a href={contentUrl} target={this.props.forExport ? "_blank" : undefined} onClick={this.onClick}>
{ children }
</a>;
return (
<a href={contentUrl} target={this.props.forExport ? "_blank" : undefined} onClick={this.onClick}>
{children}
</a>
);
}
// Overridden by MStickerBody
@ -562,13 +551,12 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
* In the room timeline or the thread context we don't need the download
* link as the message action bar will fulfill that
*/
const hasMessageActionBar = (
const hasMessageActionBar =
this.context.timelineRenderingType === TimelineRenderingType.Room ||
this.context.timelineRenderingType === TimelineRenderingType.Pinned ||
this.context.timelineRenderingType === TimelineRenderingType.Search ||
this.context.timelineRenderingType === TimelineRenderingType.Thread ||
this.context.timelineRenderingType === TimelineRenderingType.ThreadsList
);
this.context.timelineRenderingType === TimelineRenderingType.ThreadsList;
if (!hasMessageActionBar) {
return <MFileBody {...this.props} showGenericPlaceholder={false} />;
}
@ -585,11 +573,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
errorText = _t("Error downloading image");
}
return (
<MediaProcessingError className="mx_MImageBody">
{ errorText }
</MediaProcessingError>
);
return <MediaProcessingError className="mx_MImageBody">{errorText}</MediaProcessingError>;
}
let contentUrl = this.state.contentUrl;
@ -608,8 +592,8 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
return (
<div className="mx_MImageBody">
{ thumbnail }
{ fileBody }
{thumbnail}
{fileBody}
</div>
);
}
@ -623,13 +607,13 @@ interface PlaceholderIProps {
export class HiddenImagePlaceholder extends React.PureComponent<PlaceholderIProps> {
render() {
const maxWidth = this.props.maxWidth ? this.props.maxWidth + "px" : null;
let className = 'mx_HiddenImagePlaceholder';
if (this.props.hover) className += ' mx_HiddenImagePlaceholder_hover';
let className = "mx_HiddenImagePlaceholder";
if (this.props.hover) className += " mx_HiddenImagePlaceholder_hover";
return (
<div className={className} style={{ maxWidth: `min(100%, ${maxWidth}px)` }}>
<div className='mx_HiddenImagePlaceholder_button'>
<span className='mx_HiddenImagePlaceholder_eye' />
<span>{ _t("Show image") }</span>
<div className="mx_HiddenImagePlaceholder_button">
<span className="mx_HiddenImagePlaceholder_eye" />
<span>{_t("Show image")}</span>
</div>
</div>
);

View file

@ -38,8 +38,6 @@ export default class MImageReplyBody extends MImageBody {
const content = this.props.mxEvent.getContent<IMediaEventContent>();
const thumbnail = this.messageContent(this.state.contentUrl, this.state.thumbUrl, content, FORCED_IMAGE_HEIGHT);
return <div className="mx_MImageReplyBody">
{ thumbnail }
</div>;
return <div className="mx_MImageReplyBody">{thumbnail}</div>;
}
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { _t } from "../../../languageHandler";
@ -34,43 +34,49 @@ export default class MJitsiWidgetEvent extends React.PureComponent<IProps> {
}
render() {
const url = this.props.mxEvent.getContent()['url'];
const prevUrl = this.props.mxEvent.getPrevContent()['url'];
const url = this.props.mxEvent.getContent()["url"];
const prevUrl = this.props.mxEvent.getPrevContent()["url"];
const senderName = this.props.mxEvent.sender?.name || this.props.mxEvent.getSender();
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
const widgetId = this.props.mxEvent.getStateKey();
const widget = WidgetStore.instance.getRoom(room.roomId, true).widgets.find(w => w.id === widgetId);
const widget = WidgetStore.instance.getRoom(room.roomId, true).widgets.find((w) => w.id === widgetId);
let joinCopy = _t('Join the conference at the top of this room');
let joinCopy = _t("Join the conference at the top of this room");
if (widget && WidgetLayoutStore.instance.isInContainer(room, widget, Container.Right)) {
joinCopy = _t('Join the conference from the room information card on the right');
joinCopy = _t("Join the conference from the room information card on the right");
} else if (!widget) {
joinCopy = null;
}
if (!url) {
// removed
return <EventTileBubble
className="mx_MJitsiWidgetEvent"
title={_t('Video conference ended by %(senderName)s', { senderName })}
timestamp={this.props.timestamp}
/>;
return (
<EventTileBubble
className="mx_MJitsiWidgetEvent"
title={_t("Video conference ended by %(senderName)s", { senderName })}
timestamp={this.props.timestamp}
/>
);
} else if (prevUrl) {
// modified
return <EventTileBubble
className="mx_MJitsiWidgetEvent"
title={_t('Video conference updated by %(senderName)s', { senderName })}
subtitle={joinCopy}
timestamp={this.props.timestamp}
/>;
return (
<EventTileBubble
className="mx_MJitsiWidgetEvent"
title={_t("Video conference updated by %(senderName)s", { senderName })}
subtitle={joinCopy}
timestamp={this.props.timestamp}
/>
);
} else {
// assume added
return <EventTileBubble
className="mx_MJitsiWidgetEvent"
title={_t("Video conference started by %(senderName)s", { senderName })}
subtitle={joinCopy}
timestamp={this.props.timestamp}
/>;
return (
<EventTileBubble
className="mx_MJitsiWidgetEvent"
title={_t("Video conference started by %(senderName)s", { senderName })}
subtitle={joinCopy}
timestamp={this.props.timestamp}
/>
);
}
}
}

View file

@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import classNames from 'classnames';
import React from "react";
import classNames from "classnames";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import {
VerificationRequest,
@ -24,9 +24,9 @@ import {
import { EventType } from "matrix-js-sdk/src/@types/event";
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
import { getNameForEventRoom, userLabelForEventRoom } from '../../../utils/KeyVerificationStateObserver';
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { _t } from "../../../languageHandler";
import { getNameForEventRoom, userLabelForEventRoom } from "../../../utils/KeyVerificationStateObserver";
import EventTileBubble from "./EventTileBubble";
interface IProps {
@ -113,18 +113,17 @@ export default class MKeyVerificationConclusion extends React.Component<IProps>
let title;
if (request.done) {
title = _t(
"You verified %(name)s",
{ name: getNameForEventRoom(request.otherUserId, mxEvent.getRoomId()) },
);
title = _t("You verified %(name)s", {
name: getNameForEventRoom(request.otherUserId, mxEvent.getRoomId()),
});
} else if (request.cancelled) {
const userId = request.cancellingUserId;
if (userId === myUserId) {
title = _t("You cancelled verifying %(name)s",
{ name: getNameForEventRoom(request.otherUserId, mxEvent.getRoomId()) });
title = _t("You cancelled verifying %(name)s", {
name: getNameForEventRoom(request.otherUserId, mxEvent.getRoomId()),
});
} else {
title = _t("%(name)s cancelled verifying",
{ name: getNameForEventRoom(userId, mxEvent.getRoomId()) });
title = _t("%(name)s cancelled verifying", { name: getNameForEventRoom(userId, mxEvent.getRoomId()) });
}
}
@ -132,12 +131,14 @@ export default class MKeyVerificationConclusion extends React.Component<IProps>
const classes = classNames("mx_cryptoEvent mx_cryptoEvent_icon", {
mx_cryptoEvent_icon_verified: request.done,
});
return <EventTileBubble
className={classes}
title={title}
subtitle={userLabelForEventRoom(request.otherUserId, mxEvent.getRoomId())}
timestamp={this.props.timestamp}
/>;
return (
<EventTileBubble
className={classes}
title={title}
subtitle={userLabelForEventRoom(request.otherUserId, mxEvent.getRoomId())}
timestamp={this.props.timestamp}
/>
);
}
return null;

View file

@ -14,18 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { MatrixEvent } from 'matrix-js-sdk/src/matrix';
import React from "react";
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import { VerificationRequestEvent } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
import { getNameForEventRoom, userLabelForEventRoom } from '../../../utils/KeyVerificationStateObserver';
import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases';
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { _t } from "../../../languageHandler";
import { getNameForEventRoom, userLabelForEventRoom } from "../../../utils/KeyVerificationStateObserver";
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
import EventTileBubble from "./EventTileBubble";
import AccessibleButton from '../elements/AccessibleButton';
import RightPanelStore from '../../../stores/right-panel/RightPanelStore';
import AccessibleButton from "../elements/AccessibleButton";
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
interface IProps {
mxEvent: MatrixEvent;
@ -130,9 +130,11 @@ export default class MKeyVerificationRequest extends React.Component<IProps> {
let stateLabel;
const accepted = request.ready || request.started || request.done;
if (accepted) {
stateLabel = (<AccessibleButton onClick={this.openRequest}>
{ this.acceptedLabel(request.receivingUserId) }
</AccessibleButton>);
stateLabel = (
<AccessibleButton onClick={this.openRequest}>
{this.acceptedLabel(request.receivingUserId)}
</AccessibleButton>
);
} else if (request.cancelled) {
stateLabel = this.cancelledLabel(request.cancellingUserId);
} else if (request.accepting) {
@ -140,7 +142,7 @@ export default class MKeyVerificationRequest extends React.Component<IProps> {
} else if (request.declining) {
stateLabel = _t("Declining …");
}
stateNode = (<div className="mx_cryptoEvent_state">{ stateLabel }</div>);
stateNode = <div className="mx_cryptoEvent_state">{stateLabel}</div>;
}
if (!request.initiatedByMe) {
@ -148,29 +150,34 @@ export default class MKeyVerificationRequest extends React.Component<IProps> {
title = _t("%(name)s wants to verify", { name });
subtitle = userLabelForEventRoom(request.requestingUserId, mxEvent.getRoomId());
if (request.canAccept) {
stateNode = (<div className="mx_cryptoEvent_buttons">
<AccessibleButton kind="danger" onClick={this.onRejectClicked}>
{ _t("Decline") }
</AccessibleButton>
<AccessibleButton kind="primary" onClick={this.onAcceptClicked}>
{ _t("Accept") }
</AccessibleButton>
</div>);
stateNode = (
<div className="mx_cryptoEvent_buttons">
<AccessibleButton kind="danger" onClick={this.onRejectClicked}>
{_t("Decline")}
</AccessibleButton>
<AccessibleButton kind="primary" onClick={this.onAcceptClicked}>
{_t("Accept")}
</AccessibleButton>
</div>
);
}
} else { // request sent by us
} else {
// request sent by us
title = _t("You sent a verification request");
subtitle = userLabelForEventRoom(request.receivingUserId, mxEvent.getRoomId());
}
if (title) {
return <EventTileBubble
className="mx_cryptoEvent mx_cryptoEvent_icon"
title={title}
subtitle={subtitle}
timestamp={this.props.timestamp}
>
{ stateNode }
</EventTileBubble>;
return (
<EventTileBubble
className="mx_cryptoEvent mx_cryptoEvent_icon"
title={title}
subtitle={subtitle}
timestamp={this.props.timestamp}
>
{stateNode}
</EventTileBubble>
);
}
return null;
}

View file

@ -14,27 +14,27 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { randomString } from 'matrix-js-sdk/src/randomstring';
import { ClientEvent, ClientEventHandlerMap } from 'matrix-js-sdk/src/matrix';
import React from "react";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { randomString } from "matrix-js-sdk/src/randomstring";
import { ClientEvent, ClientEventHandlerMap } from "matrix-js-sdk/src/matrix";
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
import { _t } from "../../../languageHandler";
import Modal from "../../../Modal";
import {
locationEventGeoUri,
getLocationShareErrorMessage,
LocationShareError,
isSelfLocation,
} from '../../../utils/location';
import MatrixClientContext from '../../../contexts/MatrixClientContext';
import TooltipTarget from '../elements/TooltipTarget';
import { Alignment } from '../elements/Tooltip';
import LocationViewDialog from '../location/LocationViewDialog';
import Map from '../location/Map';
import SmartMarker from '../location/SmartMarker';
} from "../../../utils/location";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import TooltipTarget from "../elements/TooltipTarget";
import { Alignment } from "../elements/Tooltip";
import LocationViewDialog from "../location/LocationViewDialog";
import Map from "../location/Map";
import SmartMarker from "../location/SmartMarker";
import { IBodyProps } from "./IBodyProps";
import { createReconnectedListener } from '../../../utils/connection';
import { createReconnectedListener } from "../../../utils/connection";
interface IState {
error: Error;
@ -89,33 +89,37 @@ export default class MLocationBody extends React.Component<IBodyProps, IState> {
}
render(): React.ReactElement<HTMLDivElement> {
return this.state.error ?
<LocationBodyFallbackContent error={this.state.error} event={this.props.mxEvent} /> :
return this.state.error ? (
<LocationBodyFallbackContent error={this.state.error} event={this.props.mxEvent} />
) : (
<LocationBodyContent
mxEvent={this.props.mxEvent}
mapId={this.mapId}
onError={this.onError}
tooltip={_t("Expand map")}
onClick={this.onClick}
/>;
/>
);
}
}
export const LocationBodyFallbackContent: React.FC<{ event: MatrixEvent, error: Error }> = ({ error, event }) => {
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)}`;
const message = `${_t("Unable to load map")}: ${getLocationShareErrorMessage(errorType)}`;
const locationFallback = isSelfLocation(event.getContent()) ?
(_t('Shared their location: ') + event.getContent()?.body) :
(_t('Shared a location: ') + event.getContent()?.body);
const locationFallback = isSelfLocation(event.getContent())
? _t("Shared their location: ") + event.getContent()?.body
: _t("Shared a location: ") + event.getContent()?.body;
return <div className="mx_EventTile_body mx_MLocationBody">
<span className={errorType !== LocationShareError.MapStyleUrlNotConfigured ? "mx_EventTile_tileError" : ''}>
{ message }
</span>
<br />
{ locationFallback }
</div>;
return (
<div className="mx_EventTile_body mx_MLocationBody">
<span className={errorType !== LocationShareError.MapStyleUrlNotConfigured ? "mx_EventTile_tileError" : ""}>
{message}
</span>
<br />
{locationFallback}
</div>
);
};
interface LocationBodyContentProps {
@ -136,36 +140,23 @@ export const LocationBodyContent: React.FC<LocationBodyContentProps> = ({
const markerRoomMember = isSelfLocation(mxEvent.getContent()) ? mxEvent.sender : undefined;
const geoUri = locationEventGeoUri(mxEvent);
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>);
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">
{
tooltip
? <TooltipTarget
label={tooltip}
alignment={Alignment.InnerBottom}
maxParentWidth={450}
>
{ mapElement }
return (
<div className="mx_MLocationBody">
{tooltip ? (
<TooltipTarget label={tooltip} alignment={Alignment.InnerBottom} maxParentWidth={450}>
{mapElement}
</TooltipTarget>
: mapElement
}
</div>;
) : (
mapElement
)}
</div>
);
};

View file

@ -14,12 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import classNames from 'classnames';
import React from "react";
import classNames from "classnames";
import { logger } from "matrix-js-sdk/src/logger";
import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/event";
import { Relations, RelationsEvent } from 'matrix-js-sdk/src/models/relations';
import { MatrixClient } from 'matrix-js-sdk/src/matrix';
import { Relations, RelationsEvent } from "matrix-js-sdk/src/models/relations";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import {
M_POLL_END,
M_POLL_KIND_DISCLOSED,
@ -32,13 +32,13 @@ import {
} from "matrix-events-sdk";
import { RelatedRelations } from "matrix-js-sdk/src/models/related-relations";
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
import { _t } from "../../../languageHandler";
import Modal from "../../../Modal";
import { IBodyProps } from "./IBodyProps";
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
import StyledRadioButton from '../elements/StyledRadioButton';
import { formatCommaSeparatedList } from "../../../utils/FormattingUtils";
import StyledRadioButton from "../elements/StyledRadioButton";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import ErrorDialog from '../dialogs/ErrorDialog';
import ErrorDialog from "../dialogs/ErrorDialog";
import { GetRelationsForEvent } from "../rooms/EventTile";
import PollCreateDialog from "../elements/PollCreateDialog";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
@ -49,26 +49,15 @@ interface IState {
endRelations: RelatedRelations; // Poll end events
}
export function createVoteRelations(
getRelationsForEvent: GetRelationsForEvent,
eventId: string,
) {
export function createVoteRelations(getRelationsForEvent: GetRelationsForEvent, eventId: string) {
const relationsList: Relations[] = [];
const pollResponseRelations = getRelationsForEvent(
eventId,
"m.reference",
M_POLL_RESPONSE.name,
);
const pollResponseRelations = getRelationsForEvent(eventId, "m.reference", M_POLL_RESPONSE.name);
if (pollResponseRelations) {
relationsList.push(pollResponseRelations);
}
const pollResposnseAltRelations = getRelationsForEvent(
eventId,
"m.reference",
M_POLL_RESPONSE.altName,
);
const pollResposnseAltRelations = getRelationsForEvent(eventId, "m.reference", M_POLL_RESPONSE.altName);
if (pollResposnseAltRelations) {
relationsList.push(pollResposnseAltRelations);
}
@ -89,7 +78,7 @@ export function findTopAnswer(
if (!pollEventId) {
logger.warn(
"findTopAnswer: Poll event needs an event ID to fetch relations in order to determine " +
"the top answer - assuming no best answer",
"the top answer - assuming no best answer",
);
return "";
}
@ -101,27 +90,19 @@ export function findTopAnswer(
}
const findAnswerText = (answerId: string) => {
return poll.answers.find(a => a.id === answerId)?.text ?? "";
return poll.answers.find((a) => a.id === answerId)?.text ?? "";
};
const voteRelations = createVoteRelations(getRelationsForEvent, pollEventId);
const relationsList: Relations[] = [];
const pollEndRelations = getRelationsForEvent(
pollEventId,
"m.reference",
M_POLL_END.name,
);
const pollEndRelations = getRelationsForEvent(pollEventId, "m.reference", M_POLL_END.name);
if (pollEndRelations) {
relationsList.push(pollEndRelations);
}
const pollEndAltRelations = getRelationsForEvent(
pollEventId,
"m.reference",
M_POLL_END.altName,
);
const pollEndAltRelations = getRelationsForEvent(pollEventId, "m.reference", M_POLL_END.altName);
if (pollEndAltRelations) {
relationsList.push(pollEndAltRelations);
}
@ -160,7 +141,7 @@ export function isPollEnded(
if (!pollEventId) {
logger.warn(
"isPollEnded: Poll event must have event ID in order to determine whether it has ended " +
"- assuming poll has not ended",
"- assuming poll has not ended",
);
return false;
}
@ -169,7 +150,7 @@ export function isPollEnded(
if (!roomId) {
logger.warn(
"isPollEnded: Poll event must have room ID in order to determine whether it has ended " +
"- assuming poll has not ended",
"- assuming poll has not ended",
);
return false;
}
@ -177,28 +158,19 @@ export function isPollEnded(
const roomCurrentState = matrixClient.getRoom(roomId)?.currentState;
function userCanRedact(endEvent: MatrixEvent) {
const endEventSender = endEvent.getSender();
return endEventSender && roomCurrentState && roomCurrentState.maySendRedactionForEvent(
pollEvent,
endEventSender,
return (
endEventSender && roomCurrentState && roomCurrentState.maySendRedactionForEvent(pollEvent, endEventSender)
);
}
const relationsList: Relations[] = [];
const pollEndRelations = getRelationsForEvent(
pollEventId,
"m.reference",
M_POLL_END.name,
);
const pollEndRelations = getRelationsForEvent(pollEventId, "m.reference", M_POLL_END.name);
if (pollEndRelations) {
relationsList.push(pollEndRelations);
}
const pollEndAltRelations = getRelationsForEvent(
pollEventId,
"m.reference",
M_POLL_END.altName,
);
const pollEndAltRelations = getRelationsForEvent(pollEventId, "m.reference", M_POLL_END.altName);
if (pollEndAltRelations) {
relationsList.push(pollEndAltRelations);
}
@ -226,15 +198,10 @@ export function pollAlreadyHasVotes(mxEvent: MatrixEvent, getRelationsForEvent?:
export function launchPollEditor(mxEvent: MatrixEvent, getRelationsForEvent?: GetRelationsForEvent): void {
if (pollAlreadyHasVotes(mxEvent, getRelationsForEvent)) {
Modal.createDialog(
ErrorDialog,
{
title: _t("Can't edit poll"),
description: _t(
"Sorry, you can't edit a poll after votes have been cast.",
),
},
);
Modal.createDialog(ErrorDialog, {
title: _t("Can't edit poll"),
description: _t("Sorry, you can't edit a poll after votes have been cast."),
});
} else {
Modal.createDialog(
PollCreateDialog,
@ -243,9 +210,9 @@ export function launchPollEditor(mxEvent: MatrixEvent, getRelationsForEvent?: Ge
threadId: mxEvent.getThread()?.id ?? null,
editingMxEvent: mxEvent,
},
'mx_CompoundDialog',
"mx_CompoundDialog",
false, // isPriorityModal
true, // isStaticModal
true, // isStaticModal
);
}
}
@ -346,21 +313,13 @@ export default class MPollBody extends React.Component<IBodyProps, IState> {
const response = PollResponseEvent.from([answerId], this.props.mxEvent.getId()).serialize();
this.context.sendEvent(
this.props.mxEvent.getRoomId(),
response.type,
response.content,
).catch((e: any) => {
this.context.sendEvent(this.props.mxEvent.getRoomId(), response.type, response.content).catch((e: any) => {
console.error("Failed to submit poll response event:", e);
Modal.createDialog(
ErrorDialog,
{
title: _t("Vote not registered"),
description: _t(
"Sorry, your vote was not registered. Please try again."),
},
);
Modal.createDialog(ErrorDialog, {
title: _t("Vote not registered"),
description: _t("Sorry, your vote was not registered. Please try again."),
});
});
this.setState({ selected: answerId });
@ -387,22 +346,14 @@ export default class MPollBody extends React.Component<IBodyProps, IState> {
return null;
}
const relations = this.props.getRelationsForEvent(
eventId,
"m.reference",
eventType.name,
);
const relations = this.props.getRelationsForEvent(eventId, "m.reference", eventType.name);
if (relations) {
relationsList.push(relations);
}
// If there is an alternatve experimental event type, also look for that
if (eventType.altName) {
const altRelations = this.props.getRelationsForEvent(
eventId,
"m.reference",
eventType.altName,
);
const altRelations = this.props.getRelationsForEvent(eventId, "m.reference", eventType.altName);
if (altRelations) {
relationsList.push(altRelations);
}
@ -419,12 +370,7 @@ export default class MPollBody extends React.Component<IBodyProps, IState> {
*/
private collectUserVotes(): Map<string, UserVote> {
return collectUserVotes(
allVotes(
this.props.mxEvent,
this.context,
this.state.voteRelations,
this.state.endRelations,
),
allVotes(this.props.mxEvent, this.context, this.state.voteRelations, this.state.endRelations),
this.context.getUserId(),
this.state.selected,
);
@ -439,10 +385,10 @@ export default class MPollBody extends React.Component<IBodyProps, IState> {
* have already seen.
*/
private unselectIfNewEventFromMe() {
const newEvents: MatrixEvent[] = this.state.voteRelations.getRelations()
const newEvents: MatrixEvent[] = this.state.voteRelations
.getRelations()
.filter(isPollResponse)
.filter((mxEvent: MatrixEvent) =>
!this.seenEventIds.includes(mxEvent.getId()!));
.filter((mxEvent: MatrixEvent) => !this.seenEventIds.includes(mxEvent.getId()!));
let newSelected = this.state.selected;
if (newEvents.length > 0) {
@ -466,11 +412,7 @@ export default class MPollBody extends React.Component<IBodyProps, IState> {
}
private isEnded(): boolean {
return isPollEnded(
this.props.mxEvent,
this.context,
this.props.getRelationsForEvent,
);
return isPollEnded(this.props.mxEvent, this.context, this.props.getRelationsForEvent);
}
render() {
@ -493,36 +435,31 @@ export default class MPollBody extends React.Component<IBodyProps, IState> {
let totalText: string;
if (ended) {
totalText = _t(
"Final result based on %(count)s votes",
{ count: totalVotes },
);
totalText = _t("Final result based on %(count)s votes", { count: totalVotes });
} else if (!disclosed) {
totalText = _t("Results will be visible when the poll is ended");
} else if (myVote === undefined) {
if (totalVotes === 0) {
totalText = _t("No votes cast");
} else {
totalText = _t(
"%(count)s votes cast. Vote to see the results",
{ count: totalVotes },
);
totalText = _t("%(count)s votes cast. Vote to see the results", { count: totalVotes });
}
} else {
totalText = _t("Based on %(count)s votes", { count: totalVotes });
}
const editedSpan = (
this.props.mxEvent.replacingEvent()
? <span className="mx_MPollBody_edited"> ({ _t("edited") })</span>
: null
);
const editedSpan = this.props.mxEvent.replacingEvent() ? (
<span className="mx_MPollBody_edited"> ({_t("edited")})</span>
) : null;
return <div className="mx_MPollBody">
<h2>{ poll.question.text }{ editedSpan }</h2>
<div className="mx_MPollBody_allOptions">
{
poll.answers.map((answer: PollAnswerSubevent) => {
return (
<div className="mx_MPollBody">
<h2>
{poll.question.text}
{editedSpan}
</h2>
<div className="mx_MPollBody_allOptions">
{poll.answers.map((answer: PollAnswerSubevent) => {
let answerVotes = 0;
let votesText = "";
@ -531,53 +468,40 @@ export default class MPollBody extends React.Component<IBodyProps, IState> {
votesText = _t("%(count)s votes", { count: answerVotes });
}
const checked = (
(!ended && myVote === answer.id) ||
(ended && answerVotes === winCount)
);
const checked = (!ended && myVote === answer.id) || (ended && answerVotes === winCount);
const cls = classNames({
"mx_MPollBody_option": true,
"mx_MPollBody_option_checked": checked,
"mx_MPollBody_option_ended": ended,
mx_MPollBody_option: true,
mx_MPollBody_option_checked: checked,
mx_MPollBody_option_ended: ended,
});
const answerPercent = (
totalVotes === 0
? 0
: Math.round(100.0 * answerVotes / totalVotes)
);
return <div
key={answer.id}
className={cls}
onClick={() => this.selectOption(answer.id)}
>
{ (
ended
? <EndedPollOption
answer={answer}
checked={checked}
votesText={votesText} />
: <LivePollOption
const answerPercent = totalVotes === 0 ? 0 : Math.round((100.0 * answerVotes) / totalVotes);
return (
<div key={answer.id} className={cls} onClick={() => this.selectOption(answer.id)}>
{ended ? (
<EndedPollOption answer={answer} checked={checked} votesText={votesText} />
) : (
<LivePollOption
pollId={pollId}
answer={answer}
checked={checked}
votesText={votesText}
onOptionSelected={this.onOptionSelected} />
) }
<div className="mx_MPollBody_popularityBackground">
<div
className="mx_MPollBody_popularityAmount"
style={{ "width": `${answerPercent}%` }}
/>
onOptionSelected={this.onOptionSelected}
/>
)}
<div className="mx_MPollBody_popularityBackground">
<div
className="mx_MPollBody_popularityAmount"
style={{ width: `${answerPercent}%` }}
/>
</div>
</div>
</div>;
})
}
);
})}
</div>
<div className="mx_MPollBody_totalVotes">{totalText}</div>
</div>
<div className="mx_MPollBody_totalVotes">
{ totalText }
</div>
</div>;
);
}
}
@ -589,19 +513,17 @@ interface IEndedPollOptionProps {
function EndedPollOption(props: IEndedPollOptionProps) {
const cls = classNames({
"mx_MPollBody_endedOption": true,
"mx_MPollBody_endedOptionWinner": props.checked,
mx_MPollBody_endedOption: true,
mx_MPollBody_endedOptionWinner: props.checked,
});
return <div className={cls} data-value={props.answer.id}>
<div className="mx_MPollBody_optionDescription">
<div className="mx_MPollBody_optionText">
{ props.answer.text }
</div>
<div className="mx_MPollBody_optionVoteCount">
{ props.votesText }
return (
<div className={cls} data-value={props.answer.id}>
<div className="mx_MPollBody_optionDescription">
<div className="mx_MPollBody_optionText">{props.answer.text}</div>
<div className="mx_MPollBody_optionVoteCount">{props.votesText}</div>
</div>
</div>
</div>;
);
}
interface ILivePollOptionProps {
@ -613,27 +535,24 @@ interface ILivePollOptionProps {
}
function LivePollOption(props: ILivePollOptionProps) {
return <StyledRadioButton
className="mx_MPollBody_live-option"
name={`poll_answer_select-${props.pollId}`}
value={props.answer.id}
checked={props.checked}
onChange={props.onOptionSelected}
>
<div className="mx_MPollBody_optionDescription">
<div className="mx_MPollBody_optionText">
{ props.answer.text }
return (
<StyledRadioButton
className="mx_MPollBody_live-option"
name={`poll_answer_select-${props.pollId}`}
value={props.answer.id}
checked={props.checked}
onChange={props.onOptionSelected}
>
<div className="mx_MPollBody_optionDescription">
<div className="mx_MPollBody_optionText">{props.answer.text}</div>
<div className="mx_MPollBody_optionVoteCount">{props.votesText}</div>
</div>
<div className="mx_MPollBody_optionVoteCount">
{ props.votesText }
</div>
</div>
</StyledRadioButton>;
</StyledRadioButton>
);
}
export class UserVote {
constructor(public readonly ts: number, public readonly sender: string, public readonly answers: string[]) {
}
constructor(public readonly ts: number, public readonly sender: string, public readonly answers: string[]) {}
}
function userResponseFromPollResponseEvent(event: MatrixEvent): UserVote {
@ -642,11 +561,7 @@ function userResponseFromPollResponseEvent(event: MatrixEvent): UserVote {
throw new Error("Failed to parse Poll Response Event to determine user response");
}
return new UserVote(
event.getTs(),
event.getSender(),
response.answerIds,
);
return new UserVote(event.getTs(), event.getSender(), response.answerIds);
}
export function allVotes(
@ -660,14 +575,12 @@ export function allVotes(
function isOnOrBeforeEnd(responseEvent: MatrixEvent): boolean {
// From MSC3381:
// "Votes sent on or before the end event's timestamp are valid votes"
return (
endTs === null ||
responseEvent.getTs() <= endTs
);
return endTs === null || responseEvent.getTs() <= endTs;
}
if (voteRelations) {
return voteRelations.getRelations()
return voteRelations
.getRelations()
.filter(isPollResponse)
.filter(isOnOrBeforeEnd)
.map(userResponseFromPollResponseEvent);
@ -691,18 +604,13 @@ export function pollEndTs(
const roomCurrentState = matrixClient.getRoom(pollEvent.getRoomId()).currentState;
function userCanRedact(endEvent: MatrixEvent) {
return roomCurrentState.maySendRedactionForEvent(
pollEvent,
endEvent.getSender(),
);
return roomCurrentState.maySendRedactionForEvent(pollEvent, endEvent.getSender());
}
const tss: number[] = (
endRelations
.getRelations()
.filter(userCanRedact)
.map((evt: MatrixEvent) => evt.getTs())
);
const tss: number[] = endRelations
.getRelations()
.filter(userCanRedact)
.map((evt: MatrixEvent) => evt.getTs());
if (tss.length === 0) {
return null;
@ -744,10 +652,7 @@ function collectUserVotes(
return userVotes;
}
function countVotes(
userVotes: Map<string, UserVote>,
pollStart: PollStartEvent,
): Map<string, number> {
function countVotes(userVotes: Map<string, UserVote>, pollStart: PollStartEvent): Map<string, number> {
const collected = new Map<string, number>();
for (const response of userVotes.values()) {

View file

@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import MImageBody from './MImageBody';
import MImageBody from "./MImageBody";
import { BLURHASH_FIELD } from "../../../utils/image-media";
import Tooltip from "../elements/Tooltip";
import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent";
@ -37,7 +37,12 @@ export default class MStickerBody extends MImageBody {
if (!this.state.showImage) {
onClick = this.onClick;
}
return <div className="mx_MStickerBody_wrapper" onClick={onClick}> { children } </div>;
return (
<div className="mx_MStickerBody_wrapper" onClick={onClick}>
{" "}
{children}{" "}
</div>
);
}
// Placeholder to show in place of the sticker image if img onLoad hasn't fired yet.
@ -61,9 +66,11 @@ export default class MStickerBody extends MImageBody {
if (!content || !content.body || !content.info || !content.info.w) return null;
return <div style={{ left: content.info.w + 'px' }} className="mx_MStickerBody_tooltip">
<Tooltip label={content.body} />
</div>;
return (
<div style={{ left: content.info.w + "px" }} className="mx_MStickerBody_tooltip">
<Tooltip label={content.body} />
</div>
);
}
// Don't show "Download this_file.png ..."

View file

@ -14,13 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { decode } from "blurhash";
import { logger } from "matrix-js-sdk/src/logger";
import { _t } from '../../../languageHandler';
import { _t } from "../../../languageHandler";
import SettingsStore from "../../../settings/SettingsStore";
import InlineSpinner from '../elements/InlineSpinner';
import InlineSpinner from "../elements/InlineSpinner";
import { mediaFromContent } from "../../../customisations/Media";
import { BLURHASH_FIELD } from "../../../utils/image-media";
import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent";
@ -28,7 +28,7 @@ import { IBodyProps } from "./IBodyProps";
import MFileBody from "./MFileBody";
import { ImageSize, suggestedSize as suggestedVideoSize } from "../../../settings/enums/ImageSize";
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
import MediaProcessingError from './shared/MediaProcessingError';
import MediaProcessingError from "./shared/MediaProcessingError";
interface IState {
decryptedUrl?: string;
@ -61,7 +61,7 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
};
}
private getContentUrl(): string|null {
private getContentUrl(): string | null {
const content = this.props.mxEvent.getContent<IMediaEventContent>();
// During export, the content url will point to the MSC, which will later point to a local url
if (this.props.forExport) return content.file?.url || content.url;
@ -78,7 +78,7 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
return url && !url.startsWith("data:");
}
private getThumbUrl(): string|null {
private getThumbUrl(): string | null {
// there's no need of thumbnail when the content is local
if (this.props.forExport) return null;
@ -102,10 +102,10 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
const canvas = document.createElement("canvas");
const { w: width, h: height } = suggestedVideoSize(
SettingsStore.getValue("Images.size") as ImageSize,
{ w: info.w, h: info.h },
);
const { w: width, h: height } = suggestedVideoSize(SettingsStore.getValue("Images.size") as ImageSize, {
w: info.w,
h: info.h,
});
canvas.width = width;
canvas.height = height;
@ -205,21 +205,26 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
});
return;
}
this.setState({
decryptedUrl: await this.props.mediaEventHelper.sourceUrl.value,
decryptedBlob: await this.props.mediaEventHelper.sourceBlob.value,
fetchingData: false,
}, () => {
if (!this.videoRef.current) return;
this.videoRef.current.play();
});
this.setState(
{
decryptedUrl: await this.props.mediaEventHelper.sourceUrl.value,
decryptedBlob: await this.props.mediaEventHelper.sourceBlob.value,
fetchingData: false,
},
() => {
if (!this.videoRef.current) return;
this.videoRef.current.play();
},
);
this.props.onHeightChanged();
};
protected get showFileBody(): boolean {
return this.context.timelineRenderingType !== TimelineRenderingType.Room &&
return (
this.context.timelineRenderingType !== TimelineRenderingType.Room &&
this.context.timelineRenderingType !== TimelineRenderingType.Pinned &&
this.context.timelineRenderingType !== TimelineRenderingType.Search;
this.context.timelineRenderingType !== TimelineRenderingType.Search
);
}
private getFileBody = () => {
@ -235,19 +240,17 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
if (content.info?.w && content.info?.h) {
aspectRatio = `${content.info.w}/${content.info.h}`;
}
const { w: maxWidth, h: maxHeight } = suggestedVideoSize(
SettingsStore.getValue("Images.size") as ImageSize,
{ w: content.info?.w, h: content.info?.h },
);
const { w: maxWidth, h: maxHeight } = suggestedVideoSize(SettingsStore.getValue("Images.size") as ImageSize, {
w: content.info?.w,
h: content.info?.h,
});
// HACK: This div fills out space while the video loads, to prevent scroll jumps
const spaceFiller = <div style={{ width: maxWidth, height: maxHeight }} />;
if (this.state.error !== null) {
return (
<MediaProcessingError className="mx_MVideoBody">
{ _t("Error decrypting video") }
</MediaProcessingError>
<MediaProcessingError className="mx_MVideoBody">{_t("Error decrypting video")}</MediaProcessingError>
);
}
@ -261,7 +264,7 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
<div className="mx_MVideoBody_container" style={{ maxWidth, maxHeight, aspectRatio }}>
<InlineSpinner />
</div>
{ spaceFiller }
{spaceFiller}
</span>
);
}
@ -294,9 +297,9 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
poster={poster}
onPlay={this.videoOnPlay}
/>
{ spaceFiller }
{spaceFiller}
</div>
{ fileBody }
{fileBody}
</span>
);
}

View file

@ -16,7 +16,7 @@ limitations under the License.
import React from "react";
import InlineSpinner from '../elements/InlineSpinner';
import InlineSpinner from "../elements/InlineSpinner";
import { _t } from "../../../languageHandler";
import RecordingPlayback from "../audio_messages/RecordingPlayback";
import MAudioBody from "./MAudioBody";
@ -29,7 +29,7 @@ export default class MVoiceMessageBody extends MAudioBody {
if (this.state.error) {
return (
<MediaProcessingError className="mx_MVoiceMessageBody">
{ _t("Error processing voice message") }
{_t("Error processing voice message")}
</MediaProcessingError>
);
}
@ -46,7 +46,7 @@ export default class MVoiceMessageBody extends MAudioBody {
return (
<span className="mx_MVoiceMessageBody">
<RecordingPlayback playback={this.state.playback} />
{ this.showFileBody && <MFileBody {...this.props} showGenericPlaceholder={false} /> }
{this.showFileBody && <MFileBody {...this.props} showGenericPlaceholder={false} />}
</span>
);
}

View file

@ -16,28 +16,28 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ReactElement, useCallback, 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';
import { Thread } from 'matrix-js-sdk/src/models/thread';
import { M_BEACON_INFO } from 'matrix-js-sdk/src/@types/beacon';
import React, { ReactElement, useCallback, 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";
import { Thread } from "matrix-js-sdk/src/models/thread";
import { M_BEACON_INFO } from "matrix-js-sdk/src/@types/beacon";
import { Icon as ContextMenuIcon } from '../../../../res/img/element-icons/context-menu.svg';
import { Icon as EditIcon } from '../../../../res/img/element-icons/room/message-bar/edit.svg';
import { Icon as EmojiIcon } from '../../../../res/img/element-icons/room/message-bar/emoji.svg';
import { Icon as ResendIcon } from '../../../../res/img/element-icons/retry.svg';
import { Icon as ThreadIcon } from '../../../../res/img/element-icons/message/thread.svg';
import { Icon as TrashcanIcon } from '../../../../res/img/element-icons/trashcan.svg';
import { Icon as StarIcon } from '../../../../res/img/element-icons/room/message-bar/star.svg';
import { Icon as ReplyIcon } from '../../../../res/img/element-icons/room/message-bar/reply.svg';
import { Icon as ExpandMessageIcon } from '../../../../res/img/element-icons/expand-message.svg';
import { Icon as CollapseMessageIcon } from '../../../../res/img/element-icons/collapse-message.svg';
import type { Relations } from 'matrix-js-sdk/src/models/relations';
import { _t } from '../../../languageHandler';
import dis, { defaultDispatcher } from '../../../dispatcher/dispatcher';
import ContextMenu, { aboveLeftOf, ContextMenuTooltipButton, useContextMenu } from '../../structures/ContextMenu';
import { isContentActionable, canEditContent, editEvent, canCancel } from '../../../utils/EventUtils';
import { Icon as ContextMenuIcon } from "../../../../res/img/element-icons/context-menu.svg";
import { Icon as EditIcon } from "../../../../res/img/element-icons/room/message-bar/edit.svg";
import { Icon as EmojiIcon } from "../../../../res/img/element-icons/room/message-bar/emoji.svg";
import { Icon as ResendIcon } from "../../../../res/img/element-icons/retry.svg";
import { Icon as ThreadIcon } from "../../../../res/img/element-icons/message/thread.svg";
import { Icon as TrashcanIcon } from "../../../../res/img/element-icons/trashcan.svg";
import { Icon as StarIcon } from "../../../../res/img/element-icons/room/message-bar/star.svg";
import { Icon as ReplyIcon } from "../../../../res/img/element-icons/room/message-bar/reply.svg";
import { Icon as ExpandMessageIcon } from "../../../../res/img/element-icons/expand-message.svg";
import { Icon as CollapseMessageIcon } from "../../../../res/img/element-icons/collapse-message.svg";
import type { Relations } from "matrix-js-sdk/src/models/relations";
import { _t } from "../../../languageHandler";
import dis, { defaultDispatcher } from "../../../dispatcher/dispatcher";
import ContextMenu, { aboveLeftOf, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu";
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";
@ -46,20 +46,20 @@ 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 ReplyChain from '../elements/ReplyChain';
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/context';
import { shouldDisplayReply } from '../../../utils/Reply';
import { CardContext } from "../right_panel/context";
import { shouldDisplayReply } from "../../../utils/Reply";
import { Key } from "../../../Keyboard";
import { ALTERNATE_KEY_NAME } from "../../../accessibility/KeyboardShortcuts";
import { UserTab } from '../dialogs/UserTab';
import { Action } from '../../../dispatcher/actions';
import { UserTab } from "../dialogs/UserTab";
import { Action } from "../../../dispatcher/actions";
import SdkConfig from "../../../SdkConfig";
import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload";
import useFavouriteMessages from '../../../hooks/useFavouriteMessages';
import { GetRelationsForEvent } from '../rooms/EventTile';
import useFavouriteMessages from "../../../hooks/useFavouriteMessages";
import { GetRelationsForEvent } from "../rooms/EventTile";
interface IOptionsButtonProps {
mxEvent: MatrixEvent;
@ -85,16 +85,19 @@ const OptionsButton: React.FC<IOptionsButtonProps> = ({
onFocusChange(menuDisplayed);
}, [onFocusChange, menuDisplayed]);
const onOptionsClick = useCallback((e: React.MouseEvent): void => {
// Don't open the regular browser or our context menu on right-click
e.preventDefault();
e.stopPropagation();
openMenu();
// when the context menu is opened directly, e.g. via mouse click, the onFocus handler which tracks
// the element that is currently focused is skipped. So we want to call onFocus manually to keep the
// position in the page even when someone is clicking around.
onFocus();
}, [openMenu, onFocus]);
const onOptionsClick = useCallback(
(e: React.MouseEvent): void => {
// Don't open the regular browser or our context menu on right-click
e.preventDefault();
e.stopPropagation();
openMenu();
// when the context menu is opened directly, e.g. via mouse click, the onFocus handler which tracks
// the element that is currently focused is skipped. So we want to call onFocus manually to keep the
// position in the page even when someone is clicking around.
onFocus();
},
[openMenu, onFocus],
);
let contextMenu: ReactElement | null;
if (menuDisplayed) {
@ -102,32 +105,36 @@ const OptionsButton: React.FC<IOptionsButtonProps> = ({
const replyChain = getReplyChain && getReplyChain();
const buttonRect = button.current.getBoundingClientRect();
contextMenu = <MessageContextMenu
{...aboveLeftOf(buttonRect)}
mxEvent={mxEvent}
permalinkCreator={permalinkCreator}
eventTileOps={tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined}
collapseReplyChain={replyChain && replyChain.canCollapse() ? replyChain.collapse : undefined}
onFinished={closeMenu}
getRelationsForEvent={getRelationsForEvent}
/>;
contextMenu = (
<MessageContextMenu
{...aboveLeftOf(buttonRect)}
mxEvent={mxEvent}
permalinkCreator={permalinkCreator}
eventTileOps={tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined}
collapseReplyChain={replyChain && replyChain.canCollapse() ? replyChain.collapse : undefined}
onFinished={closeMenu}
getRelationsForEvent={getRelationsForEvent}
/>
);
}
return <React.Fragment>
<ContextMenuTooltipButton
className="mx_MessageActionBar_iconButton mx_MessageActionBar_optionsButton"
title={_t("Options")}
onClick={onOptionsClick}
onContextMenu={onOptionsClick}
isExpanded={menuDisplayed}
inputRef={button}
onFocus={onFocus}
tabIndex={isActive ? 0 : -1}
>
<ContextMenuIcon />
</ContextMenuTooltipButton>
{ contextMenu }
</React.Fragment>;
return (
<React.Fragment>
<ContextMenuTooltipButton
className="mx_MessageActionBar_iconButton mx_MessageActionBar_optionsButton"
title={_t("Options")}
onClick={onOptionsClick}
onContextMenu={onOptionsClick}
isExpanded={menuDisplayed}
inputRef={button}
onFocus={onFocus}
tabIndex={isActive ? 0 : -1}
>
<ContextMenuIcon />
</ContextMenuTooltipButton>
{contextMenu}
</React.Fragment>
);
};
interface IReactButtonProps {
@ -146,39 +153,46 @@ const ReactButton: React.FC<IReactButtonProps> = ({ mxEvent, reactions, onFocusC
let contextMenu;
if (menuDisplayed) {
const buttonRect = button.current.getBoundingClientRect();
contextMenu = <ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu} managed={false}>
<ReactionPicker mxEvent={mxEvent} reactions={reactions} onFinished={closeMenu} />
</ContextMenu>;
contextMenu = (
<ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu} managed={false}>
<ReactionPicker mxEvent={mxEvent} reactions={reactions} onFinished={closeMenu} />
</ContextMenu>
);
}
const onClick = useCallback((e: React.MouseEvent) => {
// Don't open the regular browser or our context menu on right-click
e.preventDefault();
e.stopPropagation();
const onClick = useCallback(
(e: React.MouseEvent) => {
// Don't open the regular browser or our context menu on right-click
e.preventDefault();
e.stopPropagation();
openMenu();
// when the context menu is opened directly, e.g. via mouse click, the onFocus handler which tracks
// the element that is currently focused is skipped. So we want to call onFocus manually to keep the
// position in the page even when someone is clicking around.
onFocus();
}, [openMenu, onFocus]);
openMenu();
// when the context menu is opened directly, e.g. via mouse click, the onFocus handler which tracks
// the element that is currently focused is skipped. So we want to call onFocus manually to keep the
// position in the page even when someone is clicking around.
onFocus();
},
[openMenu, onFocus],
);
return <React.Fragment>
<ContextMenuTooltipButton
className="mx_MessageActionBar_iconButton"
title={_t("React")}
onClick={onClick}
onContextMenu={onClick}
isExpanded={menuDisplayed}
inputRef={button}
onFocus={onFocus}
tabIndex={isActive ? 0 : -1}
>
<EmojiIcon />
</ContextMenuTooltipButton>
return (
<React.Fragment>
<ContextMenuTooltipButton
className="mx_MessageActionBar_iconButton"
title={_t("React")}
onClick={onClick}
onContextMenu={onClick}
isExpanded={menuDisplayed}
inputRef={button}
onFocus={onFocus}
tabIndex={isActive ? 0 : -1}
>
<EmojiIcon />
</ContextMenuTooltipButton>
{ contextMenu }
</React.Fragment>;
{contextMenu}
</React.Fragment>
);
};
interface IReplyInThreadButton {
@ -230,37 +244,38 @@ const ReplyInThreadButton = ({ mxEvent }: IReplyInThreadButton) => {
}
};
return <RovingAccessibleTooltipButton
className="mx_MessageActionBar_iconButton mx_MessageActionBar_threadButton"
disabled={hasARelation}
tooltip={<>
<div className="mx_Tooltip_title">
{ !hasARelation
return (
<RovingAccessibleTooltipButton
className="mx_MessageActionBar_iconButton 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") }
</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}
onContextMenu={onClick}
>
<ThreadIcon />
{ firstTimeSeeingThreads && !threadsEnabled && (
<div className="mx_Indicator" />
) }
</RovingAccessibleTooltipButton>;
: _t("Can't create a thread from an event with an existing relation")
}
onClick={onClick}
onContextMenu={onClick}
>
<ThreadIcon />
{firstTimeSeeingThreads && !threadsEnabled && <div className="mx_Indicator" />}
</RovingAccessibleTooltipButton>
);
};
interface IFavouriteButtonProp {
@ -272,26 +287,31 @@ const FavouriteButton = ({ mxEvent }: IFavouriteButtonProp) => {
const eventId = mxEvent.getId();
const classes = classNames("mx_MessageActionBar_iconButton mx_MessageActionBar_favouriteButton", {
'mx_MessageActionBar_favouriteButton_fillstar': isFavourite(eventId),
mx_MessageActionBar_favouriteButton_fillstar: isFavourite(eventId),
});
const onClick = useCallback((e: React.MouseEvent) => {
// Don't open the regular browser or our context menu on right-click
e.preventDefault();
e.stopPropagation();
const onClick = useCallback(
(e: React.MouseEvent) => {
// Don't open the regular browser or our context menu on right-click
e.preventDefault();
e.stopPropagation();
toggleFavourite(eventId);
}, [toggleFavourite, eventId]);
toggleFavourite(eventId);
},
[toggleFavourite, eventId],
);
return <RovingAccessibleTooltipButton
className={classes}
title={_t("Favourite")}
onClick={onClick}
onContextMenu={onClick}
data-testid={eventId}
>
<StarIcon />
</RovingAccessibleTooltipButton>;
return (
<RovingAccessibleTooltipButton
className={classes}
title={_t("Favourite")}
onClick={onClick}
onContextMenu={onClick}
data-testid={eventId}
>
<StarIcon />
</RovingAccessibleTooltipButton>
);
};
interface IMessageActionBarProps {
@ -356,7 +376,7 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
e.stopPropagation();
dis.dispatch({
action: 'reply_to_event',
action: "reply_to_event",
event: this.props.mxEvent,
context: this.context.timelineRenderingType,
});
@ -370,9 +390,7 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
editEvent(this.props.mxEvent, this.context.timelineRenderingType, this.props.getRelationsForEvent);
};
private readonly forbiddenThreadHeadMsgType = [
MsgType.KeyVerificationRequest,
];
private readonly forbiddenThreadHeadMsgType = [MsgType.KeyVerificationRequest];
private get showReplyInThreadAction(): boolean {
if (!SettingsStore.getValue("feature_thread") && !Thread.hasServerSideSupport) {
@ -380,7 +398,8 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
return null;
}
if (!SettingsStore.getBetaInfo("feature_thread") &&
if (
!SettingsStore.getBetaInfo("feature_thread") &&
!SettingsStore.getValue("feature_thread") &&
!SdkConfig.get("show_labs_settings")
) {
@ -391,15 +410,13 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
const inNotThreadTimeline = this.context.timelineRenderingType !== TimelineRenderingType.Thread;
const isAllowedMessageType = (
!this.forbiddenThreadHeadMsgType.includes(
this.props.mxEvent.getContent().msgtype as MsgType) &&
const isAllowedMessageType =
!this.forbiddenThreadHeadMsgType.includes(this.props.mxEvent.getContent().msgtype as MsgType) &&
/** forbid threads from live location shares
* until cross-platform support
* (PSF-1041)
*/
!M_BEACON_INFO.matches(this.props.mxEvent.getType())
);
!M_BEACON_INFO.matches(this.props.mxEvent.getType());
return inNotThreadTimeline && isAllowedMessageType;
}
@ -446,26 +463,30 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
public render(): JSX.Element {
const toolbarOpts = [];
if (canEditContent(this.props.mxEvent)) {
toolbarOpts.push(<RovingAccessibleTooltipButton
className="mx_MessageActionBar_iconButton"
title={_t("Edit")}
onClick={this.onEditClick}
onContextMenu={this.onEditClick}
key="edit"
>
<EditIcon />
</RovingAccessibleTooltipButton>);
toolbarOpts.push(
<RovingAccessibleTooltipButton
className="mx_MessageActionBar_iconButton"
title={_t("Edit")}
onClick={this.onEditClick}
onContextMenu={this.onEditClick}
key="edit"
>
<EditIcon />
</RovingAccessibleTooltipButton>,
);
}
const cancelSendingButton = <RovingAccessibleTooltipButton
className="mx_MessageActionBar_iconButton"
title={_t("Delete")}
onClick={this.onCancelClick}
onContextMenu={this.onCancelClick}
key="cancel"
>
<TrashcanIcon />
</RovingAccessibleTooltipButton>;
const cancelSendingButton = (
<RovingAccessibleTooltipButton
className="mx_MessageActionBar_iconButton"
title={_t("Delete")}
onClick={this.onCancelClick}
onContextMenu={this.onCancelClick}
key="cancel"
>
<TrashcanIcon />
</RovingAccessibleTooltipButton>
);
const threadTooltipButton = <ReplyInThreadButton mxEvent={this.props.mxEvent} key="reply_thread" />;
@ -478,15 +499,19 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
if (allowCancel && isFailed) {
// The resend button needs to appear ahead of the edit button, so insert to the
// start of the opts
toolbarOpts.splice(0, 0, <RovingAccessibleTooltipButton
className="mx_MessageActionBar_iconButton"
title={_t("Retry")}
onClick={this.onResendClick}
onContextMenu={this.onResendClick}
key="resend"
>
<ResendIcon />
</RovingAccessibleTooltipButton>);
toolbarOpts.splice(
0,
0,
<RovingAccessibleTooltipButton
className="mx_MessageActionBar_iconButton"
title={_t("Retry")}
onClick={this.onResendClick}
onContextMenu={this.onResendClick}
key="resend"
>
<ResendIcon />
</RovingAccessibleTooltipButton>,
);
// The delete button should appear last, so we can just drop it at the end
toolbarOpts.push(cancelSendingButton);
@ -500,7 +525,9 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
if (this.showReplyInThreadAction) {
toolbarOpts.splice(0, 0, threadTooltipButton);
}
toolbarOpts.splice(0, 0, (
toolbarOpts.splice(
0,
0,
<RovingAccessibleTooltipButton
className="mx_MessageActionBar_iconButton"
title={_t("Reply")}
@ -509,32 +536,39 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
key="reply"
>
<ReplyIcon />
</RovingAccessibleTooltipButton>
));
</RovingAccessibleTooltipButton>,
);
}
if (this.context.canReact) {
toolbarOpts.splice(0, 0, <ReactButton
mxEvent={this.props.mxEvent}
reactions={this.props.reactions}
onFocusChange={this.onFocusChange}
key="react"
/>);
toolbarOpts.splice(
0,
0,
<ReactButton
mxEvent={this.props.mxEvent}
reactions={this.props.reactions}
onFocusChange={this.onFocusChange}
key="react"
/>,
);
}
if (SettingsStore.getValue("feature_favourite_messages")) {
toolbarOpts.splice(-1, 0, (
<FavouriteButton key="favourite" mxEvent={this.props.mxEvent} />
));
toolbarOpts.splice(-1, 0, <FavouriteButton key="favourite" mxEvent={this.props.mxEvent} />);
}
// XXX: Assuming that the underlying tile will be a media event if it is eligible media.
if (MediaEventHelper.isEligible(this.props.mxEvent)) {
toolbarOpts.splice(0, 0, <DownloadActionButton
mxEvent={this.props.mxEvent}
mediaEventHelperGet={() => this.props.getTile?.().getMediaHelper?.()}
key="download"
/>);
toolbarOpts.splice(
0,
0,
<DownloadActionButton
mxEvent={this.props.mxEvent}
mediaEventHelperGet={() => this.props.getTile?.().getMediaHelper?.()}
key="download"
/>,
);
}
} else if (SettingsStore.getValue("feature_thread") &&
} else if (
SettingsStore.getValue("feature_thread") &&
// Show thread icon even for deleted messages, but only within main timeline
this.context.timelineRenderingType === TimelineRenderingType.Room &&
this.props.mxEvent.getThread()
@ -548,46 +582,49 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
if (this.props.isQuoteExpanded !== undefined && shouldDisplayReply(this.props.mxEvent)) {
const expandClassName = classNames({
'mx_MessageActionBar_iconButton': true,
'mx_MessageActionBar_expandCollapseMessageButton': true,
mx_MessageActionBar_iconButton: true,
mx_MessageActionBar_expandCollapseMessageButton: true,
});
const tooltip = <>
<div className="mx_Tooltip_title">
{ this.props.isQuoteExpanded ? _t("Collapse quotes") : _t("Expand quotes") }
</div>
<div className="mx_Tooltip_sub">
{ _t(ALTERNATE_KEY_NAME[Key.SHIFT]) + " + " + _t("Click") }
</div>
</>;
toolbarOpts.push(<RovingAccessibleTooltipButton
className={expandClassName}
title={this.props.isQuoteExpanded ? _t("Collapse quotes") : _t("Expand quotes")}
tooltip={tooltip}
onClick={this.props.toggleThreadExpanded}
key="expand"
>
{ this.props.isQuoteExpanded
? <CollapseMessageIcon />
: <ExpandMessageIcon />
}
</RovingAccessibleTooltipButton>);
const tooltip = (
<>
<div className="mx_Tooltip_title">
{this.props.isQuoteExpanded ? _t("Collapse quotes") : _t("Expand quotes")}
</div>
<div className="mx_Tooltip_sub">{_t(ALTERNATE_KEY_NAME[Key.SHIFT]) + " + " + _t("Click")}</div>
</>
);
toolbarOpts.push(
<RovingAccessibleTooltipButton
className={expandClassName}
title={this.props.isQuoteExpanded ? _t("Collapse quotes") : _t("Expand quotes")}
tooltip={tooltip}
onClick={this.props.toggleThreadExpanded}
key="expand"
>
{this.props.isQuoteExpanded ? <CollapseMessageIcon /> : <ExpandMessageIcon />}
</RovingAccessibleTooltipButton>,
);
}
// The menu button should be last, so dump it there.
toolbarOpts.push(<OptionsButton
mxEvent={this.props.mxEvent}
getReplyChain={this.props.getReplyChain}
getTile={this.props.getTile}
permalinkCreator={this.props.permalinkCreator}
onFocusChange={this.onFocusChange}
key="menu"
getRelationsForEvent={this.props.getRelationsForEvent}
/>);
toolbarOpts.push(
<OptionsButton
mxEvent={this.props.mxEvent}
getReplyChain={this.props.getReplyChain}
getTile={this.props.getTile}
permalinkCreator={this.props.permalinkCreator}
onFocusChange={this.onFocusChange}
key="menu"
getRelationsForEvent={this.props.getRelationsForEvent}
/>,
);
}
// aria-live=off to not have this read out automatically as navigating around timeline, gets repetitive.
return <Toolbar className="mx_MessageActionBar" aria-label={_t("Message Actions")} aria-live="off">
{ toolbarOpts }
</Toolbar>;
return (
<Toolbar className="mx_MessageActionBar" aria-label={_t("Message Actions")} aria-live="off">
{toolbarOpts}
</Toolbar>
);
}
}

View file

@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { createRef } from 'react';
import React, { createRef } from "react";
import { EventType, MsgType } from "matrix-js-sdk/src/@types/event";
import { M_BEACON_INFO } from 'matrix-js-sdk/src/@types/beacon';
import { M_LOCATION } from 'matrix-js-sdk/src/@types/location';
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 { MatrixEventEvent } from "matrix-js-sdk/src/models/event";
@ -29,7 +29,7 @@ import { IMediaBody } from "./IMediaBody";
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
import { ReactAnyComponent } from "../../../@types/common";
import { IBodyProps } from "./IBodyProps";
import MatrixClientContext from '../../../contexts/MatrixClientContext';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import TextualBody from "./TextualBody";
import MImageBody from "./MImageBody";
import MFileBody from "./MFileBody";
@ -41,7 +41,7 @@ import MLocationBody from "./MLocationBody";
import MjolnirBody from "./MjolnirBody";
import MBeaconBody from "./MBeaconBody";
import { GetRelationsForEvent, IEventTileOps } from "../rooms/EventTile";
import { VoiceBroadcastBody, VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from '../../../voice-broadcast';
import { VoiceBroadcastBody, VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "../../../voice-broadcast";
// onMessageAllowed is handled internally
interface IProps extends Omit<IBodyProps, "onMessageAllowed" | "mediaEventHelper"> {
@ -165,17 +165,11 @@ export default class MessageEvent extends React.Component<IProps> implements IMe
}
// TODO: move to eventTypes when location sharing spec stabilises
if (
M_LOCATION.matches(type) ||
(type === EventType.RoomMessage && msgtype === MsgType.Location)
) {
if (M_LOCATION.matches(type) || (type === EventType.RoomMessage && msgtype === MsgType.Location)) {
BodyType = MLocationBody;
}
if (
type === VoiceBroadcastInfoEventType
&& content?.state === VoiceBroadcastInfoState.Started
) {
if (type === VoiceBroadcastInfoEventType && content?.state === VoiceBroadcastInfoState.Started) {
BodyType = VoiceBroadcastBody;
}
}
@ -185,7 +179,7 @@ export default class MessageEvent extends React.Component<IProps> implements IMe
const allowRender = localStorage.getItem(key) === "true";
if (!allowRender) {
const userDomain = this.props.mxEvent.getSender().split(':').slice(1).join(':');
const userDomain = this.props.mxEvent.getSender().split(":").slice(1).join(":");
const userBanned = Mjolnir.sharedInstance().isUserBanned(this.props.mxEvent.getSender());
const serverBanned = Mjolnir.sharedInstance().isServerBanned(userDomain);
@ -196,22 +190,24 @@ export default class MessageEvent extends React.Component<IProps> implements IMe
}
// @ts-ignore - this is a dynamic react component
return BodyType ? <BodyType
ref={this.body}
mxEvent={this.props.mxEvent}
highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview}
forExport={this.props.forExport}
maxImageHeight={this.props.maxImageHeight}
replacingEventId={this.props.replacingEventId}
editState={this.props.editState}
onHeightChanged={this.props.onHeightChanged}
onMessageAllowed={this.onTileUpdate}
permalinkCreator={this.props.permalinkCreator}
mediaEventHelper={this.mediaHelper}
getRelationsForEvent={this.props.getRelationsForEvent}
isSeeingThroughMessageHiddenForModeration={this.props.isSeeingThroughMessageHiddenForModeration}
/> : null;
return BodyType ? (
<BodyType
ref={this.body}
mxEvent={this.props.mxEvent}
highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview}
forExport={this.props.forExport}
maxImageHeight={this.props.maxImageHeight}
replacingEventId={this.props.replacingEventId}
editState={this.props.editState}
onHeightChanged={this.props.onHeightChanged}
onMessageAllowed={this.onTileUpdate}
permalinkCreator={this.props.permalinkCreator}
mediaEventHelper={this.mediaHelper}
getRelationsForEvent={this.props.getRelationsForEvent}
isSeeingThroughMessageHiddenForModeration={this.props.isSeeingThroughMessageHiddenForModeration}
/>
) : null;
}
}

View file

@ -15,9 +15,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { formatFullDate, formatTime, formatFullTime, formatRelativeTime } from '../../../DateUtils';
import { formatFullDate, formatTime, formatFullTime, formatRelativeTime } from "../../../DateUtils";
interface IProps {
ts: number;
@ -47,7 +47,7 @@ export default class MessageTimestamp extends React.Component<IProps> {
title={formatFullDate(date, this.props.showTwelveHour)}
aria-hidden={true}
>
{ timestamp }
{timestamp}
</span>
);
}

View file

@ -14,11 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { _t } from '../../../languageHandler';
import AccessibleButton from '../elements/AccessibleButton';
import { _t } from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton";
interface IProps {
mxEvent: MatrixEvent;
@ -37,14 +37,21 @@ export default class MjolnirBody extends React.Component<IProps> {
public render(): JSX.Element {
return (
<div className='mx_MjolnirBody'><i>{ _t(
"You have ignored this user, so their message is hidden. <a>Show anyways.</a>",
{}, {
a: (sub) => <AccessibleButton kind="link_inline" onClick={this.onAllowClick}>
{ sub }
</AccessibleButton>,
},
) }</i></div>
<div className="mx_MjolnirBody">
<i>
{_t(
"You have ignored this user, so their message is hidden. <a>Show anyways.</a>",
{},
{
a: (sub) => (
<AccessibleButton kind="link_inline" onClick={this.onAllowClick}>
{sub}
</AccessibleButton>
),
},
)}
</i>
</div>
);
}
}

View file

@ -19,8 +19,8 @@ import classNames from "classnames";
import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/event";
import { Relations, RelationsEvent } from "matrix-js-sdk/src/models/relations";
import { _t } from '../../../languageHandler';
import { isContentActionable } from '../../../utils/EventUtils';
import { _t } from "../../../languageHandler";
import { isContentActionable } from "../../../utils/EventUtils";
import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton";
import ContextMenu, { aboveLeftOf, useContextMenu } from "../../structures/ContextMenu";
import ReactionPicker from "../emojipicker/ReactionPicker";
@ -37,28 +37,32 @@ const ReactButton = ({ mxEvent, reactions }: IProps) => {
let contextMenu;
if (menuDisplayed) {
const buttonRect = button.current.getBoundingClientRect();
contextMenu = <ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu} managed={false}>
<ReactionPicker mxEvent={mxEvent} reactions={reactions} onFinished={closeMenu} />
</ContextMenu>;
contextMenu = (
<ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu} managed={false}>
<ReactionPicker mxEvent={mxEvent} reactions={reactions} onFinished={closeMenu} />
</ContextMenu>
);
}
return <React.Fragment>
<ContextMenuTooltipButton
className={classNames("mx_ReactionsRow_addReactionButton", {
mx_ReactionsRow_addReactionButton_active: menuDisplayed,
})}
title={_t("Add reaction")}
onClick={openMenu}
onContextMenu={e => {
e.preventDefault();
openMenu();
}}
isExpanded={menuDisplayed}
inputRef={button}
/>
return (
<React.Fragment>
<ContextMenuTooltipButton
className={classNames("mx_ReactionsRow_addReactionButton", {
mx_ReactionsRow_addReactionButton_active: menuDisplayed,
})}
title={_t("Add reaction")}
onClick={openMenu}
onContextMenu={(e) => {
e.preventDefault();
openMenu();
}}
isExpanded={menuDisplayed}
inputRef={button}
/>
{ contextMenu }
</React.Fragment>;
{contextMenu}
</React.Fragment>
);
};
interface IProps {
@ -165,30 +169,37 @@ export default class ReactionsRow extends React.PureComponent<IProps, IState> {
return null;
}
let items = reactions.getSortedAnnotationsByKey().map(([content, events]) => {
const count = events.size;
if (!count) {
return null;
}
const myReactionEvent = myReactions && myReactions.find(mxEvent => {
if (mxEvent.isRedacted()) {
return false;
let items = reactions
.getSortedAnnotationsByKey()
.map(([content, events]) => {
const count = events.size;
if (!count) {
return null;
}
return mxEvent.getRelation().key === content;
});
return <ReactionsRowButton
key={content}
content={content}
count={count}
mxEvent={mxEvent}
reactionEvents={events}
myReactionEvent={myReactionEvent}
disabled={
!this.context.canReact ||
(myReactionEvent && !myReactionEvent.isRedacted() && !this.context.canSelfRedact)
}
/>;
}).filter(item => !!item);
const myReactionEvent =
myReactions &&
myReactions.find((mxEvent) => {
if (mxEvent.isRedacted()) {
return false;
}
return mxEvent.getRelation().key === content;
});
return (
<ReactionsRowButton
key={content}
content={content}
count={count}
mxEvent={mxEvent}
reactionEvents={events}
myReactionEvent={myReactionEvent}
disabled={
!this.context.canReact ||
(myReactionEvent && !myReactionEvent.isRedacted() && !this.context.canSelfRedact)
}
/>
);
})
.filter((item) => !!item);
if (!items.length) return null;
@ -196,15 +207,13 @@ export default class ReactionsRow extends React.PureComponent<IProps, IState> {
// The "+ 1" ensure that the "show all" reveals something that takes up
// more space than the button itself.
let showAllButton: JSX.Element;
if ((items.length > MAX_ITEMS_WHEN_LIMITED + 1) && !showAll) {
if (items.length > MAX_ITEMS_WHEN_LIMITED + 1 && !showAll) {
items = items.slice(0, MAX_ITEMS_WHEN_LIMITED);
showAllButton = <AccessibleButton
kind="link_inline"
className="mx_ReactionsRow_showAll"
onClick={this.onShowAllClick}
>
{ _t("Show all") }
</AccessibleButton>;
showAllButton = (
<AccessibleButton kind="link_inline" className="mx_ReactionsRow_showAll" onClick={this.onShowAllClick}>
{_t("Show all")}
</AccessibleButton>
);
}
let addReactionButton: JSX.Element;
@ -212,14 +221,12 @@ export default class ReactionsRow extends React.PureComponent<IProps, IState> {
addReactionButton = <ReactButton mxEvent={mxEvent} reactions={reactions} />;
}
return <div
className="mx_ReactionsRow"
role="toolbar"
aria-label={_t("Reactions")}
>
{ items }
{ showAllButton }
{ addReactionButton }
</div>;
return (
<div className="mx_ReactionsRow" role="toolbar" aria-label={_t("Reactions")}>
{items}
{showAllButton}
{addReactionButton}
</div>
);
}
}

View file

@ -18,8 +18,8 @@ import React from "react";
import classNames from "classnames";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { _t } from '../../../languageHandler';
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
import { _t } from "../../../languageHandler";
import { formatCommaSeparatedList } from "../../../utils/FormattingUtils";
import dis from "../../../dispatcher/dispatcher";
import ReactionsRowButtonTooltip from "./ReactionsRowButtonTooltip";
import AccessibleButton from "../elements/AccessibleButton";
@ -57,16 +57,13 @@ export default class ReactionsRowButton extends React.PureComponent<IProps, ISta
onClick = () => {
const { mxEvent, myReactionEvent, content } = this.props;
if (myReactionEvent) {
this.context.redactEvent(
mxEvent.getRoomId(),
myReactionEvent.getId(),
);
this.context.redactEvent(mxEvent.getRoomId(), myReactionEvent.getId());
} else {
this.context.sendEvent(mxEvent.getRoomId(), "m.reaction", {
"m.relates_to": {
"rel_type": "m.annotation",
"event_id": mxEvent.getId(),
"key": content,
rel_type: "m.annotation",
event_id: mxEvent.getId(),
key: content,
},
});
dis.dispatch({ action: "message_sent" });
@ -98,12 +95,14 @@ export default class ReactionsRowButton extends React.PureComponent<IProps, ISta
let tooltip;
if (this.state.tooltipRendered) {
tooltip = <ReactionsRowButtonTooltip
mxEvent={this.props.mxEvent}
content={content}
reactionEvents={reactionEvents}
visible={this.state.tooltipVisible}
/>;
tooltip = (
<ReactionsRowButtonTooltip
mxEvent={this.props.mxEvent}
content={content}
reactionEvents={reactionEvents}
visible={this.state.tooltipVisible}
/>
);
}
const room = this.context.getRoom(mxEvent.getRoomId());
@ -123,21 +122,23 @@ export default class ReactionsRowButton extends React.PureComponent<IProps, ISta
}
}
return <AccessibleButton
className={classes}
aria-label={label}
onClick={this.onClick}
disabled={this.props.disabled}
onMouseOver={this.onMouseOver}
onMouseLeave={this.onMouseLeave}
>
<span className="mx_ReactionsRowButton_content" aria-hidden="true">
{ content }
</span>
<span className="mx_ReactionsRowButton_count" aria-hidden="true">
{ count }
</span>
{ tooltip }
</AccessibleButton>;
return (
<AccessibleButton
className={classes}
aria-label={label}
onClick={this.onClick}
disabled={this.props.disabled}
onMouseOver={this.onMouseOver}
onMouseLeave={this.onMouseLeave}
>
<span className="mx_ReactionsRowButton_content" aria-hidden="true">
{content}
</span>
<span className="mx_ReactionsRowButton_count" aria-hidden="true">
{count}
</span>
{tooltip}
</AccessibleButton>
);
}
}

View file

@ -17,9 +17,9 @@ limitations under the License.
import React from "react";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { unicodeToShortcode } from '../../../HtmlUtils';
import { _t } from '../../../languageHandler';
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
import { unicodeToShortcode } from "../../../HtmlUtils";
import { _t } from "../../../languageHandler";
import { formatCommaSeparatedList } from "../../../utils/FormattingUtils";
import Tooltip from "../elements/Tooltip";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
@ -49,27 +49,27 @@ export default class ReactionsRowButtonTooltip extends React.PureComponent<IProp
senders.push(name);
}
const shortName = unicodeToShortcode(content);
tooltipLabel = <div>{ _t(
"<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>",
{
shortName,
},
{
reactors: () => {
return <div className="mx_Tooltip_title">
{ formatCommaSeparatedList(senders, 6) }
</div>;
},
reactedWith: (sub) => {
if (!shortName) {
return null;
}
return <div className="mx_Tooltip_sub">
{ sub }
</div>;
},
},
) }</div>;
tooltipLabel = (
<div>
{_t(
"<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>",
{
shortName,
},
{
reactors: () => {
return <div className="mx_Tooltip_title">{formatCommaSeparatedList(senders, 6)}</div>;
},
reactedWith: (sub) => {
if (!shortName) {
return null;
}
return <div className="mx_Tooltip_sub">{sub}</div>;
},
},
)}
</div>
);
}
let tooltip;

View file

@ -45,7 +45,7 @@ const RedactedBody = React.forwardRef<any, IProps | IBodyProps>(({ mxEvent }, re
return (
<span className="mx_RedactedBody" ref={ref} title={titleText}>
{ text }
{text}
</span>
);
});

View file

@ -16,13 +16,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
import AccessibleButton from '../elements/AccessibleButton';
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { _t } from "../../../languageHandler";
import Modal from "../../../Modal";
import AccessibleButton from "../elements/AccessibleButton";
import { mediaFromMxc } from "../../../customisations/Media";
import RoomAvatar from "../avatars/RoomAvatar";
import ImageView from "../elements/ImageView";
@ -39,9 +39,9 @@ export default class RoomAvatarEvent extends React.Component<IProps> {
const httpUrl = mediaFromMxc(ev.getContent().url).srcHttp;
const room = cli.getRoom(this.props.mxEvent.getRoomId());
const text = _t('%(senderDisplayName)s changed the avatar for %(roomName)s', {
const text = _t("%(senderDisplayName)s changed the avatar for %(roomName)s", {
senderDisplayName: ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(),
roomName: room ? room.name : '',
roomName: room ? room.name : "",
});
const params = {
@ -58,7 +58,7 @@ export default class RoomAvatarEvent extends React.Component<IProps> {
if (!ev.getContent().url || ev.getContent().url.trim().length === 0) {
return (
<div className="mx_TextualEvent">
{ _t('%(senderDisplayName)s removed the room avatar.', { senderDisplayName }) }
{_t("%(senderDisplayName)s removed the room avatar.", { senderDisplayName })}
</div>
);
}
@ -72,19 +72,21 @@ export default class RoomAvatarEvent extends React.Component<IProps> {
return (
<div className="mx_RoomAvatarEvent">
{ _t('%(senderDisplayName)s changed the room avatar to <img/>',
{_t(
"%(senderDisplayName)s changed the room avatar to <img/>",
{ senderDisplayName: senderDisplayName },
{
'img': () =>
img: () => (
<AccessibleButton
key="avatar"
className="mx_RoomAvatarEvent_avatar"
onClick={this.onAvatarClick}
>
<RoomAvatar width={14} height={14} oobData={oobData} />
</AccessibleButton>,
})
}
</AccessibleButton>
),
},
)}
</div>
);
}

View file

@ -15,14 +15,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import dis from '../../../dispatcher/dispatcher';
import { Action } from '../../../dispatcher/actions';
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import dis from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import { _t } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import EventTileBubble from "./EventTileBubble";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
@ -36,38 +36,40 @@ export default class RoomCreate extends React.Component<IProps> {
private onLinkClicked = (e: React.MouseEvent): void => {
e.preventDefault();
const predecessor = this.props.mxEvent.getContent()['predecessor'];
const predecessor = this.props.mxEvent.getContent()["predecessor"];
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
event_id: predecessor['event_id'],
event_id: predecessor["event_id"],
highlighted: true,
room_id: predecessor['room_id'],
room_id: predecessor["room_id"],
metricsTrigger: "Predecessor",
metricsViaKeyboard: e.type !== "click",
});
};
public render(): JSX.Element {
const predecessor = this.props.mxEvent.getContent()['predecessor'];
const predecessor = this.props.mxEvent.getContent()["predecessor"];
if (predecessor === undefined) {
return <div />; // We should never have been instantiated in this case
}
const prevRoom = MatrixClientPeg.get().getRoom(predecessor['room_id']);
const permalinkCreator = new RoomPermalinkCreator(prevRoom, predecessor['room_id']);
const prevRoom = MatrixClientPeg.get().getRoom(predecessor["room_id"]);
const permalinkCreator = new RoomPermalinkCreator(prevRoom, predecessor["room_id"]);
permalinkCreator.load();
const predecessorPermalink = permalinkCreator.forEvent(predecessor['event_id']);
const predecessorPermalink = permalinkCreator.forEvent(predecessor["event_id"]);
const link = (
<a href={predecessorPermalink} onClick={this.onLinkClicked}>
{ _t("Click here to see older messages.") }
{_t("Click here to see older messages.")}
</a>
);
return <EventTileBubble
className="mx_CreateEvent"
title={_t("This room is a continuation of another conversation.")}
subtitle={link}
timestamp={this.props.timestamp}
/>;
return (
<EventTileBubble
className="mx_CreateEvent"
title={_t("This room is a continuation of another conversation.")}
subtitle={link}
timestamp={this.props.timestamp}
/>
);
}
}

View file

@ -14,12 +14,12 @@
limitations under the License.
*/
import React from 'react';
import React from "react";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { MsgType } from "matrix-js-sdk/src/@types/event";
import DisambiguatedProfile from "./DisambiguatedProfile";
import { useRoomMemberProfile } from '../../../hooks/room/useRoomMemberProfile';
import { useRoomMemberProfile } from "../../../hooks/room/useRoomMemberProfile";
interface IProps {
mxEvent: MatrixEvent;
@ -32,13 +32,13 @@ export default function SenderProfile({ mxEvent, onClick }: IProps) {
member: mxEvent.sender,
});
return mxEvent.getContent().msgtype !== MsgType.Emote
? <DisambiguatedProfile
return mxEvent.getContent().msgtype !== MsgType.Emote ? (
<DisambiguatedProfile
fallbackName={mxEvent.getSender() ?? ""}
onClick={onClick}
member={member}
colored={true}
emphasizeDisplayName={true}
/>
: null;
) : null;
}

View file

@ -14,22 +14,22 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { createRef, SyntheticEvent, MouseEvent, ReactNode } from 'react';
import ReactDOM from 'react-dom';
import highlight from 'highlight.js';
import React, { createRef, SyntheticEvent, MouseEvent, ReactNode } from "react";
import ReactDOM from "react-dom";
import highlight from "highlight.js";
import { MsgType } from "matrix-js-sdk/src/@types/event";
import { isEventLike, LegacyMsgType, M_MESSAGE, MessageEvent } from "matrix-events-sdk";
import * as HtmlUtils from '../../../HtmlUtils';
import { formatDate } from '../../../DateUtils';
import Modal from '../../../Modal';
import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler';
import * as ContextMenu from '../../structures/ContextMenu';
import { ChevronFace, toRightOf } from '../../structures/ContextMenu';
import * as HtmlUtils from "../../../HtmlUtils";
import { formatDate } from "../../../DateUtils";
import Modal from "../../../Modal";
import dis from "../../../dispatcher/dispatcher";
import { _t } from "../../../languageHandler";
import * as ContextMenu from "../../structures/ContextMenu";
import { ChevronFace, toRightOf } from "../../structures/ContextMenu";
import SettingsStore from "../../../settings/SettingsStore";
import { pillifyLinks, unmountPills } from '../../../utils/pillify';
import { tooltipifyLinks, unmountTooltips } from '../../../utils/tooltipify';
import { pillifyLinks, unmountPills } from "../../../utils/pillify";
import { tooltipifyLinks, unmountTooltips } from "../../../utils/tooltipify";
import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
import { isPermalinkHost, tryTransformPermalinkToLocalHref } from "../../../utils/permalinks/Permalinks";
import { copyPlaintext } from "../../../utils/strings";
@ -41,14 +41,14 @@ import GenericTextContextMenu from "../context_menus/GenericTextContextMenu";
import Spoiler from "../elements/Spoiler";
import QuestionDialog from "../dialogs/QuestionDialog";
import MessageEditHistoryDialog from "../dialogs/MessageEditHistoryDialog";
import EditMessageComposer from '../rooms/EditMessageComposer';
import LinkPreviewGroup from '../rooms/LinkPreviewGroup';
import EditMessageComposer from "../rooms/EditMessageComposer";
import LinkPreviewGroup from "../rooms/LinkPreviewGroup";
import { IBodyProps } from "./IBodyProps";
import RoomContext from "../../../contexts/RoomContext";
import AccessibleButton from '../elements/AccessibleButton';
import AccessibleButton from "../elements/AccessibleButton";
import { options as linkifyOpts } from "../../../linkify-matrix";
import { getParentEventId } from '../../../utils/Reply';
import { EditWysiwygComposer } from '../rooms/wysiwyg_composer';
import { getParentEventId } from "../../../utils/Reply";
import { EditWysiwygComposer } from "../rooms/wysiwyg_composer";
const MAX_HIGHLIGHT_LENGTH = 4096;
@ -150,7 +150,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
// Calculate how many percent does the pre element take up.
// If it's less than 30% we don't add the expansion button.
// We also round the number as it sometimes can be 29.99...
const percentageOfViewport = Math.round(pre.offsetHeight / UIStore.instance.windowHeight * 100);
const percentageOfViewport = Math.round((pre.offsetHeight / UIStore.instance.windowHeight) * 100);
// TODO: additionally show the button if it's an expanded quoted message
if (percentageOfViewport < 30) return;
@ -196,7 +196,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
const { close } = ContextMenu.createMenu(GenericTextContextMenu, {
...toRightOf(buttonRect, 0),
chevronFace: ChevronFace.None,
message: successful ? _t('Copied!') : _t('Failed to copy'),
message: successful ? _t("Copied!") : _t("Failed to copy"),
});
button.onmouseleave = close;
};
@ -225,32 +225,34 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
private addLineNumbers(pre: HTMLPreElement): void {
// Calculate number of lines in pre
const number = pre.innerHTML.replace(/\n(<\/code>)?$/, "").split(/\n/).length;
const lineNumbers = document.createElement('span');
lineNumbers.className = 'mx_EventTile_lineNumbers';
const lineNumbers = document.createElement("span");
lineNumbers.className = "mx_EventTile_lineNumbers";
// Iterate through lines starting with 1 (number of the first line is 1)
for (let i = 1; i <= number; i++) {
const s = document.createElement('span');
const s = document.createElement("span");
s.textContent = i.toString();
lineNumbers.appendChild(s);
}
pre.prepend(lineNumbers);
pre.append(document.createElement('span'));
pre.append(document.createElement("span"));
}
private highlightCode(code: HTMLElement): void {
if (code.textContent.length > MAX_HIGHLIGHT_LENGTH) {
console.log(
"Code block is bigger than highlight limit (" +
code.textContent.length + " > " + MAX_HIGHLIGHT_LENGTH +
"): not highlighting",
code.textContent.length +
" > " +
MAX_HIGHLIGHT_LENGTH +
"): not highlighting",
);
return;
}
let advertisedLang;
for (const cl of code.className.split(/\s+/)) {
if (cl.startsWith('language-')) {
const maybeLang = cl.split('-', 2)[1];
if (cl.startsWith("language-")) {
const maybeLang = cl.split("-", 2)[1];
if (highlight.getLanguage(maybeLang)) {
advertisedLang = maybeLang;
break;
@ -299,16 +301,17 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
//console.info("shouldComponentUpdate: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
// exploit that events are immutable :)
return (nextProps.mxEvent.getId() !== this.props.mxEvent.getId() ||
nextProps.highlights !== this.props.highlights ||
nextProps.replacingEventId !== this.props.replacingEventId ||
nextProps.highlightLink !== this.props.highlightLink ||
nextProps.showUrlPreview !== this.props.showUrlPreview ||
nextProps.editState !== this.props.editState ||
nextState.links !== this.state.links ||
nextState.widgetHidden !== this.state.widgetHidden ||
nextProps.isSeeingThroughMessageHiddenForModeration
!== this.props.isSeeingThroughMessageHiddenForModeration);
return (
nextProps.mxEvent.getId() !== this.props.mxEvent.getId() ||
nextProps.highlights !== this.props.highlights ||
nextProps.replacingEventId !== this.props.replacingEventId ||
nextProps.highlightLink !== this.props.highlightLink ||
nextProps.showUrlPreview !== this.props.showUrlPreview ||
nextProps.editState !== this.props.editState ||
nextState.links !== this.state.links ||
nextState.widgetHidden !== this.state.widgetHidden ||
nextProps.isSeeingThroughMessageHiddenForModeration !== this.props.isSeeingThroughMessageHiddenForModeration
);
}
private calculateUrlPreview(): void {
@ -337,7 +340,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
let node = nodes[0];
while (node) {
if (node.tagName === "SPAN" && typeof node.getAttribute("data-mx-spoiler") === "string") {
const spoilerContainer = document.createElement('span');
const spoilerContainer = document.createElement("span");
const reason = node.getAttribute("data-mx-spoiler");
node.removeAttribute("data-mx-spoiler"); // we don't want to recurse
@ -366,8 +369,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
if (this.isLinkPreviewable(node)) {
links.push(node.getAttribute("href"));
}
} else if (node.tagName === "PRE" || node.tagName === "CODE" ||
node.tagName === "BLOCKQUOTE") {
} else if (node.tagName === "PRE" || node.tagName === "CODE" || node.tagName === "BLOCKQUOTE") {
continue;
} else if (node.children && node.children.length) {
links = links.concat(this.findLinks(node.children));
@ -378,8 +380,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
private isLinkPreviewable(node: Element): boolean {
// don't try to preview relative links
if (!node.getAttribute("href").startsWith("http://") &&
!node.getAttribute("href").startsWith("https://")) {
if (!node.getAttribute("href").startsWith("http://") && !node.getAttribute("href").startsWith("https://")) {
return false;
}
@ -486,12 +487,16 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
const integrationsUrl = integrationManager.uiUrl;
Modal.createDialog(QuestionDialog, {
title: _t("Add an Integration"),
description:
description: (
<div>
{ _t("You are about to be taken to a third-party site so you can " +
"authenticate your account for use with %(integrationsUrl)s. " +
"Do you wish to continue?", { integrationsUrl: integrationsUrl }) }
</div>,
{_t(
"You are about to be taken to a third-party site so you can " +
"authenticate your account for use with %(integrationsUrl)s. " +
"Do you wish to continue?",
{ integrationsUrl: integrationsUrl },
)}
</div>
),
button: _t("Continue"),
onFinished(confirmed) {
if (!confirmed) {
@ -502,7 +507,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
const left = (window.screen.width - width) / 2;
const top = (window.screen.height - height) / 2;
const features = `height=${height}, width=${width}, top=${top}, left=${left},`;
const wnd = window.open(completeUrl, '_blank', features);
const wnd = window.open(completeUrl, "_blank", features);
wnd.opener = null;
},
});
@ -517,14 +522,12 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
const date = this.props.mxEvent.replacingEventDate();
const dateString = date && formatDate(date);
const tooltip = <div>
<div className="mx_Tooltip_title">
{ _t("Edited at %(date)s", { date: dateString }) }
const tooltip = (
<div>
<div className="mx_Tooltip_title">{_t("Edited at %(date)s", { date: dateString })}</div>
<div className="mx_Tooltip_sub">{_t("Click to view edits")}</div>
</div>
<div className="mx_Tooltip_sub">
{ _t("Click to view edits") }
</div>
</div>;
);
return (
<AccessibleTooltipButton
@ -533,7 +536,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
title={_t("Edited at %(date)s. Click to view edits.", { date: dateString })}
tooltip={tooltip}
>
<span>{ `(${_t("edited")})` }</span>
<span>{`(${_t("edited")})`}</span>
</AccessibleTooltipButton>
);
}
@ -556,17 +559,17 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
}
break;
}
return (
<span className="mx_EventTile_pendingModeration">{ `(${text})` }</span>
);
return <span className="mx_EventTile_pendingModeration">{`(${text})`}</span>;
}
render() {
if (this.props.editState) {
const isWysiwygComposerEnabled = SettingsStore.getValue("feature_wysiwyg_composer");
return isWysiwygComposerEnabled ?
<EditWysiwygComposer editorStateTransfer={this.props.editState} className="mx_EventTile_content" /> :
<EditMessageComposer editState={this.props.editState} className="mx_EventTile_content" />;
return isWysiwygComposerEnabled ? (
<EditWysiwygComposer editorStateTransfer={this.props.editState} className="mx_EventTile_content" />
) : (
<EditMessageComposer editState={this.props.editState} className="mx_EventTile_content" />
);
}
const mxEvent = this.props.mxEvent;
const content = mxEvent.getContent();
@ -581,27 +584,29 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
if (extev?.isEquivalentTo(M_MESSAGE)) {
isEmote = isEventLike(extev.wireFormat, LegacyMsgType.Emote);
isNotice = isEventLike(extev.wireFormat, LegacyMsgType.Notice);
body = HtmlUtils.bodyToHtml({
body: extev.text,
format: extev.html ? "org.matrix.custom.html" : undefined,
formatted_body: extev.html,
msgtype: MsgType.Text,
}, this.props.highlights, {
disableBigEmoji: isEmote
|| !SettingsStore.getValue<boolean>('TextualBody.enableBigEmoji'),
// Part of Replies fallback support
stripReplyFallback: stripReply,
ref: this.contentRef,
returnString: false,
});
body = HtmlUtils.bodyToHtml(
{
body: extev.text,
format: extev.html ? "org.matrix.custom.html" : undefined,
formatted_body: extev.html,
msgtype: MsgType.Text,
},
this.props.highlights,
{
disableBigEmoji: isEmote || !SettingsStore.getValue<boolean>("TextualBody.enableBigEmoji"),
// Part of Replies fallback support
stripReplyFallback: stripReply,
ref: this.contentRef,
returnString: false,
},
);
}
}
if (!body) {
isEmote = content.msgtype === MsgType.Emote;
isNotice = content.msgtype === MsgType.Notice;
body = HtmlUtils.bodyToHtml(content, this.props.highlights, {
disableBigEmoji: isEmote
|| !SettingsStore.getValue<boolean>('TextualBody.enableBigEmoji'),
disableBigEmoji: isEmote || !SettingsStore.getValue<boolean>("TextualBody.enableBigEmoji"),
// Part of Replies fallback support
stripReplyFallback: stripReply,
ref: this.contentRef,
@ -609,73 +614,72 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
});
}
if (this.props.replacingEventId) {
body = <>
{ body }
{ this.renderEditedMarker() }
</>;
body = (
<>
{body}
{this.renderEditedMarker()}
</>
);
}
if (this.props.isSeeingThroughMessageHiddenForModeration) {
body = <>
{ body }
{ this.renderPendingModerationMarker() }
</>;
body = (
<>
{body}
{this.renderPendingModerationMarker()}
</>
);
}
if (this.props.highlightLink) {
body = <a href={this.props.highlightLink}>{ body }</a>;
body = <a href={this.props.highlightLink}>{body}</a>;
} else if (content.data && typeof content.data["org.matrix.neb.starter_link"] === "string") {
body = (
<AccessibleButton
kind="link_inline"
onClick={this.onStarterLinkClick.bind(this, content.data["org.matrix.neb.starter_link"])}
>
{ body }
{body}
</AccessibleButton>
);
}
let widgets;
if (this.state.links.length && !this.state.widgetHidden && this.props.showUrlPreview) {
widgets = <LinkPreviewGroup
links={this.state.links}
mxEvent={this.props.mxEvent}
onCancelClick={this.onCancelClick}
onHeightChanged={this.props.onHeightChanged}
/>;
widgets = (
<LinkPreviewGroup
links={this.state.links}
mxEvent={this.props.mxEvent}
onCancelClick={this.onCancelClick}
onHeightChanged={this.props.onHeightChanged}
/>
);
}
if (isEmote) {
return (
<div className="mx_MEmoteBody mx_EventTile_content"
onClick={this.onBodyLinkClick}
>
<div className="mx_MEmoteBody mx_EventTile_content" onClick={this.onBodyLinkClick}>
*&nbsp;
<span
className="mx_MEmoteBody_sender"
onClick={this.onEmoteSenderClick}
>
{ mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender() }
<span className="mx_MEmoteBody_sender" onClick={this.onEmoteSenderClick}>
{mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender()}
</span>
&nbsp;
{ body }
{ widgets }
{body}
{widgets}
</div>
);
}
if (isNotice) {
return (
<div className="mx_MNoticeBody mx_EventTile_content"
onClick={this.onBodyLinkClick}
>
{ body }
{ widgets }
<div className="mx_MNoticeBody mx_EventTile_content" onClick={this.onBodyLinkClick}>
{body}
{widgets}
</div>
);
}
return (
<div className="mx_MTextBody mx_EventTile_content" onClick={this.onBodyLinkClick}>
{ body }
{ widgets }
{body}
{widgets}
</div>
);
}

View file

@ -30,6 +30,6 @@ export default class TextualEvent extends React.Component<IProps> {
public render() {
const text = TextForEvent.textForEvent(this.props.mxEvent, true, this.context?.showHiddenEvents);
if (!text) return null;
return <div className="mx_TextualEvent">{ text }</div>;
return <div className="mx_TextualEvent">{text}</div>;
}
}

View file

@ -14,18 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import classNames from 'classnames';
import React from "react";
import classNames from "classnames";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
import { _t } from "../../../languageHandler";
import Modal from "../../../Modal";
import SdkConfig from "../../../SdkConfig";
import BugReportDialog from '../dialogs/BugReportDialog';
import AccessibleButton from '../elements/AccessibleButton';
import BugReportDialog from "../dialogs/BugReportDialog";
import AccessibleButton from "../elements/AccessibleButton";
import SettingsStore from "../../../settings/SettingsStore";
import ViewSource from "../../structures/ViewSource";
import { Layout } from '../../../settings/enums/Layout';
import { Layout } from "../../../settings/enums/Layout";
interface IProps {
mxEvent: MatrixEvent;
@ -53,15 +53,19 @@ export default class TileErrorBoundary extends React.Component<IProps, IState> {
private onBugReport = (): void => {
Modal.createDialog(BugReportDialog, {
label: 'react-soft-crash-tile',
label: "react-soft-crash-tile",
error: this.state.error,
});
};
private onViewSource = (): void => {
Modal.createDialog(ViewSource, {
mxEvent: this.props.mxEvent,
}, 'mx_Dialog_viewsource');
Modal.createDialog(
ViewSource,
{
mxEvent: this.props.mxEvent,
},
"mx_Dialog_viewsource",
);
};
render() {
@ -76,34 +80,40 @@ export default class TileErrorBoundary extends React.Component<IProps, IState> {
let submitLogsButton;
if (SdkConfig.get().bug_report_endpoint_url) {
submitLogsButton = <>
&nbsp;
<AccessibleButton kind="link" onClick={this.onBugReport}>
{ _t("Submit logs") }
</AccessibleButton>
</>;
submitLogsButton = (
<>
&nbsp;
<AccessibleButton kind="link" onClick={this.onBugReport}>
{_t("Submit logs")}
</AccessibleButton>
</>
);
}
let viewSourceButton;
if (mxEvent && SettingsStore.getValue("developerMode")) {
viewSourceButton = <>
&nbsp;
<AccessibleButton onClick={this.onViewSource} kind="link">
{ _t("View Source") }
</AccessibleButton>
</>;
viewSourceButton = (
<>
&nbsp;
<AccessibleButton onClick={this.onViewSource} kind="link">
{_t("View Source")}
</AccessibleButton>
</>
);
}
return (<li className={classNames(classes)} data-layout={this.props.layout}>
<div className="mx_EventTile_line">
<span>
{ _t("Can't load this message") }
{ mxEvent && ` (${mxEvent.getType()})` }
{ submitLogsButton }
{ viewSourceButton }
</span>
</div>
</li>);
return (
<li className={classNames(classes)} data-layout={this.props.layout}>
<div className="mx_EventTile_line">
<span>
{_t("Can't load this message")}
{mxEvent && ` (${mxEvent.getType()})`}
{submitLogsButton}
{viewSourceButton}
</span>
</div>
</li>
);
}
return this.props.children;

View file

@ -27,8 +27,8 @@ export default forwardRef(({ mxEvent, children }: IProps, ref: React.RefObject<H
const text = mxEvent.getContent().body;
return (
<div className="mx_UnknownBody" ref={ref}>
{ text }
{ children }
{text}
{children}
</div>
);
});

View file

@ -14,13 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { MatrixEvent, MatrixEventEvent } from 'matrix-js-sdk/src/matrix';
import classNames from 'classnames';
import React from "react";
import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/matrix";
import classNames from "classnames";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { _t } from '../../../languageHandler';
import AccessibleButton from '../elements/AccessibleButton';
import { _t } from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton";
interface IProps {
mxEvent: MatrixEvent;
@ -64,23 +64,25 @@ export default class ViewSourceEvent extends React.PureComponent<IProps, IState>
let content;
if (expanded) {
content = <pre>{ JSON.stringify(mxEvent, null, 4) }</pre>;
content = <pre>{JSON.stringify(mxEvent, null, 4)}</pre>;
} else {
content = <code>{ `{ "type": ${mxEvent.getType()} }` }</code>;
content = <code>{`{ "type": ${mxEvent.getType()} }`}</code>;
}
const classes = classNames("mx_ViewSourceEvent mx_EventTile_content", {
mx_ViewSourceEvent_expanded: expanded,
});
return <span className={classes}>
{ content }
<AccessibleButton
kind='link'
title={_t('toggle event')}
className="mx_ViewSourceEvent_toggle"
onClick={this.onToggle}
/>
</span>;
return (
<span className={classes}>
{content}
<AccessibleButton
kind="link"
title={_t("toggle event")}
className="mx_ViewSourceEvent_toggle"
onClick={this.onToggle}
/>
</span>
);
}
}

View file

@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { Icon as WarningIcon } from '../../../../../res/img/warning.svg';
import { Icon as WarningIcon } from "../../../../../res/img/warning.svg";
interface Props {
className?: string;
@ -25,8 +25,8 @@ interface Props {
const MediaProcessingError: React.FC<Props> = ({ className, children }) => (
<span className={className}>
<WarningIcon className='mx_MediaProcessingError_Icon' width="16" height="16" />
{ children }
<WarningIcon className="mx_MediaProcessingError_Icon" width="16" height="16" />
{children}
</span>
);