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/_BasicMessageComposer.pcss";
|
||||
@import "./views/rooms/_E2EIcon.pcss";
|
||||
@import "./views/rooms/_EmojiButton.pcss";
|
||||
@import "./views/rooms/_EditMessageComposer.pcss";
|
||||
@import "./views/rooms/_EntityTile.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.
|
||||
*/
|
||||
|
||||
@import "./_MessageComposerButton.pcss";
|
||||
|
||||
.mx_MessageComposer_wrapper {
|
||||
vertical-align: middle;
|
||||
margin: auto;
|
||||
|
@ -59,6 +61,12 @@ limitations under the License.
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.mx_MessageComposer_actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.mx_MessageComposer .mx_MessageComposer_avatar {
|
||||
position: absolute;
|
||||
left: 26px;
|
||||
|
@ -171,53 +179,16 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_MessageComposer_button_highlight {
|
||||
background: rgba($accent, 0.25);
|
||||
/* make the icon the accent color too */
|
||||
&::before {
|
||||
background-color: $accent !important;
|
||||
}
|
||||
@mixin composerButtonHighLight;
|
||||
}
|
||||
|
||||
.mx_MessageComposer_button {
|
||||
--size: 26px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
height: var(--size);
|
||||
line-height: var(--size);
|
||||
width: auto;
|
||||
padding-left: var(--size);
|
||||
border-radius: 50%;
|
||||
margin-right: 6px;
|
||||
@mixin composerButton 50%,$accent;
|
||||
|
||||
&:last-child {
|
||||
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 {
|
||||
&::after {
|
||||
background: rgba($accent, 0.1);
|
||||
|
@ -232,15 +203,43 @@ limitations under the License.
|
|||
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_e2eIcon.mx_E2EIcon,.mx_MessageComposer_button, .mx_MessageComposer_sendMessage {
|
||||
margin-top: 28px;
|
||||
.mx_MessageComposer_wrapper {
|
||||
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');
|
||||
}
|
||||
|
||||
.mx_MessageComposer_emoji::before {
|
||||
mask-image: url('$(res)/img/element-icons/room/composer/emoji.svg');
|
||||
}
|
||||
|
||||
.mx_MessageComposer_plain_text::before {
|
||||
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;
|
||||
border: 2px solid $voice-record-stop-border-color;
|
||||
border-radius: 32px;
|
||||
margin-right: 8px; /* between us and the waveform component */
|
||||
margin-right: 2px; /* between us and the waveform component */
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
|
@ -39,7 +39,7 @@ limitations under the License.
|
|||
width: 24px;
|
||||
height: 24px;
|
||||
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;
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
|
@ -69,7 +69,7 @@ limitations under the License.
|
|||
height: 32px;
|
||||
|
||||
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 */
|
||||
|
||||
|
@ -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 */
|
||||
/* 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 */
|
||||
|
|
|
@ -24,7 +24,7 @@ limitations under the License.
|
|||
gap: 8px;
|
||||
padding: 8px var(--EditWysiwygComposer-padding-inline);
|
||||
|
||||
.mx_WysiwygComposer_content {
|
||||
.mx_WysiwygComposer_Editor_content {
|
||||
border-radius: 4px;
|
||||
border: solid 1px $primary-hairline-color;
|
||||
background-color: $background;
|
||||
|
|
|
@ -22,24 +22,50 @@ limitations under the License.
|
|||
/* fixed line height to prevent emoji from being taller than text */
|
||||
line-height: $font-18px;
|
||||
justify-content: center;
|
||||
margin-right: 6px;
|
||||
/* don't grow wider than available space */
|
||||
min-width: 0;
|
||||
margin-right: 13px;
|
||||
gap: 8px;
|
||||
|
||||
.mx_WysiwygComposer_container {
|
||||
.mx_FormattingButtons {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.mx_WysiwygComposer_Editor {
|
||||
border: 1px solid;
|
||||
border-color: $quinary-content;
|
||||
padding: 6px 11px 6px 12px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 10px;
|
||||
|
||||
.mx_E2EIcon {
|
||||
margin: 0 0 7px 0;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
&[data-is-expanded="true"] {
|
||||
border-radius: 14px;
|
||||
|
||||
.mx_WysiwygComposer_Editor_container {
|
||||
margin-top: 3px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-is-expanded="false"] {
|
||||
border-radius: 40px;
|
||||
}
|
||||
|
||||
.mx_WysiwygComposer_Editor_container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* min-height at this level so the mx_BasicMessageComposer_input */
|
||||
/* 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;
|
||||
min-height: 22px;
|
||||
margin-bottom: 2px;
|
||||
/* don't grow wider than available space */
|
||||
width: 0;
|
||||
|
||||
.mx_WysiwygComposer_content {
|
||||
border: 1px solid;
|
||||
border-radius: 20px;
|
||||
padding: 8px 10px;
|
||||
.mx_WysiwygComposer_Editor_content {
|
||||
/* this will center the contenteditable */
|
||||
/* in it's parent vertically */
|
||||
/* while keeping the autocomplete at the top */
|
||||
|
@ -51,3 +77,10 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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.
|
||||
*/
|
||||
|
||||
.mx_WysiwygComposer_container {
|
||||
position: relative;
|
||||
|
||||
.mx_WysiwygComposer_Editor_container {
|
||||
@keyframes visualbell {
|
||||
from { background-color: $visual-bell-bg-color; }
|
||||
to { background-color: $background; }
|
||||
}
|
||||
|
||||
.mx_WysiwygComposer_content {
|
||||
.mx_WysiwygComposer_Editor_content {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
outline: none;
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
.mx_FormattingButtons {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
gap: 8px;
|
||||
|
||||
.mx_FormattingButtons_Button {
|
||||
--size: 28px;
|
||||
|
@ -26,18 +27,9 @@ limitations under the License.
|
|||
line-height: var(--size);
|
||||
width: auto;
|
||||
padding-left: 22px;
|
||||
margin-right: 8px;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
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">
|
||||
<g clip-path="url(#clip0_1456_146350)">
|
||||
<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"/>
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<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)"
|
||||
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>
|
||||
<clipPath id="clip0_1456_146350">
|
||||
<rect width="20" height="20" fill="white"/>
|
||||
<defs
|
||||
id="defs58">
|
||||
<clipPath
|
||||
id="clip0_1456_146365">
|
||||
<rect
|
||||
width="20"
|
||||
height="20"
|
||||
fill="white"
|
||||
id="rect55" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</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">
|
||||
<g clip-path="url(#clip0_1456_146365)">
|
||||
<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="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 clip-path="url(#clip0_1456_146350)">
|
||||
<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"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1456_146365">
|
||||
<clipPath id="clip0_1456_146350">
|
||||
<rect width="20" height="20" fill="white"/>
|
||||
</clipPath>
|
||||
</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.
|
||||
*/
|
||||
|
||||
import React, { createRef } from 'react';
|
||||
import React, { createRef, ReactNode } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { IEventRelation, MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
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 E2EIcon from './E2EIcon';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { aboveLeftOf, AboveLeftOf } from "../../structures/ContextMenu";
|
||||
import { aboveLeftOf } from "../../structures/ContextMenu";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import ReplyPreview from "./ReplyPreview";
|
||||
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);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const controls = [
|
||||
this.props.e2eStatus ?
|
||||
<E2EIcon key="e2eIcon" status={this.props.e2eStatus} className="mx_MessageComposer_e2eIcon" /> :
|
||||
null,
|
||||
];
|
||||
|
||||
let menuPosition: AboveLeftOf | undefined;
|
||||
private getMenuPosition() {
|
||||
if (this.ref.current) {
|
||||
const hasFormattingButtons = this.state.isWysiwygLabEnabled && this.state.isRichTextEnabled;
|
||||
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;
|
||||
let composer: ReactNode;
|
||||
if (canSendMessages) {
|
||||
if (this.state.isWysiwygLabEnabled) {
|
||||
controls.push(
|
||||
if (this.state.isWysiwygLabEnabled && menuPosition) {
|
||||
composer =
|
||||
<SendWysiwygComposer key="controls_input"
|
||||
disabled={this.state.haveRecording}
|
||||
onChange={this.onWysiwygChange}
|
||||
onSend={this.sendMessage}
|
||||
isRichTextEnabled={this.state.isRichTextEnabled}
|
||||
initialContent={this.state.initialComposerContent}
|
||||
/>,
|
||||
);
|
||||
e2eStatus={this.props.e2eStatus}
|
||||
menuPosition={menuPosition}
|
||||
/>;
|
||||
} else {
|
||||
controls.push(
|
||||
composer =
|
||||
<SendMessageComposer
|
||||
ref={this.messageComposerInput}
|
||||
key="controls_input"
|
||||
|
@ -458,8 +473,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
|||
onChange={this.onChange}
|
||||
disabled={this.state.haveRecording}
|
||||
toggleStickerPickerOpen={this.toggleStickerPickerOpen}
|
||||
/>,
|
||||
);
|
||||
/>;
|
||||
}
|
||||
|
||||
controls.push(<VoiceRecordComposerTile
|
||||
|
@ -529,8 +543,8 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
|||
const classes = classNames({
|
||||
"mx_MessageComposer": true,
|
||||
"mx_MessageComposer--compact": this.props.compact,
|
||||
"mx_MessageComposer_e2eStatus": this.props.e2eStatus != undefined,
|
||||
"mx_MessageComposer_wysiwyg": this.state.isWysiwygLabEnabled && this.state.isRichTextEnabled,
|
||||
"mx_MessageComposer_e2eStatus": hasE2EIcon,
|
||||
"mx_MessageComposer_wysiwyg": this.state.isWysiwygLabEnabled,
|
||||
});
|
||||
|
||||
return (
|
||||
|
@ -541,6 +555,9 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
|||
replyToEvent={this.props.replyToEvent}
|
||||
permalinkCreator={this.props.permalinkCreator} />
|
||||
<div className="mx_MessageComposer_row">
|
||||
{ e2eIcon }
|
||||
{ composer }
|
||||
<div className="mx_MessageComposer_actions">
|
||||
{ controls }
|
||||
{ canSendMessages && <MessageComposerButtons
|
||||
addEmoji={this.addEmoji}
|
||||
|
@ -559,7 +576,6 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
|||
showLocationButton={!window.electron}
|
||||
showPollsButton={this.state.showPollsButton}
|
||||
showStickersButton={this.showStickersButton}
|
||||
showComposerModeButton={this.state.isWysiwygLabEnabled}
|
||||
isRichTextEnabled={this.state.isRichTextEnabled}
|
||||
onComposerModeClick={this.onRichTextToggle}
|
||||
toggleButtonMenu={this.toggleButtonMenu}
|
||||
|
@ -583,6 +599,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,9 +25,8 @@ import { THREAD_RELATION_TYPE } from 'matrix-js-sdk/src/models/thread';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import { CollapsibleButton } from './CollapsibleButton';
|
||||
import ContextMenu, { aboveLeftOf, AboveLeftOf, useContextMenu } from '../../structures/ContextMenu';
|
||||
import { AboveLeftOf } from '../../structures/ContextMenu';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import EmojiPicker from '../emojipicker/EmojiPicker';
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
import LocationButton from '../location/LocationButton';
|
||||
import Modal from "../../../Modal";
|
||||
|
@ -39,6 +38,8 @@ import RoomContext from '../../../contexts/RoomContext';
|
|||
import { useDispatcher } from "../../../hooks/useDispatcher";
|
||||
import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds";
|
||||
import IconizedContextMenu, { IconizedContextMenuOptionList } from '../context_menus/IconizedContextMenu';
|
||||
import { EmojiButton } from './EmojiButton';
|
||||
import { useSettingValue } from '../../../hooks/useSettings';
|
||||
|
||||
interface IProps {
|
||||
addEmoji: (emoji: string) => boolean;
|
||||
|
@ -56,7 +57,6 @@ interface IProps {
|
|||
showVoiceBroadcastButton: boolean;
|
||||
onStartVoiceBroadcastClick: () => void;
|
||||
isRichTextEnabled: boolean;
|
||||
showComposerModeButton: boolean;
|
||||
onComposerModeClick: () => void;
|
||||
}
|
||||
|
||||
|
@ -67,6 +67,8 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
|
|||
const matrixClient: MatrixClient = useContext(MatrixClientContext);
|
||||
const { room, roomId, narrow } = useContext(RoomContext);
|
||||
|
||||
const isWysiwygLabEnabled = useSettingValue<boolean>('feature_wysiwyg_composer');
|
||||
|
||||
if (props.haveRecording) {
|
||||
return null;
|
||||
}
|
||||
|
@ -75,6 +77,8 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
|
|||
let moreButtons: ReactElement[];
|
||||
if (narrow) {
|
||||
mainButtons = [
|
||||
isWysiwygLabEnabled ?
|
||||
<ComposerModeButton key="composerModeButton" isRichTextEnabled={props.isRichTextEnabled} onClick={props.onComposerModeClick} /> :
|
||||
emojiButton(props),
|
||||
];
|
||||
moreButtons = [
|
||||
|
@ -87,9 +91,9 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
|
|||
];
|
||||
} else {
|
||||
mainButtons = [
|
||||
isWysiwygLabEnabled ?
|
||||
<ComposerModeButton key="composerModeButton" isRichTextEnabled={props.isRichTextEnabled} onClick={props.onComposerModeClick} /> :
|
||||
emojiButton(props),
|
||||
props.showComposerModeButton &&
|
||||
<ComposerModeButton key="composerModeButton" isRichTextEnabled={props.isRichTextEnabled} onClick={props.onComposerModeClick} />,
|
||||
uploadButton(), // props passed via UploadButtonContext
|
||||
];
|
||||
moreButtons = [
|
||||
|
@ -139,58 +143,10 @@ function emojiButton(props: IProps): ReactElement {
|
|||
key="emoji_button"
|
||||
addEmoji={props.addEmoji}
|
||||
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 {
|
||||
return <UploadButton key="controls_upload" />;
|
||||
}
|
||||
|
@ -408,7 +364,7 @@ interface 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
|
||||
className="mx_MessageComposer_button"
|
||||
|
|
|
@ -14,21 +14,28 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { forwardRef, RefObject } from 'react';
|
||||
import React, { ForwardedRef, forwardRef, MutableRefObject } from 'react';
|
||||
|
||||
import { useWysiwygSendActionHandler } from './hooks/useWysiwygSendActionHandler';
|
||||
import { WysiwygComposer } from './components/WysiwygComposer';
|
||||
import { PlainTextComposer } from './components/PlainTextComposer';
|
||||
import { ComposerFunctions } from './types';
|
||||
import { E2EStatus } from '../../../../utils/ShieldUtils';
|
||||
import E2EIcon from '../E2EIcon';
|
||||
import { EmojiButton } from '../EmojiButton';
|
||||
import { AboveLeftOf } from '../../../structures/ContextMenu';
|
||||
|
||||
interface ContentProps {
|
||||
disabled: boolean;
|
||||
disabled?: boolean;
|
||||
composerFunctions: ComposerFunctions;
|
||||
}
|
||||
|
||||
const Content = forwardRef<HTMLElement, ContentProps>(
|
||||
function Content({ disabled, composerFunctions }: ContentProps, forwardRef: RefObject<HTMLElement>) {
|
||||
useWysiwygSendActionHandler(disabled, forwardRef, composerFunctions);
|
||||
function Content(
|
||||
{ disabled = false, composerFunctions }: ContentProps,
|
||||
forwardRef: ForwardedRef<HTMLElement>,
|
||||
) {
|
||||
useWysiwygSendActionHandler(disabled, forwardRef as MutableRefObject<HTMLElement>, composerFunctions);
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
@ -37,14 +44,23 @@ interface SendWysiwygComposerProps {
|
|||
initialContent?: string;
|
||||
isRichTextEnabled: boolean;
|
||||
disabled?: boolean;
|
||||
e2eStatus?: E2EStatus;
|
||||
onChange: (content: string) => void;
|
||||
onSend: () => void;
|
||||
menuPosition: AboveLeftOf;
|
||||
}
|
||||
|
||||
export function SendWysiwygComposer({ isRichTextEnabled, ...props }: SendWysiwygComposerProps) {
|
||||
export function SendWysiwygComposer(
|
||||
{ isRichTextEnabled, e2eStatus, menuPosition, ...props }: SendWysiwygComposerProps) {
|
||||
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) => (
|
||||
<Content disabled={props.disabled} ref={ref} composerFunctions={composerFunctions} />
|
||||
) }
|
||||
|
|
|
@ -14,19 +14,33 @@ See the License for the specific language governing permissions and
|
|||
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 {
|
||||
disabled: boolean;
|
||||
leftComponent?: ReactNode;
|
||||
rightComponent?: ReactNode;
|
||||
}
|
||||
|
||||
export const Editor = memo(
|
||||
forwardRef<HTMLDivElement, EditorProps>(
|
||||
function Editor({ disabled }: EditorProps, ref,
|
||||
function Editor({ disabled, leftComponent, rightComponent }: EditorProps, ref,
|
||||
) {
|
||||
return <div className="mx_WysiwygComposer_container">
|
||||
<div className="mx_WysiwygComposer_content"
|
||||
ref={ref!}
|
||||
const isExpanded = useIsExpanded(ref as MutableRefObject<HTMLDivElement | null>, HEIGHT_BREAKING_POINT);
|
||||
|
||||
return <div
|
||||
data-testid="WysiwygComposerEditor"
|
||||
className="mx_WysiwygComposer_Editor"
|
||||
data-is-expanded={isExpanded}
|
||||
>
|
||||
{ leftComponent }
|
||||
<div className="mx_WysiwygComposer_Editor_container">
|
||||
<div className="mx_WysiwygComposer_Editor_content"
|
||||
ref={ref}
|
||||
contentEditable={!disabled}
|
||||
role="textbox"
|
||||
aria-multiline="true"
|
||||
|
@ -35,6 +49,8 @@ export const Editor = memo(
|
|||
dir="auto"
|
||||
aria-disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
{ rightComponent }
|
||||
</div>;
|
||||
},
|
||||
),
|
||||
|
|
|
@ -14,9 +14,11 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React, { MutableRefObject, ReactNode } from 'react';
|
||||
|
||||
import { useComposerFunctions } from '../hooks/useComposerFunctions';
|
||||
import { useIsFocused } from '../hooks/useIsFocused';
|
||||
import { usePlainTextInitialization } from '../hooks/usePlainTextInitialization';
|
||||
import { usePlainTextListeners } from '../hooks/usePlainTextListeners';
|
||||
import { useSetCursorPosition } from '../hooks/useSetCursorPosition';
|
||||
|
@ -26,9 +28,11 @@ import { Editor } from "./Editor";
|
|||
interface PlainTextComposerProps {
|
||||
disabled?: boolean;
|
||||
onChange?: (content: string) => void;
|
||||
onSend: () => void;
|
||||
onSend?: () => void;
|
||||
initialContent?: string;
|
||||
className?: string;
|
||||
leftComponent?: ReactNode;
|
||||
rightComponent?: ReactNode;
|
||||
children?: (
|
||||
ref: MutableRefObject<HTMLDivElement | null>,
|
||||
composerFunctions: ComposerFunctions,
|
||||
|
@ -36,21 +40,32 @@ interface PlainTextComposerProps {
|
|||
}
|
||||
|
||||
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 composerFunctions = useComposerFunctions(ref);
|
||||
usePlainTextInitialization(initialContent, ref);
|
||||
useSetCursorPosition(disabled, ref);
|
||||
const { isFocused, onFocus } = useIsFocused();
|
||||
|
||||
return <div
|
||||
data-testid="PlainTextComposer"
|
||||
className={className}
|
||||
className={classNames(className, { [`${className}-focused`]: isFocused })}
|
||||
onFocus={onFocus}
|
||||
onBlur={onFocus}
|
||||
onInput={onInput}
|
||||
onPaste={onPaste}
|
||||
onKeyDown={onKeyDown}
|
||||
>
|
||||
<Editor ref={ref} disabled={disabled} />
|
||||
<Editor ref={ref} disabled={disabled} leftComponent={leftComponent} rightComponent={rightComponent} />
|
||||
{ children?.(ref, composerFunctions) }
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -16,11 +16,13 @@ limitations under the License.
|
|||
|
||||
import React, { memo, MutableRefObject, ReactNode, useEffect } from 'react';
|
||||
import { useWysiwyg, FormattingFunctions } from "@matrix-org/matrix-wysiwyg";
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { FormattingButtons } from './FormattingButtons';
|
||||
import { Editor } from './Editor';
|
||||
import { useInputEventProcessor } from '../hooks/useInputEventProcessor';
|
||||
import { useSetCursorPosition } from '../hooks/useSetCursorPosition';
|
||||
import { useIsFocused } from '../hooks/useIsFocused';
|
||||
|
||||
interface WysiwygComposerProps {
|
||||
disabled?: boolean;
|
||||
|
@ -28,6 +30,8 @@ interface WysiwygComposerProps {
|
|||
onSend: () => void;
|
||||
initialContent?: string;
|
||||
className?: string;
|
||||
leftComponent?: ReactNode;
|
||||
rightComponent?: ReactNode;
|
||||
children?: (
|
||||
ref: MutableRefObject<HTMLDivElement | null>,
|
||||
wysiwyg: FormattingFunctions,
|
||||
|
@ -35,7 +39,16 @@ interface WysiwygComposerProps {
|
|||
}
|
||||
|
||||
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);
|
||||
|
||||
|
@ -51,10 +64,12 @@ export const WysiwygComposer = memo(function WysiwygComposer(
|
|||
const isReady = isWysiwygReady && !disabled;
|
||||
useSetCursorPosition(!isReady, ref);
|
||||
|
||||
const { isFocused, onFocus } = useIsFocused();
|
||||
|
||||
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} />
|
||||
<Editor ref={ref} disabled={!isReady} />
|
||||
<Editor ref={ref} disabled={!isReady} leftComponent={leftComponent} rightComponent={rightComponent} />
|
||||
{ children?.(ref, wysiwyg) }
|
||||
</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";
|
||||
|
||||
export function usePlainTextInitialization(initialContent: string, ref: RefObject<HTMLElement>) {
|
||||
export function usePlainTextInitialization(initialContent = '', ref: RefObject<HTMLElement>) {
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
ref.current.innerText = initialContent;
|
||||
|
|
|
@ -22,18 +22,18 @@ function isDivElement(target: EventTarget): target is HTMLDivElement {
|
|||
return target instanceof HTMLDivElement;
|
||||
}
|
||||
|
||||
export function usePlainTextListeners(onChange: (content: string) => void, onSend: () => void) {
|
||||
const ref = useRef<HTMLDivElement>();
|
||||
export function usePlainTextListeners(onChange?: (content: string) => void, onSend?: () => void) {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const send = useCallback((() => {
|
||||
if (ref.current) {
|
||||
ref.current.innerHTML = '';
|
||||
}
|
||||
onSend();
|
||||
onSend?.();
|
||||
}), [ref, onSend]);
|
||||
|
||||
const onInput = useCallback((event: SyntheticEvent<HTMLDivElement, InputEvent | ClipboardEvent>) => {
|
||||
if (isDivElement(event.target)) {
|
||||
onChange(event.target.innerHTML);
|
||||
onChange?.(event.target.innerHTML);
|
||||
}
|
||||
}, [onChange]);
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ export function useWysiwygEditActionHandler(
|
|||
composerElement: RefObject<HTMLElement>,
|
||||
) {
|
||||
const roomContext = useRoomContext();
|
||||
const timeoutId = useRef<number>();
|
||||
const timeoutId = useRef<number | null>(null);
|
||||
|
||||
const handler = useCallback((payload: ActionPayload) => {
|
||||
// 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.
|
||||
*/
|
||||
|
||||
import { RefObject, useCallback, useRef } from "react";
|
||||
import { MutableRefObject, useCallback, useRef } from "react";
|
||||
|
||||
import defaultDispatcher from "../../../../../dispatcher/dispatcher";
|
||||
import { Action } from "../../../../../dispatcher/actions";
|
||||
|
@ -26,16 +26,16 @@ import { ComposerFunctions } from "../types";
|
|||
|
||||
export function useWysiwygSendActionHandler(
|
||||
disabled: boolean,
|
||||
composerElement: RefObject<HTMLElement>,
|
||||
composerElement: MutableRefObject<HTMLElement>,
|
||||
composerFunctions: ComposerFunctions,
|
||||
) {
|
||||
const roomContext = useRoomContext();
|
||||
const timeoutId = useRef<number>();
|
||||
const timeoutId = useRef<number | null>(null);
|
||||
|
||||
const handler = useCallback((payload: ActionPayload) => {
|
||||
// don't let the user into the composer if it is disabled - all of these branches lead
|
||||
// to the cursor being in the composer
|
||||
if (disabled || !composerElement.current) return;
|
||||
if (disabled || !composerElement?.current) return;
|
||||
|
||||
const context = payload.context ?? TimelineRenderingType.Room;
|
||||
|
||||
|
|
|
@ -14,14 +14,16 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MutableRefObject } from "react";
|
||||
|
||||
import { TimelineRenderingType } from "../../../../../contexts/RoomContext";
|
||||
import { IRoomState } from "../../../../structures/RoomView";
|
||||
|
||||
export function focusComposer(
|
||||
composerElement: React.MutableRefObject<HTMLElement>,
|
||||
composerElement: MutableRefObject<HTMLElement | null>,
|
||||
renderingType: TimelineRenderingType,
|
||||
roomContext: IRoomState,
|
||||
timeoutId: React.MutableRefObject<number>,
|
||||
timeoutId: MutableRefObject<number | null>,
|
||||
) {
|
||||
if (renderingType === roomContext.timelineRenderingType) {
|
||||
// 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",
|
||||
"Everyone in this room is verified": "Everyone in this room is verified",
|
||||
"Edit message": "Edit message",
|
||||
"Emoji": "Emoji",
|
||||
"Mod": "Mod",
|
||||
"From a thread": "From a thread",
|
||||
"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",
|
||||
"%(seconds)ss left": "%(seconds)ss left",
|
||||
"Send voice message": "Send voice message",
|
||||
"Emoji": "Emoji",
|
||||
"Hide stickers": "Hide stickers",
|
||||
"Sticker": "Sticker",
|
||||
"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.",
|
||||
"Poll": "Poll",
|
||||
"Show plain text": "Show plain text",
|
||||
"Hide formatting": "Hide formatting",
|
||||
"Show formatting": "Show formatting",
|
||||
"Bold": "Bold",
|
||||
"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 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 * as useComposerFunctions
|
||||
from "../../../../../src/components/views/rooms/wysiwyg_composer/hooks/useComposerFunctions";
|
||||
import { aboveLeftOf } from "../../../../../src/components/structures/ContextMenu";
|
||||
|
||||
const mockClear = jest.fn();
|
||||
|
||||
|
@ -78,7 +79,7 @@ describe('SendWysiwygComposer', () => {
|
|||
return render(
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<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>
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
|
|
@ -21,10 +21,6 @@ import userEvent from "@testing-library/user-event";
|
|||
import { PlainTextComposer }
|
||||
from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/PlainTextComposer";
|
||||
|
||||
// Work around missing ClipboardEvent type
|
||||
class MyClipboardEvent {}
|
||||
window.ClipboardEvent = MyClipboardEvent as any;
|
||||
|
||||
describe('PlainTextComposer', () => {
|
||||
const customRender = (
|
||||
onChange = (_content: string) => void 0,
|
||||
|
@ -91,4 +87,46 @@ describe('PlainTextComposer', () => {
|
|||
// Then
|
||||
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";
|
||||
import SettingsStore from "../../../../../../src/settings/SettingsStore";
|
||||
|
||||
// Work around missing ClipboardEvent type
|
||||
class MyClipboardEvent {}
|
||||
window.ClipboardEvent = MyClipboardEvent as any;
|
||||
|
||||
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
|
||||
|
|
|
@ -31,6 +31,29 @@ class 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
|
||||
const mockMatchMedia = jest.fn().mockImplementation(query => ({
|
||||
matches: false,
|
||||
|
@ -51,6 +74,7 @@ global.URL.revokeObjectURL = jest.fn();
|
|||
// polyfilling TextEncoder as it is not available on JSDOM
|
||||
// view https://github.com/facebook/jest/issues/9983
|
||||
global.TextEncoder = TextEncoder;
|
||||
// @ts-ignore
|
||||
global.TextDecoder = TextDecoder;
|
||||
|
||||
// prevent errors whenever a component tries to manually scroll.
|
||||
|
@ -60,4 +84,5 @@ window.HTMLElement.prototype.scrollIntoView = jest.fn();
|
|||
fetchMock.config.overwriteRoutes = false;
|
||||
fetchMock.catch("");
|
||||
fetchMock.get("/image-file-stub", "image file stub");
|
||||
// @ts-ignore
|
||||
window.fetch = fetchMock.sandbox();
|
||||
|
|
Loading…
Reference in a new issue