Apply prettier formatting
This commit is contained in:
parent
1cac306093
commit
526645c791
1576 changed files with 65385 additions and 62478 deletions
|
@ -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} />;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}>*
|
||||
<span className="mx_MEmoteBody_sender">{ name }</span>
|
||||
{ contentElements }
|
||||
<div className="mx_EventTile_content" ref={this.content}>
|
||||
*
|
||||
<span className="mx_MEmoteBody_sender">{name}</span>
|
||||
{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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -47,7 +47,7 @@ const HiddenBody = React.forwardRef<any, IProps | IBodyProps>(({ mxEvent }, ref)
|
|||
|
||||
return (
|
||||
<span className="mx_HiddenBody" ref={ref}>
|
||||
{ text }
|
||||
{text}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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 ..."
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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}>
|
||||
*
|
||||
<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>
|
||||
|
||||
{ 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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = <>
|
||||
|
||||
<AccessibleButton kind="link" onClick={this.onBugReport}>
|
||||
{ _t("Submit logs") }
|
||||
</AccessibleButton>
|
||||
</>;
|
||||
submitLogsButton = (
|
||||
<>
|
||||
|
||||
<AccessibleButton kind="link" onClick={this.onBugReport}>
|
||||
{_t("Submit logs")}
|
||||
</AccessibleButton>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
let viewSourceButton;
|
||||
if (mxEvent && SettingsStore.getValue("developerMode")) {
|
||||
viewSourceButton = <>
|
||||
|
||||
<AccessibleButton onClick={this.onViewSource} kind="link">
|
||||
{ _t("View Source") }
|
||||
</AccessibleButton>
|
||||
</>;
|
||||
viewSourceButton = (
|
||||
<>
|
||||
|
||||
<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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue