Add full emoji picker for reactions

Signed-off-by: Tulir Asokan <tulir@maunium.net>
This commit is contained in:
Tulir Asokan 2019-10-14 00:32:11 +03:00
parent 385e83fdbc
commit 497b779334
17 changed files with 858 additions and 435 deletions

View file

@ -108,6 +108,7 @@
@import "./views/elements/_Tooltip.scss"; @import "./views/elements/_Tooltip.scss";
@import "./views/elements/_TooltipButton.scss"; @import "./views/elements/_TooltipButton.scss";
@import "./views/elements/_Validation.scss"; @import "./views/elements/_Validation.scss";
@import "./views/emojipicker/_EmojiPicker.scss";
@import "./views/globals/_MatrixToolbar.scss"; @import "./views/globals/_MatrixToolbar.scss";
@import "./views/groups/_GroupPublicityToggle.scss"; @import "./views/groups/_GroupPublicityToggle.scss";
@import "./views/groups/_GroupRoomList.scss"; @import "./views/groups/_GroupRoomList.scss";
@ -122,8 +123,6 @@
@import "./views/messages/_MTextBody.scss"; @import "./views/messages/_MTextBody.scss";
@import "./views/messages/_MessageActionBar.scss"; @import "./views/messages/_MessageActionBar.scss";
@import "./views/messages/_MessageTimestamp.scss"; @import "./views/messages/_MessageTimestamp.scss";
@import "./views/messages/_ReactionQuickTooltip.scss";
@import "./views/messages/_ReactionTooltipButton.scss";
@import "./views/messages/_ReactionsRow.scss"; @import "./views/messages/_ReactionsRow.scss";
@import "./views/messages/_ReactionsRowButton.scss"; @import "./views/messages/_ReactionsRowButton.scss";
@import "./views/messages/_ReactionsRowButtonTooltip.scss"; @import "./views/messages/_ReactionsRowButtonTooltip.scss";

View file

