Improve design of the rich text editor (#9533)
New design for rich text composer
This commit is contained in:
parent
9101b42de8
commit
5ca9accce2
31 changed files with 668 additions and 270 deletions
|
@ -260,6 +260,7 @@
|
||||||
@import "./views/rooms/_AuxPanel.pcss";
|
@import "./views/rooms/_AuxPanel.pcss";
|
||||||
@import "./views/rooms/_BasicMessageComposer.pcss";
|
@import "./views/rooms/_BasicMessageComposer.pcss";
|
||||||
@import "./views/rooms/_E2EIcon.pcss";
|
@import "./views/rooms/_E2EIcon.pcss";
|
||||||
|
@import "./views/rooms/_EmojiButton.pcss";
|
||||||
@import "./views/rooms/_EditMessageComposer.pcss";
|
@import "./views/rooms/_EditMessageComposer.pcss";
|
||||||
@import "./views/rooms/_EntityTile.pcss";
|
@import "./views/rooms/_EntityTile.pcss";
|
||||||
@import "./views/rooms/_EventBubbleTile.pcss";
|
@import "./views/rooms/_EventBubbleTile.pcss";
|
||||||
|
|
35
res/css/views/rooms/_EmojiButton.pcss
Normal file
35
res/css/views/rooms/_EmojiButton.pcss
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 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 "./_MessageComposerButton.pcss";
|
||||||
|
|
||||||
|
.mx_EmojiButton {
|
||||||
|
@mixin composerButton 50%,$accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EmojiButton_highlight {
|
||||||
|
@mixin composerButtonHighLight;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EmojiButton_icon::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/room/composer/emoji.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MessageComposer_wysiwyg {
|
||||||
|
.mx_EmojiButton {
|
||||||
|
@mixin composerButton 5px,$tertiary-content;
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,6 +15,8 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@import "./_MessageComposerButton.pcss";
|
||||||
|
|
||||||
.mx_MessageComposer_wrapper {
|
.mx_MessageComposer_wrapper {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
@ -59,6 +61,12 @@ limitations under the License.
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_MessageComposer_actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_MessageComposer .mx_MessageComposer_avatar {
|
.mx_MessageComposer .mx_MessageComposer_avatar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 26px;
|
left: 26px;
|
||||||
|
@ -171,53 +179,16 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MessageComposer_button_highlight {
|
.mx_MessageComposer_button_highlight {
|
||||||
background: rgba($accent, 0.25);
|
@mixin composerButtonHighLight;
|
||||||
/* make the icon the accent color too */
|
|
||||||
&::before {
|
|
||||||
background-color: $accent !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MessageComposer_button {
|
.mx_MessageComposer_button {
|
||||||
--size: 26px;
|
@mixin composerButton 50%,$accent;
|
||||||
position: relative;
|
|
||||||
cursor: pointer;
|
|
||||||
height: var(--size);
|
|
||||||
line-height: var(--size);
|
|
||||||
width: auto;
|
|
||||||
padding-left: var(--size);
|
|
||||||
border-radius: 50%;
|
|
||||||
margin-right: 6px;
|
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 3px;
|
|
||||||
left: 3px;
|
|
||||||
height: 20px;
|
|
||||||
width: 20px;
|
|
||||||
background-color: $icon-button-color;
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
mask-size: contain;
|
|
||||||
mask-position: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
z-index: 0;
|
|
||||||
width: var(--size);
|
|
||||||
height: var(--size);
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&.mx_MessageComposer_closeButtonMenu {
|
&.mx_MessageComposer_closeButtonMenu {
|
||||||
&::after {
|
&::after {
|
||||||
background: rgba($accent, 0.1);
|
background: rgba($accent, 0.1);
|
||||||
|
@ -232,15 +203,43 @@ limitations under the License.
|
||||||
background-color: $alert;
|
background-color: $alert;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
The wysisyg composer increase the size of the MessageComposer. We temporary move the buttons
|
|
||||||
Soon the dom structure of the MessageComposer will change with the next evolution of the wysiwyg composer
|
|
||||||
and this workaround will disappear
|
|
||||||
*/
|
|
||||||
.mx_MessageComposer_wysiwyg {
|
.mx_MessageComposer_wysiwyg {
|
||||||
.mx_MessageComposer_e2eIcon.mx_E2EIcon,.mx_MessageComposer_button, .mx_MessageComposer_sendMessage {
|
.mx_MessageComposer_wrapper {
|
||||||
margin-top: 28px;
|
padding-left: 16px;
|
||||||
|
margin-top: 6px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MessageComposer_row {
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MessageComposer_actions {
|
||||||
|
/* Height of the composer editor */
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MediaBody {
|
||||||
|
padding-top: 4px;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MessageComposer_button {
|
||||||
|
@mixin composerButton 5px,$tertiary-content;
|
||||||
|
|
||||||
|
&.mx_MessageComposer_closeButtonMenu {
|
||||||
|
&::after {
|
||||||
|
background: rgba($accent, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
background-color: $accent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_MessageComposer_hangup:not(.mx_AccessibleButton_disabled)::before {
|
||||||
|
background-color: $alert;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -260,10 +259,6 @@ limitations under the License.
|
||||||
mask-image: url('$(res)/img/element-icons/live.svg');
|
mask-image: url('$(res)/img/element-icons/live.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MessageComposer_emoji::before {
|
|
||||||
mask-image: url('$(res)/img/element-icons/room/composer/emoji.svg');
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_MessageComposer_plain_text::before {
|
.mx_MessageComposer_plain_text::before {
|
||||||
mask-image: url('$(res)/img/element-icons/room/composer/plain_text.svg');
|
mask-image: url('$(res)/img/element-icons/room/composer/plain_text.svg');
|
||||||
}
|
}
|
||||||
|
|
68
res/css/views/rooms/_MessageComposerButton.pcss
Normal file
68
res/css/views/rooms/_MessageComposerButton.pcss
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@define-mixin composerButtonHighLight {
|
||||||
|
background: rgba($accent, 0.25);
|
||||||
|
/* make the icon the accent color too */
|
||||||
|
&::before {
|
||||||
|
background-color: $accent !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@define-mixin composerButton $border-radius,$hover-color {
|
||||||
|
--size: 26px;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
height: var(--size);
|
||||||
|
line-height: var(--size);
|
||||||
|
width: auto;
|
||||||
|
padding-left: var(--size);
|
||||||
|
border-radius: $border-radius;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 3px;
|
||||||
|
left: 3px;
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
background-color: $icon-button-color;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-size: contain;
|
||||||
|
mask-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
z-index: 0;
|
||||||
|
width: var(--size);
|
||||||
|
height: var(--size);
|
||||||
|
border-radius: $border-radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
&::after {
|
||||||
|
background: rgba($hover-color, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
background-color: $hover-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,7 +20,7 @@ limitations under the License.
|
||||||
height: 28px;
|
height: 28px;
|
||||||
border: 2px solid $voice-record-stop-border-color;
|
border: 2px solid $voice-record-stop-border-color;
|
||||||
border-radius: 32px;
|
border-radius: 32px;
|
||||||
margin-right: 8px; /* between us and the waveform component */
|
margin-right: 2px; /* between us and the waveform component */
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
|
@ -39,7 +39,7 @@ limitations under the License.
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
margin-right: 8px; /* distance from left edge of waveform container (container has some margin too) */
|
margin-right: 2px; /* distance from left edge of waveform container (container has some margin too) */
|
||||||
background-color: $voice-record-icon-color;
|
background-color: $voice-record-icon-color;
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
mask-size: contain;
|
mask-size: contain;
|
||||||
|
@ -69,7 +69,7 @@ limitations under the License.
|
||||||
height: 32px;
|
height: 32px;
|
||||||
|
|
||||||
margin: 6px; /* force the composer area to put a gutter around us */
|
margin: 6px; /* force the composer area to put a gutter around us */
|
||||||
margin-right: 12px; /* isolate from stop/send button */
|
margin-right: 6px; /* isolate from stop/send button */
|
||||||
|
|
||||||
position: relative; /* important for the live circle */
|
position: relative; /* important for the live circle */
|
||||||
|
|
||||||
|
@ -93,6 +93,14 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_MessageComposer_wysiwyg .mx_VoiceMessagePrimaryContainer {
|
||||||
|
&.mx_VoiceRecordComposerTile_recording {
|
||||||
|
&::before {
|
||||||
|
top: 15px; /* vertically center (middle align with clock) */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* The keyframes are slightly weird here to help make a ramping/punch effect */
|
/* The keyframes are slightly weird here to help make a ramping/punch effect */
|
||||||
/* for the recording dot. We start and end at 100% opacity to help make the */
|
/* for the recording dot. We start and end at 100% opacity to help make the */
|
||||||
/* dot feel a bit like a real lamp that is blinking: the animation ends up */
|
/* dot feel a bit like a real lamp that is blinking: the animation ends up */
|
||||||
|
|
|
@ -24,7 +24,7 @@ limitations under the License.
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 8px var(--EditWysiwygComposer-padding-inline);
|
padding: 8px var(--EditWysiwygComposer-padding-inline);
|
||||||
|
|
||||||
.mx_WysiwygComposer_content {
|
.mx_WysiwygComposer_Editor_content {
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: solid 1px $primary-hairline-color;
|
border: solid 1px $primary-hairline-color;
|
||||||
background-color: $background;
|
background-color: $background;
|
||||||
|
|
|
@ -22,32 +22,65 @@ limitations under the License.
|
||||||
/* fixed line height to prevent emoji from being taller than text */
|
/* fixed line height to prevent emoji from being taller than text */
|
||||||
line-height: $font-18px;
|
line-height: $font-18px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-right: 6px;
|
margin-right: 13px;
|
||||||
/* don't grow wider than available space */
|
gap: 8px;
|
||||||
min-width: 0;
|
|
||||||
|
|
||||||
.mx_WysiwygComposer_container {
|
.mx_FormattingButtons {
|
||||||
flex: 1;
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_WysiwygComposer_Editor {
|
||||||
|
border: 1px solid;
|
||||||
|
border-color: $quinary-content;
|
||||||
|
padding: 6px 11px 6px 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
align-items: flex-end;
|
||||||
/* min-height at this level so the mx_BasicMessageComposer_input */
|
gap: 10px;
|
||||||
/* still stays vertically centered when less than 55px. */
|
|
||||||
/* We also set this to ensure the voice message recording widget */
|
|
||||||
/* doesn't cause a jump. */
|
|
||||||
min-height: 55px;
|
|
||||||
|
|
||||||
.mx_WysiwygComposer_content {
|
.mx_E2EIcon {
|
||||||
border: 1px solid;
|
margin: 0 0 7px 0;
|
||||||
border-radius: 20px;
|
width: 12px;
|
||||||
padding: 8px 10px;
|
height: 12px;
|
||||||
/* this will center the contenteditable */
|
}
|
||||||
/* in it's parent vertically */
|
|
||||||
/* while keeping the autocomplete at the top */
|
&[data-is-expanded="true"] {
|
||||||
/* of the composer. The parent needs to be a flex container for this to work. */
|
border-radius: 14px;
|
||||||
margin: auto 0;
|
|
||||||
/* max-height at this level so autocomplete doesn't get scrolled too */
|
.mx_WysiwygComposer_Editor_container {
|
||||||
max-height: 140px;
|
margin-top: 3px;
|
||||||
overflow-y: auto;
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-is-expanded="false"] {
|
||||||
|
border-radius: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_WysiwygComposer_Editor_container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 22px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
/* don't grow wider than available space */
|
||||||
|
width: 0;
|
||||||
|
|
||||||
|
.mx_WysiwygComposer_Editor_content {
|
||||||
|
/* this will center the contenteditable */
|
||||||
|
/* in it's parent vertically */
|
||||||
|
/* while keeping the autocomplete at the top */
|
||||||
|
/* of the composer. The parent needs to be a flex container for this to work. */
|
||||||
|
margin: auto 0;
|
||||||
|
/* max-height at this level so autocomplete doesn't get scrolled too */
|
||||||
|
max-height: 140px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_SendWysiwygComposer-focused {
|
||||||
|
.mx_WysiwygComposer_Editor {
|
||||||
|
border-color: $quaternary-content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -14,15 +14,13 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_WysiwygComposer_container {
|
.mx_WysiwygComposer_Editor_container {
|
||||||
position: relative;
|
|
||||||
|
|
||||||
@keyframes visualbell {
|
@keyframes visualbell {
|
||||||
from { background-color: $visual-bell-bg-color; }
|
from { background-color: $visual-bell-bg-color; }
|
||||||
to { background-color: $background; }
|
to { background-color: $background; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_WysiwygComposer_content {
|
.mx_WysiwygComposer_Editor_content {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
.mx_FormattingButtons {
|
.mx_FormattingButtons {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
.mx_FormattingButtons_Button {
|
.mx_FormattingButtons_Button {
|
||||||
--size: 28px;
|
--size: 28px;
|
||||||
|
@ -26,18 +27,9 @@ limitations under the License.
|
||||||
line-height: var(--size);
|
line-height: var(--size);
|
||||||
width: auto;
|
width: auto;
|
||||||
padding-left: 22px;
|
padding-left: 22px;
|
||||||
margin-right: 8px;
|
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
margin-left: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
|
@ -1,10 +1,34 @@
|
||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<g clip-path="url(#clip0_1456_146350)">
|
<svg
|
||||||
<path d="M1 18.6667C1 19.4 1.6 20 2.33333 20H18.3333C19.0667 20 19.6667 19.4 19.6667 18.6667C19.6667 17.9333 19.0667 17.3333 18.3333 17.3333H2.33333C1.6 17.3333 1 17.9333 1 18.6667ZM7 11.7333H13.6667L14.5467 13.8667C14.7467 14.3467 15.2133 14.6667 15.7333 14.6667C16.6533 14.6667 17.2667 13.72 16.9067 12.88L11.7333 0.92C11.4933 0.36 10.9467 0 10.3333 0C9.72 0 9.17333 0.36 8.93333 0.92L3.76 12.88C3.4 13.72 4.02667 14.6667 4.94667 14.6667C5.46667 14.6667 5.93333 14.3467 6.13333 13.8667L7 11.7333ZM10.3333 2.64L12.8267 9.33333H7.84L10.3333 2.64Z" fill="#C1C6CD"/>
|
width="20"
|
||||||
</g>
|
height="20"
|
||||||
<defs>
|
viewBox="0 0 20 20"
|
||||||
<clipPath id="clip0_1456_146350">
|
fill="none"
|
||||||
<rect width="20" height="20" fill="white"/>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
</clipPath>
|
>
|
||||||
</defs>
|
<g
|
||||||
|
clip-path="url(#clip0_1456_146365)"
|
||||||
|
id="g53">
|
||||||
|
<path
|
||||||
|
d="M7.00042 13.7333H13.6671L14.5471 15.8667C14.7471 16.3467 15.2137 16.6667 15.7337 16.6667C16.6537 16.6667 17.2671 15.72 16.9071 14.88L11.7337 2.92C11.4937 2.36 10.9471 2 10.3337 2C9.72042 2 9.17375 2.36 8.93375 2.92L3.76042 14.88C3.40042 15.72 4.02708 16.6667 4.94708 16.6667C5.46708 16.6667 5.93375 16.3467 6.13375 15.8667L7.00042 13.7333ZM10.3337 4.64L12.8271 11.3333H7.84042L10.3337 4.64Z"
|
||||||
|
fill="#C1C6CD"
|
||||||
|
id="path49" />
|
||||||
|
<path
|
||||||
|
d="m 1.497495,8.96927 c 0,0.793654 0.7402877,1.441437 1.6473569,1.441437 H 17.521786 c 0.907096,0 1.647419,-0.647783 1.647419,-1.441437 0,-0.7936857 -0.740323,-1.4414375 -1.647419,-1.4414375 H 11.127487 3.1448519 c -0.4734211,0 -0.9014103,0.1764504 -1.2024293,0.4580061 C 1.7722258,8.1450309 1.6426187,8.3378225 1.568339,8.5513189 1.522281,8.6837006 1.497495,8.8240421 1.497495,8.96927 Z"
|
||||||
|
fill="#c1c6cd"
|
||||||
|
stroke="#ffffff"
|
||||||
|
id="path51"
|
||||||
|
style="stroke:none;stroke-width:0.840525;stroke-opacity:1" />
|
||||||
|
</g>
|
||||||
|
<defs
|
||||||
|
id="defs58">
|
||||||
|
<clipPath
|
||||||
|
id="clip0_1456_146365">
|
||||||
|
<rect
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
fill="white"
|
||||||
|
id="rect55" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 818 B After Width: | Height: | Size: 1.4 KiB |
|
@ -1,10 +1,9 @@
|
||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<g clip-path="url(#clip0_1456_146365)">
|
<g clip-path="url(#clip0_1456_146350)">
|
||||||
<path d="M7.00042 13.7333H13.6671L14.5471 15.8667C14.7471 16.3467 15.2137 16.6667 15.7337 16.6667C16.6537 16.6667 17.2671 15.72 16.9071 14.88L11.7337 2.92C11.4937 2.36 10.9471 2 10.3337 2C9.72042 2 9.17375 2.36 8.93375 2.92L3.76042 14.88C3.40042 15.72 4.02708 16.6667 4.94708 16.6667C5.46708 16.6667 5.93375 16.3467 6.13375 15.8667L7.00042 13.7333ZM10.3337 4.64L12.8271 11.3333H7.84042L10.3337 4.64Z" fill="#C1C6CD"/>
|
<path d="M1 18.6667C1 19.4 1.6 20 2.33333 20H18.3333C19.0667 20 19.6667 19.4 19.6667 18.6667C19.6667 17.9333 19.0667 17.3333 18.3333 17.3333H2.33333C1.6 17.3333 1 17.9333 1 18.6667ZM7 11.7333H13.6667L14.5467 13.8667C14.7467 14.3467 15.2133 14.6667 15.7333 14.6667C16.6533 14.6667 17.2667 13.72 16.9067 12.88L11.7333 0.92C11.4933 0.36 10.9467 0 10.3333 0C9.72 0 9.17333 0.36 8.93333 0.92L3.76 12.88C3.4 13.72 4.02667 14.6667 4.94667 14.6667C5.46667 14.6667 5.93333 14.3467 6.13333 13.8667L7 11.7333ZM10.3333 2.64L12.8267 9.33333H7.84L10.3333 2.64Z" fill="#C1C6CD"/>
|
||||||
<path d="M0.5 9.66927C0.5 10.6787 1.32386 11.5026 2.33333 11.5026H18.3333C19.3428 11.5026 20.1667 10.6787 20.1667 9.66927C20.1667 8.6598 19.3428 7.83594 18.3333 7.83594H2.33333C1.32386 7.83594 0.5 8.6598 0.5 9.66927Z" fill="#C1C6CD" stroke="white"/>
|
|
||||||
</g>
|
</g>
|
||||||
<defs>
|
<defs>
|
||||||
<clipPath id="clip0_1456_146365">
|
<clipPath id="clip0_1456_146350">
|
||||||
<rect width="20" height="20" fill="white"/>
|
<rect width="20" height="20" fill="white"/>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
</defs>
|
</defs>
|
||||||
|
|
Before Width: | Height: | Size: 921 B After Width: | Height: | Size: 818 B |
75
src/components/views/rooms/EmojiButton.tsx
Normal file
75
src/components/views/rooms/EmojiButton.tsx
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 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 classNames from "classnames";
|
||||||
|
import React, { useContext } from "react";
|
||||||
|
|
||||||
|
import { _t } from "../../../languageHandler";
|
||||||
|
import ContextMenu, { aboveLeftOf, AboveLeftOf, useContextMenu } from "../../structures/ContextMenu";
|
||||||
|
import EmojiPicker from "../emojipicker/EmojiPicker";
|
||||||
|
import { CollapsibleButton } from "./CollapsibleButton";
|
||||||
|
import { OverflowMenuContext } from "./MessageComposerButtons";
|
||||||
|
|
||||||
|
interface IEmojiButtonProps {
|
||||||
|
addEmoji: (unicode: string) => boolean;
|
||||||
|
menuPosition: AboveLeftOf;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EmojiButton({ addEmoji, menuPosition, className }: IEmojiButtonProps) {
|
||||||
|
const overflowMenuCloser = useContext(OverflowMenuContext);
|
||||||
|
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
|
||||||
|
|
||||||
|
let contextMenu: React.ReactElement | null = null;
|
||||||
|
if (menuDisplayed && button.current) {
|
||||||
|
const position = (
|
||||||
|
menuPosition ?? aboveLeftOf(button.current.getBoundingClientRect())
|
||||||
|
);
|
||||||
|
|
||||||
|
contextMenu = <ContextMenu
|
||||||
|
{...position}
|
||||||
|
onFinished={() => {
|
||||||
|
closeMenu();
|
||||||
|
overflowMenuCloser?.();
|
||||||
|
}}
|
||||||
|
managed={false}
|
||||||
|
>
|
||||||
|
<EmojiPicker onChoose={addEmoji} showQuickReactions={true} />
|
||||||
|
</ContextMenu>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const computedClassName = classNames(
|
||||||
|
"mx_EmojiButton",
|
||||||
|
className,
|
||||||
|
{
|
||||||
|
"mx_EmojiButton_highlight": menuDisplayed,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: replace ContextMenuTooltipButton with a unified representation of
|
||||||
|
// the header buttons and the right panel buttons
|
||||||
|
return <>
|
||||||
|
<CollapsibleButton
|
||||||
|
className={computedClassName}
|
||||||
|
iconClassName="mx_EmojiButton_icon"
|
||||||
|
onClick={openMenu}
|
||||||
|
title={_t("Emoji")}
|
||||||
|
inputRef={button}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{ contextMenu }
|
||||||
|
</>;
|
||||||
|
}
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createRef } from 'react';
|
import React, { createRef, ReactNode } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { IEventRelation, MatrixEvent } from "matrix-js-sdk/src/models/event";
|
import { IEventRelation, MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
@ -31,7 +31,7 @@ import Stickerpicker from './Stickerpicker';
|
||||||
import { makeRoomPermalink, RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
|
import { makeRoomPermalink, RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
|
||||||
import E2EIcon from './E2EIcon';
|
import E2EIcon from './E2EIcon';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { aboveLeftOf, AboveLeftOf } from "../../structures/ContextMenu";
|
import { aboveLeftOf } from "../../structures/ContextMenu";
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
import ReplyPreview from "./ReplyPreview";
|
import ReplyPreview from "./ReplyPreview";
|
||||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||||
|
@ -420,33 +420,48 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
||||||
return this.state.showStickersButton && !isLocalRoom(this.props.room);
|
return this.state.showStickersButton && !isLocalRoom(this.props.room);
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
private getMenuPosition() {
|
||||||
const controls = [
|
|
||||||
this.props.e2eStatus ?
|
|
||||||
<E2EIcon key="e2eIcon" status={this.props.e2eStatus} className="mx_MessageComposer_e2eIcon" /> :
|
|
||||||
null,
|
|
||||||
];
|
|
||||||
|
|
||||||
let menuPosition: AboveLeftOf | undefined;
|
|
||||||
if (this.ref.current) {
|
if (this.ref.current) {
|
||||||
|
const hasFormattingButtons = this.state.isWysiwygLabEnabled && this.state.isRichTextEnabled;
|
||||||
const contentRect = this.ref.current.getBoundingClientRect();
|
const contentRect = this.ref.current.getBoundingClientRect();
|
||||||
menuPosition = aboveLeftOf(contentRect);
|
// Here we need to remove the all the extra space above the editor
|
||||||
|
// Instead of doing a querySelector or pass a ref to find the compute the height formatting buttons
|
||||||
|
// We are using an arbitrary value, the formatting buttons height doesn't change during the lifecycle of the component
|
||||||
|
// It's easier to just use a constant here instead of an over-engineering way to find the height
|
||||||
|
const heightToRemove = hasFormattingButtons ? 36 : 0;
|
||||||
|
const fixedRect = new DOMRect(
|
||||||
|
contentRect.x,
|
||||||
|
contentRect.y + heightToRemove,
|
||||||
|
contentRect.width,
|
||||||
|
contentRect.height - heightToRemove);
|
||||||
|
return aboveLeftOf(fixedRect);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const hasE2EIcon = Boolean(!this.state.isWysiwygLabEnabled && this.props.e2eStatus);
|
||||||
|
const e2eIcon = hasE2EIcon &&
|
||||||
|
<E2EIcon key="e2eIcon" status={this.props.e2eStatus} className="mx_MessageComposer_e2eIcon" />;
|
||||||
|
|
||||||
|
const controls: ReactNode[] = [];
|
||||||
|
const menuPosition = this.getMenuPosition();
|
||||||
|
|
||||||
const canSendMessages = this.context.canSendMessages && !this.context.tombstone;
|
const canSendMessages = this.context.canSendMessages && !this.context.tombstone;
|
||||||
|
let composer: ReactNode;
|
||||||
if (canSendMessages) {
|
if (canSendMessages) {
|
||||||
if (this.state.isWysiwygLabEnabled) {
|
if (this.state.isWysiwygLabEnabled && menuPosition) {
|
||||||
controls.push(
|
composer =
|
||||||
<SendWysiwygComposer key="controls_input"
|
<SendWysiwygComposer key="controls_input"
|
||||||
disabled={this.state.haveRecording}
|
disabled={this.state.haveRecording}
|
||||||
onChange={this.onWysiwygChange}
|
onChange={this.onWysiwygChange}
|
||||||
onSend={this.sendMessage}
|
onSend={this.sendMessage}
|
||||||
isRichTextEnabled={this.state.isRichTextEnabled}
|
isRichTextEnabled={this.state.isRichTextEnabled}
|
||||||
initialContent={this.state.initialComposerContent}
|
initialContent={this.state.initialComposerContent}
|
||||||
/>,
|
e2eStatus={this.props.e2eStatus}
|
||||||
);
|
menuPosition={menuPosition}
|
||||||
|
/>;
|
||||||
} else {
|
} else {
|
||||||
controls.push(
|
composer =
|
||||||
<SendMessageComposer
|
<SendMessageComposer
|
||||||
ref={this.messageComposerInput}
|
ref={this.messageComposerInput}
|
||||||
key="controls_input"
|
key="controls_input"
|
||||||
|
@ -458,8 +473,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
||||||
onChange={this.onChange}
|
onChange={this.onChange}
|
||||||
disabled={this.state.haveRecording}
|
disabled={this.state.haveRecording}
|
||||||
toggleStickerPickerOpen={this.toggleStickerPickerOpen}
|
toggleStickerPickerOpen={this.toggleStickerPickerOpen}
|
||||||
/>,
|
/>;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
controls.push(<VoiceRecordComposerTile
|
controls.push(<VoiceRecordComposerTile
|
||||||
|
@ -529,8 +543,8 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
||||||
const classes = classNames({
|
const classes = classNames({
|
||||||
"mx_MessageComposer": true,
|
"mx_MessageComposer": true,
|
||||||
"mx_MessageComposer--compact": this.props.compact,
|
"mx_MessageComposer--compact": this.props.compact,
|
||||||
"mx_MessageComposer_e2eStatus": this.props.e2eStatus != undefined,
|
"mx_MessageComposer_e2eStatus": hasE2EIcon,
|
||||||
"mx_MessageComposer_wysiwyg": this.state.isWysiwygLabEnabled && this.state.isRichTextEnabled,
|
"mx_MessageComposer_wysiwyg": this.state.isWysiwygLabEnabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -541,45 +555,48 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
||||||
replyToEvent={this.props.replyToEvent}
|
replyToEvent={this.props.replyToEvent}
|
||||||
permalinkCreator={this.props.permalinkCreator} />
|
permalinkCreator={this.props.permalinkCreator} />
|
||||||
<div className="mx_MessageComposer_row">
|
<div className="mx_MessageComposer_row">
|
||||||
{ controls }
|
{ e2eIcon }
|
||||||
{ canSendMessages && <MessageComposerButtons
|
{ composer }
|
||||||
addEmoji={this.addEmoji}
|
<div className="mx_MessageComposer_actions">
|
||||||
haveRecording={this.state.haveRecording}
|
{ controls }
|
||||||
isMenuOpen={this.state.isMenuOpen}
|
{ canSendMessages && <MessageComposerButtons
|
||||||
isStickerPickerOpen={this.state.isStickerPickerOpen}
|
addEmoji={this.addEmoji}
|
||||||
menuPosition={menuPosition}
|
haveRecording={this.state.haveRecording}
|
||||||
relation={this.props.relation}
|
isMenuOpen={this.state.isMenuOpen}
|
||||||
onRecordStartEndClick={() => {
|
isStickerPickerOpen={this.state.isStickerPickerOpen}
|
||||||
this.voiceRecordingButton.current?.onRecordStartEndClick();
|
menuPosition={menuPosition}
|
||||||
if (this.context.narrow) {
|
relation={this.props.relation}
|
||||||
|
onRecordStartEndClick={() => {
|
||||||
|
this.voiceRecordingButton.current?.onRecordStartEndClick();
|
||||||
|
if (this.context.narrow) {
|
||||||
|
this.toggleButtonMenu();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
setStickerPickerOpen={this.setStickerPickerOpen}
|
||||||
|
showLocationButton={!window.electron}
|
||||||
|
showPollsButton={this.state.showPollsButton}
|
||||||
|
showStickersButton={this.showStickersButton}
|
||||||
|
isRichTextEnabled={this.state.isRichTextEnabled}
|
||||||
|
onComposerModeClick={this.onRichTextToggle}
|
||||||
|
toggleButtonMenu={this.toggleButtonMenu}
|
||||||
|
showVoiceBroadcastButton={this.state.showVoiceBroadcastButton}
|
||||||
|
onStartVoiceBroadcastClick={() => {
|
||||||
|
startNewVoiceBroadcastRecording(
|
||||||
|
this.props.room,
|
||||||
|
MatrixClientPeg.get(),
|
||||||
|
VoiceBroadcastRecordingsStore.instance(),
|
||||||
|
);
|
||||||
this.toggleButtonMenu();
|
this.toggleButtonMenu();
|
||||||
}
|
}}
|
||||||
}}
|
/> }
|
||||||
setStickerPickerOpen={this.setStickerPickerOpen}
|
{ showSendButton && (
|
||||||
showLocationButton={!window.electron}
|
<SendButton
|
||||||
showPollsButton={this.state.showPollsButton}
|
key="controls_send"
|
||||||
showStickersButton={this.showStickersButton}
|
onClick={this.sendMessage}
|
||||||
showComposerModeButton={this.state.isWysiwygLabEnabled}
|
title={this.state.haveRecording ? _t("Send voice message") : undefined}
|
||||||
isRichTextEnabled={this.state.isRichTextEnabled}
|
/>
|
||||||
onComposerModeClick={this.onRichTextToggle}
|
) }
|
||||||
toggleButtonMenu={this.toggleButtonMenu}
|
</div>
|
||||||
showVoiceBroadcastButton={this.state.showVoiceBroadcastButton}
|
|
||||||
onStartVoiceBroadcastClick={() => {
|
|
||||||
startNewVoiceBroadcastRecording(
|
|
||||||
this.props.room,
|
|
||||||
MatrixClientPeg.get(),
|
|
||||||
VoiceBroadcastRecordingsStore.instance(),
|
|
||||||
);
|
|
||||||
this.toggleButtonMenu();
|
|
||||||
}}
|
|
||||||
/> }
|
|
||||||
{ showSendButton && (
|
|
||||||
<SendButton
|
|
||||||
key="controls_send"
|
|
||||||
onClick={this.sendMessage}
|
|
||||||
title={this.state.haveRecording ? _t("Send voice message") : undefined}
|
|
||||||
/>
|
|
||||||
) }
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -25,9 +25,8 @@ import { THREAD_RELATION_TYPE } from 'matrix-js-sdk/src/models/thread';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
import { CollapsibleButton } from './CollapsibleButton';
|
import { CollapsibleButton } from './CollapsibleButton';
|
||||||
import ContextMenu, { aboveLeftOf, AboveLeftOf, useContextMenu } from '../../structures/ContextMenu';
|
import { AboveLeftOf } from '../../structures/ContextMenu';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import EmojiPicker from '../emojipicker/EmojiPicker';
|
|
||||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||||
import LocationButton from '../location/LocationButton';
|
import LocationButton from '../location/LocationButton';
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
|
@ -39,6 +38,8 @@ import RoomContext from '../../../contexts/RoomContext';
|
||||||
import { useDispatcher } from "../../../hooks/useDispatcher";
|
import { useDispatcher } from "../../../hooks/useDispatcher";
|
||||||
import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds";
|
import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds";
|
||||||
import IconizedContextMenu, { IconizedContextMenuOptionList } from '../context_menus/IconizedContextMenu';
|
import IconizedContextMenu, { IconizedContextMenuOptionList } from '../context_menus/IconizedContextMenu';
|
||||||
|
import { EmojiButton } from './EmojiButton';
|
||||||
|
import { useSettingValue } from '../../../hooks/useSettings';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
addEmoji: (emoji: string) => boolean;
|
addEmoji: (emoji: string) => boolean;
|
||||||
|
@ -56,7 +57,6 @@ interface IProps {
|
||||||
showVoiceBroadcastButton: boolean;
|
showVoiceBroadcastButton: boolean;
|
||||||
onStartVoiceBroadcastClick: () => void;
|
onStartVoiceBroadcastClick: () => void;
|
||||||
isRichTextEnabled: boolean;
|
isRichTextEnabled: boolean;
|
||||||
showComposerModeButton: boolean;
|
|
||||||
onComposerModeClick: () => void;
|
onComposerModeClick: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,6 +67,8 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
|
||||||
const matrixClient: MatrixClient = useContext(MatrixClientContext);
|
const matrixClient: MatrixClient = useContext(MatrixClientContext);
|
||||||
const { room, roomId, narrow } = useContext(RoomContext);
|
const { room, roomId, narrow } = useContext(RoomContext);
|
||||||
|
|
||||||
|
const isWysiwygLabEnabled = useSettingValue<boolean>('feature_wysiwyg_composer');
|
||||||
|
|
||||||
if (props.haveRecording) {
|
if (props.haveRecording) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -75,7 +77,9 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
|
||||||
let moreButtons: ReactElement[];
|
let moreButtons: ReactElement[];
|
||||||
if (narrow) {
|
if (narrow) {
|
||||||
mainButtons = [
|
mainButtons = [
|
||||||
emojiButton(props),
|
isWysiwygLabEnabled ?
|
||||||
|
<ComposerModeButton key="composerModeButton" isRichTextEnabled={props.isRichTextEnabled} onClick={props.onComposerModeClick} /> :
|
||||||
|
emojiButton(props),
|
||||||
];
|
];
|
||||||
moreButtons = [
|
moreButtons = [
|
||||||
uploadButton(), // props passed via UploadButtonContext
|
uploadButton(), // props passed via UploadButtonContext
|
||||||
|
@ -87,9 +91,9 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
mainButtons = [
|
mainButtons = [
|
||||||
emojiButton(props),
|
isWysiwygLabEnabled ?
|
||||||
props.showComposerModeButton &&
|
<ComposerModeButton key="composerModeButton" isRichTextEnabled={props.isRichTextEnabled} onClick={props.onComposerModeClick} /> :
|
||||||
<ComposerModeButton key="composerModeButton" isRichTextEnabled={props.isRichTextEnabled} onClick={props.onComposerModeClick} />,
|
emojiButton(props),
|
||||||
uploadButton(), // props passed via UploadButtonContext
|
uploadButton(), // props passed via UploadButtonContext
|
||||||
];
|
];
|
||||||
moreButtons = [
|
moreButtons = [
|
||||||
|
@ -139,58 +143,10 @@ function emojiButton(props: IProps): ReactElement {
|
||||||
key="emoji_button"
|
key="emoji_button"
|
||||||
addEmoji={props.addEmoji}
|
addEmoji={props.addEmoji}
|
||||||
menuPosition={props.menuPosition}
|
menuPosition={props.menuPosition}
|
||||||
|
className="mx_MessageComposer_button"
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IEmojiButtonProps {
|
|
||||||
addEmoji: (unicode: string) => boolean;
|
|
||||||
menuPosition: AboveLeftOf;
|
|
||||||
}
|
|
||||||
|
|
||||||
const EmojiButton: React.FC<IEmojiButtonProps> = ({ addEmoji, menuPosition }) => {
|
|
||||||
const overflowMenuCloser = useContext(OverflowMenuContext);
|
|
||||||
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
|
|
||||||
|
|
||||||
let contextMenu: React.ReactElement | null = null;
|
|
||||||
if (menuDisplayed) {
|
|
||||||
const position = (
|
|
||||||
menuPosition ?? aboveLeftOf(button.current.getBoundingClientRect())
|
|
||||||
);
|
|
||||||
|
|
||||||
contextMenu = <ContextMenu
|
|
||||||
{...position}
|
|
||||||
onFinished={() => {
|
|
||||||
closeMenu();
|
|
||||||
overflowMenuCloser?.();
|
|
||||||
}}
|
|
||||||
managed={false}
|
|
||||||
>
|
|
||||||
<EmojiPicker onChoose={addEmoji} showQuickReactions={true} />
|
|
||||||
</ContextMenu>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const className = classNames(
|
|
||||||
"mx_MessageComposer_button",
|
|
||||||
{
|
|
||||||
"mx_MessageComposer_button_highlight": menuDisplayed,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// TODO: replace ContextMenuTooltipButton with a unified representation of
|
|
||||||
// the header buttons and the right panel buttons
|
|
||||||
return <React.Fragment>
|
|
||||||
<CollapsibleButton
|
|
||||||
className={className}
|
|
||||||
iconClassName="mx_MessageComposer_emoji"
|
|
||||||
onClick={openMenu}
|
|
||||||
title={_t("Emoji")}
|
|
||||||
inputRef={button}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{ contextMenu }
|
|
||||||
</React.Fragment>;
|
|
||||||
};
|
|
||||||
|
|
||||||
function uploadButton(): ReactElement {
|
function uploadButton(): ReactElement {
|
||||||
return <UploadButton key="controls_upload" />;
|
return <UploadButton key="controls_upload" />;
|
||||||
}
|
}
|
||||||
|
@ -408,7 +364,7 @@ interface WysiwygToggleButtonProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
function ComposerModeButton({ isRichTextEnabled, onClick }: WysiwygToggleButtonProps) {
|
function ComposerModeButton({ isRichTextEnabled, onClick }: WysiwygToggleButtonProps) {
|
||||||
const title = isRichTextEnabled ? _t("Show plain text") : _t("Show formatting");
|
const title = isRichTextEnabled ? _t("Hide formatting") : _t("Show formatting");
|
||||||
|
|
||||||
return <CollapsibleButton
|
return <CollapsibleButton
|
||||||
className="mx_MessageComposer_button"
|
className="mx_MessageComposer_button"
|
||||||
|
|
|
@ -14,21 +14,28 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { forwardRef, RefObject } from 'react';
|
import React, { ForwardedRef, forwardRef, MutableRefObject } from 'react';
|
||||||
|
|
||||||
import { useWysiwygSendActionHandler } from './hooks/useWysiwygSendActionHandler';
|
import { useWysiwygSendActionHandler } from './hooks/useWysiwygSendActionHandler';
|
||||||
import { WysiwygComposer } from './components/WysiwygComposer';
|
import { WysiwygComposer } from './components/WysiwygComposer';
|
||||||
import { PlainTextComposer } from './components/PlainTextComposer';
|
import { PlainTextComposer } from './components/PlainTextComposer';
|
||||||
import { ComposerFunctions } from './types';
|
import { ComposerFunctions } from './types';
|
||||||
|
import { E2EStatus } from '../../../../utils/ShieldUtils';
|
||||||
|
import E2EIcon from '../E2EIcon';
|
||||||
|
import { EmojiButton } from '../EmojiButton';
|
||||||
|
import { AboveLeftOf } from '../../../structures/ContextMenu';
|
||||||
|
|
||||||
interface ContentProps {
|
interface ContentProps {
|
||||||
disabled: boolean;
|
disabled?: boolean;
|
||||||
composerFunctions: ComposerFunctions;
|
composerFunctions: ComposerFunctions;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Content = forwardRef<HTMLElement, ContentProps>(
|
const Content = forwardRef<HTMLElement, ContentProps>(
|
||||||
function Content({ disabled, composerFunctions }: ContentProps, forwardRef: RefObject<HTMLElement>) {
|
function Content(
|
||||||
useWysiwygSendActionHandler(disabled, forwardRef, composerFunctions);
|
{ disabled = false, composerFunctions }: ContentProps,
|
||||||
|
forwardRef: ForwardedRef<HTMLElement>,
|
||||||
|
) {
|
||||||
|
useWysiwygSendActionHandler(disabled, forwardRef as MutableRefObject<HTMLElement>, composerFunctions);
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -37,14 +44,23 @@ interface SendWysiwygComposerProps {
|
||||||
initialContent?: string;
|
initialContent?: string;
|
||||||
isRichTextEnabled: boolean;
|
isRichTextEnabled: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
e2eStatus?: E2EStatus;
|
||||||
onChange: (content: string) => void;
|
onChange: (content: string) => void;
|
||||||
onSend: () => void;
|
onSend: () => void;
|
||||||
|
menuPosition: AboveLeftOf;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SendWysiwygComposer({ isRichTextEnabled, ...props }: SendWysiwygComposerProps) {
|
export function SendWysiwygComposer(
|
||||||
|
{ isRichTextEnabled, e2eStatus, menuPosition, ...props }: SendWysiwygComposerProps) {
|
||||||
const Composer = isRichTextEnabled ? WysiwygComposer : PlainTextComposer;
|
const Composer = isRichTextEnabled ? WysiwygComposer : PlainTextComposer;
|
||||||
|
|
||||||
return <Composer className="mx_SendWysiwygComposer" {...props}>
|
return <Composer
|
||||||
|
className="mx_SendWysiwygComposer"
|
||||||
|
leftComponent={e2eStatus && <E2EIcon status={e2eStatus} />}
|
||||||
|
// TODO add emoji support
|
||||||
|
rightComponent={<EmojiButton menuPosition={menuPosition} addEmoji={() => false} />}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
{ (ref, composerFunctions) => (
|
{ (ref, composerFunctions) => (
|
||||||
<Content disabled={props.disabled} ref={ref} composerFunctions={composerFunctions} />
|
<Content disabled={props.disabled} ref={ref} composerFunctions={composerFunctions} />
|
||||||
) }
|
) }
|
||||||
|
|
|
@ -14,27 +14,43 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { forwardRef, memo } from 'react';
|
import React, { forwardRef, memo, MutableRefObject, ReactNode } from 'react';
|
||||||
|
|
||||||
|
import { useIsExpanded } from '../hooks/useIsExpanded';
|
||||||
|
|
||||||
|
const HEIGHT_BREAKING_POINT = 20;
|
||||||
|
|
||||||
interface EditorProps {
|
interface EditorProps {
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
|
leftComponent?: ReactNode;
|
||||||
|
rightComponent?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Editor = memo(
|
export const Editor = memo(
|
||||||
forwardRef<HTMLDivElement, EditorProps>(
|
forwardRef<HTMLDivElement, EditorProps>(
|
||||||
function Editor({ disabled }: EditorProps, ref,
|
function Editor({ disabled, leftComponent, rightComponent }: EditorProps, ref,
|
||||||
) {
|
) {
|
||||||
return <div className="mx_WysiwygComposer_container">
|
const isExpanded = useIsExpanded(ref as MutableRefObject<HTMLDivElement | null>, HEIGHT_BREAKING_POINT);
|
||||||
<div className="mx_WysiwygComposer_content"
|
|
||||||
ref={ref!}
|
return <div
|
||||||
contentEditable={!disabled}
|
data-testid="WysiwygComposerEditor"
|
||||||
role="textbox"
|
className="mx_WysiwygComposer_Editor"
|
||||||
aria-multiline="true"
|
data-is-expanded={isExpanded}
|
||||||
aria-autocomplete="list"
|
>
|
||||||
aria-haspopup="listbox"
|
{ leftComponent }
|
||||||
dir="auto"
|
<div className="mx_WysiwygComposer_Editor_container">
|
||||||
aria-disabled={disabled}
|
<div className="mx_WysiwygComposer_Editor_content"
|
||||||
/>
|
ref={ref}
|
||||||
|
contentEditable={!disabled}
|
||||||
|
role="textbox"
|
||||||
|
aria-multiline="true"
|
||||||
|
aria-autocomplete="list"
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
dir="auto"
|
||||||
|
aria-disabled={disabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{ rightComponent }
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
@ -14,9 +14,11 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
import React, { MutableRefObject, ReactNode } from 'react';
|
import React, { MutableRefObject, ReactNode } from 'react';
|
||||||
|
|
||||||
import { useComposerFunctions } from '../hooks/useComposerFunctions';
|
import { useComposerFunctions } from '../hooks/useComposerFunctions';
|
||||||
|
import { useIsFocused } from '../hooks/useIsFocused';
|
||||||
import { usePlainTextInitialization } from '../hooks/usePlainTextInitialization';
|
import { usePlainTextInitialization } from '../hooks/usePlainTextInitialization';
|
||||||
import { usePlainTextListeners } from '../hooks/usePlainTextListeners';
|
import { usePlainTextListeners } from '../hooks/usePlainTextListeners';
|
||||||
import { useSetCursorPosition } from '../hooks/useSetCursorPosition';
|
import { useSetCursorPosition } from '../hooks/useSetCursorPosition';
|
||||||
|
@ -26,9 +28,11 @@ import { Editor } from "./Editor";
|
||||||
interface PlainTextComposerProps {
|
interface PlainTextComposerProps {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onChange?: (content: string) => void;
|
onChange?: (content: string) => void;
|
||||||
onSend: () => void;
|
onSend?: () => void;
|
||||||
initialContent?: string;
|
initialContent?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
leftComponent?: ReactNode;
|
||||||
|
rightComponent?: ReactNode;
|
||||||
children?: (
|
children?: (
|
||||||
ref: MutableRefObject<HTMLDivElement | null>,
|
ref: MutableRefObject<HTMLDivElement | null>,
|
||||||
composerFunctions: ComposerFunctions,
|
composerFunctions: ComposerFunctions,
|
||||||
|
@ -36,21 +40,32 @@ interface PlainTextComposerProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PlainTextComposer({
|
export function PlainTextComposer({
|
||||||
className, disabled, onSend, onChange, children, initialContent }: PlainTextComposerProps,
|
className,
|
||||||
|
disabled = false,
|
||||||
|
onSend,
|
||||||
|
onChange,
|
||||||
|
children,
|
||||||
|
initialContent,
|
||||||
|
leftComponent,
|
||||||
|
rightComponent,
|
||||||
|
}: PlainTextComposerProps,
|
||||||
) {
|
) {
|
||||||
const { ref, onInput, onPaste, onKeyDown } = usePlainTextListeners(onChange, onSend);
|
const { ref, onInput, onPaste, onKeyDown } = usePlainTextListeners(onChange, onSend);
|
||||||
const composerFunctions = useComposerFunctions(ref);
|
const composerFunctions = useComposerFunctions(ref);
|
||||||
usePlainTextInitialization(initialContent, ref);
|
usePlainTextInitialization(initialContent, ref);
|
||||||
useSetCursorPosition(disabled, ref);
|
useSetCursorPosition(disabled, ref);
|
||||||
|
const { isFocused, onFocus } = useIsFocused();
|
||||||
|
|
||||||
return <div
|
return <div
|
||||||
data-testid="PlainTextComposer"
|
data-testid="PlainTextComposer"
|
||||||
className={className}
|
className={classNames(className, { [`${className}-focused`]: isFocused })}
|
||||||
|
onFocus={onFocus}
|
||||||
|
onBlur={onFocus}
|
||||||
onInput={onInput}
|
onInput={onInput}
|
||||||
onPaste={onPaste}
|
onPaste={onPaste}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
>
|
>
|
||||||
<Editor ref={ref} disabled={disabled} />
|
<Editor ref={ref} disabled={disabled} leftComponent={leftComponent} rightComponent={rightComponent} />
|
||||||
{ children?.(ref, composerFunctions) }
|
{ children?.(ref, composerFunctions) }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,11 +16,13 @@ limitations under the License.
|
||||||
|
|
||||||
import React, { memo, MutableRefObject, ReactNode, useEffect } from 'react';
|
import React, { memo, MutableRefObject, ReactNode, useEffect } from 'react';
|
||||||
import { useWysiwyg, FormattingFunctions } from "@matrix-org/matrix-wysiwyg";
|
import { useWysiwyg, FormattingFunctions } from "@matrix-org/matrix-wysiwyg";
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { FormattingButtons } from './FormattingButtons';
|
import { FormattingButtons } from './FormattingButtons';
|
||||||
import { Editor } from './Editor';
|
import { Editor } from './Editor';
|
||||||
import { useInputEventProcessor } from '../hooks/useInputEventProcessor';
|
import { useInputEventProcessor } from '../hooks/useInputEventProcessor';
|
||||||
import { useSetCursorPosition } from '../hooks/useSetCursorPosition';
|
import { useSetCursorPosition } from '../hooks/useSetCursorPosition';
|
||||||
|
import { useIsFocused } from '../hooks/useIsFocused';
|
||||||
|
|
||||||
interface WysiwygComposerProps {
|
interface WysiwygComposerProps {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
@ -28,6 +30,8 @@ interface WysiwygComposerProps {
|
||||||
onSend: () => void;
|
onSend: () => void;
|
||||||
initialContent?: string;
|
initialContent?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
leftComponent?: ReactNode;
|
||||||
|
rightComponent?: ReactNode;
|
||||||
children?: (
|
children?: (
|
||||||
ref: MutableRefObject<HTMLDivElement | null>,
|
ref: MutableRefObject<HTMLDivElement | null>,
|
||||||
wysiwyg: FormattingFunctions,
|
wysiwyg: FormattingFunctions,
|
||||||
|
@ -35,7 +39,16 @@ interface WysiwygComposerProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WysiwygComposer = memo(function WysiwygComposer(
|
export const WysiwygComposer = memo(function WysiwygComposer(
|
||||||
{ disabled = false, onChange, onSend, initialContent, className, children }: WysiwygComposerProps,
|
{
|
||||||
|
disabled = false,
|
||||||
|
onChange,
|
||||||
|
onSend,
|
||||||
|
initialContent,
|
||||||
|
className,
|
||||||
|
leftComponent,
|
||||||
|
rightComponent,
|
||||||
|
children,
|
||||||
|
}: WysiwygComposerProps,
|
||||||
) {
|
) {
|
||||||
const inputEventProcessor = useInputEventProcessor(onSend);
|
const inputEventProcessor = useInputEventProcessor(onSend);
|
||||||
|
|
||||||
|
@ -51,10 +64,12 @@ export const WysiwygComposer = memo(function WysiwygComposer(
|
||||||
const isReady = isWysiwygReady && !disabled;
|
const isReady = isWysiwygReady && !disabled;
|
||||||
useSetCursorPosition(!isReady, ref);
|
useSetCursorPosition(!isReady, ref);
|
||||||
|
|
||||||
|
const { isFocused, onFocus } = useIsFocused();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-testid="WysiwygComposer" className={className}>
|
<div data-testid="WysiwygComposer" className={classNames(className, { [`${className}-focused`]: isFocused })} onFocus={onFocus} onBlur={onFocus}>
|
||||||
<FormattingButtons composer={wysiwyg} formattingStates={formattingStates} />
|
<FormattingButtons composer={wysiwyg} formattingStates={formattingStates} />
|
||||||
<Editor ref={ref} disabled={!isReady} />
|
<Editor ref={ref} disabled={!isReady} leftComponent={leftComponent} rightComponent={rightComponent} />
|
||||||
{ children?.(ref, wysiwyg) }
|
{ children?.(ref, wysiwyg) }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 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 { MutableRefObject, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export function useIsExpanded(ref: MutableRefObject<HTMLElement | null>, breakingPoint: number) {
|
||||||
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (ref.current) {
|
||||||
|
const editor = ref.current;
|
||||||
|
const resizeObserver = new ResizeObserver(entries => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const height = entries[0]?.contentBoxSize?.[0].blockSize;
|
||||||
|
setIsExpanded(height >= breakingPoint);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
resizeObserver.observe(editor);
|
||||||
|
return () => resizeObserver.unobserve(editor);
|
||||||
|
}
|
||||||
|
}, [ref, breakingPoint]);
|
||||||
|
|
||||||
|
return isExpanded;
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 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 { FocusEvent, useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
export function useIsFocused() {
|
||||||
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
|
const timeoutIDRef = useRef<number>();
|
||||||
|
|
||||||
|
useEffect(() => () => clearTimeout(timeoutIDRef.current), [timeoutIDRef]);
|
||||||
|
const onFocus = useCallback((event: FocusEvent<HTMLElement>) => {
|
||||||
|
clearTimeout(timeoutIDRef.current);
|
||||||
|
if (event.type === 'focus') {
|
||||||
|
setIsFocused(true);
|
||||||
|
} else {
|
||||||
|
// To avoid a blink when we switch mode between plain text and rich text mode
|
||||||
|
// We delay the unfocused action
|
||||||
|
timeoutIDRef.current = setTimeout(() => setIsFocused(false), 100);
|
||||||
|
}
|
||||||
|
}, [setIsFocused, timeoutIDRef]);
|
||||||
|
|
||||||
|
return { isFocused, onFocus };
|
||||||
|
}
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
import { RefObject, useEffect } from "react";
|
import { RefObject, useEffect } from "react";
|
||||||
|
|
||||||
export function usePlainTextInitialization(initialContent: string, ref: RefObject<HTMLElement>) {
|
export function usePlainTextInitialization(initialContent = '', ref: RefObject<HTMLElement>) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ref.current) {
|
if (ref.current) {
|
||||||
ref.current.innerText = initialContent;
|
ref.current.innerText = initialContent;
|
||||||
|
|
|
@ -22,18 +22,18 @@ function isDivElement(target: EventTarget): target is HTMLDivElement {
|
||||||
return target instanceof HTMLDivElement;
|
return target instanceof HTMLDivElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePlainTextListeners(onChange: (content: string) => void, onSend: () => void) {
|
export function usePlainTextListeners(onChange?: (content: string) => void, onSend?: () => void) {
|
||||||
const ref = useRef<HTMLDivElement>();
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
const send = useCallback((() => {
|
const send = useCallback((() => {
|
||||||
if (ref.current) {
|
if (ref.current) {
|
||||||
ref.current.innerHTML = '';
|
ref.current.innerHTML = '';
|
||||||
}
|
}
|
||||||
onSend();
|
onSend?.();
|
||||||
}), [ref, onSend]);
|
}), [ref, onSend]);
|
||||||
|
|
||||||
const onInput = useCallback((event: SyntheticEvent<HTMLDivElement, InputEvent | ClipboardEvent>) => {
|
const onInput = useCallback((event: SyntheticEvent<HTMLDivElement, InputEvent | ClipboardEvent>) => {
|
||||||
if (isDivElement(event.target)) {
|
if (isDivElement(event.target)) {
|
||||||
onChange(event.target.innerHTML);
|
onChange?.(event.target.innerHTML);
|
||||||
}
|
}
|
||||||
}, [onChange]);
|
}, [onChange]);
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ export function useWysiwygEditActionHandler(
|
||||||
composerElement: RefObject<HTMLElement>,
|
composerElement: RefObject<HTMLElement>,
|
||||||
) {
|
) {
|
||||||
const roomContext = useRoomContext();
|
const roomContext = useRoomContext();
|
||||||
const timeoutId = useRef<number>();
|
const timeoutId = useRef<number | null>(null);
|
||||||
|
|
||||||
const handler = useCallback((payload: ActionPayload) => {
|
const handler = useCallback((payload: ActionPayload) => {
|
||||||
// don't let the user into the composer if it is disabled - all of these branches lead
|
// don't let the user into the composer if it is disabled - all of these branches lead
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { RefObject, useCallback, useRef } from "react";
|
import { MutableRefObject, useCallback, useRef } from "react";
|
||||||
|
|
||||||
import defaultDispatcher from "../../../../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../../../../dispatcher/dispatcher";
|
||||||
import { Action } from "../../../../../dispatcher/actions";
|
import { Action } from "../../../../../dispatcher/actions";
|
||||||
|
@ -26,16 +26,16 @@ import { ComposerFunctions } from "../types";
|
||||||
|
|
||||||
export function useWysiwygSendActionHandler(
|
export function useWysiwygSendActionHandler(
|
||||||
disabled: boolean,
|
disabled: boolean,
|
||||||
composerElement: RefObject<HTMLElement>,
|
composerElement: MutableRefObject<HTMLElement>,
|
||||||
composerFunctions: ComposerFunctions,
|
composerFunctions: ComposerFunctions,
|
||||||
) {
|
) {
|
||||||
const roomContext = useRoomContext();
|
const roomContext = useRoomContext();
|
||||||
const timeoutId = useRef<number>();
|
const timeoutId = useRef<number | null>(null);
|
||||||
|
|
||||||
const handler = useCallback((payload: ActionPayload) => {
|
const handler = useCallback((payload: ActionPayload) => {
|
||||||
// don't let the user into the composer if it is disabled - all of these branches lead
|
// don't let the user into the composer if it is disabled - all of these branches lead
|
||||||
// to the cursor being in the composer
|
// to the cursor being in the composer
|
||||||
if (disabled || !composerElement.current) return;
|
if (disabled || !composerElement?.current) return;
|
||||||
|
|
||||||
const context = payload.context ?? TimelineRenderingType.Room;
|
const context = payload.context ?? TimelineRenderingType.Room;
|
||||||
|
|
||||||
|
|
|
@ -14,14 +14,16 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { MutableRefObject } from "react";
|
||||||
|
|
||||||
import { TimelineRenderingType } from "../../../../../contexts/RoomContext";
|
import { TimelineRenderingType } from "../../../../../contexts/RoomContext";
|
||||||
import { IRoomState } from "../../../../structures/RoomView";
|
import { IRoomState } from "../../../../structures/RoomView";
|
||||||
|
|
||||||
export function focusComposer(
|
export function focusComposer(
|
||||||
composerElement: React.MutableRefObject<HTMLElement>,
|
composerElement: MutableRefObject<HTMLElement | null>,
|
||||||
renderingType: TimelineRenderingType,
|
renderingType: TimelineRenderingType,
|
||||||
roomContext: IRoomState,
|
roomContext: IRoomState,
|
||||||
timeoutId: React.MutableRefObject<number>,
|
timeoutId: MutableRefObject<number | null>,
|
||||||
) {
|
) {
|
||||||
if (renderingType === roomContext.timelineRenderingType) {
|
if (renderingType === roomContext.timelineRenderingType) {
|
||||||
// Immediately set the focus, so if you start typing it
|
// Immediately set the focus, so if you start typing it
|
||||||
|
|
|
@ -1829,6 +1829,7 @@
|
||||||
"This room is end-to-end encrypted": "This room is end-to-end encrypted",
|
"This room is end-to-end encrypted": "This room is end-to-end encrypted",
|
||||||
"Everyone in this room is verified": "Everyone in this room is verified",
|
"Everyone in this room is verified": "Everyone in this room is verified",
|
||||||
"Edit message": "Edit message",
|
"Edit message": "Edit message",
|
||||||
|
"Emoji": "Emoji",
|
||||||
"Mod": "Mod",
|
"Mod": "Mod",
|
||||||
"From a thread": "From a thread",
|
"From a thread": "From a thread",
|
||||||
"This event could not be displayed": "This event could not be displayed",
|
"This event could not be displayed": "This event could not be displayed",
|
||||||
|
@ -1878,13 +1879,12 @@
|
||||||
"You do not have permission to post to this room": "You do not have permission to post to this room",
|
"You do not have permission to post to this room": "You do not have permission to post to this room",
|
||||||
"%(seconds)ss left": "%(seconds)ss left",
|
"%(seconds)ss left": "%(seconds)ss left",
|
||||||
"Send voice message": "Send voice message",
|
"Send voice message": "Send voice message",
|
||||||
"Emoji": "Emoji",
|
|
||||||
"Hide stickers": "Hide stickers",
|
"Hide stickers": "Hide stickers",
|
||||||
"Sticker": "Sticker",
|
"Sticker": "Sticker",
|
||||||
"Voice Message": "Voice Message",
|
"Voice Message": "Voice Message",
|
||||||
"You do not have permission to start polls in this room.": "You do not have permission to start polls in this room.",
|
"You do not have permission to start polls in this room.": "You do not have permission to start polls in this room.",
|
||||||
"Poll": "Poll",
|
"Poll": "Poll",
|
||||||
"Show plain text": "Show plain text",
|
"Hide formatting": "Hide formatting",
|
||||||
"Show formatting": "Show formatting",
|
"Show formatting": "Show formatting",
|
||||||
"Bold": "Bold",
|
"Bold": "Bold",
|
||||||
"Italics": "Italics",
|
"Italics": "Italics",
|
||||||
|
|
|
@ -4,6 +4,6 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
|
||||||
|
|
||||||
exports[`RoomView for a local room in state ERROR should match the snapshot 1`] = `"<div class="mx_RoomView mx_RoomView--local"><header class="mx_RoomHeader light-panel"><div class="mx_RoomHeader_wrapper"><div class="mx_RoomHeader_avatar"><div class="mx_DecoratedRoomAvatar"><span class="mx_BaseAvatar" role="presentation"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 15.600000000000001px; width: 24px; line-height: 24px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" style="width: 24px; height: 24px;" aria-hidden="true"></span></div></div><div class="mx_E2EIcon mx_E2EIcon_normal mx_RoomHeader_icon"></div><div class="mx_RoomHeader_name mx_RoomHeader_name--textonly"><div dir="auto" class="mx_RoomHeader_nametext" title="@user:example.com" role="heading" aria-level="1">@user:example.com</div></div><div class="mx_RoomHeader_topic mx_RoomTopic" dir="auto"><div tabindex="0"><div><span dir="auto"></span></div></div></div></div></header><main class="mx_RoomView_body"><div class="mx_RoomView_timeline"><div class="mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel" tabindex="-1"><div class="mx_RoomView_messageListWrapper"><ol class="mx_RoomView_MessageList" aria-live="polite" style="height: 400px;"><li class="mx_NewRoomIntro"><div class="mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon_warning"><div class="mx_EventTileBubble_title">End-to-end encryption isn't enabled</div><div class="mx_EventTileBubble_subtitle"><span> Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. </span></div></div><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 33.800000000000004px; width: 52px; line-height: 52px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" style="width: 52px; height: 52px;" aria-hidden="true"></span><h2>@user:example.com</h2><p><span>Send your first message to invite <b>@user:example.com</b> to chat</span></p></li></ol></div></div></div><div class="mx_RoomStatusBar mx_RoomStatusBar_unsentMessages"><div role="alert"><div class="mx_RoomStatusBar_unsentBadge"><div class="mx_NotificationBadge mx_NotificationBadge_visible mx_NotificationBadge_highlighted mx_NotificationBadge_2char"><span class="mx_NotificationBadge_count">!</span></div></div><div><div class="mx_RoomStatusBar_unsentTitle">Some of your messages have not been sent</div></div><div class="mx_RoomStatusBar_unsentButtonBar"><div role="button" tabindex="0" class="mx_AccessibleButton mx_RoomStatusBar_unsentRetry">Retry</div></div></div></div></main></div>"`;
|
exports[`RoomView for a local room in state ERROR should match the snapshot 1`] = `"<div class="mx_RoomView mx_RoomView--local"><header class="mx_RoomHeader light-panel"><div class="mx_RoomHeader_wrapper"><div class="mx_RoomHeader_avatar"><div class="mx_DecoratedRoomAvatar"><span class="mx_BaseAvatar" role="presentation"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 15.600000000000001px; width: 24px; line-height: 24px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" style="width: 24px; height: 24px;" aria-hidden="true"></span></div></div><div class="mx_E2EIcon mx_E2EIcon_normal mx_RoomHeader_icon"></div><div class="mx_RoomHeader_name mx_RoomHeader_name--textonly"><div dir="auto" class="mx_RoomHeader_nametext" title="@user:example.com" role="heading" aria-level="1">@user:example.com</div></div><div class="mx_RoomHeader_topic mx_RoomTopic" dir="auto"><div tabindex="0"><div><span dir="auto"></span></div></div></div></div></header><main class="mx_RoomView_body"><div class="mx_RoomView_timeline"><div class="mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel" tabindex="-1"><div class="mx_RoomView_messageListWrapper"><ol class="mx_RoomView_MessageList" aria-live="polite" style="height: 400px;"><li class="mx_NewRoomIntro"><div class="mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon_warning"><div class="mx_EventTileBubble_title">End-to-end encryption isn't enabled</div><div class="mx_EventTileBubble_subtitle"><span> Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. </span></div></div><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 33.800000000000004px; width: 52px; line-height: 52px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" style="width: 52px; height: 52px;" aria-hidden="true"></span><h2>@user:example.com</h2><p><span>Send your first message to invite <b>@user:example.com</b> to chat</span></p></li></ol></div></div></div><div class="mx_RoomStatusBar mx_RoomStatusBar_unsentMessages"><div role="alert"><div class="mx_RoomStatusBar_unsentBadge"><div class="mx_NotificationBadge mx_NotificationBadge_visible mx_NotificationBadge_highlighted mx_NotificationBadge_2char"><span class="mx_NotificationBadge_count">!</span></div></div><div><div class="mx_RoomStatusBar_unsentTitle">Some of your messages have not been sent</div></div><div class="mx_RoomStatusBar_unsentButtonBar"><div role="button" tabindex="0" class="mx_AccessibleButton mx_RoomStatusBar_unsentRetry">Retry</div></div></div></div></main></div>"`;
|
||||||
|
|
||||||
exports[`RoomView for a local room in state NEW should match the snapshot 1`] = `"<div class="mx_RoomView mx_RoomView--local"><header class="mx_RoomHeader light-panel"><div class="mx_RoomHeader_wrapper"><div class="mx_RoomHeader_avatar"><div class="mx_DecoratedRoomAvatar"><span class="mx_BaseAvatar" role="presentation"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 15.600000000000001px; width: 24px; line-height: 24px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" style="width: 24px; height: 24px;" aria-hidden="true"></span></div></div><div class="mx_E2EIcon mx_E2EIcon_normal mx_RoomHeader_icon"></div><div class="mx_RoomHeader_name mx_RoomHeader_name--textonly"><div dir="auto" class="mx_RoomHeader_nametext" title="@user:example.com" role="heading" aria-level="1">@user:example.com</div></div><div class="mx_RoomHeader_topic mx_RoomTopic" dir="auto"><div tabindex="0"><div><span dir="auto"></span></div></div></div></div></header><main class="mx_RoomView_body"><div class="mx_RoomView_timeline"><div class="mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel" tabindex="-1"><div class="mx_RoomView_messageListWrapper"><ol class="mx_RoomView_MessageList" aria-live="polite" style="height: 400px;"><li class="mx_NewRoomIntro"><div class="mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon_warning"><div class="mx_EventTileBubble_title">End-to-end encryption isn't enabled</div><div class="mx_EventTileBubble_subtitle"><span> Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. </span></div></div><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 33.800000000000004px; width: 52px; line-height: 52px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" style="width: 52px; height: 52px;" aria-hidden="true"></span><h2>@user:example.com</h2><p><span>Send your first message to invite <b>@user:example.com</b> to chat</span></p></li></ol></div></div></div><div class="mx_MessageComposer"><div class="mx_MessageComposer_wrapper"><div class="mx_MessageComposer_row"><div class="mx_SendMessageComposer"><div class="mx_BasicMessageComposer"><div class="mx_MessageComposerFormatBar"><button type="button" aria-label="Bold" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconBold"></button><button type="button" aria-label="Italics" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconItalic"></button><button type="button" aria-label="Strikethrough" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconStrikethrough"></button><button type="button" aria-label="Code block" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconCode"></button><button type="button" aria-label="Quote" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconQuote"></button><button type="button" aria-label="Insert link" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconInsertLink"></button></div><div class="mx_BasicMessageComposer_input mx_BasicMessageComposer_input_shouldShowPillAvatar mx_BasicMessageComposer_inputEmpty" contenteditable="true" tabindex="0" aria-label="Send a message…" role="textbox" aria-multiline="true" aria-autocomplete="list" aria-haspopup="listbox" dir="auto" aria-disabled="false" data-testid="basicmessagecomposer" style="--placeholder: 'Send a message…';"><div><br></div></div></div></div><div aria-label="Emoji" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_emoji"></div><div aria-label="Attachment" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_upload"></div><div aria-label="More options" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_buttonMenu"></div><input type="file" style="display: none;" multiple=""></div></div></div></main></div>"`;
|
exports[`RoomView for a local room in state NEW should match the snapshot 1`] = `"<div class="mx_RoomView mx_RoomView--local"><header class="mx_RoomHeader light-panel"><div class="mx_RoomHeader_wrapper"><div class="mx_RoomHeader_avatar"><div class="mx_DecoratedRoomAvatar"><span class="mx_BaseAvatar" role="presentation"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 15.600000000000001px; width: 24px; line-height: 24px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" style="width: 24px; height: 24px;" aria-hidden="true"></span></div></div><div class="mx_E2EIcon mx_E2EIcon_normal mx_RoomHeader_icon"></div><div class="mx_RoomHeader_name mx_RoomHeader_name--textonly"><div dir="auto" class="mx_RoomHeader_nametext" title="@user:example.com" role="heading" aria-level="1">@user:example.com</div></div><div class="mx_RoomHeader_topic mx_RoomTopic" dir="auto"><div tabindex="0"><div><span dir="auto"></span></div></div></div></div></header><main class="mx_RoomView_body"><div class="mx_RoomView_timeline"><div class="mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel" tabindex="-1"><div class="mx_RoomView_messageListWrapper"><ol class="mx_RoomView_MessageList" aria-live="polite" style="height: 400px;"><li class="mx_NewRoomIntro"><div class="mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon_warning"><div class="mx_EventTileBubble_title">End-to-end encryption isn't enabled</div><div class="mx_EventTileBubble_subtitle"><span> Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. </span></div></div><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 33.800000000000004px; width: 52px; line-height: 52px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" style="width: 52px; height: 52px;" aria-hidden="true"></span><h2>@user:example.com</h2><p><span>Send your first message to invite <b>@user:example.com</b> to chat</span></p></li></ol></div></div></div><div class="mx_MessageComposer"><div class="mx_MessageComposer_wrapper"><div class="mx_MessageComposer_row"><div class="mx_SendMessageComposer"><div class="mx_BasicMessageComposer"><div class="mx_MessageComposerFormatBar"><button type="button" aria-label="Bold" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconBold"></button><button type="button" aria-label="Italics" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconItalic"></button><button type="button" aria-label="Strikethrough" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconStrikethrough"></button><button type="button" aria-label="Code block" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconCode"></button><button type="button" aria-label="Quote" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconQuote"></button><button type="button" aria-label="Insert link" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconInsertLink"></button></div><div class="mx_BasicMessageComposer_input mx_BasicMessageComposer_input_shouldShowPillAvatar mx_BasicMessageComposer_inputEmpty" contenteditable="true" tabindex="0" aria-label="Send a message…" role="textbox" aria-multiline="true" aria-autocomplete="list" aria-haspopup="listbox" dir="auto" aria-disabled="false" data-testid="basicmessagecomposer" style="--placeholder: 'Send a message…';"><div><br></div></div></div></div><div class="mx_MessageComposer_actions"><div aria-label="Emoji" role="button" tabindex="0" class="mx_AccessibleButton mx_EmojiButton mx_MessageComposer_button mx_EmojiButton_icon"></div><div aria-label="Attachment" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_upload"></div><div aria-label="More options" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_buttonMenu"></div><input type="file" style="display: none;" multiple=""></div></div></div></div></main></div>"`;
|
||||||
|
|
||||||
exports[`RoomView for a local room in state NEW that is encrypted should match the snapshot 1`] = `"<div class="mx_RoomView mx_RoomView--local"><header class="mx_RoomHeader light-panel"><div class="mx_RoomHeader_wrapper"><div class="mx_RoomHeader_avatar"><div class="mx_DecoratedRoomAvatar"><span class="mx_BaseAvatar" role="presentation"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 15.600000000000001px; width: 24px; line-height: 24px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" style="width: 24px; height: 24px;" aria-hidden="true"></span></div></div><div class="mx_E2EIcon mx_E2EIcon_normal mx_RoomHeader_icon"></div><div class="mx_RoomHeader_name mx_RoomHeader_name--textonly"><div dir="auto" class="mx_RoomHeader_nametext" title="@user:example.com" role="heading" aria-level="1">@user:example.com</div></div><div class="mx_RoomHeader_topic mx_RoomTopic" dir="auto"><div tabindex="0"><div><span dir="auto"></span></div></div></div></div></header><main class="mx_RoomView_body"><div class="mx_RoomView_timeline"><div class="mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel" tabindex="-1"><div class="mx_RoomView_messageListWrapper"><ol class="mx_RoomView_MessageList" aria-live="polite" style="height: 400px;"><div class="mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon"><div class="mx_EventTileBubble_title">Encryption enabled</div><div class="mx_EventTileBubble_subtitle">Messages in this chat will be end-to-end encrypted.</div></div><li class="mx_NewRoomIntro"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 33.800000000000004px; width: 52px; line-height: 52px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" style="width: 52px; height: 52px;" aria-hidden="true"></span><h2>@user:example.com</h2><p><span>Send your first message to invite <b>@user:example.com</b> to chat</span></p></li></ol></div></div></div><div class="mx_MessageComposer"><div class="mx_MessageComposer_wrapper"><div class="mx_MessageComposer_row"><div class="mx_SendMessageComposer"><div class="mx_BasicMessageComposer"><div class="mx_MessageComposerFormatBar"><button type="button" aria-label="Bold" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconBold"></button><button type="button" aria-label="Italics" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconItalic"></button><button type="button" aria-label="Strikethrough" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconStrikethrough"></button><button type="button" aria-label="Code block" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconCode"></button><button type="button" aria-label="Quote" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconQuote"></button><button type="button" aria-label="Insert link" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconInsertLink"></button></div><div class="mx_BasicMessageComposer_input mx_BasicMessageComposer_input_shouldShowPillAvatar mx_BasicMessageComposer_inputEmpty" contenteditable="true" tabindex="0" aria-label="Send a message…" role="textbox" aria-multiline="true" aria-autocomplete="list" aria-haspopup="listbox" dir="auto" aria-disabled="false" data-testid="basicmessagecomposer" style="--placeholder: 'Send a message…';"><div><br></div></div></div></div><div aria-label="Emoji" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_emoji"></div><div aria-label="Attachment" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_upload"></div><div aria-label="More options" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_buttonMenu"></div><input type="file" style="display: none;" multiple=""></div></div></div></main></div>"`;
|
exports[`RoomView for a local room in state NEW that is encrypted should match the snapshot 1`] = `"<div class="mx_RoomView mx_RoomView--local"><header class="mx_RoomHeader light-panel"><div class="mx_RoomHeader_wrapper"><div class="mx_RoomHeader_avatar"><div class="mx_DecoratedRoomAvatar"><span class="mx_BaseAvatar" role="presentation"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 15.600000000000001px; width: 24px; line-height: 24px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" style="width: 24px; height: 24px;" aria-hidden="true"></span></div></div><div class="mx_E2EIcon mx_E2EIcon_normal mx_RoomHeader_icon"></div><div class="mx_RoomHeader_name mx_RoomHeader_name--textonly"><div dir="auto" class="mx_RoomHeader_nametext" title="@user:example.com" role="heading" aria-level="1">@user:example.com</div></div><div class="mx_RoomHeader_topic mx_RoomTopic" dir="auto"><div tabindex="0"><div><span dir="auto"></span></div></div></div></div></header><main class="mx_RoomView_body"><div class="mx_RoomView_timeline"><div class="mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel" tabindex="-1"><div class="mx_RoomView_messageListWrapper"><ol class="mx_RoomView_MessageList" aria-live="polite" style="height: 400px;"><div class="mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon"><div class="mx_EventTileBubble_title">Encryption enabled</div><div class="mx_EventTileBubble_subtitle">Messages in this chat will be end-to-end encrypted.</div></div><li class="mx_NewRoomIntro"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 33.800000000000004px; width: 52px; line-height: 52px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" style="width: 52px; height: 52px;" aria-hidden="true"></span><h2>@user:example.com</h2><p><span>Send your first message to invite <b>@user:example.com</b> to chat</span></p></li></ol></div></div></div><div class="mx_MessageComposer"><div class="mx_MessageComposer_wrapper"><div class="mx_MessageComposer_row"><div class="mx_SendMessageComposer"><div class="mx_BasicMessageComposer"><div class="mx_MessageComposerFormatBar"><button type="button" aria-label="Bold" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconBold"></button><button type="button" aria-label="Italics" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconItalic"></button><button type="button" aria-label="Strikethrough" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconStrikethrough"></button><button type="button" aria-label="Code block" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconCode"></button><button type="button" aria-label="Quote" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconQuote"></button><button type="button" aria-label="Insert link" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconInsertLink"></button></div><div class="mx_BasicMessageComposer_input mx_BasicMessageComposer_input_shouldShowPillAvatar mx_BasicMessageComposer_inputEmpty" contenteditable="true" tabindex="0" aria-label="Send a message…" role="textbox" aria-multiline="true" aria-autocomplete="list" aria-haspopup="listbox" dir="auto" aria-disabled="false" data-testid="basicmessagecomposer" style="--placeholder: 'Send a message…';"><div><br></div></div></div></div><div class="mx_MessageComposer_actions"><div aria-label="Emoji" role="button" tabindex="0" class="mx_AccessibleButton mx_EmojiButton mx_MessageComposer_button mx_EmojiButton_icon"></div><div aria-label="Attachment" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_upload"></div><div aria-label="More options" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_buttonMenu"></div><input type="file" style="display: none;" multiple=""></div></div></div></div></main></div>"`;
|
||||||
|
|
|
@ -28,6 +28,7 @@ import { createTestClient, flushPromises, getRoomContext, mkEvent, mkStubRoom }
|
||||||
import { SendWysiwygComposer } from "../../../../../src/components/views/rooms/wysiwyg_composer";
|
import { SendWysiwygComposer } from "../../../../../src/components/views/rooms/wysiwyg_composer";
|
||||||
import * as useComposerFunctions
|
import * as useComposerFunctions
|
||||||
from "../../../../../src/components/views/rooms/wysiwyg_composer/hooks/useComposerFunctions";
|
from "../../../../../src/components/views/rooms/wysiwyg_composer/hooks/useComposerFunctions";
|
||||||
|
import { aboveLeftOf } from "../../../../../src/components/structures/ContextMenu";
|
||||||
|
|
||||||
const mockClear = jest.fn();
|
const mockClear = jest.fn();
|
||||||
|
|
||||||
|
@ -78,7 +79,7 @@ describe('SendWysiwygComposer', () => {
|
||||||
return render(
|
return render(
|
||||||
<MatrixClientContext.Provider value={mockClient}>
|
<MatrixClientContext.Provider value={mockClient}>
|
||||||
<RoomContext.Provider value={defaultRoomContext}>
|
<RoomContext.Provider value={defaultRoomContext}>
|
||||||
<SendWysiwygComposer onChange={onChange} onSend={onSend} disabled={disabled} isRichTextEnabled={isRichTextEnabled} />
|
<SendWysiwygComposer onChange={onChange} onSend={onSend} disabled={disabled} isRichTextEnabled={isRichTextEnabled} menuPosition={aboveLeftOf({ top: 0, bottom: 0, right: 0 })} />
|
||||||
</RoomContext.Provider>
|
</RoomContext.Provider>
|
||||||
</MatrixClientContext.Provider>,
|
</MatrixClientContext.Provider>,
|
||||||
);
|
);
|
||||||
|
|
|
@ -21,10 +21,6 @@ import userEvent from "@testing-library/user-event";
|
||||||
import { PlainTextComposer }
|
import { PlainTextComposer }
|
||||||
from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/PlainTextComposer";
|
from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/PlainTextComposer";
|
||||||
|
|
||||||
// Work around missing ClipboardEvent type
|
|
||||||
class MyClipboardEvent {}
|
|
||||||
window.ClipboardEvent = MyClipboardEvent as any;
|
|
||||||
|
|
||||||
describe('PlainTextComposer', () => {
|
describe('PlainTextComposer', () => {
|
||||||
const customRender = (
|
const customRender = (
|
||||||
onChange = (_content: string) => void 0,
|
onChange = (_content: string) => void 0,
|
||||||
|
@ -91,4 +87,46 @@ describe('PlainTextComposer', () => {
|
||||||
// Then
|
// Then
|
||||||
expect(screen.getByRole('textbox').innerHTML).toBeFalsy();
|
expect(screen.getByRole('textbox').innerHTML).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Should have data-is-expanded when it has two lines', async () => {
|
||||||
|
let resizeHandler: ResizeObserverCallback = jest.fn();
|
||||||
|
let editor: Element | null = null;
|
||||||
|
jest.spyOn(global, 'ResizeObserver').mockImplementation((handler) => {
|
||||||
|
resizeHandler = handler;
|
||||||
|
return {
|
||||||
|
observe: (element) => {
|
||||||
|
editor = element;
|
||||||
|
},
|
||||||
|
unobserve: jest.fn(),
|
||||||
|
disconnect: jest.fn(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
jest.spyOn(global, 'requestAnimationFrame').mockImplementation(cb => {
|
||||||
|
cb(0);
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
//When
|
||||||
|
render(
|
||||||
|
<PlainTextComposer onChange={jest.fn()} onSend={jest.fn()} />,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(screen.getByTestId('WysiwygComposerEditor').attributes['data-is-expanded'].value).toBe('false');
|
||||||
|
expect(editor).toBe(screen.getByRole('textbox'));
|
||||||
|
|
||||||
|
// When
|
||||||
|
resizeHandler(
|
||||||
|
[{ contentBoxSize: [{ blockSize: 100 }] } as unknown as ResizeObserverEntry],
|
||||||
|
{} as ResizeObserver,
|
||||||
|
);
|
||||||
|
jest.runAllTimers();
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(screen.getByTestId('WysiwygComposerEditor').attributes['data-is-expanded'].value).toBe('true');
|
||||||
|
|
||||||
|
(global.ResizeObserver as jest.Mock).mockRestore();
|
||||||
|
(global.requestAnimationFrame as jest.Mock).mockRestore();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -23,10 +23,6 @@ import { WysiwygComposer }
|
||||||
from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer";
|
from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer";
|
||||||
import SettingsStore from "../../../../../../src/settings/SettingsStore";
|
import SettingsStore from "../../../../../../src/settings/SettingsStore";
|
||||||
|
|
||||||
// Work around missing ClipboardEvent type
|
|
||||||
class MyClipboardEvent {}
|
|
||||||
window.ClipboardEvent = MyClipboardEvent as any;
|
|
||||||
|
|
||||||
let inputEventProcessor: InputEventProcessor | null = null;
|
let inputEventProcessor: InputEventProcessor | null = null;
|
||||||
|
|
||||||
// The wysiwyg fetch wasm bytes and a specific workaround is needed to make it works in a node (jest) environnement
|
// The wysiwyg fetch wasm bytes and a specific workaround is needed to make it works in a node (jest) environnement
|
||||||
|
|
|
@ -31,6 +31,29 @@ class ResizeObserver {
|
||||||
}
|
}
|
||||||
window.ResizeObserver = ResizeObserver;
|
window.ResizeObserver = ResizeObserver;
|
||||||
|
|
||||||
|
// Stub DOMRect
|
||||||
|
class DOMRect {
|
||||||
|
x = 0;
|
||||||
|
y = 0;
|
||||||
|
top = 0;
|
||||||
|
bottom = 0;
|
||||||
|
left = 0;
|
||||||
|
right = 0;
|
||||||
|
height = 0;
|
||||||
|
width = 0;
|
||||||
|
|
||||||
|
static fromRect() {
|
||||||
|
return new DOMRect();
|
||||||
|
}
|
||||||
|
toJSON() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.DOMRect = DOMRect;
|
||||||
|
|
||||||
|
// Work around missing ClipboardEvent type
|
||||||
|
class MyClipboardEvent {}
|
||||||
|
window.ClipboardEvent = MyClipboardEvent as any;
|
||||||
|
|
||||||
// matchMedia is not included in jsdom
|
// matchMedia is not included in jsdom
|
||||||
const mockMatchMedia = jest.fn().mockImplementation(query => ({
|
const mockMatchMedia = jest.fn().mockImplementation(query => ({
|
||||||
matches: false,
|
matches: false,
|
||||||
|
@ -51,6 +74,7 @@ global.URL.revokeObjectURL = jest.fn();
|
||||||
// polyfilling TextEncoder as it is not available on JSDOM
|
// polyfilling TextEncoder as it is not available on JSDOM
|
||||||
// view https://github.com/facebook/jest/issues/9983
|
// view https://github.com/facebook/jest/issues/9983
|
||||||
global.TextEncoder = TextEncoder;
|
global.TextEncoder = TextEncoder;
|
||||||
|
// @ts-ignore
|
||||||
global.TextDecoder = TextDecoder;
|
global.TextDecoder = TextDecoder;
|
||||||
|
|
||||||
// prevent errors whenever a component tries to manually scroll.
|
// prevent errors whenever a component tries to manually scroll.
|
||||||
|
@ -60,4 +84,5 @@ window.HTMLElement.prototype.scrollIntoView = jest.fn();
|
||||||
fetchMock.config.overwriteRoutes = false;
|
fetchMock.config.overwriteRoutes = false;
|
||||||
fetchMock.catch("");
|
fetchMock.catch("");
|
||||||
fetchMock.get("/image-file-stub", "image file stub");
|
fetchMock.get("/image-file-stub", "image file stub");
|
||||||
|
// @ts-ignore
|
||||||
window.fetch = fetchMock.sandbox();
|
window.fetch = fetchMock.sandbox();
|
||||||
|
|
Loading…
Reference in a new issue