From 318754d31c82a365b07a27a387918bf4434df779 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 15 Oct 2019 19:07:04 +0300 Subject: [PATCH] Reorganize reaction sending and show if emoji is selected Signed-off-by: Tulir Asokan --- res/css/views/emojipicker/_EmojiPicker.scss | 6 + src/components/views/emojipicker/Category.js | 7 +- src/components/views/emojipicker/Emoji.js | 8 +- .../views/emojipicker/EmojiPicker.js | 18 +-- src/components/views/emojipicker/Preview.js | 13 +- .../views/emojipicker/QuickReactions.js | 4 +- .../views/emojipicker/ReactionPicker.js | 120 ++++++++++++++++++ src/components/views/emojipicker/Search.js | 2 +- .../views/messages/MessageActionBar.js | 71 +++-------- 9 files changed, 174 insertions(+), 75 deletions(-) create mode 100644 src/components/views/emojipicker/ReactionPicker.js diff --git a/res/css/views/emojipicker/_EmojiPicker.scss b/res/css/views/emojipicker/_EmojiPicker.scss index 88606538d5..549a7c3621 100644 --- a/res/css/views/emojipicker/_EmojiPicker.scss +++ b/res/css/views/emojipicker/_EmojiPicker.scss @@ -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 { font-size: 16px; font-weight: 600; diff --git a/src/components/views/emojipicker/Category.js b/src/components/views/emojipicker/Category.js index dcda106719..b8e56488d8 100644 --- a/src/components/views/emojipicker/Category.js +++ b/src/components/views/emojipicker/Category.js @@ -27,10 +27,11 @@ class Category extends React.PureComponent { onMouseEnter: PropTypes.func.isRequired, onMouseLeave: PropTypes.func.isRequired, onClick: PropTypes.func.isRequired, + selectedEmojis: PropTypes.instanceOf(Set), }; render() { - const { onClick, onMouseEnter, onMouseLeave, emojis, name } = this.props; + const { onClick, onMouseEnter, onMouseLeave, emojis, name, selectedEmojis } = this.props; if (!emojis || emojis.length === 0) { return null; } @@ -42,11 +43,11 @@ class Category extends React.PureComponent { {name} - ) + ); } } diff --git a/src/components/views/emojipicker/Emoji.js b/src/components/views/emojipicker/Emoji.js index 3bbbe3a771..3db5882fb3 100644 --- a/src/components/views/emojipicker/Emoji.js +++ b/src/components/views/emojipicker/Emoji.js @@ -23,18 +23,20 @@ class Emoji extends React.PureComponent { onMouseEnter: PropTypes.func, onMouseLeave: PropTypes.func, emoji: PropTypes.object.isRequired, + selectedEmojis: PropTypes.instanceOf(Set), }; render() { - const { onClick, onMouseEnter, onMouseLeave, emoji } = this.props; + const { onClick, onMouseEnter, onMouseLeave, emoji, selectedEmojis } = this.props; + const isSelected = selectedEmojis && selectedEmojis.has(emoji.unicode); return (
  • onClick(emoji)} onMouseEnter={() => onMouseEnter(emoji)} onMouseLeave={() => onMouseLeave(emoji)} - className="mx_EmojiPicker_item"> + className={`mx_EmojiPicker_item ${isSelected ? 'mx_EmojiPicker_item_selected' : ''}`}> {emoji.unicode}
  • - ) + ); } } diff --git a/src/components/views/emojipicker/EmojiPicker.js b/src/components/views/emojipicker/EmojiPicker.js index d6d79d7b8c..1d5b11edb1 100644 --- a/src/components/views/emojipicker/EmojiPicker.js +++ b/src/components/views/emojipicker/EmojiPicker.js @@ -61,7 +61,8 @@ EMOJIBASE.forEach(emoji => { class EmojiPicker extends React.Component { static propTypes = { onChoose: PropTypes.func.isRequired, - closeMenu: PropTypes.func, + selectedEmojis: PropTypes.instanceOf(Set), + showQuickReactions: PropTypes.bool, }; constructor(props) { @@ -204,10 +205,8 @@ class EmojiPicker extends React.Component { } onClickEmoji(emoji) { - recent.add(emoji.unicode); - this.props.onChoose(emoji.unicode); - if (this.props.closeMenu) { - this.props.closeMenu(); + if (this.props.onChoose(emoji.unicode) !== false) { + recent.add(emoji.unicode); } } @@ -225,14 +224,15 @@ class EmojiPicker extends React.Component { {this.categories.map(category => ( + onMouseEnter={this.onHoverEmoji} onMouseLeave={this.onHoverEmojiEnd} + selectedEmojis={this.props.selectedEmojis} /> ))} - {this.state.previewEmoji + {this.state.previewEmoji || !this.props.showQuickReactions ? - : } + : } - ) + ); } } diff --git a/src/components/views/emojipicker/Preview.js b/src/components/views/emojipicker/Preview.js index 1757a04801..75d3e35f31 100644 --- a/src/components/views/emojipicker/Preview.js +++ b/src/components/views/emojipicker/Preview.js @@ -19,21 +19,26 @@ import PropTypes from 'prop-types'; class Preview extends React.PureComponent { static propTypes = { - emoji: PropTypes.object.isRequired, + emoji: PropTypes.object, }; render() { + const { + unicode = "", + annotation = "", + shortcodes: [shortcode = ""] + } = this.props.emoji || {}; return (
    - {this.props.emoji.unicode} + {unicode}
    - {this.props.emoji.annotation} + {annotation}
    - {this.props.emoji.shortcodes[0]} + {shortcode}
    diff --git a/src/components/views/emojipicker/QuickReactions.js b/src/components/views/emojipicker/QuickReactions.js index 58c3095d34..2357345460 100644 --- a/src/components/views/emojipicker/QuickReactions.js +++ b/src/components/views/emojipicker/QuickReactions.js @@ -32,6 +32,7 @@ EMOJIBASE.forEach(emoji => { class QuickReactions extends React.Component { static propTypes = { onClick: PropTypes.func.isRequired, + selectedEmojis: PropTypes.instanceOf(Set), }; constructor(props) { @@ -72,7 +73,8 @@ class QuickReactions extends React.Component {
      {QUICK_REACTIONS.map(emoji => )} + onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave} + selectedEmojis={this.props.selectedEmojis}/>)}
    ) diff --git a/src/components/views/emojipicker/ReactionPicker.js b/src/components/views/emojipicker/ReactionPicker.js new file mode 100644 index 0000000000..d539fa30e6 --- /dev/null +++ b/src/components/views/emojipicker/ReactionPicker.js @@ -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 + } +} + +export default ReactionPicker diff --git a/src/components/views/emojipicker/Search.js b/src/components/views/emojipicker/Search.js index 19da7c2e6c..547f673815 100644 --- a/src/components/views/emojipicker/Search.js +++ b/src/components/views/emojipicker/Search.js @@ -45,7 +45,7 @@ class Search extends React.PureComponent { {this.props.query ? icons.search.delete() : icons.search.search()} - ) + ); } } diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index 48554d8cc0..df1bc9a294 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -85,45 +85,9 @@ export default class MessageActionBar extends React.PureComponent { }); }; - onReactClick = (ev) => { - const EmojiPicker = sdk.getComponent('emojipicker.EmojiPicker'); + getMenuOptions = (ev) => { + const menuOptions = {}; 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 const buttonRight = buttonRect.right + window.pageXOffset; const buttonBottom = buttonRect.bottom + window.pageYOffset; @@ -137,15 +101,27 @@ export default class MessageActionBar extends React.PureComponent { } else { 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); }; onOptionsClick = (ev) => { const MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu'); - const buttonRect = ev.target.getBoundingClientRect(); const { getTile, getReplyThread } = this.props; const tile = getTile && getTile(); @@ -157,6 +133,7 @@ export default class MessageActionBar extends React.PureComponent { } const menuOptions = { + ...this.getMenuOptions(ev), mxEvent: this.props.mxEvent, chevronFace: "none", 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); this.onFocusChange(true);