Fix emoji verification responsive layout (#31899)
* Extract SasEmoji to shared-components and improve responsive layout Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Add baseline screenshots Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Fix e2e test Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Add SasEmoji snapshot test Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Add figma link Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Improve doc Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Add new dir to crypto-web-reviewers codeowners as per ask Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
@@ -16,6 +16,7 @@
|
|||||||
/src/components/views/dialogs/devtools/Crypto.tsx @element-hq/element-crypto-web-reviewers
|
/src/components/views/dialogs/devtools/Crypto.tsx @element-hq/element-crypto-web-reviewers
|
||||||
/playwright/e2e/crypto/ @element-hq/element-crypto-web-reviewers
|
/playwright/e2e/crypto/ @element-hq/element-crypto-web-reviewers
|
||||||
/playwright/e2e/settings/encryption-user-tab/ @element-hq/element-crypto-web-reviewers
|
/playwright/e2e/settings/encryption-user-tab/ @element-hq/element-crypto-web-reviewers
|
||||||
|
/packages/shared-components/src/crypto/ @element-hq/element-crypto-web-reviewers
|
||||||
|
|
||||||
|
|
||||||
/src/models/Call.ts @element-hq/element-call-reviewers
|
/src/models/Call.ts @element-hq/element-call-reviewers
|
||||||
|
|||||||
@@ -90,7 +90,6 @@
|
|||||||
"@matrix-org/analytics-events": "^0.31.0",
|
"@matrix-org/analytics-events": "^0.31.0",
|
||||||
"@matrix-org/emojibase-bindings": "^1.5.0",
|
"@matrix-org/emojibase-bindings": "^1.5.0",
|
||||||
"@matrix-org/react-sdk-module-api": "^2.4.0",
|
"@matrix-org/react-sdk-module-api": "^2.4.0",
|
||||||
"@matrix-org/spec": "^1.7.0",
|
|
||||||
"@sentry/browser": "^10.0.0",
|
"@sentry/browser": "^10.0.0",
|
||||||
"@types/png-chunks-extract": "^1.0.2",
|
"@types/png-chunks-extract": "^1.0.2",
|
||||||
"@vector-im/compound-design-tokens": "6.9.0",
|
"@vector-im/compound-design-tokens": "6.9.0",
|
||||||
|
|||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -50,6 +50,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-hq/element-web-module-api": "^1.8.0",
|
"@element-hq/element-web-module-api": "^1.8.0",
|
||||||
|
"@matrix-org/spec": "^1.7.0",
|
||||||
"@vector-im/compound-design-tokens": "^6.4.3",
|
"@vector-im/compound-design-tokens": "^6.4.3",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"counterpart": "^0.18.6",
|
"counterpart": "^0.18.6",
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--cpd-space-2x);
|
||||||
|
justify-content: space-evenly;
|
||||||
|
}
|
||||||
|
|
||||||
|
.segment {
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: var(--cpd-space-4x);
|
||||||
|
text-align: center;
|
||||||
|
/* Allow maximum 4 per line, accounting for 8px gap */
|
||||||
|
min-width: calc(25% - 8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji {
|
||||||
|
/* Use the Twemoji font for consistency with other clients */
|
||||||
|
font-family: Twemoji, var(--cpd-font-family-sans);
|
||||||
|
font-size: var(--cpd-font-size-heading-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-weight: var(--cpd-font-weight-regular);
|
||||||
|
font-size: var(--cpd-font-size-body-lg);
|
||||||
|
color: var(--cpd-color-text-secondary);
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 Element Creations Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { type Meta, type StoryObj } from "@storybook/react-vite";
|
||||||
|
|
||||||
|
import { SasEmoji } from "./SasEmoji";
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: "Crypto/SasEmoji",
|
||||||
|
component: SasEmoji,
|
||||||
|
tags: ["autodocs"],
|
||||||
|
args: {
|
||||||
|
emoji: ["🍕", "🌽", "🚀", "🔒", "🔧", "🍓", "⌛"],
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
design: {
|
||||||
|
type: "figma",
|
||||||
|
url: "https://www.figma.com/design/XLWIAB5n8yObYvU0INKPK1/Verification-by-Emoji?node-id=1-2935&t=NrV9JnuItrAyyh53-4",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof SasEmoji>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Default: Story = {};
|
||||||
|
|
||||||
|
export const WorstCaseAlbanian: Story = {
|
||||||
|
globals: {
|
||||||
|
language: "sq",
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
emoji: ["🎅", "🎅", "🎅", "🎅", "🎅", "🎅", "🎅"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WorstCaseGerman: Story = {
|
||||||
|
globals: {
|
||||||
|
language: "de",
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
emoji: ["🔧", "🔧", "🔧", "🔧", "🔧", "🔧", "🔧"],
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 Element Creations Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { render } from "@test-utils";
|
||||||
|
|
||||||
|
import { SasEmoji } from "./SasEmoji";
|
||||||
|
|
||||||
|
describe("<SasEmoji/>", () => {
|
||||||
|
it("should match snapshot", () => {
|
||||||
|
const { asFragment } = render(<SasEmoji emoji={["🦋", "🍄", "⚽", "🌏", "🦄", "🚀", "🔧"]} />);
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { type JSX } from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
import { type SasEmoji, tEmoji } from "./SasEmojiTranslate.ts";
|
||||||
|
import styles from "./SasEmoji.module.css";
|
||||||
|
import { useI18n } from "../../utils/i18nContext.ts";
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
/**
|
||||||
|
* The emoji to render
|
||||||
|
*/
|
||||||
|
emoji: [SasEmoji, SasEmoji, SasEmoji, SasEmoji, SasEmoji, SasEmoji, SasEmoji];
|
||||||
|
/**
|
||||||
|
* Optional className to apply to the container
|
||||||
|
*/
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the 7 emoji used for SAS verification.
|
||||||
|
* The component is responsive so can be rendered in any context, dialog, side panel.
|
||||||
|
*/
|
||||||
|
export function SasEmoji({ emoji, className }: Props): JSX.Element {
|
||||||
|
const { language } = useI18n();
|
||||||
|
|
||||||
|
const emojiBlocks = emoji.map((emoji, i) => (
|
||||||
|
<div className={styles.segment} key={i}>
|
||||||
|
<div className={styles.emoji} aria-hidden={true}>
|
||||||
|
{emoji}
|
||||||
|
</div>
|
||||||
|
<div className={styles.label}>{tEmoji(emoji, language)}</div>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
|
||||||
|
return <div className={classNames(styles.container, className)}>{emojiBlocks}</div>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 Element Creations Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
|
||||||
|
import { tEmoji, type SasEmoji } from "./SasEmojiTranslate.ts";
|
||||||
|
|
||||||
|
describe("tEmoji", () => {
|
||||||
|
it.each([
|
||||||
|
["🐶", "en-GB", "Dog"],
|
||||||
|
["🐶", "en", "Dog"],
|
||||||
|
["🐶", "de-DE", "Hund"],
|
||||||
|
["🐶", "pt", "Cachorro"],
|
||||||
|
["🔧", "de-DE", "Schraubenschlüssel"],
|
||||||
|
["🎅", "sq", "Babagjyshi i Vitit të Ri"],
|
||||||
|
] as [emoji: SasEmoji, locale: string, expectation: string][])(
|
||||||
|
"should handle locale %s",
|
||||||
|
(emoji, locale, expectation) => {
|
||||||
|
expect(tEmoji(emoji, locale)).toEqual(expectation);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import SasEmojiJson from "@matrix-org/spec/sas-emoji.json";
|
||||||
|
import { getNormalizedLanguageKeys } from "matrix-web-i18n";
|
||||||
|
|
||||||
|
// Type as specified in https://spec.matrix.org/v1.17/client-server-api/#sas-method-emoji
|
||||||
|
export type SasEmoji =
|
||||||
|
| "🐶"
|
||||||
|
| "🐱"
|
||||||
|
| "🦁"
|
||||||
|
| "🐎"
|
||||||
|
| "🦄"
|
||||||
|
| "🐷"
|
||||||
|
| "🐘"
|
||||||
|
| "🐰"
|
||||||
|
| "🐼"
|
||||||
|
| "🐓"
|
||||||
|
| "🐧"
|
||||||
|
| "🐢"
|
||||||
|
| "🐟"
|
||||||
|
| "🐙"
|
||||||
|
| "🦋"
|
||||||
|
| "🌷"
|
||||||
|
| "🌳"
|
||||||
|
| "🌵"
|
||||||
|
| "🍄"
|
||||||
|
| "🌏"
|
||||||
|
| "🌙"
|
||||||
|
| "☁"
|
||||||
|
| "🔥"
|
||||||
|
| "🍌"
|
||||||
|
| "🍎"
|
||||||
|
| "🍓"
|
||||||
|
| "🌽"
|
||||||
|
| "🍕"
|
||||||
|
| "🎂"
|
||||||
|
| "❤"
|
||||||
|
| "😀"
|
||||||
|
| "🤖"
|
||||||
|
| "🎩"
|
||||||
|
| "👓"
|
||||||
|
| "🔧"
|
||||||
|
| "🎅"
|
||||||
|
| "👍"
|
||||||
|
| "☂"
|
||||||
|
| "⌛"
|
||||||
|
| "⏰"
|
||||||
|
| "🎁"
|
||||||
|
| "💡"
|
||||||
|
| "📕"
|
||||||
|
| "✏"
|
||||||
|
| "📎"
|
||||||
|
| "✂"
|
||||||
|
| "🔒"
|
||||||
|
| "🔑"
|
||||||
|
| "🔨"
|
||||||
|
| "☎"
|
||||||
|
| "🏁"
|
||||||
|
| "🚂"
|
||||||
|
| "🚲"
|
||||||
|
| "✈"
|
||||||
|
| "🚀"
|
||||||
|
| "🏆"
|
||||||
|
| "⚽"
|
||||||
|
| "🎸"
|
||||||
|
| "🎺"
|
||||||
|
| "🔔"
|
||||||
|
| "⚓"
|
||||||
|
| "🎧"
|
||||||
|
| "📁"
|
||||||
|
| "📌";
|
||||||
|
|
||||||
|
const SasEmojiMap = new Map<
|
||||||
|
SasEmoji,
|
||||||
|
[
|
||||||
|
description: string,
|
||||||
|
translations: {
|
||||||
|
[normalizedLanguageKey: string]: string;
|
||||||
|
},
|
||||||
|
]
|
||||||
|
>(
|
||||||
|
SasEmojiJson.map(({ emoji, description, translated_descriptions: translations }) => [
|
||||||
|
emoji as SasEmoji,
|
||||||
|
[
|
||||||
|
description,
|
||||||
|
// Normalize the translation keys
|
||||||
|
Object.keys(translations).reduce<Record<string, string>>((o, k) => {
|
||||||
|
for (const key of getNormalizedLanguageKeys(k)) {
|
||||||
|
o[key] = translations[k as keyof typeof translations]!;
|
||||||
|
}
|
||||||
|
return o;
|
||||||
|
}, {}),
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translate given SAS emoji into the target locale
|
||||||
|
* @param emoji - the SAS emoji to translate
|
||||||
|
* @param locale - the BCP 47 locale to translate to, will fall back to English as the base locale for Matrix SAS Emoji.
|
||||||
|
*/
|
||||||
|
export function tEmoji(emoji: SasEmoji, locale: string): string {
|
||||||
|
const mapping = SasEmojiMap.get(emoji);
|
||||||
|
if (!mapping) {
|
||||||
|
throw new Error(`Emoji mapping not found for emoji ${emoji}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [description, translations] = mapping;
|
||||||
|
|
||||||
|
for (const key of getNormalizedLanguageKeys(locale)) {
|
||||||
|
if (translations[key]) {
|
||||||
|
return translations[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return description;
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`<SasEmoji/> > should match snapshot 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<div
|
||||||
|
class="container"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="segment"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
class="emoji"
|
||||||
|
>
|
||||||
|
🦋
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="label"
|
||||||
|
>
|
||||||
|
Butterfly
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="segment"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
class="emoji"
|
||||||
|
>
|
||||||
|
🍄
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="label"
|
||||||
|
>
|
||||||
|
Mushroom
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="segment"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
class="emoji"
|
||||||
|
>
|
||||||
|
⚽
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="label"
|
||||||
|
>
|
||||||
|
Ball
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="segment"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
class="emoji"
|
||||||
|
>
|
||||||
|
🌏
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="label"
|
||||||
|
>
|
||||||
|
Globe
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="segment"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
class="emoji"
|
||||||
|
>
|
||||||
|
🦄
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="label"
|
||||||
|
>
|
||||||
|
Unicorn
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="segment"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
class="emoji"
|
||||||
|
>
|
||||||
|
🚀
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="label"
|
||||||
|
>
|
||||||
|
Rocket
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="segment"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
class="emoji"
|
||||||
|
>
|
||||||
|
🔧
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="label"
|
||||||
|
>
|
||||||
|
Spanner
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { SasEmoji } from "./SasEmoji.tsx";
|
||||||
@@ -12,6 +12,7 @@ export * from "./audio/PlayPauseButton";
|
|||||||
export * from "./audio/SeekBar";
|
export * from "./audio/SeekBar";
|
||||||
export * from "./avatar/AvatarWithDetails";
|
export * from "./avatar/AvatarWithDetails";
|
||||||
export * from "./composer/Banner";
|
export * from "./composer/Banner";
|
||||||
|
export * from "./crypto/SasEmoji";
|
||||||
export * from "./event-tiles/TextualEventView";
|
export * from "./event-tiles/TextualEventView";
|
||||||
export * from "./message-body/MediaBody";
|
export * from "./message-body/MediaBody";
|
||||||
export * from "./pill-input/Pill";
|
export * from "./pill-input/Pill";
|
||||||
|
|||||||
@@ -667,6 +667,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.17.9"
|
"@babel/runtime" "^7.17.9"
|
||||||
|
|
||||||
|
"@matrix-org/spec@^1.7.0":
|
||||||
|
version "1.16.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@matrix-org/spec/-/spec-1.16.0.tgz#c88f4ed521e4c0bd3a4c108bcaf13f25173a0fdc"
|
||||||
|
integrity sha512-xUKHkwGXXISMCfTrx6JW6uGEK5O8IeZVOjBm7FX1h/ihpK6l50nlSIMRYdtz4V6q3pvOVBOCft4hPYTJVeTZDA==
|
||||||
|
|
||||||
"@mdx-js/react@^3.0.0":
|
"@mdx-js/react@^3.0.0":
|
||||||
version "3.1.1"
|
version "3.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/@mdx-js/react/-/react-3.1.1.tgz#24bda7fffceb2fe256f954482123cda1be5f5fef"
|
resolved "https://registry.yarnpkg.com/@mdx-js/react/-/react-3.1.1.tgz#24bda7fffceb2fe256f954482123cda1be5f5fef"
|
||||||
|
|||||||
@@ -290,7 +290,7 @@ export async function doTwoWaySasVerification(page: Page, verifier: JSHandle<Ver
|
|||||||
// on the bot side, wait for the emojis, confirm they match, and return them
|
// on the bot side, wait for the emojis, confirm they match, and return them
|
||||||
const emojis = await handleSasVerification(verifier);
|
const emojis = await handleSasVerification(verifier);
|
||||||
|
|
||||||
const emojiBlocks = page.locator(".mx_VerificationShowSas_emojiSas_block");
|
const emojiBlocks = page.locator(".mx_VerificationShowSas_emojiSas > div");
|
||||||
await expect(emojiBlocks).toHaveCount(emojis.length);
|
await expect(emojiBlocks).toHaveCount(emojis.length);
|
||||||
|
|
||||||
// then, check that our application shows an emoji panel with the same emojis.
|
// then, check that our application shows an emoji panel with the same emojis.
|
||||||
|
|||||||
@@ -20,40 +20,9 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mx_VerificationShowSas_emojiSas {
|
.mx_VerificationShowSas_emojiSas {
|
||||||
text-align: center;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
margin: 25px 0;
|
margin: 25px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_VerificationShowSas_emojiSas_block {
|
|
||||||
display: inline-block;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
position: relative;
|
|
||||||
width: 52px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_Dialog .mx_VerificationShowSas_emojiSas_block,
|
|
||||||
.mx_AuthPage_modal .mx_VerificationShowSas_emojiSas_block {
|
|
||||||
width: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_VerificationShowSas_emojiSas_emoji {
|
|
||||||
font-size: $font-32px;
|
|
||||||
/* Use the Twemoji font for consistency with other clients */
|
|
||||||
font-family: Twemoji, var(--cpd-font-family-sans);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_VerificationShowSas_emojiSas_label {
|
|
||||||
font-size: $font-12px;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_VerificationShowSas_emojiSas_break {
|
|
||||||
flex-basis: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_VerificationShowSas_buttonRow {
|
.mx_VerificationShowSas_buttonRow {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React, { type ComponentProps } from "react";
|
||||||
import { type Device } from "matrix-js-sdk/src/matrix";
|
import { type Device } from "matrix-js-sdk/src/matrix";
|
||||||
import { type GeneratedSas, type EmojiMapping } from "matrix-js-sdk/src/crypto-api";
|
import { type GeneratedSas } from "matrix-js-sdk/src/crypto-api";
|
||||||
import SasEmoji from "@matrix-org/spec/sas-emoji.json";
|
import { SasEmoji } from "@element-hq/web-shared-components";
|
||||||
|
|
||||||
import { _t, getNormalizedLanguageKeys, getUserLanguage } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { PendingActionSpinner } from "../right_panel/EncryptionInfo";
|
import { PendingActionSpinner } from "../right_panel/EncryptionInfo";
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
|
|
||||||
@@ -34,52 +34,6 @@ interface IState {
|
|||||||
cancelling?: boolean;
|
cancelling?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SasEmojiMap = new Map<
|
|
||||||
string, // lowercase
|
|
||||||
{
|
|
||||||
description: string;
|
|
||||||
translations: {
|
|
||||||
[normalizedLanguageKey: string]: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
>(
|
|
||||||
SasEmoji.map(({ description, translated_descriptions: translations }) => [
|
|
||||||
description.toLowerCase(),
|
|
||||||
{
|
|
||||||
description,
|
|
||||||
// Normalize the translation keys
|
|
||||||
translations: Object.keys(translations).reduce<Record<string, string>>((o, k) => {
|
|
||||||
for (const key of getNormalizedLanguageKeys(k)) {
|
|
||||||
o[key] = translations[k as keyof typeof translations]!;
|
|
||||||
}
|
|
||||||
return o;
|
|
||||||
}, {}),
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Translate given EmojiMapping into the target locale
|
|
||||||
* @param mapping - the given EmojiMapping to translate
|
|
||||||
* @param locale - the BCP 47 locale to translate to, will fall back to English as the base locale for Matrix SAS Emoji.
|
|
||||||
*/
|
|
||||||
export function tEmoji(mapping: EmojiMapping, locale: string): string {
|
|
||||||
const name = mapping[1];
|
|
||||||
const emoji = SasEmojiMap.get(name.toLowerCase());
|
|
||||||
if (!emoji) {
|
|
||||||
console.warn("Emoji not found for translation", name);
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const key of getNormalizedLanguageKeys(locale)) {
|
|
||||||
if (!!emoji.translations[key]) {
|
|
||||||
return emoji.translations[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return emoji.description;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class VerificationShowSas extends React.Component<IProps, IState> {
|
export default class VerificationShowSas extends React.Component<IProps, IState> {
|
||||||
public constructor(props: IProps) {
|
public constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -100,25 +54,14 @@ export default class VerificationShowSas extends React.Component<IProps, IState>
|
|||||||
};
|
};
|
||||||
|
|
||||||
public render(): React.ReactNode {
|
public render(): React.ReactNode {
|
||||||
const locale = getUserLanguage();
|
|
||||||
|
|
||||||
let sasDisplay;
|
let sasDisplay;
|
||||||
let sasCaption;
|
let sasCaption;
|
||||||
if (this.props.sas.emoji) {
|
if (this.props.sas.emoji) {
|
||||||
const emojiBlocks = this.props.sas.emoji.map((emoji, i) => (
|
|
||||||
<div className="mx_VerificationShowSas_emojiSas_block" key={i}>
|
|
||||||
<div className="mx_VerificationShowSas_emojiSas_emoji" aria-hidden={true}>
|
|
||||||
{emoji[0]}
|
|
||||||
</div>
|
|
||||||
<div className="mx_VerificationShowSas_emojiSas_label">{tEmoji(emoji, locale)}</div>
|
|
||||||
</div>
|
|
||||||
));
|
|
||||||
sasDisplay = (
|
sasDisplay = (
|
||||||
<div className="mx_VerificationShowSas_emojiSas">
|
<SasEmoji
|
||||||
{emojiBlocks.slice(0, 4)}
|
className="mx_VerificationShowSas_emojiSas"
|
||||||
<div className="mx_VerificationShowSas_emojiSas_break" />
|
emoji={this.props.sas.emoji.map((e) => e[0]) as ComponentProps<typeof SasEmoji>["emoji"]}
|
||||||
{emojiBlocks.slice(4)}
|
/>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
sasCaption = this.props.isSelf
|
sasCaption = this.props.isSelf
|
||||||
? _t("encryption|verification|confirm_the_emojis")
|
? _t("encryption|verification|confirm_the_emojis")
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { type EmojiMapping } from "matrix-js-sdk/src/crypto-api";
|
|
||||||
|
|
||||||
import { tEmoji } from "../../../../src/components/views/verification/VerificationShowSas";
|
|
||||||
|
|
||||||
describe("tEmoji", () => {
|
|
||||||
it.each([
|
|
||||||
["en-GB", "Dog"],
|
|
||||||
["en", "Dog"],
|
|
||||||
["de-DE", "Hund"],
|
|
||||||
["pt", "Cachorro"],
|
|
||||||
])("should handle locale %s", (locale, expectation) => {
|
|
||||||
const emoji: EmojiMapping = ["🐶", "Dog"];
|
|
||||||
expect(tEmoji(emoji, locale)).toEqual(expectation);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -34,7 +34,7 @@ describe("IncomingSasDialog", () => {
|
|||||||
|
|
||||||
it("should show some emojis once keys are exchanged", () => {
|
it("should show some emojis once keys are exchanged", () => {
|
||||||
const mockVerifier = makeMockVerifier();
|
const mockVerifier = makeMockVerifier();
|
||||||
const { container } = renderComponent(mockVerifier);
|
const { getAllByText } = renderComponent(mockVerifier);
|
||||||
|
|
||||||
// fire the ShowSas event
|
// fire the ShowSas event
|
||||||
const sasEvent = makeMockSasCallbacks();
|
const sasEvent = makeMockSasCallbacks();
|
||||||
@@ -42,11 +42,8 @@ describe("IncomingSasDialog", () => {
|
|||||||
mockVerifier.emit(VerifierEvent.ShowSas, sasEvent);
|
mockVerifier.emit(VerifierEvent.ShowSas, sasEvent);
|
||||||
});
|
});
|
||||||
|
|
||||||
const emojis = container.getElementsByClassName("mx_VerificationShowSas_emojiSas_block");
|
expect(getAllByText("🦄")).toHaveLength(7);
|
||||||
expect(emojis.length).toEqual(7);
|
expect(getAllByText("Unicorn")).toHaveLength(7);
|
||||||
for (const emoji of emojis) {
|
|
||||||
expect(emoji).toHaveTextContent("🦄Unicorn");
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+7
-10
@@ -456,41 +456,38 @@ exports[`VerificationRequestDialog When other device accepted emoji, displays em
|
|||||||
Confirm that the emojis below match those shown on your other device.
|
Confirm that the emojis below match those shown on your other device.
|
||||||
</p>
|
</p>
|
||||||
<div
|
<div
|
||||||
class="mx_VerificationShowSas_emojiSas"
|
class="_container_1lqqy_8 mx_VerificationShowSas_emojiSas"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="mx_VerificationShowSas_emojiSas_block"
|
class="_segment_1lqqy_15"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
class="mx_VerificationShowSas_emojiSas_emoji"
|
class="_emoji_1lqqy_23"
|
||||||
>
|
>
|
||||||
🐶
|
🐶
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="mx_VerificationShowSas_emojiSas_label"
|
class="_label_1lqqy_29"
|
||||||
>
|
>
|
||||||
Dog
|
Dog
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="mx_VerificationShowSas_emojiSas_block"
|
class="_segment_1lqqy_15"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
class="mx_VerificationShowSas_emojiSas_emoji"
|
class="_emoji_1lqqy_23"
|
||||||
>
|
>
|
||||||
🐱
|
🐱
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="mx_VerificationShowSas_emojiSas_label"
|
class="_label_1lqqy_29"
|
||||||
>
|
>
|
||||||
Cat
|
Cat
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="mx_VerificationShowSas_emojiSas_break"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<p />
|
<p />
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ describe("<VerificationPanel />", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should show some emojis once keys are exchanged", () => {
|
it("should show some emojis once keys are exchanged", () => {
|
||||||
const { container } = renderComponent({
|
const { getAllByText } = renderComponent({
|
||||||
request: mockRequest,
|
request: mockRequest,
|
||||||
phase: Phase.Started,
|
phase: Phase.Started,
|
||||||
});
|
});
|
||||||
@@ -117,11 +117,8 @@ describe("<VerificationPanel />", () => {
|
|||||||
mockVerifier.emit(VerifierEvent.ShowSas, sasEvent);
|
mockVerifier.emit(VerifierEvent.ShowSas, sasEvent);
|
||||||
});
|
});
|
||||||
|
|
||||||
const emojis = container.getElementsByClassName("mx_VerificationShowSas_emojiSas_block");
|
expect(getAllByText("🦄")).toHaveLength(7);
|
||||||
expect(emojis.length).toEqual(7);
|
expect(getAllByText("Unicorn")).toHaveLength(7);
|
||||||
for (const emoji of emojis) {
|
|
||||||
expect(emoji).toHaveTextContent("🦄Unicorn");
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("'Verify own device' flow", () => {
|
describe("'Verify own device' flow", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user