From 2f2c4200267496e0d0acd9e09f6c8ab83aab0f85 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Thu, 29 Aug 2024 14:12:52 +1000 Subject: [PATCH] feat: remove custom tag renderer and refactor hover emojis --- ts/components/basic/I18n.tsx | 65 +--------------- .../basic/SessionCustomTagRenderer.tsx | 40 ---------- .../message/reactions/ReactionPopup.tsx | 72 +++++++++--------- ts/types/Localizer.ts | 74 +++---------------- ts/types/Reaction.ts | 1 + ts/util/emoji.ts | 3 + 6 files changed, 57 insertions(+), 198 deletions(-) delete mode 100644 ts/components/basic/SessionCustomTagRenderer.tsx diff --git a/ts/components/basic/I18n.tsx b/ts/components/basic/I18n.tsx index 74032dbee..079894398 100644 --- a/ts/components/basic/I18n.tsx +++ b/ts/components/basic/I18n.tsx @@ -1,21 +1,13 @@ import styled from 'styled-components'; -import { Fragment } from 'react'; -import { +import type { ArgsRecord, GetMessageArgs, I18nProps, LocalizerDictionary, LocalizerToken, } from '../../types/Localizer'; - import { useIsDarkTheme } from '../../state/selectors/theme'; import { SessionHtmlRenderer } from './SessionHTMLRenderer'; -import { - type CustomTag, - CustomTagProps, - SessionCustomTagRenderer, - supportedCustomTags, -} from './SessionCustomTagRenderer'; /** An array of supported html tags to render if found in a string */ export const supportedFormattingTags = ['b', 'i', 'u', 's', 'br', 'span']; @@ -29,10 +21,6 @@ function createSupportedFormattingTagsRegex() { ); } -function createSupportedCustomTagsRegex() { - return new RegExp(`<(${supportedCustomTags.join('|')})/>`, 'g'); -} - /** * Replaces all html tag identifiers with their escaped equivalents * @param str The string to sanitize @@ -121,41 +109,12 @@ export const I18n = (props: I18nProps) => { const containsFormattingTags = createSupportedFormattingTagsRegex().test(rawString); const cleanArgs = args && containsFormattingTags ? sanitizeArgs(args) : args; - let i18nString = window.i18n.formatMessageWithArgs( + const i18nString = window.i18n.formatMessageWithArgs( rawString as LocalizerDictionary[T], cleanArgs as ArgsRecord - ) as string; - - let startTag: CustomTag | null = null; - let endTag: CustomTag | null = null; - - i18nString = i18nString.replace( - createSupportedCustomTagsRegex(), - /** - * @param match - The entire match, including the custom tag. - * @param group - The custom tag, without the angle brackets. - * @param index - The index of the match in the string. - */ - (match: string, group: CustomTag, index: number) => { - if (index === 0) { - startTag = group; - } else if (index === i18nString.length - match.length) { - endTag = group; - } else { - /** - * If the match is not at the start or end of the string, throw an error. - * NOTE: This should never happen as this rule is enforced when the dictionary is generated. - */ - throw new Error( - `Custom tag ${group} (${match}) is not at the start or end (i=${index}) of the string: ${i18nString}` - ); - } - - return ''; - } ); - const content = containsFormattingTags ? ( + return containsFormattingTags ? ( /** If the string contains a relevant formatting tag, render it as HTML */ @@ -163,22 +122,4 @@ export const I18n = (props: I18nProps) => { ) : ( i18nString ); - - return ( - - {startTag ? ( - } - /> - ) : null} - {content} - {endTag ? ( - } - /> - ) : null} - - ); }; diff --git a/ts/components/basic/SessionCustomTagRenderer.tsx b/ts/components/basic/SessionCustomTagRenderer.tsx deleted file mode 100644 index 7ae662a6d..000000000 --- a/ts/components/basic/SessionCustomTagRenderer.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import styled from 'styled-components'; -import { nativeEmojiData } from '../../util/emoji'; - -const StyledEmoji = styled.span` - font-size: 36px; - margin-left: 8px; -`; - -export const supportedCustomTags = ['emoji'] as const; - -export type CustomTag = (typeof supportedCustomTags)[number]; - -/** - * A dictionary of custom tags and their rendering functions. - */ -export const customTag = { - emoji: ({ emoji }: { emoji: string }) => ( - - {emoji} - - ), -} as const; - -export type CustomTagProps = Parameters<(typeof customTag)[Tag]>[0]; - -/** - * Render a custom tag with its props. - * - * @param tag - The custom tag to render. - * @param tagProps - The props to pass to the custom tag. - */ -export const SessionCustomTagRenderer = ({ - tag, - tagProps, -}: { - tag: Tag; - tagProps: CustomTagProps; -}) => { - return customTag[tag](tagProps); -}; diff --git a/ts/components/conversation/message/reactions/ReactionPopup.tsx b/ts/components/conversation/message/reactions/ReactionPopup.tsx index 50a5b9f5a..97d651135 100644 --- a/ts/components/conversation/message/reactions/ReactionPopup.tsx +++ b/ts/components/conversation/message/reactions/ReactionPopup.tsx @@ -4,9 +4,12 @@ import { findAndFormatContact } from '../../../../models/message'; import { PubKey } from '../../../../session/types/PubKey'; import { I18n } from '../../../basic/I18n'; +import { nativeEmojiData } from '../../../../util/emoji'; +import { I18nProps, LocalizerToken } from '../../../../types/Localizer'; export type TipPosition = 'center' | 'left' | 'right'; +// TODO: Look into adjusting the width to match the new strings better export const POPUP_WIDTH = 216; // px export const StyledPopupContainer = styled.div<{ tooltipPosition: TipPosition }>` @@ -52,6 +55,11 @@ export const StyledPopupContainer = styled.div<{ tooltipPosition: TipPosition }> } `; +const StyledEmoji = styled.span` + font-size: 36px; + margin-block-start: 8px; +`; + const generateContactsString = ( senders: Array ): { contacts: Array; numberOfReactors: number; hasMe: boolean } => { @@ -72,46 +80,31 @@ const generateContactsString = ( return { contacts, hasMe, numberOfReactors }; }; -const getI18nComponent = ( +const getI18nComponentProps = ( isYou: boolean, contacts: Array, numberOfReactors: number, - emoji: string -) => { + emoji: string, + emojiName?: string +): I18nProps => { const name = contacts[0]; const other_name = contacts[1]; + const emoji_name = emojiName ? `:${emojiName}:` : emoji; + const count = numberOfReactors - 1; switch (numberOfReactors) { case 1: - return isYou ? ( - - ) : ( - - ); + return isYou + ? { token: 'emojiReactsHoverYouNameDesktop', args: { emoji_name } } + : { token: 'emojiReactsHoverNameDesktop', args: { name, emoji_name } }; case 2: - return isYou ? ( - - ) : ( - - ); + return isYou + ? { token: 'emojiReactsHoverYouNameTwoDesktop', args: { name, emoji_name } } + : { token: 'emojiReactsHoverNameTwoDesktop', args: { name, other_name, emoji_name } }; default: - return isYou ? ( - - ) : ( - - ); + return isYou + ? { token: 'emojiReactsHoverYouNameMultipleDesktop', args: { count, emoji_name } } + : { token: 'emojiReactsHoverTwoNameMultipleDesktop', args: { name, count, emoji_name } }; } }; @@ -127,19 +120,30 @@ type Props = { export const ReactionPopup = (props: Props) => { const { emoji, senders, tooltipPosition = 'center', onClick } = props; + const { emojiName, emojiAriaLabel } = useMemo( + () => ({ + emojiName: nativeEmojiData?.ids?.[emoji], + emojiAriaLabel: nativeEmojiData?.ariaLabels?.[emoji], + }), + [emoji] + ); + const { contacts, hasMe, numberOfReactors } = useMemo( () => generateContactsString(senders), [senders] ); - const content = useMemo( - () => getI18nComponent(hasMe, contacts, numberOfReactors, emoji), - [hasMe, contacts, numberOfReactors, emoji] + const i18nProps = useMemo( + () => getI18nComponentProps(hasMe, contacts, numberOfReactors, emoji, emojiName), + [hasMe, contacts, numberOfReactors, emoji, emojiName] ); return ( - {content} + + + {emoji} + ); }; diff --git a/ts/types/Localizer.ts b/ts/types/Localizer.ts index 6b9c3d642..146e2dc1e 100644 --- a/ts/types/Localizer.ts +++ b/ts/types/Localizer.ts @@ -1,24 +1,24 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import type { ElementType } from 'react'; import type { Dictionary } from '../localization/locales'; -import { CustomTag, CustomTagProps } from '../components/basic/SessionCustomTagRenderer'; -import { LOCALE_DEFAULTS } from '../localization/constants'; +import type { LOCALE_DEFAULTS } from '../localization/constants'; + +/** The dictionary of localized strings */ +export type LocalizerDictionary = Dictionary; /** A localization dictionary key */ -type Token = keyof Dictionary; +export type LocalizerToken = keyof Dictionary; /** A dynamic argument that can be used in a localized string */ export type DynamicArg = string | number; /** A record of dynamic arguments for a specific key in the localization dictionary */ -export type ArgsRecord = Record, DynamicArg>; - -export type PluralKey = 'count'; - -export type PluralString = `{${string}, plural, one [${string}] other [${string}]}`; +export type ArgsRecord = Record, DynamicArg>; // TODO: create a proper type for this export type DictionaryWithoutPluralStrings = Dictionary; +export type PluralKey = 'count'; +export type PluralString = `{${string}, plural, one [${string}] other [${string}]}`; /** The dynamic arguments in a localized string */ type DynamicArgs = @@ -28,18 +28,17 @@ type DynamicArgs = ? PluralVar | DynamicArgs | DynamicArgs : /** If a string segment follows the variable form parse its variable name and recursively * check for more dynamic args */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars -- We dont care about _Pre TODO: see if we can remove this infer LocalizedString extends `${string}{${infer Var}}${infer Rest}` ? Var | DynamicArgs : never; -export type ArgsRecordExcludingDefaults = Omit< +export type ArgsRecordExcludingDefaults = Omit< ArgsRecord, keyof typeof LOCALE_DEFAULTS >; /** The arguments for retrieving a localized message */ -export type GetMessageArgs = T extends Token +export type GetMessageArgs = T extends LocalizerToken ? DynamicArgs extends never ? [T] : ArgsRecordExcludingDefaults extends Record @@ -48,17 +47,14 @@ export type GetMessageArgs = T extends Token : never; /** Basic props for all calls of the I18n component */ -type I18nBaseProps = { +type I18nBaseProps = { token: T; asTag?: ElementType; className?: string; - // TODO: investigate making these required when required and not required when not required - startTagProps?: CustomStartTagProps; - endTagProps?: CustomEndTagProps; }; /** The props for the localization component */ -export type I18nProps = T extends Token +export type I18nProps = T extends LocalizerToken ? DynamicArgs extends never ? I18nBaseProps : ArgsRecordExcludingDefaults extends Record @@ -66,52 +62,6 @@ export type I18nProps = T extends Token : I18nBaseProps & { args: ArgsRecordExcludingDefaults } : never; -/** The props for custom tags at the start of an i18n strings */ -export type CustomStartTagProps = T extends Token - ? Dictionary[T] extends `<${infer Tag}/>${string}` - ? Tag extends CustomTag - ? CustomTagProps - : never - : never - : never; - -/** - * This is used to find the end tag. TypeScript navigates from outwards to inwards when doing magic - * with strings. This means we need a recursive type to find the actual end tag. - * - * @example For the string `{name} reacted with ` - * The first iteration will find `Tag` as `emoji` because it grabs the first `<` and the last `/>` - * Because it doesn't contain a `<` it will return the Tag. - * - * @example For the string `You, {name} & 1 other reacted with ` - * The first iteration will find `Tag` as `span>1 other reacted with , so we then check if Tag contains a `<`: - * - If it doesn't then we have found it; - * - If it does then we need to run it through the same process again to search deeper. - */ -type CustomEndTag = - LocalizedString extends `${string}<${infer Tag}/>${string}` ? FindCustomTag : never; - -type FindCustomTag = S extends CustomTag - ? S - : S extends `${string}<${infer Tag}` - ? Tag extends CustomTag - ? Tag - : FindCustomTag - : never; - -/** The props for custom tags at the end of an i18n strings */ -type CustomEndTagProps = - CustomEndTag extends CustomTag - ? CustomTagProps> - : never; - -/** The dictionary of localized strings */ -export type LocalizerDictionary = Dictionary; - -/** A localization dictionary key */ -export type LocalizerToken = Token; - export type I18nMethods = { /** @see {@link window.i18n.stripped} */ stripped: ( diff --git a/ts/types/Reaction.ts b/ts/types/Reaction.ts index 4e0c7a465..a2d8ef4dc 100644 --- a/ts/types/Reaction.ts +++ b/ts/types/Reaction.ts @@ -41,6 +41,7 @@ export interface FixedBaseEmoji extends Emoji { export interface NativeEmojiData extends EmojiMartData { ariaLabels?: Record; + ids?: Record; } export enum Action { diff --git a/ts/util/emoji.ts b/ts/util/emoji.ts index d4f967292..ca2950b0f 100644 --- a/ts/util/emoji.ts +++ b/ts/util/emoji.ts @@ -51,6 +51,7 @@ export let i18nEmojiData: typeof I18n | null = null; export async function initialiseEmojiData(data: any): Promise { const ariaLabels: Record = {}; + const ids: Record = {}; Object.entries(data.emojis).forEach(([key, value]: [string, any]) => { value.search = `,${[ [value.id, false], @@ -74,12 +75,14 @@ export async function initialiseEmojiData(data: any): Promise { (value as FixedBaseEmoji).skins.forEach(skin => { ariaLabels[skin.native] = value.name; + ids[skin.native] = value.id; }); data.emojis[key] = value; }); data.ariaLabels = ariaLabels; + data.ids = ids; nativeEmojiData = data; i18nEmojiData = await loadEmojiPanelI18n();