Allow replying with quoting message part.

This commit is contained in:
John Preston 2023-10-11 21:49:26 +04:00
parent 00db325e91
commit ef0539c9fc
20 changed files with 167 additions and 29 deletions

View file

@ -71,11 +71,7 @@ void ApplyPeerCloudDraft(
textWithTags, textWithTags,
FullReplyTo{ FullReplyTo{
.messageId = FullMsgId(replyPeerId, reply.messageId), .messageId = FullMsgId(replyPeerId, reply.messageId),
.quote = TextWithTags{ .quote = reply.quote,
reply.quote.text,
TextUtilities::ConvertEntitiesToTextTags(
reply.quote.entities),
},
.storyId = (reply.storyId .storyId = (reply.storyId
? FullStoryId{ replyPeerId, reply.storyId } ? FullStoryId{ replyPeerId, reply.storyId }
: FullStoryId()), : FullStoryId()),

View file

@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#include "data/data_histories.h" #include "data/data_histories.h"
#include "api/api_text_entities.h"
#include "data/data_session.h" #include "data/data_session.h"
#include "data/data_channel.h" #include "data/data_channel.h"
#include "data/data_chat.h" #include "data/data_chat.h"
@ -46,10 +47,20 @@ MTPInputReplyTo ReplyToForMTP(
} }
} else if (replyTo.messageId || replyTo.topicRootId) { } else if (replyTo.messageId || replyTo.topicRootId) {
const auto external = (replyTo.messageId.peer != history->peer->id); 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; using Flag = MTPDinputReplyToMessage::Flag;
return MTP_inputReplyToMessage( return MTP_inputReplyToMessage(
MTP_flags((replyTo.topicRootId ? Flag::f_top_msg_id : Flag()) 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 MTP_int(replyTo.messageId
? replyTo.messageId.msg ? replyTo.messageId.msg
: replyTo.topicRootId), : replyTo.topicRootId),
@ -57,8 +68,8 @@ MTPInputReplyTo ReplyToForMTP(
(external (external
? owner->peer(replyTo.messageId.peer)->input ? owner->peer(replyTo.messageId.peer)->input
: MTPInputPeer()), : MTPInputPeer()),
MTPstring(), // quote_text MTP_string(replyTo.quote.text),
MTPVector<MTPMessageEntity>()); // quote_entities quoteEntities);
} }
return MTPInputReplyTo(); return MTPInputReplyTo();
} }
@ -945,6 +956,7 @@ int Histories::sendPreparedMessage(
} }
const auto realReplyTo = FullReplyTo{ const auto realReplyTo = FullReplyTo{
.messageId = convertTopicReplyToId(history, replyTo.messageId), .messageId = convertTopicReplyToId(history, replyTo.messageId),
.quote = replyTo.quote,
.storyId = replyTo.storyId, .storyId = replyTo.storyId,
.topicRootId = convertTopicReplyToId(history, replyTo.topicRootId), .topicRootId = convertTopicReplyToId(history, replyTo.topicRootId),
}; };

View file

@ -158,7 +158,7 @@ Q_DECLARE_METATYPE(FullMsgId);
struct FullReplyTo { struct FullReplyTo {
FullMsgId messageId; FullMsgId messageId;
TextWithTags quote; TextWithEntities quote;
FullStoryId storyId; FullStoryId storyId;
MsgId topicRootId = 0; MsgId topicRootId = 0;

View file

@ -2101,6 +2101,20 @@ void HistoryInner::toggleFavoriteReaction(not_null<Element*> view) const {
item->toggleReaction(favorite, HistoryItem::ReactionSource::Quick); item->toggleReaction(favorite, HistoryItem::ReactionSource::Quick);
} }
TextWithEntities HistoryInner::selectedQuote(
not_null<HistoryItem*> 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) { void HistoryInner::contextMenuEvent(QContextMenuEvent *e) {
showContextMenu(e); showContextMenu(e);
} }
@ -2215,13 +2229,14 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
return true; return true;
}(); }();
if (canReply) { if (canReply) {
const auto quote = selectedQuote(item);
_menu->addAction(tr::lng_context_reply_msg(tr::now), [=] { _menu->addAction(tr::lng_context_reply_msg(tr::now), [=] {
if (canSendReply) { if (canSendReply) {
_widget->replyToMessage({ itemId }); _widget->replyToMessage({ itemId, quote });
} else { } else {
HistoryView::Controls::ShowReplyToChatBox( HistoryView::Controls::ShowReplyToChatBox(
controller->uiShow(), controller->uiShow(),
{ itemId }); { itemId, quote });
} }
}, &st::menuIconReply); }, &st::menuIconReply);
} }

View file

