Add formatting buttons for WysisygComposer

This commit is contained in:
Florian Duros 2022-10-13 12:20:31 +02:00
parent b336e18eae
commit 01858354f8
No known key found for this signature in database
GPG key ID: 9700AA5870258A0B
14 changed files with 271 additions and 24 deletions

View file

@ -295,6 +295,7 @@
@import "./views/rooms/_TopUnreadMessagesBar.pcss";
@import "./views/rooms/_VoiceRecordComposerTile.pcss";
@import "./views/rooms/_WhoIsTypingTile.pcss";
@import "./views/rooms/wysiwyg_composer/_FormattingButtons.pcss";
@import "./views/rooms/wysiwyg_composer/_WysiwygComposer.pcss";
@import "./views/settings/_AvatarSetting.pcss";
@import "./views/settings/_CrossSigningPanel.pcss";

View file

@ -233,6 +233,17 @@ limitations under the License.
}
}
/*
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: 22px;
}
}
.mx_MessageComposer_upload::before {
mask-image: url('$(res)/img/element-icons/room/composer/attach.svg');
}

View file

@ -0,0 +1,114 @@
/*
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.
*/
.mx_FormattingButtons {
display: flex;
justify-content: start;
.mx_FormattingButtons_Button {
--size: 26px;
position: relative;
cursor: pointer;
height: var(--size);
line-height: var(--size);
width: auto;
padding-left: var(--size);
margin-right: 6px;
background-color: transparent;
border: none;
&:last-child {
margin-right: auto;
}
&::before {
content: '';
position: absolute;
top: 7px;
left: 7px;
height: 12px;
width: 12px;
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: 5px;
}
&:hover {
&::after {
background: rgba($secondary-content, 0.1);
}
&::before {
background-color: $secondary-content;
}
}
}
.mx_FormattingButtons_active {
&::after {
background: rgba($accent, 0.1);
}
&::before {
background-color: $accent;
}
}
.mx_FormattingButtons_Button_bold::before {
mask-image: url('$(res)/img/element-icons/room/composer/bold.svg');
}
.mx_FormattingButtons_Button_italic::before {
mask-image: url('$(res)/img/element-icons/room/composer/italic.svg');
}
.mx_FormattingButtons_Button_underline::before {
mask-image: url('$(res)/img/element-icons/room/composer/underline.svg');
}
.mx_FormattingButtons_Button_strike-through::before {
mask-image: url('$(res)/img/element-icons/room/composer/strike_through.svg');
}
}
.mx_FormattingButtons_Tooltip {
padding: 0 2px 0 2px;
.mx_FormattingButtons_Tooltip_KeyboardShortcut {
color: $tertiary-content;
kbd {
margin-top: 2px;
text-align: center;
display: inline-block;
text-transform: capitalize;
font-size: 12px;
font-family: Inter, sans-serif;
}
}
}

View file

@ -0,0 +1,3 @@
<svg width="9" height="12" viewBox="0 0 9 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 1C0 0.447715 0.447715 0 1 0H4.19231C6.09295 0 7.5 1.64388 7.5 3.5C7.5 4.25349 7.26813 4.97201 6.86549 5.55977C7.84346 6.1788 8.5 7.25485 8.5 8.5C8.5 10.4594 6.87427 12 4.92857 12H1C0.447715 12 0 11.5523 0 11V1ZM2 2V5H4.19231C4.84067 5 5.5 4.4053 5.5 3.5C5.5 2.5947 4.84067 2 4.19231 2H2ZM2 7V10H4.92857C5.82319 10 6.5 9.30206 6.5 8.5C6.5 7.69794 5.82319 7 4.92857 7H2Z" fill="#737D8C"/>
</svg>

After

Width:  |  Height:  |  Size: 544 B

View file

