Merge pull request #6808 from SimonBrandner/task/context_menus-ts
Convert `/context_menus` and `/avatars` to TS
This commit is contained in:
commit
333418232d
5 changed files with 92 additions and 88 deletions
|
@ -15,43 +15,48 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createRef } from 'react';
|
import React, { createRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import MemberAvatar from '../avatars/MemberAvatar';
|
import MemberAvatar from '../avatars/MemberAvatar';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import StatusMessageContextMenu from "../context_menus/StatusMessageContextMenu";
|
import StatusMessageContextMenu from "../context_menus/StatusMessageContextMenu";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { ContextMenu, ContextMenuButton } from "../../structures/ContextMenu";
|
import { ChevronFace, ContextMenu, ContextMenuButton } from "../../structures/ContextMenu";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
|
import { ResizeMethod } from "matrix-js-sdk/src/@types/partials";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
member: RoomMember;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
resizeMethod?: ResizeMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
hasStatus: boolean;
|
||||||
|
menuDisplayed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.avatars.MemberStatusMessageAvatar")
|
@replaceableComponent("views.avatars.MemberStatusMessageAvatar")
|
||||||
export default class MemberStatusMessageAvatar extends React.Component {
|
export default class MemberStatusMessageAvatar extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
public static defaultProps: Partial<IProps> = {
|
||||||
member: PropTypes.object.isRequired,
|
|
||||||
width: PropTypes.number,
|
|
||||||
height: PropTypes.number,
|
|
||||||
resizeMethod: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
resizeMethod: 'crop',
|
resizeMethod: 'crop',
|
||||||
};
|
};
|
||||||
|
private button = createRef<HTMLDivElement>();
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
hasStatus: this.hasStatus,
|
hasStatus: this.hasStatus,
|
||||||
menuDisplayed: false,
|
menuDisplayed: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
this._button = createRef();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
public componentDidMount(): void {
|
||||||
if (this.props.member.userId !== MatrixClientPeg.get().getUserId()) {
|
if (this.props.member.userId !== MatrixClientPeg.get().getUserId()) {
|
||||||
throw new Error("Cannot use MemberStatusMessageAvatar on anyone but the logged in user");
|
throw new Error("Cannot use MemberStatusMessageAvatar on anyone but the logged in user");
|
||||||
}
|
}
|
||||||
|
@ -62,44 +67,44 @@ export default class MemberStatusMessageAvatar extends React.Component {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
user.on("User._unstable_statusMessage", this._onStatusMessageCommitted);
|
user.on("User._unstable_statusMessage", this.onStatusMessageCommitted);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentWillUnmount(): void {
|
||||||
const { user } = this.props.member;
|
const { user } = this.props.member;
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
user.removeListener(
|
user.removeListener(
|
||||||
"User._unstable_statusMessage",
|
"User._unstable_statusMessage",
|
||||||
this._onStatusMessageCommitted,
|
this.onStatusMessageCommitted,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasStatus() {
|
private get hasStatus(): boolean {
|
||||||
const { user } = this.props.member;
|
const { user } = this.props.member;
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return !!user._unstable_statusMessage;
|
return !!user.unstable_statusMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onStatusMessageCommitted = () => {
|
private onStatusMessageCommitted = (): void => {
|
||||||
// The `User` object has observed a status message change.
|
// The `User` object has observed a status message change.
|
||||||
this.setState({
|
this.setState({
|
||||||
hasStatus: this.hasStatus,
|
hasStatus: this.hasStatus,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
openMenu = () => {
|
private openMenu = (): void => {
|
||||||
this.setState({ menuDisplayed: true });
|
this.setState({ menuDisplayed: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
closeMenu = () => {
|
private closeMenu = (): void => {
|
||||||
this.setState({ menuDisplayed: false });
|
this.setState({ menuDisplayed: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const avatar = <MemberAvatar
|
const avatar = <MemberAvatar
|
||||||
member={this.props.member}
|
member={this.props.member}
|
||||||
width={this.props.width}
|
width={this.props.width}
|
||||||
|
@ -118,7 +123,7 @@ export default class MemberStatusMessageAvatar extends React.Component {
|
||||||
|
|
||||||
let contextMenu;
|
let contextMenu;
|
||||||
if (this.state.menuDisplayed) {
|
if (this.state.menuDisplayed) {
|
||||||
const elementRect = this._button.current.getBoundingClientRect();
|
const elementRect = this.button.current.getBoundingClientRect();
|
||||||
|
|
||||||
const chevronWidth = 16; // See .mx_ContextualMenu_chevron_bottom
|
const chevronWidth = 16; // See .mx_ContextualMenu_chevron_bottom
|
||||||
const chevronMargin = 1; // Add some spacing away from target
|
const chevronMargin = 1; // Add some spacing away from target
|
||||||
|
@ -126,13 +131,13 @@ export default class MemberStatusMessageAvatar extends React.Component {
|
||||||
contextMenu = (
|
contextMenu = (
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
chevronOffset={(elementRect.width - chevronWidth) / 2}
|
chevronOffset={(elementRect.width - chevronWidth) / 2}
|
||||||
chevronFace="bottom"
|
chevronFace={ChevronFace.Bottom}
|
||||||
left={elementRect.left + window.pageXOffset}
|
left={elementRect.left + window.pageXOffset}
|
||||||
top={elementRect.top + window.pageYOffset - chevronMargin}
|
top={elementRect.top + window.pageYOffset - chevronMargin}
|
||||||
menuWidth={226}
|
menuWidth={226}
|
||||||
onFinished={this.closeMenu}
|
onFinished={this.closeMenu}
|
||||||
>
|
>
|
||||||
<StatusMessageContextMenu user={this.props.member.user} onFinished={this.closeMenu} />
|
<StatusMessageContextMenu user={this.props.member.user} />
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -140,7 +145,7 @@ export default class MemberStatusMessageAvatar extends React.Component {
|
||||||
return <React.Fragment>
|
return <React.Fragment>
|
||||||
<ContextMenuButton
|
<ContextMenuButton
|
||||||
className={classes}
|
className={classes}
|
||||||
inputRef={this._button}
|
inputRef={this.button}
|
||||||
onClick={this.openMenu}
|
onClick={this.openMenu}
|
||||||
isExpanded={this.state.menuDisplayed}
|
isExpanded={this.state.menuDisplayed}
|
||||||
label={_t("User Status")}
|
label={_t("User Status")}
|
|
@ -15,45 +15,41 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
/*
|
interface IProps {
|
||||||
* This component can be used to display generic HTML content in a contextual
|
element: React.ReactNode;
|
||||||
* menu.
|
|
||||||
*/
|
|
||||||
|
|
||||||
@replaceableComponent("views.context_menus.GenericElementContextMenu")
|
|
||||||
export default class GenericElementContextMenu extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
element: PropTypes.element.isRequired,
|
|
||||||
// Function to be called when the parent window is resized
|
// Function to be called when the parent window is resized
|
||||||
// This can be used to reposition or close the menu on resize and
|
// This can be used to reposition or close the menu on resize and
|
||||||
// ensure that it is not displayed in a stale position.
|
// ensure that it is not displayed in a stale position.
|
||||||
onResize: PropTypes.func,
|
onResize?: () => void;
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.resize = this.resize.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
/**
|
||||||
this.resize = this.resize.bind(this);
|
* This component can be used to display generic HTML content in a contextual
|
||||||
|
* menu.
|
||||||
|
*/
|
||||||
|
@replaceableComponent("views.context_menus.GenericElementContextMenu")
|
||||||
|
export default class GenericElementContextMenu extends React.Component<IProps> {
|
||||||
|
constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidMount(): void {
|
||||||
window.addEventListener("resize", this.resize);
|
window.addEventListener("resize", this.resize);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentWillUnmount(): void {
|
||||||
window.removeEventListener("resize", this.resize);
|
window.removeEventListener("resize", this.resize);
|
||||||
}
|
}
|
||||||
|
|
||||||
resize() {
|
private resize = (): void => {
|
||||||
if (this.props.onResize) {
|
if (this.props.onResize) {
|
||||||
this.props.onResize();
|
this.props.onResize();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
return <div>{ this.props.element }</div>;
|
return <div>{ this.props.element }</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -15,16 +15,15 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
@replaceableComponent("views.context_menus.GenericTextContextMenu")
|
interface IProps {
|
||||||
export default class GenericTextContextMenu extends React.Component {
|
message: string;
|
||||||
static propTypes = {
|
}
|
||||||
message: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
@replaceableComponent("views.context_menus.GenericTextContextMenu")
|
||||||
|
export default class GenericTextContextMenu extends React.Component<IProps> {
|
||||||
|
public render(): JSX.Element {
|
||||||
return <div>{ this.props.message }</div>;
|
return <div>{ this.props.message }</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -14,53 +14,59 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { ChangeEvent } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import * as sdk from '../../../index';
|
import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import { User } from "matrix-js-sdk/src/models/user";
|
||||||
|
import Spinner from "../elements/Spinner";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
// js-sdk User object. Not required because it might not exist.
|
||||||
|
user?: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
message: string;
|
||||||
|
waiting: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.context_menus.StatusMessageContextMenu")
|
@replaceableComponent("views.context_menus.StatusMessageContextMenu")
|
||||||
export default class StatusMessageContextMenu extends React.Component {
|
export default class StatusMessageContextMenu extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
constructor(props: IProps) {
|
||||||
// js-sdk User object. Not required because it might not exist.
|
|
||||||
user: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
message: this.comittedStatusMessage,
|
message: this.comittedStatusMessage,
|
||||||
|
waiting: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
public componentDidMount(): void {
|
||||||
const { user } = this.props;
|
const { user } = this.props;
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
user.on("User._unstable_statusMessage", this._onStatusMessageCommitted);
|
user.on("User._unstable_statusMessage", this.onStatusMessageCommitted);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentWillUnmount(): void {
|
||||||
const { user } = this.props;
|
const { user } = this.props;
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
user.removeListener(
|
user.removeListener(
|
||||||
"User._unstable_statusMessage",
|
"User._unstable_statusMessage",
|
||||||
this._onStatusMessageCommitted,
|
this.onStatusMessageCommitted,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get comittedStatusMessage() {
|
get comittedStatusMessage(): string {
|
||||||
return this.props.user ? this.props.user._unstable_statusMessage : "";
|
return this.props.user ? this.props.user.unstable_statusMessage : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
_onStatusMessageCommitted = () => {
|
private onStatusMessageCommitted = (): void => {
|
||||||
// The `User` object has observed a status message change.
|
// The `User` object has observed a status message change.
|
||||||
this.setState({
|
this.setState({
|
||||||
message: this.comittedStatusMessage,
|
message: this.comittedStatusMessage,
|
||||||
|
@ -68,14 +74,14 @@ export default class StatusMessageContextMenu extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onClearClick = (e) => {
|
private onClearClick = (): void=> {
|
||||||
MatrixClientPeg.get()._unstable_setStatusMessage("");
|
MatrixClientPeg.get()._unstable_setStatusMessage("");
|
||||||
this.setState({
|
this.setState({
|
||||||
waiting: true,
|
waiting: true,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onSubmit = (e) => {
|
private onSubmit = (e: ButtonEvent): void => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
MatrixClientPeg.get()._unstable_setStatusMessage(this.state.message);
|
MatrixClientPeg.get()._unstable_setStatusMessage(this.state.message);
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -83,27 +89,25 @@ export default class StatusMessageContextMenu extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onStatusChange = (e) => {
|
private onStatusChange = (e: ChangeEvent): void => {
|
||||||
// The input field's value was changed.
|
// The input field's value was changed.
|
||||||
this.setState({
|
this.setState({
|
||||||
message: e.target.value,
|
message: (e.target as HTMLInputElement).value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const Spinner = sdk.getComponent('views.elements.Spinner');
|
|
||||||
|
|
||||||
let actionButton;
|
let actionButton;
|
||||||
if (this.comittedStatusMessage) {
|
if (this.comittedStatusMessage) {
|
||||||
if (this.state.message === this.comittedStatusMessage) {
|
if (this.state.message === this.comittedStatusMessage) {
|
||||||
actionButton = <AccessibleButton className="mx_StatusMessageContextMenu_clear"
|
actionButton = <AccessibleButton className="mx_StatusMessageContextMenu_clear"
|
||||||
onClick={this._onClearClick}
|
onClick={this.onClearClick}
|
||||||
>
|
>
|
||||||
<span>{ _t("Clear status") }</span>
|
<span>{ _t("Clear status") }</span>
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
} else {
|
} else {
|
||||||
actionButton = <AccessibleButton className="mx_StatusMessageContextMenu_submit"
|
actionButton = <AccessibleButton className="mx_StatusMessageContextMenu_submit"
|
||||||
onClick={this._onSubmit}
|
onClick={this.onSubmit}
|
||||||
>
|
>
|
||||||
<span>{ _t("Update status") }</span>
|
<span>{ _t("Update status") }</span>
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
|
@ -112,7 +116,7 @@ export default class StatusMessageContextMenu extends React.Component {
|
||||||
actionButton = <AccessibleButton
|
actionButton = <AccessibleButton
|
||||||
className="mx_StatusMessageContextMenu_submit"
|
className="mx_StatusMessageContextMenu_submit"
|
||||||
disabled={!this.state.message}
|
disabled={!this.state.message}
|
||||||
onClick={this._onSubmit}
|
onClick={this.onSubmit}
|
||||||
>
|
>
|
||||||
<span>{ _t("Set status") }</span>
|
<span>{ _t("Set status") }</span>
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
|
@ -120,13 +124,13 @@ export default class StatusMessageContextMenu extends React.Component {
|
||||||
|
|
||||||
let spinner = null;
|
let spinner = null;
|
||||||
if (this.state.waiting) {
|
if (this.state.waiting) {
|
||||||
spinner = <Spinner w="24" h="24" />;
|
spinner = <Spinner w={24} h={24} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const form = <form
|
const form = <form
|
||||||
className="mx_StatusMessageContextMenu_form"
|
className="mx_StatusMessageContextMenu_form"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
onSubmit={this._onSubmit}
|
onSubmit={this.onSubmit}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -134,9 +138,9 @@ export default class StatusMessageContextMenu extends React.Component {
|
||||||
key="message"
|
key="message"
|
||||||
placeholder={_t("Set a new status...")}
|
placeholder={_t("Set a new status...")}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
maxLength="60"
|
maxLength={60}
|
||||||
value={this.state.message}
|
value={this.state.message}
|
||||||
onChange={this._onStatusChange}
|
onChange={this.onStatusChange}
|
||||||
/>
|
/>
|
||||||
<div className="mx_StatusMessageContextMenu_actionContainer">
|
<div className="mx_StatusMessageContextMenu_actionContainer">
|
||||||
{ actionButton }
|
{ actionButton }
|
|
@ -57,7 +57,7 @@ let instanceCount = 0;
|
||||||
const NARROW_MODE_BREAKPOINT = 500;
|
const NARROW_MODE_BREAKPOINT = 500;
|
||||||
|
|
||||||
interface IComposerAvatarProps {
|
interface IComposerAvatarProps {
|
||||||
me: object;
|
me: RoomMember;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ComposerAvatar(props: IComposerAvatarProps) {
|
function ComposerAvatar(props: IComposerAvatarProps) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue