Merge branch 'develop' into kegan/lists-as-keys
commit
fcde4b7880
|
@ -1,3 +1,9 @@
|
|||
Changes in [3.64.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.64.1) (2023-01-18)
|
||||
=====================================================================================================
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix crash in older browsers (replace .at() with array.length-1) ([\#9933](https://github.com/matrix-org/matrix-react-sdk/pull/9933)). Fixes matrix-org/element-web-rageshakes#19281.
|
||||
|
||||
Changes in [3.64.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.64.0) (2023-01-18)
|
||||
=====================================================================================================
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ import type { ISasEvent } from "matrix-js-sdk/src/crypto/verification/SAS";
|
|||
import type { CypressBot } from "../../support/bot";
|
||||
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||
import Chainable = Cypress.Chainable;
|
||||
import { UserCredentials } from "../../support/login";
|
||||
|
||||
type EmojiMapping = [emoji: string, name: string];
|
||||
interface CryptoTestContext extends Mocha.Context {
|
||||
|
@ -154,11 +155,15 @@ const verify = function (this: CryptoTestContext) {
|
|||
};
|
||||
|
||||
describe("Cryptography", function () {
|
||||
let aliceCredentials: UserCredentials;
|
||||
|
||||
beforeEach(function () {
|
||||
cy.startHomeserver("default")
|
||||
.as("homeserver")
|
||||
.then((homeserver: HomeserverInstance) => {
|
||||
cy.initTestUser(homeserver, "Alice", undefined, "alice_");
|
||||
cy.initTestUser(homeserver, "Alice", undefined, "alice_").then((credentials) => {
|
||||
aliceCredentials = credentials;
|
||||
});
|
||||
cy.getBot(homeserver, { displayName: "Bob", autoAcceptInvites: false, userIdPrefix: "bob_" }).as("bob");
|
||||
});
|
||||
});
|
||||
|
@ -183,7 +188,7 @@ describe("Cryptography", function () {
|
|||
});
|
||||
|
||||
it("creating a DM should work, being e2e-encrypted / user verification", function (this: CryptoTestContext) {
|
||||
cy.bootstrapCrossSigning();
|
||||
cy.bootstrapCrossSigning(aliceCredentials);
|
||||
startDMWithBob.call(this);
|
||||
// send first message
|
||||
cy.get(".mx_BasicMessageComposer_input").click().should("have.focus").type("Hey!{enter}");
|
||||
|
@ -194,7 +199,7 @@ describe("Cryptography", function () {
|
|||
});
|
||||
|
||||
it("should allow verification when there is no existing DM", function (this: CryptoTestContext) {
|
||||
cy.bootstrapCrossSigning();
|
||||
cy.bootstrapCrossSigning(aliceCredentials);
|
||||
autoJoin(this.bob);
|
||||
|
||||
// we need to have a room with the other user present, so we can open the verification panel
|
||||
|
@ -212,7 +217,7 @@ describe("Cryptography", function () {
|
|||
});
|
||||
|
||||
it("should show the correct shield on edited e2e events", function (this: CryptoTestContext) {
|
||||
cy.bootstrapCrossSigning();
|
||||
cy.bootstrapCrossSigning(aliceCredentials);
|
||||
|
||||
// bob has a second, not cross-signed, device
|
||||
cy.loginBot(this.homeserver, this.bob.getUserId(), this.bob.__cypress_password, {}).as("bobSecondDevice");
|
||||
|
|
|
@ -105,15 +105,9 @@ describe("Decryption Failure Bar", () => {
|
|||
"and there are other verified devices or backups",
|
||||
() => {
|
||||
let otherDevice: MatrixClient | undefined;
|
||||
cy.loginBot(homeserver, testUser.username, testUser.password, {})
|
||||
cy.loginBot(homeserver, testUser.username, testUser.password, { bootstrapCrossSigning: true })
|
||||
.then(async (cli) => {
|
||||
otherDevice = cli;
|
||||
await otherDevice.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async (makeRequest) => {
|
||||
await makeRequest({});
|
||||
},
|
||||
setupNewCrossSigning: true,
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
cy.botSendMessage(bot, roomId, "test");
|
||||
|
@ -169,15 +163,11 @@ describe("Decryption Failure Bar", () => {
|
|||
"should prompt the user to reset keys, if this device isn't verified " +
|
||||
"and there are no other verified devices or backups",
|
||||
() => {
|
||||
cy.loginBot(homeserver, testUser.username, testUser.password, {}).then(async (cli) => {
|
||||
await cli.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async (makeRequest) => {
|
||||
await makeRequest({});
|
||||
},
|
||||
setupNewCrossSigning: true,
|
||||
});
|
||||
await cli.logout(true);
|
||||
});
|
||||
cy.loginBot(homeserver, testUser.username, testUser.password, { bootstrapCrossSigning: true }).then(
|
||||
async (cli) => {
|
||||
await cli.logout(true);
|
||||
},
|
||||
);
|
||||
|
||||
cy.botSendMessage(bot, roomId, "test");
|
||||
cy.wait(5000);
|
||||
|
|
|
@ -150,7 +150,14 @@ function setupBotClient(
|
|||
if (opts.bootstrapCrossSigning) {
|
||||
await cli.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async (func) => {
|
||||
await func({});
|
||||
await func({
|
||||
type: "m.login.password",
|
||||
identifier: {
|
||||
type: "m.id.user",
|
||||
user: credentials.userId,
|
||||
},
|
||||
password: credentials.password,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import type { MatrixClient } from "matrix-js-sdk/src/client";
|
|||
import type { Room } from "matrix-js-sdk/src/models/room";
|
||||
import type { IContent } from "matrix-js-sdk/src/models/event";
|
||||
import Chainable = Cypress.Chainable;
|
||||
import { UserCredentials } from "./login";
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
|
@ -119,7 +120,7 @@ declare global {
|
|||
/**
|
||||
* Boostraps cross-signing.
|
||||
*/
|
||||
bootstrapCrossSigning(): Chainable<void>;
|
||||
bootstrapCrossSigning(credendtials: UserCredentials): Chainable<void>;
|
||||
/**
|
||||
* Joins the given room by alias or ID
|
||||
* @param roomIdOrAlias the id or alias of the room to join
|
||||
|
@ -210,11 +211,18 @@ Cypress.Commands.add("setAvatarUrl", (url: string): Chainable<{}> => {
|
|||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add("bootstrapCrossSigning", () => {
|
||||
Cypress.Commands.add("bootstrapCrossSigning", (credentials: UserCredentials) => {
|
||||
cy.window({ log: false }).then((win) => {
|
||||
win.mxMatrixClientPeg.matrixClient.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async (func) => {
|
||||
await func({});
|
||||
await func({
|
||||
type: "m.login.password",
|
||||
identifier: {
|
||||
type: "m.id.user",
|
||||
user: credentials.userId,
|
||||
},
|
||||
password: credentials.password,
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -76,6 +76,7 @@ export interface Credentials {
|
|||
userId: string;
|
||||
deviceId: string;
|
||||
homeServer: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
function registerUser(
|
||||
|
@ -120,6 +121,7 @@ function registerUser(
|
|||
accessToken: response.body.access_token,
|
||||
userId: response.body.user_id,
|
||||
deviceId: response.body.device_id,
|
||||
password: password,
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "matrix-react-sdk",
|
||||
"version": "3.64.0",
|
||||
"version": "3.64.1",
|
||||
"description": "SDK for matrix.org using React",
|
||||
"author": "matrix.org",
|
||||
"repository": {
|
||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
|
||||
/* 1rem :: 10px */
|
||||
|
||||
$spacing-2: 2px;
|
||||
$spacing-4: 4px;
|
||||
$spacing-8: 8px;
|
||||
$spacing-12: 12px;
|
||||
|
|
|
@ -548,7 +548,19 @@ $left-gutter: 64px;
|
|||
pre,
|
||||
code {
|
||||
font-family: $monospace-font-family !important;
|
||||
background-color: $codeblock-background-color;
|
||||
background-color: $system;
|
||||
}
|
||||
|
||||
code:not(pre *) {
|
||||
background-color: $inlinecode-background-color;
|
||||
border: 1px solid $inlinecode-border-color;
|
||||
border-radius: 4px;
|
||||
// The horizontal padding is added by gfm.css .markdown-body
|
||||
padding: $spacing-2 0;
|
||||
// Avoid inline code blocks to be sticked when on multiple lines
|
||||
line-height: $font-22px;
|
||||
// Avoid the border to be glued to the other words
|
||||
margin-right: $spacing-2;
|
||||
}
|
||||
|
||||
code {
|
||||
|
@ -566,6 +578,8 @@ $left-gutter: 64px;
|
|||
background: transparent;
|
||||
}
|
||||
|
||||
border: 1px solid $quinary-content;
|
||||
|
||||
code {
|
||||
white-space: pre; /* we want code blocks to be scrollable and not wrap */
|
||||
|
||||
|
@ -744,6 +758,8 @@ $left-gutter: 64px;
|
|||
|
||||
.mx_EventTile_collapsedCodeBlock {
|
||||
max-height: 30vh;
|
||||
padding-top: $spacing-12;
|
||||
padding-bottom: $spacing-12;
|
||||
}
|
||||
|
||||
/* Inserted adjacent to <pre> blocks, (See TextualBody) */
|
||||
|
|
|
@ -46,9 +46,30 @@ limitations under the License.
|
|||
|
||||
// model output always includes a linebreak but we do not want the user
|
||||
// to see it when writing input in lists
|
||||
:is(ol, ul) + br:last-of-type {
|
||||
:is(ol, ul, pre) + br:last-of-type {
|
||||
display: none;
|
||||
}
|
||||
|
||||
> pre {
|
||||
font-size: $font-15px;
|
||||
line-height: $font-24px;
|
||||
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
padding: $spacing-8 $spacing-12;
|
||||
|
||||
background-color: $inlinecode-background-color;
|
||||
border: 1px solid $inlinecode-border-color;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: $monospace-font-family !important;
|
||||
background-color: $inlinecode-background-color;
|
||||
border: 1px solid $inlinecode-border-color;
|
||||
border-radius: 4px;
|
||||
padding: $spacing-2;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_WysiwygComposer_Editor_content_placeholder::before {
|
||||
|
|
|
@ -50,6 +50,12 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_FormattingButtons_disabled {
|
||||
.mx_FormattingButtons_Icon {
|
||||
color: $quinary-content;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_FormattingButtons_Icon {
|
||||
--size: 16px;
|
||||
height: var(--size);
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.14288 6.99997L5.47622 5.66663C5.5905 5.55235 5.64765 5.41266 5.64765 5.24758C5.64765 5.0825 5.5905 4.94282 5.47622 4.82854C5.36193 4.71425 5.22225 4.65711 5.05717 4.65711C4.89209 4.65711 4.75241 4.71425 4.63812 4.82854L2.86669 6.59997C2.8032 6.66346 2.75876 6.72695 2.73336 6.79044C2.70796 6.85393 2.69526 6.92377 2.69526 6.99997C2.69526 7.07616 2.70796 7.146 2.73336 7.20949C2.75876 7.27298 2.8032 7.33647 2.86669 7.39996L4.65717 9.19044C4.77145 9.30473 4.91114 9.36187 5.07622 9.36187C5.2413 9.36187 5.38098 9.30473 5.49526 9.19044C5.60955 9.07616 5.66669 8.93647 5.66669 8.77139C5.66669 8.60631 5.60955 8.46663 5.49526 8.35235L4.14288 6.99997ZM9.85717 6.99997L8.50479 8.35235C8.3905 8.46663 8.33336 8.60631 8.33336 8.77139C8.33336 8.93647 8.3905 9.07616 8.50479 9.19044C8.61907 9.30473 8.75876 9.36187 8.92384 9.36187C9.08891 9.36187 9.2286 9.30473 9.34288 9.19044L11.1334 7.39996C11.1969 7.33647 11.2413 7.27298 11.2667 7.20949C11.2921 7.146 11.3048 7.07616 11.3048 6.99997C11.3048 6.92377 11.2921 6.85393 11.2667 6.79044C11.2413 6.72695 11.1969 6.66346 11.1334 6.59997L9.34288 4.80949C9.29209 4.746 9.2286 4.70155 9.15241 4.67616C9.07622 4.65076 9.00003 4.63806 8.92384 4.63806C8.84765 4.63806 8.77463 4.65076 8.70479 4.67616C8.63495 4.70155 8.56828 4.746 8.50479 4.80949C8.3905 4.92377 8.33336 5.06346 8.33336 5.22854C8.33336 5.39362 8.3905 5.5333 8.50479 5.64758L9.85717 6.99997ZM1.28574 13.8571C0.980979 13.8571 0.714312 13.7428 0.48574 13.5143C0.257169 13.2857 0.142883 13.019 0.142883 12.7143V1.28568C0.142883 0.980918 0.257169 0.714251 0.48574 0.485679C0.714312 0.257108 0.980979 0.142822 1.28574 0.142822H12.7143C13.0191 0.142822 13.2857 0.257108 13.5143 0.485679C13.7429 0.714251 13.8572 0.980918 13.8572 1.28568V12.7143C13.8572 13.019 13.7429 13.2857 13.5143 13.5143C13.2857 13.7428 13.0191 13.8571 12.7143 13.8571H1.28574ZM1.28574 12.7143H12.7143V1.28568H1.28574V12.7143Z" fill="currentColor"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.0 KiB |
|
@ -224,6 +224,8 @@ $composer-shadow-color: rgba(0, 0, 0, 0.28);
|
|||
$breadcrumb-placeholder-bg-color: #272c35;
|
||||
$theme-button-bg-color: #e3e8f0;
|
||||
$resend-button-divider-color: rgba($header-panel-text-primary-color, 0.74);
|
||||
$inlinecode-border-color: $quinary-content;
|
||||
$inlinecode-background-color: $system;
|
||||
$codeblock-background-color: #2a3039;
|
||||
$scrollbar-thumb-color: rgba(255, 255, 255, 0.2);
|
||||
$selected-color: $room-highlight-color;
|
||||
|
|
|
@ -190,6 +190,8 @@ $appearance-tab-border-color: $room-highlight-color;
|
|||
$composer-shadow-color: tranparent;
|
||||
|
||||
$codeblock-background-color: #2a3039;
|
||||
$inlinecode-border-color: #2a3039;
|
||||
$inlinecode-background-color: #2a3039;
|
||||
|
||||
/* Bubble tiles */
|
||||
$eventbubble-self-bg: #14322e;
|
||||
|
|
|
@ -290,6 +290,8 @@ $appearance-tab-border-color: $input-darker-bg-color;
|
|||
$composer-shadow-color: tranparent;
|
||||
|
||||
$codeblock-background-color: $header-panel-bg-color;
|
||||
$inlinecode-border-color: $header-panel-bg-color;
|
||||
$inlinecode-background-color: $header-panel-bg-color;
|
||||
|
||||
/* Bubble tiles */
|
||||
$eventbubble-self-bg: #f0fbf8;
|
||||
|
|
|
@ -295,6 +295,8 @@ $composer-shadow-color: rgba(0, 0, 0, 0.04);
|
|||
$breadcrumb-placeholder-bg-color: #e8eef5;
|
||||
$theme-button-bg-color: $quinary-content;
|
||||
$resend-button-divider-color: $input-darker-bg-color;
|
||||
$inlinecode-border-color: $quinary-content;
|
||||
$inlinecode-background-color: $system;
|
||||
$codeblock-background-color: $header-panel-bg-color;
|
||||
$scrollbar-thumb-color: rgba(0, 0, 0, 0.2);
|
||||
$selected-color: $secondary-accent-color;
|
||||
|
|
|
@ -85,7 +85,7 @@ export function doesRoomOrThreadHaveUnreadMessages(roomOrThread: Room | Thread):
|
|||
// https://github.com/vector-im/element-web/issues/2427
|
||||
// ...and possibly some of the others at
|
||||
// https://github.com/vector-im/element-web/issues/3363
|
||||
if (roomOrThread.timeline.at(-1)?.getSender() === myUserId) {
|
||||
if (roomOrThread.timeline[roomOrThread.timeline.length - 1]?.getSender() === myUserId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -262,6 +262,8 @@ export default class ForgotPassword extends React.Component<Props, State> {
|
|||
|
||||
try {
|
||||
await this.reset.setNewPassword(this.state.password);
|
||||
this.setState({ phase: Phase.Done });
|
||||
return;
|
||||
} catch (err: any) {
|
||||
if (err.httpStatus !== 401) {
|
||||
// 401 = waiting for email verification, else unknown error
|
||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, { MouseEventHandler, ReactNode } from "react";
|
||||
import { FormattingFunctions, AllActionStates } from "@matrix-org/matrix-wysiwyg";
|
||||
import { FormattingFunctions, AllActionStates, ActionState } from "@matrix-org/matrix-wysiwyg";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { Icon as BoldIcon } from "../../../../../../res/img/element-icons/room/composer/bold.svg";
|
||||
|
@ -26,6 +26,7 @@ import { Icon as InlineCodeIcon } from "../../../../../../res/img/element-icons/
|
|||
import { Icon as LinkIcon } from "../../../../../../res/img/element-icons/room/composer/link.svg";
|
||||
import { Icon as BulletedListIcon } from "../../../../../../res/img/element-icons/room/composer/bulleted_list.svg";
|
||||
import { Icon as NumberedListIcon } from "../../../../../../res/img/element-icons/room/composer/numbered_list.svg";
|
||||
import { Icon as CodeBlockIcon } from "../../../../../../res/img/element-icons/room/composer/code_block.svg";
|
||||
import AccessibleTooltipButton from "../../../elements/AccessibleTooltipButton";
|
||||
import { Alignment } from "../../../elements/Tooltip";
|
||||
import { KeyboardShortcut } from "../../../settings/KeyboardShortcut";
|
||||
|
@ -53,21 +54,23 @@ function Tooltip({ label, keyCombo }: TooltipProps): JSX.Element {
|
|||
|
||||
interface ButtonProps extends TooltipProps {
|
||||
icon: ReactNode;
|
||||
isActive: boolean;
|
||||
actionState: ActionState;
|
||||
onClick: MouseEventHandler<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
function Button({ label, keyCombo, onClick, isActive, icon }: ButtonProps): JSX.Element {
|
||||
function Button({ label, keyCombo, onClick, actionState, icon }: ButtonProps): JSX.Element {
|
||||
return (
|
||||
<AccessibleTooltipButton
|
||||
element="button"
|
||||
onClick={onClick as (e: ButtonEvent) => void}
|
||||
title={label}
|
||||
className={classNames("mx_FormattingButtons_Button", {
|
||||
mx_FormattingButtons_active: isActive,
|
||||
mx_FormattingButtons_Button_hover: !isActive,
|
||||
mx_FormattingButtons_active: actionState === "reversed",
|
||||
mx_FormattingButtons_Button_hover: actionState === "enabled",
|
||||
mx_FormattingButtons_disabled: actionState === "disabled",
|
||||
})}
|
||||
tooltip={keyCombo && <Tooltip label={label} keyCombo={keyCombo} />}
|
||||
forceHide={actionState === "disabled"}
|
||||
alignment={Alignment.Top}
|
||||
>
|
||||
{icon}
|
||||
|
@ -85,53 +88,59 @@ export function FormattingButtons({ composer, actionStates }: FormattingButtonsP
|
|||
return (
|
||||
<div className="mx_FormattingButtons">
|
||||
<Button
|
||||
isActive={actionStates.bold === "reversed"}
|
||||
actionState={actionStates.bold}
|
||||
label={_td("Bold")}
|
||||
keyCombo={{ ctrlOrCmdKey: true, key: "b" }}
|
||||
onClick={() => composer.bold()}
|
||||
icon={<BoldIcon className="mx_FormattingButtons_Icon" />}
|
||||
/>
|
||||
<Button
|
||||
isActive={actionStates.italic === "reversed"}
|
||||
actionState={actionStates.italic}
|
||||
label={_td("Italic")}
|
||||
keyCombo={{ ctrlOrCmdKey: true, key: "i" }}
|
||||
onClick={() => composer.italic()}
|
||||
icon={<ItalicIcon className="mx_FormattingButtons_Icon" />}
|
||||
/>
|
||||
<Button
|
||||
isActive={actionStates.underline === "reversed"}
|
||||
actionState={actionStates.underline}
|
||||
label={_td("Underline")}
|
||||
keyCombo={{ ctrlOrCmdKey: true, key: "u" }}
|
||||
onClick={() => composer.underline()}
|
||||
icon={<UnderlineIcon className="mx_FormattingButtons_Icon" />}
|
||||
/>
|
||||
<Button
|
||||
isActive={actionStates.strikeThrough === "reversed"}
|
||||
actionState={actionStates.strikeThrough}
|
||||
label={_td("Strikethrough")}
|
||||
onClick={() => composer.strikeThrough()}
|
||||
icon={<StrikeThroughIcon className="mx_FormattingButtons_Icon" />}
|
||||
/>
|
||||
<Button
|
||||
isActive={actionStates.unorderedList === "reversed"}
|
||||
actionState={actionStates.unorderedList}
|
||||
label={_td("Bulleted list")}
|
||||
onClick={() => composer.unorderedList()}
|
||||
icon={<BulletedListIcon className="mx_FormattingButtons_Icon" />}
|
||||
/>
|
||||
<Button
|
||||
isActive={actionStates.orderedList === "reversed"}
|
||||
actionState={actionStates.orderedList}
|
||||
label={_td("Numbered list")}
|
||||
onClick={() => composer.orderedList()}
|
||||
icon={<NumberedListIcon className="mx_FormattingButtons_Icon" />}
|
||||
/>
|
||||
<Button
|
||||
isActive={actionStates.inlineCode === "reversed"}
|
||||
actionState={actionStates.inlineCode}
|
||||
label={_td("Code")}
|
||||
keyCombo={{ ctrlOrCmdKey: true, key: "e" }}
|
||||
onClick={() => composer.inlineCode()}
|
||||
icon={<InlineCodeIcon className="mx_FormattingButtons_Icon" />}
|
||||
/>
|
||||
<Button
|
||||
isActive={actionStates.link === "reversed"}
|
||||
actionState={actionStates.codeBlock}
|
||||
label={_td("Code block")}
|
||||
onClick={() => composer.codeBlock()}
|
||||
icon={<CodeBlockIcon className="mx_FormattingButtons_Icon" />}
|
||||
/>
|
||||
<Button
|
||||
actionState={actionStates.link}
|
||||
label={_td("Link")}
|
||||
onClick={() => openLinkModal(composer, composerContext, actionStates.link === "reversed")}
|
||||
icon={<LinkIcon className="mx_FormattingButtons_Icon" />}
|
||||
|
|
|
@ -19,6 +19,12 @@ import { useCallback } from "react";
|
|||
|
||||
import { useSettingValue } from "../../../../../hooks/useSettings";
|
||||
|
||||
function isEnterPressed(event: KeyboardEvent): boolean {
|
||||
// Ugly but here we need to send the message only if Enter is pressed
|
||||
// And we need to stop the event propagation on enter to avoid the composer to grow
|
||||
return event.key === "Enter" && !event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey;
|
||||
}
|
||||
|
||||
export function useInputEventProcessor(onSend: () => void): (event: WysiwygEvent) => WysiwygEvent | null {
|
||||
const isCtrlEnter = useSettingValue<boolean>("MessageComposerInput.ctrlEnterToSend");
|
||||
return useCallback(
|
||||
|
@ -28,12 +34,12 @@ export function useInputEventProcessor(onSend: () => void): (event: WysiwygEvent
|
|||
}
|
||||
|
||||
const isKeyboardEvent = event instanceof KeyboardEvent;
|
||||
const isEnterPress =
|
||||
!isCtrlEnter && (isKeyboardEvent ? event.key === "Enter" : event.inputType === "insertParagraph");
|
||||
// sendMessage is sent when ctrl+enter is pressed
|
||||
const isSendMessage = !isKeyboardEvent && event.inputType === "sendMessage";
|
||||
const isEnterPress = !isCtrlEnter && isKeyboardEvent && isEnterPressed(event);
|
||||
const isInsertParagraph = !isCtrlEnter && !isKeyboardEvent && event.inputType === "insertParagraph";
|
||||
// sendMessage is sent when cmd+enter is pressed
|
||||
const isSendMessage = isCtrlEnter && !isKeyboardEvent && event.inputType === "sendMessage";
|
||||
|
||||
if (isEnterPress || isSendMessage) {
|
||||
if (isEnterPress || isInsertParagraph || isSendMessage) {
|
||||
event.stopPropagation?.();
|
||||
event.preventDefault?.();
|
||||
onSend();
|
||||
|
|
|
@ -288,6 +288,37 @@ describe("<ForgotPassword>", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("and confirm the email link and submitting the new password", () => {
|
||||
beforeEach(async () => {
|
||||
// fake link confirmed by resolving client.setPassword instead of raising an error
|
||||
mocked(client.setPassword).mockResolvedValue({});
|
||||
await click(screen.getByText("Reset password"));
|
||||
});
|
||||
|
||||
it("should send the new password (once)", () => {
|
||||
expect(client.setPassword).toHaveBeenCalledWith(
|
||||
{
|
||||
type: "m.login.email.identity",
|
||||
threepid_creds: {
|
||||
client_secret: expect.any(String),
|
||||
sid: testSid,
|
||||
},
|
||||
threepidCreds: {
|
||||
client_secret: expect.any(String),
|
||||
sid: testSid,
|
||||
},
|
||||
},
|
||||
testPassword,
|
||||
false,
|
||||
);
|
||||
|
||||
// be sure that the next attempt to set the password would have been sent
|
||||
jest.advanceTimersByTime(3000);
|
||||
// it should not retry to set the password
|
||||
expect(client.setPassword).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("and submitting it", () => {
|
||||
beforeEach(async () => {
|
||||
await click(screen.getByText("Reset password"));
|
||||
|
|
|
@ -28,6 +28,7 @@ const mockWysiwyg = {
|
|||
underline: jest.fn(),
|
||||
strikeThrough: jest.fn(),
|
||||
inlineCode: jest.fn(),
|
||||
codeBlock: jest.fn(),
|
||||
link: jest.fn(),
|
||||
orderedList: jest.fn(),
|
||||
unorderedList: jest.fn(),
|
||||
|
@ -36,7 +37,7 @@ const mockWysiwyg = {
|
|||
const openLinkModalSpy = jest.spyOn(LinkModal, "openLinkModal");
|
||||
|
||||
const testCases: Record<
|
||||
Exclude<ActionTypes, "undo" | "redo" | "clear" | "codeBlock">,
|
||||
Exclude<ActionTypes, "undo" | "redo" | "clear">,
|
||||
{ label: string; mockFormatFn: jest.Func | jest.SpyInstance }
|
||||
> = {
|
||||
bold: { label: "Bold", mockFormatFn: mockWysiwyg.bold },
|
||||
|
@ -44,6 +45,7 @@ const testCases: Record<
|
|||
underline: { label: "Underline", mockFormatFn: mockWysiwyg.underline },
|
||||
strikeThrough: { label: "Strikethrough", mockFormatFn: mockWysiwyg.strikeThrough },
|
||||
inlineCode: { label: "Code", mockFormatFn: mockWysiwyg.inlineCode },
|
||||
codeBlock: { label: "Code block", mockFormatFn: mockWysiwyg.inlineCode },
|
||||
link: { label: "Link", mockFormatFn: openLinkModalSpy },
|
||||
orderedList: { label: "Numbered list", mockFormatFn: mockWysiwyg.orderedList },
|
||||
unorderedList: { label: "Bulleted list", mockFormatFn: mockWysiwyg.unorderedList },
|
||||
|
@ -62,6 +64,7 @@ const renderComponent = (props = {}) => {
|
|||
const classes = {
|
||||
active: "mx_FormattingButtons_active",
|
||||
hover: "mx_FormattingButtons_Button_hover",
|
||||
disabled: "mx_FormattingButtons_disabled",
|
||||
};
|
||||
|
||||
describe("FormattingButtons", () => {
|
||||
|
@ -87,6 +90,16 @@ describe("FormattingButtons", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("Each button should have disabled class when disabled", () => {
|
||||
const disabledActionStates = createActionStates("disabled");
|
||||
renderComponent({ actionStates: disabledActionStates });
|
||||
|
||||
Object.values(testCases).forEach((testCase) => {
|
||||
const { label } = testCase;
|
||||
expect(screen.getByLabelText(label)).toHaveClass(classes.disabled);
|
||||
});
|
||||
});
|
||||
|
||||
it("Should call wysiwyg function on button click", async () => {
|
||||
renderComponent();
|
||||
|
||||
|
@ -98,14 +111,26 @@ describe("FormattingButtons", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("Each button should display the tooltip on mouse over", async () => {
|
||||
it("Each button should display the tooltip on mouse over when not disabled", async () => {
|
||||
renderComponent();
|
||||
|
||||
for (const testCase of Object.values(testCases)) {
|
||||
const { label } = testCase;
|
||||
|
||||
await userEvent.hover(screen.getByLabelText(label));
|
||||
expect(await screen.findByText(label)).toBeTruthy();
|
||||
expect(screen.getByText(label)).toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
|
||||
it("Each button should not display the tooltip on mouse over when disabled", async () => {
|
||||
const disabledActionStates = createActionStates("disabled");
|
||||
renderComponent({ actionStates: disabledActionStates });
|
||||
|
||||
for (const testCase of Object.values(testCases)) {
|
||||
const { label } = testCase;
|
||||
|
||||
await userEvent.hover(screen.getByLabelText(label));
|
||||
expect(screen.queryByText(label)).not.toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
import "@testing-library/jest-dom";
|
||||
import React from "react";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import { WysiwygComposer } from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer";
|
||||
import SettingsStore from "../../../../../../src/settings/SettingsStore";
|
||||
|
@ -87,6 +88,45 @@ describe("WysiwygComposer", () => {
|
|||
// Then it sends a message
|
||||
await waitFor(() => expect(onSend).toBeCalledTimes(1));
|
||||
});
|
||||
|
||||
it("Should not call onSend when Shift+Enter is pressed ", async () => {
|
||||
//When
|
||||
await userEvent.type(screen.getByRole("textbox"), "{shift>}{enter}");
|
||||
|
||||
// Then it sends a message
|
||||
await waitFor(() => expect(onSend).toBeCalledTimes(0));
|
||||
});
|
||||
|
||||
it("Should not call onSend when ctrl+Enter is pressed ", async () => {
|
||||
//When
|
||||
// Using userEvent.type or .keyboard wasn't working as expected in the case of ctrl+enter
|
||||
fireEvent(
|
||||
screen.getByRole("textbox"),
|
||||
new KeyboardEvent("keydown", {
|
||||
ctrlKey: true,
|
||||
code: "Enter",
|
||||
}),
|
||||
);
|
||||
|
||||
// Then it sends a message
|
||||
await waitFor(() => expect(onSend).toBeCalledTimes(0));
|
||||
});
|
||||
|
||||
it("Should not call onSend when alt+Enter is pressed ", async () => {
|
||||
//When
|
||||
await userEvent.type(screen.getByRole("textbox"), "{alt>}{enter}");
|
||||
|
||||
// Then it sends a message
|
||||
await waitFor(() => expect(onSend).toBeCalledTimes(0));
|
||||
});
|
||||
|
||||
it("Should not call onSend when meta+Enter is pressed ", async () => {
|
||||
//When
|
||||
await userEvent.type(screen.getByRole("textbox"), "{meta>}{enter}");
|
||||
|
||||
// Then it sends a message
|
||||
await waitFor(() => expect(onSend).toBeCalledTimes(0));
|
||||
});
|
||||
});
|
||||
|
||||
describe("When settings require Ctrl+Enter to send", () => {
|
||||
|
|
|
@ -6493,7 +6493,7 @@ matrix-events-sdk@0.0.1:
|
|||
|
||||
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
|
||||
version "23.1.0"
|
||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/2fcc4811dd913bb774dd1c7f67cb693c4456d71e"
|
||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/83563c7a01bbeaf7f83f4b7feccc03647b536e7c"
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
"@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.2"
|
||||
|
|
Loading…
Reference in New Issue