@ -0,0 +1,3 @@
<svg width="8" height="13" viewBox="0 0 8 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.08599 1.60016L2.28071 10.4045H0.8C0.358172 10.4045 0 10.7627 0 11.2045C0 11.6464 0.358172 12.0045 0.8 12.0045H2.92107C2.92982 12.0047 2.93855 12.0047 2.94725 12.0045H5.06667C5.50849 12.0045 5.86667 11.6464 5.86667 11.2045C5.86667 10.7627 5.50849 10.4045 5.06667 10.4045H3.914L5.71927 1.60016H7.2C7.64183 1.60016 8 1.24199 8 0.800158C8 0.358331 7.64183 0.00015831 7.2 0.00015831H5.08171C5.0711 -5.33571e-05 5.06051 -5.22589e-05 5.04996 0.00015831H2.93333C2.4915 0.00015831 2.13333 0.358331 2.13333 0.800158C2.13333 1.24199 2.4915 1.60016 2.93333 1.60016H4.08599Z" fill="#8E99A4"/>
</svg>

After

Width:  |  Height:  |  Size: 733 B

View file

@ -0,0 +1,4 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.93167 2.76958C7.48979 1.88101 6.58375 1.47362 5.58232 1.58148C4.03349 1.74829 3.62648 2.94831 3.81497 3.72822C4.02118 4.58143 4.69765 4.91317 5.89252 5.21717H11.28C11.6776 5.21717 12 5.56757 12 5.99981C12 6.43204 11.6776 6.78244 11.28 6.78244H0.72C0.322355 6.78244 0 6.43204 0 5.99981C0 5.56757 0.322355 5.21717 0.72 5.21717H2.90308C2.69392 4.91824 2.52701 4.55948 2.42223 4.12592C2.0021 2.38757 3.03605 0.282791 5.44033 0.0238381C6.85635 -0.128674 8.41032 0.440447 9.19844 2.02524C9.38753 2.40548 9.25724 2.88035 8.90743 3.08589C8.55763 3.29143 8.12076 3.14981 7.93167 2.76958Z" fill="#8E99A4"/>
<path d="M8.28458 8.08683H9.77971C9.92538 8.87051 9.8142 9.70668 9.36651 10.4212C8.74261 11.4169 7.57984 12 5.98987 12C3.38435 12 2.18628 10.3895 1.94151 9.32405C1.84516 8.90469 2.07981 8.47984 2.4656 8.37511C2.8514 8.27038 3.24225 8.52544 3.3386 8.9448C3.41285 9.268 4.00136 10.4347 5.98987 10.4347C7.27118 10.4347 7.90296 9.97636 8.17634 9.54006C8.42836 9.13783 8.47197 8.60621 8.28458 8.08683Z" fill="#8E99A4"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,3 @@
<svg width="10" height="12" viewBox="0 0 10 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.19333 9.3C7.21333 9.04 8.66667 7.22667 8.66667 5.19333V0.833333C8.66667 0.373333 8.29333 0 7.83333 0C7.37333 0 7 0.373333 7 0.833333V5.26667C7 6.38 6.24667 7.39333 5.15333 7.61333C3.65333 7.92667 2.33333 6.78 2.33333 5.33333V0.833333C2.33333 0.373333 1.96 0 1.5 0C1.04 0 0.666667 0.373333 0.666667 0.833333V5.33333C0.666667 7.71333 2.75333 9.61333 5.19333 9.3ZM0 11.3333C0 11.7 0.3 12 0.666667 12H8.66667C9.03333 12 9.33333 11.7 9.33333 11.3333C9.33333 10.9667 9.03333 10.6667 8.66667 10.6667H0.666667C0.3 10.6667 0 10.9667 0 11.3333Z" fill="#8E99A4"/>
</svg>

After

Width:  |  Height:  |  Size: 668 B

View file

