From 497b779334cd0e5f92ccc068c8a9fb9d4c8fe01a Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 14 Oct 2019 00:32:11 +0300 Subject: [PATCH 01/16] Add full emoji picker for reactions Signed-off-by: Tulir Asokan --- res/css/_components.scss | 3 +- res/css/views/emojipicker/_EmojiPicker.scss | 157 ++++++++++++++ .../views/messages/_ReactionQuickTooltip.scss | 29 --- .../messages/_ReactionTooltipButton.scss | 31 --- src/components/views/emojipicker/Category.js | 57 +++++ src/components/views/emojipicker/Emoji.js | 41 ++++ .../views/emojipicker/EmojiPicker.js | 154 ++++++++++++++ src/components/views/emojipicker/Header.js | 58 ++++++ src/components/views/emojipicker/Preview.js | 44 ++++ .../views/emojipicker/QuickReactions.js | 82 ++++++++ src/components/views/emojipicker/Search.js | 38 ++++ src/components/views/emojipicker/icons.js | 170 +++++++++++++++ .../views/messages/MessageActionBar.js | 57 +++-- .../views/messages/ReactMessageAction.js | 97 --------- .../views/messages/ReactionTooltipButton.js | 68 ------ .../views/messages/ReactionsQuickTooltip.js | 195 ------------------ src/i18n/strings/en_EN.json | 12 +- 17 files changed, 858 insertions(+), 435 deletions(-) create mode 100644 res/css/views/emojipicker/_EmojiPicker.scss delete mode 100644 res/css/views/messages/_ReactionQuickTooltip.scss delete mode 100644 res/css/views/messages/_ReactionTooltipButton.scss create mode 100644 src/components/views/emojipicker/Category.js create mode 100644 src/components/views/emojipicker/Emoji.js create mode 100644 src/components/views/emojipicker/EmojiPicker.js create mode 100644 src/components/views/emojipicker/Header.js create mode 100644 src/components/views/emojipicker/Preview.js create mode 100644 src/components/views/emojipicker/QuickReactions.js create mode 100644 src/components/views/emojipicker/Search.js create mode 100644 src/components/views/emojipicker/icons.js delete mode 100644 src/components/views/messages/ReactMessageAction.js delete mode 100644 src/components/views/messages/ReactionTooltipButton.js delete mode 100644 src/components/views/messages/ReactionsQuickTooltip.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 4891fd90c02..7e1a280dd35 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -108,6 +108,7 @@ @import "./views/elements/_Tooltip.scss"; @import "./views/elements/_TooltipButton.scss"; @import "./views/elements/_Validation.scss"; +@import "./views/emojipicker/_EmojiPicker.scss"; @import "./views/globals/_MatrixToolbar.scss"; @import "./views/groups/_GroupPublicityToggle.scss"; @import "./views/groups/_GroupRoomList.scss"; @@ -122,8 +123,6 @@ @import "./views/messages/_MTextBody.scss"; @import "./views/messages/_MessageActionBar.scss"; @import "./views/messages/_MessageTimestamp.scss"; -@import "./views/messages/_ReactionQuickTooltip.scss"; -@import "./views/messages/_ReactionTooltipButton.scss"; @import "./views/messages/_ReactionsRow.scss"; @import "./views/messages/_ReactionsRowButton.scss"; @import "./views/messages/_ReactionsRowButtonTooltip.scss"; diff --git a/res/css/views/emojipicker/_EmojiPicker.scss b/res/css/views/emojipicker/_EmojiPicker.scss new file mode 100644 index 00000000000..99a75b9d10c --- /dev/null +++ b/res/css/views/emojipicker/_EmojiPicker.scss @@ -0,0 +1,157 @@ +/* +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. +*/ + +.mx_EmojiPicker { + width: 340px; + height: 450px; + + border-radius: 4px; + + display: flex; + flex-direction: column; +} + +.mx_EmojiPicker_body { + flex: 1; + overflow-y: scroll; + scrollbar-width: thin; + scrollbar-color: rgba(0, 0, 0, 0.2) transparent; +} + +.mx_EmojiPicker_header { + padding: 4px 8px 0; + border-bottom: 1px solid $message-action-bar-border-color; +} + +.mx_EmojiPicker_anchor { + border: none; + padding: 8px 8px 6px; + border-bottom: 2px solid transparent; + background-color: transparent; + border-radius: 4px 4px 0 0; + + svg { + width: 20px; + height: 20px; + fill: $primary-fg-color; + } + + &:hover { + background-color: $focus-bg-color; + border-bottom: 2px solid $button-bg-color; + } + + .mx_EmojiPicker_anchor_selected { + border-bottom: 2px solid $button-bg-color; + } +} + +.mx_EmojiPicker_search { + margin: 8px; + border-radius: 4px; + border: 1px solid $input-border-color; + background-color: $primary-bg-color; + display: flex; + + input { + flex: 1; + border: none; + padding: 8px 12px; + border-radius: 4px 0; + } + + svg { + align-self: center; + width: 16px; + height: 16px; + margin: 8px; + } +} + +.mx_EmojiPicker_category { + padding: 0 12px; +} + +.mx_EmojiPicker_category_label, .mx_EmojiPicker_preview_name { + font-size: 16px; + font-weight: 600; + margin: 0; +} + +.mx_EmojiPicker_list { + padding: 0; + margin: 0; + // TODO the emoji rows need to be center-aligned, but the individual emojis shouldn't be. + //text-align: center; +} + +.mx_EmojiPicker_item { + list-style: none; + display: inline-block; + font-size: 20px; + margin: 1px; + padding: 4px 0; + width: 36px; + box-sizing: border-box; + text-align: center; + border-radius: 4px; + cursor: pointer; + + &:hover { + background-color: $focus-bg-color; + } +} + +.mx_EmojiPicker_footer { + border-top: 1px solid $message-action-bar-border-color; + height: 72px; + + display: flex; + align-items: center; +} + +.mx_EmojiPicker_preview_emoji { + font-size: 32px; + padding: 8px 16px; +} + +.mx_EmojiPicker_preview_text { + display: flex; + flex-direction: column; +} + +.mx_EmojiPicker_name { + text-transform: capitalize; +} + +.mx_EmojiPicker_shortcode { + color: $light-fg-color; + font-size: 14px; + + &::before, &::after { + content: ":"; + } +} + +.mx_EmojiPicker_quick { + flex-direction: column; + align-items: start; + justify-content: space-around; +} + +.mx_EmojiPicker_quick_header .mx_EmojiPicker_name { + margin-right: 4px; +} diff --git a/res/css/views/messages/_ReactionQuickTooltip.scss b/res/css/views/messages/_ReactionQuickTooltip.scss deleted file mode 100644 index 7b1611483b9..00000000000 --- a/res/css/views/messages/_ReactionQuickTooltip.scss +++ /dev/null @@ -1,29 +0,0 @@ -/* -Copyright 2019 The Matrix.org Foundation C.I.C. - -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. -*/ - -.mx_ReactionsQuickTooltip_buttons { - display: grid; - grid-template-columns: repeat(4, auto); -} - -.mx_ReactionsQuickTooltip_label { - text-align: center; -} - -.mx_ReactionsQuickTooltip_shortcode { - padding-left: 6px; - opacity: 0.7; -} diff --git a/res/css/views/messages/_ReactionTooltipButton.scss b/res/css/views/messages/_ReactionTooltipButton.scss deleted file mode 100644 index 59244ab63b7..00000000000 --- a/res/css/views/messages/_ReactionTooltipButton.scss +++ /dev/null @@ -1,31 +0,0 @@ -/* -Copyright 2019 The Matrix.org Foundation C.I.C. - -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. -*/ - -.mx_ReactionTooltipButton { - font-size: 16px; - padding: 6px; - user-select: none; - cursor: pointer; - transition: transform 0.25s; - - &:hover { - transform: scale(1.2); - } -} - -.mx_ReactionTooltipButton_selected { - opacity: 0.4; -} diff --git a/src/components/views/emojipicker/Category.js b/src/components/views/emojipicker/Category.js new file mode 100644 index 00000000000..9573a246301 --- /dev/null +++ b/src/components/views/emojipicker/Category.js @@ -0,0 +1,57 @@ +/* +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 sdk from '../../../index'; + +class Category extends React.PureComponent { + static propTypes = { + emojis: PropTypes.arrayOf(PropTypes.object).isRequired, + name: PropTypes.string.isRequired, + onMouseEnter: PropTypes.func.isRequired, + onMouseLeave: PropTypes.func.isRequired, + onClick: PropTypes.func.isRequired, + filter: PropTypes.string, + }; + + render() { + const { onClick, onMouseEnter, onMouseLeave, emojis, name, filter } = this.props; + + const Emoji = sdk.getComponent("emojipicker.Emoji"); + const renderedEmojis = (emojis || []).map(emoji => !filter || emoji.filterString.includes(filter) ? ( + + ) : null).filter(component => component !== null); + if (renderedEmojis.length === 0) { + return null; + } + + return ( +
+

+ {name} +

+
    + {renderedEmojis} +
+
+ ) + } +} + +export default Category; diff --git a/src/components/views/emojipicker/Emoji.js b/src/components/views/emojipicker/Emoji.js new file mode 100644 index 00000000000..3bbbe3a7719 --- /dev/null +++ b/src/components/views/emojipicker/Emoji.js @@ -0,0 +1,41 @@ +/* +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'; + +class Emoji extends React.PureComponent { + static propTypes = { + onClick: PropTypes.func, + onMouseEnter: PropTypes.func, + onMouseLeave: PropTypes.func, + emoji: PropTypes.object.isRequired, + }; + + render() { + const { onClick, onMouseEnter, onMouseLeave, emoji } = this.props; + return ( +
  • onClick(emoji)} + onMouseEnter={() => onMouseEnter(emoji)} + onMouseLeave={() => onMouseLeave(emoji)} + className="mx_EmojiPicker_item"> + {emoji.unicode} +
  • + ) + } +} + +export default Emoji; diff --git a/src/components/views/emojipicker/EmojiPicker.js b/src/components/views/emojipicker/EmojiPicker.js new file mode 100644 index 00000000000..0ffe3a06f7c --- /dev/null +++ b/src/components/views/emojipicker/EmojiPicker.js @@ -0,0 +1,154 @@ +/* +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 EMOJIBASE from 'emojibase-data/en/compact.json'; +import sdk from '../../../index'; +import { _t } from '../../../languageHandler'; + +const EMOJIBASE_CATEGORY_IDS = [ + "people", // smileys + "people", // actually people + "control", // modifiers and such, not displayed in picker + "nature", + "foods", + "places", + "activity", + "objects", + "symbols", + "flags", +]; + +const DATA_BY_CATEGORY = { + "people": [], + "nature": [], + "foods": [], + "places": [], + "activity": [], + "objects": [], + "symbols": [], + "flags": [], + "control": [], +}; + +EMOJIBASE.forEach(emoji => { + DATA_BY_CATEGORY[EMOJIBASE_CATEGORY_IDS[emoji.group]].push(emoji); + // This is used as the string to match the query against when filtering emojis. + emoji.filterString = `${emoji.annotation}\n${emoji.shortcodes.join('\n')}}\n${emoji.emoticon || ''}`; +}); + +class EmojiPicker extends React.Component { + static propTypes = { + onChoose: PropTypes.func.isRequired, + }; + + constructor(props) { + super(props); + + this.state = { + filter: "", + previewEmoji: null, + }; + + this.categories = [{ + id: "recent", + name: _t("Frequently Used"), + }, { + id: "people", + name: _t("Smileys & People"), + }, { + id: "nature", + name: _t("Animals & Nature"), + }, { + id: "foods", + name: _t("Food & Drink"), + }, { + id: "activity", + name: _t("Activities"), + }, { + id: "places", + name: _t("Travel & Places"), + }, { + id: "objects", + name: _t("Objects"), + }, { + id: "symbols", + name: _t("Symbols"), + }, { + id: "flags", + name: _t("Flags"), + }]; + + this.onChangeFilter = this.onChangeFilter.bind(this); + this.onHoverEmoji = this.onHoverEmoji.bind(this); + this.onHoverEmojiEnd = this.onHoverEmojiEnd.bind(this); + this.onClickEmoji = this.onClickEmoji.bind(this); + } + + scrollToCategory() { + // TODO + } + + onChangeFilter(ev) { + this.setState({ + filter: ev.target.value, + }); + } + + onHoverEmoji(emoji) { + this.setState({ + previewEmoji: emoji, + }); + } + + onHoverEmojiEnd(emoji) { + this.setState({ + previewEmoji: null, + }); + } + + onClickEmoji(emoji) { + this.props.onChoose(emoji.unicode); + } + + 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"); + return ( +
    +
    + +
    + {this.categories.map(category => ( + + ))} +
    + {this.state.previewEmoji + ? + : } +
    + ) + } +} + +export default EmojiPicker; diff --git a/src/components/views/emojipicker/Header.js b/src/components/views/emojipicker/Header.js new file mode 100644 index 00000000000..d061f8559a5 --- /dev/null +++ b/src/components/views/emojipicker/Header.js @@ -0,0 +1,58 @@ +/* +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 * as icons from "./icons"; + +class Header extends React.Component { + static propTypes = { + categories: PropTypes.arrayOf(PropTypes.object).isRequired, + onAnchorClick: PropTypes.func.isRequired, + defaultCategory: PropTypes.string, + }; + + constructor(props) { + super(props); + this.state = { + selected: props.defaultCategory || props.categories[0].id, + }; + this.handleClick = this.handleClick.bind(this); + } + + handleClick(ev) { + const selected = ev.target.getAttribute("data-category-id"); + this.setState({selected}); + this.props.onAnchorClick(selected); + }; + + render() { + return ( + + ) + } +} + +export default Header; diff --git a/src/components/views/emojipicker/Preview.js b/src/components/views/emojipicker/Preview.js new file mode 100644 index 00000000000..1757a048014 --- /dev/null +++ b/src/components/views/emojipicker/Preview.js @@ -0,0 +1,44 @@ +/* +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'; + +class Preview extends React.PureComponent { + static propTypes = { + emoji: PropTypes.object.isRequired, + }; + + render() { + return ( +
    +
    + {this.props.emoji.unicode} +
    +
    +
    + {this.props.emoji.annotation} +
    +
    + {this.props.emoji.shortcodes[0]} +
    +
    +
    + ) + } +} + +export default Preview; diff --git a/src/components/views/emojipicker/QuickReactions.js b/src/components/views/emojipicker/QuickReactions.js new file mode 100644 index 00000000000..58c3095d345 --- /dev/null +++ b/src/components/views/emojipicker/QuickReactions.js @@ -0,0 +1,82 @@ +/* +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 EMOJIBASE from 'emojibase-data/en/compact.json'; + +import sdk from '../../../index'; +import { _t } from '../../../languageHandler'; + +const QUICK_REACTIONS = ["👍️", "👎️", "😄", "🎉", "😕", "❤️", "🚀", "👀"]; +EMOJIBASE.forEach(emoji => { + const index = QUICK_REACTIONS.indexOf(emoji.unicode); + if (index !== -1) { + QUICK_REACTIONS[index] = emoji; + } +}); + +class QuickReactions extends React.Component { + static propTypes = { + onClick: PropTypes.func.isRequired, + }; + + constructor(props) { + super(props); + this.state = { + hover: null, + }; + this.onMouseEnter = this.onMouseEnter.bind(this); + this.onMouseLeave = this.onMouseLeave.bind(this); + } + + onMouseEnter(emoji) { + this.setState({ + hover: emoji, + }); + } + + onMouseLeave() { + this.setState({ + hover: null, + }); + } + + render() { + const Emoji = sdk.getComponent("emojipicker.Emoji"); + + return ( +
    +

    + {!this.state.hover + ? _t("Quick Reactions") + : + {this.state.hover.annotation} + {this.state.hover.shortcodes[0]} + + } +

    +
      + {QUICK_REACTIONS.map(emoji => )} +
    +
    + ) + } +} + +export default QuickReactions; diff --git a/src/components/views/emojipicker/Search.js b/src/components/views/emojipicker/Search.js new file mode 100644 index 00000000000..a0200a29d43 --- /dev/null +++ b/src/components/views/emojipicker/Search.js @@ -0,0 +1,38 @@ +/* +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 * as icons from "./icons"; + +class Search extends React.PureComponent { + static propTypes = { + query: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, + }; + + render() { + return ( +
    + + {icons.search.search()} +
    + ) + } +} + +export default Search; diff --git a/src/components/views/emojipicker/icons.js b/src/components/views/emojipicker/icons.js new file mode 100644 index 00000000000..b6cf1ad371b --- /dev/null +++ b/src/components/views/emojipicker/icons.js @@ -0,0 +1,170 @@ +// Copyright (c) 2016, Missive +// From https://github.com/missive/emoji-mart/blob/master/src/svgs/index.js +// Licensed under BSD-3-Clause: https://github.com/missive/emoji-mart/blob/master/LICENSE + +import React from 'react' + +const categories = { + activity: () => ( + + + + ), + + custom: () => ( + + + + + + + + ), + + flags: () => ( + + + + ), + + foods: () => ( + + + + ), + + nature: () => ( + + + + + ), + + objects: () => ( + + + + + ), + + people: () => ( + + + + + ), + + places: () => ( + + + + + ), + + recent: () => ( + + + + + ), + + symbols: () => ( + + + + ), +} + +const search = { + search: () => ( + + + + ), + + delete: () => ( + + + + ), +} + +export { categories, search } diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index 2b43c5fe2a8..95ab57d3245 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -25,6 +25,7 @@ import Modal from '../../../Modal'; import { createMenu } from '../../structures/ContextualMenu'; import { isContentActionable, canEditContent } from '../../../utils/EventUtils'; import {RoomContext} from "../../structures/RoomView"; +import MatrixClientPeg from '../../../MatrixClientPeg'; export default class MessageActionBar extends React.PureComponent { static propTypes = { @@ -84,6 +85,45 @@ export default class MessageActionBar extends React.PureComponent { }); }; + onReactClick = (ev) => { + const EmojiPicker = sdk.getComponent('emojipicker.EmojiPicker'); + const buttonRect = ev.target.getBoundingClientRect(); + + const menuOptions = { + reactions: this.props.reactions, + chevronFace: "none", + onFinished: () => this.onFocusChange(false), + onChoose: reaction => { + this.onFocusChange(false); + 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; + 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(EmojiPicker, menuOptions); + + this.onFocusChange(true); + }; + onOptionsClick = (ev) => { const MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu'); const buttonRect = ev.target.getBoundingClientRect(); @@ -128,17 +168,6 @@ export default class MessageActionBar extends React.PureComponent { this.onFocusChange(true); }; - renderReactButton() { - const ReactMessageAction = sdk.getComponent('messages.ReactMessageAction'); - const { mxEvent, reactions } = this.props; - - return ; - } - render() { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); @@ -148,7 +177,11 @@ export default class MessageActionBar extends React.PureComponent { if (isContentActionable(this.props.mxEvent)) { if (this.context.room.canReact) { - reactButton = this.renderReactButton(); + reactButton = ; } if (this.context.room.canReply) { replyButton = { - if (!this.props.onFocusChange) { - return; - } - this.props.onFocusChange(focused); - } - - componentDidUpdate(prevProps) { - if (prevProps.reactions !== 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); - 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, - ); - } - } - - onReactionsChange = () => { - // Force a re-render of the tooltip because a change in the reactions - // set means the event tile's layout may have changed and possibly - // altered the location where the tooltip should be shown. - this.forceUpdate(); - } - - render() { - const ReactionsQuickTooltip = sdk.getComponent('messages.ReactionsQuickTooltip'); - const InteractiveTooltip = sdk.getComponent('elements.InteractiveTooltip'); - const { mxEvent, reactions } = this.props; - - const content = ; - - return - - ; - } -} diff --git a/src/components/views/messages/ReactionTooltipButton.js b/src/components/views/messages/ReactionTooltipButton.js deleted file mode 100644 index e09b9ade69e..00000000000 --- a/src/components/views/messages/ReactionTooltipButton.js +++ /dev/null @@ -1,68 +0,0 @@ -/* -Copyright 2019 The Matrix.org Foundation C.I.C. - -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 classNames from 'classnames'; - -import MatrixClientPeg from '../../../MatrixClientPeg'; - -export default class ReactionTooltipButton extends React.PureComponent { - static propTypes = { - mxEvent: PropTypes.object.isRequired, - // The reaction content / key / emoji - content: PropTypes.string.isRequired, - title: PropTypes.string, - // A possible Matrix event if the current user has voted for this type - myReactionEvent: PropTypes.object, - }; - - onClick = (ev) => { - const { mxEvent, myReactionEvent, content } = this.props; - if (myReactionEvent) { - MatrixClientPeg.get().redactEvent( - mxEvent.getRoomId(), - myReactionEvent.getId(), - ); - } else { - MatrixClientPeg.get().sendEvent(mxEvent.getRoomId(), "m.reaction", { - "m.relates_to": { - "rel_type": "m.annotation", - "event_id": mxEvent.getId(), - "key": content, - }, - }); - } - } - - render() { - const { content, myReactionEvent } = this.props; - - const classes = classNames({ - mx_ReactionTooltipButton: true, - mx_ReactionTooltipButton_selected: !!myReactionEvent, - }); - - return - {content} - ; - } -} diff --git a/src/components/views/messages/ReactionsQuickTooltip.js b/src/components/views/messages/ReactionsQuickTooltip.js deleted file mode 100644 index 0505bbd2dfe..00000000000 --- a/src/components/views/messages/ReactionsQuickTooltip.js +++ /dev/null @@ -1,195 +0,0 @@ -/* -Copyright 2019 The Matrix.org Foundation C.I.C. - -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 { _t } from '../../../languageHandler'; -import sdk from '../../../index'; -import MatrixClientPeg from '../../../MatrixClientPeg'; -import { unicodeToShortcode } from '../../../HtmlUtils'; - -export default class ReactionsQuickTooltip extends React.PureComponent { - static propTypes = { - mxEvent: PropTypes.object.isRequired, - // The Relations model from the JS SDK for reactions to `mxEvent` - reactions: PropTypes.object, - }; - - constructor(props) { - super(props); - - if (props.reactions) { - props.reactions.on("Relations.add", this.onReactionsChange); - props.reactions.on("Relations.remove", this.onReactionsChange); - props.reactions.on("Relations.redaction", this.onReactionsChange); - } - - this.state = { - hoveredItem: null, - myReactions: this.getMyReactions(), - }; - } - - componentDidUpdate(prevProps) { - if (prevProps.reactions !== 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); - 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, - ); - } - } - - onReactionsChange = () => { - this.setState({ - myReactions: this.getMyReactions(), - }); - } - - getMyReactions() { - const reactions = this.props.reactions; - if (!reactions) { - return null; - } - const userId = MatrixClientPeg.get().getUserId(); - const myReactions = reactions.getAnnotationsBySender()[userId]; - if (!myReactions) { - return null; - } - return [...myReactions.values()]; - } - - onMouseOver = (ev) => { - const { key } = ev.target.dataset; - const item = this.items.find(({ content }) => content === key); - this.setState({ - hoveredItem: item, - }); - } - - onMouseOut = (ev) => { - this.setState({ - hoveredItem: null, - }); - } - - get items() { - return [ - { - content: "👍", - title: _t("Agree"), - }, - { - content: "👎", - title: _t("Disagree"), - }, - { - content: "😄", - title: _t("Happy"), - }, - { - content: "🎉", - title: _t("Party Popper"), - }, - { - content: "😕", - title: _t("Confused"), - }, - { - content: "❤️", - title: _t("Heart"), - }, - { - content: "🚀", - title: _t("Rocket"), - }, - { - content: "👀", - title: _t("Eyes"), - }, - ]; - } - - render() { - const { mxEvent } = this.props; - const { myReactions, hoveredItem } = this.state; - const ReactionTooltipButton = sdk.getComponent('messages.ReactionTooltipButton'); - - const buttons = this.items.map(({ content, title }) => { - const myReactionEvent = myReactions && myReactions.find(mxEvent => { - if (mxEvent.isRedacted()) { - return false; - } - return mxEvent.getRelation().key === content; - }); - - return ; - }); - - let label = " "; // non-breaking space to keep layout the same when empty - if (hoveredItem) { - const { content, title } = hoveredItem; - - let shortcodeLabel; - const shortcode = unicodeToShortcode(content); - if (shortcode) { - shortcodeLabel = - {shortcode} - ; - } - - label =
    - - {title} - - {shortcodeLabel} -
    ; - } - - return
    -
    - {buttons} -
    - {label} -
    ; - } -} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f524a22d4b1..44812f8bbc2 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1829,5 +1829,15 @@ "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.", "Failed to set direct chat tag": "Failed to set direct chat tag", "Failed to remove tag %(tagName)s from room": "Failed to remove tag %(tagName)s from room", - "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room" + "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room", + "Quick Reactions": "Quick Reactions", + "Frequently Used": "Frequently Used", + "Smileys & People": "Smileys & People", + "Animals & Nature": "Animals & Nature", + "Food & Drink": "Food & Drink", + "Activities": "Activities", + "Travel & Places": "Travel & Places", + "Objects": "Objects", + "Symbols": "Symbols", + "Flags": "Flags" } From 088c9bff9ede44bde253a8ecd1d96b70e8b83675 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 14 Oct 2019 19:40:57 +0300 Subject: [PATCH 02/16] Add recently used section and scroll to category Signed-off-by: Tulir Asokan --- res/css/views/emojipicker/_EmojiPicker.scss | 20 ++++-- src/components/views/emojipicker/Category.js | 16 ++--- .../views/emojipicker/EmojiPicker.js | 61 +++++++++++++++---- src/components/views/emojipicker/Header.js | 7 +-- src/components/views/emojipicker/Search.js | 7 ++- src/components/views/emojipicker/recent.js | 35 +++++++++++ src/i18n/strings/en_EN.json | 3 +- 7 files changed, 115 insertions(+), 34 deletions(-) create mode 100644 src/components/views/emojipicker/recent.js diff --git a/res/css/views/emojipicker/_EmojiPicker.scss b/res/css/views/emojipicker/_EmojiPicker.scss index 99a75b9d10c..50eeb4281ca 100644 --- a/res/css/views/emojipicker/_EmojiPicker.scss +++ b/res/css/views/emojipicker/_EmojiPicker.scss @@ -49,7 +49,11 @@ limitations under the License. fill: $primary-fg-color; } - &:hover { + &:disabled svg { + fill: $focus-bg-color; + } + + &:not(:disabled):hover { background-color: $focus-bg-color; border-bottom: 2px solid $button-bg-color; } @@ -73,11 +77,17 @@ limitations under the License. border-radius: 4px 0; } - svg { - align-self: center; - width: 16px; - height: 16px; + button { + border: none; + background-color: inherit; + padding: 0; margin: 8px; + + svg { + align-self: center; + width: 16px; + height: 16px; + } } } diff --git a/src/components/views/emojipicker/Category.js b/src/components/views/emojipicker/Category.js index 9573a246301..629dd3e570c 100644 --- a/src/components/views/emojipicker/Category.js +++ b/src/components/views/emojipicker/Category.js @@ -23,31 +23,27 @@ class Category extends React.PureComponent { 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, - filter: PropTypes.string, }; render() { const { onClick, onMouseEnter, onMouseLeave, emojis, name, filter } = this.props; - - const Emoji = sdk.getComponent("emojipicker.Emoji"); - const renderedEmojis = (emojis || []).map(emoji => !filter || emoji.filterString.includes(filter) ? ( - - ) : null).filter(component => component !== null); - if (renderedEmojis.length === 0) { + if (!emojis || emojis.length === 0) { return null; } + const Emoji = sdk.getComponent("emojipicker.Emoji"); return ( -
    +

    {name}

      - {renderedEmojis} + {emojis.map(emoji => )}
    ) diff --git a/src/components/views/emojipicker/EmojiPicker.js b/src/components/views/emojipicker/EmojiPicker.js index 0ffe3a06f7c..d1f784f0629 100644 --- a/src/components/views/emojipicker/EmojiPicker.js +++ b/src/components/views/emojipicker/EmojiPicker.js @@ -16,11 +16,13 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; - import EMOJIBASE from 'emojibase-data/en/compact.json'; + import sdk from '../../../index'; import { _t } from '../../../languageHandler'; +import * as recent from './recent'; + const EMOJIBASE_CATEGORY_IDS = [ "people", // smileys "people", // actually people @@ -43,11 +45,15 @@ const DATA_BY_CATEGORY = { "objects": [], "symbols": [], "flags": [], - "control": [], }; +const DATA_BY_EMOJI = {}; EMOJIBASE.forEach(emoji => { - DATA_BY_CATEGORY[EMOJIBASE_CATEGORY_IDS[emoji.group]].push(emoji); + DATA_BY_EMOJI[emoji.unicode] = emoji; + const categoryId = EMOJIBASE_CATEGORY_IDS[emoji.group]; + if (DATA_BY_CATEGORY.hasOwnProperty(categoryId)) { + DATA_BY_CATEGORY[categoryId].push(emoji); + } // This is used as the string to match the query against when filtering emojis. emoji.filterString = `${emoji.annotation}\n${emoji.shortcodes.join('\n')}}\n${emoji.emoticon || ''}`; }); @@ -65,49 +71,76 @@ class EmojiPicker extends React.Component { previewEmoji: null, }; + this.bodyRef = React.createRef(); + + this.recentlyUsed = recent.get().map(unicode => DATA_BY_EMOJI[unicode]); + this.memoizedDataByCategory = { + recent: this.recentlyUsed, + ...DATA_BY_CATEGORY, + }; + this.categories = [{ id: "recent", name: _t("Frequently Used"), + enabled: this.recentlyUsed.length > 0, }, { id: "people", name: _t("Smileys & People"), + enabled: true, }, { id: "nature", name: _t("Animals & Nature"), + enabled: true, }, { id: "foods", name: _t("Food & Drink"), + enabled: true, }, { id: "activity", name: _t("Activities"), + enabled: true, }, { id: "places", name: _t("Travel & Places"), + enabled: true, }, { id: "objects", name: _t("Objects"), + enabled: true, }, { id: "symbols", name: _t("Symbols"), + enabled: true, }, { id: "flags", name: _t("Flags"), + enabled: true, }]; 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); + + window.bodyRef = this.bodyRef; } - scrollToCategory() { - // TODO + scrollToCategory(category) { + const index = this.categories.findIndex(cat => cat.id === category); + this.bodyRef.current.querySelector(`[data-category-id="${category}"]`).scrollIntoView(); } - onChangeFilter(ev) { - this.setState({ - filter: ev.target.value, - }); + onChangeFilter(filter) { + for (let [id, emojis] of Object.entries(this.memoizedDataByCategory)) { + // If the new filter string includes the old filter string, we don't have to re-filter the whole dataset. + if (!filter.includes(this.state.filter)) { + emojis = id === "recent" ? this.recentlyUsed : DATA_BY_CATEGORY[id]; + } + this.memoizedDataByCategory[id] = emojis.filter(emoji => emoji.filterString.includes(filter)); + this.categories.find(cat => cat.id === id).enabled = this.memoizedDataByCategory[id].length > 0; + } + this.setState({ filter }); } onHoverEmoji(emoji) { @@ -124,6 +157,10 @@ class EmojiPicker extends React.Component { onClickEmoji(emoji) { this.props.onChoose(emoji.unicode); + recent.add(emoji.unicode); + this.recentlyUsed = recent.get().map(unicode => DATA_BY_EMOJI[unicode]); + this.memoizedDataByCategory.recent = this.recentlyUsed.filter(emoji => + emoji.filterString.includes(this.state.filter)) } render() { @@ -136,10 +173,10 @@ class EmojiPicker extends React.Component {
    -
    +
    {this.categories.map(category => ( - ))}
    diff --git a/src/components/views/emojipicker/Header.js b/src/components/views/emojipicker/Header.js index d061f8559a5..95af68f4a6d 100644 --- a/src/components/views/emojipicker/Header.js +++ b/src/components/views/emojipicker/Header.js @@ -34,8 +34,7 @@ class Header extends React.Component { this.handleClick = this.handleClick.bind(this); } - handleClick(ev) { - const selected = ev.target.getAttribute("data-category-id"); + handleClick(selected) { this.setState({selected}); this.props.onAnchorClick(selected); }; @@ -44,9 +43,9 @@ class Header extends React.Component { return (
    - ) + ); } } diff --git a/src/components/views/emojipicker/Emoji.js b/src/components/views/emojipicker/Emoji.js index 3bbbe3a7719..3db5882fb32 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 d6d79d7b8c4..1d5b11edb19 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 1757a048014..75d3e35f317 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 58c3095d345..2357345460a 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 00000000000..d539fa30e6d --- /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 19da7c2e6c2..547f673815e 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 48554d8cc0f..df1bc9a294e 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; + }; + + 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(EmojiPicker, menuOptions); + 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); From 6ce2e3d796ecd9d048b591eac3937c0428534bf1 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 15 Oct 2019 19:10:02 +0300 Subject: [PATCH 11/16] Make selected emojis more transparent Signed-off-by: Tulir Asokan --- res/css/views/emojipicker/_EmojiPicker.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/emojipicker/_EmojiPicker.scss b/res/css/views/emojipicker/_EmojiPicker.scss index 549a7c3621e..ddb3e82eca4 100644 --- a/res/css/views/emojipicker/_EmojiPicker.scss +++ b/res/css/views/emojipicker/_EmojiPicker.scss @@ -134,7 +134,7 @@ limitations under the License. } .mx_EmojiPicker_item_selected { - color: rgba(0, 0, 0, .75); + color: rgba(0, 0, 0, .5); border: 1px solid $input-valid-border-color; margin: 0; } From 30ffd65b6c9e68d5934c2b72c80d8a8095ce2337 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 16 Oct 2019 21:35:08 +0300 Subject: [PATCH 12/16] Fix reacting to messages with reactions from other users Signed-off-by: Tulir Asokan --- src/components/views/emojipicker/ReactionPicker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/emojipicker/ReactionPicker.js b/src/components/views/emojipicker/ReactionPicker.js index d539fa30e6d..4dd7ea0da7f 100644 --- a/src/components/views/emojipicker/ReactionPicker.js +++ b/src/components/views/emojipicker/ReactionPicker.js @@ -75,7 +75,7 @@ class ReactionPicker extends React.Component { return {}; } const userId = MatrixClientPeg.get().getUserId(); - const myAnnotations = this.props.reactions.getAnnotationsBySender()[userId]; + const myAnnotations = this.props.reactions.getAnnotationsBySender()[userId] || []; return Object.fromEntries([...myAnnotations] .filter(event => !event.isRedacted()) .map(event => [event.getRelation().key, event.getId()])); From be829980f68aed3d466de0e863ed5d888d9fda30 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 20 Oct 2019 12:13:32 +0300 Subject: [PATCH 13/16] Split inline SVGs to their own files Signed-off-by: Tulir Asokan --- res/css/views/emojipicker/_EmojiPicker.scss | 49 ++++-- res/img/emojipicker/activity.svg | 14 ++ res/img/emojipicker/custom.svg | 34 ++++ res/img/emojipicker/delete.svg | 15 ++ res/img/emojipicker/flags.svg | 14 ++ res/img/emojipicker/foods.svg | 14 ++ res/img/emojipicker/nature.svg | 15 ++ res/img/emojipicker/objects.svg | 15 ++ res/img/emojipicker/people.svg | 15 ++ res/img/emojipicker/places.svg | 15 ++ res/img/emojipicker/recent.svg | 15 ++ res/img/emojipicker/search.svg | 15 ++ res/img/emojipicker/symbols.svg | 14 ++ src/components/views/emojipicker/Header.js | 11 +- src/components/views/emojipicker/Search.js | 9 +- src/components/views/emojipicker/icons.js | 170 -------------------- 16 files changed, 241 insertions(+), 193 deletions(-) create mode 100644 res/img/emojipicker/activity.svg create mode 100644 res/img/emojipicker/custom.svg create mode 100644 res/img/emojipicker/delete.svg create mode 100644 res/img/emojipicker/flags.svg create mode 100644 res/img/emojipicker/foods.svg create mode 100644 res/img/emojipicker/nature.svg create mode 100644 res/img/emojipicker/objects.svg create mode 100644 res/img/emojipicker/people.svg create mode 100644 res/img/emojipicker/places.svg create mode 100644 res/img/emojipicker/recent.svg create mode 100644 res/img/emojipicker/search.svg create mode 100644 res/img/emojipicker/symbols.svg delete mode 100644 src/components/views/emojipicker/icons.js diff --git a/res/css/views/emojipicker/_EmojiPicker.scss b/res/css/views/emojipicker/_EmojiPicker.scss index ddb3e82eca4..5f3cfb8133d 100644 --- a/res/css/views/emojipicker/_EmojiPicker.scss +++ b/res/css/views/emojipicker/_EmojiPicker.scss @@ -36,16 +36,6 @@ limitations under the License. border-bottom: 1px solid $message-action-bar-border-color; } -.mx_EmojiPicker button > svg { - width: 100%; - height: 100%; - fill: $primary-fg-color; -} - -.mx_EmojiPicker button:disabled > svg { - fill: $focus-bg-color; -} - .mx_EmojiPicker_anchor { border: none; padding: 8px 8px 6px; @@ -66,6 +56,31 @@ limitations under the License. } } +.mx_EmojiPicker_anchor::before { + background-color: $primary-fg-color; + content: ''; + display: inline-block; + mask-size: 100%; + mask-repeat: no-repeat; + width: 100%; + height: 100%; +} + +.mx_EmojiPicker_anchor:disabled::before { + background-color: $focus-bg-color; +} + +.mx_EmojiPicker_anchor_activity::before { mask-image: url('$(res)/img/emojipicker/activity.svg') } +.mx_EmojiPicker_anchor_custom::before { mask-image: url('$(res)/img/emojipicker/custom.svg') } +.mx_EmojiPicker_anchor_flags::before { mask-image: url('$(res)/img/emojipicker/flags.svg') } +.mx_EmojiPicker_anchor_foods::before { mask-image: url('$(res)/img/emojipicker/foods.svg') } +.mx_EmojiPicker_anchor_nature::before { mask-image: url('$(res)/img/emojipicker/nature.svg') } +.mx_EmojiPicker_anchor_objects::before { mask-image: url('$(res)/img/emojipicker/objects.svg') } +.mx_EmojiPicker_anchor_people::before { mask-image: url('$(res)/img/emojipicker/people.svg') } +.mx_EmojiPicker_anchor_places::before { mask-image: url('$(res)/img/emojipicker/places.svg') } +.mx_EmojiPicker_anchor_recent::before { mask-image: url('$(res)/img/emojipicker/recent.svg') } +.mx_EmojiPicker_anchor_symbols::before { mask-image: url('$(res)/img/emojipicker/symbols.svg') } + .mx_EmojiPicker_anchor_visible { border-bottom: 2px solid $button-bg-color; } @@ -99,6 +114,20 @@ limitations under the License. cursor: pointer; } +.mx_EmojiPicker_search_icon::after { + mask: url('$(res)/img/emojipicker/search.svg') no-repeat; + mask-size: 100%; + background-color: $primary-fg-color; + content: ''; + display: inline-block; + width: 100%; + height: 100%; +} + +.mx_EmojiPicker_search_clear::after { + mask-image: url('$(res)/img/emojipicker/delete.svg'); +} + .mx_EmojiPicker_category { padding: 0 12px; display: flex; diff --git a/res/img/emojipicker/activity.svg b/res/img/emojipicker/activity.svg new file mode 100644 index 00000000000..d921667e7a9 --- /dev/null +++ b/res/img/emojipicker/activity.svg @@ -0,0 +1,14 @@ + + + + + diff --git a/res/img/emojipicker/custom.svg b/res/img/emojipicker/custom.svg new file mode 100644 index 00000000000..814cd8ec13e --- /dev/null +++ b/res/img/emojipicker/custom.svg @@ -0,0 +1,34 @@ + + + + + + + + + diff --git a/res/img/emojipicker/delete.svg b/res/img/emojipicker/delete.svg new file mode 100644 index 00000000000..5f5d4e52eb9 --- /dev/null +++ b/res/img/emojipicker/delete.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/res/img/emojipicker/flags.svg b/res/img/emojipicker/flags.svg new file mode 100644 index 00000000000..bd0a9352658 --- /dev/null +++ b/res/img/emojipicker/flags.svg @@ -0,0 +1,14 @@ + + + + + diff --git a/res/img/emojipicker/foods.svg b/res/img/emojipicker/foods.svg new file mode 100644 index 00000000000..57a15976d8c --- /dev/null +++ b/res/img/emojipicker/foods.svg @@ -0,0 +1,14 @@ + + + + + diff --git a/res/img/emojipicker/nature.svg b/res/img/emojipicker/nature.svg new file mode 100644 index 00000000000..a4778be9275 --- /dev/null +++ b/res/img/emojipicker/nature.svg @@ -0,0 +1,15 @@ + + + + + + diff --git a/res/img/emojipicker/objects.svg b/res/img/emojipicker/objects.svg new file mode 100644 index 00000000000..e0d39f985ab --- /dev/null +++ b/res/img/emojipicker/objects.svg @@ -0,0 +1,15 @@ + + + + + + diff --git a/res/img/emojipicker/people.svg b/res/img/emojipicker/people.svg new file mode 100644 index 00000000000..c2fdb579f6d --- /dev/null +++ b/res/img/emojipicker/people.svg @@ -0,0 +1,15 @@ + + + + + + diff --git a/res/img/emojipicker/places.svg b/res/img/emojipicker/places.svg new file mode 100644 index 00000000000..0947baaf048 --- /dev/null +++ b/res/img/emojipicker/places.svg @@ -0,0 +1,15 @@ + + + + + + diff --git a/res/img/emojipicker/recent.svg b/res/img/emojipicker/recent.svg new file mode 100644 index 00000000000..2fdcc65cd23 --- /dev/null +++ b/res/img/emojipicker/recent.svg @@ -0,0 +1,15 @@ + + + + + + diff --git a/res/img/emojipicker/search.svg b/res/img/emojipicker/search.svg new file mode 100644 index 00000000000..b5f660b3aca --- /dev/null +++ b/res/img/emojipicker/search.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/res/img/emojipicker/symbols.svg b/res/img/emojipicker/symbols.svg new file mode 100644 index 00000000000..a2b86d9ec80 --- /dev/null +++ b/res/img/emojipicker/symbols.svg @@ -0,0 +1,14 @@ + + + + + diff --git a/src/components/views/emojipicker/Header.js b/src/components/views/emojipicker/Header.js index 04addfc81df..05cbebbfb1a 100644 --- a/src/components/views/emojipicker/Header.js +++ b/src/components/views/emojipicker/Header.js @@ -17,8 +17,6 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import * as icons from "./icons"; - class Header extends React.PureComponent { static propTypes = { categories: PropTypes.arrayOf(PropTypes.object).isRequired, @@ -31,13 +29,12 @@ class Header extends React.PureComponent { - ) + ); } } diff --git a/src/components/views/emojipicker/Search.js b/src/components/views/emojipicker/Search.js index 547f673815e..dbdb91ff10f 100644 --- a/src/components/views/emojipicker/Search.js +++ b/src/components/views/emojipicker/Search.js @@ -17,8 +17,6 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import * as icons from "./icons"; - class Search extends React.PureComponent { static propTypes = { query: PropTypes.string.isRequired, @@ -39,11 +37,10 @@ class Search extends React.PureComponent { return (
    this.props.onChange(ev.target.value)} ref={this.inputRef}/> + onChange={ev => this.props.onChange(ev.target.value)} ref={this.inputRef} /> + className={`mx_EmojiPicker_search_icon ${this.props.query ? "mx_EmojiPicker_search_clear" : ""}`} + title={this.props.query ? "Cancel search" : "Search"} />
    ); } diff --git a/src/components/views/emojipicker/icons.js b/src/components/views/emojipicker/icons.js deleted file mode 100644 index b6cf1ad371b..00000000000 --- a/src/components/views/emojipicker/icons.js +++ /dev/null @@ -1,170 +0,0 @@ -// Copyright (c) 2016, Missive -// From https://github.com/missive/emoji-mart/blob/master/src/svgs/index.js -// Licensed under BSD-3-Clause: https://github.com/missive/emoji-mart/blob/master/LICENSE - -import React from 'react' - -const categories = { - activity: () => ( - - - - ), - - custom: () => ( - - - - - - - - ), - - flags: () => ( - - - - ), - - foods: () => ( - - - - ), - - nature: () => ( - - - - - ), - - objects: () => ( - - - - - ), - - people: () => ( - - - - - ), - - places: () => ( - - - - - ), - - recent: () => ( - - - - - ), - - symbols: () => ( - - - - ), -} - -const search = { - search: () => ( - - - - ), - - delete: () => ( - - - - ), -} - -export { categories, search } From 10732e8e7341a0cb60a7919ea4904c208428dc7c Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 20 Oct 2019 12:13:42 +0300 Subject: [PATCH 14/16] Fix license headers Signed-off-by: Tulir Asokan --- src/components/views/emojipicker/Category.js | 2 +- src/components/views/emojipicker/Emoji.js | 2 +- src/components/views/emojipicker/EmojiPicker.js | 2 +- src/components/views/emojipicker/Header.js | 2 +- src/components/views/emojipicker/Preview.js | 2 +- src/components/views/emojipicker/QuickReactions.js | 2 +- src/components/views/emojipicker/ReactionPicker.js | 2 +- src/components/views/emojipicker/Search.js | 2 +- src/components/views/emojipicker/recent.js | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/views/emojipicker/Category.js b/src/components/views/emojipicker/Category.js index b8e56488d8f..ba48c8842b8 100644 --- a/src/components/views/emojipicker/Category.js +++ b/src/components/views/emojipicker/Category.js @@ -1,5 +1,5 @@ /* -Copyright 2019 Tulir Asokan +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. diff --git a/src/components/views/emojipicker/Emoji.js b/src/components/views/emojipicker/Emoji.js index 3db5882fb32..8d6ffe122b7 100644 --- a/src/components/views/emojipicker/Emoji.js +++ b/src/components/views/emojipicker/Emoji.js @@ -1,5 +1,5 @@ /* -Copyright 2019 Tulir Asokan +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. diff --git a/src/components/views/emojipicker/EmojiPicker.js b/src/components/views/emojipicker/EmojiPicker.js index 1d5b11edb19..6bf79d26232 100644 --- a/src/components/views/emojipicker/EmojiPicker.js +++ b/src/components/views/emojipicker/EmojiPicker.js @@ -1,5 +1,5 @@ /* -Copyright 2019 Tulir Asokan +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. diff --git a/src/components/views/emojipicker/Header.js b/src/components/views/emojipicker/Header.js index 05cbebbfb1a..b98e90e9b16 100644 --- a/src/components/views/emojipicker/Header.js +++ b/src/components/views/emojipicker/Header.js @@ -1,5 +1,5 @@ /* -Copyright 2019 Tulir Asokan +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. diff --git a/src/components/views/emojipicker/Preview.js b/src/components/views/emojipicker/Preview.js index 75d3e35f317..618b9473c71 100644 --- a/src/components/views/emojipicker/Preview.js +++ b/src/components/views/emojipicker/Preview.js @@ -1,5 +1,5 @@ /* -Copyright 2019 Tulir Asokan +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. diff --git a/src/components/views/emojipicker/QuickReactions.js b/src/components/views/emojipicker/QuickReactions.js index 2357345460a..820865dc882 100644 --- a/src/components/views/emojipicker/QuickReactions.js +++ b/src/components/views/emojipicker/QuickReactions.js @@ -1,5 +1,5 @@ /* -Copyright 2019 Tulir Asokan +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. diff --git a/src/components/views/emojipicker/ReactionPicker.js b/src/components/views/emojipicker/ReactionPicker.js index 4dd7ea0da7f..d027ae6fd3d 100644 --- a/src/components/views/emojipicker/ReactionPicker.js +++ b/src/components/views/emojipicker/ReactionPicker.js @@ -1,5 +1,5 @@ /* -Copyright 2019 Tulir Asokan +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. diff --git a/src/components/views/emojipicker/Search.js b/src/components/views/emojipicker/Search.js index dbdb91ff10f..17fbde648b6 100644 --- a/src/components/views/emojipicker/Search.js +++ b/src/components/views/emojipicker/Search.js @@ -1,5 +1,5 @@ /* -Copyright 2019 Tulir Asokan +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. diff --git a/src/components/views/emojipicker/recent.js b/src/components/views/emojipicker/recent.js index f37fbdb2355..1d2106fbfb4 100644 --- a/src/components/views/emojipicker/recent.js +++ b/src/components/views/emojipicker/recent.js @@ -1,5 +1,5 @@ /* -Copyright 2019 Tulir Asokan +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. From 438ad54701202c544bae72af341e19c8bff337db Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 20 Oct 2019 12:31:28 +0300 Subject: [PATCH 15/16] Remove space between emojis in picker Signed-off-by: Tulir Asokan --- res/css/views/emojipicker/_EmojiPicker.scss | 17 +++++++++++------ src/components/views/emojipicker/Emoji.js | 6 ++++-- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/res/css/views/emojipicker/_EmojiPicker.scss b/res/css/views/emojipicker/_EmojiPicker.scss index 5f3cfb8133d..6dcc4d75b9f 100644 --- a/res/css/views/emojipicker/_EmojiPicker.scss +++ b/res/css/views/emojipicker/_EmojiPicker.scss @@ -145,17 +145,22 @@ limitations under the License. margin: 0; } -.mx_EmojiPicker_item { +.mx_EmojiPicker_item_wrapper { + display: inline-block; list-style: none; + width: 38px; + cursor: pointer; +} + +.mx_EmojiPicker_item { display: inline-block; font-size: 20px; - margin: 1px; - padding: 4px 0; - width: 36px; + padding: 5px; + width: 100%; + height: 100%; box-sizing: border-box; text-align: center; border-radius: 4px; - cursor: pointer; &:hover { background-color: $focus-bg-color; @@ -165,7 +170,7 @@ limitations under the License. .mx_EmojiPicker_item_selected { color: rgba(0, 0, 0, .5); border: 1px solid $input-valid-border-color; - margin: 0; + padding: 4px; } .mx_EmojiPicker_category_label, .mx_EmojiPicker_preview_name { diff --git a/src/components/views/emojipicker/Emoji.js b/src/components/views/emojipicker/Emoji.js index 8d6ffe122b7..75f23c57610 100644 --- a/src/components/views/emojipicker/Emoji.js +++ b/src/components/views/emojipicker/Emoji.js @@ -33,8 +33,10 @@ class Emoji extends React.PureComponent {
  • onClick(emoji)} onMouseEnter={() => onMouseEnter(emoji)} onMouseLeave={() => onMouseLeave(emoji)} - className={`mx_EmojiPicker_item ${isSelected ? 'mx_EmojiPicker_item_selected' : ''}`}> - {emoji.unicode} + className="mx_EmojiPicker_item_wrapper"> +
    + {emoji.unicode} +
  • ); } From b2deb548d309c29826198fddff4485ae5aa4b80c Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 20 Oct 2019 12:40:55 +0300 Subject: [PATCH 16/16] Translate search button titles Signed-off-by: Tulir Asokan --- src/components/views/emojipicker/Search.js | 3 ++- src/i18n/strings/en_EN.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/views/emojipicker/Search.js b/src/components/views/emojipicker/Search.js index 17fbde648b6..8646559fed3 100644 --- a/src/components/views/emojipicker/Search.js +++ b/src/components/views/emojipicker/Search.js @@ -16,6 +16,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; +import { _t } from '../../../languageHandler'; class Search extends React.PureComponent { static propTypes = { @@ -40,7 +41,7 @@ class Search extends React.PureComponent { onChange={ev => this.props.onChange(ev.target.value)} ref={this.inputRef} />