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:
Michael Telatynski
2026-01-29 17:40:04 +00:00
committed by GitHub
parent 24018f7e94
commit e07e26cae5
23 changed files with 444 additions and 143 deletions

View File

@@ -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);
}

View File

@@ -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: ["🔧", "🔧", "🔧", "🔧", "🔧", "🔧", "🔧"],
},
};

View File

@@ -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();
});
});

View File

@@ -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>;
}

View File

@@ -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);
},
);
});

View File

@@ -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;
}

View File

@@ -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>
`;

View File

@@ -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";

View File

@@ -12,6 +12,7 @@ export * from "./audio/PlayPauseButton";
export * from "./audio/SeekBar";
export * from "./avatar/AvatarWithDetails";
export * from "./composer/Banner";
export * from "./crypto/SasEmoji";
export * from "./event-tiles/TextualEventView";
export * from "./message-body/MediaBody";
export * from "./pill-input/Pill";