@ -314,6 +314,8 @@ private:
QPoint mapPointToItem(QPoint p, const Element *view) const; QPoint mapPointToItem(QPoint p, const Element *view) const;
QPoint mapPointToItem(QPoint p, const HistoryItem *item) const; QPoint mapPointToItem(QPoint p, const HistoryItem *item) const;
[[nodiscard]] TextWithEntities selectedQuote(
not_null<HistoryItem*> item) const;
void showContextMenu(QContextMenuEvent *e, bool showFromTouch = false); void showContextMenu(QContextMenuEvent *e, bool showFromTouch = false);
void cancelContextDownload(not_null<DocumentData*> document); void cancelContextDownload(not_null<DocumentData*> document);

View file

@ -7072,7 +7072,7 @@ void HistoryWidget::replyToMessage(FullReplyTo id) {
void HistoryWidget::replyToMessage( void HistoryWidget::replyToMessage(
not_null<HistoryItem*> item, not_null<HistoryItem*> item,
TextWithTags quote) { TextWithEntities quote) {
if (isJoinChannel()) { if (isJoinChannel()) {
return; return;
} }

View file

@ -185,7 +185,7 @@ public:
void replyToMessage(FullReplyTo id); void replyToMessage(FullReplyTo id);
void replyToMessage( void replyToMessage(
not_null<HistoryItem*> item, not_null<HistoryItem*> item,
TextWithTags quote = {}); TextWithEntities quote = {});
void editMessage(FullMsgId itemId); void editMessage(FullMsgId itemId);
void editMessage(not_null<HistoryItem*> item); void editMessage(not_null<HistoryItem*> item);

View file

@ -388,7 +388,10 @@ public:
int bottom, int bottom,
QPoint point, QPoint point,
InfoDisplayType type) const; 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; TextSelection selection) const = 0;
[[nodiscard]] virtual TextSelection adjustSelection( [[nodiscard]] virtual TextSelection adjustSelection(
TextSelection selection, TextSelection selection,

View file

@ -2611,6 +2611,80 @@ TextForMimeData Message::selectedText(TextSelection selection) const {
return result; 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 Message::adjustSelection(
TextSelection selection, TextSelection selection,
TextSelectType type) const { TextSelectType type) const {

View file

@ -99,6 +99,10 @@ public:
QPoint point, QPoint point,
InfoDisplayType type) const override; InfoDisplayType type) const override;
TextForMimeData selectedText(TextSelection selection) 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 adjustSelection(
TextSelection selection, TextSelection selection,
TextSelectType type) const override; TextSelectType type) const override;

View file

@ -669,6 +669,16 @@ TextForMimeData Service::selectedText(TextSelection selection) const {
return text().toTextForMimeData(selection); 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 Service::adjustSelection(
TextSelection selection, TextSelection selection,
TextSelectType type) const { TextSelectType type) const {

View file

@ -43,6 +43,10 @@ public:
StateRequest request) const override; StateRequest request) const override;
void updatePressed(QPoint point) override; void updatePressed(QPoint point) override;
TextForMimeData selectedText(TextSelection selection) 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 adjustSelection(
TextSelection selection, TextSelection selection,
TextSelectType type) const override; TextSelectType type) const override;

View file

@ -1210,6 +1210,22 @@ TextForMimeData Document::selectedText(TextSelection selection) const {
return result; return result;
} }
TextWithEntities Document::selectedQuote(TextSelection selection) const {
if (const auto voice = Get<HistoryDocumentVoice>()) {
const auto length = voice->transcribeText.length();
if (selection.from < length) {
return {};
}
selection = HistoryView::UnshiftItemSelection(
selection,
voice->transcribeText);
}
if (const auto captioned = Get<HistoryDocumentCaptioned>()) {
return parent()->selectedQuote(captioned->caption, selection);
}
return {};
}
bool Document::uploading() const { bool Document::uploading() const {
return _data->uploading(); return _data->uploading();
} }

View file

@ -46,6 +46,7 @@ public:
bool hasTextForCopy() const override; bool hasTextForCopy() const override;
TextForMimeData selectedText(TextSelection selection) const override; TextForMimeData selectedText(TextSelection selection) const override;
TextWithEntities selectedQuote(TextSelection selection) const override;
bool uploading() const override; bool uploading() const override;

View file

@ -1181,6 +1181,10 @@ TextForMimeData Gif::selectedText(TextSelection selection) const {
return _caption.toTextForMimeData(selection); return _caption.toTextForMimeData(selection);
} }
TextWithEntities Gif::selectedQuote(TextSelection selection) const {
return parent()->selectedQuote(_caption, selection);
}
bool Gif::fullFeaturedGrouped(RectParts sides) const { bool Gif::fullFeaturedGrouped(RectParts sides) const {
return (sides & RectPart::Left) && (sides & RectPart::Right); return (sides & RectPart::Left) && (sides & RectPart::Right);
} }

View file

@ -68,6 +68,7 @@ public:
} }
TextForMimeData selectedText(TextSelection selection) const override; TextForMimeData selectedText(TextSelection selection) const override;
TextWithEntities selectedQuote(TextSelection selection) const override;
bool uploading() const override; bool uploading() const override;

View file