@ -389,6 +389,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
}
public render() {
const isWysiwygComposerEnabled = SettingsStore.getValue("feature_wysiwyg_composer");
const controls = [
this.props.e2eStatus ?
<E2EIcon key="e2eIcon" status={this.props.e2eStatus} className="mx_MessageComposer_e2eIcon" /> :
@ -403,8 +404,6 @@ export default class MessageComposer extends React.Component<IProps, IState> {
const canSendMessages = this.context.canSendMessages && !this.context.tombstone;
if (canSendMessages) {
const isWysiwygComposerEnabled = SettingsStore.getValue("feature_wysiwyg_composer");
if (isWysiwygComposerEnabled) {
controls.push(
<WysiwygComposer key="controls_input"
@ -503,6 +502,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
"mx_MessageComposer": true,
"mx_MessageComposer--compact": this.props.compact,
"mx_MessageComposer_e2eStatus": this.props.e2eStatus != undefined,
"mx_MessageComposer_wysiwyg": isWysiwygComposerEnabled,
});
return (

View file

@ -0,0 +1,41 @@
/*
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 React, { forwardRef, memo } from 'react';
interface EditorProps {
disabled: boolean;
}
export const Editor = memo(
forwardRef<HTMLDivElement, EditorProps>(
function Editor({ disabled }: EditorProps, ref,
) {
return <div className="mx_WysiwygComposer_container">
<div className="mx_WysiwygComposer_content"
ref={ref}
contentEditable={!disabled}
role="textbox"
aria-multiline="true"
aria-autocomplete="list"
aria-haspopup="listbox"
dir="auto"
aria-disabled={disabled}
/>
</div>;
},
),
);

View file

@ -0,0 +1,69 @@
/*
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 React from "react";
import { useWysiwyg } from "@matrix-org/matrix-wysiwyg";
import classNames from "classnames";
import AccessibleTooltipButton from "../../elements/AccessibleTooltipButton";
import { Alignment } from "../../elements/Tooltip";
import { KeyboardShortcut } from "../../settings/KeyboardShortcut";
import { KeyCombo } from "../../../../KeyBindingsManager";
import { _td } from "../../../../languageHandler";
interface TooltipProps {
label: string;
keyCombo?: KeyCombo;
}
function Tooltip({ label, keyCombo }: TooltipProps) {
return <div className="mx_FormattingButtons_Tooltip">
{ label }
{ keyCombo && <KeyboardShortcut value={keyCombo} className="mx_FormattingButtons_Tooltip_KeyboardShortcut" /> }
</div>;
}
interface ButtonProps extends TooltipProps {
className: string;
isActive: boolean;
onClick: () => void;
}
function Button({ label, keyCombo, onClick, isActive, className }: ButtonProps) {
return <AccessibleTooltipButton
element="button"
onClick={onClick}
title={label}
className={
classNames('mx_FormattingButtons_Button', className, { 'mx_FormattingButtons_active': isActive })}
tooltip={keyCombo && <Tooltip label={label} keyCombo={keyCombo} />}
alignment={Alignment.Top}
/>;
}
interface FormattingButtonsProps {
wysiwyg: ReturnType<typeof useWysiwyg>['wysiwyg'];
formattingStates: ReturnType<typeof useWysiwyg>['formattingStates'];
}
export function FormattingButtons({ wysiwyg, formattingStates }: FormattingButtonsProps) {
return <div className="mx_FormattingButtons">
<Button isActive={formattingStates.bold === 'reversed'} label={_td("Bold")} keyCombo={{ ctrlOrCmdKey: true, key: 'b' }} onClick={() => wysiwyg.bold()} className="mx_FormattingButtons_Button_bold" />
<Button isActive={formattingStates.italic === 'reversed'} label={_td('Italic')} keyCombo={{ ctrlOrCmdKey: true, key: 'i' }} onClick={() => wysiwyg.italic()} className="mx_FormattingButtons_Button_italic" />
<Button isActive={formattingStates.underline === 'reversed'} label={_td('Underline')} keyCombo={{ ctrlOrCmdKey: true, key: 'u' }} onClick={() => wysiwyg.underline()} className="mx_FormattingButtons_Button_underline" />
<Button isActive={formattingStates.strikeThrough === 'reversed'} label={_td('Strike through')} onClick={() => wysiwyg.strikeThrough()} className="mx_FormattingButtons_Button_strike-through" />
</div>;
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useCallback, useState } from 'react';
import React, { useCallback, useEffect } from 'react';
import { useWysiwyg } from "@matrix-org/matrix-wysiwyg";
import { IEventRelation, MatrixEvent } from 'matrix-js-sdk/src/models/event';
@ -22,6 +22,8 @@ import { useRoomContext } from '../../../../contexts/RoomContext';
import { sendMessage } from './message';
import { RoomPermalinkCreator } from '../../../../utils/permalinks/Permalinks';
import { useMatrixClientContext } from '../../../../contexts/MatrixClientContext';
import { FormattingButtons } from './FormattingButtons';
import { Editor } from './Editor';
interface WysiwygProps {
disabled?: boolean;
@ -39,11 +41,13 @@ export function WysiwygComposer(
const roomContext = useRoomContext();
const mxClient = useMatrixClientContext();
const [content, setContent] = useState<string>();
const { ref, isWysiwygReady, wysiwyg } = useWysiwyg({ onChange: (_content) => {
setContent(_content);
onChange(_content);
} });
const { ref, isWysiwygReady, content, formattingStates, wysiwyg } = useWysiwyg();
useEffect(() => {
if (content !== null) {
onChange(content);
}
}, [onChange, content]);
const memoizedSendMessage = useCallback(() => {
sendMessage(content, { mxClient, roomContext, ...props });
@ -53,18 +57,8 @@ export function WysiwygComposer(
return (
<div className="mx_WysiwygComposer">
<div className="mx_WysiwygComposer_container">
<div className="mx_WysiwygComposer_content"
ref={ref}
contentEditable={!disabled && isWysiwygReady}
role="textbox"
aria-multiline="true"
aria-autocomplete="list"
aria-haspopup="listbox"
dir="auto"
aria-disabled={disabled || !isWysiwygReady}
/>
</div>
<FormattingButtons wysiwyg={wysiwyg} formattingStates={formattingStates} />
<Editor ref={ref} disabled={!isWysiwygReady || disabled} />
{ children?.(memoizedSendMessage) }
</div>
);

View file

@ -38,9 +38,10 @@ export const KeyboardKey: React.FC<IKeyboardKeyProps> = ({ name, last }) => {
interface IKeyboardShortcutProps {
value: KeyCombo;
className?: string;
}
export const KeyboardShortcut: React.FC<IKeyboardShortcutProps> = ({ value }) => {
export const KeyboardShortcut: React.FC<IKeyboardShortcutProps> = ({ value, className = 'mx_KeyboardShortcut' }) => {
if (!value) return null;
const modifiersElement = [];
@ -58,7 +59,7 @@ export const KeyboardShortcut: React.FC<IKeyboardShortcutProps> = ({ value }) =>
modifiersElement.push(<KeyboardKey key="shiftKey" name={Key.SHIFT} />);
}
return <div className="mx_KeyboardShortcut">
return <div className={className}>
{ modifiersElement }
<KeyboardKey name={value.key} last />
</div>;

View file

@ -901,7 +901,7 @@
"How can I leave the beta?": "How can I leave the beta?",
"To leave, return to this page and use the “%(leaveTheBeta)s” button.": "To leave, return to this page and use the “%(leaveTheBeta)s” button.",
"Leave the beta": "Leave the beta",
"Wysiwyg composer (plain text mode coming soon) (under active development)": "Wysiwyg composer (plain text mode coming soon) (under active development)",
"Try out the rich text editor (plain text mode coming soon)": "Try out the rich text editor (plain text mode coming soon)",
"Render simple counters in room header": "Render simple counters in room header",
"Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)",
"Support adding custom themes": "Support adding custom themes",
@ -2054,6 +2054,9 @@
"No microphone found": "No microphone found",
"We didn't find a microphone on your device. Please check your settings and try again.": "We didn't find a microphone on your device. Please check your settings and try again.",
"Stop recording": "Stop recording",
"Italic": "Italic",
"Underline": "Underline",
"Strike through": "Strike through",
"Error updating main address": "Error updating main address",
"There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.": "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.",
"There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.": "There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.",

View file

@ -306,7 +306,7 @@ export const SETTINGS: {[setting: string]: ISetting} = {
"feature_wysiwyg_composer": {
isFeature: true,
labsGroup: LabGroup.Messaging,
displayName: _td("Wysiwyg composer (plain text mode coming soon) (under active development)"),
displayName: _td("Try out the rich text editor (plain text mode coming soon)"),
supportedLevels: LEVELS_FEATURE,
default: false,
},