From c92a18895e67ef53b8e02d9ee361bc155b73e29c Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Mon, 16 May 2022 13:34:27 +0200 Subject: [PATCH] [Refactoring] Teact: Introduce `TeactNode` type for JSX typing --- src/@types/teact.d.ts | 3 --- src/components/common/Avatar.tsx | 6 ++++-- src/components/common/PickerSelectedItem.tsx | 4 ++-- src/components/common/ProfilePhoto.tsx | 4 ++-- src/components/common/code/CodeBlock.tsx | 2 +- src/components/common/helpers/renderText.tsx | 16 ++++++++-------- .../middle/composer/inlineResults/BaseResult.tsx | 6 +++--- src/components/ui/Checkbox.tsx | 6 ++++-- src/components/ui/ConfirmDialog.tsx | 4 ++-- src/components/ui/Modal.tsx | 4 ++-- src/components/ui/Radio.tsx | 4 ++-- src/components/ui/RadioGroup.tsx | 6 ++++-- src/lib/teact/teact.ts | 9 +++++---- src/types/index.ts | 3 ++- src/util/highlightCode.ts | 8 ++++---- 15 files changed, 45 insertions(+), 40 deletions(-) delete mode 100644 src/@types/teact.d.ts diff --git a/src/@types/teact.d.ts b/src/@types/teact.d.ts deleted file mode 100644 index 05d546a0a..000000000 --- a/src/@types/teact.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -declare namespace JSX { - type Element = VirtualElement; -} diff --git a/src/components/common/Avatar.tsx b/src/components/common/Avatar.tsx index b84f62b47..41441f8fa 100644 --- a/src/components/common/Avatar.tsx +++ b/src/components/common/Avatar.tsx @@ -1,5 +1,7 @@ import { MouseEvent as ReactMouseEvent } from 'react'; -import React, { FC, memo, useCallback } from '../../lib/teact/teact'; +import React, { + FC, memo, TeactNode, useCallback, +} from '../../lib/teact/teact'; import { ApiChat, ApiMediaFormat, ApiPhoto, ApiUser, ApiUserStatus, @@ -75,7 +77,7 @@ const Avatar: FC = ({ const lang = useLang(); - let content: string | undefined = ''; + let content: TeactNode | undefined; const author = user ? getUserFullName(user) : (chat ? getChatTitle(lang, chat) : text); if (isSavedMessages) { diff --git a/src/components/common/PickerSelectedItem.tsx b/src/components/common/PickerSelectedItem.tsx index b62d2cdce..bff87e9c7 100644 --- a/src/components/common/PickerSelectedItem.tsx +++ b/src/components/common/PickerSelectedItem.tsx @@ -1,4 +1,4 @@ -import React, { FC, memo } from '../../lib/teact/teact'; +import React, { FC, memo, TeactNode } from '../../lib/teact/teact'; import { withGlobal } from '../../global'; import { ApiChat, ApiUser } from '../../api/types'; @@ -42,7 +42,7 @@ const PickerSelectedItem: FC = ({ }) => { const lang = useLang(); - let iconElement: any; + let iconElement: TeactNode | undefined; let titleText: any; if (icon && title) { diff --git a/src/components/common/ProfilePhoto.tsx b/src/components/common/ProfilePhoto.tsx index 3696dbd3a..3c4537b09 100644 --- a/src/components/common/ProfilePhoto.tsx +++ b/src/components/common/ProfilePhoto.tsx @@ -1,4 +1,4 @@ -import React, { FC, memo } from '../../lib/teact/teact'; +import React, { FC, memo, TeactNode } from '../../lib/teact/teact'; import { ApiChat, ApiMediaFormat, ApiPhoto, ApiUser, @@ -68,7 +68,7 @@ const ProfilePhoto: FC = ({ const avatarBlobUrl = useMedia(avatarMediaHash, false, ApiMediaFormat.BlobUrl, lastSyncTime); const imageSrc = photoBlobUrl || avatarBlobUrl || photo?.thumbnail?.dataUri; - let content: string | undefined = ''; + let content: TeactNode | undefined; if (isSavedMessages) { content = ; diff --git a/src/components/common/code/CodeBlock.tsx b/src/components/common/code/CodeBlock.tsx index 5119c2afe..0ac21a9c0 100644 --- a/src/components/common/code/CodeBlock.tsx +++ b/src/components/common/code/CodeBlock.tsx @@ -21,7 +21,7 @@ const CodeBlock: FC = ({ text, language, noCopy }) => { const [isWordWrap, setWordWrap] = useState(true); const { result: highlighted } = useAsync(() => { - if (!language) return Promise.resolve(); + if (!language) return Promise.resolve(undefined); return import('../../../util/highlightCode') .then((lib) => lib.default(text, language)); }, [language, text]); diff --git a/src/components/common/helpers/renderText.tsx b/src/components/common/helpers/renderText.tsx index ca22bcfb8..1f1124ea9 100644 --- a/src/components/common/helpers/renderText.tsx +++ b/src/components/common/helpers/renderText.tsx @@ -76,7 +76,7 @@ export default function renderText( function escapeHtml(textParts: TextPart[]): TextPart[] { const divEl = document.createElement('div'); - return textParts.reduce((result, part) => { + return textParts.reduce((result: TextPart[], part) => { if (typeof part !== 'string') { result.push(part); return result; @@ -86,7 +86,7 @@ function escapeHtml(textParts: TextPart[]): TextPart[] { result.push(divEl.innerHTML); return result; - }, [] as TextPart[]); + }, []); } function replaceEmojis(textParts: TextPart[], size: 'big' | 'small', type: 'jsx' | 'html'): TextPart[] { @@ -174,7 +174,7 @@ function addLineBreaks(textParts: TextPart[], type: 'jsx' | 'html'): TextPart[] } function addHighlight(textParts: TextPart[], highlight: string | undefined): TextPart[] { - return textParts.reduce((result, part) => { + return textParts.reduce((result, part) => { if (typeof part !== 'string' || !highlight) { result.push(part); return result; @@ -198,13 +198,13 @@ function addHighlight(textParts: TextPart[], highlight: string | undefined): Tex newParts.push(part.substring(queryPosition + highlight.length)); return [...result, ...newParts]; - }, [] as TextPart[]); + }, []); } const RE_LINK = new RegExp(`${RE_LINK_TEMPLATE}|${RE_MENTION_TEMPLATE}`, 'ig'); function addLinks(textParts: TextPart[]): TextPart[] { - return textParts.reduce((result, part) => { + return textParts.reduce((result, part) => { if (typeof part !== 'string') { result.push(part); return result; @@ -244,11 +244,11 @@ function addLinks(textParts: TextPart[]): TextPart[] { content.push(part.substring(lastIndex)); return [...result, ...content]; - }, [] as TextPart[]); + }, []); } function replaceSimpleMarkdown(textParts: TextPart[], type: 'jsx' | 'html'): TextPart[] { - return textParts.reduce((result, part) => { + return textParts.reduce((result, part) => { if (typeof part !== 'string') { result.push(part); return result; @@ -280,7 +280,7 @@ function replaceSimpleMarkdown(textParts: TextPart[], type: 'jsx' | 'html'): Tex return entityResult; }, result); - }, [] as TextPart[]); + }, []); } export function areLinesWrapping(text: string, element: HTMLElement) { diff --git a/src/components/middle/composer/inlineResults/BaseResult.tsx b/src/components/middle/composer/inlineResults/BaseResult.tsx index 671392573..ca3745a2b 100644 --- a/src/components/middle/composer/inlineResults/BaseResult.tsx +++ b/src/components/middle/composer/inlineResults/BaseResult.tsx @@ -1,15 +1,15 @@ -import React, { FC, memo } from '../../../../lib/teact/teact'; +import React, { FC, memo, TeactNode } from '../../../../lib/teact/teact'; import { ApiWebDocument } from '../../../../api/types'; import { getFirstLetters } from '../../../../util/textFormat'; import renderText from '../../../common/helpers/renderText'; import useMedia from '../../../../hooks/useMedia'; +import { preventMessageInputBlurWithBubbling } from '../../helpers/preventMessageInputBlur'; import ListItem from '../../../ui/ListItem'; import './BaseResult.scss'; -import { preventMessageInputBlurWithBubbling } from '../../helpers/preventMessageInputBlur'; export type OwnProps = { focus?: boolean; @@ -30,7 +30,7 @@ const BaseResult: FC = ({ transitionClassNames = '', onClick, }) => { - let content: string | undefined = ''; + let content: TeactNode | undefined; const thumbnailDataUrl = useMedia(thumbnail ? `webDocument:${thumbnail.url}` : undefined); thumbUrl = thumbUrl || thumbnailDataUrl; diff --git a/src/components/ui/Checkbox.tsx b/src/components/ui/Checkbox.tsx index 88b58b39c..4016fd762 100644 --- a/src/components/ui/Checkbox.tsx +++ b/src/components/ui/Checkbox.tsx @@ -1,5 +1,7 @@ import { ChangeEvent } from 'react'; -import React, { FC, memo, useCallback } from '../../lib/teact/teact'; +import React, { + FC, memo, TeactNode, useCallback, +} from '../../lib/teact/teact'; import buildClassName from '../../util/buildClassName'; import useLang from '../../hooks/useLang'; @@ -13,7 +15,7 @@ type OwnProps = { id?: string; name?: string; value?: string; - label: string; + label: TeactNode; subLabel?: string; checked: boolean; disabled?: boolean; diff --git a/src/components/ui/ConfirmDialog.tsx b/src/components/ui/ConfirmDialog.tsx index dd0fcfe83..21b3de16a 100644 --- a/src/components/ui/ConfirmDialog.tsx +++ b/src/components/ui/ConfirmDialog.tsx @@ -1,5 +1,5 @@ import React, { - FC, memo, useCallback, useRef, + FC, memo, TeactNode, useCallback, useRef, } from '../../lib/teact/teact'; import { TextPart } from '../../types'; @@ -15,7 +15,7 @@ type OwnProps = { onClose: () => void; onCloseAnimationEnd?: () => void; title?: string; - header?: FC; + header?: TeactNode; textParts?: TextPart[]; text?: string; confirmLabel?: string; diff --git a/src/components/ui/Modal.tsx b/src/components/ui/Modal.tsx index 705a7fcff..b07de4d62 100644 --- a/src/components/ui/Modal.tsx +++ b/src/components/ui/Modal.tsx @@ -1,6 +1,6 @@ import { RefObject } from 'react'; import React, { - FC, useEffect, useRef, + FC, TeactNode, useEffect, useRef, } from '../../lib/teact/teact'; import { TextPart } from '../../types'; @@ -25,7 +25,7 @@ type OwnProps = { title?: string | TextPart[]; className?: string; isOpen?: boolean; - header?: any; + header?: TeactNode; hasCloseButton?: boolean; noBackdrop?: boolean; children: React.ReactNode; diff --git a/src/components/ui/Radio.tsx b/src/components/ui/Radio.tsx index f15d659a1..8a593939d 100644 --- a/src/components/ui/Radio.tsx +++ b/src/components/ui/Radio.tsx @@ -1,5 +1,5 @@ import { ChangeEvent } from 'react'; -import React, { FC, memo } from '../../lib/teact/teact'; +import React, { FC, memo, TeactNode } from '../../lib/teact/teact'; import buildClassName from '../../util/buildClassName'; import useLang from '../../hooks/useLang'; @@ -11,7 +11,7 @@ import './Radio.scss'; type OwnProps = { id?: string; name: string; - label: string; + label: TeactNode; subLabel?: string; value: string; checked: boolean; diff --git a/src/components/ui/RadioGroup.tsx b/src/components/ui/RadioGroup.tsx index 4a2136ec8..869de3130 100644 --- a/src/components/ui/RadioGroup.tsx +++ b/src/components/ui/RadioGroup.tsx @@ -1,10 +1,12 @@ import { ChangeEvent } from 'react'; -import React, { FC, useCallback, memo } from '../../lib/teact/teact'; +import React, { + FC, useCallback, memo, TeactNode, +} from '../../lib/teact/teact'; import Radio from './Radio'; export type IRadioOption = { - label: string; + label: TeactNode; subLabel?: string; value: string; hidden?: boolean; diff --git a/src/lib/teact/teact.ts b/src/lib/teact/teact.ts index 96407c72a..89b5f1d6b 100644 --- a/src/lib/teact/teact.ts +++ b/src/lib/teact/teact.ts @@ -1,3 +1,4 @@ +import type { ReactElement } from 'react'; import { DEBUG, DEBUG_MORE } from '../../config'; import { fastRaf, fastRafPrimary, onTickEnd, onTickEndPrimary, throttleWithPrimaryRaf, throttleWithRaf, @@ -102,6 +103,9 @@ export type VirtualRealElement = | VirtualElementComponent; export type VirtualElementChildren = VirtualElement[]; +// Compatibility with JSX types +export type TeactNode = ReactElement | string | number | boolean; + const Fragment = Symbol('Fragment'); const DEBUG_RENDER_THRESHOLD = 7; @@ -712,10 +716,7 @@ export function memo(Component: T, debugKey?: string) { } as T; } -// We need to keep it here for JSX. -const Teact = { +export default { createElement, Fragment, }; - -export default Teact; diff --git a/src/types/index.ts b/src/types/index.ts index c860f90c8..8f29ccb9e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,3 +1,4 @@ +import { TeactNode } from '../lib/teact/teact'; import { ApiBotInlineMediaResult, ApiBotInlineResult, ApiBotInlineSwitchPm, ApiChatInviteImporter, @@ -5,7 +6,7 @@ import { ApiLanguage, ApiMessage, ApiShippingAddress, ApiStickerSet, } from '../api/types'; -export type TextPart = string | JSX.Element; +export type TextPart = TeactNode; export enum LoadMoreDirection { Backwards, diff --git a/src/util/highlightCode.ts b/src/util/highlightCode.ts index e7143a10f..202926d86 100644 --- a/src/util/highlightCode.ts +++ b/src/util/highlightCode.ts @@ -1,6 +1,6 @@ import type { Element, Root } from 'hast'; import { lowlight } from 'lowlight/lib/core'; -import Teact from '../lib/teact/teact'; +import Teact, { TeactNode } from '../lib/teact/teact'; // First element in alias array MUST BE a language package name const SUPPORTED_LANGUAGES = { @@ -85,7 +85,7 @@ async function ensureLanguage(language: string) { return true; } -function treeToElements(tree: Element | Root): JSX.Element { +function treeToElements(tree: Element | Root): TeactNode { const children = tree.children.map((child) => { if (child.type === 'text') { return child.value; @@ -97,12 +97,12 @@ function treeToElements(tree: Element | Root): JSX.Element { }).filter(Boolean); if (tree.type === 'root') { - return Teact.createElement('code', { className: 'hljs custom-scroll-x' }, children); + return Teact.createElement('code', { className: 'hljs custom-scroll-x' }, children) as unknown as TeactNode; } const name = tree.tagName; const classNameArray = tree.properties?.className as string[]; const className = classNameArray?.join(' '); - return Teact.createElement(name, { className }, children); + return Teact.createElement(name, { className }, children) as unknown as TeactNode; }