Convert emojipicker to typescript

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2020-09-28 15:47:03 +01:00
parent 559414f97d
commit 6873402666
8 changed files with 211 additions and 160 deletions

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2019 Tulir Asokan <tulir@maunium.net> Copyright 2019 Tulir Asokan <tulir@maunium.net>
Copyright 2020 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.
@ -14,32 +15,53 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React, {RefObject} from 'react';
import PropTypes from 'prop-types';
import { CATEGORY_HEADER_HEIGHT, EMOJI_HEIGHT, EMOJIS_PER_ROW } from "./EmojiPicker"; import { CATEGORY_HEADER_HEIGHT, EMOJI_HEIGHT, EMOJIS_PER_ROW } from "./EmojiPicker";
import * as sdk from '../../../index'; import LazyRenderList from "../elements/LazyRenderList";
import {DATA_BY_CATEGORY, IEmoji} from "../../../emoji";
import Emoji from './Emoji';
const OVERFLOW_ROWS = 3; const OVERFLOW_ROWS = 3;
class Category extends React.PureComponent { export type CategoryKey = (keyof typeof DATA_BY_CATEGORY) | "recent";
static propTypes = {
emojis: PropTypes.arrayOf(PropTypes.object).isRequired,
name: PropTypes.string.isRequired,
id: PropTypes.string.isRequired,
onMouseEnter: PropTypes.func.isRequired,
onMouseLeave: PropTypes.func.isRequired,
onClick: PropTypes.func.isRequired,
selectedEmojis: PropTypes.instanceOf(Set),
};
_renderEmojiRow = (rowIndex) => { export interface ICategory {
id: CategoryKey;
name: string;
enabled: boolean;
visible: boolean;
ref: RefObject<HTMLButtonElement>;
}
interface IProps {
id: string;
name: string;
emojis: IEmoji[];
selectedEmojis: Set<string>;
heightBefore: number;
viewportHeight: number;
scrollTop: number;
onClick(emoji: IEmoji): void;
onMouseEnter(emoji: IEmoji): void;
onMouseLeave(emoji: IEmoji): void;
}
class Category extends React.PureComponent<IProps> {
private renderEmojiRow = (rowIndex: number) => {
const { onClick, onMouseEnter, onMouseLeave, selectedEmojis, emojis } = this.props; const { onClick, onMouseEnter, onMouseLeave, selectedEmojis, emojis } = this.props;
const emojisForRow = emojis.slice(rowIndex * 8, (rowIndex + 1) * 8); const emojisForRow = emojis.slice(rowIndex * 8, (rowIndex + 1) * 8);
const Emoji = sdk.getComponent("emojipicker.Emoji");
return (<div key={rowIndex}>{ return (<div key={rowIndex}>{
emojisForRow.map(emoji => emojisForRow.map(emoji => ((
<Emoji key={emoji.hexcode} emoji={emoji} selectedEmojis={selectedEmojis} <Emoji
onClick={onClick} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} />) key={emoji.hexcode}
emoji={emoji}
selectedEmojis={selectedEmojis}
onClick={onClick}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
/>
)))
}</div>); }</div>);
}; };
@ -52,7 +74,6 @@ class Category extends React.PureComponent {
for (let counter = 0; counter < rows.length; ++counter) { for (let counter = 0; counter < rows.length; ++counter) {
rows[counter] = counter; rows[counter] = counter;
} }
const LazyRenderList = sdk.getComponent('elements.LazyRenderList');
const viewportTop = scrollTop; const viewportTop = scrollTop;
const viewportBottom = viewportTop + viewportHeight; const viewportBottom = viewportTop + viewportHeight;
@ -84,7 +105,7 @@ class Category extends React.PureComponent {
height={localHeight} height={localHeight}
overflowItems={OVERFLOW_ROWS} overflowItems={OVERFLOW_ROWS}
overflowMargin={0} overflowMargin={0}
renderItem={this._renderEmojiRow}> renderItem={this.renderEmojiRow}>
</LazyRenderList> </LazyRenderList>
</section> </section>
); );

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2019 Tulir Asokan <tulir@maunium.net> Copyright 2019 Tulir Asokan <tulir@maunium.net>
Copyright 2020 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.
@ -15,18 +16,19 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import {MenuItem} from "../../structures/ContextMenu"; import {MenuItem} from "../../structures/ContextMenu";
import {IEmoji} from "../../../emoji";
class Emoji extends React.PureComponent { interface IProps {
static propTypes = { emoji: IEmoji;
onClick: PropTypes.func, selectedEmojis?: Set<string>;
onMouseEnter: PropTypes.func, onClick(emoji: IEmoji): void;
onMouseLeave: PropTypes.func, onMouseEnter(emoji: IEmoji): void;
emoji: PropTypes.object.isRequired, onMouseLeave(emoji: IEmoji): void;
selectedEmojis: PropTypes.instanceOf(Set), }
};
class Emoji extends React.PureComponent<IProps> {
render() { render() {
const { onClick, onMouseEnter, onMouseLeave, emoji, selectedEmojis } = this.props; const { onClick, onMouseEnter, onMouseLeave, emoji, selectedEmojis } = this.props;
const isSelected = selectedEmojis && selectedEmojis.has(emoji.unicode); const isSelected = selectedEmojis && selectedEmojis.has(emoji.unicode);

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2019 Tulir Asokan <tulir@maunium.net> Copyright 2019 Tulir Asokan <tulir@maunium.net>
Copyright 2020 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.
@ -15,25 +16,43 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import * as recent from '../../../emojipicker/recent'; import * as recent from '../../../emojipicker/recent';
import {DATA_BY_CATEGORY, getEmojiFromUnicode} from "../../../emoji"; import {DATA_BY_CATEGORY, getEmojiFromUnicode, IEmoji} from "../../../emoji";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import Header from "./Header";
import Search from "./Search";
import Preview from "./Preview";
import QuickReactions from "./QuickReactions";
import Category, {ICategory, CategoryKey} from "./Category";
export const CATEGORY_HEADER_HEIGHT = 22; export const CATEGORY_HEADER_HEIGHT = 22;
export const EMOJI_HEIGHT = 37; export const EMOJI_HEIGHT = 37;
export const EMOJIS_PER_ROW = 8; export const EMOJIS_PER_ROW = 8;
class EmojiPicker extends React.Component { interface IProps {
static propTypes = { selectedEmojis: Set<string>;
onChoose: PropTypes.func.isRequired, showQuickReactions?: boolean;
selectedEmojis: PropTypes.instanceOf(Set), onChoose(unicode: string): boolean;
showQuickReactions: PropTypes.bool, }
};
interface IState {
filter: string;
previewEmoji?: IEmoji;
scrollTop: number;
// initial estimation of height, dialog is hardcoded to 450px height.
// should be enough to never have blank rows of emojis as
// 3 rows of overflow are also rendered. The actual value is updated on scroll.
viewportHeight: number;
}
class EmojiPicker extends React.Component<IProps, IState> {
private readonly recentlyUsed: IEmoji[];
private readonly memoizedDataByCategory: Record<CategoryKey, IEmoji[]>;
private readonly categories: ICategory[];
private bodyRef = React.createRef<HTMLElement>();
constructor(props) { constructor(props) {
super(props); super(props);
@ -42,9 +61,6 @@ class EmojiPicker extends React.Component {
filter: "", filter: "",
previewEmoji: null, previewEmoji: null,
scrollTop: 0, scrollTop: 0,
// initial estimation of height, dialog is hardcoded to 450px height.
// should be enough to never have blank rows of emojis as
// 3 rows of overflow are also rendered. The actual value is updated on scroll.
viewportHeight: 280, viewportHeight: 280,
}; };
@ -110,18 +126,9 @@ class EmojiPicker extends React.Component {
visible: false, visible: false,
ref: React.createRef(), ref: React.createRef(),
}]; }];
this.bodyRef = React.createRef();
this.onChangeFilter = this.onChangeFilter.bind(this);
this.onHoverEmoji = this.onHoverEmoji.bind(this);
this.onHoverEmojiEnd = this.onHoverEmojiEnd.bind(this);
this.onClickEmoji = this.onClickEmoji.bind(this);
this.scrollToCategory = this.scrollToCategory.bind(this);
this.updateVisibility = this.updateVisibility.bind(this);
} }
onScroll = () => { private onScroll = () => {
const body = this.bodyRef.current; const body = this.bodyRef.current;
this.setState({ this.setState({
scrollTop: body.scrollTop, scrollTop: body.scrollTop,
@ -130,7 +137,7 @@ class EmojiPicker extends React.Component {
this.updateVisibility(); this.updateVisibility();
}; };
updateVisibility() { private updateVisibility = () => {
const body = this.bodyRef.current; const body = this.bodyRef.current;
const rect = body.getBoundingClientRect(); const rect = body.getBoundingClientRect();
for (const cat of this.categories) { for (const cat of this.categories) {
@ -147,21 +154,21 @@ class EmojiPicker extends React.Component {
// We update this here instead of through React to avoid re-render on scroll. // We update this here instead of through React to avoid re-render on scroll.
if (cat.visible) { if (cat.visible) {
cat.ref.current.classList.add("mx_EmojiPicker_anchor_visible"); cat.ref.current.classList.add("mx_EmojiPicker_anchor_visible");
cat.ref.current.setAttribute("aria-selected", true); cat.ref.current.setAttribute("aria-selected", "true");
cat.ref.current.setAttribute("tabindex", 0); cat.ref.current.setAttribute("tabindex", "0");
} else { } else {
cat.ref.current.classList.remove("mx_EmojiPicker_anchor_visible"); cat.ref.current.classList.remove("mx_EmojiPicker_anchor_visible");
cat.ref.current.setAttribute("aria-selected", false); cat.ref.current.setAttribute("aria-selected", "false");
cat.ref.current.setAttribute("tabindex", -1); cat.ref.current.setAttribute("tabindex", "-1");
} }
} }
} };
scrollToCategory(category) { private scrollToCategory = (category: string) => {
this.bodyRef.current.querySelector(`[data-category-id="${category}"]`).scrollIntoView(); this.bodyRef.current.querySelector(`[data-category-id="${category}"]`).scrollIntoView();
} };
onChangeFilter(filter) { private onChangeFilter = (filter: string) => {
filter = filter.toLowerCase(); // filter is case insensitive stored lower-case filter = filter.toLowerCase(); // filter is case insensitive stored lower-case
for (const cat of this.categories) { for (const cat of this.categories) {
let emojis; let emojis;
@ -181,27 +188,27 @@ class EmojiPicker extends React.Component {
// Header underlines need to be updated, but updating requires knowing // Header underlines need to be updated, but updating requires knowing
// where the categories are, so we wait for a tick. // where the categories are, so we wait for a tick.
setTimeout(this.updateVisibility, 0); setTimeout(this.updateVisibility, 0);
} };
onHoverEmoji(emoji) { private onHoverEmoji = (emoji: IEmoji) => {
this.setState({ this.setState({
previewEmoji: emoji, previewEmoji: emoji,
}); });
} };
onHoverEmojiEnd(emoji) { private onHoverEmojiEnd = (emoji: IEmoji) => {
this.setState({ this.setState({
previewEmoji: null, previewEmoji: null,
}); });
} };
onClickEmoji(emoji) { private onClickEmoji = (emoji: IEmoji) => {
if (this.props.onChoose(emoji.unicode) !== false) { if (this.props.onChoose(emoji.unicode) !== false) {
recent.add(emoji.unicode); recent.add(emoji.unicode);
} }
} };
_categoryHeightForEmojiCount(count) { private static categoryHeightForEmojiCount(count: number) {
if (count === 0) { if (count === 0) {
return 0; return 0;
} }
@ -209,25 +216,30 @@ class EmojiPicker extends React.Component {
} }
render() { render() {
const Header = sdk.getComponent("emojipicker.Header");
const Search = sdk.getComponent("emojipicker.Search");
const Category = sdk.getComponent("emojipicker.Category");
const Preview = sdk.getComponent("emojipicker.Preview");
const QuickReactions = sdk.getComponent("emojipicker.QuickReactions");
let heightBefore = 0; let heightBefore = 0;
return ( return (
<div className="mx_EmojiPicker"> <div className="mx_EmojiPicker">
<Header categories={this.categories} defaultCategory="recent" onAnchorClick={this.scrollToCategory} /> <Header categories={this.categories} onAnchorClick={this.scrollToCategory} />
<Search query={this.state.filter} onChange={this.onChangeFilter} /> <Search query={this.state.filter} onChange={this.onChangeFilter} />
<AutoHideScrollbar className="mx_EmojiPicker_body" wrappedRef={e => this.bodyRef.current = e} onScroll={this.onScroll}> <AutoHideScrollbar className="mx_EmojiPicker_body" wrappedRef={this.bodyRef} onScroll={this.onScroll}>
{this.categories.map(category => { {this.categories.map(category => {
const emojis = this.memoizedDataByCategory[category.id]; const emojis = this.memoizedDataByCategory[category.id];
const categoryElement = (<Category key={category.id} id={category.id} name={category.name} const categoryElement = ((
heightBefore={heightBefore} viewportHeight={this.state.viewportHeight} <Category
scrollTop={this.state.scrollTop} emojis={emojis} onClick={this.onClickEmoji} key={category.id}
onMouseEnter={this.onHoverEmoji} onMouseLeave={this.onHoverEmojiEnd} id={category.id}
selectedEmojis={this.props.selectedEmojis} />); name={category.name}
const height = this._categoryHeightForEmojiCount(emojis.length); heightBefore={heightBefore}
viewportHeight={this.state.viewportHeight}
scrollTop={this.state.scrollTop}
emojis={emojis}
onClick={this.onClickEmoji}
onMouseEnter={this.onHoverEmoji}
onMouseLeave={this.onHoverEmojiEnd}
selectedEmojis={this.props.selectedEmojis}
/>
));
const height = EmojiPicker.categoryHeightForEmojiCount(emojis.length);
heightBefore += height; heightBefore += height;
return categoryElement; return categoryElement;
})} })}

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2019 Tulir Asokan <tulir@maunium.net> Copyright 2019 Tulir Asokan <tulir@maunium.net>
Copyright 2020 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.
@ -15,19 +16,19 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import classNames from "classnames"; import classNames from "classnames";
import {_t} from "../../../languageHandler"; import {_t} from "../../../languageHandler";
import {Key} from "../../../Keyboard"; import {Key} from "../../../Keyboard";
import {CategoryKey, ICategory} from "./Category";
class Header extends React.PureComponent { interface IProps {
static propTypes = { categories: ICategory[];
categories: PropTypes.arrayOf(PropTypes.object).isRequired, onAnchorClick(id: CategoryKey): void
onAnchorClick: PropTypes.func.isRequired, }
};
findNearestEnabled(index, delta) { class Header extends React.PureComponent<IProps> {
private findNearestEnabled(index: number, delta: number) {
index += this.props.categories.length; index += this.props.categories.length;
const cats = [...this.props.categories, ...this.props.categories, ...this.props.categories]; const cats = [...this.props.categories, ...this.props.categories, ...this.props.categories];
@ -37,12 +38,12 @@ class Header extends React.PureComponent {
} }
} }
changeCategoryRelative(delta) { private changeCategoryRelative(delta: number) {
const current = this.props.categories.findIndex(c => c.visible); const current = this.props.categories.findIndex(c => c.visible);
this.changeCategoryAbsolute(current + delta, delta); this.changeCategoryAbsolute(current + delta, delta);
} }
changeCategoryAbsolute(index, delta=1) { private changeCategoryAbsolute(index: number, delta=1) {
const category = this.props.categories[this.findNearestEnabled(index, delta)]; const category = this.props.categories[this.findNearestEnabled(index, delta)];
if (category) { if (category) {
this.props.onAnchorClick(category.id); this.props.onAnchorClick(category.id);
@ -52,7 +53,7 @@ class Header extends React.PureComponent {
// Implements ARIA Tabs with Automatic Activation pattern // Implements ARIA Tabs with Automatic Activation pattern
// https://www.w3.org/TR/wai-aria-practices/examples/tabs/tabs-1/tabs.html // https://www.w3.org/TR/wai-aria-practices/examples/tabs/tabs-1/tabs.html
onKeyDown = (ev) => { private onKeyDown = (ev: React.KeyboardEvent) => {
let handled = true; let handled = true;
switch (ev.key) { switch (ev.key) {
case Key.ARROW_LEFT: case Key.ARROW_LEFT:
@ -80,7 +81,12 @@ class Header extends React.PureComponent {
render() { render() {
return ( return (
<nav className="mx_EmojiPicker_header" role="tablist" aria-label={_t("Categories")} onKeyDown={this.onKeyDown}> <nav
className="mx_EmojiPicker_header"
role="tablist"
aria-label={_t("Categories")}
onKeyDown={this.onKeyDown}
>
{this.props.categories.map(category => { {this.props.categories.map(category => {
const classes = classNames(`mx_EmojiPicker_anchor mx_EmojiPicker_anchor_${category.id}`, { const classes = classNames(`mx_EmojiPicker_anchor mx_EmojiPicker_anchor_${category.id}`, {
mx_EmojiPicker_anchor_visible: category.visible, mx_EmojiPicker_anchor_visible: category.visible,

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2019 Tulir Asokan <tulir@maunium.net> Copyright 2019 Tulir Asokan <tulir@maunium.net>
Copyright 2020 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.
@ -15,19 +16,21 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
class Preview extends React.PureComponent { import {IEmoji} from "../../../emoji";
static propTypes = {
emoji: PropTypes.object,
};
interface IProps {
emoji: IEmoji;
}
class Preview extends React.PureComponent<IProps> {
render() { render() {
const { const {
unicode = "", unicode = "",
annotation = "", annotation = "",
shortcodes: [shortcode = ""], shortcodes: [shortcode = ""],
} = this.props.emoji || {}; } = this.props.emoji || {};
return ( return (
<div className="mx_EmojiPicker_footer mx_EmojiPicker_preview"> <div className="mx_EmojiPicker_footer mx_EmojiPicker_preview">
<div className="mx_EmojiPicker_preview_emoji"> <div className="mx_EmojiPicker_preview_emoji">

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2019 Tulir Asokan <tulir@maunium.net> Copyright 2019 Tulir Asokan <tulir@maunium.net>
Copyright 2020 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.
@ -15,11 +16,10 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import {getEmojiFromUnicode} from "../../../emoji"; import {getEmojiFromUnicode, IEmoji} from "../../../emoji";
import Emoji from "./Emoji";
// We use the variation-selector Heart in Quick Reactions for some reason // We use the variation-selector Heart in Quick Reactions for some reason
const QUICK_REACTIONS = ["👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀"].map(emoji => { const QUICK_REACTIONS = ["👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀"].map(emoji => {
@ -30,36 +30,36 @@ const QUICK_REACTIONS = ["👍", "👎", "😄", "🎉", "😕", "❤️", "🚀
return data; return data;
}); });
class QuickReactions extends React.Component { interface IProps {
static propTypes = { selectedEmojis?: Set<string>;
onClick: PropTypes.func.isRequired, onClick(emoji: IEmoji): void;
selectedEmojis: PropTypes.instanceOf(Set), }
};
interface IState {
hover?: IEmoji;
}
class QuickReactions extends React.Component<IProps, IState> {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
hover: null, hover: null,
}; };
this.onMouseEnter = this.onMouseEnter.bind(this);
this.onMouseLeave = this.onMouseLeave.bind(this);
} }
onMouseEnter(emoji) { private onMouseEnter = (emoji: IEmoji) => {
this.setState({ this.setState({
hover: emoji, hover: emoji,
}); });
} };
onMouseLeave() { private onMouseLeave = () => {
this.setState({ this.setState({
hover: null, hover: null,
}); });
} };
render() { render() {
const Emoji = sdk.getComponent("emojipicker.Emoji");
return ( return (
<section className="mx_EmojiPicker_footer mx_EmojiPicker_quick mx_EmojiPicker_category"> <section className="mx_EmojiPicker_footer mx_EmojiPicker_quick mx_EmojiPicker_category">
<h2 className="mx_EmojiPicker_quick_header mx_EmojiPicker_category_label"> <h2 className="mx_EmojiPicker_quick_header mx_EmojiPicker_category_label">
@ -72,10 +72,16 @@ class QuickReactions extends React.Component {
} }
</h2> </h2>
<ul className="mx_EmojiPicker_list" aria-label={_t("Quick Reactions")}> <ul className="mx_EmojiPicker_list" aria-label={_t("Quick Reactions")}>
{QUICK_REACTIONS.map(emoji => <Emoji {QUICK_REACTIONS.map(emoji => ((
key={emoji.hexcode} emoji={emoji} onClick={this.props.onClick} <Emoji
onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave} key={emoji.hexcode}
selectedEmojis={this.props.selectedEmojis} />)} emoji={emoji}
onClick={this.props.onClick}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
selectedEmojis={this.props.selectedEmojis}
/>
)))}
</ul> </ul>
</section> </section>
); );

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2019 Tulir Asokan <tulir@maunium.net> Copyright 2019 Tulir Asokan <tulir@maunium.net>
Copyright 2020 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.
@ -15,26 +16,29 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from "prop-types"; import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import EmojiPicker from "./EmojiPicker"; import EmojiPicker from "./EmojiPicker";
import {MatrixClientPeg} from "../../../MatrixClientPeg"; import {MatrixClientPeg} from "../../../MatrixClientPeg";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
class ReactionPicker extends React.Component { interface IProps {
static propTypes = { mxEvent: MatrixEvent;
mxEvent: PropTypes.object.isRequired, reactions: any; // TODO type this once js-sdk is more typescripted
onFinished: PropTypes.func.isRequired, onFinished(): void;
reactions: PropTypes.object, }
};
interface IState {
selectedEmojis: Set<string>;
}
class ReactionPicker extends React.Component<IProps, IState> {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
selectedEmojis: new Set(Object.keys(this.getReactions())), selectedEmojis: new Set(Object.keys(this.getReactions())),
}; };
this.onChoose = this.onChoose.bind(this);
this.onReactionsChange = this.onReactionsChange.bind(this);
this.addListeners(); this.addListeners();
} }
@ -45,7 +49,7 @@ class ReactionPicker extends React.Component {
} }
} }
addListeners() { private addListeners() {
if (this.props.reactions) { if (this.props.reactions) {
this.props.reactions.on("Relations.add", this.onReactionsChange); this.props.reactions.on("Relations.add", this.onReactionsChange);
this.props.reactions.on("Relations.remove", this.onReactionsChange); this.props.reactions.on("Relations.remove", this.onReactionsChange);
@ -55,22 +59,13 @@ class ReactionPicker extends React.Component {
componentWillUnmount() { componentWillUnmount() {
if (this.props.reactions) { if (this.props.reactions) {
this.props.reactions.removeListener( this.props.reactions.removeListener("Relations.add", this.onReactionsChange);
"Relations.add", this.props.reactions.removeListener("Relations.remove", this.onReactionsChange);
this.onReactionsChange, this.props.reactions.removeListener("Relations.redaction", this.onReactionsChange);
);
this.props.reactions.removeListener(
"Relations.remove",
this.onReactionsChange,
);
this.props.reactions.removeListener(
"Relations.redaction",
this.onReactionsChange,
);
} }
} }
getReactions() { private getReactions() {
if (!this.props.reactions) { if (!this.props.reactions) {
return {}; return {};
} }
@ -81,13 +76,13 @@ class ReactionPicker extends React.Component {
.map(event => [event.getRelation().key, event.getId()])); .map(event => [event.getRelation().key, event.getId()]));
} }
onReactionsChange() { private onReactionsChange = () => {
this.setState({ this.setState({
selectedEmojis: new Set(Object.keys(this.getReactions())), selectedEmojis: new Set(Object.keys(this.getReactions())),
}); });
} };
onChoose(reaction) { onChoose = (reaction: string) => {
this.componentWillUnmount(); this.componentWillUnmount();
this.props.onFinished(); this.props.onFinished();
const myReactions = this.getReactions(); const myReactions = this.getReactions();
@ -109,7 +104,7 @@ class ReactionPicker extends React.Component {
dis.dispatch({action: "message_sent"}); dis.dispatch({action: "message_sent"});
return true; return true;
} }
} };
render() { render() {
return <EmojiPicker return <EmojiPicker

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2019 Tulir Asokan <tulir@maunium.net> Copyright 2019 Tulir Asokan <tulir@maunium.net>
Copyright 2020 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.
@ -15,19 +16,16 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
class Search extends React.PureComponent { interface IProps {
static propTypes = { query: string;
query: PropTypes.string.isRequired, onChange(value: string): void;
onChange: PropTypes.func.isRequired, }
};
constructor(props) { class Search extends React.PureComponent<IProps> {
super(props); private inputRef = React.createRef<HTMLInputElement>();
this.inputRef = React.createRef();
}
componentDidMount() { componentDidMount() {
// For some reason, neither the autoFocus nor just calling focus() here worked, so here's a setTimeout // For some reason, neither the autoFocus nor just calling focus() here worked, so here's a setTimeout
@ -38,9 +36,11 @@ class Search extends React.PureComponent {
let rightButton; let rightButton;
if (this.props.query) { if (this.props.query) {
rightButton = ( rightButton = (
<button onClick={() => this.props.onChange("")} <button
className="mx_EmojiPicker_search_icon mx_EmojiPicker_search_clear" onClick={() => this.props.onChange("")}
title={_t("Cancel search")} /> className="mx_EmojiPicker_search_icon mx_EmojiPicker_search_clear"
title={_t("Cancel search")}
/>
); );
} else { } else {
rightButton = <span className="mx_EmojiPicker_search_icon" />; rightButton = <span className="mx_EmojiPicker_search_icon" />;
@ -48,8 +48,14 @@ class Search extends React.PureComponent {
return ( return (
<div className="mx_EmojiPicker_search"> <div className="mx_EmojiPicker_search">
<input autoFocus type="text" placeholder="Search" value={this.props.query} <input
onChange={ev => this.props.onChange(ev.target.value)} ref={this.inputRef} /> autoFocus
type="text"
placeholder="Search"
value={this.props.query}
onChange={ev => this.props.onChange(ev.target.value)}
ref={this.inputRef}
/>
{rightButton} {rightButton}
</div> </div>
); );