Message, Composer: Support copy-paste formatting (#2096)
This commit is contained in:
parent
b69d4ee2f1
commit
0a5ea95c52
@ -1259,7 +1259,7 @@ export function buildLocalForwardedMessage(
|
|||||||
const shouldDropCustomEmoji = !isCurrentUserPremium;
|
const shouldDropCustomEmoji = !isCurrentUserPremium;
|
||||||
const strippedText = content.text?.entities && shouldDropCustomEmoji ? {
|
const strippedText = content.text?.entities && shouldDropCustomEmoji ? {
|
||||||
text: content.text.text,
|
text: content.text.text,
|
||||||
entities: content.text.entities?.filter((entity) => entity.type !== ApiMessageEntityTypes.CustomEmoji),
|
entities: content.text.entities.filter((entity) => entity.type !== ApiMessageEntityTypes.CustomEmoji),
|
||||||
} : content.text;
|
} : content.text;
|
||||||
const emojiOnlyCount = content.text && parseEmojiOnlyString(content.text.text);
|
const emojiOnlyCount = content.text && parseEmojiOnlyString(content.text.text);
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import { Api as GramJs } from '../../../lib/gramjs';
|
|||||||
|
|
||||||
import type {
|
import type {
|
||||||
ApiAppConfig,
|
ApiAppConfig,
|
||||||
ApiChat,
|
|
||||||
ApiError,
|
ApiError,
|
||||||
ApiLangString,
|
ApiLangString,
|
||||||
ApiLanguage,
|
ApiLanguage,
|
||||||
|
|||||||
@ -28,8 +28,24 @@
|
|||||||
|
|
||||||
border-radius: 0 !important;
|
border-radius: 0 !important;
|
||||||
|
|
||||||
|
user-select: none !important;
|
||||||
|
|
||||||
:global(canvas) {
|
:global(canvas) {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
height: 100% !important;
|
height: 100% !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.highlightCatch {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 1;
|
||||||
|
user-select: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.altEmoji {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { getGlobal } from '../../global';
|
|||||||
|
|
||||||
import type { FC, TeactNode } from '../../lib/teact/teact';
|
import type { FC, TeactNode } from '../../lib/teact/teact';
|
||||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||||
|
import { ApiMessageEntityTypes } from '../../api/types';
|
||||||
|
|
||||||
import { getPropertyHexColor } from '../../util/themeStyle';
|
import { getPropertyHexColor } from '../../util/themeStyle';
|
||||||
import { hexToRgb } from '../../util/switchTheme';
|
import { hexToRgb } from '../../util/switchTheme';
|
||||||
@ -19,6 +20,7 @@ import StickerView from './StickerView';
|
|||||||
|
|
||||||
import styles from './CustomEmoji.module.scss';
|
import styles from './CustomEmoji.module.scss';
|
||||||
import svgPlaceholder from '../../assets/square.svg';
|
import svgPlaceholder from '../../assets/square.svg';
|
||||||
|
import blankImg from '../../assets/blank.png';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
ref?: React.RefObject<HTMLDivElement>;
|
ref?: React.RefObject<HTMLDivElement>;
|
||||||
@ -121,8 +123,12 @@ const CustomEmoji: FC<OwnProps> = ({
|
|||||||
withGridFix && styles.withGridFix,
|
withGridFix && styles.withGridFix,
|
||||||
)}
|
)}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
data-entity-type={ApiMessageEntityTypes.CustomEmoji}
|
||||||
|
data-document-id={documentId}
|
||||||
|
data-alt={customEmoji?.emoji}
|
||||||
style={style}
|
style={style}
|
||||||
>
|
>
|
||||||
|
<img className={styles.highlightCatch} src={blankImg} alt={customEmoji?.emoji} draggable={false} />
|
||||||
{!customEmoji ? (
|
{!customEmoji ? (
|
||||||
<img className={styles.thumb} src={svgPlaceholder} alt="Emoji" />
|
<img className={styles.thumb} src={svgPlaceholder} alt="Emoji" />
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
import type { FC } from '../../lib/teact/teact';
|
|
||||||
import React, { memo, useCallback } from '../../lib/teact/teact';
|
import React, { memo, useCallback } from '../../lib/teact/teact';
|
||||||
import { getActions } from '../../global';
|
import { getActions } from '../../global';
|
||||||
import convertPunycode from '../../lib/punycode';
|
import convertPunycode from '../../lib/punycode';
|
||||||
|
|
||||||
|
import type { FC } from '../../lib/teact/teact';
|
||||||
|
import { ApiMessageEntityTypes } from '../../api/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DEBUG,
|
DEBUG,
|
||||||
} from '../../config';
|
} from '../../config';
|
||||||
@ -55,6 +57,7 @@ const SafeLink: FC<OwnProps> = ({
|
|||||||
className={classNames}
|
className={classNames}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
dir={isRtl ? 'rtl' : 'auto'}
|
dir={isRtl ? 'rtl' : 'auto'}
|
||||||
|
data-entity-type={ApiMessageEntityTypes.Url}
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import type { FC } from '../../../lib/teact/teact';
|
|
||||||
import React, { memo, useCallback, useState } from '../../../lib/teact/teact';
|
import React, { memo, useCallback, useState } from '../../../lib/teact/teact';
|
||||||
|
|
||||||
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
|
import { ApiMessageEntityTypes } from '../../../api/types';
|
||||||
|
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
|
||||||
import useAsync from '../../../hooks/useAsync';
|
import useAsync from '../../../hooks/useAsync';
|
||||||
@ -36,7 +38,7 @@ const CodeBlock: FC<OwnProps> = ({ text, language, noCopy }) => {
|
|||||||
const blockClass = buildClassName('code-block', !isWordWrap && 'no-word-wrap');
|
const blockClass = buildClassName('code-block', !isWordWrap && 'no-word-wrap');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<pre className={blockClass}>
|
<pre className={blockClass} data-entity-type={ApiMessageEntityTypes.Pre} data-language={language}>
|
||||||
{highlighted}
|
{highlighted}
|
||||||
<CodeOverlay
|
<CodeOverlay
|
||||||
text={text}
|
text={text}
|
||||||
|
|||||||
@ -23,6 +23,7 @@ export function renderMessageText(
|
|||||||
truncateLength?: number,
|
truncateLength?: number,
|
||||||
isProtected?: boolean,
|
isProtected?: boolean,
|
||||||
observeIntersection?: ObserveFn,
|
observeIntersection?: ObserveFn,
|
||||||
|
shouldRenderAsHtml?: boolean,
|
||||||
) {
|
) {
|
||||||
const { text, entities } = message.content.text || {};
|
const { text, entities } = message.content.text || {};
|
||||||
|
|
||||||
@ -36,7 +37,7 @@ export function renderMessageText(
|
|||||||
entities,
|
entities,
|
||||||
highlight,
|
highlight,
|
||||||
emojiSize,
|
emojiSize,
|
||||||
undefined,
|
shouldRenderAsHtml,
|
||||||
message.id,
|
message.id,
|
||||||
isSimple,
|
isSimple,
|
||||||
isProtected,
|
isProtected,
|
||||||
|
|||||||
@ -335,15 +335,16 @@ function processEntity(
|
|||||||
|
|
||||||
switch (entity.type) {
|
switch (entity.type) {
|
||||||
case ApiMessageEntityTypes.Bold:
|
case ApiMessageEntityTypes.Bold:
|
||||||
return <strong>{renderNestedMessagePart()}</strong>;
|
return <strong data-entity-type={entity.type}>{renderNestedMessagePart()}</strong>;
|
||||||
case ApiMessageEntityTypes.Blockquote:
|
case ApiMessageEntityTypes.Blockquote:
|
||||||
return <blockquote>{renderNestedMessagePart()}</blockquote>;
|
return <blockquote data-entity-type={entity.type}>{renderNestedMessagePart()}</blockquote>;
|
||||||
case ApiMessageEntityTypes.BotCommand:
|
case ApiMessageEntityTypes.BotCommand:
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
onClick={handleBotCommandClick}
|
onClick={handleBotCommandClick}
|
||||||
className="text-entity-link"
|
className="text-entity-link"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
|
data-entity-type={entity.type}
|
||||||
>
|
>
|
||||||
{renderNestedMessagePart()}
|
{renderNestedMessagePart()}
|
||||||
</a>
|
</a>
|
||||||
@ -354,6 +355,7 @@ function processEntity(
|
|||||||
onClick={handleHashtagClick}
|
onClick={handleHashtagClick}
|
||||||
className="text-entity-link"
|
className="text-entity-link"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
|
data-entity-type={entity.type}
|
||||||
>
|
>
|
||||||
{renderNestedMessagePart()}
|
{renderNestedMessagePart()}
|
||||||
</a>
|
</a>
|
||||||
@ -364,6 +366,7 @@ function processEntity(
|
|||||||
onClick={handleHashtagClick}
|
onClick={handleHashtagClick}
|
||||||
className="text-entity-link"
|
className="text-entity-link"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
|
data-entity-type={entity.type}
|
||||||
>
|
>
|
||||||
{renderNestedMessagePart()}
|
{renderNestedMessagePart()}
|
||||||
</a>
|
</a>
|
||||||
@ -375,6 +378,7 @@ function processEntity(
|
|||||||
onClick={!isProtected ? handleCodeClick : undefined}
|
onClick={!isProtected ? handleCodeClick : undefined}
|
||||||
role="textbox"
|
role="textbox"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
|
data-entity-type={entity.type}
|
||||||
>
|
>
|
||||||
{renderNestedMessagePart()}
|
{renderNestedMessagePart()}
|
||||||
</code>
|
</code>
|
||||||
@ -387,12 +391,13 @@ function processEntity(
|
|||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-entity-link"
|
className="text-entity-link"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
|
data-entity-type={entity.type}
|
||||||
>
|
>
|
||||||
{renderNestedMessagePart()}
|
{renderNestedMessagePart()}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
case ApiMessageEntityTypes.Italic:
|
case ApiMessageEntityTypes.Italic:
|
||||||
return <em>{renderNestedMessagePart()}</em>;
|
return <em data-entity-type={entity.type}>{renderNestedMessagePart()}</em>;
|
||||||
case ApiMessageEntityTypes.MentionName:
|
case ApiMessageEntityTypes.MentionName:
|
||||||
return (
|
return (
|
||||||
<MentionLink userId={entity.userId}>
|
<MentionLink userId={entity.userId}>
|
||||||
@ -411,6 +416,7 @@ function processEntity(
|
|||||||
href={`tel:${entityText}`}
|
href={`tel:${entityText}`}
|
||||||
className="text-entity-link"
|
className="text-entity-link"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
|
data-entity-type={entity.type}
|
||||||
>
|
>
|
||||||
{renderNestedMessagePart()}
|
{renderNestedMessagePart()}
|
||||||
</a>
|
</a>
|
||||||
@ -418,7 +424,7 @@ function processEntity(
|
|||||||
case ApiMessageEntityTypes.Pre:
|
case ApiMessageEntityTypes.Pre:
|
||||||
return <CodeBlock text={entityText} language={entity.language} noCopy={isProtected} />;
|
return <CodeBlock text={entityText} language={entity.language} noCopy={isProtected} />;
|
||||||
case ApiMessageEntityTypes.Strike:
|
case ApiMessageEntityTypes.Strike:
|
||||||
return <del>{renderNestedMessagePart()}</del>;
|
return <del data-entity-type={entity.type}>{renderNestedMessagePart()}</del>;
|
||||||
case ApiMessageEntityTypes.TextUrl:
|
case ApiMessageEntityTypes.TextUrl:
|
||||||
case ApiMessageEntityTypes.Url:
|
case ApiMessageEntityTypes.Url:
|
||||||
return (
|
return (
|
||||||
@ -430,7 +436,7 @@ function processEntity(
|
|||||||
</SafeLink>
|
</SafeLink>
|
||||||
);
|
);
|
||||||
case ApiMessageEntityTypes.Underline:
|
case ApiMessageEntityTypes.Underline:
|
||||||
return <ins>{renderNestedMessagePart()}</ins>;
|
return <ins data-entity-type={entity.type}>{renderNestedMessagePart()}</ins>;
|
||||||
case ApiMessageEntityTypes.Spoiler:
|
case ApiMessageEntityTypes.Spoiler:
|
||||||
return <Spoiler messageId={messageId}>{renderNestedMessagePart()}</Spoiler>;
|
return <Spoiler messageId={messageId}>{renderNestedMessagePart()}</Spoiler>;
|
||||||
case ApiMessageEntityTypes.CustomEmoji:
|
case ApiMessageEntityTypes.CustomEmoji:
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import type { MouseEvent as ReactMouseEvent } from 'react';
|
|
||||||
import type { FC } from '../../../lib/teact/teact';
|
|
||||||
import React, {
|
import React, {
|
||||||
memo, useCallback, useEffect, useRef,
|
memo, useCallback, useEffect, useRef,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
|
|
||||||
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
|
import { ApiMessageEntityTypes } from '../../../api/types';
|
||||||
|
|
||||||
import { createClassNameBuilder } from '../../../util/buildClassName';
|
import { createClassNameBuilder } from '../../../util/buildClassName';
|
||||||
import useFlag from '../../../hooks/useFlag';
|
import useFlag from '../../../hooks/useFlag';
|
||||||
|
|
||||||
@ -34,7 +35,7 @@ const Spoiler: FC<OwnProps> = ({
|
|||||||
|
|
||||||
const [isRevealed, reveal, conceal] = useFlag();
|
const [isRevealed, reveal, conceal] = useFlag();
|
||||||
|
|
||||||
const handleClick = useCallback((e: ReactMouseEvent<HTMLDivElement, MouseEvent>) => {
|
const handleClick = useCallback((e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
@ -74,6 +75,7 @@ const Spoiler: FC<OwnProps> = ({
|
|||||||
!isRevealed && Boolean(messageId) && 'animated',
|
!isRevealed && Boolean(messageId) && 'animated',
|
||||||
)}
|
)}
|
||||||
onClick={messageId && !isRevealed ? handleClick : undefined}
|
onClick={messageId && !isRevealed ? handleClick : undefined}
|
||||||
|
data-entity-type={ApiMessageEntityTypes.Spoiler}
|
||||||
>
|
>
|
||||||
<span className={buildClassName('content')} ref={contentRef}>
|
<span className={buildClassName('content')} ref={contentRef}>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@ -78,6 +78,7 @@ import { getServerTime } from '../../../util/serverTime';
|
|||||||
import { selectCurrentLimit } from '../../../global/selectors/limits';
|
import { selectCurrentLimit } from '../../../global/selectors/limits';
|
||||||
import { buildCustomEmojiHtml } from './helpers/customEmoji';
|
import { buildCustomEmojiHtml } from './helpers/customEmoji';
|
||||||
import { processMessageInputForCustomEmoji } from '../../../util/customEmojiManager';
|
import { processMessageInputForCustomEmoji } from '../../../util/customEmojiManager';
|
||||||
|
import { getTextWithEntitiesAsHtml } from '../../common/helpers/renderTextWithEntities';
|
||||||
|
|
||||||
import useFlag from '../../../hooks/useFlag';
|
import useFlag from '../../../hooks/useFlag';
|
||||||
import usePrevious from '../../../hooks/usePrevious';
|
import usePrevious from '../../../hooks/usePrevious';
|
||||||
@ -512,6 +513,13 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
});
|
});
|
||||||
}, [htmlRef, setHtml]);
|
}, [htmlRef, setHtml]);
|
||||||
|
|
||||||
|
const insertFormattedTextAndUpdateCursor = useCallback((
|
||||||
|
text: ApiFormattedText, inputId: string = EDITABLE_INPUT_ID,
|
||||||
|
) => {
|
||||||
|
const newHtml = getTextWithEntitiesAsHtml(text);
|
||||||
|
insertHtmlAndUpdateCursor(newHtml, inputId);
|
||||||
|
}, [insertHtmlAndUpdateCursor]);
|
||||||
|
|
||||||
const insertTextAndUpdateCursor = useCallback((text: string, inputId: string = EDITABLE_INPUT_ID) => {
|
const insertTextAndUpdateCursor = useCallback((text: string, inputId: string = EDITABLE_INPUT_ID) => {
|
||||||
const newHtml = renderText(text, ['escape_html', 'emoji_html', 'br_html'])
|
const newHtml = renderText(text, ['escape_html', 'emoji_html', 'br_html'])
|
||||||
.join('')
|
.join('')
|
||||||
@ -565,6 +573,24 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
};
|
};
|
||||||
}, [chatId, resetComposer, stopRecordingVoiceRef]);
|
}, [chatId, resetComposer, stopRecordingVoiceRef]);
|
||||||
|
|
||||||
|
const showCustomEmojiPremiumNotification = useCallback(() => {
|
||||||
|
const notificationNumber = customEmojiNotificationNumber.current;
|
||||||
|
if (!notificationNumber) {
|
||||||
|
showNotification({
|
||||||
|
message: lang('UnlockPremiumEmojiHint'),
|
||||||
|
action: () => openPremiumModal({ initialSection: 'animated_emoji' }),
|
||||||
|
actionText: lang('PremiumMore'),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
showNotification({
|
||||||
|
message: lang('UnlockPremiumEmojiHint2'),
|
||||||
|
action: () => openChat({ id: currentUserId, shouldReplaceHistory: true }),
|
||||||
|
actionText: lang('Open'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
customEmojiNotificationNumber.current = Number(!notificationNumber);
|
||||||
|
}, [currentUserId, lang, openChat, openPremiumModal, showNotification]);
|
||||||
|
|
||||||
const [handleEditComplete, handleEditCancel] = useEditing(
|
const [handleEditComplete, handleEditCancel] = useEditing(
|
||||||
htmlRef,
|
htmlRef,
|
||||||
setHtml,
|
setHtml,
|
||||||
@ -578,7 +604,14 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
editingDraft,
|
editingDraft,
|
||||||
);
|
);
|
||||||
useDraft(draft, chatId, threadId, htmlRef, setHtml, editingMessage, lastSyncTime);
|
useDraft(draft, chatId, threadId, htmlRef, setHtml, editingMessage, lastSyncTime);
|
||||||
useClipboardPaste(isForCurrentMessageList, insertTextAndUpdateCursor, handleSetAttachments, editingMessage);
|
useClipboardPaste(
|
||||||
|
isForCurrentMessageList,
|
||||||
|
insertFormattedTextAndUpdateCursor,
|
||||||
|
handleSetAttachments,
|
||||||
|
editingMessage,
|
||||||
|
!isCurrentUserPremium && !isChatWithSelf,
|
||||||
|
showCustomEmojiPremiumNotification,
|
||||||
|
);
|
||||||
|
|
||||||
const handleEmbeddedClear = useCallback(() => {
|
const handleEmbeddedClear = useCallback(() => {
|
||||||
if (editingMessage) {
|
if (editingMessage) {
|
||||||
@ -775,29 +808,12 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
const handleCustomEmojiSelect = useCallback((emoji: ApiSticker) => {
|
const handleCustomEmojiSelect = useCallback((emoji: ApiSticker) => {
|
||||||
if (!emoji.isFree && !isCurrentUserPremium && !isChatWithSelf) {
|
if (!emoji.isFree && !isCurrentUserPremium && !isChatWithSelf) {
|
||||||
const notificationNumber = customEmojiNotificationNumber.current;
|
showCustomEmojiPremiumNotification();
|
||||||
if (!notificationNumber) {
|
|
||||||
showNotification({
|
|
||||||
message: lang('UnlockPremiumEmojiHint'),
|
|
||||||
action: () => openPremiumModal({ initialSection: 'animated_emoji' }),
|
|
||||||
actionText: lang('PremiumMore'),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
showNotification({
|
|
||||||
message: lang('UnlockPremiumEmojiHint2'),
|
|
||||||
action: () => openChat({ id: currentUserId, shouldReplaceHistory: true }),
|
|
||||||
actionText: lang('Open'),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
customEmojiNotificationNumber.current = Number(!notificationNumber);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
insertCustomEmojiAndUpdateCursor(emoji);
|
insertCustomEmojiAndUpdateCursor(emoji);
|
||||||
}, [
|
}, [insertCustomEmojiAndUpdateCursor, isChatWithSelf, isCurrentUserPremium, showCustomEmojiPremiumNotification]);
|
||||||
currentUserId, insertCustomEmojiAndUpdateCursor, isChatWithSelf, isCurrentUserPremium, lang,
|
|
||||||
openChat, openPremiumModal, showNotification,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const handleStickerSelect = useCallback((
|
const handleStickerSelect = useCallback((
|
||||||
sticker: ApiSticker, isSilent?: boolean, isScheduleRequested?: boolean, shouldPreserveInput = false,
|
sticker: ApiSticker, isSilent?: boolean, isScheduleRequested?: boolean, shouldPreserveInput = false,
|
||||||
@ -1396,7 +1412,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
const { currentUserId } = global;
|
const { currentUserId } = global;
|
||||||
const defaultSendAsId = chat?.fullInfo ? chat?.fullInfo?.sendAsId || currentUserId : undefined;
|
const defaultSendAsId = chat?.fullInfo ? chat?.fullInfo?.sendAsId || currentUserId : undefined;
|
||||||
const sendAsId = chat?.sendAsPeerIds && defaultSendAsId
|
const sendAsId = chat?.sendAsPeerIds && defaultSendAsId
|
||||||
&& chat.sendAsPeerIds.some((peer) => peer.id === defaultSendAsId) ? defaultSendAsId
|
&& chat.sendAsPeerIds.some((peer) => peer.id === defaultSendAsId) ? defaultSendAsId
|
||||||
: (chat?.adminRights?.anonymous ? chat?.id : undefined);
|
: (chat?.adminRights?.anonymous ? chat?.id : undefined);
|
||||||
const sendAsUser = sendAsId ? selectUser(global, sendAsId) : undefined;
|
const sendAsUser = sendAsId ? selectUser(global, sendAsId) : undefined;
|
||||||
const sendAsChat = !sendAsUser && sendAsId ? selectChat(global, sendAsId) : undefined;
|
const sendAsChat = !sendAsUser && sendAsId ? selectChat(global, sendAsId) : undefined;
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import { getActions, withGlobal } from '../../../global';
|
|||||||
|
|
||||||
import type { FC } from '../../../lib/teact/teact';
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
import type { ApiChat, ApiMessage, ApiUser } from '../../../api/types';
|
import type { ApiChat, ApiMessage, ApiUser } from '../../../api/types';
|
||||||
import { ApiMessageEntityTypes } from '../../../api/types';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
selectChat,
|
selectChat,
|
||||||
@ -23,7 +22,7 @@ import {
|
|||||||
} from '../../../global/selectors';
|
} from '../../../global/selectors';
|
||||||
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import { isUserId } from '../../../global/helpers';
|
import { isUserId, stripCustomEmoji } from '../../../global/helpers';
|
||||||
|
|
||||||
import useAsyncRendering from '../../right/hooks/useAsyncRendering';
|
import useAsyncRendering from '../../right/hooks/useAsyncRendering';
|
||||||
import useShowTransition from '../../../hooks/useShowTransition';
|
import useShowTransition from '../../../hooks/useShowTransition';
|
||||||
@ -164,18 +163,14 @@ const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const strippedMessage = useMemo(() => {
|
const strippedMessage = useMemo(() => {
|
||||||
const textEntities = message?.content.text?.entities;
|
if (!message || !isForwarding || !message.content.text || !noAuthors || isCurrentUserPremium) return message;
|
||||||
if (!message || !isForwarding || !textEntities?.length || !noAuthors || isCurrentUserPremium) return message;
|
|
||||||
|
|
||||||
const filteredEntities = textEntities.filter((entity) => entity.type !== ApiMessageEntityTypes.CustomEmoji);
|
const strippedText = stripCustomEmoji(message.content.text);
|
||||||
return {
|
return {
|
||||||
...message,
|
...message,
|
||||||
content: {
|
content: {
|
||||||
...message.content,
|
...message.content,
|
||||||
text: {
|
text: strippedText,
|
||||||
text: message.content.text!.text,
|
|
||||||
entities: filteredEntities,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}, [isCurrentUserPremium, isForwarding, message, noAuthors]);
|
}, [isCurrentUserPremium, isForwarding, message, noAuthors]);
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import type { ApiMessageEntityCustomEmoji, ApiSticker } from '../../../../api/types';
|
import type { ApiMessageEntityCustomEmoji, ApiSticker } from '../../../../api/types';
|
||||||
|
import { ApiMessageEntityTypes } from '../../../../api/types';
|
||||||
|
|
||||||
import { EMOJI_SIZES } from '../../../../config';
|
import { EMOJI_SIZES } from '../../../../config';
|
||||||
import { REM } from '../../../common/helpers/mediaDimensions';
|
import { REM } from '../../../common/helpers/mediaDimensions';
|
||||||
@ -16,6 +17,7 @@ export function buildCustomEmojiHtml(emoji: ApiSticker) {
|
|||||||
draggable="false"
|
draggable="false"
|
||||||
alt="${emoji.emoji}"
|
alt="${emoji.emoji}"
|
||||||
data-document-id="${emoji.id}"
|
data-document-id="${emoji.id}"
|
||||||
|
data-entity-type="${ApiMessageEntityTypes.CustomEmoji}"
|
||||||
src="${mediaData || placeholderSrc}"
|
src="${mediaData || placeholderSrc}"
|
||||||
/>`;
|
/>`;
|
||||||
}
|
}
|
||||||
@ -28,6 +30,7 @@ export function buildCustomEmojiHtmlFromEntity(rawText: string, entity: ApiMessa
|
|||||||
draggable="false"
|
draggable="false"
|
||||||
alt="${rawText}"
|
alt="${rawText}"
|
||||||
data-document-id="${entity.documentId}"
|
data-document-id="${entity.documentId}"
|
||||||
|
data-entity-type="${ApiMessageEntityTypes.CustomEmoji}"
|
||||||
src="${mediaData || placeholderSrc}"
|
src="${mediaData || placeholderSrc}"
|
||||||
/>`;
|
/>`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,67 @@
|
|||||||
import type { StateHookSetter } from '../../../../lib/teact/teact';
|
import type { StateHookSetter } from '../../../../lib/teact/teact';
|
||||||
import { useEffect } from '../../../../lib/teact/teact';
|
import { useEffect } from '../../../../lib/teact/teact';
|
||||||
import type { ApiAttachment, ApiMessage } from '../../../../api/types';
|
|
||||||
|
import type { ApiAttachment, ApiFormattedText, ApiMessage } from '../../../../api/types';
|
||||||
|
import { ApiMessageEntityTypes } from '../../../../api/types';
|
||||||
|
|
||||||
import buildAttachment from '../helpers/buildAttachment';
|
import buildAttachment from '../helpers/buildAttachment';
|
||||||
import { EDITABLE_INPUT_ID, EDITABLE_INPUT_MODAL_ID } from '../../../../config';
|
import { EDITABLE_INPUT_ID, EDITABLE_INPUT_MODAL_ID } from '../../../../config';
|
||||||
import getFilesFromDataTransferItems from '../helpers/getFilesFromDataTransferItems';
|
import getFilesFromDataTransferItems from '../helpers/getFilesFromDataTransferItems';
|
||||||
|
import parseMessageInput, { ENTITY_CLASS_BY_NODE_NAME } from '../../../../util/parseMessageInput';
|
||||||
|
import { containsCustomEmoji, stripCustomEmoji } from '../../../../global/helpers/symbols';
|
||||||
|
|
||||||
const CLIPBOARD_ACCEPTED_TYPES = ['image/png', 'image/jpeg', 'image/gif'];
|
const CLIPBOARD_ACCEPTED_TYPES = ['image/png', 'image/jpeg', 'image/gif'];
|
||||||
const MAX_MESSAGE_LENGTH = 4096;
|
const MAX_MESSAGE_LENGTH = 4096;
|
||||||
|
|
||||||
|
const STYLE_TAG_REGEX = /<style>(.*?)<\/style>/gs;
|
||||||
|
|
||||||
|
function preparePastedHtml(html: string) {
|
||||||
|
let fragment = document.createElement('div');
|
||||||
|
fragment.innerHTML = html.replace(/\u00a0/g, ' ').replace(STYLE_TAG_REGEX, ''); // Strip   and styles
|
||||||
|
|
||||||
|
const textContents = fragment.querySelectorAll<HTMLDivElement>('.text-content');
|
||||||
|
if (textContents.length) {
|
||||||
|
fragment = textContents[textContents.length - 1]; // Replace with the last copied message
|
||||||
|
}
|
||||||
|
|
||||||
|
Array.from(fragment.getElementsByTagName('*')).forEach((node) => {
|
||||||
|
if (!(node instanceof HTMLElement)) return;
|
||||||
|
node.removeAttribute('style');
|
||||||
|
|
||||||
|
// Fix newlines
|
||||||
|
if (node.tagName === 'BR') node.replaceWith('\n');
|
||||||
|
if (node.tagName === 'P') node.appendChild(document.createTextNode('\n'));
|
||||||
|
if (node.tagName === 'IMG' && !node.dataset.entityType) node.replaceWith(node.getAttribute('alt') || '');
|
||||||
|
// We do not intercept copy logic, so we remove some nodes here
|
||||||
|
if (node.dataset.ignoreOnPaste) node.remove();
|
||||||
|
|
||||||
|
if (ENTITY_CLASS_BY_NODE_NAME[node.tagName]) {
|
||||||
|
node.setAttribute('data-entity-type', ENTITY_CLASS_BY_NODE_NAME[node.tagName]);
|
||||||
|
}
|
||||||
|
// Strip non-entity tags
|
||||||
|
if (!node.dataset.entityType && node.textContent === node.innerText) node.replaceWith(node.textContent);
|
||||||
|
// Append entity parameters for parsing
|
||||||
|
if (node.dataset.alt) node.setAttribute('alt', node.dataset.alt);
|
||||||
|
switch (node.dataset.entityType) {
|
||||||
|
case ApiMessageEntityTypes.MentionName:
|
||||||
|
node.replaceWith(node.textContent || '');
|
||||||
|
break;
|
||||||
|
case ApiMessageEntityTypes.CustomEmoji:
|
||||||
|
node.textContent = node.dataset.alt || '';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return fragment.innerHTML.trimEnd();
|
||||||
|
}
|
||||||
|
|
||||||
const useClipboardPaste = (
|
const useClipboardPaste = (
|
||||||
isActive: boolean,
|
isActive: boolean,
|
||||||
insertTextAndUpdateCursor: (text: string, inputId?: string) => void,
|
insertTextAndUpdateCursor: (text: ApiFormattedText, inputId?: string) => void,
|
||||||
setAttachments: StateHookSetter<ApiAttachment[]>,
|
setAttachments: StateHookSetter<ApiAttachment[]>,
|
||||||
editedMessage: ApiMessage | undefined,
|
editedMessage: ApiMessage | undefined,
|
||||||
|
shouldStripCustomEmoji?: boolean,
|
||||||
|
onCustomEmojiStripped?: VoidFunction,
|
||||||
) => {
|
) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isActive) {
|
if (!isActive) {
|
||||||
@ -31,6 +79,16 @@ const useClipboardPaste = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pastedText = e.clipboardData.getData('text').substring(0, MAX_MESSAGE_LENGTH);
|
const pastedText = e.clipboardData.getData('text').substring(0, MAX_MESSAGE_LENGTH);
|
||||||
|
const html = e.clipboardData.getData('text/html');
|
||||||
|
let pastedFormattedText = html ? parseMessageInput(
|
||||||
|
preparePastedHtml(html), undefined, true,
|
||||||
|
) : undefined;
|
||||||
|
|
||||||
|
if (pastedFormattedText && containsCustomEmoji(pastedFormattedText) && shouldStripCustomEmoji) {
|
||||||
|
pastedFormattedText = stripCustomEmoji(pastedFormattedText);
|
||||||
|
onCustomEmojiStripped?.();
|
||||||
|
}
|
||||||
|
|
||||||
const { items } = e.clipboardData;
|
const { items } = e.clipboardData;
|
||||||
let files: File[] = [];
|
let files: File[] = [];
|
||||||
|
|
||||||
@ -50,8 +108,10 @@ const useClipboardPaste = (
|
|||||||
setAttachments((attachments) => attachments.concat(newAttachments));
|
setAttachments((attachments) => attachments.concat(newAttachments));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pastedText) {
|
const textToPaste = pastedFormattedText?.entities?.length ? pastedFormattedText : { text: pastedText };
|
||||||
insertTextAndUpdateCursor(pastedText, input?.id);
|
|
||||||
|
if (textToPaste) {
|
||||||
|
insertTextAndUpdateCursor(textToPaste, input?.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,7 +120,9 @@ const useClipboardPaste = (
|
|||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('paste', handlePaste, false);
|
document.removeEventListener('paste', handlePaste, false);
|
||||||
};
|
};
|
||||||
}, [insertTextAndUpdateCursor, editedMessage, setAttachments, isActive]);
|
}, [
|
||||||
|
insertTextAndUpdateCursor, editedMessage, setAttachments, isActive, shouldStripCustomEmoji, onCustomEmojiStripped,
|
||||||
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useClipboardPaste;
|
export default useClipboardPaste;
|
||||||
|
|||||||
@ -38,7 +38,7 @@ const BaseResult: FC<OwnProps> = ({
|
|||||||
|
|
||||||
if (thumbUrl) {
|
if (thumbUrl) {
|
||||||
content = (
|
content = (
|
||||||
<img src={thumbUrl} className={transitionClassNames} alt="" decoding="async" draggable="false" />
|
<img src={thumbUrl} className={transitionClassNames} alt="" decoding="async" draggable={false} />
|
||||||
);
|
);
|
||||||
} else if (title) {
|
} else if (title) {
|
||||||
content = getFirstLetters(title, 1);
|
content = getFirstLetters(title, 1);
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import React from '../../../lib/teact/teact';
|
|||||||
import { getActions, withGlobal } from '../../../global';
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
import type { ApiChat, ApiUser } from '../../../api/types';
|
import type { ApiChat, ApiUser } from '../../../api/types';
|
||||||
|
import { ApiMessageEntityTypes } from '../../../api/types';
|
||||||
|
|
||||||
import { selectUser } from '../../../global/selectors';
|
import { selectUser } from '../../../global/selectors';
|
||||||
|
|
||||||
@ -17,6 +18,7 @@ type StateProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const MentionLink: FC<OwnProps & StateProps> = ({
|
const MentionLink: FC<OwnProps & StateProps> = ({
|
||||||
|
userId,
|
||||||
username,
|
username,
|
||||||
userOrChat,
|
userOrChat,
|
||||||
children,
|
children,
|
||||||
@ -35,7 +37,12 @@ const MentionLink: FC<OwnProps & StateProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a onClick={handleClick} className="text-entity-link" dir="auto">
|
<a
|
||||||
|
onClick={handleClick}
|
||||||
|
className="text-entity-link"
|
||||||
|
dir="auto"
|
||||||
|
data-entity-type={userId ? ApiMessageEntityTypes.MentionName : ApiMessageEntityTypes.Mention}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -856,10 +856,10 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!hasAnimatedEmoji && textParts && (
|
{!hasAnimatedEmoji && textParts && (
|
||||||
<p className={textContentClass} dir="auto">
|
<div className={textContentClass} dir="auto">
|
||||||
{textParts}
|
{textParts}
|
||||||
{metaPosition === 'in-text' && renderReactionsAndMeta()}
|
{metaPosition === 'in-text' && renderReactionsAndMeta()}
|
||||||
</p>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{webPage && (
|
{webPage && (
|
||||||
|
|||||||
@ -63,6 +63,7 @@ const MessageMeta: FC<OwnProps> = ({
|
|||||||
className={buildClassName('MessageMeta', withReactionOffset && 'reactions-offset')}
|
className={buildClassName('MessageMeta', withReactionOffset && 'reactions-offset')}
|
||||||
dir={lang.isRtl ? 'rtl' : 'ltr'}
|
dir={lang.isRtl ? 'rtl' : 'ltr'}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
data-ignore-on-paste
|
||||||
>
|
>
|
||||||
{reactions && reactions.map((l) => (
|
{reactions && reactions.map((l) => (
|
||||||
<ReactionAnimatedEmoji
|
<ReactionAnimatedEmoji
|
||||||
|
|||||||
@ -121,7 +121,7 @@ const SponsoredMessage: FC<OwnProps & StateProps> = ({
|
|||||||
{channel && renderText(message.chatInviteTitle || getChatTitle(lang, channel, bot) || '')}
|
{channel && renderText(message.chatInviteTitle || getChatTitle(lang, channel, bot) || '')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-content with-meta" dir="auto" ref={contentRef}>
|
<div className="text-content with-meta" dir="auto" ref={contentRef}>
|
||||||
<span className="text-content-inner" dir="auto">
|
<span className="text-content-inner" dir="auto">
|
||||||
{renderTextWithEntities(message.text.text, message.text.entities)}
|
{renderTextWithEntities(message.text.text, message.text.entities)}
|
||||||
</span>
|
</span>
|
||||||
@ -131,7 +131,7 @@ const SponsoredMessage: FC<OwnProps & StateProps> = ({
|
|||||||
{message.isRecommended ? lang('Message.RecommendedLabel') : lang('SponsoredMessage')}
|
{message.isRecommended ? lang('Message.RecommendedLabel') : lang('SponsoredMessage')}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</div>
|
||||||
|
|
||||||
<Button color="secondary" size="tiny" ripple onClick={handleClick} className="SponsoredMessage__button">
|
<Button color="secondary" size="tiny" ripple onClick={handleClick} className="SponsoredMessage__button">
|
||||||
{lang(message.isBot
|
{lang(message.isBot
|
||||||
|
|||||||
@ -12,8 +12,13 @@ import {
|
|||||||
getMessageWebPageVideo,
|
getMessageWebPageVideo,
|
||||||
hasMessageLocalBlobUrl,
|
hasMessageLocalBlobUrl,
|
||||||
} from '../../../../global/helpers';
|
} from '../../../../global/helpers';
|
||||||
import { CLIPBOARD_ITEM_SUPPORTED, copyImageToClipboard, copyTextToClipboard } from '../../../../util/clipboard';
|
import {
|
||||||
|
CLIPBOARD_ITEM_SUPPORTED,
|
||||||
|
copyHtmlToClipboard,
|
||||||
|
copyImageToClipboard,
|
||||||
|
} from '../../../../util/clipboard';
|
||||||
import getMessageIdsForSelectedText from '../../../../util/getMessageIdsForSelectedText';
|
import getMessageIdsForSelectedText from '../../../../util/getMessageIdsForSelectedText';
|
||||||
|
import { renderMessageText } from '../../../common/helpers/renderMessageText';
|
||||||
|
|
||||||
type ICopyOptions = {
|
type ICopyOptions = {
|
||||||
label: string;
|
label: string;
|
||||||
@ -65,9 +70,13 @@ export function getMessageCopyOptions(
|
|||||||
const messageIds = getMessageIdsForSelectedText();
|
const messageIds = getMessageIdsForSelectedText();
|
||||||
if (messageIds?.length && onCopyMessages) {
|
if (messageIds?.length && onCopyMessages) {
|
||||||
onCopyMessages(messageIds);
|
onCopyMessages(messageIds);
|
||||||
|
} else if (hasSelection) {
|
||||||
|
document.execCommand('copy');
|
||||||
} else {
|
} else {
|
||||||
const clipboardText = hasSelection && selection ? selection.toString() : getMessageTextWithSpoilers(message)!;
|
const clipboardText = renderMessageText(
|
||||||
copyTextToClipboard(clipboardText);
|
message, undefined, undefined, undefined, undefined, undefined, undefined, true,
|
||||||
|
);
|
||||||
|
if (clipboardText) copyHtmlToClipboard(clipboardText.join(''), getMessageTextWithSpoilers(message)!);
|
||||||
}
|
}
|
||||||
|
|
||||||
afterEffect?.();
|
afterEffect?.();
|
||||||
|
|||||||
@ -44,8 +44,9 @@ import versionNotification from '../../../versionNotification.txt';
|
|||||||
import parseMessageInput from '../../../util/parseMessageInput';
|
import parseMessageInput from '../../../util/parseMessageInput';
|
||||||
import { getMessageSummaryText, getSenderTitle } from '../../helpers';
|
import { getMessageSummaryText, getSenderTitle } from '../../helpers';
|
||||||
import * as langProvider from '../../../util/langProvider';
|
import * as langProvider from '../../../util/langProvider';
|
||||||
import { copyTextToClipboard } from '../../../util/clipboard';
|
import { copyHtmlToClipboard } from '../../../util/clipboard';
|
||||||
import type { GlobalState } from '../../types';
|
import type { GlobalState } from '../../types';
|
||||||
|
import { renderMessageSummaryHtml } from '../../helpers/renderMessageSummaryHtml';
|
||||||
|
|
||||||
const FOCUS_DURATION = 1500;
|
const FOCUS_DURATION = 1500;
|
||||||
const FOCUS_NO_HIGHLIGHT_DURATION = FAST_SMOOTH_MAX_DURATION + ANIMATION_END_DELAY;
|
const FOCUS_NO_HIGHLIGHT_DURATION = FAST_SMOOTH_MAX_DURATION + ANIMATION_END_DELAY;
|
||||||
@ -750,11 +751,21 @@ function copyTextForMessages(global: GlobalState, chatId: string, messageIds: nu
|
|||||||
|
|
||||||
const result = messages.reduce((acc, message) => {
|
const result = messages.reduce((acc, message) => {
|
||||||
const sender = selectSender(global, message);
|
const sender = selectSender(global, message);
|
||||||
|
|
||||||
|
acc.push(`> ${sender ? getSenderTitle(lang, sender) : ''}:`);
|
||||||
|
acc.push(`${renderMessageSummaryHtml(lang, message)}\n`);
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, [] as string[]);
|
||||||
|
|
||||||
|
const resultText = messages.reduce((acc, message) => {
|
||||||
|
const sender = selectSender(global, message);
|
||||||
|
|
||||||
acc.push(`> ${sender ? getSenderTitle(lang, sender) : ''}:`);
|
acc.push(`> ${sender ? getSenderTitle(lang, sender) : ''}:`);
|
||||||
acc.push(`${getMessageSummaryText(lang, message, false, 0, undefined, true)}\n`);
|
acc.push(`${getMessageSummaryText(lang, message, false, 0, undefined, true)}\n`);
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, [] as string[]);
|
}, [] as string[]);
|
||||||
|
|
||||||
copyTextToClipboard(result.join('\n'));
|
copyHtmlToClipboard(result.join('\n'), resultText.join('\n'));
|
||||||
}
|
}
|
||||||
|
|||||||
18
src/global/helpers/renderMessageSummaryHtml.ts
Normal file
18
src/global/helpers/renderMessageSummaryHtml.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import type { LangFn } from '../../hooks/useLang';
|
||||||
|
import type { ApiMessage } from '../../api/types';
|
||||||
|
import { renderMessageText } from '../../components/common/helpers/renderMessageText';
|
||||||
|
import { getMessageSummaryDescription, getMessageSummaryEmoji } from './messageSummary';
|
||||||
|
|
||||||
|
export function renderMessageSummaryHtml(
|
||||||
|
lang: LangFn,
|
||||||
|
message: ApiMessage,
|
||||||
|
) {
|
||||||
|
const emoji = getMessageSummaryEmoji(message);
|
||||||
|
const emojiWithSpace = emoji ? `${emoji} ` : '';
|
||||||
|
const text = renderMessageText(
|
||||||
|
message, undefined, undefined, undefined, undefined, undefined, undefined, true,
|
||||||
|
)?.join('');
|
||||||
|
const description = getMessageSummaryDescription(lang, message, text, true, true);
|
||||||
|
|
||||||
|
return `${emojiWithSpace}${description}`;
|
||||||
|
}
|
||||||
@ -1,3 +1,17 @@
|
|||||||
|
import type { ApiFormattedText } from '../../api/types';
|
||||||
|
import { ApiMessageEntityTypes } from '../../api/types';
|
||||||
|
|
||||||
export function getStickerPreviewHash(stickerId: string) {
|
export function getStickerPreviewHash(stickerId: string) {
|
||||||
return `sticker${stickerId}?size=m`;
|
return `sticker${stickerId}?size=m`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function containsCustomEmoji(formattedText: ApiFormattedText) {
|
||||||
|
return formattedText.entities?.some((e) => e.type === ApiMessageEntityTypes.CustomEmoji);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stripCustomEmoji(text: ApiFormattedText): ApiFormattedText {
|
||||||
|
if (!text.entities) return text;
|
||||||
|
|
||||||
|
const entities = text.entities.filter((entity) => entity.type !== ApiMessageEntityTypes.CustomEmoji);
|
||||||
|
return { ...text, entities };
|
||||||
|
}
|
||||||
|
|||||||
@ -27,6 +27,20 @@ export const copyTextToClipboard = (str: string): void => {
|
|||||||
document.body.removeChild(textCopyEl);
|
document.body.removeChild(textCopyEl);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const copyHtmlToClipboard = (html: string, text: string): void => {
|
||||||
|
if (!window.navigator.clipboard?.write) {
|
||||||
|
copyTextToClipboard(text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.navigator.clipboard.write([
|
||||||
|
new ClipboardItem({
|
||||||
|
'text/plain': new Blob([text], { type: 'text/plain' }),
|
||||||
|
'text/html': new Blob([html], { type: 'text/html' }),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
export const copyImageToClipboard = (imageUrl?: string) => {
|
export const copyImageToClipboard = (imageUrl?: string) => {
|
||||||
if (!imageUrl) return;
|
if (!imageUrl) return;
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import type { ApiMessageEntity, ApiFormattedText } from '../api/types';
|
|||||||
import { ApiMessageEntityTypes } from '../api/types';
|
import { ApiMessageEntityTypes } from '../api/types';
|
||||||
import { RE_LINK_TEMPLATE } from '../config';
|
import { RE_LINK_TEMPLATE } from '../config';
|
||||||
|
|
||||||
const ENTITY_CLASS_BY_NODE_NAME: Record<string, ApiMessageEntityTypes> = {
|
export const ENTITY_CLASS_BY_NODE_NAME: Record<string, ApiMessageEntityTypes> = {
|
||||||
B: ApiMessageEntityTypes.Bold,
|
B: ApiMessageEntityTypes.Bold,
|
||||||
STRONG: ApiMessageEntityTypes.Bold,
|
STRONG: ApiMessageEntityTypes.Bold,
|
||||||
I: ApiMessageEntityTypes.Italic,
|
I: ApiMessageEntityTypes.Italic,
|
||||||
@ -18,16 +18,21 @@ const ENTITY_CLASS_BY_NODE_NAME: Record<string, ApiMessageEntityTypes> = {
|
|||||||
|
|
||||||
const MAX_TAG_DEEPNESS = 3;
|
const MAX_TAG_DEEPNESS = 3;
|
||||||
|
|
||||||
export default function parseMessageInput(html: string, withMarkdownLinks = false): ApiFormattedText {
|
export default function parseMessageInput(
|
||||||
|
html: string, withMarkdownLinks = false, skipMarkdown = false,
|
||||||
|
): ApiFormattedText {
|
||||||
const fragment = document.createElement('div');
|
const fragment = document.createElement('div');
|
||||||
fragment.innerHTML = withMarkdownLinks ? parseMarkdown(parseMarkdownLinks(html)) : parseMarkdown(html);
|
fragment.innerHTML = skipMarkdown ? html
|
||||||
|
: withMarkdownLinks ? parseMarkdown(parseMarkdownLinks(html)) : parseMarkdown(html);
|
||||||
fixImageContent(fragment);
|
fixImageContent(fragment);
|
||||||
const text = fragment.innerText.trim().replace(/\u200b+/g, '');
|
const text = fragment.innerText.trim().replace(/\u200b+/g, '');
|
||||||
let textIndex = 0;
|
const trimShift = fragment.innerText.indexOf(text[0]);
|
||||||
|
let textIndex = -trimShift;
|
||||||
let recursionDeepness = 0;
|
let recursionDeepness = 0;
|
||||||
const entities: ApiMessageEntity[] = [];
|
const entities: ApiMessageEntity[] = [];
|
||||||
|
|
||||||
function addEntity(node: ChildNode) {
|
function addEntity(node: ChildNode) {
|
||||||
|
if (node.nodeType === Node.COMMENT_NODE) return;
|
||||||
const { index, entity } = getEntityDataFromNode(node, text, textIndex);
|
const { index, entity } = getEntityDataFromNode(node, text, textIndex);
|
||||||
|
|
||||||
if (entity) {
|
if (entity) {
|
||||||
@ -208,6 +213,10 @@ function getEntityDataFromNode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getEntityTypeFromNode(node: ChildNode): ApiMessageEntityTypes | undefined {
|
function getEntityTypeFromNode(node: ChildNode): ApiMessageEntityTypes | undefined {
|
||||||
|
if (node instanceof HTMLElement && node.dataset.entityType) {
|
||||||
|
return node.dataset.entityType as ApiMessageEntityTypes;
|
||||||
|
}
|
||||||
|
|
||||||
if (ENTITY_CLASS_BY_NODE_NAME[node.nodeName]) {
|
if (ENTITY_CLASS_BY_NODE_NAME[node.nodeName]) {
|
||||||
return ENTITY_CLASS_BY_NODE_NAME[node.nodeName];
|
return ENTITY_CLASS_BY_NODE_NAME[node.nodeName];
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user