Message: Allow quote message (#4067)
This commit is contained in:
parent
df84448372
commit
89df119add
@ -886,6 +886,7 @@ function buildReplyInfo(inputInfo: ApiInputReplyInfo, isForum?: boolean): ApiRep
|
|||||||
replyToPeerId: inputInfo.replyToPeerId,
|
replyToPeerId: inputInfo.replyToPeerId,
|
||||||
quoteText: inputInfo.quoteText,
|
quoteText: inputInfo.quoteText,
|
||||||
isForumTopic: isForum && inputInfo.replyToTopId ? true : undefined,
|
isForumTopic: isForum && inputInfo.replyToTopId ? true : undefined,
|
||||||
|
...(Boolean(inputInfo.quoteText) && { isQuote: true }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1
src/assets/font-icons/quote-text.svg
Normal file
1
src/assets/font-icons/quote-text.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none"><path fill="#000" fill-rule="evenodd" d="M20 2.667a2.667 2.667 0 0 0-.306 5.316c-.141.498-.329.984-.562 1.45l-.32.64a1.328 1.328 0 0 0 2.376 1.188l.32-.64a10.928 10.928 0 0 0 1.153-4.887v-.23A2.667 2.667 0 0 0 20 2.666Zm6.667 0a2.667 2.667 0 0 0-.307 5.316c-.14.498-.328.984-.561 1.45l-.32.64a1.328 1.328 0 0 0 2.376 1.188l.32-.64a10.928 10.928 0 0 0 1.153-4.887v-.23a2.667 2.667 0 0 0-2.661-2.837ZM5.333 6.672a1.328 1.328 0 1 0 0 2.656H14a1.328 1.328 0 1 0 0-2.656H5.333Zm0 8a1.328 1.328 0 1 0 0 2.656h21.334a1.328 1.328 0 0 0 0-2.656H5.333ZM4.005 24c0-.733.595-1.328 1.328-1.328h21.334a1.328 1.328 0 1 1 0 2.656H5.333A1.328 1.328 0 0 1 4.005 24Z" clip-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 752 B |
@ -85,7 +85,7 @@ import { formatMediaDuration, formatVoiceRecordDuration } from '../../util/dateF
|
|||||||
import deleteLastCharacterOutsideSelection from '../../util/deleteLastCharacterOutsideSelection';
|
import deleteLastCharacterOutsideSelection from '../../util/deleteLastCharacterOutsideSelection';
|
||||||
import focusEditableElement from '../../util/focusEditableElement';
|
import focusEditableElement from '../../util/focusEditableElement';
|
||||||
import { MEMO_EMPTY_ARRAY } from '../../util/memo';
|
import { MEMO_EMPTY_ARRAY } from '../../util/memo';
|
||||||
import parseMessageInput from '../../util/parseMessageInput';
|
import parseHtmlAsFormattedText from '../../util/parseHtmlAsFormattedText';
|
||||||
import { insertHtmlInSelection } from '../../util/selection';
|
import { insertHtmlInSelection } from '../../util/selection';
|
||||||
import { getServerTime } from '../../util/serverTime';
|
import { getServerTime } from '../../util/serverTime';
|
||||||
import { IS_IOS, IS_VOICE_RECORDING_SUPPORTED } from '../../util/windowEnvironment';
|
import { IS_IOS, IS_VOICE_RECORDING_SUPPORTED } from '../../util/windowEnvironment';
|
||||||
@ -869,7 +869,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { text, entities } = parseMessageInput(getHtml());
|
const { text, entities } = parseHtmlAsFormattedText(getHtml());
|
||||||
if (!text && !attachmentsToSend.length) {
|
if (!text && !attachmentsToSend.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -931,7 +931,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { text, entities } = parseMessageInput(getHtml());
|
const { text, entities } = parseHtmlAsFormattedText(getHtml());
|
||||||
|
|
||||||
if (currentAttachments.length) {
|
if (currentAttachments.length) {
|
||||||
sendAttachments({
|
sendAttachments({
|
||||||
@ -1398,7 +1398,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
showCustomEmojiPremiumNotification();
|
showCustomEmojiPremiumNotification();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const customEmojiMessage = parseMessageInput(buildCustomEmojiHtml(sticker));
|
const customEmojiMessage = parseHtmlAsFormattedText(buildCustomEmojiHtml(sticker));
|
||||||
text = customEmojiMessage.text;
|
text = customEmojiMessage.text;
|
||||||
entities = customEmojiMessage.entities;
|
entities = customEmojiMessage.entities;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -127,7 +127,14 @@ const CustomEmoji: FC<OwnProps> = ({
|
|||||||
data-alt={customEmoji?.emoji}
|
data-alt={customEmoji?.emoji}
|
||||||
style={style}
|
style={style}
|
||||||
>
|
>
|
||||||
<img className={styles.highlightCatch} src={blankImg} alt={customEmoji?.emoji} draggable={false} />
|
<img
|
||||||
|
className={styles.highlightCatch}
|
||||||
|
src={blankImg}
|
||||||
|
alt={customEmoji?.emoji}
|
||||||
|
data-entity-type={ApiMessageEntityTypes.CustomEmoji}
|
||||||
|
data-document-id={documentId}
|
||||||
|
draggable={false}
|
||||||
|
/>
|
||||||
{!customEmoji ? (
|
{!customEmoji ? (
|
||||||
<img className={styles.thumb} src={svgPlaceholder} alt="Emoji" draggable={false} />
|
<img className={styles.thumb} src={svgPlaceholder} alt="Emoji" draggable={false} />
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -51,6 +51,7 @@ type OwnProps = {
|
|||||||
customText?: string;
|
customText?: string;
|
||||||
noUserColors?: boolean;
|
noUserColors?: boolean;
|
||||||
isProtected?: boolean;
|
isProtected?: boolean;
|
||||||
|
isInComposer?: boolean;
|
||||||
chatTranslations?: ChatTranslatedMessages;
|
chatTranslations?: ChatTranslatedMessages;
|
||||||
requestedChatTranslationLanguage?: string;
|
requestedChatTranslationLanguage?: string;
|
||||||
observeIntersectionForLoading?: ObserveFn;
|
observeIntersectionForLoading?: ObserveFn;
|
||||||
@ -71,6 +72,7 @@ const EmbeddedMessage: FC<OwnProps> = ({
|
|||||||
title,
|
title,
|
||||||
customText,
|
customText,
|
||||||
isProtected,
|
isProtected,
|
||||||
|
isInComposer,
|
||||||
noUserColors,
|
noUserColors,
|
||||||
chatTranslations,
|
chatTranslations,
|
||||||
requestedChatTranslationLanguage,
|
requestedChatTranslationLanguage,
|
||||||
@ -118,6 +120,7 @@ const EmbeddedMessage: FC<OwnProps> = ({
|
|||||||
return renderTextWithEntities({
|
return renderTextWithEntities({
|
||||||
text: replyInfo.quoteText.text,
|
text: replyInfo.quoteText.text,
|
||||||
entities: replyInfo.quoteText.entities,
|
entities: replyInfo.quoteText.entities,
|
||||||
|
noLineBreaks: isInComposer,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,7 +176,11 @@ const EmbeddedMessage: FC<OwnProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!isChatSender && <span className="embedded-sender">{renderText(senderTitle)}</span>}
|
{!isChatSender && (
|
||||||
|
<span className="embedded-sender">
|
||||||
|
{renderText(isInComposer ? lang('ReplyToQuote', senderTitle) : senderTitle)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{icon && <Icon name={icon} className="embedded-chat-icon" />}
|
{icon && <Icon name={icon} className="embedded-chat-icon" />}
|
||||||
{icon && senderChatTitle && renderText(senderChatTitle)}
|
{icon && senderChatTitle && renderText(senderChatTitle)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -37,6 +37,7 @@ export function renderTextWithEntities({
|
|||||||
containerId,
|
containerId,
|
||||||
isSimple,
|
isSimple,
|
||||||
isProtected,
|
isProtected,
|
||||||
|
noLineBreaks,
|
||||||
observeIntersectionForLoading,
|
observeIntersectionForLoading,
|
||||||
observeIntersectionForPlaying,
|
observeIntersectionForPlaying,
|
||||||
withTranslucentThumbs,
|
withTranslucentThumbs,
|
||||||
@ -54,6 +55,7 @@ export function renderTextWithEntities({
|
|||||||
containerId?: string;
|
containerId?: string;
|
||||||
isSimple?: boolean;
|
isSimple?: boolean;
|
||||||
isProtected?: boolean;
|
isProtected?: boolean;
|
||||||
|
noLineBreaks?: boolean;
|
||||||
observeIntersectionForLoading?: ObserveFn;
|
observeIntersectionForLoading?: ObserveFn;
|
||||||
observeIntersectionForPlaying?: ObserveFn;
|
observeIntersectionForPlaying?: ObserveFn;
|
||||||
withTranslucentThumbs?: boolean;
|
withTranslucentThumbs?: boolean;
|
||||||
@ -64,7 +66,15 @@ export function renderTextWithEntities({
|
|||||||
focusedQuote?: string;
|
focusedQuote?: string;
|
||||||
}) {
|
}) {
|
||||||
if (!entities?.length) {
|
if (!entities?.length) {
|
||||||
return renderMessagePart(text, highlight, focusedQuote, emojiSize, shouldRenderAsHtml, isSimple);
|
return renderMessagePart({
|
||||||
|
content: text,
|
||||||
|
highlight,
|
||||||
|
focusedQuote,
|
||||||
|
emojiSize,
|
||||||
|
shouldRenderAsHtml,
|
||||||
|
isSimple,
|
||||||
|
noLineBreaks,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const result: TextPart[] = [];
|
const result: TextPart[] = [];
|
||||||
@ -92,9 +102,15 @@ export function renderTextWithEntities({
|
|||||||
deleteLineBreakAfterPre = false;
|
deleteLineBreakAfterPre = false;
|
||||||
}
|
}
|
||||||
if (textBefore) {
|
if (textBefore) {
|
||||||
renderResult.push(...renderMessagePart(
|
renderResult.push(...renderMessagePart({
|
||||||
textBefore, highlight, focusedQuote, emojiSize, shouldRenderAsHtml, isSimple,
|
content: textBefore,
|
||||||
) as TextPart[]);
|
highlight,
|
||||||
|
focusedQuote,
|
||||||
|
emojiSize,
|
||||||
|
shouldRenderAsHtml,
|
||||||
|
isSimple,
|
||||||
|
noLineBreaks,
|
||||||
|
}) as TextPart[]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,8 +157,10 @@ export function renderTextWithEntities({
|
|||||||
entityContent,
|
entityContent,
|
||||||
nestedEntityContent,
|
nestedEntityContent,
|
||||||
highlight,
|
highlight,
|
||||||
|
focusedQuote,
|
||||||
containerId,
|
containerId,
|
||||||
isSimple,
|
isSimple,
|
||||||
|
noLineBreaks,
|
||||||
isProtected,
|
isProtected,
|
||||||
observeIntersectionForLoading,
|
observeIntersectionForLoading,
|
||||||
observeIntersectionForPlaying,
|
observeIntersectionForPlaying,
|
||||||
@ -168,9 +186,15 @@ export function renderTextWithEntities({
|
|||||||
textAfter = textAfter.substring(1);
|
textAfter = textAfter.substring(1);
|
||||||
}
|
}
|
||||||
if (textAfter) {
|
if (textAfter) {
|
||||||
renderResult.push(...renderMessagePart(
|
renderResult.push(...renderMessagePart({
|
||||||
textAfter, highlight, focusedQuote, emojiSize, shouldRenderAsHtml, isSimple,
|
content: textAfter,
|
||||||
) as TextPart[]);
|
highlight,
|
||||||
|
focusedQuote,
|
||||||
|
emojiSize,
|
||||||
|
shouldRenderAsHtml,
|
||||||
|
isSimple,
|
||||||
|
noLineBreaks,
|
||||||
|
}) as TextPart[]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -217,19 +241,36 @@ export function getTextWithEntitiesAsHtml(formattedText?: ApiFormattedText) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderMessagePart(
|
function renderMessagePart({
|
||||||
content: TextPart | TextPart[],
|
content,
|
||||||
highlight?: string,
|
highlight,
|
||||||
focusedQuote?: string,
|
focusedQuote,
|
||||||
emojiSize?: number,
|
emojiSize,
|
||||||
shouldRenderAsHtml?: boolean,
|
shouldRenderAsHtml,
|
||||||
isSimple?: boolean,
|
isSimple,
|
||||||
) {
|
noLineBreaks,
|
||||||
|
} : {
|
||||||
|
content: TextPart | TextPart[];
|
||||||
|
highlight?: string;
|
||||||
|
focusedQuote?: string;
|
||||||
|
emojiSize?: number;
|
||||||
|
shouldRenderAsHtml?: boolean;
|
||||||
|
isSimple?: boolean;
|
||||||
|
noLineBreaks?: boolean;
|
||||||
|
}) {
|
||||||
if (Array.isArray(content)) {
|
if (Array.isArray(content)) {
|
||||||
const result: TextPart[] = [];
|
const result: TextPart[] = [];
|
||||||
|
|
||||||
content.forEach((c) => {
|
content.forEach((c) => {
|
||||||
result.push(...renderMessagePart(c, highlight, focusedQuote, emojiSize, shouldRenderAsHtml, isSimple));
|
result.push(...renderMessagePart({
|
||||||
|
content: c,
|
||||||
|
highlight,
|
||||||
|
focusedQuote,
|
||||||
|
emojiSize,
|
||||||
|
shouldRenderAsHtml,
|
||||||
|
isSimple,
|
||||||
|
noLineBreaks,
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@ -243,7 +284,7 @@ function renderMessagePart(
|
|||||||
|
|
||||||
const filters: TextFilter[] = [emojiFilter];
|
const filters: TextFilter[] = [emojiFilter];
|
||||||
const params: RenderTextParams = {};
|
const params: RenderTextParams = {};
|
||||||
if (!isSimple) {
|
if (!isSimple && !noLineBreaks) {
|
||||||
filters.push('br');
|
filters.push('br');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -330,8 +371,10 @@ function processEntity({
|
|||||||
entityContent,
|
entityContent,
|
||||||
nestedEntityContent,
|
nestedEntityContent,
|
||||||
highlight,
|
highlight,
|
||||||
|
focusedQuote,
|
||||||
containerId,
|
containerId,
|
||||||
isSimple,
|
isSimple,
|
||||||
|
noLineBreaks,
|
||||||
isProtected,
|
isProtected,
|
||||||
observeIntersectionForLoading,
|
observeIntersectionForLoading,
|
||||||
observeIntersectionForPlaying,
|
observeIntersectionForPlaying,
|
||||||
@ -346,8 +389,10 @@ function processEntity({
|
|||||||
entityContent: TextPart;
|
entityContent: TextPart;
|
||||||
nestedEntityContent: TextPart[];
|
nestedEntityContent: TextPart[];
|
||||||
highlight?: string;
|
highlight?: string;
|
||||||
|
focusedQuote?: string;
|
||||||
containerId?: string;
|
containerId?: string;
|
||||||
isSimple?: boolean;
|
isSimple?: boolean;
|
||||||
|
noLineBreaks?: boolean;
|
||||||
isProtected?: boolean;
|
isProtected?: boolean;
|
||||||
observeIntersectionForLoading?: ObserveFn;
|
observeIntersectionForLoading?: ObserveFn;
|
||||||
observeIntersectionForPlaying?: ObserveFn;
|
observeIntersectionForPlaying?: ObserveFn;
|
||||||
@ -362,9 +407,14 @@ function processEntity({
|
|||||||
const renderedContent = nestedEntityContent.length ? nestedEntityContent : entityContent;
|
const renderedContent = nestedEntityContent.length ? nestedEntityContent : entityContent;
|
||||||
|
|
||||||
function renderNestedMessagePart() {
|
function renderNestedMessagePart() {
|
||||||
return renderMessagePart(
|
return renderMessagePart({
|
||||||
renderedContent, highlight, undefined, emojiSize, undefined, isSimple,
|
content: renderedContent,
|
||||||
);
|
highlight,
|
||||||
|
focusedQuote,
|
||||||
|
emojiSize,
|
||||||
|
isSimple,
|
||||||
|
noLineBreaks,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!entityText) {
|
if (!entityText) {
|
||||||
@ -402,11 +452,11 @@ function processEntity({
|
|||||||
return <strong data-entity-type={entity.type}>{renderNestedMessagePart()}</strong>;
|
return <strong data-entity-type={entity.type}>{renderNestedMessagePart()}</strong>;
|
||||||
case ApiMessageEntityTypes.Blockquote:
|
case ApiMessageEntityTypes.Blockquote:
|
||||||
return (
|
return (
|
||||||
<div className="text-entity-blockquote-wrapper">
|
<span className="text-entity-blockquote-wrapper">
|
||||||
<blockquote data-entity-type={entity.type}>
|
<blockquote data-entity-type={entity.type}>
|
||||||
{renderNestedMessagePart()}
|
{renderNestedMessagePart()}
|
||||||
</blockquote>
|
</blockquote>
|
||||||
</div>
|
</span>
|
||||||
);
|
);
|
||||||
case ApiMessageEntityTypes.BotCommand:
|
case ApiMessageEntityTypes.BotCommand:
|
||||||
return (
|
return (
|
||||||
@ -585,10 +635,10 @@ function processEntityAsHtml(
|
|||||||
case ApiMessageEntityTypes.CustomEmoji:
|
case ApiMessageEntityTypes.CustomEmoji:
|
||||||
return buildCustomEmojiHtmlFromEntity(rawEntityText, entity);
|
return buildCustomEmojiHtmlFromEntity(rawEntityText, entity);
|
||||||
case ApiMessageEntityTypes.Blockquote:
|
case ApiMessageEntityTypes.Blockquote:
|
||||||
return `<span
|
return `<blockquote
|
||||||
class="blockquote"
|
class="blockquote"
|
||||||
data-entity-type="${ApiMessageEntityTypes.Blockquote}"
|
data-entity-type="${ApiMessageEntityTypes.Blockquote}"
|
||||||
>${renderedContent}</span>`;
|
>${renderedContent}</blockquote>`;
|
||||||
default:
|
default:
|
||||||
return renderedContent;
|
return renderedContent;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,6 +23,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
& .embedded-left-icon {
|
& .embedded-left-icon {
|
||||||
|
position: relative;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
background: none !important;
|
background: none !important;
|
||||||
height: 2.625rem;
|
height: 2.625rem;
|
||||||
@ -63,4 +64,15 @@
|
|||||||
width: 1.5rem;
|
width: 1.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.quote-reply {
|
||||||
|
position: absolute;
|
||||||
|
right: 0.75rem;
|
||||||
|
top: 0.5rem;
|
||||||
|
font-size: 0.5rem;
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
right: 0.625rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,6 +34,7 @@ import useShowTransition from '../../../hooks/useShowTransition';
|
|||||||
import useAsyncRendering from '../../right/hooks/useAsyncRendering';
|
import useAsyncRendering from '../../right/hooks/useAsyncRendering';
|
||||||
|
|
||||||
import EmbeddedMessage from '../../common/embedded/EmbeddedMessage';
|
import EmbeddedMessage from '../../common/embedded/EmbeddedMessage';
|
||||||
|
import Icon from '../../common/Icon';
|
||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
import Menu from '../../ui/Menu';
|
import Menu from '../../ui/Menu';
|
||||||
import MenuItem from '../../ui/MenuItem';
|
import MenuItem from '../../ui/MenuItem';
|
||||||
@ -173,13 +174,13 @@ const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
const leftIcon = useMemo(() => {
|
const leftIcon = useMemo(() => {
|
||||||
if (isShowingReply) {
|
if (isShowingReply) {
|
||||||
return 'icon-reply';
|
return 'reply';
|
||||||
}
|
}
|
||||||
if (editingId) {
|
if (editingId) {
|
||||||
return 'icon-edit';
|
return 'edit';
|
||||||
}
|
}
|
||||||
if (isForwarding) {
|
if (isForwarding) {
|
||||||
return 'icon-forward';
|
return 'forward';
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -210,11 +211,15 @@ const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({
|
|||||||
<div className={className} ref={ref} onContextMenu={handleContextMenu} onClick={handleContextMenu}>
|
<div className={className} ref={ref} onContextMenu={handleContextMenu} onClick={handleContextMenu}>
|
||||||
<div className={innerClassName}>
|
<div className={innerClassName}>
|
||||||
<div className="embedded-left-icon">
|
<div className="embedded-left-icon">
|
||||||
<i className={buildClassName('icon', leftIcon)} />
|
{leftIcon && <Icon name={leftIcon} />}
|
||||||
|
{Boolean(replyInfo?.quoteText) && (
|
||||||
|
<Icon name="quote" className="quote-reply" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<EmbeddedMessage
|
<EmbeddedMessage
|
||||||
className="inside-input"
|
className="inside-input"
|
||||||
replyInfo={replyInfo}
|
replyInfo={replyInfo}
|
||||||
|
isInComposer
|
||||||
message={strippedMessage}
|
message={strippedMessage}
|
||||||
sender={!noAuthors ? sender : undefined}
|
sender={!noAuthors ? sender : undefined}
|
||||||
customText={customText}
|
customText={customText}
|
||||||
@ -339,7 +344,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
sender = selectForwardedSender(global, message);
|
sender = selectForwardedSender(global, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sender && !forwardInfo?.hiddenUserName) {
|
if (!sender && (!forwardInfo?.hiddenUserName || Boolean(replyInfo.quoteText))) {
|
||||||
sender = selectSender(global, message);
|
sender = selectSender(global, message);
|
||||||
}
|
}
|
||||||
} else if (isForwarding) {
|
} else if (isForwarding) {
|
||||||
@ -352,8 +357,8 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
if (!sender) {
|
if (!sender) {
|
||||||
sender = selectPeer(global, fromChatId!);
|
sender = selectPeer(global, fromChatId!);
|
||||||
}
|
}
|
||||||
} else if (editingId) {
|
} else if (editingId && message) {
|
||||||
sender = selectSender(global, message!);
|
sender = selectSender(global, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
const forwardsHaveCaptions = forwardedMessages?.some((forward) => (
|
const forwardsHaveCaptions = forwardedMessages?.some((forward) => (
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import type { ApiNewPoll } from '../../../api/types';
|
|||||||
|
|
||||||
import { requestNextMutation } from '../../../lib/fasterdom/fasterdom';
|
import { requestNextMutation } from '../../../lib/fasterdom/fasterdom';
|
||||||
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
||||||
import parseMessageInput from '../../../util/parseMessageInput';
|
import parseHtmlAsFormattedText from '../../../util/parseHtmlAsFormattedText';
|
||||||
|
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
import useLastCallback from '../../../hooks/useLastCallback';
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
@ -142,7 +142,8 @@ const PollModal: FC<OwnProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (isQuizMode) {
|
if (isQuizMode) {
|
||||||
const { text, entities } = (solution && parseMessageInput(solution.substring(0, MAX_SOLUTION_LENGTH))) || {};
|
const { text, entities } = (solution && parseHtmlAsFormattedText(solution.substring(0, MAX_SOLUTION_LENGTH)))
|
||||||
|
|| {};
|
||||||
|
|
||||||
payload.quiz = {
|
payload.quiz = {
|
||||||
correctAnswers: [String(correctOption)],
|
correctAnswers: [String(correctOption)],
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import { ApiMessageEntityTypes } from '../../../api/types';
|
|||||||
import { RE_LINK_TEMPLATE } from '../../../config';
|
import { RE_LINK_TEMPLATE } from '../../../config';
|
||||||
import { selectNoWebPage, selectTabState, selectTheme } from '../../../global/selectors';
|
import { selectNoWebPage, selectTabState, selectTheme } from '../../../global/selectors';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import parseMessageInput from '../../../util/parseMessageInput';
|
import parseHtmlAsFormattedText from '../../../util/parseHtmlAsFormattedText';
|
||||||
|
|
||||||
import { useDebouncedResolver } from '../../../hooks/useAsyncResolvers';
|
import { useDebouncedResolver } from '../../../hooks/useAsyncResolvers';
|
||||||
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
|
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
|
||||||
@ -61,7 +61,7 @@ const WebPagePreview: FC<OwnProps & StateProps> = ({
|
|||||||
const formattedTextWithLinkRef = useRef<ApiFormattedText>();
|
const formattedTextWithLinkRef = useRef<ApiFormattedText>();
|
||||||
|
|
||||||
const detectLinkDebounced = useDebouncedResolver(() => {
|
const detectLinkDebounced = useDebouncedResolver(() => {
|
||||||
const formattedText = parseMessageInput(getHtml());
|
const formattedText = parseHtmlAsFormattedText(getHtml());
|
||||||
const linkEntity = formattedText.entities?.find((entity): entity is ApiMessageEntityTextUrl => (
|
const linkEntity = formattedText.entities?.find((entity): entity is ApiMessageEntityTextUrl => (
|
||||||
entity.type === ApiMessageEntityTypes.TextUrl
|
entity.type === ApiMessageEntityTypes.TextUrl
|
||||||
));
|
));
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { ApiMessageEntityTypes } from '../../../../api/types';
|
|||||||
|
|
||||||
import { DEBUG } from '../../../../config';
|
import { DEBUG } from '../../../../config';
|
||||||
import cleanDocsHtml from '../../../../lib/cleanDocsHtml';
|
import cleanDocsHtml from '../../../../lib/cleanDocsHtml';
|
||||||
import { ENTITY_CLASS_BY_NODE_NAME } from '../../../../util/parseMessageInput';
|
import { ENTITY_CLASS_BY_NODE_NAME } from '../../../../util/parseHtmlAsFormattedText';
|
||||||
|
|
||||||
const STYLE_TAG_REGEX = /<style>(.*?)<\/style>/gs;
|
const STYLE_TAG_REGEX = /<style>(.*?)<\/style>/gs;
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { fixImageContent } from '../../../../util/parseMessageInput';
|
import { fixImageContent } from '../../../../util/parseHtmlAsFormattedText';
|
||||||
|
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import {
|
|||||||
EDITABLE_INPUT_ID, EDITABLE_INPUT_MODAL_ID, EDITABLE_STORY_INPUT_ID,
|
EDITABLE_INPUT_ID, EDITABLE_INPUT_MODAL_ID, EDITABLE_STORY_INPUT_ID,
|
||||||
} from '../../../../config';
|
} from '../../../../config';
|
||||||
import { containsCustomEmoji, stripCustomEmoji } from '../../../../global/helpers/symbols';
|
import { containsCustomEmoji, stripCustomEmoji } from '../../../../global/helpers/symbols';
|
||||||
import parseMessageInput from '../../../../util/parseMessageInput';
|
import parseHtmlAsFormattedText from '../../../../util/parseHtmlAsFormattedText';
|
||||||
import buildAttachment from '../helpers/buildAttachment';
|
import buildAttachment from '../helpers/buildAttachment';
|
||||||
import { preparePastedHtml } from '../helpers/cleanHtml';
|
import { preparePastedHtml } from '../helpers/cleanHtml';
|
||||||
import getFilesFromDataTransferItems from '../helpers/getFilesFromDataTransferItems';
|
import getFilesFromDataTransferItems from '../helpers/getFilesFromDataTransferItems';
|
||||||
@ -45,7 +45,7 @@ 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');
|
const html = e.clipboardData.getData('text/html');
|
||||||
|
|
||||||
let pastedFormattedText = html ? parseMessageInput(
|
let pastedFormattedText = html ? parseHtmlAsFormattedText(
|
||||||
preparePastedHtml(html), undefined, true,
|
preparePastedHtml(html), undefined, true,
|
||||||
) : undefined;
|
) : undefined;
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import {
|
|||||||
requestMeasure, requestNextMutation,
|
requestMeasure, requestNextMutation,
|
||||||
} from '../../../../lib/fasterdom/fasterdom';
|
} from '../../../../lib/fasterdom/fasterdom';
|
||||||
import focusEditableElement from '../../../../util/focusEditableElement';
|
import focusEditableElement from '../../../../util/focusEditableElement';
|
||||||
import parseMessageInput from '../../../../util/parseMessageInput';
|
import parseHtmlAsFormattedText from '../../../../util/parseHtmlAsFormattedText';
|
||||||
import { IS_TOUCH_ENV } from '../../../../util/windowEnvironment';
|
import { IS_TOUCH_ENV } from '../../../../util/windowEnvironment';
|
||||||
import { getTextWithEntitiesAsHtml } from '../../../common/helpers/renderTextWithEntities';
|
import { getTextWithEntitiesAsHtml } from '../../../common/helpers/renderTextWithEntities';
|
||||||
|
|
||||||
@ -77,7 +77,7 @@ const useDraft = ({
|
|||||||
saveDraft({
|
saveDraft({
|
||||||
chatId: prevState.chatId ?? chatId,
|
chatId: prevState.chatId ?? chatId,
|
||||||
threadId: prevState.threadId ?? threadId,
|
threadId: prevState.threadId ?? threadId,
|
||||||
text: parseMessageInput(html),
|
text: parseHtmlAsFormattedText(html),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
clearDraft({
|
clearDraft({
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import { EDITABLE_INPUT_CSS_SELECTOR } from '../../../../config';
|
|||||||
import { requestMeasure, requestNextMutation } from '../../../../lib/fasterdom/fasterdom';
|
import { requestMeasure, requestNextMutation } from '../../../../lib/fasterdom/fasterdom';
|
||||||
import { hasMessageMedia } from '../../../../global/helpers';
|
import { hasMessageMedia } from '../../../../global/helpers';
|
||||||
import focusEditableElement from '../../../../util/focusEditableElement';
|
import focusEditableElement from '../../../../util/focusEditableElement';
|
||||||
import parseMessageInput from '../../../../util/parseMessageInput';
|
import parseHtmlAsFormattedText from '../../../../util/parseHtmlAsFormattedText';
|
||||||
import { getTextWithEntitiesAsHtml } from '../../../common/helpers/renderTextWithEntities';
|
import { getTextWithEntitiesAsHtml } from '../../../common/helpers/renderTextWithEntities';
|
||||||
|
|
||||||
import { useDebouncedResolver } from '../../../../hooks/useAsyncResolvers';
|
import { useDebouncedResolver } from '../../../../hooks/useAsyncResolvers';
|
||||||
@ -87,7 +87,7 @@ const useEditing = (
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editedMessage) return undefined;
|
if (!editedMessage) return undefined;
|
||||||
return () => {
|
return () => {
|
||||||
const edited = parseMessageInput(getHtml());
|
const edited = parseHtmlAsFormattedText(getHtml());
|
||||||
const update = edited.text.length ? edited : undefined;
|
const update = edited.text.length ? edited : undefined;
|
||||||
|
|
||||||
setEditingDraft({
|
setEditingDraft({
|
||||||
@ -99,7 +99,7 @@ const useEditing = (
|
|||||||
const detectLinkDebounced = useDebouncedResolver(() => {
|
const detectLinkDebounced = useDebouncedResolver(() => {
|
||||||
if (!editedMessage) return false;
|
if (!editedMessage) return false;
|
||||||
|
|
||||||
const edited = parseMessageInput(getHtml());
|
const edited = parseHtmlAsFormattedText(getHtml());
|
||||||
return !('webPage' in editedMessage.content)
|
return !('webPage' in editedMessage.content)
|
||||||
&& editedMessage.content.text?.entities?.some((entity) => URL_ENTITIES.has(entity.type))
|
&& editedMessage.content.text?.entities?.some((entity) => URL_ENTITIES.has(entity.type))
|
||||||
&& !(edited.entities?.some((entity) => URL_ENTITIES.has(entity.type)));
|
&& !(edited.entities?.some((entity) => URL_ENTITIES.has(entity.type)));
|
||||||
@ -144,7 +144,7 @@ const useEditing = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleEditComplete = useLastCallback(() => {
|
const handleEditComplete = useLastCallback(() => {
|
||||||
const { text, entities } = parseMessageInput(getHtml());
|
const { text, entities } = parseHtmlAsFormattedText(getHtml());
|
||||||
|
|
||||||
if (!editedMessage) {
|
if (!editedMessage) {
|
||||||
return;
|
return;
|
||||||
@ -167,7 +167,7 @@ const useEditing = (
|
|||||||
|
|
||||||
const handleBlur = useLastCallback(() => {
|
const handleBlur = useLastCallback(() => {
|
||||||
if (!editedMessage) return;
|
if (!editedMessage) return;
|
||||||
const edited = parseMessageInput(getHtml());
|
const edited = parseHtmlAsFormattedText(getHtml());
|
||||||
const update = edited.text.length ? edited : undefined;
|
const update = edited.text.length ? edited : undefined;
|
||||||
|
|
||||||
setEditingDraft({
|
setEditingDraft({
|
||||||
|
|||||||
@ -43,6 +43,8 @@ import {
|
|||||||
} from '../../../global/selectors';
|
} from '../../../global/selectors';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import { copyTextToClipboard } from '../../../util/clipboard';
|
import { copyTextToClipboard } from '../../../util/clipboard';
|
||||||
|
import { getSelectionAsFormattedText } from './helpers/getSelectionAsFormattedText';
|
||||||
|
import { isSelectionRangeInsideMessage } from './helpers/isSelectionRangeInsideMessage';
|
||||||
|
|
||||||
import useFlag from '../../../hooks/useFlag';
|
import useFlag from '../../../hooks/useFlag';
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
@ -93,6 +95,7 @@ type StateProps = {
|
|||||||
canCopy?: boolean;
|
canCopy?: boolean;
|
||||||
canTranslate?: boolean;
|
canTranslate?: boolean;
|
||||||
canShowOriginal?: boolean;
|
canShowOriginal?: boolean;
|
||||||
|
isMessageTranslated?: boolean;
|
||||||
canSelectLanguage?: boolean;
|
canSelectLanguage?: boolean;
|
||||||
isPrivate?: boolean;
|
isPrivate?: boolean;
|
||||||
isCurrentUserPremium?: boolean;
|
isCurrentUserPremium?: boolean;
|
||||||
@ -113,6 +116,8 @@ type StateProps = {
|
|||||||
messageLink?: string;
|
messageLink?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const selection = window.getSelection();
|
||||||
|
|
||||||
const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||||
availableReactions,
|
availableReactions,
|
||||||
topReactions,
|
topReactions,
|
||||||
@ -158,6 +163,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
canShowSeenBy,
|
canShowSeenBy,
|
||||||
canScheduleUntilOnline,
|
canScheduleUntilOnline,
|
||||||
canTranslate,
|
canTranslate,
|
||||||
|
isMessageTranslated,
|
||||||
canShowOriginal,
|
canShowOriginal,
|
||||||
canSelectLanguage,
|
canSelectLanguage,
|
||||||
isReactionPickerOpen,
|
isReactionPickerOpen,
|
||||||
@ -202,7 +208,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
const [isReportModalOpen, setIsReportModalOpen] = useState(false);
|
const [isReportModalOpen, setIsReportModalOpen] = useState(false);
|
||||||
const [isPinModalOpen, setIsPinModalOpen] = useState(false);
|
const [isPinModalOpen, setIsPinModalOpen] = useState(false);
|
||||||
const [isClosePollDialogOpen, openClosePollDialog, closeClosePollDialog] = useFlag();
|
const [isClosePollDialogOpen, openClosePollDialog, closeClosePollDialog] = useFlag();
|
||||||
|
const [canQuoteSelection, setCanQuoteSelection] = useState(false);
|
||||||
const [requestCalendar, calendar] = useSchedule(canScheduleUntilOnline, onClose, message.date);
|
const [requestCalendar, calendar] = useSchedule(canScheduleUntilOnline, onClose, message.date);
|
||||||
|
|
||||||
// `undefined` indicates that emoji are present and loading
|
// `undefined` indicates that emoji are present and loading
|
||||||
@ -265,6 +271,35 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
return activeDownloads?.[message.isScheduled ? 'scheduledIds' : 'ids']?.includes(message.id);
|
return activeDownloads?.[message.isScheduled ? 'scheduledIds' : 'ids']?.includes(message.id);
|
||||||
}, [activeDownloads, album, message]);
|
}, [activeDownloads, album, message]);
|
||||||
|
|
||||||
|
const selectionRange = canReply && selection?.rangeCount ? selection.getRangeAt(0) : undefined;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isMessageTranslated) {
|
||||||
|
setCanQuoteSelection(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMessageTextSelected = selectionRange
|
||||||
|
&& !selectionRange.collapsed
|
||||||
|
&& Boolean(message.content.text?.text)
|
||||||
|
&& isSelectionRangeInsideMessage(selectionRange);
|
||||||
|
|
||||||
|
if (!isMessageTextSelected) {
|
||||||
|
setCanQuoteSelection(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectionText = getSelectionAsFormattedText(selectionRange);
|
||||||
|
|
||||||
|
setCanQuoteSelection(
|
||||||
|
selectionText.text.trim().length > 0
|
||||||
|
&& message.content.text!.text!.includes(selectionText.text),
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
selectionRange, selectionRange?.collapsed, selectionRange?.startOffset, selectionRange?.endOffset,
|
||||||
|
isMessageTranslated, message.content.text,
|
||||||
|
]);
|
||||||
|
|
||||||
const handleDelete = useLastCallback(() => {
|
const handleDelete = useLastCallback(() => {
|
||||||
setIsMenuOpen(false);
|
setIsMenuOpen(false);
|
||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
@ -296,7 +331,10 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleReply = useLastCallback(() => {
|
const handleReply = useLastCallback(() => {
|
||||||
updateDraftReplyInfo({ replyToMsgId: message.id });
|
updateDraftReplyInfo({
|
||||||
|
replyToMsgId: message.id,
|
||||||
|
quoteText: canQuoteSelection && selectionRange ? getSelectionAsFormattedText(selectionRange) : undefined,
|
||||||
|
});
|
||||||
closeMenu();
|
closeMenu();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -493,6 +531,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
canSendNow={canSendNow}
|
canSendNow={canSendNow}
|
||||||
canReschedule={canReschedule}
|
canReschedule={canReschedule}
|
||||||
canReply={canReply}
|
canReply={canReply}
|
||||||
|
canQuote={canQuoteSelection}
|
||||||
canDelete={canDelete}
|
canDelete={canDelete}
|
||||||
canReport={canReport}
|
canReport={canReport}
|
||||||
canPin={canPin}
|
canPin={canPin}
|
||||||
@ -679,6 +718,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
canTranslate,
|
canTranslate,
|
||||||
canShowOriginal: hasTranslation && !isChatTranslated,
|
canShowOriginal: hasTranslation && !isChatTranslated,
|
||||||
canSelectLanguage: hasTranslation && !isChatTranslated,
|
canSelectLanguage: hasTranslation && !isChatTranslated,
|
||||||
|
isMessageTranslated: hasTranslation,
|
||||||
canPlayAnimatedEmojis: selectCanPlayAnimatedEmojis(global),
|
canPlayAnimatedEmojis: selectCanPlayAnimatedEmojis(global),
|
||||||
isReactionPickerOpen: selectIsReactionPickerOpen(global),
|
isReactionPickerOpen: selectIsReactionPickerOpen(global),
|
||||||
messageLink,
|
messageLink,
|
||||||
|
|||||||
@ -95,6 +95,7 @@ import {
|
|||||||
import { isAnimatingScroll } from '../../../util/animateScroll';
|
import { isAnimatingScroll } from '../../../util/animateScroll';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import { isElementInViewport } from '../../../util/isElementInViewport';
|
import { isElementInViewport } from '../../../util/isElementInViewport';
|
||||||
|
import stopEvent from '../../../util/stopEvent';
|
||||||
import { IS_ANDROID, IS_ELECTRON, IS_TRANSLATION_SUPPORTED } from '../../../util/windowEnvironment';
|
import { IS_ANDROID, IS_ELECTRON, IS_TRANSLATION_SUPPORTED } from '../../../util/windowEnvironment';
|
||||||
import {
|
import {
|
||||||
calculateDimensionsForMessageMedia,
|
calculateDimensionsForMessageMedia,
|
||||||
@ -522,6 +523,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
const avatarPeer = shouldPreferOriginSender ? originSender : messageSender;
|
const avatarPeer = shouldPreferOriginSender ? originSender : messageSender;
|
||||||
const messageColorPeer = originSender || sender;
|
const messageColorPeer = originSender || sender;
|
||||||
const senderPeer = (forwardInfo || message.content.storyData) ? originSender : messageSender;
|
const senderPeer = (forwardInfo || message.content.storyData) ? originSender : messageSender;
|
||||||
|
const hasText = hasMessageText(message);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
handleMouseDown,
|
handleMouseDown,
|
||||||
@ -605,7 +607,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
const containerClassName = buildClassName(
|
const containerClassName = buildClassName(
|
||||||
'Message message-list-item',
|
'Message message-list-item',
|
||||||
isFirstInGroup && 'first-in-group',
|
isFirstInGroup && 'first-in-group',
|
||||||
isProtected ? 'is-protected' : 'allow-selection',
|
isProtected && !hasText ? 'is-protected' : 'allow-selection',
|
||||||
isLastInGroup && 'last-in-group',
|
isLastInGroup && 'last-in-group',
|
||||||
isFirstInDocumentGroup && 'first-in-document-group',
|
isFirstInDocumentGroup && 'first-in-document-group',
|
||||||
isLastInDocumentGroup && 'last-in-document-group',
|
isLastInDocumentGroup && 'last-in-document-group',
|
||||||
@ -686,7 +688,6 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const withAppendix = contentClassName.includes('has-appendix');
|
const withAppendix = contentClassName.includes('has-appendix');
|
||||||
const hasText = hasMessageText(message);
|
|
||||||
const emojiSize = getCustomEmojiSize(message.emojiOnlyCount);
|
const emojiSize = getCustomEmojiSize(message.emojiOnlyCount);
|
||||||
|
|
||||||
let metaPosition!: MetaPosition;
|
let metaPosition!: MetaPosition;
|
||||||
@ -1319,6 +1320,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
id={getMessageHtmlId(message.id)}
|
id={getMessageHtmlId(message.id)}
|
||||||
className={containerClassName}
|
className={containerClassName}
|
||||||
data-message-id={messageId}
|
data-message-id={messageId}
|
||||||
|
onCopy={isProtected ? stopEvent : undefined}
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
|
|||||||
@ -53,6 +53,7 @@ type OwnProps = {
|
|||||||
maxUniqueReactions?: number;
|
maxUniqueReactions?: number;
|
||||||
canReschedule?: boolean;
|
canReschedule?: boolean;
|
||||||
canReply?: boolean;
|
canReply?: boolean;
|
||||||
|
canQuote?: boolean;
|
||||||
repliesThreadInfo?: ApiThreadInfo;
|
repliesThreadInfo?: ApiThreadInfo;
|
||||||
canPin?: boolean;
|
canPin?: boolean;
|
||||||
canUnpin?: boolean;
|
canUnpin?: boolean;
|
||||||
@ -140,6 +141,7 @@ const MessageContextMenu: FC<OwnProps> = ({
|
|||||||
canReschedule,
|
canReschedule,
|
||||||
canBuyPremium,
|
canBuyPremium,
|
||||||
canReply,
|
canReply,
|
||||||
|
canQuote,
|
||||||
canEdit,
|
canEdit,
|
||||||
noReplies,
|
noReplies,
|
||||||
canPin,
|
canPin,
|
||||||
@ -361,7 +363,11 @@ const MessageContextMenu: FC<OwnProps> = ({
|
|||||||
{canReschedule && (
|
{canReschedule && (
|
||||||
<MenuItem icon="schedule" onClick={onReschedule}>{lang('MessageScheduleEditTime')}</MenuItem>
|
<MenuItem icon="schedule" onClick={onReschedule}>{lang('MessageScheduleEditTime')}</MenuItem>
|
||||||
)}
|
)}
|
||||||
{canReply && <MenuItem icon="reply" onClick={onReply}>{lang('Reply')}</MenuItem>}
|
{canReply && (
|
||||||
|
<MenuItem icon="reply" onClick={onReply}>
|
||||||
|
{lang(canQuote ? 'lng_context_quote_and_reply' : 'Reply')}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
{!noReplies && Boolean(repliesThreadInfo?.messagesCount) && (
|
{!noReplies && Boolean(repliesThreadInfo?.messagesCount) && (
|
||||||
<MenuItem icon="replies" onClick={onOpenThread}>
|
<MenuItem icon="replies" onClick={onOpenThread}>
|
||||||
{lang('Conversation.ContextViewReplies', repliesThreadInfo!.messagesCount, 'i')}
|
{lang('Conversation.ContextViewReplies', repliesThreadInfo!.messagesCount, 'i')}
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import {
|
|||||||
selectPeerStory, selectTabState,
|
selectPeerStory, selectTabState,
|
||||||
} from '../../../global/selectors';
|
} from '../../../global/selectors';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import parseMessageInput from '../../../util/parseMessageInput';
|
import parseHtmlAsFormattedText from '../../../util/parseHtmlAsFormattedText';
|
||||||
import { REM } from '../../common/helpers/mediaDimensions';
|
import { REM } from '../../common/helpers/mediaDimensions';
|
||||||
import { buildCustomEmojiHtml } from '../composer/helpers/customEmoji';
|
import { buildCustomEmojiHtml } from '../composer/helpers/customEmoji';
|
||||||
|
|
||||||
@ -162,7 +162,7 @@ const ReactionPicker: FC<OwnProps & StateProps> = ({
|
|||||||
if ('emoticon' in item) {
|
if ('emoticon' in item) {
|
||||||
text = item.emoticon;
|
text = item.emoticon;
|
||||||
} else {
|
} else {
|
||||||
const customEmojiMessage = parseMessageInput(buildCustomEmojiHtml(sticker!));
|
const customEmojiMessage = parseHtmlAsFormattedText(buildCustomEmojiHtml(sticker!));
|
||||||
text = customEmojiMessage.text;
|
text = customEmojiMessage.text;
|
||||||
entities = customEmojiMessage.entities;
|
entities = customEmojiMessage.entities;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -953,6 +953,12 @@
|
|||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove extra bottom padding from `blockquote`
|
||||||
|
.text-entity-blockquote-wrapper {
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%
|
||||||
|
}
|
||||||
|
|
||||||
blockquote, .blockquote {
|
blockquote, .blockquote {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@ -0,0 +1,57 @@
|
|||||||
|
import type { ApiFormattedText } from '../../../../api/types';
|
||||||
|
import { ApiMessageEntityTypes } from '../../../../api/types';
|
||||||
|
|
||||||
|
import parseHtmlAsFormattedText from '../../../../util/parseHtmlAsFormattedText';
|
||||||
|
|
||||||
|
const div = document.createElement('div');
|
||||||
|
const ALLOWED_QUOTE_ENTITIES = new Set([
|
||||||
|
ApiMessageEntityTypes.Bold,
|
||||||
|
ApiMessageEntityTypes.Italic,
|
||||||
|
ApiMessageEntityTypes.Underline,
|
||||||
|
ApiMessageEntityTypes.Strike,
|
||||||
|
ApiMessageEntityTypes.Spoiler,
|
||||||
|
ApiMessageEntityTypes.CustomEmoji,
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function getSelectionAsFormattedText(range: Range) {
|
||||||
|
const html = getSelectionAsHtml(range);
|
||||||
|
const formattedText = parseHtmlAsFormattedText(html, false, true);
|
||||||
|
|
||||||
|
return stripEntitiesForQuote(formattedText);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectionAsHtml(range: Range) {
|
||||||
|
const clonedSelection = range.cloneContents();
|
||||||
|
div.appendChild(clonedSelection);
|
||||||
|
|
||||||
|
const html = wrapHtmlWithMarkupTags(range, div.innerHTML);
|
||||||
|
div.innerHTML = '';
|
||||||
|
|
||||||
|
return html
|
||||||
|
.replace(/<br\s*\/?>/gi, '\n')
|
||||||
|
.replace(/ /gi, ' ') // Convert nbsp's to spaces
|
||||||
|
.replace(/\u00a0/gi, ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripEntitiesForQuote(text: ApiFormattedText): ApiFormattedText {
|
||||||
|
if (!text.entities) return text;
|
||||||
|
|
||||||
|
const entities = text.entities.filter((entity) => ALLOWED_QUOTE_ENTITIES.has(entity.type as ApiMessageEntityTypes));
|
||||||
|
return { ...text, entities: entities.length ? entities : undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
function wrapHtmlWithMarkupTags(range: Range, html: string) {
|
||||||
|
const container = range.commonAncestorContainer;
|
||||||
|
if (container.nodeType === Node.ELEMENT_NODE && (container as Element).classList.contains('text-content')) {
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
let currentElement = range.commonAncestorContainer.parentElement;
|
||||||
|
while (currentElement && !currentElement.classList.contains('text-content')) {
|
||||||
|
const tag = currentElement.tagName.toLowerCase();
|
||||||
|
const entityType = currentElement.dataset.entityType;
|
||||||
|
html = `<${tag} ${entityType ? `data-entity-type="${entityType}"` : ''}>${html}</${tag}>`;
|
||||||
|
currentElement = currentElement.parentElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
export function isSelectionRangeInsideMessage(range: Range) {
|
||||||
|
const ancestor = range.commonAncestorContainer;
|
||||||
|
const el = ancestor.nodeType === Node.TEXT_NODE
|
||||||
|
? ancestor.parentNode! as Element
|
||||||
|
: ancestor as Element;
|
||||||
|
|
||||||
|
return Boolean(el.closest('.message-content-wrapper .text-content'))
|
||||||
|
&& !(Boolean(el.closest('.EmbeddedMessage')) || Boolean(el.closest('.WebPage')));
|
||||||
|
}
|
||||||
@ -16,7 +16,7 @@ import { copyHtmlToClipboard } from '../../../util/clipboard';
|
|||||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||||
import { compact, findLast } from '../../../util/iteratees';
|
import { compact, findLast } from '../../../util/iteratees';
|
||||||
import * as langProvider from '../../../util/langProvider';
|
import * as langProvider from '../../../util/langProvider';
|
||||||
import parseMessageInput from '../../../util/parseMessageInput';
|
import parseHtmlAsFormattedText from '../../../util/parseHtmlAsFormattedText';
|
||||||
import { getServerTime } from '../../../util/serverTime';
|
import { getServerTime } from '../../../util/serverTime';
|
||||||
import { IS_TOUCH_ENV } from '../../../util/windowEnvironment';
|
import { IS_TOUCH_ENV } from '../../../util/windowEnvironment';
|
||||||
import versionNotification from '../../../versionNotification.txt';
|
import versionNotification from '../../../versionNotification.txt';
|
||||||
@ -689,7 +689,7 @@ addActionHandler('checkVersionNotification', (global, actions): ActionReturnType
|
|||||||
chatId: SERVICE_NOTIFICATIONS_USER_ID,
|
chatId: SERVICE_NOTIFICATIONS_USER_ID,
|
||||||
date: getServerTime(),
|
date: getServerTime(),
|
||||||
content: {
|
content: {
|
||||||
text: parseMessageInput(versionNotification, true),
|
text: parseHtmlAsFormattedText(versionNotification, true),
|
||||||
},
|
},
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -175,80 +175,81 @@ $icons-map: (
|
|||||||
"premium": "\f190",
|
"premium": "\f190",
|
||||||
"previous": "\f191",
|
"previous": "\f191",
|
||||||
"privacy-policy": "\f192",
|
"privacy-policy": "\f192",
|
||||||
"quote": "\f193",
|
"quote-text": "\f193",
|
||||||
"readchats": "\f194",
|
"quote": "\f194",
|
||||||
"recent": "\f195",
|
"readchats": "\f195",
|
||||||
"reload": "\f196",
|
"recent": "\f196",
|
||||||
"remove": "\f197",
|
"reload": "\f197",
|
||||||
"reopen-topic": "\f198",
|
"remove": "\f198",
|
||||||
"replace": "\f199",
|
"reopen-topic": "\f199",
|
||||||
"replies": "\f19a",
|
"replace": "\f19a",
|
||||||
"reply-filled": "\f19b",
|
"replies": "\f19b",
|
||||||
"reply": "\f19c",
|
"reply-filled": "\f19c",
|
||||||
"revote": "\f19d",
|
"reply": "\f19d",
|
||||||
"save-story": "\f19e",
|
"revote": "\f19e",
|
||||||
"saved-messages": "\f19f",
|
"save-story": "\f19f",
|
||||||
"schedule": "\f1a0",
|
"saved-messages": "\f1a0",
|
||||||
"search": "\f1a1",
|
"schedule": "\f1a1",
|
||||||
"select": "\f1a2",
|
"search": "\f1a2",
|
||||||
"send-outline": "\f1a3",
|
"select": "\f1a3",
|
||||||
"send": "\f1a4",
|
"send-outline": "\f1a4",
|
||||||
"settings-filled": "\f1a5",
|
"send": "\f1a5",
|
||||||
"settings": "\f1a6",
|
"settings-filled": "\f1a6",
|
||||||
"share-filled": "\f1a7",
|
"settings": "\f1a7",
|
||||||
"share-screen-outlined": "\f1a8",
|
"share-filled": "\f1a8",
|
||||||
"share-screen-stop": "\f1a9",
|
"share-screen-outlined": "\f1a9",
|
||||||
"share-screen": "\f1aa",
|
"share-screen-stop": "\f1aa",
|
||||||
"sidebar": "\f1ab",
|
"share-screen": "\f1ab",
|
||||||
"skip-next": "\f1ac",
|
"sidebar": "\f1ac",
|
||||||
"skip-previous": "\f1ad",
|
"skip-next": "\f1ad",
|
||||||
"smallscreen": "\f1ae",
|
"skip-previous": "\f1ae",
|
||||||
"smile": "\f1af",
|
"smallscreen": "\f1af",
|
||||||
"sort": "\f1b0",
|
"smile": "\f1b0",
|
||||||
"speaker-muted-story": "\f1b1",
|
"sort": "\f1b1",
|
||||||
"speaker-outline": "\f1b2",
|
"speaker-muted-story": "\f1b2",
|
||||||
"speaker-story": "\f1b3",
|
"speaker-outline": "\f1b3",
|
||||||
"speaker": "\f1b4",
|
"speaker-story": "\f1b4",
|
||||||
"spoiler-disable": "\f1b5",
|
"speaker": "\f1b5",
|
||||||
"spoiler": "\f1b6",
|
"spoiler-disable": "\f1b6",
|
||||||
"sport": "\f1b7",
|
"spoiler": "\f1b7",
|
||||||
"stats": "\f1b8",
|
"sport": "\f1b8",
|
||||||
"stealth-future": "\f1b9",
|
"stats": "\f1b9",
|
||||||
"stealth-past": "\f1ba",
|
"stealth-future": "\f1ba",
|
||||||
"stickers": "\f1bb",
|
"stealth-past": "\f1bb",
|
||||||
"stop-raising-hand": "\f1bc",
|
"stickers": "\f1bc",
|
||||||
"stop": "\f1bd",
|
"stop-raising-hand": "\f1bd",
|
||||||
"story-caption": "\f1be",
|
"stop": "\f1be",
|
||||||
"story-expired": "\f1bf",
|
"story-caption": "\f1bf",
|
||||||
"story-priority": "\f1c0",
|
"story-expired": "\f1c0",
|
||||||
"story-reply": "\f1c1",
|
"story-priority": "\f1c1",
|
||||||
"strikethrough": "\f1c2",
|
"story-reply": "\f1c2",
|
||||||
"timer": "\f1c3",
|
"strikethrough": "\f1c3",
|
||||||
"transcribe": "\f1c4",
|
"timer": "\f1c4",
|
||||||
"truck": "\f1c5",
|
"transcribe": "\f1c5",
|
||||||
"unarchive": "\f1c6",
|
"truck": "\f1c6",
|
||||||
"underlined": "\f1c7",
|
"unarchive": "\f1c7",
|
||||||
"unlock-badge": "\f1c8",
|
"underlined": "\f1c8",
|
||||||
"unlock": "\f1c9",
|
"unlock-badge": "\f1c9",
|
||||||
"unmute": "\f1ca",
|
"unlock": "\f1ca",
|
||||||
"unpin": "\f1cb",
|
"unmute": "\f1cb",
|
||||||
"unread": "\f1cc",
|
"unpin": "\f1cc",
|
||||||
"up": "\f1cd",
|
"unread": "\f1cd",
|
||||||
"user-filled": "\f1ce",
|
"up": "\f1ce",
|
||||||
"user-online": "\f1cf",
|
"user-filled": "\f1cf",
|
||||||
"user": "\f1d0",
|
"user-online": "\f1d0",
|
||||||
"video-outlined": "\f1d1",
|
"user": "\f1d1",
|
||||||
"video-stop": "\f1d2",
|
"video-outlined": "\f1d2",
|
||||||
"video": "\f1d3",
|
"video-stop": "\f1d3",
|
||||||
"voice-chat": "\f1d4",
|
"video": "\f1d4",
|
||||||
"volume-1": "\f1d5",
|
"voice-chat": "\f1d5",
|
||||||
"volume-2": "\f1d6",
|
"volume-1": "\f1d6",
|
||||||
"volume-3": "\f1d7",
|
"volume-2": "\f1d7",
|
||||||
"web": "\f1d8",
|
"volume-3": "\f1d8",
|
||||||
"webapp": "\f1d9",
|
"web": "\f1d9",
|
||||||
"word-wrap": "\f1da",
|
"webapp": "\f1da",
|
||||||
"zoom-in": "\f1db",
|
"word-wrap": "\f1db",
|
||||||
"zoom-out": "\f1dc",
|
"zoom-in": "\f1dc",
|
||||||
|
"zoom-out": "\f1dd",
|
||||||
);
|
);
|
||||||
|
|
||||||
.icon-active-sessions::before {
|
.icon-active-sessions::before {
|
||||||
@ -689,6 +690,9 @@ $icons-map: (
|
|||||||
.icon-privacy-policy::before {
|
.icon-privacy-policy::before {
|
||||||
content: map.get($icons-map, "privacy-policy");
|
content: map.get($icons-map, "privacy-policy");
|
||||||
}
|
}
|
||||||
|
.icon-quote-text::before {
|
||||||
|
content: map.get($icons-map, "quote-text");
|
||||||
|
}
|
||||||
.icon-quote::before {
|
.icon-quote::before {
|
||||||
content: map.get($icons-map, "quote");
|
content: map.get($icons-map, "quote");
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@ -145,6 +145,7 @@ export type FontIconName =
|
|||||||
| 'premium'
|
| 'premium'
|
||||||
| 'previous'
|
| 'previous'
|
||||||
| 'privacy-policy'
|
| 'privacy-policy'
|
||||||
|
| 'quote-text'
|
||||||
| 'quote'
|
| 'quote'
|
||||||
| 'readchats'
|
| 'readchats'
|
||||||
| 'recent'
|
| 'recent'
|
||||||
|
|||||||
@ -21,7 +21,7 @@ export const ENTITY_CLASS_BY_NODE_NAME: Record<string, ApiMessageEntityTypes> =
|
|||||||
|
|
||||||
const MAX_TAG_DEEPNESS = 3;
|
const MAX_TAG_DEEPNESS = 3;
|
||||||
|
|
||||||
export default function parseMessageInput(
|
export default function parseHtmlAsFormattedText(
|
||||||
html: string, withMarkdownLinks = false, skipMarkdown = false,
|
html: string, withMarkdownLinks = false, skipMarkdown = false,
|
||||||
): ApiFormattedText {
|
): ApiFormattedText {
|
||||||
const fragment = document.createElement('div');
|
const fragment = document.createElement('div');
|
||||||
Loading…
x
Reference in New Issue
Block a user