Message: Support quote highlight with entities (#5842)
This commit is contained in:
parent
185a42be5d
commit
58e79daddb
@ -286,6 +286,7 @@ export function buildMessageDraft(draft: GramJs.TypeDraftMessage): ApiDraft | un
|
|||||||
replyToTopId: replyTo.topMsgId,
|
replyToTopId: replyTo.topMsgId,
|
||||||
replyToPeerId: replyTo.replyToPeerId && getApiChatIdFromMtpPeer(replyTo.replyToPeerId),
|
replyToPeerId: replyTo.replyToPeerId && getApiChatIdFromMtpPeer(replyTo.replyToPeerId),
|
||||||
quoteText: replyTo.quoteText ? buildMessageTextContent(replyTo.quoteText, replyTo.quoteEntities) : undefined,
|
quoteText: replyTo.quoteText ? buildMessageTextContent(replyTo.quoteText, replyTo.quoteEntities) : undefined,
|
||||||
|
quoteOffset: replyTo.quoteOffset,
|
||||||
} satisfies ApiInputMessageReplyInfo : undefined;
|
} satisfies ApiInputMessageReplyInfo : undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -340,6 +341,7 @@ function buildApiReplyInfo(
|
|||||||
quote,
|
quote,
|
||||||
quoteText,
|
quoteText,
|
||||||
quoteEntities,
|
quoteEntities,
|
||||||
|
quoteOffset,
|
||||||
} = replyHeader;
|
} = replyHeader;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -352,6 +354,7 @@ function buildApiReplyInfo(
|
|||||||
replyMedia: replyMedia && buildMessageMediaContent(replyMedia, context),
|
replyMedia: replyMedia && buildMessageMediaContent(replyMedia, context),
|
||||||
isQuote: quote,
|
isQuote: quote,
|
||||||
quoteText: quoteText ? buildMessageTextContent(quoteText, quoteEntities) : undefined,
|
quoteText: quoteText ? buildMessageTextContent(quoteText, quoteEntities) : undefined,
|
||||||
|
quoteOffset,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -554,6 +557,7 @@ function buildReplyInfo(inputInfo: ApiInputReplyInfo, isForum?: boolean): ApiRep
|
|||||||
replyToTopId: inputInfo.replyToTopId,
|
replyToTopId: inputInfo.replyToTopId,
|
||||||
replyToPeerId: inputInfo.replyToPeerId,
|
replyToPeerId: inputInfo.replyToPeerId,
|
||||||
quoteText: inputInfo.quoteText,
|
quoteText: inputInfo.quoteText,
|
||||||
|
quoteOffset: inputInfo.quoteOffset,
|
||||||
isForumTopic: isForum && inputInfo.replyToTopId ? true : undefined,
|
isForumTopic: isForum && inputInfo.replyToTopId ? true : undefined,
|
||||||
...(Boolean(inputInfo.quoteText) && { isQuote: true }),
|
...(Boolean(inputInfo.quoteText) && { isQuote: true }),
|
||||||
};
|
};
|
||||||
|
|||||||
@ -805,7 +805,7 @@ export function buildInputReplyTo(replyInfo: ApiInputReplyInfo) {
|
|||||||
|
|
||||||
if (replyInfo.type === 'message') {
|
if (replyInfo.type === 'message') {
|
||||||
const {
|
const {
|
||||||
replyToMsgId, replyToTopId, replyToPeerId, quoteText,
|
replyToMsgId, replyToTopId, replyToPeerId, quoteText, quoteOffset,
|
||||||
} = replyInfo;
|
} = replyInfo;
|
||||||
return new GramJs.InputReplyToMessage({
|
return new GramJs.InputReplyToMessage({
|
||||||
replyToMsgId,
|
replyToMsgId,
|
||||||
@ -813,6 +813,7 @@ export function buildInputReplyTo(replyInfo: ApiInputReplyInfo) {
|
|||||||
replyToPeerId: replyToPeerId ? buildInputPeerFromLocalDb(replyToPeerId)! : undefined,
|
replyToPeerId: replyToPeerId ? buildInputPeerFromLocalDb(replyToPeerId)! : undefined,
|
||||||
quoteText: quoteText?.text,
|
quoteText: quoteText?.text,
|
||||||
quoteEntities: quoteText?.entities?.map(buildMtpMessageEntity),
|
quoteEntities: quoteText?.entities?.map(buildMtpMessageEntity),
|
||||||
|
quoteOffset,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -365,6 +365,7 @@ export interface ApiMessageReplyInfo {
|
|||||||
isForumTopic?: true;
|
isForumTopic?: true;
|
||||||
isQuote?: true;
|
isQuote?: true;
|
||||||
quoteText?: ApiFormattedText;
|
quoteText?: ApiFormattedText;
|
||||||
|
quoteOffset?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiStoryReplyInfo {
|
export interface ApiStoryReplyInfo {
|
||||||
@ -379,6 +380,7 @@ export interface ApiInputMessageReplyInfo {
|
|||||||
replyToTopId?: number;
|
replyToTopId?: number;
|
||||||
replyToPeerId?: string;
|
replyToPeerId?: string;
|
||||||
quoteText?: ApiFormattedText;
|
quoteText?: ApiFormattedText;
|
||||||
|
quoteOffset?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiInputStoryReplyInfo {
|
export interface ApiInputStoryReplyInfo {
|
||||||
@ -457,6 +459,7 @@ export type ApiMessageEntityCustomEmoji = {
|
|||||||
documentId: string;
|
documentId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Local entities
|
||||||
export type ApiMessageEntityTimestamp = {
|
export type ApiMessageEntityTimestamp = {
|
||||||
type: ApiMessageEntityTypes.Timestamp;
|
type: ApiMessageEntityTypes.Timestamp;
|
||||||
offset: number;
|
offset: number;
|
||||||
@ -464,8 +467,15 @@ export type ApiMessageEntityTimestamp = {
|
|||||||
timestamp: number;
|
timestamp: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ApiMessageEntityQuoteFocus = {
|
||||||
|
type: 'quoteFocus';
|
||||||
|
offset: number;
|
||||||
|
length: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type ApiMessageEntity = ApiMessageEntityDefault | ApiMessageEntityPre | ApiMessageEntityTextUrl |
|
export type ApiMessageEntity = ApiMessageEntityDefault | ApiMessageEntityPre | ApiMessageEntityTextUrl |
|
||||||
ApiMessageEntityMentionName | ApiMessageEntityCustomEmoji | ApiMessageEntityBlockquote | ApiMessageEntityTimestamp;
|
ApiMessageEntityMentionName | ApiMessageEntityCustomEmoji | ApiMessageEntityBlockquote | ApiMessageEntityTimestamp |
|
||||||
|
ApiMessageEntityQuoteFocus;
|
||||||
|
|
||||||
export enum ApiMessageEntityTypes {
|
export enum ApiMessageEntityTypes {
|
||||||
Bold = 'MessageEntityBold',
|
Bold = 'MessageEntityBold',
|
||||||
@ -487,6 +497,7 @@ export enum ApiMessageEntityTypes {
|
|||||||
Spoiler = 'MessageEntitySpoiler',
|
Spoiler = 'MessageEntitySpoiler',
|
||||||
CustomEmoji = 'MessageEntityCustomEmoji',
|
CustomEmoji = 'MessageEntityCustomEmoji',
|
||||||
Timestamp = 'MessageEntityTimestamp',
|
Timestamp = 'MessageEntityTimestamp',
|
||||||
|
QuoteFocus = 'MessageEntityQuoteFocus',
|
||||||
Unknown = 'MessageEntityUnknown',
|
Unknown = 'MessageEntityUnknown',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import { ApiMessageEntityTypes } from '../../api/types';
|
|||||||
import { CONTENT_NOT_SUPPORTED } from '../../config';
|
import { CONTENT_NOT_SUPPORTED } from '../../config';
|
||||||
import { extractMessageText, stripCustomEmoji } from '../../global/helpers';
|
import { extractMessageText, stripCustomEmoji } from '../../global/helpers';
|
||||||
import trimText from '../../util/trimText';
|
import trimText from '../../util/trimText';
|
||||||
import { renderTextWithEntities } from './helpers/renderTextWithEntities';
|
import { insertTextEntity, renderTextWithEntities } from './helpers/renderTextWithEntities';
|
||||||
|
|
||||||
import useSyncEffect from '../../hooks/useSyncEffect';
|
import useSyncEffect from '../../hooks/useSyncEffect';
|
||||||
import useUniqueId from '../../hooks/useUniqueId';
|
import useUniqueId from '../../hooks/useUniqueId';
|
||||||
@ -32,6 +32,7 @@ interface OwnProps {
|
|||||||
inChatList?: boolean;
|
inChatList?: boolean;
|
||||||
forcePlayback?: boolean;
|
forcePlayback?: boolean;
|
||||||
focusedQuote?: string;
|
focusedQuote?: string;
|
||||||
|
focusedQuoteOffset?: number;
|
||||||
isInSelectMode?: boolean;
|
isInSelectMode?: boolean;
|
||||||
canBeEmpty?: boolean;
|
canBeEmpty?: boolean;
|
||||||
maxTimestamp?: number;
|
maxTimestamp?: number;
|
||||||
@ -55,6 +56,7 @@ function MessageText({
|
|||||||
inChatList,
|
inChatList,
|
||||||
forcePlayback,
|
forcePlayback,
|
||||||
focusedQuote,
|
focusedQuote,
|
||||||
|
focusedQuoteOffset,
|
||||||
isInSelectMode,
|
isInSelectMode,
|
||||||
canBeEmpty,
|
canBeEmpty,
|
||||||
maxTimestamp,
|
maxTimestamp,
|
||||||
@ -71,21 +73,38 @@ function MessageText({
|
|||||||
const adaptedFormattedText = isForAnimation && formattedText ? stripCustomEmoji(formattedText) : formattedText;
|
const adaptedFormattedText = isForAnimation && formattedText ? stripCustomEmoji(formattedText) : formattedText;
|
||||||
const { text, entities } = adaptedFormattedText || {};
|
const { text, entities } = adaptedFormattedText || {};
|
||||||
|
|
||||||
|
const entitiesWithFocusedQuote = useMemo(() => {
|
||||||
|
if (!text || !focusedQuote) return entities;
|
||||||
|
|
||||||
|
const index = text.indexOf(focusedQuote, focusedQuoteOffset);
|
||||||
|
const lendth = focusedQuote.length;
|
||||||
|
if (index >= 0) {
|
||||||
|
return insertTextEntity(entities || [], {
|
||||||
|
offset: index,
|
||||||
|
length: lendth,
|
||||||
|
type: ApiMessageEntityTypes.QuoteFocus,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return entities;
|
||||||
|
}, [text, entities, focusedQuote, focusedQuoteOffset]);
|
||||||
|
|
||||||
const containerId = useUniqueId();
|
const containerId = useUniqueId();
|
||||||
|
|
||||||
useSyncEffect(() => {
|
useSyncEffect(() => {
|
||||||
textCacheBusterRef.current += 1;
|
textCacheBusterRef.current += 1;
|
||||||
}, [text, entities]);
|
}, [text, entitiesWithFocusedQuote]);
|
||||||
|
|
||||||
const withSharedCanvas = useMemo(() => {
|
const withSharedCanvas = useMemo(() => {
|
||||||
const hasSpoilers = entities?.some((e) => e.type === ApiMessageEntityTypes.Spoiler);
|
const hasSpoilers = entitiesWithFocusedQuote?.some((e) => e.type === ApiMessageEntityTypes.Spoiler);
|
||||||
if (hasSpoilers) {
|
if (hasSpoilers) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const customEmojisCount = entities?.filter((e) => e.type === ApiMessageEntityTypes.CustomEmoji).length || 0;
|
const customEmojisCount = entitiesWithFocusedQuote
|
||||||
|
?.filter((e) => e.type === ApiMessageEntityTypes.CustomEmoji).length || 0;
|
||||||
return customEmojisCount >= MIN_CUSTOM_EMOJIS_FOR_SHARED_CANVAS;
|
return customEmojisCount >= MIN_CUSTOM_EMOJIS_FOR_SHARED_CANVAS;
|
||||||
}, [entities]) || 0;
|
}, [entitiesWithFocusedQuote]) || 0;
|
||||||
|
|
||||||
if (!text && !canBeEmpty) {
|
if (!text && !canBeEmpty) {
|
||||||
return <span className="content-unsupported">{CONTENT_NOT_SUPPORTED}</span>;
|
return <span className="content-unsupported">{CONTENT_NOT_SUPPORTED}</span>;
|
||||||
@ -98,7 +117,7 @@ function MessageText({
|
|||||||
withSharedCanvas && <canvas ref={sharedCanvasHqRef} className="shared-canvas" />,
|
withSharedCanvas && <canvas ref={sharedCanvasHqRef} className="shared-canvas" />,
|
||||||
renderTextWithEntities({
|
renderTextWithEntities({
|
||||||
text: trimText(text!, truncateLength),
|
text: trimText(text!, truncateLength),
|
||||||
entities,
|
entities: entitiesWithFocusedQuote,
|
||||||
highlight,
|
highlight,
|
||||||
emojiSize,
|
emojiSize,
|
||||||
shouldRenderAsHtml,
|
shouldRenderAsHtml,
|
||||||
@ -112,7 +131,6 @@ function MessageText({
|
|||||||
sharedCanvasHqRef,
|
sharedCanvasHqRef,
|
||||||
cacheBuster: textCacheBusterRef.current.toString(),
|
cacheBuster: textCacheBusterRef.current.toString(),
|
||||||
forcePlayback,
|
forcePlayback,
|
||||||
focusedQuote,
|
|
||||||
isInSelectMode,
|
isInSelectMode,
|
||||||
maxTimestamp,
|
maxTimestamp,
|
||||||
chatId: 'chatId' in messageOrStory ? messageOrStory.chatId : undefined,
|
chatId: 'chatId' in messageOrStory ? messageOrStory.chatId : undefined,
|
||||||
|
|||||||
@ -23,7 +23,7 @@ import SafeLink from '../SafeLink';
|
|||||||
|
|
||||||
export type TextFilter = (
|
export type TextFilter = (
|
||||||
'escape_html' | 'hq_emoji' | 'emoji' | 'emoji_html' | 'br' | 'br_html' | 'highlight' | 'links' |
|
'escape_html' | 'hq_emoji' | 'emoji' | 'emoji_html' | 'br' | 'br_html' | 'highlight' | 'links' |
|
||||||
'simple_markdown' | 'simple_markdown_html' | 'quote' | 'tg_links'
|
'simple_markdown' | 'simple_markdown_html' | 'tg_links'
|
||||||
);
|
);
|
||||||
|
|
||||||
const SIMPLE_MARKDOWN_REGEX = /(\*\*|__).+?\1/g;
|
const SIMPLE_MARKDOWN_REGEX = /(\*\*|__).+?\1/g;
|
||||||
@ -31,7 +31,10 @@ const SIMPLE_MARKDOWN_REGEX = /(\*\*|__).+?\1/g;
|
|||||||
export default function renderText(
|
export default function renderText(
|
||||||
part: TextPart,
|
part: TextPart,
|
||||||
filters: Array<TextFilter> = ['emoji'],
|
filters: Array<TextFilter> = ['emoji'],
|
||||||
params?: { highlight?: string; quote?: string; markdownPostProcessor?: (part: string) => TeactNode },
|
params?: {
|
||||||
|
highlight?: string;
|
||||||
|
markdownPostProcessor?: (part: string) => TeactNode;
|
||||||
|
},
|
||||||
): TeactNode[] {
|
): TeactNode[] {
|
||||||
if (typeof part !== 'string') {
|
if (typeof part !== 'string') {
|
||||||
return [part];
|
return [part];
|
||||||
@ -63,9 +66,6 @@ export default function renderText(
|
|||||||
case 'highlight':
|
case 'highlight':
|
||||||
return addHighlight(text, params!.highlight);
|
return addHighlight(text, params!.highlight);
|
||||||
|
|
||||||
case 'quote':
|
|
||||||
return addHighlight(text, params!.quote, true);
|
|
||||||
|
|
||||||
case 'links':
|
case 'links':
|
||||||
return addLinks(text);
|
return addLinks(text);
|
||||||
|
|
||||||
@ -190,7 +190,7 @@ function addLineBreaks(textParts: TextPart[], type: 'jsx' | 'html'): TextPart[]
|
|||||||
}, []);
|
}, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
function addHighlight(textParts: TextPart[], highlight: string | undefined, isQuote?: true): TextPart[] {
|
function addHighlight(textParts: TextPart[], highlight: string | undefined): TextPart[] {
|
||||||
return textParts.reduce<TextPart[]>((result, part) => {
|
return textParts.reduce<TextPart[]>((result, part) => {
|
||||||
if (typeof part !== 'string' || !highlight) {
|
if (typeof part !== 'string' || !highlight) {
|
||||||
result.push(part);
|
result.push(part);
|
||||||
@ -207,7 +207,7 @@ function addHighlight(textParts: TextPart[], highlight: string | undefined, isQu
|
|||||||
const newParts: TextPart[] = [];
|
const newParts: TextPart[] = [];
|
||||||
newParts.push(part.substring(0, queryPosition));
|
newParts.push(part.substring(0, queryPosition));
|
||||||
newParts.push(
|
newParts.push(
|
||||||
<span className={buildClassName('matching-text-highlight', isQuote && 'is-quote')}>
|
<span className="matching-text-highlight">
|
||||||
{part.substring(queryPosition, queryPosition + highlight.length)}
|
{part.substring(queryPosition, queryPosition + highlight.length)}
|
||||||
</span>,
|
</span>,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -46,7 +46,6 @@ export function renderTextWithEntities({
|
|||||||
cacheBuster,
|
cacheBuster,
|
||||||
forcePlayback,
|
forcePlayback,
|
||||||
noCustomEmojiPlayback,
|
noCustomEmojiPlayback,
|
||||||
focusedQuote,
|
|
||||||
isInSelectMode,
|
isInSelectMode,
|
||||||
chatId,
|
chatId,
|
||||||
messageId,
|
messageId,
|
||||||
@ -69,7 +68,6 @@ export function renderTextWithEntities({
|
|||||||
cacheBuster?: string;
|
cacheBuster?: string;
|
||||||
forcePlayback?: boolean;
|
forcePlayback?: boolean;
|
||||||
noCustomEmojiPlayback?: boolean;
|
noCustomEmojiPlayback?: boolean;
|
||||||
focusedQuote?: string;
|
|
||||||
isInSelectMode?: boolean;
|
isInSelectMode?: boolean;
|
||||||
chatId?: string;
|
chatId?: string;
|
||||||
messageId?: number;
|
messageId?: number;
|
||||||
@ -80,7 +78,6 @@ export function renderTextWithEntities({
|
|||||||
return renderMessagePart({
|
return renderMessagePart({
|
||||||
content: text,
|
content: text,
|
||||||
highlight,
|
highlight,
|
||||||
focusedQuote,
|
|
||||||
emojiSize,
|
emojiSize,
|
||||||
shouldRenderAsHtml,
|
shouldRenderAsHtml,
|
||||||
asPreview,
|
asPreview,
|
||||||
@ -115,7 +112,6 @@ export function renderTextWithEntities({
|
|||||||
renderResult.push(...renderMessagePart({
|
renderResult.push(...renderMessagePart({
|
||||||
content: textBefore,
|
content: textBefore,
|
||||||
highlight,
|
highlight,
|
||||||
focusedQuote,
|
|
||||||
emojiSize,
|
emojiSize,
|
||||||
shouldRenderAsHtml,
|
shouldRenderAsHtml,
|
||||||
asPreview,
|
asPreview,
|
||||||
@ -166,7 +162,6 @@ export function renderTextWithEntities({
|
|||||||
entityContent,
|
entityContent,
|
||||||
nestedEntityContent,
|
nestedEntityContent,
|
||||||
highlight,
|
highlight,
|
||||||
focusedQuote,
|
|
||||||
containerId,
|
containerId,
|
||||||
asPreview,
|
asPreview,
|
||||||
isProtected,
|
isProtected,
|
||||||
@ -203,7 +198,6 @@ export function renderTextWithEntities({
|
|||||||
renderResult.push(...renderMessagePart({
|
renderResult.push(...renderMessagePart({
|
||||||
content: textAfter,
|
content: textAfter,
|
||||||
highlight,
|
highlight,
|
||||||
focusedQuote,
|
|
||||||
emojiSize,
|
emojiSize,
|
||||||
shouldRenderAsHtml,
|
shouldRenderAsHtml,
|
||||||
asPreview,
|
asPreview,
|
||||||
@ -302,14 +296,63 @@ function renderMessagePart({
|
|||||||
filters.push('highlight');
|
filters.push('highlight');
|
||||||
params.highlight = highlight;
|
params.highlight = highlight;
|
||||||
}
|
}
|
||||||
if (focusedQuote) {
|
|
||||||
filters.push('quote');
|
|
||||||
params.quote = focusedQuote;
|
|
||||||
}
|
|
||||||
|
|
||||||
return renderText(content, filters, params);
|
return renderText(content, filters, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function insertTextEntity(entities: ApiMessageEntity[], newEntity: ApiMessageEntity) {
|
||||||
|
const resultEntities: ApiMessageEntity[] = [];
|
||||||
|
|
||||||
|
const newEntityStart = newEntity.offset;
|
||||||
|
const newEntityEnd = newEntity.offset + newEntity.length;
|
||||||
|
|
||||||
|
for (const existingEntity of entities) {
|
||||||
|
const existingEntityStart = existingEntity.offset;
|
||||||
|
const existingEntityEnd = existingEntity.offset + existingEntity.length;
|
||||||
|
// Push as is if edges do not overlap
|
||||||
|
if (existingEntityEnd <= newEntityStart
|
||||||
|
|| existingEntityStart > newEntityEnd
|
||||||
|
|| (existingEntityStart > newEntityStart
|
||||||
|
&& existingEntityEnd < newEntityEnd)) {
|
||||||
|
resultEntities.push(existingEntity);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If start edge overlaps
|
||||||
|
if (existingEntityStart < newEntityStart && existingEntityEnd > newEntityStart) {
|
||||||
|
// Split entity in two
|
||||||
|
resultEntities.push({
|
||||||
|
...existingEntity,
|
||||||
|
length: newEntityStart - existingEntityStart,
|
||||||
|
});
|
||||||
|
resultEntities.push({
|
||||||
|
...existingEntity,
|
||||||
|
offset: newEntityStart,
|
||||||
|
length: existingEntityEnd - newEntityStart,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// If end edge overlaps
|
||||||
|
if (existingEntityStart < newEntityEnd
|
||||||
|
&& existingEntityEnd > newEntityEnd) {
|
||||||
|
// Split entity in two
|
||||||
|
resultEntities.push({
|
||||||
|
...existingEntity,
|
||||||
|
offset: newEntityEnd,
|
||||||
|
length: existingEntityEnd - newEntityStart - newEntity.length,
|
||||||
|
});
|
||||||
|
resultEntities.push({
|
||||||
|
...existingEntity,
|
||||||
|
length: newEntityEnd - existingEntityStart,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resultEntities.push(newEntity);
|
||||||
|
// Sort entities by offset, longer entities first
|
||||||
|
return resultEntities.sort((a, b) => a.offset - b.offset || b.length - a.length);
|
||||||
|
}
|
||||||
|
|
||||||
// Organize entities in a tree-like structure to better represent how the text will be displayed
|
// Organize entities in a tree-like structure to better represent how the text will be displayed
|
||||||
function organizeEntities(entities: ApiMessageEntity[]) {
|
function organizeEntities(entities: ApiMessageEntity[]) {
|
||||||
const organizedEntityIndexes: Set<number> = new Set();
|
const organizedEntityIndexes: Set<number> = new Set();
|
||||||
@ -381,7 +424,6 @@ function processEntity({
|
|||||||
entityContent,
|
entityContent,
|
||||||
nestedEntityContent,
|
nestedEntityContent,
|
||||||
highlight,
|
highlight,
|
||||||
focusedQuote,
|
|
||||||
containerId,
|
containerId,
|
||||||
asPreview,
|
asPreview,
|
||||||
isProtected,
|
isProtected,
|
||||||
@ -430,7 +472,6 @@ function processEntity({
|
|||||||
return renderMessagePart({
|
return renderMessagePart({
|
||||||
content: renderedContent,
|
content: renderedContent,
|
||||||
highlight,
|
highlight,
|
||||||
focusedQuote,
|
|
||||||
emojiSize,
|
emojiSize,
|
||||||
asPreview,
|
asPreview,
|
||||||
});
|
});
|
||||||
@ -613,6 +654,10 @@ function processEntity({
|
|||||||
noPlay={noCustomEmojiPlayback}
|
noPlay={noCustomEmojiPlayback}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
case ApiMessageEntityTypes.QuoteFocus:
|
||||||
|
return (
|
||||||
|
<span className="matching-text-highlight is-quote">{renderNestedMessagePart()}</span>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return renderNestedMessagePart();
|
return renderNestedMessagePart();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -159,6 +159,7 @@ type StateProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const selection = window.getSelection();
|
const selection = window.getSelection();
|
||||||
|
const UNQUOTABLE_OFFSET = -1;
|
||||||
|
|
||||||
const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||||
threadId,
|
threadId,
|
||||||
@ -272,7 +273,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
const [isMenuOpen, setIsMenuOpen] = useState(true);
|
const [isMenuOpen, setIsMenuOpen] = useState(true);
|
||||||
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 [selectionQuoteOffset, setSelectionQuoteOffset] = useState(UNQUOTABLE_OFFSET);
|
||||||
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
|
||||||
@ -349,7 +350,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isMessageTranslated) {
|
if (isMessageTranslated) {
|
||||||
setCanQuoteSelection(false);
|
setSelectionQuoteOffset(UNQUOTABLE_OFFSET);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -359,7 +360,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
&& isSelectionRangeInsideMessage(selectionRange);
|
&& isSelectionRangeInsideMessage(selectionRange);
|
||||||
|
|
||||||
if (!isMessageTextSelected) {
|
if (!isMessageTextSelected) {
|
||||||
setCanQuoteSelection(false);
|
setSelectionQuoteOffset(UNQUOTABLE_OFFSET);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -367,10 +368,14 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
const messageText = message.content.text!.text!.replace(/\u00A0/g, ' ');
|
const messageText = message.content.text!.text!.replace(/\u00A0/g, ' ');
|
||||||
|
|
||||||
setCanQuoteSelection(
|
const canQuote = selectionText.text.trim().length > 0
|
||||||
selectionText.text.trim().length > 0
|
&& messageText.includes(selectionText.text);
|
||||||
&& messageText.includes(selectionText.text),
|
if (!canQuote) {
|
||||||
);
|
setSelectionQuoteOffset(UNQUOTABLE_OFFSET);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectionQuoteOffset(selectionRange.startOffset);
|
||||||
}, [
|
}, [
|
||||||
selectionRange, selectionRange?.collapsed, selectionRange?.startOffset, selectionRange?.endOffset,
|
selectionRange, selectionRange?.collapsed, selectionRange?.startOffset, selectionRange?.endOffset,
|
||||||
isMessageTranslated, message.content.text,
|
isMessageTranslated, message.content.text,
|
||||||
@ -400,13 +405,17 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleReply = useLastCallback(() => {
|
const handleReply = useLastCallback(() => {
|
||||||
const quoteText = canQuoteSelection && selectionRange ? getSelectionAsFormattedText(selectionRange) : undefined;
|
const quoteText = selectionQuoteOffset !== UNQUOTABLE_OFFSET && selectionRange
|
||||||
|
? getSelectionAsFormattedText(selectionRange) : undefined;
|
||||||
if (!canReplyInChat) {
|
if (!canReplyInChat) {
|
||||||
openReplyMenu({ fromChatId: message.chatId, messageId: message.id, quoteText });
|
openReplyMenu({
|
||||||
|
fromChatId: message.chatId, messageId: message.id, quoteText, quoteOffset: selectionQuoteOffset,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
updateDraftReplyInfo({
|
updateDraftReplyInfo({
|
||||||
replyToMsgId: message.id,
|
replyToMsgId: message.id,
|
||||||
quoteText,
|
quoteText,
|
||||||
|
quoteOffset: selectionQuoteOffset,
|
||||||
replyToPeerId: undefined,
|
replyToPeerId: undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -647,7 +656,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
canSendNow={canSendNow}
|
canSendNow={canSendNow}
|
||||||
canReschedule={canReschedule}
|
canReschedule={canReschedule}
|
||||||
canReply={canReply}
|
canReply={canReply}
|
||||||
canQuote={canQuoteSelection}
|
canQuote={selectionQuoteOffset !== UNQUOTABLE_OFFSET}
|
||||||
canDelete={canDelete}
|
canDelete={canDelete}
|
||||||
canPin={canPin}
|
canPin={canPin}
|
||||||
canReport={canReport}
|
canReport={canReport}
|
||||||
|
|||||||
@ -245,6 +245,7 @@ type StateProps = {
|
|||||||
isFocused?: boolean;
|
isFocused?: boolean;
|
||||||
focusDirection?: FocusDirection;
|
focusDirection?: FocusDirection;
|
||||||
focusedQuote?: string;
|
focusedQuote?: string;
|
||||||
|
focusedQuoteOffset?: number;
|
||||||
noFocusHighlight?: boolean;
|
noFocusHighlight?: boolean;
|
||||||
scrollTargetPosition?: ScrollTargetPosition;
|
scrollTargetPosition?: ScrollTargetPosition;
|
||||||
isResizingContainer?: boolean;
|
isResizingContainer?: boolean;
|
||||||
@ -368,6 +369,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
isFocused,
|
isFocused,
|
||||||
focusDirection,
|
focusDirection,
|
||||||
focusedQuote,
|
focusedQuote,
|
||||||
|
focusedQuoteOffset,
|
||||||
noFocusHighlight,
|
noFocusHighlight,
|
||||||
scrollTargetPosition,
|
scrollTargetPosition,
|
||||||
isResizingContainer,
|
isResizingContainer,
|
||||||
@ -971,6 +973,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
translatedText={requestedTranslationLanguage ? currentTranslatedText : undefined}
|
translatedText={requestedTranslationLanguage ? currentTranslatedText : undefined}
|
||||||
isForAnimation={isForAnimation}
|
isForAnimation={isForAnimation}
|
||||||
focusedQuote={focusedQuote}
|
focusedQuote={focusedQuote}
|
||||||
|
focusedQuoteOffset={focusedQuoteOffset}
|
||||||
emojiSize={emojiSize}
|
emojiSize={emojiSize}
|
||||||
highlight={highlight}
|
highlight={highlight}
|
||||||
isProtected={isProtected}
|
isProtected={isProtected}
|
||||||
@ -1788,7 +1791,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
direction: focusDirection, noHighlight: noFocusHighlight, isResizingContainer,
|
direction: focusDirection, noHighlight: noFocusHighlight, isResizingContainer,
|
||||||
quote: focusedQuote, scrollTargetPosition,
|
quote: focusedQuote, quoteOffset: focusedQuoteOffset, scrollTargetPosition,
|
||||||
} = (isFocused && focusedMessage) || {};
|
} = (isFocused && focusedMessage) || {};
|
||||||
|
|
||||||
const middleSearch = selectCurrentMiddleSearch(global);
|
const middleSearch = selectCurrentMiddleSearch(global);
|
||||||
@ -1935,6 +1938,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
noFocusHighlight,
|
noFocusHighlight,
|
||||||
isResizingContainer,
|
isResizingContainer,
|
||||||
focusedQuote,
|
focusedQuote,
|
||||||
|
focusedQuoteOffset,
|
||||||
scrollTargetPosition,
|
scrollTargetPosition,
|
||||||
}),
|
}),
|
||||||
senderBoosts,
|
senderBoosts,
|
||||||
|
|||||||
@ -65,7 +65,7 @@ export default function useInnerHandlers({
|
|||||||
} = message;
|
} = message;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
replyToMsgId, replyToPeerId, replyToTopId, isQuote, quoteText,
|
replyToMsgId, replyToPeerId, replyToTopId, isQuote, quoteText, quoteOffset,
|
||||||
} = getMessageReplyInfo(message) || {};
|
} = getMessageReplyInfo(message) || {};
|
||||||
|
|
||||||
const handleSenderClick = useLastCallback(() => {
|
const handleSenderClick = useLastCallback(() => {
|
||||||
@ -114,7 +114,7 @@ export default function useInnerHandlers({
|
|||||||
messageId: replyToMsgId,
|
messageId: replyToMsgId,
|
||||||
replyMessageId: replyToPeerId ? undefined : messageId,
|
replyMessageId: replyToPeerId ? undefined : messageId,
|
||||||
noForumTopicPanel: !replyToPeerId, // Open topic panel for cross-chat replies
|
noForumTopicPanel: !replyToPeerId, // Open topic panel for cross-chat replies
|
||||||
...(isQuote && { quote: quoteText?.text }),
|
...(isQuote && { quote: quoteText?.text, quoteOffset }),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -138,7 +138,9 @@ export default function useOuterHandlers(
|
|||||||
function handleContainerDoubleClick() {
|
function handleContainerDoubleClick() {
|
||||||
if (IS_TOUCH_ENV || !canReply) return;
|
if (IS_TOUCH_ENV || !canReply) return;
|
||||||
|
|
||||||
updateDraftReplyInfo({ replyToMsgId: messageId, replyToPeerId: undefined, quoteText: undefined });
|
updateDraftReplyInfo({
|
||||||
|
replyToMsgId: messageId, replyToPeerId: undefined, quoteText: undefined, quoteOffset: undefined,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopPropagation(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
|
function stopPropagation(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
|
||||||
|
|||||||
@ -2145,6 +2145,7 @@ addActionHandler('openChatOrTopicWithReplyInDraft', (global, actions, payload):
|
|||||||
replyToTopId: replyingInfo.toThreadId,
|
replyToTopId: replyingInfo.toThreadId,
|
||||||
replyToPeerId: currentChatId,
|
replyToPeerId: currentChatId,
|
||||||
quoteText: replyingInfo.quoteText,
|
quoteText: replyingInfo.quoteText,
|
||||||
|
quoteOffset: replyingInfo.quoteOffset,
|
||||||
} as ApiInputMessageReplyInfo;
|
} as ApiInputMessageReplyInfo;
|
||||||
|
|
||||||
const currentReplyInfo = replyingInfo.messageId
|
const currentReplyInfo = replyingInfo.messageId
|
||||||
|
|||||||
@ -405,8 +405,8 @@ addActionHandler('focusNextReply', (global, actions, payload): ActionReturnType
|
|||||||
addActionHandler('focusMessage', (global, actions, payload): ActionReturnType => {
|
addActionHandler('focusMessage', (global, actions, payload): ActionReturnType => {
|
||||||
const {
|
const {
|
||||||
chatId, threadId = MAIN_THREAD_ID, messageListType = 'thread', noHighlight, groupedId, groupedChatId,
|
chatId, threadId = MAIN_THREAD_ID, messageListType = 'thread', noHighlight, groupedId, groupedChatId,
|
||||||
replyMessageId, isResizingContainer, shouldReplaceHistory, noForumTopicPanel, quote, scrollTargetPosition,
|
replyMessageId, isResizingContainer, shouldReplaceHistory, noForumTopicPanel, quote, quoteOffset,
|
||||||
timestamp, tabId = getCurrentTabId(),
|
scrollTargetPosition, timestamp, tabId = getCurrentTabId(),
|
||||||
} = payload;
|
} = payload;
|
||||||
|
|
||||||
let { messageId } = payload;
|
let { messageId } = payload;
|
||||||
@ -455,6 +455,7 @@ addActionHandler('focusMessage', (global, actions, payload): ActionReturnType =>
|
|||||||
noHighlight,
|
noHighlight,
|
||||||
isResizingContainer,
|
isResizingContainer,
|
||||||
quote,
|
quote,
|
||||||
|
quoteOffset,
|
||||||
scrollTargetPosition,
|
scrollTargetPosition,
|
||||||
}, tabId);
|
}, tabId);
|
||||||
global = updateFocusDirection(global, undefined, tabId);
|
global = updateFocusDirection(global, undefined, tabId);
|
||||||
@ -525,13 +526,14 @@ addActionHandler('setShouldPreventComposerAnimation', (global, actions, payload)
|
|||||||
|
|
||||||
addActionHandler('openReplyMenu', (global, actions, payload): ActionReturnType => {
|
addActionHandler('openReplyMenu', (global, actions, payload): ActionReturnType => {
|
||||||
const {
|
const {
|
||||||
fromChatId, messageId, quoteText, tabId = getCurrentTabId(),
|
fromChatId, messageId, quoteText, quoteOffset, tabId = getCurrentTabId(),
|
||||||
} = payload;
|
} = payload;
|
||||||
return updateTabState(global, {
|
return updateTabState(global, {
|
||||||
replyingMessage: {
|
replyingMessage: {
|
||||||
fromChatId,
|
fromChatId,
|
||||||
messageId,
|
messageId,
|
||||||
quoteText,
|
quoteText,
|
||||||
|
quoteOffset,
|
||||||
},
|
},
|
||||||
isShareMessageModalShown: true,
|
isShareMessageModalShown: true,
|
||||||
}, tabId);
|
}, tabId);
|
||||||
|
|||||||
@ -726,6 +726,7 @@ export function updateFocusedMessage<T extends GlobalState>(
|
|||||||
noHighlight = false,
|
noHighlight = false,
|
||||||
isResizingContainer = false,
|
isResizingContainer = false,
|
||||||
quote,
|
quote,
|
||||||
|
quoteOffset,
|
||||||
scrollTargetPosition,
|
scrollTargetPosition,
|
||||||
}: {
|
}: {
|
||||||
global: T;
|
global: T;
|
||||||
@ -735,6 +736,7 @@ export function updateFocusedMessage<T extends GlobalState>(
|
|||||||
noHighlight?: boolean;
|
noHighlight?: boolean;
|
||||||
isResizingContainer?: boolean;
|
isResizingContainer?: boolean;
|
||||||
quote?: string;
|
quote?: string;
|
||||||
|
quoteOffset?: number;
|
||||||
scrollTargetPosition?: ScrollTargetPosition;
|
scrollTargetPosition?: ScrollTargetPosition;
|
||||||
},
|
},
|
||||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||||
@ -748,6 +750,7 @@ export function updateFocusedMessage<T extends GlobalState>(
|
|||||||
noHighlight,
|
noHighlight,
|
||||||
isResizingContainer,
|
isResizingContainer,
|
||||||
quote,
|
quote,
|
||||||
|
quoteOffset,
|
||||||
scrollTargetPosition,
|
scrollTargetPosition,
|
||||||
},
|
},
|
||||||
}, tabId);
|
}, tabId);
|
||||||
|
|||||||
@ -956,6 +956,7 @@ export interface ActionPayloads {
|
|||||||
shouldReplaceHistory?: boolean;
|
shouldReplaceHistory?: boolean;
|
||||||
noForumTopicPanel?: boolean;
|
noForumTopicPanel?: boolean;
|
||||||
quote?: string;
|
quote?: string;
|
||||||
|
quoteOffset?: number;
|
||||||
scrollTargetPosition?: ScrollTargetPosition;
|
scrollTargetPosition?: ScrollTargetPosition;
|
||||||
timestamp?: number;
|
timestamp?: number;
|
||||||
} & WithTabId;
|
} & WithTabId;
|
||||||
@ -1769,6 +1770,7 @@ export interface ActionPayloads {
|
|||||||
fromChatId: string;
|
fromChatId: string;
|
||||||
messageId?: number;
|
messageId?: number;
|
||||||
quoteText?: ApiFormattedText;
|
quoteText?: ApiFormattedText;
|
||||||
|
quoteOffset?: number;
|
||||||
} & WithTabId;
|
} & WithTabId;
|
||||||
|
|
||||||
// Forwards
|
// Forwards
|
||||||
|
|||||||
@ -156,6 +156,7 @@ export type TabState = {
|
|||||||
noHighlight?: boolean;
|
noHighlight?: boolean;
|
||||||
isResizingContainer?: boolean;
|
isResizingContainer?: boolean;
|
||||||
quote?: string;
|
quote?: string;
|
||||||
|
quoteOffset?: number;
|
||||||
scrollTargetPosition?: ScrollTargetPosition;
|
scrollTargetPosition?: ScrollTargetPosition;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -352,6 +353,7 @@ export type TabState = {
|
|||||||
fromChatId?: string;
|
fromChatId?: string;
|
||||||
messageId?: number;
|
messageId?: number;
|
||||||
quoteText?: ApiFormattedText;
|
quoteText?: ApiFormattedText;
|
||||||
|
quoteOffset?: number;
|
||||||
toChatId?: string;
|
toChatId?: string;
|
||||||
toThreadId?: ThreadId;
|
toThreadId?: ThreadId;
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user