@ -86,7 +86,11 @@ public:
[[nodiscard]] virtual TextForMimeData selectedText( [[nodiscard]] virtual TextForMimeData selectedText(
TextSelection selection) const { TextSelection selection) const {
return TextForMimeData(); return {};
}
[[nodiscard]] virtual TextWithEntities selectedQuote(
TextSelection selection) const {
return {};
} }
[[nodiscard]] virtual bool isDisplayed() const; [[nodiscard]] virtual bool isDisplayed() const;

View file

@ -1049,6 +1049,10 @@ TextForMimeData Photo::selectedText(TextSelection selection) const {
return _caption.toTextForMimeData(selection); return _caption.toTextForMimeData(selection);
} }
TextWithEntities Photo::selectedQuote(TextSelection selection) const {
return parent()->selectedQuote(_caption, selection);
}
void Photo::hideSpoilers() { void Photo::hideSpoilers() {
_caption.setSpoilerRevealed(false, anim::type::instant); _caption.setSpoilerRevealed(false, anim::type::instant);
if (_spoiler) { if (_spoiler) {

View file

@ -57,6 +57,7 @@ public:
} }
TextForMimeData selectedText(TextSelection selection) const override; TextForMimeData selectedText(TextSelection selection) const override;
TextWithEntities selectedQuote(TextSelection selection) const override;
PhotoData *getPhoto() const override { PhotoData *getPhoto() const override {
return _data; return _data;

View file

@ -1126,11 +1126,8 @@ void Account::writeDrafts(not_null<History*> history) {
auto&&) { // cursor auto&&) { // cursor
size += sizeof(qint64) // key size += sizeof(qint64) // key
+ Serialize::stringSize(text.text) + Serialize::stringSize(text.text)
+ sizeof(qint64) + TextUtilities::SerializeTagsSize(text.tags) + TextUtilities::SerializeTagsSize(text.tags)
+ sizeof(qint64) + sizeof(qint64) // messageId + sizeof(qint64) + sizeof(qint64) // messageId
+ Serialize::stringSize(reply.quote.text)
+ sizeof(qint64)
+ TextUtilities::SerializeTagsSize(reply.quote.tags)
+ sizeof(qint32); // previewState + sizeof(qint32); // previewState
}; };
EnumerateDrafts( EnumerateDrafts(
@ -1157,8 +1154,6 @@ void Account::writeDrafts(not_null<History*> history) {
<< TextUtilities::SerializeTags(text.tags) << TextUtilities::SerializeTags(text.tags)
<< qint64(reply.messageId.peer.value) << qint64(reply.messageId.peer.value)
<< qint64(reply.messageId.msg.bare) << qint64(reply.messageId.msg.bare)
<< reply.quote.text
<< TextUtilities::SerializeTags(reply.quote.tags)
<< qint32(previewState); << qint32(previewState);
}; };
EnumerateDrafts( EnumerateDrafts(
@ -1371,10 +1366,8 @@ void Account::readDraftsWithCursors(not_null<History*> history) {
const auto keysOld = (tag == kMultiDraftTagOld); const auto keysOld = (tag == kMultiDraftTagOld);
const auto rich = (tag == kRichDraftsTag); const auto rich = (tag == kRichDraftsTag);
for (auto i = 0; i != count; ++i) { for (auto i = 0; i != count; ++i) {
TextWithTags quote;
TextWithTags text; TextWithTags text;
QByteArray textTagsSerialized; QByteArray textTagsSerialized;
QByteArray quoteTagsSerialized;
qint64 keyValue = 0; qint64 keyValue = 0;
qint64 messageIdPeer = 0, messageIdMsg = 0; qint64 messageIdPeer = 0, messageIdMsg = 0;
qint32 keyValueOld = 0, uncheckedPreviewState = 0; qint32 keyValueOld = 0, uncheckedPreviewState = 0;
@ -1396,12 +1389,7 @@ void Account::readDraftsWithCursors(not_null<History*> history) {
>> textTagsSerialized >> textTagsSerialized
>> messageIdPeer >> messageIdPeer
>> messageIdMsg >> messageIdMsg
>> quote.text
>> quoteTagsSerialized
>> uncheckedPreviewState; >> uncheckedPreviewState;
quote.tags = TextUtilities::DeserializeTags(
quoteTagsSerialized,
quote.text.size());
} }
text.tags = TextUtilities::DeserializeTags( text.tags = TextUtilities::DeserializeTags(
textTagsSerialized, textTagsSerialized,
@ -1422,7 +1410,6 @@ void Account::readDraftsWithCursors(not_null<History*> history) {
.messageId = FullMsgId( .messageId = FullMsgId(
PeerId(messageIdPeer), PeerId(messageIdPeer),
MsgId(messageIdMsg)), MsgId(messageIdMsg)),
.quote = quote,
.topicRootId = key.topicRootId(), .topicRootId = key.topicRootId(),
}, },
MessageCursor(), MessageCursor(),