diff --git a/Telegram/SourceFiles/data/data_drafts.cpp b/Telegram/SourceFiles/data/data_drafts.cpp index 55cb28708..58ad920ef 100644 --- a/Telegram/SourceFiles/data/data_drafts.cpp +++ b/Telegram/SourceFiles/data/data_drafts.cpp @@ -71,11 +71,7 @@ void ApplyPeerCloudDraft( textWithTags, FullReplyTo{ .messageId = FullMsgId(replyPeerId, reply.messageId), - .quote = TextWithTags{ - reply.quote.text, - TextUtilities::ConvertEntitiesToTextTags( - reply.quote.entities), - }, + .quote = reply.quote, .storyId = (reply.storyId ? FullStoryId{ replyPeerId, reply.storyId } : FullStoryId()), diff --git a/Telegram/SourceFiles/data/data_histories.cpp b/Telegram/SourceFiles/data/data_histories.cpp index ab002d24f..9c9b9059a 100644 --- a/Telegram/SourceFiles/data/data_histories.cpp +++ b/Telegram/SourceFiles/data/data_histories.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "data/data_histories.h" +#include "api/api_text_entities.h" #include "data/data_session.h" #include "data/data_channel.h" #include "data/data_chat.h" @@ -46,10 +47,20 @@ MTPInputReplyTo ReplyToForMTP( } } else if (replyTo.messageId || replyTo.topicRootId) { const auto external = (replyTo.messageId.peer != history->peer->id); + const auto quoteEntities = Api::EntitiesToMTP( + &history->session(), + replyTo.quote.entities, + Api::ConvertOption::SkipLocal); using Flag = MTPDinputReplyToMessage::Flag; return MTP_inputReplyToMessage( MTP_flags((replyTo.topicRootId ? Flag::f_top_msg_id : Flag()) - | (external ? Flag::f_reply_to_peer_id : Flag())), + | (external ? Flag::f_reply_to_peer_id : Flag()) + | (replyTo.quote.text.isEmpty() + ? Flag() + : Flag::f_quote_text) + | (quoteEntities.v.isEmpty() + ? Flag() + : Flag::f_quote_entities)), MTP_int(replyTo.messageId ? replyTo.messageId.msg : replyTo.topicRootId), @@ -57,8 +68,8 @@ MTPInputReplyTo ReplyToForMTP( (external ? owner->peer(replyTo.messageId.peer)->input : MTPInputPeer()), - MTPstring(), // quote_text - MTPVector()); // quote_entities + MTP_string(replyTo.quote.text), + quoteEntities); } return MTPInputReplyTo(); } @@ -945,6 +956,7 @@ int Histories::sendPreparedMessage( } const auto realReplyTo = FullReplyTo{ .messageId = convertTopicReplyToId(history, replyTo.messageId), + .quote = replyTo.quote, .storyId = replyTo.storyId, .topicRootId = convertTopicReplyToId(history, replyTo.topicRootId), }; diff --git a/Telegram/SourceFiles/data/data_msg_id.h b/Telegram/SourceFiles/data/data_msg_id.h index 4fa1fde54..43260a10c 100644 --- a/Telegram/SourceFiles/data/data_msg_id.h +++ b/Telegram/SourceFiles/data/data_msg_id.h @@ -158,7 +158,7 @@ Q_DECLARE_METATYPE(FullMsgId); struct FullReplyTo { FullMsgId messageId; - TextWithTags quote; + TextWithEntities quote; FullStoryId storyId; MsgId topicRootId = 0; diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 4d68ab061..04f7c5518 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -2101,6 +2101,20 @@ void HistoryInner::toggleFavoriteReaction(not_null view) const { item->toggleReaction(favorite, HistoryItem::ReactionSource::Quick); } +TextWithEntities HistoryInner::selectedQuote( + not_null item) const { + if (_selected.size() != 1 + || _selected.begin()->first != item + || _selected.begin()->second == FullSelection) { + return {}; + } + const auto view = item->mainView(); + if (!view) { + return {}; + } + return view->selectedQuote(_selected.begin()->second); +} + void HistoryInner::contextMenuEvent(QContextMenuEvent *e) { showContextMenu(e); } @@ -2215,13 +2229,14 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { return true; }(); if (canReply) { + const auto quote = selectedQuote(item); _menu->addAction(tr::lng_context_reply_msg(tr::now), [=] { if (canSendReply) { - _widget->replyToMessage({ itemId }); + _widget->replyToMessage({ itemId, quote }); } else { HistoryView::Controls::ShowReplyToChatBox( controller->uiShow(), - { itemId }); + { itemId, quote }); } }, &st::menuIconReply); } diff --git a/Telegram/SourceFiles/history/history_inner_widget.h b/Telegram/SourceFiles/history/history_inner_widget.h index 38d68cdde..981b9e424 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.h +++ b/Telegram/SourceFiles/history/history_inner_widget.h @@ -314,6 +314,8 @@ private: QPoint mapPointToItem(QPoint p, const Element *view) const; QPoint mapPointToItem(QPoint p, const HistoryItem *item) const; + [[nodiscard]] TextWithEntities selectedQuote( + not_null item) const; void showContextMenu(QContextMenuEvent *e, bool showFromTouch = false); void cancelContextDownload(not_null document); diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index ad1ec6f5e..c8d72b6e1 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -7072,7 +7072,7 @@ void HistoryWidget::replyToMessage(FullReplyTo id) { void HistoryWidget::replyToMessage( not_null item, - TextWithTags quote) { + TextWithEntities quote) { if (isJoinChannel()) { return; } diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index 84e4b8be9..1e0358eaf 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -185,7 +185,7 @@ public: void replyToMessage(FullReplyTo id); void replyToMessage( not_null item, - TextWithTags quote = {}); + TextWithEntities quote = {}); void editMessage(FullMsgId itemId); void editMessage(not_null item); diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h index 02635118a..6c16cef30 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.h +++ b/Telegram/SourceFiles/history/view/history_view_element.h @@ -388,7 +388,10 @@ public: int bottom, QPoint point, InfoDisplayType type) const; - virtual TextForMimeData selectedText( + virtual TextForMimeData selectedText(TextSelection selection) const = 0; + virtual TextWithEntities selectedQuote(TextSelection selection) const = 0; + virtual TextWithEntities selectedQuote( + const Ui::Text::String &text, TextSelection selection) const = 0; [[nodiscard]] virtual TextSelection adjustSelection( TextSelection selection, diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 0b46cc479..8b86fa2e8 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -2611,6 +2611,80 @@ TextForMimeData Message::selectedText(TextSelection selection) const { return result; } +TextWithEntities Message::selectedQuote(TextSelection selection) const { + const auto item = data(); + const auto &translated = item->translatedText(); + const auto &original = item->originalText(); + if (&translated != &original + || selection.empty() + || selection == FullSelection) { + return {}; + } else if (hasVisibleText()) { + return selectedQuote(text(), selection); + } else if (const auto media = this->media()) { + if (media->isDisplayed() || isHiddenByGroup()) { + return media->selectedQuote(selection); + } + } + return {}; +} + +TextWithEntities Message::selectedQuote( + const Ui::Text::String &text, + TextSelection selection) const { + if (selection.to > text.length()) { + return {}; + } + auto modified = selection; + const auto &modifications = text.modifications(); + for (const auto &modification : text.modifications()) { + if (modification.position >= selection.to) { + break; + } else if (modification.position <= selection.from) { + modified.from += modification.skipped; + if (modification.added + && modification.position < selection.from) { + --modified.from; + } + } + modified.to += modification.skipped; + if (modification.added && modified.to > modified.from) { + --modified.to; + } + } + auto result = data()->originalText(); + if (modified.empty() || modified.to > result.text.size()) { + return {}; + } + result.text = result.text.mid( + modified.from, + modified.to - modified.from); + const auto allowed = std::array{ + EntityType::Bold, + EntityType::Italic, + EntityType::Underline, + EntityType::StrikeOut, + EntityType::Spoiler, + EntityType::CustomEmoji, + }; + for (auto i = result.entities.begin(); i != result.entities.end();) { + const auto offset = i->offset(); + const auto till = offset + i->length(); + if ((till <= modified.from) + || (offset >= modified.to) + || !ranges::contains(allowed, i->type())) { + i = result.entities.erase(i); + } else { + if (till > modified.to) { + i->shrinkFromRight(till - modified.to); + } + i->shiftLeft(modified.from); + ++i; + } + } + return result; +} + TextSelection Message::adjustSelection( TextSelection selection, TextSelectType type) const { diff --git a/Telegram/SourceFiles/history/view/history_view_message.h b/Telegram/SourceFiles/history/view/history_view_message.h index 2c0ebaf0e..1efc0892b 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.h +++ b/Telegram/SourceFiles/history/view/history_view_message.h @@ -99,6 +99,10 @@ public: QPoint point, InfoDisplayType type) const override; TextForMimeData selectedText(TextSelection selection) const override; + TextWithEntities selectedQuote(TextSelection selection) const override; + TextWithEntities selectedQuote( + const Ui::Text::String &text, + TextSelection selection) const override; TextSelection adjustSelection( TextSelection selection, TextSelectType type) const override; diff --git a/Telegram/SourceFiles/history/view/history_view_service_message.cpp b/Telegram/SourceFiles/history/view/history_view_service_message.cpp index 44ddb1c17..983d7ff11 100644 --- a/Telegram/SourceFiles/history/view/history_view_service_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_service_message.cpp @@ -669,6 +669,16 @@ TextForMimeData Service::selectedText(TextSelection selection) const { return text().toTextForMimeData(selection); } +TextWithEntities Service::selectedQuote(TextSelection selection) const { + return {}; +} + +TextWithEntities Service::selectedQuote( + const Ui::Text::String &text, + TextSelection selection) const { + return {}; +} + TextSelection Service::adjustSelection( TextSelection selection, TextSelectType type) const { diff --git a/Telegram/SourceFiles/history/view/history_view_service_message.h b/Telegram/SourceFiles/history/view/history_view_service_message.h index 4fcd5748f..d40a69c8b 100644 --- a/Telegram/SourceFiles/history/view/history_view_service_message.h +++ b/Telegram/SourceFiles/history/view/history_view_service_message.h @@ -43,6 +43,10 @@ public: StateRequest request) const override; void updatePressed(QPoint point) override; TextForMimeData selectedText(TextSelection selection) const override; + TextWithEntities selectedQuote(TextSelection selection) const override; + TextWithEntities selectedQuote( + const Ui::Text::String &text, + TextSelection selection) const override; TextSelection adjustSelection( TextSelection selection, TextSelectType type) const override; diff --git a/Telegram/SourceFiles/history/view/media/history_view_document.cpp b/Telegram/SourceFiles/history/view/media/history_view_document.cpp index c8e25a551..165f79dea 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_document.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_document.cpp @@ -1210,6 +1210,22 @@ TextForMimeData Document::selectedText(TextSelection selection) const { return result; } +TextWithEntities Document::selectedQuote(TextSelection selection) const { + if (const auto voice = Get()) { + const auto length = voice->transcribeText.length(); + if (selection.from < length) { + return {}; + } + selection = HistoryView::UnshiftItemSelection( + selection, + voice->transcribeText); + } + if (const auto captioned = Get()) { + return parent()->selectedQuote(captioned->caption, selection); + } + return {}; +} + bool Document::uploading() const { return _data->uploading(); } diff --git a/Telegram/SourceFiles/history/view/media/history_view_document.h b/Telegram/SourceFiles/history/view/media/history_view_document.h index 08f0c3ada..6084820eb 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_document.h +++ b/Telegram/SourceFiles/history/view/media/history_view_document.h @@ -46,6 +46,7 @@ public: bool hasTextForCopy() const override; TextForMimeData selectedText(TextSelection selection) const override; + TextWithEntities selectedQuote(TextSelection selection) const override; bool uploading() const override; diff --git a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp index 2e01c30c9..cd475f2d2 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp @@ -1181,6 +1181,10 @@ TextForMimeData Gif::selectedText(TextSelection selection) const { return _caption.toTextForMimeData(selection); } +TextWithEntities Gif::selectedQuote(TextSelection selection) const { + return parent()->selectedQuote(_caption, selection); +} + bool Gif::fullFeaturedGrouped(RectParts sides) const { return (sides & RectPart::Left) && (sides & RectPart::Right); } diff --git a/Telegram/SourceFiles/history/view/media/history_view_gif.h b/Telegram/SourceFiles/history/view/media/history_view_gif.h index c737d2391..9a81457eb 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_gif.h +++ b/Telegram/SourceFiles/history/view/media/history_view_gif.h @@ -68,6 +68,7 @@ public: } TextForMimeData selectedText(TextSelection selection) const override; + TextWithEntities selectedQuote(TextSelection selection) const override; bool uploading() const override; diff --git a/Telegram/SourceFiles/history/view/media/history_view_media.h b/Telegram/SourceFiles/history/view/media/history_view_media.h index 386f4e868..ea60e186f 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media.h +++ b/Telegram/SourceFiles/history/view/media/history_view_media.h @@ -86,7 +86,11 @@ public: [[nodiscard]] virtual TextForMimeData selectedText( TextSelection selection) const { - return TextForMimeData(); + return {}; + } + [[nodiscard]] virtual TextWithEntities selectedQuote( + TextSelection selection) const { + return {}; } [[nodiscard]] virtual bool isDisplayed() const; diff --git a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp index 8e0763e39..5e912d33c 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp @@ -1049,6 +1049,10 @@ TextForMimeData Photo::selectedText(TextSelection selection) const { return _caption.toTextForMimeData(selection); } +TextWithEntities Photo::selectedQuote(TextSelection selection) const { + return parent()->selectedQuote(_caption, selection); +} + void Photo::hideSpoilers() { _caption.setSpoilerRevealed(false, anim::type::instant); if (_spoiler) { diff --git a/Telegram/SourceFiles/history/view/media/history_view_photo.h b/Telegram/SourceFiles/history/view/media/history_view_photo.h index cfe9e9627..76c1ff6ad 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_photo.h +++ b/Telegram/SourceFiles/history/view/media/history_view_photo.h @@ -57,6 +57,7 @@ public: } TextForMimeData selectedText(TextSelection selection) const override; + TextWithEntities selectedQuote(TextSelection selection) const override; PhotoData *getPhoto() const override { return _data; diff --git a/Telegram/SourceFiles/storage/storage_account.cpp b/Telegram/SourceFiles/storage/storage_account.cpp index 8559bf621..78374de82 100644 --- a/Telegram/SourceFiles/storage/storage_account.cpp +++ b/Telegram/SourceFiles/storage/storage_account.cpp @@ -1126,11 +1126,8 @@ void Account::writeDrafts(not_null history) { auto&&) { // cursor size += sizeof(qint64) // key + Serialize::stringSize(text.text) - + sizeof(qint64) + TextUtilities::SerializeTagsSize(text.tags) + + TextUtilities::SerializeTagsSize(text.tags) + sizeof(qint64) + sizeof(qint64) // messageId - + Serialize::stringSize(reply.quote.text) - + sizeof(qint64) - + TextUtilities::SerializeTagsSize(reply.quote.tags) + sizeof(qint32); // previewState }; EnumerateDrafts( @@ -1157,8 +1154,6 @@ void Account::writeDrafts(not_null history) { << TextUtilities::SerializeTags(text.tags) << qint64(reply.messageId.peer.value) << qint64(reply.messageId.msg.bare) - << reply.quote.text - << TextUtilities::SerializeTags(reply.quote.tags) << qint32(previewState); }; EnumerateDrafts( @@ -1371,10 +1366,8 @@ void Account::readDraftsWithCursors(not_null history) { const auto keysOld = (tag == kMultiDraftTagOld); const auto rich = (tag == kRichDraftsTag); for (auto i = 0; i != count; ++i) { - TextWithTags quote; TextWithTags text; QByteArray textTagsSerialized; - QByteArray quoteTagsSerialized; qint64 keyValue = 0; qint64 messageIdPeer = 0, messageIdMsg = 0; qint32 keyValueOld = 0, uncheckedPreviewState = 0; @@ -1396,12 +1389,7 @@ void Account::readDraftsWithCursors(not_null history) { >> textTagsSerialized >> messageIdPeer >> messageIdMsg - >> quote.text - >> quoteTagsSerialized >> uncheckedPreviewState; - quote.tags = TextUtilities::DeserializeTags( - quoteTagsSerialized, - quote.text.size()); } text.tags = TextUtilities::DeserializeTags( textTagsSerialized, @@ -1422,7 +1410,6 @@ void Account::readDraftsWithCursors(not_null history) { .messageId = FullMsgId( PeerId(messageIdPeer), MsgId(messageIdMsg)), - .quote = quote, .topicRootId = key.topicRootId(), }, MessageCursor(),