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:
committed by
GitHub
parent
24018f7e94
commit
e07e26cae5
@@ -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();
|
||||
});
|
||||
});
|
||||
43
packages/shared-components/src/crypto/SasEmoji/SasEmoji.tsx
Normal file
43
packages/shared-components/src/crypto/SasEmoji/SasEmoji.tsx
Normal 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>;
|
||||
}
|
||||
@@ -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>
|
||||
`;
|
||||
8
packages/shared-components/src/crypto/SasEmoji/index.ts
Normal file
8
packages/shared-components/src/crypto/SasEmoji/index.ts
Normal 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";
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user