diff --git a/res/css/_components.scss b/res/css/_components.scss index 4891fd90c0..7e1a280dd3 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 0000000000..99a75b9d10 --- /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 7b1611483b..0000000000 --- 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 59244ab63b..0000000000 --- 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 0000000000..9573a24630 --- /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 0000000000..3bbbe3a771 --- /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 0000000000..0ffe3a06f7 --- /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 0000000000..d061f8559a --- /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 0000000000..1757a04801 --- /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 0000000000..58c3095d34 --- /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 0000000000..a0200a29d4 --- /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 0000000000..b6cf1ad371 --- /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 2b43c5fe2a..95ab57d324 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 e09b9ade69..0000000000 --- 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 0505bbd2df..0000000000 --- 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 f524a22d4b..44812f8bbc 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" }