Convert MessageContextMenu to Typescript

This commit is contained in:
Michael Telatynski 2021-07-08 14:47:36 +01:00
parent 72acd5a611
commit 8e7e4c9e8d
2 changed files with 96 additions and 85 deletions

View file

@ -1,6 +1,6 @@
/* /*
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2015, 2016, 2018, 2019, 2021 The Matrix.org Foundation C.I.C. Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -16,12 +16,11 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import { EventStatus, MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { EventStatus } from 'matrix-js-sdk/src/models/event'; import { EventType, RelationType } from "matrix-js-sdk/src/@types/event";
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
import dis from '../../../dispatcher/dispatcher'; import dis from '../../../dispatcher/dispatcher';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import Resend from '../../../Resend'; import Resend from '../../../Resend';
@ -29,53 +28,65 @@ import SettingsStore from '../../../settings/SettingsStore';
import { isUrlPermitted } from '../../../HtmlUtils'; import { isUrlPermitted } from '../../../HtmlUtils';
import { isContentActionable } from '../../../utils/EventUtils'; import { isContentActionable } from '../../../utils/EventUtils';
import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from './IconizedContextMenu'; import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from './IconizedContextMenu';
import { EventType } from "matrix-js-sdk/src/@types/event";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard"; import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard";
import ForwardDialog from "../dialogs/ForwardDialog"; import ForwardDialog from "../dialogs/ForwardDialog";
import { Action } from "../../../dispatcher/actions"; import { Action } from "../../../dispatcher/actions";
import ReportEventDialog from '../dialogs/ReportEventDialog';
import ViewSource from '../../structures/ViewSource';
import ConfirmRedactDialog from '../dialogs/ConfirmRedactDialog';
import ErrorDialog from '../dialogs/ErrorDialog';
import ShareDialog from '../dialogs/ShareDialog';
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
export function canCancel(eventStatus) { export function canCancel(eventStatus: EventStatus): boolean {
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT; return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
} }
interface IEventTileOps {
isWidgetHidden(): boolean;
unhideWidget(): void;
}
interface IProps {
/* the MatrixEvent associated with the context menu */
mxEvent: MatrixEvent;
/* an optional EventTileOps implementation that can be used to unhide preview widgets */
eventTileOps?: IEventTileOps;
permalinkCreator?: RoomPermalinkCreator;
/* an optional function to be called when the user clicks collapse thread, if not provided hide button */
collapseReplyThread?(): void;
/* callback called when the menu is dismissed */
onFinished(): void;
/* if the menu is inside a dialog, we sometimes need to close that dialog after click (forwarding) */
onCloseDialog?(): void;
}
interface IState {
canRedact: boolean;
canPin: boolean;
}
@replaceableComponent("views.context_menus.MessageContextMenu") @replaceableComponent("views.context_menus.MessageContextMenu")
export default class MessageContextMenu extends React.Component { export default class MessageContextMenu extends React.Component<IProps, IState> {
static propTypes = {
/* the MatrixEvent associated with the context menu */
mxEvent: PropTypes.object.isRequired,
/* an optional EventTileOps implementation that can be used to unhide preview widgets */
eventTileOps: PropTypes.object,
/* an optional function to be called when the user clicks collapse thread, if not provided hide button */
collapseReplyThread: PropTypes.func,
/* callback called when the menu is dismissed */
onFinished: PropTypes.func,
/* if the menu is inside a dialog, we sometimes need to close that dialog after click (forwarding) */
onCloseDialog: PropTypes.func,
};
state = { state = {
canRedact: false, canRedact: false,
canPin: false, canPin: false,
}; };
componentDidMount() { componentDidMount() {
MatrixClientPeg.get().on('RoomMember.powerLevel', this._checkPermissions); MatrixClientPeg.get().on('RoomMember.powerLevel', this.checkPermissions);
this._checkPermissions(); this.checkPermissions();
} }
componentWillUnmount() { componentWillUnmount() {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
if (cli) { if (cli) {
cli.removeListener('RoomMember.powerLevel', this._checkPermissions); cli.removeListener('RoomMember.powerLevel', this.checkPermissions);
} }
} }
_checkPermissions = () => { private checkPermissions = (): void => {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const room = cli.getRoom(this.props.mxEvent.getRoomId()); const room = cli.getRoom(this.props.mxEvent.getRoomId());
@ -93,7 +104,7 @@ export default class MessageContextMenu extends React.Component {
this.setState({ canRedact, canPin }); this.setState({ canRedact, canPin });
}; };
_isPinned() { private isPinned(): boolean {
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
const pinnedEvent = room.currentState.getStateEvents(EventType.RoomPinnedEvents, ''); const pinnedEvent = room.currentState.getStateEvents(EventType.RoomPinnedEvents, '');
if (!pinnedEvent) return false; if (!pinnedEvent) return false;
@ -101,38 +112,35 @@ export default class MessageContextMenu extends React.Component {
return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId()); return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId());
} }
onResendReactionsClick = () => { private onResendReactionsClick = (): void => {
for (const reaction of this._getUnsentReactions()) { for (const reaction of this.getUnsentReactions()) {
Resend.resend(reaction); Resend.resend(reaction);
} }
this.closeMenu(); this.closeMenu();
}; };
onReportEventClick = () => { private onReportEventClick = (): void => {
const ReportEventDialog = sdk.getComponent("dialogs.ReportEventDialog");
Modal.createTrackedDialog('Report Event', '', ReportEventDialog, { Modal.createTrackedDialog('Report Event', '', ReportEventDialog, {
mxEvent: this.props.mxEvent, mxEvent: this.props.mxEvent,
}, 'mx_Dialog_reportEvent'); }, 'mx_Dialog_reportEvent');
this.closeMenu(); this.closeMenu();
}; };
onViewSourceClick = () => { private onViewSourceClick = (): void => {
const ViewSource = sdk.getComponent('structures.ViewSource');
Modal.createTrackedDialog('View Event Source', '', ViewSource, { Modal.createTrackedDialog('View Event Source', '', ViewSource, {
mxEvent: this.props.mxEvent, mxEvent: this.props.mxEvent,
}, 'mx_Dialog_viewsource'); }, 'mx_Dialog_viewsource');
this.closeMenu(); this.closeMenu();
}; };
onRedactClick = () => { private onRedactClick = (): void => {
const ConfirmRedactDialog = sdk.getComponent("dialogs.ConfirmRedactDialog");
Modal.createTrackedDialog('Confirm Redact Dialog', '', ConfirmRedactDialog, { Modal.createTrackedDialog('Confirm Redact Dialog', '', ConfirmRedactDialog, {
onFinished: async (proceed, reason) => { onFinished: async (proceed: boolean, reason?: string) => {
if (!proceed) return; if (!proceed) return;
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
try { try {
if (this.props.onCloseDialog) this.props.onCloseDialog(); this.props.onCloseDialog?.();
await cli.redactEvent( await cli.redactEvent(
this.props.mxEvent.getRoomId(), this.props.mxEvent.getRoomId(),
this.props.mxEvent.getId(), this.props.mxEvent.getId(),
@ -145,7 +153,6 @@ export default class MessageContextMenu extends React.Component {
// (e.g. no errcode or statusCode) as in that case the redactions end up in the // (e.g. no errcode or statusCode) as in that case the redactions end up in the
// detached queue and we show the room status bar to allow retry // detached queue and we show the room status bar to allow retry
if (typeof code !== "undefined") { if (typeof code !== "undefined") {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
// display error message stating you couldn't delete this. // display error message stating you couldn't delete this.
Modal.createTrackedDialog('You cannot delete this message', '', ErrorDialog, { Modal.createTrackedDialog('You cannot delete this message', '', ErrorDialog, {
title: _t('Error'), title: _t('Error'),
@ -158,7 +165,7 @@ export default class MessageContextMenu extends React.Component {
this.closeMenu(); this.closeMenu();
}; };
onForwardClick = () => { private onForwardClick = (): void => {
Modal.createTrackedDialog('Forward Message', '', ForwardDialog, { Modal.createTrackedDialog('Forward Message', '', ForwardDialog, {
matrixClient: MatrixClientPeg.get(), matrixClient: MatrixClientPeg.get(),
event: this.props.mxEvent, event: this.props.mxEvent,
@ -167,7 +174,7 @@ export default class MessageContextMenu extends React.Component {
this.closeMenu(); this.closeMenu();
}; };
onPinClick = () => { private onPinClick = (): void => {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const room = cli.getRoom(this.props.mxEvent.getRoomId()); const room = cli.getRoom(this.props.mxEvent.getRoomId());
const eventId = this.props.mxEvent.getId(); const eventId = this.props.mxEvent.getId();
@ -188,18 +195,16 @@ export default class MessageContextMenu extends React.Component {
this.closeMenu(); this.closeMenu();
}; };
closeMenu = () => { private closeMenu = (): void => {
if (this.props.onFinished) this.props.onFinished(); this.props.onFinished();
}; };
onUnhidePreviewClick = () => { private onUnhidePreviewClick = (): void => {
if (this.props.eventTileOps) { this.props.eventTileOps?.unhideWidget();
this.props.eventTileOps.unhideWidget();
}
this.closeMenu(); this.closeMenu();
}; };
onQuoteClick = () => { private onQuoteClick = (): void => {
dis.dispatch({ dis.dispatch({
action: Action.ComposerInsert, action: Action.ComposerInsert,
event: this.props.mxEvent, event: this.props.mxEvent,
@ -207,9 +212,8 @@ export default class MessageContextMenu extends React.Component {
this.closeMenu(); this.closeMenu();
}; };
onPermalinkClick = (e) => { private onPermalinkClick = (e: React.MouseEvent): void => {
e.preventDefault(); e.preventDefault();
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
Modal.createTrackedDialog('share room message dialog', '', ShareDialog, { Modal.createTrackedDialog('share room message dialog', '', ShareDialog, {
target: this.props.mxEvent, target: this.props.mxEvent,
permalinkCreator: this.props.permalinkCreator, permalinkCreator: this.props.permalinkCreator,
@ -217,30 +221,27 @@ export default class MessageContextMenu extends React.Component {
this.closeMenu(); this.closeMenu();
}; };
onCollapseReplyThreadClick = () => { private onCollapseReplyThreadClick = (): void => {
this.props.collapseReplyThread(); this.props.collapseReplyThread();
this.closeMenu(); this.closeMenu();
}; };
_getReactions(filter) { private getReactions(filter: (e: MatrixEvent) => boolean): MatrixEvent[] {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const room = cli.getRoom(this.props.mxEvent.getRoomId()); const room = cli.getRoom(this.props.mxEvent.getRoomId());
const eventId = this.props.mxEvent.getId(); const eventId = this.props.mxEvent.getId();
return room.getPendingEvents().filter(e => { return room.getPendingEvents().filter(e => {
const relation = e.getRelation(); const relation = e.getRelation();
return relation && return relation?.rel_type === RelationType.Annotation && relation.event_id === eventId && filter(e);
relation.rel_type === "m.annotation" &&
relation.event_id === eventId &&
filter(e);
}); });
} }
_getPendingReactions() { private getPendingReactions(): MatrixEvent[] {
return this._getReactions(e => canCancel(e.status)); return this.getReactions(e => canCancel(e.status));
} }
_getUnsentReactions() { private getUnsentReactions(): MatrixEvent[] {
return this._getReactions(e => e.status === EventStatus.NOT_SENT); return this.getReactions(e => e.status === EventStatus.NOT_SENT);
} }
render() { render() {
@ -248,16 +249,17 @@ export default class MessageContextMenu extends React.Component {
const me = cli.getUserId(); const me = cli.getUserId();
const mxEvent = this.props.mxEvent; const mxEvent = this.props.mxEvent;
const eventStatus = mxEvent.status; const eventStatus = mxEvent.status;
const unsentReactionsCount = this._getUnsentReactions().length; const unsentReactionsCount = this.getUnsentReactions().length;
let resendReactionsButton;
let redactButton; let resendReactionsButton: JSX.Element;
let forwardButton; let redactButton: JSX.Element;
let pinButton; let forwardButton: JSX.Element;
let unhidePreviewButton; let pinButton: JSX.Element;
let externalURLButton; let unhidePreviewButton: JSX.Element;
let quoteButton; let externalURLButton: JSX.Element;
let collapseReplyThread; let quoteButton: JSX.Element;
let redactItemList; let collapseReplyThread: JSX.Element;
let redactItemList: JSX.Element;
// status is SENT before remote-echo, null after // status is SENT before remote-echo, null after
const isSent = !eventStatus || eventStatus === EventStatus.SENT; const isSent = !eventStatus || eventStatus === EventStatus.SENT;
@ -296,7 +298,7 @@ export default class MessageContextMenu extends React.Component {
pinButton = ( pinButton = (
<IconizedContextMenuOption <IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconPin" iconClassName="mx_MessageContextMenu_iconPin"
label={ this._isPinned() ? _t('Unpin') : _t('Pin') } label={ this.isPinned() ? _t('Unpin') : _t('Pin') }
onClick={this.onPinClick} onClick={this.onPinClick}
/> />
); );
@ -327,16 +329,20 @@ export default class MessageContextMenu extends React.Component {
if (this.props.permalinkCreator) { if (this.props.permalinkCreator) {
permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId()); permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId());
} }
// XXX: if we use room ID, we should also include a server where the event can be found (other than in the domain of the event ID)
const permalinkButton = ( const permalinkButton = (
<IconizedContextMenuOption <IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconPermalink" iconClassName="mx_MessageContextMenu_iconPermalink"
onClick={this.onPermalinkClick} onClick={this.onPermalinkClick}
label= {_t('Share')} label= {_t('Share')}
element="a" element="a"
href={permalink} {
target="_blank" // XXX: Typescript signature for AccessibleButton doesn't work properly for non-inputs like `a`
rel="noreferrer noopener" ...{
href: permalink,
target: "_blank",
rel: "noreferrer noopener",
}
}
/> />
); );
@ -351,8 +357,8 @@ export default class MessageContextMenu extends React.Component {
} }
// Bridges can provide a 'external_url' to link back to the source. // Bridges can provide a 'external_url' to link back to the source.
if (typeof (mxEvent.event.content.external_url) === "string" && if (typeof (mxEvent.getContent().external_url) === "string" &&
isUrlPermitted(mxEvent.event.content.external_url) isUrlPermitted(mxEvent.getContent().external_url)
) { ) {
externalURLButton = ( externalURLButton = (
<IconizedContextMenuOption <IconizedContextMenuOption
@ -360,9 +366,14 @@ export default class MessageContextMenu extends React.Component {
onClick={this.closeMenu} onClick={this.closeMenu}
label={ _t('Source URL') } label={ _t('Source URL') }
element="a" element="a"
target="_blank" {
rel="noreferrer noopener" // XXX: Typescript signature for AccessibleButton doesn't work properly for non-inputs like `a`
href={mxEvent.event.content.external_url} ...{
target: "_blank",
rel: "noreferrer noopener",
href: mxEvent.getContent().external_url,
}
}
/> />
); );
} }
@ -377,7 +388,7 @@ export default class MessageContextMenu extends React.Component {
); );
} }
let reportEventButton; let reportEventButton: JSX.Element;
if (mxEvent.getSender() !== me) { if (mxEvent.getSender() !== me) {
reportEventButton = ( reportEventButton = (
<IconizedContextMenuOption <IconizedContextMenuOption

View file

@ -14,7 +14,7 @@
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React, { ReactHTML } from 'react';
import { Key } from '../../../Keyboard'; import { Key } from '../../../Keyboard';
import classnames from 'classnames'; import classnames from 'classnames';
@ -29,7 +29,7 @@ export type ButtonEvent = React.MouseEvent<Element> | React.KeyboardEvent<Elemen
*/ */
interface IProps extends React.InputHTMLAttributes<Element> { interface IProps extends React.InputHTMLAttributes<Element> {
inputRef?: React.Ref<Element>; inputRef?: React.Ref<Element>;
element?: string; element?: keyof ReactHTML;
// The kind of button, similar to how Bootstrap works. // The kind of button, similar to how Bootstrap works.
// See available classes for AccessibleButton for options. // See available classes for AccessibleButton for options.
kind?: string; kind?: string;
@ -122,7 +122,7 @@ export default function AccessibleButton({
} }
AccessibleButton.defaultProps = { AccessibleButton.defaultProps = {
element: 'div', element: 'div' as keyof ReactHTML,
role: 'button', role: 'button',
tabIndex: 0, tabIndex: 0,
}; };