Reorganize reaction sending and show if emoji is selected

Signed-off-by: Tulir Asokan <tulir@maunium.net>
This commit is contained in:
Tulir Asokan 2019-10-15 19:07:04 +03:00
parent 7acae6dc32
commit 318754d31c
9 changed files with 174 additions and 75 deletions

View file

@ -133,6 +133,12 @@ limitations under the License.
} }
} }
.mx_EmojiPicker_item_selected {
color: rgba(0, 0, 0, .75);
border: 1px solid $input-valid-border-color;
margin: 0;
}
.mx_EmojiPicker_category_label, .mx_EmojiPicker_preview_name { .mx_EmojiPicker_category_label, .mx_EmojiPicker_preview_name {
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 600;

View file

@ -27,10 +27,11 @@ class Category extends React.PureComponent {
onMouseEnter: PropTypes.func.isRequired, onMouseEnter: PropTypes.func.isRequired,
onMouseLeave: PropTypes.func.isRequired, onMouseLeave: PropTypes.func.isRequired,
onClick: PropTypes.func.isRequired, onClick: PropTypes.func.isRequired,
selectedEmojis: PropTypes.instanceOf(Set),
}; };
render() { render() {
const { onClick, onMouseEnter, onMouseLeave, emojis, name } = this.props; const { onClick, onMouseEnter, onMouseLeave, emojis, name, selectedEmojis } = this.props;
if (!emojis || emojis.length === 0) { if (!emojis || emojis.length === 0) {
return null; return null;
} }
@ -42,11 +43,11 @@ class Category extends React.PureComponent {
{name} {name}
</h2> </h2>
<ul className="mx_EmojiPicker_list"> <ul className="mx_EmojiPicker_list">
{emojis.map(emoji => <Emoji key={emoji.hexcode} emoji={emoji} {emojis.map(emoji => <Emoji key={emoji.hexcode} emoji={emoji} selectedEmojis={selectedEmojis}
onClick={onClick} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} />)} onClick={onClick} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} />)}
</ul> </ul>
</section> </section>
) );
} }
} }

View file

@ -23,18 +23,20 @@ class Emoji extends React.PureComponent {
onMouseEnter: PropTypes.func, onMouseEnter: PropTypes.func,
onMouseLeave: PropTypes.func, onMouseLeave: PropTypes.func,
emoji: PropTypes.object.isRequired, emoji: PropTypes.object.isRequired,
selectedEmojis: PropTypes.instanceOf(Set),
}; };
render() { render() {
const { onClick, onMouseEnter, onMouseLeave, emoji } = this.props; const { onClick, onMouseEnter, onMouseLeave, emoji, selectedEmojis } = this.props;
const isSelected = selectedEmojis && selectedEmojis.has(emoji.unicode);
return ( return (
<li onClick={() => onClick(emoji)} <li onClick={() => onClick(emoji)}
onMouseEnter={() => onMouseEnter(emoji)} onMouseEnter={() => onMouseEnter(emoji)}
onMouseLeave={() => onMouseLeave(emoji)} onMouseLeave={() => onMouseLeave(emoji)}
className="mx_EmojiPicker_item"> className={`mx_EmojiPicker_item ${isSelected ? 'mx_EmojiPicker_item_selected' : ''}`}>
{emoji.unicode} {emoji.unicode}
</li> </li>
) );
} }
} }

View file

@ -61,7 +61,8 @@ EMOJIBASE.forEach(emoji => {
class EmojiPicker extends React.Component { class EmojiPicker extends React.Component {
static propTypes = { static propTypes = {
onChoose: PropTypes.func.isRequired, onChoose: PropTypes.func.isRequired,
closeMenu: PropTypes.func, selectedEmojis: PropTypes.instanceOf(Set),
showQuickReactions: PropTypes.bool,
}; };
constructor(props) { constructor(props) {
@ -204,10 +205,8 @@ class EmojiPicker extends React.Component {
} }
onClickEmoji(emoji) { onClickEmoji(emoji) {
recent.add(emoji.unicode); if (this.props.onChoose(emoji.unicode) !== false) {
this.props.onChoose(emoji.unicode); recent.add(emoji.unicode);
if (this.props.closeMenu) {
this.props.closeMenu();
} }
} }
@ -225,14 +224,15 @@ class EmojiPicker extends React.Component {
{this.categories.map(category => ( {this.categories.map(category => (
<Category key={category.id} id={category.id} name={category.name} <Category key={category.id} id={category.id} name={category.name}
emojis={this.memoizedDataByCategory[category.id]} onClick={this.onClickEmoji} emojis={this.memoizedDataByCategory[category.id]} onClick={this.onClickEmoji}
onMouseEnter={this.onHoverEmoji} onMouseLeave={this.onHoverEmojiEnd} /> onMouseEnter={this.onHoverEmoji} onMouseLeave={this.onHoverEmojiEnd}
selectedEmojis={this.props.selectedEmojis} />
))} ))}
</div> </div>
{this.state.previewEmoji {this.state.previewEmoji || !this.props.showQuickReactions
? <Preview emoji={this.state.previewEmoji} /> ? <Preview emoji={this.state.previewEmoji} />
: <QuickReactions onClick={this.onClickEmoji} /> } : <QuickReactions onClick={this.onClickEmoji} selectedEmojis={this.props.selectedEmojis} /> }
</div> </div>
) );
} }
} }

