Merge pull request #3554 from maunium/emoji-picker
Add full emoji picker for reactions
|
@ -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";
|
||||
|
|
221
res/css/views/emojipicker/_EmojiPicker.scss
Normal file
|
@ -0,0 +1,221 @@
|
|||
/*
|
||||
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;
|
||||
|
||||
width: 36px;
|
||||
height: 38px;
|
||||
|
||||
&:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:not(:disabled):hover {
|
||||
background-color: $focus-bg-color;
|
||||
border-bottom: 2px solid $button-bg-color;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
button {
|
||||
border: none;
|
||||
background-color: inherit;
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
align-self: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_EmojiPicker_search_clear {
|
||||
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;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mx_EmojiPicker_category_label {
|
||||
width: 304px;
|
||||
}
|
||||
|
||||
.mx_EmojiPicker_list {
|
||||
width: 304px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.mx_EmojiPicker_item_wrapper {
|
||||
display: inline-block;
|
||||
list-style: none;
|
||||
width: 38px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mx_EmojiPicker_item {
|
||||
display: inline-block;
|
||||
font-size: 20px;
|
||||
padding: 5px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background-color: $focus-bg-color;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_EmojiPicker_item_selected {
|
||||
color: rgba(0, 0, 0, .5);
|
||||
border: 1px solid $input-valid-border-color;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.mx_EmojiPicker_category_label, .mx_EmojiPicker_preview_name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
14
res/img/emojipicker/activity.svg
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
<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>
|
After Width: | Height: | Size: 1.1 KiB |
34
res/img/emojipicker/custom.svg
Normal file
|
@ -0,0 +1,34 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
<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>
|
After Width: | Height: | Size: 997 B |
15
res/img/emojipicker/delete.svg
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
<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>
|
After Width: | Height: | Size: 541 B |
14
res/img/emojipicker/flags.svg
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
<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>
|
After Width: | Height: | Size: 537 B |
14
res/img/emojipicker/foods.svg
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
<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>
|
After Width: | Height: | Size: 1.2 KiB |
15
res/img/emojipicker/nature.svg
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
<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>
|
After Width: | Height: | Size: 2 KiB |
15
res/img/emojipicker/objects.svg
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
<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>
|
After Width: | Height: | Size: 1.3 KiB |
15
res/img/emojipicker/people.svg
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
<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>
|
After Width: | Height: | Size: 705 B |
15
res/img/emojipicker/places.svg
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
<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>
|
After Width: | Height: | Size: 1.3 KiB |
15
res/img/emojipicker/recent.svg
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
<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>
|
After Width: | Height: | Size: 575 B |
15
res/img/emojipicker/search.svg
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
<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>
|
After Width: | Height: | Size: 493 B |
14
res/img/emojipicker/symbols.svg
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
<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>
|
After Width: | Height: | Size: 1.8 KiB |
54
src/components/views/emojipicker/Category.js
Normal file
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
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,
|
||||
id: PropTypes.string.isRequired,
|
||||
onMouseEnter: PropTypes.func.isRequired,
|
||||
onMouseLeave: PropTypes.func.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
selectedEmojis: PropTypes.instanceOf(Set),
|
||||
};
|
||||
|
||||
render() {
|
||||
const { onClick, onMouseEnter, onMouseLeave, emojis, name, selectedEmojis } = this.props;
|
||||
if (!emojis || emojis.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const Emoji = sdk.getComponent("emojipicker.Emoji");
|
||||
return (
|
||||
<section className="mx_EmojiPicker_category" data-category-id={this.props.id}>
|
||||
<h2 className="mx_EmojiPicker_category_label">
|
||||
{name}
|
||||
</h2>
|
||||
<ul className="mx_EmojiPicker_list">
|
||||
{emojis.map(emoji => <Emoji key={emoji.hexcode} emoji={emoji} selectedEmojis={selectedEmojis}
|
||||
onClick={onClick} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} />)}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Category;
|
45
src/components/views/emojipicker/Emoji.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
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,
|
||||
selectedEmojis: PropTypes.instanceOf(Set),
|
||||
};
|
||||
|
||||
render() {
|
||||
const { onClick, onMouseEnter, onMouseLeave, emoji, selectedEmojis } = this.props;
|
||||
const isSelected = selectedEmojis && selectedEmojis.has(emoji.unicode);
|
||||
return (
|
||||
<li onClick={() => onClick(emoji)}
|
||||
onMouseEnter={() => onMouseEnter(emoji)}
|
||||
onMouseLeave={() => onMouseLeave(emoji)}
|
||||
className="mx_EmojiPicker_item_wrapper">
|
||||
<div className={`mx_EmojiPicker_item ${isSelected ? 'mx_EmojiPicker_item_selected' : ''}`}>
|
||||
{emoji.unicode}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Emoji;
|
239
src/components/views/emojipicker/EmojiPicker.js
Normal file
|
@ -0,0 +1,239 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
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
|
||||
"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": [],
|
||||
};
|
||||
const DATA_BY_EMOJI = {};
|
||||
|
||||
EMOJIBASE.forEach(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 || ''}`;
|
||||
});
|
||||
|
||||
class EmojiPicker extends React.Component {
|
||||
static propTypes = {
|
||||
onChoose: PropTypes.func.isRequired,
|
||||
selectedEmojis: PropTypes.instanceOf(Set),
|
||||
showQuickReactions: PropTypes.bool,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
filter: "",
|
||||
previewEmoji: null,
|
||||
};
|
||||
|
||||
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,
|
||||
visible: this.recentlyUsed.length > 0,
|
||||
ref: React.createRef(),
|
||||
}, {
|
||||
id: "people",
|
||||
name: _t("Smileys & People"),
|
||||
enabled: true,
|
||||
visible: true,
|
||||
ref: React.createRef(),
|
||||
}, {
|
||||
id: "nature",
|
||||
name: _t("Animals & Nature"),
|
||||
enabled: true,
|
||||
visible: false,
|
||||
ref: React.createRef(),
|
||||
}, {
|
||||
id: "foods",
|
||||
name: _t("Food & Drink"),
|
||||
enabled: true,
|
||||
visible: false,
|
||||
ref: React.createRef(),
|
||||
}, {
|
||||
id: "activity",
|
||||
name: _t("Activities"),
|
||||
enabled: true,
|
||||
visible: false,
|
||||
ref: React.createRef(),
|
||||
}, {
|
||||
id: "places",
|
||||
name: _t("Travel & Places"),
|
||||
enabled: true,
|
||||
visible: false,
|
||||
ref: React.createRef(),
|
||||
}, {
|
||||
id: "objects",
|
||||
name: _t("Objects"),
|
||||
enabled: true,
|
||||
visible: false,
|
||||
ref: React.createRef(),
|
||||
}, {
|
||||
id: "symbols",
|
||||
name: _t("Symbols"),
|
||||
enabled: true,
|
||||
visible: false,
|
||||
ref: React.createRef(),
|
||||
}, {
|
||||
id: "flags",
|
||||
name: _t("Flags"),
|
||||
enabled: true,
|
||||
visible: false,
|
||||
ref: React.createRef(),
|
||||
}];
|
||||
|
||||
this.bodyRef = React.createRef();
|
||||
|
||||
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);
|
||||
this.updateVisibility = this.updateVisibility.bind(this);
|
||||
}
|
||||
|
||||
updateVisibility() {
|
||||
const rect = this.bodyRef.current.getBoundingClientRect();
|
||||
for (const cat of this.categories) {
|
||||
const elem = this.bodyRef.current.querySelector(`[data-category-id="${cat.id}"]`);
|
||||
if (!elem) {
|
||||
cat.visible = false;
|
||||
cat.ref.current.classList.remove("mx_EmojiPicker_anchor_visible");
|
||||
continue;
|
||||
}
|
||||
const elemRect = elem.getBoundingClientRect();
|
||||
const y = elemRect.y - rect.y;
|
||||
const yEnd = elemRect.y + elemRect.height - rect.y;
|
||||
cat.visible = y < rect.height && yEnd > 0;
|
||||
// We update this here instead of through React to avoid re-render on scroll.
|
||||
if (cat.visible) {
|
||||
cat.ref.current.classList.add("mx_EmojiPicker_anchor_visible");
|
||||
} else {
|
||||
cat.ref.current.classList.remove("mx_EmojiPicker_anchor_visible");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scrollToCategory(category) {
|
||||
this.bodyRef.current.querySelector(`[data-category-id="${category}"]`).scrollIntoView();
|
||||
}
|
||||
|
||||
onChangeFilter(filter) {
|
||||
for (const cat of this.categories) {
|
||||
let emojis;
|
||||
// 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 = this.memoizedDataByCategory[cat.id];
|
||||
} else {
|
||||
emojis = cat.id === "recent" ? this.recentlyUsed : DATA_BY_CATEGORY[cat.id];
|
||||
}
|
||||
emojis = emojis.filter(emoji => emoji.filterString.includes(filter));
|
||||
this.memoizedDataByCategory[cat.id] = emojis;
|
||||
cat.enabled = emojis.length > 0;
|
||||
// The setState below doesn't re-render the header and we already have the refs for updateVisibility, so...
|
||||
cat.ref.current.disabled = !cat.enabled;
|
||||
}
|
||||
this.setState({ filter });
|
||||
// Header underlines need to be updated, but updating requires knowing
|
||||
// where the categories are, so we wait for a tick.
|
||||
setTimeout(this.updateVisibility, 0);
|
||||
}
|
||||
|
||||
onHoverEmoji(emoji) {
|
||||
this.setState({
|
||||
previewEmoji: emoji,
|
||||
});
|
||||
}
|
||||
|
||||
onHoverEmojiEnd(emoji) {
|
||||
this.setState({
|
||||
previewEmoji: null,
|
||||
});
|
||||
}
|
||||
|
||||
onClickEmoji(emoji) {
|
||||
if (this.props.onChoose(emoji.unicode) !== false) {
|
||||
recent.add(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" ref={this.bodyRef} onScroll={this.updateVisibility}>
|
||||
{this.categories.map(category => (
|
||||
<Category key={category.id} id={category.id} name={category.name}
|
||||
emojis={this.memoizedDataByCategory[category.id]} onClick={this.onClickEmoji}
|
||||
onMouseEnter={this.onHoverEmoji} onMouseLeave={this.onHoverEmojiEnd}
|
||||
selectedEmojis={this.props.selectedEmojis} />
|
||||
))}
|
||||
</div>
|
||||
{this.state.previewEmoji || !this.props.showQuickReactions
|
||||
? <Preview emoji={this.state.previewEmoji} />
|
||||
: <QuickReactions onClick={this.onClickEmoji} selectedEmojis={this.props.selectedEmojis} /> }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default EmojiPicker;
|
41
src/components/views/emojipicker/Header.js
Normal file
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class Header extends React.PureComponent {
|
||||
static propTypes = {
|
||||
categories: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onAnchorClick: PropTypes.func.isRequired,
|
||||
refs: PropTypes.object,
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<nav className="mx_EmojiPicker_header">
|
||||
{this.props.categories.map(category => (
|
||||
<button disabled={!category.enabled} key={category.id} ref={category.ref}
|
||||
className={`mx_EmojiPicker_anchor ${category.visible ? 'mx_EmojiPicker_anchor_visible' : ''}
|
||||
mx_EmojiPicker_anchor_${category.id}`}
|
||||
onClick={() => this.props.onAnchorClick(category.id)} title={category.name} />
|
||||
))}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Header;
|
49
src/components/views/emojipicker/Preview.js
Normal file
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class Preview extends React.PureComponent {
|
||||
static propTypes = {
|
||||
emoji: PropTypes.object,
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
unicode = "",
|
||||
annotation = "",
|
||||
shortcodes: [shortcode = ""]
|
||||
} = this.props.emoji || {};
|
||||
return (
|
||||
<div className="mx_EmojiPicker_footer mx_EmojiPicker_preview">
|
||||
<div className="mx_EmojiPicker_preview_emoji">
|
||||
{unicode}
|
||||
</div>
|
||||
<div className="mx_EmojiPicker_preview_text">
|
||||
<div className="mx_EmojiPicker_name mx_EmojiPicker_preview_name">
|
||||
{annotation}
|
||||
</div>
|
||||
<div className="mx_EmojiPicker_shortcode">
|
||||
{shortcode}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Preview;
|
84
src/components/views/emojipicker/QuickReactions.js
Normal file
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
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,
|
||||
selectedEmojis: PropTypes.instanceOf(Set),
|
||||
};
|
||||
|
||||
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}
|
||||
selectedEmojis={this.props.selectedEmojis}/>)}
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default QuickReactions;
|
120
src/components/views/emojipicker/ReactionPicker.js
Normal file
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from "prop-types";
|
||||
import EmojiPicker from "./EmojiPicker";
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
|
||||
class ReactionPicker extends React.Component {
|
||||
static propTypes = {
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
closeMenu: PropTypes.func.isRequired,
|
||||
reactions: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
selectedEmojis: new Set(Object.keys(this.getReactions())),
|
||||
};
|
||||
this.onChoose = this.onChoose.bind(this);
|
||||
this.onReactionsChange = this.onReactionsChange.bind(this);
|
||||
this.addListeners();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.reactions !== this.props.reactions) {
|
||||
this.addListeners();
|
||||
this.onReactionsChange();
|
||||
}
|
||||
}
|
||||
|
||||
addListeners() {
|
||||
if (this.props.reactions) {
|
||||
this.props.reactions.on("Relations.add", this.onReactionsChange);
|
||||
this.props.reactions.on("Relations.remove", this.onReactionsChange);
|
||||
this.props.reactions.on("Relations.redaction", this.onReactionsChange);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.props.reactions) {
|
||||
this.props.reactions.removeListener(
|
||||
"Relations.add",
|
||||
this.onReactionsChange,
|
||||
);
|
||||
this.props.reactions.removeListener(
|
||||
"Relations.remove",
|
||||
this.onReactionsChange,
|
||||
);
|
||||
this.props.reactions.removeListener(
|
||||
"Relations.redaction",
|
||||
this.onReactionsChange,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
getReactions() {
|
||||
if (!this.props.reactions) {
|
||||
return {};
|
||||
}
|
||||
const userId = MatrixClientPeg.get().getUserId();
|
||||
const myAnnotations = this.props.reactions.getAnnotationsBySender()[userId] || [];
|
||||
return Object.fromEntries([...myAnnotations]
|
||||
.filter(event => !event.isRedacted())
|
||||
.map(event => [event.getRelation().key, event.getId()]));
|
||||
};
|
||||
|
||||
onReactionsChange() {
|
||||
this.setState({
|
||||
selectedEmojis: new Set(Object.keys(this.getReactions()))
|
||||
});
|
||||
}
|
||||
|
||||
onChoose(reaction) {
|
||||
this.componentWillUnmount();
|
||||
this.props.closeMenu();
|
||||
this.props.onFinished();
|
||||
const myReactions = this.getReactions();
|
||||
if (myReactions.hasOwnProperty(reaction)) {
|
||||
MatrixClientPeg.get().redactEvent(
|
||||
this.props.mxEvent.getRoomId(),
|
||||
myReactions[reaction],
|
||||
);
|
||||
// Tell the emoji picker not to bump this in the more frequently used list.
|
||||
return false;
|
||||
} else {
|
||||
MatrixClientPeg.get().sendEvent(this.props.mxEvent.getRoomId(), "m.reaction", {
|
||||
"m.relates_to": {
|
||||
"rel_type": "m.annotation",
|
||||
"event_id": this.props.mxEvent.getId(),
|
||||
"key": reaction,
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <EmojiPicker onChoose={this.onChoose} selectedEmojis={this.state.selectedEmojis}
|
||||
showQuickReactions={true}/>
|
||||
}
|
||||
}
|
||||
|
||||
export default ReactionPicker
|
50
src/components/views/emojipicker/Search.js
Normal file
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
class Search extends React.PureComponent {
|
||||
static propTypes = {
|
||||
query: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.inputRef = React.createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// For some reason, neither the autoFocus nor just calling focus() here worked, so here's a setTimeout
|
||||
setTimeout(() => this.inputRef.current.focus(), 0);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="mx_EmojiPicker_search">
|
||||
<input autoFocus type="text" placeholder="Search" value={this.props.query}
|
||||
onChange={ev => this.props.onChange(ev.target.value)} ref={this.inputRef} />
|
||||
<button onClick={() => this.props.onChange("")}
|
||||
className={`mx_EmojiPicker_search_icon ${this.props.query ? "mx_EmojiPicker_search_clear" : ""}`}
|
||||
title={this.props.query ? _t("Cancel search") : _t("Search")} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Search;
|
35
src/components/views/emojipicker/recent.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
const REACTION_COUNT = JSON.parse(window.localStorage.mx_reaction_count || '{}');
|
||||
let sorted = null;
|
||||
|
||||
export function add(emoji) {
|
||||
const [count] = REACTION_COUNT[emoji] || [0];
|
||||
REACTION_COUNT[emoji] = [count + 1, Date.now()];
|
||||
window.localStorage.mx_reaction_count = JSON.stringify(REACTION_COUNT);
|
||||
sorted = null;
|
||||
}
|
||||
|
||||
export function get(limit = 24) {
|
||||
if (sorted === null) {
|
||||
sorted = Object.entries(REACTION_COUNT)
|
||||
.sort(([, [count1, date1]], [, [count2, date2]]) =>
|
||||
count2 === count1 ? date2 - date1 : count2 - count1)
|
||||
.map(([emoji, count]) => emoji);
|
||||
}
|
||||
return sorted.slice(0, limit);
|
||||
}
|
|
@ -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,31 +85,9 @@ export default class MessageActionBar extends React.PureComponent {
|
|||
});
|
||||
};
|
||||
|
||||
onOptionsClick = (ev) => {
|
||||
const MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu');
|
||||
getMenuOptions = (ev) => {
|
||||
const menuOptions = {};
|
||||
const buttonRect = ev.target.getBoundingClientRect();
|
||||
|
||||
const { getTile, getReplyThread } = this.props;
|
||||
const tile = getTile && getTile();
|
||||
const replyThread = getReplyThread && getReplyThread();
|
||||
|
||||
let e2eInfoCallback = null;
|
||||
if (this.props.mxEvent.isEncrypted()) {
|
||||
e2eInfoCallback = () => this.onCryptoClick();
|
||||
}
|
||||
|
||||
const menuOptions = {
|
||||
mxEvent: this.props.mxEvent,
|
||||
chevronFace: "none",
|
||||
permalinkCreator: this.props.permalinkCreator,
|
||||
eventTileOps: tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined,
|
||||
collapseReplyThread: replyThread && replyThread.canCollapse() ? replyThread.collapse : undefined,
|
||||
e2eInfoCallback: e2eInfoCallback,
|
||||
onFinished: () => {
|
||||
this.onFocusChange(false);
|
||||
},
|
||||
};
|
||||
|
||||
// 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;
|
||||
|
@ -122,23 +101,55 @@ 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(ReactionPicker, menuOptions);
|
||||
|
||||
this.onFocusChange(true);
|
||||
};
|
||||
|
||||
onOptionsClick = (ev) => {
|
||||
const MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu');
|
||||
|
||||
const { getTile, getReplyThread } = this.props;
|
||||
const tile = getTile && getTile();
|
||||
const replyThread = getReplyThread && getReplyThread();
|
||||
|
||||
let e2eInfoCallback = null;
|
||||
if (this.props.mxEvent.isEncrypted()) {
|
||||
e2eInfoCallback = () => this.onCryptoClick();
|
||||
}
|
||||
|
||||
const menuOptions = {
|
||||
...this.getMenuOptions(ev),
|
||||
mxEvent: this.props.mxEvent,
|
||||
chevronFace: "none",
|
||||
permalinkCreator: this.props.permalinkCreator,
|
||||
eventTileOps: tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined,
|
||||
collapseReplyThread: replyThread && replyThread.canCollapse() ? replyThread.collapse : undefined,
|
||||
e2eInfoCallback: e2eInfoCallback,
|
||||
onFinished: () => {
|
||||
this.onFocusChange(false);
|
||||
},
|
||||
};
|
||||
|
||||
createMenu(MessageContextMenu, menuOptions);
|
||||
|
||||
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() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
|
@ -148,7 +159,11 @@ export default class MessageActionBar extends React.PureComponent {
|
|||
|
||||
if (isContentActionable(this.props.mxEvent)) {
|
||||
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) {
|
||||
replyButton = <AccessibleButton
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
}
|
|
@ -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>;
|
||||
}
|
||||
}
|
|
@ -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>;
|
||||
}
|
||||
}
|
|
@ -1836,5 +1836,17 @@
|
|||
"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",
|
||||
"React": "React",
|
||||
"Cancel search": "Cancel search"
|
||||
}
|
||||
|
|