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,
|
||||
replyToPeerId: replyTo.replyToPeerId && getApiChatIdFromMtpPeer(replyTo.replyToPeerId),
|
||||
quoteText: replyTo.quoteText ? buildMessageTextContent(replyTo.quoteText, replyTo.quoteEntities) : undefined,
|
||||
quoteOffset: replyTo.quoteOffset,
|
||||
} satisfies ApiInputMessageReplyInfo : undefined;
|
||||
|
||||
return {
|
||||
@ -340,6 +341,7 @@ function buildApiReplyInfo(
|
||||
quote,
|
||||
quoteText,
|
||||
quoteEntities,
|
||||
quoteOffset,
|
||||
} = replyHeader;
|
||||
|
||||
return {
|
||||
@ -352,6 +354,7 @@ function buildApiReplyInfo(
|
||||
replyMedia: replyMedia && buildMessageMediaContent(replyMedia, context),
|
||||
isQuote: quote,
|
||||
quoteText: quoteText ? buildMessageTextContent(quoteText, quoteEntities) : undefined,
|
||||
quoteOffset,
|
||||
};
|
||||
}
|
||||
|
||||
@ -554,6 +557,7 @@ function buildReplyInfo(inputInfo: ApiInputReplyInfo, isForum?: boolean): ApiRep
|
||||
replyToTopId: inputInfo.replyToTopId,
|
||||
replyToPeerId: inputInfo.replyToPeerId,
|
||||
quoteText: inputInfo.quoteText,
|
||||
quoteOffset: inputInfo.quoteOffset,
|
||||
isForumTopic: isForum && inputInfo.replyToTopId ? true : undefined,
|
||||
...(Boolean(inputInfo.quoteText) && { isQuote: true }),
|
||||
};
|
||||
|
||||
@ -805,7 +805,7 @@ export function buildInputReplyTo(replyInfo: ApiInputReplyInfo) {
|
||||
|
||||
if (replyInfo.type === 'message') {
|
||||
const {
|
||||
replyToMsgId, replyToTopId, replyToPeerId, quoteText,
|
||||
replyToMsgId, replyToTopId, replyToPeerId, quoteText, quoteOffset,
|
||||
} = replyInfo;
|
||||
return new GramJs.InputReplyToMessage({
|
||||
replyToMsgId,
|
||||
@ -813,6 +813,7 @@ export function buildInputReplyTo(replyInfo: ApiInputReplyInfo) {
|
||||
replyToPeerId: replyToPeerId ? buildInputPeerFromLocalDb(replyToPeerId)! : undefined,
|
||||
quoteText: quoteText?.text,
|
||||
quoteEntities: quoteText?.entities?.map(buildMtpMessageEntity),
|
||||
quoteOffset,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -365,6 +365,7 @@ export interface ApiMessageReplyInfo {
|
||||
isForumTopic?: true;
|
||||
isQuote?: true;
|
||||
quoteText?: ApiFormattedText;
|
||||
quoteOffset?: number;
|
||||
}
|
||||
|
||||
export interface ApiStoryReplyInfo {
|
||||
@ -379,6 +380,7 @@ export interface ApiInputMessageReplyInfo {
|
||||
replyToTopId?: number;
|
||||
replyToPeerId?: string;
|
||||
quoteText?: ApiFormattedText;
|
||||
quoteOffset?: number;
|
||||
}
|
||||
|
||||
export interface ApiInputStoryReplyInfo {
|
||||
@ -457,6 +459,7 @@ export type ApiMessageEntityCustomEmoji = {
|
||||
documentId: string;
|
||||
};
|
||||
|
||||
// Local entities
|
||||
export type ApiMessageEntityTimestamp = {
|
||||
type: ApiMessageEntityTypes.Timestamp;
|
||||
offset: number;
|
||||
@ -464,8 +467,15 @@ export type ApiMessageEntityTimestamp = {
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
export type ApiMessageEntityQuoteFocus = {
|
||||
type: 'quoteFocus';
|
||||
offset: number;
|
||||
length: number;
|
||||
};
|
||||
|
||||
export type ApiMessageEntity = ApiMessageEntityDefault | ApiMessageEntityPre | ApiMessageEntityTextUrl |
|
||||
ApiMessageEntityMentionName | ApiMessageEntityCustomEmoji | ApiMessageEntityBlockquote | ApiMessageEntityTimestamp;
|
||||
ApiMessageEntityMentionName | ApiMessageEntityCustomEmoji | ApiMessageEntityBlockquote | ApiMessageEntityTimestamp |
|
||||
ApiMessageEntityQuoteFocus;
|
||||
|
||||
export enum ApiMessageEntityTypes {
|
||||
Bold = 'MessageEntityBold',
|
||||
@ -487,6 +497,7 @@ export enum ApiMessageEntityTypes {
|
||||
Spoiler = 'MessageEntitySpoiler',
|
||||
CustomEmoji = 'MessageEntityCustomEmoji',
|
||||
Timestamp = 'MessageEntityTimestamp',
|
||||
QuoteFocus = 'MessageEntityQuoteFocus',
|
||||
Unknown = 'MessageEntityUnknown',
|
||||
}
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ import { ApiMessageEntityTypes } from '../../api/types';
|
||||
import { CONTENT_NOT_SUPPORTED } from '../../config';
|
||||
import { extractMessageText, stripCustomEmoji } from '../../global/helpers';
|
||||
import trimText from '../../util/trimText';
|
||||
import { renderTextWithEntities } from './helpers/renderTextWithEntities';
|
||||
import { insertTextEntity, renderTextWithEntities } from './helpers/renderTextWithEntities';
|
||||
|
||||
import useSyncEffect from '../../hooks/useSyncEffect';
|
||||
import useUniqueId from '../../hooks/useUniqueId';
|
||||
@ -32,6 +32,7 @@ interface OwnProps {
|
||||
inChatList?: boolean;
|
||||
forcePlayback?: boolean;
|
||||
focusedQuote?: string;
|
||||
focusedQuoteOffset?: number;
|
||||
isInSelectMode?: boolean;
|
||||
canBeEmpty?: boolean;
|
||||
maxTimestamp?: number;
|
||||
@ -55,6 +56,7 @@ function MessageText({
|
||||
inChatList,
|
||||
forcePlayback,
|
||||
focusedQuote,
|
||||
focusedQuoteOffset,
|
||||
isInSelectMode,
|
||||
canBeEmpty,
|
||||
maxTimestamp,
|
||||
@ -71,21 +73,38 @@ function MessageText({
|
||||
const adaptedFormattedText = isForAnimation && formattedText ? stripCustomEmoji(formattedText) : formattedText;
|
||||
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();
|
||||
|
||||
useSyncEffect(() => {
|
||||
textCacheBusterRef.current += 1;
|
||||
}, [text, entities]);
|
||||
}, [text, entitiesWithFocusedQuote]);
|
||||
|
||||
const withSharedCanvas = useMemo(() => {
|
||||
const hasSpoilers = entities?.some((e) => e.type === ApiMessageEntityTypes.Spoiler);
|
||||
const hasSpoilers = entitiesWithFocusedQuote?.some((e) => e.type === ApiMessageEntityTypes.Spoiler);
|
||||
if (hasSpoilers) {
|
||||
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;
|
||||
}, [entities]) || 0;
|
||||
}, [entitiesWithFocusedQuote]) || 0;
|
||||
|
||||
if (!text && !canBeEmpty) {
|
||||
return <span className="content-unsupported">{CONTENT_NOT_SUPPORTED}</span>;
|
||||
@ -98,7 +117,7 @@ function MessageText({
|
||||
withSharedCanvas && <canvas ref={sharedCanvasHqRef} className="shared-canvas" />,
|
||||
renderTextWithEntities({
|
||||
text: trimText(text!, truncateLength),
|
||||
entities,
|
||||
entities: entitiesWithFocusedQuote,
|
||||
highlight,
|
||||
emojiSize,
|
||||
shouldRenderAsHtml,
|
||||
@ -112,7 +131,6 @@ function MessageText({
|
||||
sharedCanvasHqRef,
|
||||
cacheBuster: textCacheBusterRef.current.toString(),
|
||||
forcePlayback,
|
||||
focusedQuote,
|
||||
isInSelectMode,
|
||||
maxTimestamp,
|
||||
chatId: 'chatId' in messageOrStory ? messageOrStory.chatId : undefined,
|
||||
|
||||
@ -23,7 +23,7 @@ import SafeLink from '../SafeLink';
|
||||
|
||||
export type TextFilter = (
|
||||
'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;
|
||||
@ -31,7 +31,10 @@ const SIMPLE_MARKDOWN_REGEX = /(\*\*|__).+?\1/g;
|
||||
export default function renderText(
|
||||
part: TextPart,
|
||||
filters: Array<TextFilter> = ['emoji'],
|
||||
params?: { highlight?: string; quote?: string; markdownPostProcessor?: (part: string) => TeactNode },
|
||||
params?: {
|
||||
highlight?: string;
|
||||
markdownPostProcessor?: (part: string) => TeactNode;
|
||||
},
|
||||
): TeactNode[] {
|
||||
if (typeof part !== 'string') {
|
||||
return [part];
|
||||
@ -63,9 +66,6 @@ export default function renderText(
|
||||
case 'highlight':
|
||||
return addHighlight(text, params!.highlight);
|
||||
|
||||
case 'quote':
|
||||
return addHighlight(text, params!.quote, true);
|
||||
|
||||
case 'links':
|
||||
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) => {
|
||||
if (typeof part !== 'string' || !highlight) {
|
||||
result.push(part);
|
||||
@ -207,7 +207,7 @@ function addHighlight(textParts: TextPart[], highlight: string | undefined, isQu
|
||||
const newParts: TextPart[] = [];
|
||||
newParts.push(part.substring(0, queryPosition));
|
||||
newParts.push(
|
||||
<span className={buildClassName('matching-text-highlight', isQuote && 'is-quote')}>
|
||||
<span className="matching-text-highlight">
|
||||
{part.substring(queryPosition, queryPosition + highlight.length)}
|
||||
</span>,
|
||||
);
|
||||
|
||||
@ -46,7 +46,6 @@ export function renderTextWithEntities({
|
||||
cacheBuster,
|
||||
forcePlayback,
|
||||
noCustomEmojiPlayback,
|
||||
focusedQuote,
|
||||
isInSelectMode,
|
||||
chatId,
|
||||
messageId,
|
||||
@ -69,7 +68,6 @@ export function renderTextWithEntities({
|
||||
cacheBuster?: string;
|
||||
forcePlayback?: boolean;
|
||||
noCustomEmojiPlayback?: boolean;
|
||||
focusedQuote?: string;
|
||||
isInSelectMode?: boolean;
|
||||
chatId?: string;
|
||||
messageId?: number;
|
||||
@ -80,7 +78,6 @@ export function renderTextWithEntities({
|
||||
return renderMessagePart({
|
||||
content: text,
|
||||
highlight,
|
||||
focusedQuote,
|
||||
emojiSize,
|
||||
shouldRenderAsHtml,
|
||||
asPreview,
|
||||
@ -115,7 +112,6 @@ export function renderTextWithEntities({
|
||||
renderResult.push(...renderMessagePart({
|
||||
content: textBefore,
|
||||
highlight,
|
||||
focusedQuote,
|
||||
emojiSize,
|
||||
shouldRenderAsHtml,
|
||||
asPreview,
|
||||
@ -166,7 +162,6 @@ export function renderTextWithEntities({
|
||||
entityContent,
|
||||
nestedEntityContent,
|
||||
highlight,
|
||||
focusedQuote,
|
||||
containerId,
|
||||
asPreview,
|
||||
isProtected,
|
||||
@ -203,7 +198,6 @@ export function renderTextWithEntities({
|
||||
renderResult.push(...renderMessagePart({
|
||||
content: textAfter,
|
||||
highlight,
|
||||
focusedQuote,
|
||||
emojiSize,
|
||||
shouldRenderAsHtml,
|
||||
asPreview,
|
||||
@ -302,14 +296,63 @@ function renderMessagePart({
|
||||
filters.push('highlight');
|
||||
params.highlight = highlight;
|
||||
}
|
||||
if (focusedQuote) {
|
||||
filters.push('quote');
|
||||
params.quote = focusedQuote;
|
||||
}
|
||||
|
||||
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
|
||||
function organizeEntities(entities: ApiMessageEntity[]) {
|
||||
const organizedEntityIndexes: Set<number> = new Set();
|
||||
@ -381,7 +424,6 @@ function processEntity({
|
||||
entityContent,
|
||||
nestedEntityContent,
|
||||
highlight,
|
||||
focusedQuote,
|
||||
containerId,
|
||||
asPreview,
|
||||
isProtected,
|
||||
@ -430,7 +472,6 @@ function processEntity({
|
||||
return renderMessagePart({
|
||||
content: renderedContent,
|
||||
highlight,
|
||||
focusedQuote,
|
||||
emojiSize,
|
||||
asPreview,
|
||||
});
|
||||
@ -613,6 +654,10 @@ function processEntity({
|
||||
noPlay={noCustomEmojiPlayback}
|
||||
/>
|
||||
);
|
||||
case ApiMessageEntityTypes.QuoteFocus:
|
||||
return (
|
||||
<span className="matching-text-highlight is-quote">{renderNestedMessagePart()}</span>
|
||||
);
|
||||
default:
|
||||
return renderNestedMessagePart();
|
||||
}
|
||||
|
||||
@ -159,6 +159,7 @@ type StateProps = {
|
||||
};
|
||||
|
||||
const selection = window.getSelection();
|
||||
const UNQUOTABLE_OFFSET = -1;
|
||||
|
||||
const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
threadId,
|
||||
@ -272,7 +273,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(true);
|
||||
const [isPinModalOpen, setIsPinModalOpen] = useState(false);
|
||||
const [isClosePollDialogOpen, openClosePollDialog, closeClosePollDialog] = useFlag();
|
||||
const [canQuoteSelection, setCanQuoteSelection] = useState(false);
|
||||
const [selectionQuoteOffset, setSelectionQuoteOffset] = useState(UNQUOTABLE_OFFSET);
|
||||
const [requestCalendar, calendar] = useSchedule(canScheduleUntilOnline, onClose, message.date);
|
||||
|
||||
// `undefined` indicates that emoji are present and loading
|
||||
@ -349,7 +350,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (isMessageTranslated) {
|
||||
setCanQuoteSelection(false);
|
||||
setSelectionQuoteOffset(UNQUOTABLE_OFFSET);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -359,7 +360,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
&& isSelectionRangeInsideMessage(selectionRange);
|
||||
|
||||
if (!isMessageTextSelected) {
|
||||
setCanQuoteSelection(false);
|
||||
setSelectionQuoteOffset(UNQUOTABLE_OFFSET);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -367,10 +368,14 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const messageText = message.content.text!.text!.replace(/\u00A0/g, ' ');
|
||||
|
||||
setCanQuoteSelection(
|
||||
selectionText.text.trim().length > 0
|
||||
&& messageText.includes(selectionText.text),
|
||||
);
|
||||
const canQuote = selectionText.text.trim().length > 0
|
||||
&& messageText.includes(selectionText.text);
|
||||
if (!canQuote) {
|
||||
setSelectionQuoteOffset(UNQUOTABLE_OFFSET);
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectionQuoteOffset(selectionRange.startOffset);
|
||||
}, [
|
||||
selectionRange, selectionRange?.collapsed, selectionRange?.startOffset, selectionRange?.endOffset,
|
||||
isMessageTranslated, message.content.text,
|
||||
@ -400,13 +405,17 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
});
|
||||
|
||||
const handleReply = useLastCallback(() => {
|
||||
const quoteText = canQuoteSelection && selectionRange ? getSelectionAsFormattedText(selectionRange) : undefined;
|
||||
const quoteText = selectionQuoteOffset !== UNQUOTABLE_OFFSET && selectionRange
|
||||
? getSelectionAsFormattedText(selectionRange) : undefined;
|
||||
if (!canReplyInChat) {
|
||||
openReplyMenu({ fromChatId: message.chatId, messageId: message.id, quoteText });
|
||||
openReplyMenu({
|
||||
fromChatId: message.chatId, messageId: message.id, quoteText, quoteOffset: selectionQuoteOffset,
|
||||
});
|
||||
} else {
|
||||
updateDraftReplyInfo({
|
||||
replyToMsgId: message.id,
|
||||
quoteText,
|
||||
quoteOffset: selectionQuoteOffset,
|
||||
replyToPeerId: undefined,
|
||||
});
|
||||
}
|
||||
@ -647,7 +656,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
canSendNow={canSendNow}
|
||||
canReschedule={canReschedule}
|
||||
canReply={canReply}
|
||||
canQuote={canQuoteSelection}
|
||||
canQuote={selectionQuoteOffset !== UNQUOTABLE_OFFSET}
|
||||
canDelete={canDelete}
|
||||
canPin={canPin}
|
||||
canReport={canReport}
|
||||
|
||||
@ -245,6 +245,7 @@ type StateProps = {
|
||||
isFocused?: boolean;
|
||||
focusDirection?: FocusDirection;
|
||||
focusedQuote?: string;
|
||||
focusedQuoteOffset?: number;
|
||||
noFocusHighlight?: boolean;
|
||||
scrollTargetPosition?: ScrollTargetPosition;
|
||||
isResizingContainer?: boolean;
|
||||
@ -368,6 +369,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
isFocused,
|
||||
focusDirection,
|
||||
focusedQuote,
|
||||
focusedQuoteOffset,
|
||||
noFocusHighlight,
|
||||
scrollTargetPosition,
|
||||
isResizingContainer,
|
||||
@ -971,6 +973,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
translatedText={requestedTranslationLanguage ? currentTranslatedText : undefined}
|
||||
isForAnimation={isForAnimation}
|
||||
focusedQuote={focusedQuote}
|
||||
focusedQuoteOffset={focusedQuoteOffset}
|
||||
emojiSize={emojiSize}
|
||||
highlight={highlight}
|
||||
isProtected={isProtected}
|
||||
@ -1788,7 +1791,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
|
||||
const {
|
||||
direction: focusDirection, noHighlight: noFocusHighlight, isResizingContainer,
|
||||
quote: focusedQuote, scrollTargetPosition,
|
||||
quote: focusedQuote, quoteOffset: focusedQuoteOffset, scrollTargetPosition,
|
||||
} = (isFocused && focusedMessage) || {};
|
||||
|
||||
const middleSearch = selectCurrentMiddleSearch(global);
|
||||
@ -1935,6 +1938,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
noFocusHighlight,
|
||||
isResizingContainer,
|
||||
focusedQuote,
|
||||
focusedQuoteOffset,
|
||||
scrollTargetPosition,
|
||||
}),
|
||||
senderBoosts,
|
||||
|
||||
@ -65,7 +65,7 @@ export default function useInnerHandlers({
|
||||
} = message;
|
||||
|
||||
const {
|
||||
replyToMsgId, replyToPeerId, replyToTopId, isQuote, quoteText,
|
||||
replyToMsgId, replyToPeerId, replyToTopId, isQuote, quoteText, quoteOffset,
|
||||
} = getMessageReplyInfo(message) || {};
|
||||
|
||||
const handleSenderClick = useLastCallback(() => {
|
||||
@ -114,7 +114,7 @@ export default function useInnerHandlers({
|
||||
messageId: replyToMsgId,
|
||||
replyMessageId: replyToPeerId ? undefined : messageId,
|
||||
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() {
|
||||
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>) {
|
||||
|
||||
@ -2145,6 +2145,7 @@ addActionHandler('openChatOrTopicWithReplyInDraft', (global, actions, payload):
|
||||
replyToTopId: replyingInfo.toThreadId,
|
||||
replyToPeerId: currentChatId,
|
||||
quoteText: replyingInfo.quoteText,
|
||||
quoteOffset: replyingInfo.quoteOffset,
|
||||
} as ApiInputMessageReplyInfo;
|
||||
|
||||
const currentReplyInfo = replyingInfo.messageId
|
||||
|
||||
@ -405,8 +405,8 @@ addActionHandler('focusNextReply', (global, actions, payload): ActionReturnType
|
||||
addActionHandler('focusMessage', (global, actions, payload): ActionReturnType => {
|
||||
const {
|
||||
chatId, threadId = MAIN_THREAD_ID, messageListType = 'thread', noHighlight, groupedId, groupedChatId,
|
||||
replyMessageId, isResizingContainer, shouldReplaceHistory, noForumTopicPanel, quote, scrollTargetPosition,
|
||||
timestamp, tabId = getCurrentTabId(),
|
||||
replyMessageId, isResizingContainer, shouldReplaceHistory, noForumTopicPanel, quote, quoteOffset,
|
||||
scrollTargetPosition, timestamp, tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
|
||||
let { messageId } = payload;
|
||||
@ -455,6 +455,7 @@ addActionHandler('focusMessage', (global, actions, payload): ActionReturnType =>
|
||||
noHighlight,
|
||||
isResizingContainer,
|
||||
quote,
|
||||
quoteOffset,
|
||||
scrollTargetPosition,
|
||||
}, tabId);
|
||||
global = updateFocusDirection(global, undefined, tabId);
|
||||
@ -525,13 +526,14 @@ addActionHandler('setShouldPreventComposerAnimation', (global, actions, payload)
|
||||
|
||||
addActionHandler('openReplyMenu', (global, actions, payload): ActionReturnType => {
|
||||
const {
|
||||
fromChatId, messageId, quoteText, tabId = getCurrentTabId(),
|
||||
fromChatId, messageId, quoteText, quoteOffset, tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
return updateTabState(global, {
|
||||
replyingMessage: {
|
||||
fromChatId,
|
||||
messageId,
|
||||
quoteText,
|
||||
quoteOffset,
|
||||
},
|
||||
isShareMessageModalShown: true,
|
||||
}, tabId);
|
||||
|
||||
@ -726,6 +726,7 @@ export function updateFocusedMessage<T extends GlobalState>(
|
||||
noHighlight = false,
|
||||
isResizingContainer = false,
|
||||
quote,
|
||||
quoteOffset,
|
||||
scrollTargetPosition,
|
||||
}: {
|
||||
global: T;
|
||||
@ -735,6 +736,7 @@ export function updateFocusedMessage<T extends GlobalState>(
|
||||
noHighlight?: boolean;
|
||||
isResizingContainer?: boolean;
|
||||
quote?: string;
|
||||
quoteOffset?: number;
|
||||
scrollTargetPosition?: ScrollTargetPosition;
|
||||
},
|
||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
@ -748,6 +750,7 @@ export function updateFocusedMessage<T extends GlobalState>(
|
||||
noHighlight,
|
||||
isResizingContainer,
|
||||
quote,
|
||||
quoteOffset,
|
||||
scrollTargetPosition,
|
||||
},
|
||||
}, tabId);
|
||||
|
||||
@ -956,6 +956,7 @@ export interface ActionPayloads {
|
||||
shouldReplaceHistory?: boolean;
|
||||
noForumTopicPanel?: boolean;
|
||||
quote?: string;
|
||||
quoteOffset?: number;
|
||||
scrollTargetPosition?: ScrollTargetPosition;
|
||||
timestamp?: number;
|
||||
} & WithTabId;
|
||||
@ -1769,6 +1770,7 @@ export interface ActionPayloads {
|
||||
fromChatId: string;
|
||||
messageId?: number;
|
||||
quoteText?: ApiFormattedText;
|
||||
quoteOffset?: number;
|
||||
} & WithTabId;
|
||||
|
||||
// Forwards
|
||||
|
||||
@ -156,6 +156,7 @@ export type TabState = {
|
||||
noHighlight?: boolean;
|
||||
isResizingContainer?: boolean;
|
||||
quote?: string;
|
||||
quoteOffset?: number;
|
||||
scrollTargetPosition?: ScrollTargetPosition;
|
||||
};
|
||||
|
||||
@ -352,6 +353,7 @@ export type TabState = {
|
||||
fromChatId?: string;
|
||||
messageId?: number;
|
||||
quoteText?: ApiFormattedText;
|
||||
quoteOffset?: number;
|
||||
toChatId?: string;
|
||||
toThreadId?: ThreadId;
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user