View file

@ -19,21 +19,26 @@ import PropTypes from 'prop-types';
class Preview extends React.PureComponent { class Preview extends React.PureComponent {
static propTypes = { static propTypes = {
emoji: PropTypes.object.isRequired, emoji: PropTypes.object,
}; };
render() { render() {
const {
unicode = "",
annotation = "",
shortcodes: [shortcode = ""]
} = 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">
{this.props.emoji.unicode} {unicode}
</div> </div>
<div className="mx_EmojiPicker_preview_text"> <div className="mx_EmojiPicker_preview_text">
<div className="mx_EmojiPicker_name mx_EmojiPicker_preview_name"> <div className="mx_EmojiPicker_name mx_EmojiPicker_preview_name">
{this.props.emoji.annotation} {annotation}
</div> </div>
<div className="mx_EmojiPicker_shortcode"> <div className="mx_EmojiPicker_shortcode">
{this.props.emoji.shortcodes[0]} {shortcode}
</div> </div>
</div> </div>
</div> </div>

View file

@ -32,6 +32,7 @@ EMOJIBASE.forEach(emoji => {
class QuickReactions extends React.Component { class QuickReactions extends React.Component {
static propTypes = { static propTypes = {
onClick: PropTypes.func.isRequired, onClick: PropTypes.func.isRequired,
selectedEmojis: PropTypes.instanceOf(Set),
}; };
constructor(props) { constructor(props) {
@ -72,7 +73,8 @@ class QuickReactions extends React.Component {
<ul className="mx_EmojiPicker_list"> <ul className="mx_EmojiPicker_list">
{QUICK_REACTIONS.map(emoji => <Emoji {QUICK_REACTIONS.map(emoji => <Emoji
key={emoji.hexcode} emoji={emoji} onClick={this.props.onClick} key={emoji.hexcode} emoji={emoji} onClick={this.props.onClick}
onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}/>)} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}
selectedEmojis={this.props.selectedEmojis}/>)}
</ul> </ul>
</section> </section>
) )

View file

@ -0,0 +1,120 @@
/*
Copyright 2019 Tulir Asokan
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from "prop-types";
import EmojiPicker from "./EmojiPicker";
import MatrixClientPeg from "../../../MatrixClientPeg";
class ReactionPicker extends React.Component {
static propTypes = {
mxEvent: PropTypes.object.isRequired,
onFinished: PropTypes.func.isRequired,
closeMenu: PropTypes.func.isRequired,
reactions: PropTypes.object.isRequired,
};
constructor(props) {
super(props);
this.state = {
selectedEmojis: new Set(Object.keys(this.getReactions())),
};
this.onChoose = this.onChoose.bind(this);
this.onReactionsChange = this.onReactionsChange.bind(this);
this.addListeners();
}
componentDidUpdate(prevProps) {
if (prevProps.reactions !== this.props.reactions) {
this.addListeners();
this.onReactionsChange();
}
}
addListeners() {
if (this.props.reactions) {
this.props.reactions.on("Relations.add", this.onReactionsChange);
this.props.reactions.on("Relations.remove", this.onReactionsChange);
this.props.reactions.on("Relations.redaction", this.onReactionsChange);
}
}
componentWillUnmount() {
if (this.props.reactions) {
this.props.reactions.removeListener(
"Relations.add",
this.onReactionsChange,
);
this.props.reactions.removeListener(
"Relations.remove",
this.onReactionsChange,
);
this.props.reactions.removeListener(
"Relations.redaction",
this.onReactionsChange,
);
}
}
getReactions() {
if (!this.props.reactions) {
return {};
}
const userId = MatrixClientPeg.get().getUserId();
const myAnnotations = this.props.reactions.getAnnotationsBySender()[userId];
return Object.fromEntries([...myAnnotations]
.filter(event => !event.isRedacted())
.map(event => [event.getRelation().key, event.getId()]));
};
onReactionsChange() {
this.setState({
selectedEmojis: new Set(Object.keys(this.getReactions()))
});
}
onChoose(reaction) {
this.componentWillUnmount();
this.props.closeMenu();
this.props.onFinished();
const myReactions = this.getReactions();
if (myReactions.hasOwnProperty(reaction)) {
MatrixClientPeg.get().redactEvent(
this.props.mxEvent.getRoomId(),
myReactions[reaction],
);
// Tell the emoji picker not to bump this in the more frequently used list.
return false;
} else {
MatrixClientPeg.get().sendEvent(this.props.mxEvent.getRoomId(), "m.reaction", {
"m.relates_to": {
"rel_type": "m.annotation",
"event_id": this.props.mxEvent.getId(),
"key": reaction,
},
});
return true;
}
}
render() {
return <EmojiPicker onChoose={this.onChoose} selectedEmojis={this.state.selectedEmojis}
showQuickReactions={true}/>
}
}
export default ReactionPicker

View file

@ -45,7 +45,7 @@ class Search extends React.PureComponent {
{this.props.query ? icons.search.delete() : icons.search.search()} {this.props.query ? icons.search.delete() : icons.search.search()}
</button> </button>
</div> </div>
) );
} }
} }

View file

@ -85,45 +85,9 @@ export default class MessageActionBar extends React.PureComponent {
}); });
}; };
onReactClick = (ev) => { getMenuOptions = (ev) => {
const EmojiPicker = sdk.getComponent('emojipicker.EmojiPicker'); const menuOptions = {};
const buttonRect = ev.target.getBoundingClientRect(); const buttonRect = ev.target.getBoundingClientRect();
const getReactions = () => {
if (!this.props.reactions) {
return [];
}
const userId = MatrixClientPeg.get().getUserId();
const myAnnotations = this.props.reactions.getAnnotationsBySender()[userId];
return Object.fromEntries([...myAnnotations]
.filter(event => !event.isRedacted())
.map(event => [event.getRelation().key, event.getId()]));
};
const menuOptions = {
reactions: this.props.reactions,
chevronFace: "none",
onFinished: () => this.onFocusChange(false),
onChoose: reaction => {
this.onFocusChange(false);
const myReactions = getReactions();
if (myReactions.hasOwnProperty(reaction)) {
MatrixClientPeg.get().redactEvent(
this.props.mxEvent.getRoomId(),
myReactions[reaction],
);
} else {
MatrixClientPeg.get().sendEvent(this.props.mxEvent.getRoomId(), "m.reaction", {
"m.relates_to": {
"rel_type": "m.annotation",
"event_id": this.props.mxEvent.getId(),
"key": reaction,
},
});
}
},
};
// The window X and Y offsets are to adjust position when zoomed in to page // The window X and Y offsets are to adjust position when zoomed in to page
const buttonRight = buttonRect.right + window.pageXOffset; const buttonRight = buttonRect.right + window.pageXOffset;
const buttonBottom = buttonRect.bottom + window.pageYOffset; const buttonBottom = buttonRect.bottom + window.pageYOffset;
@ -137,15 +101,27 @@ export default class MessageActionBar extends React.PureComponent {
} else { } else {
menuOptions.bottom = window.innerHeight - buttonTop; menuOptions.bottom = window.innerHeight - buttonTop;
} }
return menuOptions;
};
createMenu(EmojiPicker, menuOptions); onReactClick = (ev) => {
const ReactionPicker = sdk.getComponent('emojipicker.ReactionPicker');
const menuOptions = {
...this.getMenuOptions(ev),
mxEvent: this.props.mxEvent,
reactions: this.props.reactions,
chevronFace: "none",
onFinished: () => this.onFocusChange(false),
};
createMenu(ReactionPicker, menuOptions);
this.onFocusChange(true); this.onFocusChange(true);
}; };
onOptionsClick = (ev) => { onOptionsClick = (ev) => {
const MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu'); const MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu');
const buttonRect = ev.target.getBoundingClientRect();
const { getTile, getReplyThread } = this.props; const { getTile, getReplyThread } = this.props;
const tile = getTile && getTile(); const tile = getTile && getTile();
@ -157,6 +133,7 @@ export default class MessageActionBar extends React.PureComponent {
} }
const menuOptions = { const menuOptions = {
...this.getMenuOptions(ev),
mxEvent: this.props.mxEvent, mxEvent: this.props.mxEvent,
chevronFace: "none", chevronFace: "none",
permalinkCreator: this.props.permalinkCreator, permalinkCreator: this.props.permalinkCreator,
@ -168,20 +145,6 @@ export default class MessageActionBar extends React.PureComponent {
}, },
}; };
// The window X and Y offsets are to adjust position when zoomed in to page
const buttonRight = buttonRect.right + window.pageXOffset;
const buttonBottom = buttonRect.bottom + window.pageYOffset;
const buttonTop = buttonRect.top + window.pageYOffset;
// Align the right edge of the menu to the right edge of the button
menuOptions.right = window.innerWidth - buttonRight;
// Align the menu vertically on whichever side of the button has more
// space available.
if (buttonBottom < window.innerHeight / 2) {
menuOptions.top = buttonBottom;
} else {
menuOptions.bottom = window.innerHeight - buttonTop;
}
createMenu(MessageContextMenu, menuOptions); createMenu(MessageContextMenu, menuOptions);
this.onFocusChange(true); this.onFocusChange(true);