@ -0,0 +1,157 @@
/*
Copyright 2019 Tulir Asokan <tulir@maunium.net>
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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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) ? (
<Emoji key={emoji.hexcode} emoji={emoji}
onClick={onClick} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} />
) : null).filter(component => component !== null);
if (renderedEmojis.length === 0) {
return null;
}
return (
<section className="mx_EmojiPicker_category">
<h2 className="mx_EmojiPicker_category_label">
{name}
</h2>
<ul className="mx_EmojiPicker_list">
{renderedEmojis}
</ul>
</section>
)
}
}
export default Category;

View file

@ -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 (
<li onClick={() => onClick(emoji)}
onMouseEnter={() => onMouseEnter(emoji)}
onMouseLeave={() => onMouseLeave(emoji)}
className="mx_EmojiPicker_item">
{emoji.unicode}
</li>
)
}
}
export default Emoji;

View file

@ -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 (
<div className="mx_EmojiPicker">
<Header categories={this.categories} defaultCategory="recent" onAnchorClick={this.scrollToCategory}/>
<Search query={this.state.filter} onChange={this.onChangeFilter}/>
<div className="mx_EmojiPicker_body">
{this.categories.map(category => (
<Category key={category.id} emojis={DATA_BY_CATEGORY[category.id]} name={category.name}
filter={this.state.filter} onClick={this.onClickEmoji}
onMouseEnter={this.onHoverEmoji} onMouseLeave={this.onHoverEmojiEnd} />
))}
</div>
{this.state.previewEmoji
? <Preview emoji={this.state.previewEmoji} />
: <QuickReactions onClick={this.onClickEmoji} /> }
</div>
)
}
}
export default EmojiPicker;

View file

@ -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 (
<nav className="mx_EmojiPicker_header">
{this.props.categories.map(category => (
<button key={category.id} className={`mx_EmojiPicker_anchor ${
this.state.selected === category.id ? 'mx_EmojiPicker_anchor_selected' : ''}`}
onClick={this.handleClick} data-category-id={category.id} title={category.name}>
{icons.categories[category.id]()}
</button>
))}
</nav>
)
}
}
export default Header;

View file

@ -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 (
<div className="mx_EmojiPicker_footer mx_EmojiPicker_preview">
<div className="mx_EmojiPicker_preview_emoji">
{this.props.emoji.unicode}
</div>
<div className="mx_EmojiPicker_preview_text">
<div className="mx_EmojiPicker_name mx_EmojiPicker_preview_name">
{this.props.emoji.annotation}
</div>
<div className="mx_EmojiPicker_shortcode">
{this.props.emoji.shortcodes[0]}
</div>
</div>
</div>
)
}
}
export default Preview;

View file

@ -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 (
<section className="mx_EmojiPicker_footer mx_EmojiPicker_quick mx_EmojiPicker_category">
<h2 className="mx_EmojiPicker_quick_header mx_EmojiPicker_category_label">
{!this.state.hover
? _t("Quick Reactions")
: <React.Fragment>
<span className="mx_EmojiPicker_name">{this.state.hover.annotation}</span>
<span className="mx_EmojiPicker_shortcode">{this.state.hover.shortcodes[0]}</span>
</React.Fragment>
}
</h2>
<ul className="mx_EmojiPicker_list">
{QUICK_REACTIONS.map(emoji => <Emoji
key={emoji.hexcode} emoji={emoji} onClick={this.props.onClick}
onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}/>)}
</ul>
</section>
)
}
}
export default QuickReactions;

View file

@ -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 (
<div className="mx_EmojiPicker_search">
<input type="text" placeholder="Search" value={this.props.query} onChange={this.props.onChange}/>
{icons.search.search()}
</div>
)
}
}
export default Search;

View file

@ -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: () => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
>
<path d="M12 0C5.373 0 0 5.372 0 12c0 6.627 5.373 12 12 12 6.628 0 12-5.373 12-12 0-6.628-5.372-12-12-12m9.949 11H17.05c.224-2.527 1.232-4.773 1.968-6.113A9.966 9.966 0 0 1 21.949 11M13 11V2.051a9.945 9.945 0 0 1 4.432 1.564c-.858 1.491-2.156 4.22-2.392 7.385H13zm-2 0H8.961c-.238-3.165-1.536-5.894-2.393-7.385A9.95 9.95 0 0 1 11 2.051V11zm0 2v8.949a9.937 9.937 0 0 1-4.432-1.564c.857-1.492 2.155-4.221 2.393-7.385H11zm4.04 0c.236 3.164 1.534 5.893 2.392 7.385A9.92 9.92 0 0 1 13 21.949V13h2.04zM4.982 4.887C5.718 6.227 6.726 8.473 6.951 11h-4.9a9.977 9.977 0 0 1 2.931-6.113M2.051 13h4.9c-.226 2.527-1.233 4.771-1.969 6.113A9.972 9.972 0 0 1 2.051 13m16.967 6.113c-.735-1.342-1.744-3.586-1.968-6.113h4.899a9.961 9.961 0 0 1-2.931 6.113" />
</svg>
),
custom: () => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
>
<g transform="translate(2.000000, 1.000000)">
<rect id="Rectangle" x="8" y="0" width="3" height="21" rx="1.5" />
<rect
id="Rectangle"
transform="translate(9.843, 10.549) rotate(60) translate(-9.843, -10.549) "
x="8.343"
y="0.049"
width="3"
height="21"
rx="1.5"
/>
<rect
id="Rectangle"
transform="translate(9.843, 10.549) rotate(-60) translate(-9.843, -10.549) "
x="8.343"
y="0.049"
width="3"
height="21"
rx="1.5"
/>
</g>
</svg>
),
flags: () => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
>
<path d="M0 0l6.084 24H8L1.916 0zM21 5h-4l-1-4H4l3 12h3l1 4h13L21 5zM6.563 3h7.875l2 8H8.563l-2-8zm8.832 10l-2.856 1.904L12.063 13h3.332zM19 13l-1.5-6h1.938l2 8H16l3-2z" />
</svg>
),
foods: () => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
>
<path d="M17 4.978c-1.838 0-2.876.396-3.68.934.513-1.172 1.768-2.934 4.68-2.934a1 1 0 0 0 0-2c-2.921 0-4.629 1.365-5.547 2.512-.064.078-.119.162-.18.244C11.73 1.838 10.798.023 9.207.023 8.579.022 7.85.306 7 .978 5.027 2.54 5.329 3.902 6.492 4.999 3.609 5.222 0 7.352 0 12.969c0 4.582 4.961 11.009 9 11.009 1.975 0 2.371-.486 3-1 .629.514 1.025 1 3 1 4.039 0 9-6.418 9-11 0-5.953-4.055-8-7-8M8.242 2.546c.641-.508.943-.523.965-.523.426.169.975 1.405 1.357 3.055-1.527-.629-2.741-1.352-2.98-1.846.059-.112.241-.356.658-.686M15 21.978c-1.08 0-1.21-.109-1.559-.402l-.176-.146c-.367-.302-.816-.452-1.266-.452s-.898.15-1.266.452l-.176.146c-.347.292-.477.402-1.557.402-2.813 0-7-5.389-7-9.009 0-5.823 4.488-5.991 5-5.991 1.939 0 2.484.471 3.387 1.251l.323.276a1.995 1.995 0 0 0 2.58 0l.323-.276c.902-.78 1.447-1.251 3.387-1.251.512 0 5 .168 5 6 0 3.617-4.187 9-7 9" />
</svg>
),
nature: () => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
>
<path d="M15.5 8a1.5 1.5 0 1 0 .001 3.001A1.5 1.5 0 0 0 15.5 8M8.5 8a1.5 1.5 0 1 0 .001 3.001A1.5 1.5 0 0 0 8.5 8" />
<path d="M18.933 0h-.027c-.97 0-2.138.787-3.018 1.497-1.274-.374-2.612-.51-3.887-.51-1.285 0-2.616.133-3.874.517C7.245.79 6.069 0 5.093 0h-.027C3.352 0 .07 2.67.002 7.026c-.039 2.479.276 4.238 1.04 5.013.254.258.882.677 1.295.882.191 3.177.922 5.238 2.536 6.38.897.637 2.187.949 3.2 1.102C8.04 20.6 8 20.795 8 21c0 1.773 2.35 3 4 3 1.648 0 4-1.227 4-3 0-.201-.038-.393-.072-.586 2.573-.385 5.435-1.877 5.925-7.587.396-.22.887-.568 1.104-.788.763-.774 1.079-2.534 1.04-5.013C23.929 2.67 20.646 0 18.933 0M3.223 9.135c-.237.281-.837 1.155-.884 1.238-.15-.41-.368-1.349-.337-3.291.051-3.281 2.478-4.972 3.091-5.031.256.015.731.27 1.265.646-1.11 1.171-2.275 2.915-2.352 5.125-.133.546-.398.858-.783 1.313M12 22c-.901 0-1.954-.693-2-1 0-.654.475-1.236 1-1.602V20a1 1 0 1 0 2 0v-.602c.524.365 1 .947 1 1.602-.046.307-1.099 1-2 1m3-3.48v.02a4.752 4.752 0 0 0-1.262-1.02c1.092-.516 2.239-1.334 2.239-2.217 0-1.842-1.781-2.195-3.977-2.195-2.196 0-3.978.354-3.978 2.195 0 .883 1.148 1.701 2.238 2.217A4.8 4.8 0 0 0 9 18.539v-.025c-1-.076-2.182-.281-2.973-.842-1.301-.92-1.838-3.045-1.853-6.478l.023-.041c.496-.826 1.49-1.45 1.804-3.102 0-2.047 1.357-3.631 2.362-4.522C9.37 3.178 10.555 3 11.948 3c1.447 0 2.685.192 3.733.57 1 .9 2.316 2.465 2.316 4.48.313 1.651 1.307 2.275 1.803 3.102.035.058.068.117.102.178-.059 5.967-1.949 7.01-4.902 7.19m6.628-8.202c-.037-.065-.074-.13-.113-.195a7.587 7.587 0 0 0-.739-.987c-.385-.455-.648-.768-.782-1.313-.076-2.209-1.241-3.954-2.353-5.124.531-.376 1.004-.63 1.261-.647.636.071 3.044 1.764 3.096 5.031.027 1.81-.347 3.218-.37 3.235" />
</svg>
),
objects: () => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
>
<path d="M12 0a9 9 0 0 0-5 16.482V21s2.035 3 5 3 5-3 5-3v-4.518A9 9 0 0 0 12 0zm0 2c3.86 0 7 3.141 7 7s-3.14 7-7 7-7-3.141-7-7 3.14-7 7-7zM9 17.477c.94.332 1.946.523 3 .523s2.06-.19 3-.523v.834c-.91.436-1.925.689-3 .689a6.924 6.924 0 0 1-3-.69v-.833zm.236 3.07A8.854 8.854 0 0 0 12 21c.965 0 1.888-.167 2.758-.451C14.155 21.173 13.153 22 12 22c-1.102 0-2.117-.789-2.764-1.453z" />
<path d="M14.745 12.449h-.004c-.852-.024-1.188-.858-1.577-1.824-.421-1.061-.703-1.561-1.182-1.566h-.009c-.481 0-.783.497-1.235 1.537-.436.982-.801 1.811-1.636 1.791l-.276-.043c-.565-.171-.853-.691-1.284-1.794-.125-.313-.202-.632-.27-.913-.051-.213-.127-.53-.195-.634C7.067 9.004 7.039 9 6.99 9A1 1 0 0 1 7 7h.01c1.662.017 2.015 1.373 2.198 2.134.486-.981 1.304-2.058 2.797-2.075 1.531.018 2.28 1.153 2.731 2.141l.002-.008C14.944 8.424 15.327 7 16.979 7h.032A1 1 0 1 1 17 9h-.011c-.149.076-.256.474-.319.709a6.484 6.484 0 0 1-.311.951c-.429.973-.79 1.789-1.614 1.789" />
</svg>
),
people: () => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
>
<path d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0m0 22C6.486 22 2 17.514 2 12S6.486 2 12 2s10 4.486 10 10-4.486 10-10 10" />
<path d="M8 7a2 2 0 1 0-.001 3.999A2 2 0 0 0 8 7M16 7a2 2 0 1 0-.001 3.999A2 2 0 0 0 16 7M15.232 15c-.693 1.195-1.87 2-3.349 2-1.477 0-2.655-.805-3.347-2H15m3-2H6a6 6 0 1 0 12 0" />
</svg>
),
places: () => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
>
<path d="M6.5 12C5.122 12 4 13.121 4 14.5S5.122 17 6.5 17 9 15.879 9 14.5 7.878 12 6.5 12m0 3c-.275 0-.5-.225-.5-.5s.225-.5.5-.5.5.225.5.5-.225.5-.5.5M17.5 12c-1.378 0-2.5 1.121-2.5 2.5s1.122 2.5 2.5 2.5 2.5-1.121 2.5-2.5-1.122-2.5-2.5-2.5m0 3c-.275 0-.5-.225-.5-.5s.225-.5.5-.5.5.225.5.5-.225.5-.5.5" />
<path d="M22.482 9.494l-1.039-.346L21.4 9h.6c.552 0 1-.439 1-.992 0-.006-.003-.008-.003-.008H23c0-1-.889-2-1.984-2h-.642l-.731-1.717C19.262 3.012 18.091 2 16.764 2H7.236C5.909 2 4.738 3.012 4.357 4.283L3.626 6h-.642C1.889 6 1 7 1 8h.003S1 8.002 1 8.008C1 8.561 1.448 9 2 9h.6l-.043.148-1.039.346a2.001 2.001 0 0 0-1.359 2.097l.751 7.508a1 1 0 0 0 .994.901H3v1c0 1.103.896 2 2 2h2c1.104 0 2-.897 2-2v-1h6v1c0 1.103.896 2 2 2h2c1.104 0 2-.897 2-2v-1h1.096a.999.999 0 0 0 .994-.901l.751-7.508a2.001 2.001 0 0 0-1.359-2.097M6.273 4.857C6.402 4.43 6.788 4 7.236 4h9.527c.448 0 .834.43.963.857L19.313 9H4.688l1.585-4.143zM7 21H5v-1h2v1zm12 0h-2v-1h2v1zm2.189-3H2.811l-.662-6.607L3 11h18l.852.393L21.189 18z" />
</svg>
),
recent: () => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
>
<path d="M13 4h-2l-.001 7H9v2h2v2h2v-2h4v-2h-4z" />
<path d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0m0 22C6.486 22 2 17.514 2 12S6.486 2 12 2s10 4.486 10 10-4.486 10-10 10" />
</svg>
),
symbols: () => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
>
<path d="M0 0h11v2H0zM4 11h3V6h4V4H0v2h4zM15.5 17c1.381 0 2.5-1.116 2.5-2.493s-1.119-2.493-2.5-2.493S13 13.13 13 14.507 14.119 17 15.5 17m0-2.986c.276 0 .5.222.5.493 0 .272-.224.493-.5.493s-.5-.221-.5-.493.224-.493.5-.493M21.5 19.014c-1.381 0-2.5 1.116-2.5 2.493S20.119 24 21.5 24s2.5-1.116 2.5-2.493-1.119-2.493-2.5-2.493m0 2.986a.497.497 0 0 1-.5-.493c0-.271.224-.493.5-.493s.5.222.5.493a.497.497 0 0 1-.5.493M22 13l-9 9 1.513 1.5 8.99-9.009zM17 11c2.209 0 4-1.119 4-2.5V2s.985-.161 1.498.949C23.01 4.055 23 6 23 6s1-1.119 1-3.135C24-.02 21 0 21 0h-2v6.347A5.853 5.853 0 0 0 17 6c-2.209 0-4 1.119-4 2.5s1.791 2.5 4 2.5M10.297 20.482l-1.475-1.585a47.54 47.54 0 0 1-1.442 1.129c-.307-.288-.989-1.016-2.045-2.183.902-.836 1.479-1.466 1.729-1.892s.376-.871.376-1.336c0-.592-.273-1.178-.818-1.759-.546-.581-1.329-.871-2.349-.871-1.008 0-1.79.293-2.344.879-.556.587-.832 1.181-.832 1.784 0 .813.419 1.748 1.256 2.805-.847.614-1.444 1.208-1.794 1.784a3.465 3.465 0 0 0-.523 1.833c0 .857.308 1.56.924 2.107.616.549 1.423.823 2.42.823 1.173 0 2.444-.379 3.813-1.137L8.235 24h2.819l-2.09-2.383 1.333-1.135zm-6.736-6.389a1.02 1.02 0 0 1 .73-.286c.31 0 .559.085.747.254a.849.849 0 0 1 .283.659c0 .518-.419 1.112-1.257 1.784-.536-.651-.805-1.231-.805-1.742a.901.901 0 0 1 .302-.669M3.74 22c-.427 0-.778-.116-1.057-.349-.279-.232-.418-.487-.418-.766 0-.594.509-1.288 1.527-2.083.968 1.134 1.717 1.946 2.248 2.438-.921.507-1.686.76-2.3.76" />
</svg>
),
}
const search = {
search: () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="13"
height="13"
viewBox="0 0 20 20"
opacity="0.5"
>
<path d="M12.9 14.32a8 8 0 1 1 1.41-1.41l5.35 5.33-1.42 1.42-5.33-5.34zM8 14A6 6 0 1 0 8 2a6 6 0 0 0 0 12z" />
</svg>
),
delete: () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="13"
height="13"
viewBox="0 0 20 20"
opacity="0.5"
>
<path d="M10 8.586L2.929 1.515 1.515 2.929 8.586 10l-7.071 7.071 1.414 1.414L10 11.414l7.071 7.071 1.414-1.414L11.414 10l7.071-7.071-1.414-1.414L10 8.586z" />
</svg>
),
}
export { categories, search }

View file

@ -25,6 +25,7 @@ import Modal from '../../../Modal';
import { createMenu } from '../../structures/ContextualMenu'; import { createMenu } from '../../structures/ContextualMenu';
import { isContentActionable, canEditContent } from '../../../utils/EventUtils'; import { isContentActionable, canEditContent } from '../../../utils/EventUtils';
import {RoomContext} from "../../structures/RoomView"; import {RoomContext} from "../../structures/RoomView";
import MatrixClientPeg from '../../../MatrixClientPeg';
export default class MessageActionBar extends React.PureComponent { export default class MessageActionBar extends React.PureComponent {
static propTypes = { 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) => { onOptionsClick = (ev) => {
const MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu'); const MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu');
const buttonRect = ev.target.getBoundingClientRect(); const buttonRect = ev.target.getBoundingClientRect();
@ -128,17 +168,6 @@ export default class MessageActionBar extends React.PureComponent {
this.onFocusChange(true); this.onFocusChange(true);
}; };
renderReactButton() {
const ReactMessageAction = sdk.getComponent('messages.ReactMessageAction');
const { mxEvent, reactions } = this.props;
return <ReactMessageAction
mxEvent={mxEvent}
reactions={reactions}
onFocusChange={this.onFocusChange}
/>;
}
render() { render() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
@ -148,7 +177,11 @@ export default class MessageActionBar extends React.PureComponent {
if (isContentActionable(this.props.mxEvent)) { if (isContentActionable(this.props.mxEvent)) {
if (this.context.room.canReact) { if (this.context.room.canReact) {
reactButton = this.renderReactButton(); reactButton = <AccessibleButton
className="mx_MessageActionBar_maskButton mx_MessageActionBar_reactButton"
title={_t("React")}
onClick={this.onReactClick}
/>;
} }
if (this.context.room.canReply) { if (this.context.room.canReply) {
replyButton = <AccessibleButton replyButton = <AccessibleButton

View file

@ -1,97 +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 sdk from '../../../index';
export default class ReactMessageAction extends React.PureComponent {
static propTypes = {
mxEvent: PropTypes.object.isRequired,
// The Relations model from the JS SDK for reactions to `mxEvent`
reactions: PropTypes.object,
onFocusChange: PropTypes.func,
}
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);
}
}
onFocusChange = (focused) => {
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 = <ReactionsQuickTooltip
mxEvent={mxEvent}
reactions={reactions}
/>;
return <InteractiveTooltip
content={content}
onVisibilityChange={this.onFocusChange}
>
<span className="mx_MessageActionBar_maskButton mx_MessageActionBar_reactButton" />
</InteractiveTooltip>;
}
}

View file

@ -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 <span className={classes}
data-key={content}
title={this.props.title}
aria-hidden={true}
onClick={this.onClick}
>
{content}
</span>;
}
}

View file

@ -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 <ReactionTooltipButton
key={content}
content={content}
title={title}
mxEvent={mxEvent}
myReactionEvent={myReactionEvent}
/>;
});
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 = <span className="mx_ReactionsQuickTooltip_shortcode">
{shortcode}
</span>;
}
label = <div className="mx_ReactionsQuickTooltip_label">
<span className="mx_ReactionsQuickTooltip_title">
{title}
</span>
{shortcodeLabel}
</div>;
}
return <div className="mx_ReactionsQuickTooltip"
onMouseOver={this.onMouseOver}
onMouseOut={this.onMouseOut}
>
<div className="mx_ReactionsQuickTooltip_buttons">
{buttons}
</div>
{label}
</div>;
}
}

View file

@ -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.", "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 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 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"
} }