From a197ed9e95a94aed4ef402d718ff9ec312946586 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 26 Oct 2023 11:30:36 +0400 Subject: [PATCH] Allow choosing the link for the preview. --- .../chat_helpers/message_field.cpp | 8 +- .../SourceFiles/chat_helpers/message_field.h | 39 +- .../history/history_inner_widget.cpp | 3 +- .../SourceFiles/history/history_widget.cpp | 23 +- .../controls/history_view_draft_options.cpp | 614 +++++++++++------- .../controls/history_view_draft_options.h | 23 +- .../history_view_webpage_processor.cpp | 185 ++++-- .../controls/history_view_webpage_processor.h | 42 +- .../history/view/history_view_cursor_state.h | 1 + .../history/view/history_view_message.cpp | 1 + 10 files changed, 588 insertions(+), 351 deletions(-) diff --git a/Telegram/SourceFiles/chat_helpers/message_field.cpp b/Telegram/SourceFiles/chat_helpers/message_field.cpp index 0ef3bceca..9c5b37751 100644 --- a/Telegram/SourceFiles/chat_helpers/message_field.cpp +++ b/Telegram/SourceFiles/chat_helpers/message_field.cpp @@ -648,10 +648,6 @@ bool MessageLinksParser::eventFilter(QObject *object, QEvent *event) { return QObject::eventFilter(object, event); } -const rpl::variable &MessageLinksParser::list() const { - return _list; -} - void MessageLinksParser::parse() { const auto &textWithTags = _field->getTextWithTags(); const auto &text = textWithTags.text; @@ -781,7 +777,7 @@ void MessageLinksParser::parse() { continue; } } - const auto range = LinkRange { + const auto range = MessageLinkRange{ int(domainOffset), static_cast(p - start - domainOffset), QString() @@ -802,7 +798,7 @@ void MessageLinksParser::parse() { void MessageLinksParser::applyRanges(const QString &text) { const auto count = int(_ranges.size()); const auto current = _list.current(); - const auto computeLink = [&](const LinkRange &range) { + const auto computeLink = [&](const MessageLinkRange &range) { return range.custom.isEmpty() ? base::StringViewMid(text, range.start, range.length) : QStringView(range.custom); diff --git a/Telegram/SourceFiles/chat_helpers/message_field.h b/Telegram/SourceFiles/chat_helpers/message_field.h index d52d1d670..a78abd94c 100644 --- a/Telegram/SourceFiles/chat_helpers/message_field.h +++ b/Telegram/SourceFiles/chat_helpers/message_field.h @@ -7,9 +7,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -#include "ui/widgets/fields/input_field.h" +#include "base/qt/qt_compare.h" #include "base/timer.h" #include "chat_helpers/compose/compose_features.h" +#include "ui/widgets/fields/input_field.h" #ifndef TDESKTOP_DISABLE_SPELLCHECK #include "boxes/dictionaries_manager.h" @@ -96,6 +97,19 @@ AutocompleteQuery ParseMentionHashtagBotCommandQuery( not_null field, ChatHelpers::ComposeFeatures features); +struct MessageLinkRange { + int start = 0; + int length = 0; + QString custom; + + friend inline auto operator<=>( + const MessageLinkRange&, + const MessageLinkRange&) = default; + friend inline bool operator==( + const MessageLinkRange&, + const MessageLinkRange&) = default; +}; + class MessageLinksParser final : private QObject { public: MessageLinksParser(not_null field); @@ -103,21 +117,12 @@ public: void parseNow(); void setDisabled(bool disabled); - struct LinkRange { - int start = 0; - int length = 0; - QString custom; - - friend inline auto operator<=>( - const LinkRange&, - const LinkRange&) = default; - friend inline bool operator==( - const LinkRange&, - const LinkRange&) = default; - }; - - [[nodiscard]] const rpl::variable &list() const; - [[nodiscard]] const std::vector &ranges() const; + [[nodiscard]] const rpl::variable &list() const { + return _list; + } + [[nodiscard]] const std::vector &ranges() const { + return _ranges; + } private: bool eventFilter(QObject *object, QEvent *event) override; @@ -127,7 +132,7 @@ private: not_null _field; rpl::variable _list; - std::vector _ranges; + std::vector _ranges; int _lastLength = 0; bool _disabled = false; base::Timer _timer; diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index e0944e7a8..2da2c97e4 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -2403,9 +2403,10 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { if (canReply) { const auto itemId = item->fullId(); const auto quote = selectedQuote(item); - const auto text = quote.empty() + auto text = quote.empty() ? tr::lng_context_reply_msg(tr::now) : tr::lng_context_quote_and_reply(tr::now); + text.replace('&', u"&&"_q); _menu->addAction(text, [=] { if (canSendReply) { _widget->replyToMessage({ itemId, quote }); diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index fbdb5138e..7f2f9077a 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -6284,21 +6284,26 @@ void HistoryWidget::editDraftOptions() { } _preview->apply(webpage); }; + const auto replyToId = reply.messageId; const auto highlight = [=] { controller()->showPeerHistory( - reply.messageId.peer, + replyToId.peer, Window::SectionShow::Way::Forward, - reply.messageId.msg); + replyToId.msg); }; using namespace HistoryView::Controls; - EditDraftOptions( - controller()->uiShow(), - history, - Data::Draft(_field, reply, _preview->draft()), - done, - highlight, - [=] { ClearDraftReplyTo(history, reply.messageId); }); + EditDraftOptions({ + .show = controller()->uiShow(), + .history = history, + .draft = Data::Draft(_field, reply, _preview->draft()), + .usedLink = _preview->link(), + .links = _preview->links(), + .resolver = _preview->resolver(), + .done = done, + .highlight = highlight, + .clearOldDraft = [=] { ClearDraftReplyTo(history, replyToId); }, + }); } void HistoryWidget::keyPressEvent(QKeyEvent *e) { diff --git a/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp index 60e9d1bb0..bee32e72b 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp @@ -18,11 +18,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_thread.h" #include "data/data_user.h" #include "data/data_web_page.h" +#include "history/view/controls/history_view_webpage_processor.h" +#include "history/view/history_view_element.h" +#include "history/view/history_view_cursor_state.h" #include "history/history.h" #include "history/history_item.h" #include "history/history_item_components.h" -#include "history/view/history_view_element.h" -#include "history/view/history_view_cursor_state.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "settings/settings_common.h" @@ -102,6 +103,28 @@ private: return result; } +[[nodiscard]] TextWithEntities HighlightParsedLinks( + TextWithEntities text, + const std::vector &links) { + auto i = text.entities.begin(); + for (const auto &range : links) { + if (range.custom.isEmpty()) { + while (i != text.entities.end()) { + if (i->offset() > range.start) { + break; + } + ++i; + } + i = text.entities.insert( + i, + EntityInText(EntityType::Url, range.start, range.length)); + ++i; + } + } + return text; +} + + class PreviewWrap final : public Ui::RpWidget { public: PreviewWrap( @@ -114,7 +137,9 @@ public: const TextWithEntities "e); [[nodiscard]] rpl::producer showLinkSelector( const TextWithTags &message, - Data::WebPageDraft webpage); + Data::WebPageDraft webpage, + const std::vector &links, + const QString &usedLink); private: void paintEvent(QPaintEvent *e) override; @@ -125,6 +150,10 @@ private: void mouseDoubleClickEvent(QMouseEvent *e) override; void initElement(); + void highlightUsedLink( + const TextWithTags &message, + const QString &usedLink, + const std::vector &links); void startSelection(TextSelectType type); [[nodiscard]] TextSelection resolveNewSelection() const; @@ -138,12 +167,15 @@ private: HistoryItem *_draftItem = nullptr; std::unique_ptr _element; rpl::variable _selection; + rpl::event_stream _chosenUrl; Ui::PeerUserpicView _userpic; rpl::lifetime _elementLifetime; QPoint _position; base::Timer _trippleClickTimer; + ClickHandlerPtr _link; + ClickHandlerPtr _pressedLink; TextSelectType _selectType = TextSelectType::Letters; uint16 _symbol = 0; uint16 _selectionStartSymbol = 0; @@ -219,6 +251,8 @@ rpl::producer PreviewWrap::showQuoteSelector( _selection.reset(element->selectionFromQuote(quote)); _element = std::move(element); + _link = _pressedLink = nullptr; + if (const auto was = base::take(_draftItem)) { was->destroy(); } @@ -240,7 +274,9 @@ rpl::producer PreviewWrap::showQuoteSelector( rpl::producer PreviewWrap::showLinkSelector( const TextWithTags &message, - Data::WebPageDraft webpage) { + Data::WebPageDraft webpage, + const std::vector &links, + const QString &usedLink) { _selection.reset(TextSelection()); _element = nullptr; @@ -259,10 +295,10 @@ rpl::producer PreviewWrap::showLinkSelector( base::unixtime::now(), // date _history->session().userPeerId(), QString(), // postAuthor - TextWithEntities{ + HighlightParsedLinks({ message.text, TextUtilities::ConvertTextTagsToEntities(message.tags), - }, + }, links), MTP_messageMediaWebPage( MTP_flags(Flag() | (webpage.forceLargeMedia @@ -287,8 +323,50 @@ rpl::producer PreviewWrap::showLinkSelector( _section = Section::Link; initElement(); + highlightUsedLink(message, usedLink, links); - return rpl::never(); + return _chosenUrl.events(); +} + +void PreviewWrap::highlightUsedLink( + const TextWithTags &message, + const QString &usedLink, + const std::vector &links) { + auto selection = TextSelection(); + const auto view = QStringView(message.text); + for (const auto &range : links) { + auto text = view.mid(range.start, range.length); + if (range.custom == usedLink + || (range.custom.isEmpty() + && range.length == usedLink.size() + && text == usedLink)) { + selection = { + uint16(range.start), + uint16(range.start + range.length), + }; + const auto skip = [](QChar ch) { + return ch.isSpace() || Ui::Text::IsNewline(ch); + }; + while (!text.isEmpty() && skip(text.front())) { + text = text.mid(1); + ++selection.from; + } + while (!text.isEmpty() && skip(text.back())) { + text = text.mid(0, text.size() - 1); + --selection.to; + } + const auto basic = _element->textState(QPoint(0, 0), { + .flags = Ui::Text::StateRequest::Flag::LookupSymbol, + .onlyMessageText = true, + }); + if (basic.symbol > 0) { + selection.from += basic.symbol; + selection.to += basic.symbol; + } + break; + } + } + _selection = selection; } void PreviewWrap::paintEvent(QPaintEvent *e) { @@ -385,7 +463,10 @@ void PreviewWrap::mouseMoveEvent(QMouseEvent *e) { _over = true; const auto text = (_section == Section::Reply) && (resolved.cursor == CursorState::Text); - const auto link = (_section == Section::Link) && resolved.link; + _link = (_section == Section::Link && resolved.overMessageText) + ? resolved.link + : nullptr; + const auto link = (_link != nullptr) || (_pressedLink != nullptr); if (_textCursor != text || _linkCursor != link) { _textCursor = text; _linkCursor = link; @@ -412,13 +493,16 @@ void PreviewWrap::mousePressEvent(QMouseEvent *e) { startSelection(_trippleClickTimer.isActive() ? TextSelectType::Paragraphs : TextSelectType::Letters); + } else { + _pressedLink = _link; } } void PreviewWrap::mouseReleaseEvent(QMouseEvent *e) { - if (!_selecting) { - return; - } else if (_section == Section::Reply) { + if (_section == Section::Reply) { + if (!_selecting) { + return; + } const auto result = resolveNewSelection(); _selecting = false; _selectType = TextSelectType::Letters; @@ -426,6 +510,12 @@ void PreviewWrap::mouseReleaseEvent(QMouseEvent *e) { setCursor(style::cur_default); } _selection = result; + } else if (base::take(_pressedLink) == _link && _link) { + if (const auto url = _link->url(); !url.isEmpty()) { + _chosenUrl.fire_copy(url); + } + } else if (!_link) { + setCursor(style::cur_default); } } @@ -509,6 +599,276 @@ Context PreviewDelegate::elementContext() { return Context::History; } +void AddFilledSkip(not_null container) { + const auto skip = container->add(object_ptr( + container, + st::settingsPrivacySkipTop)); + skip->paintRequest() | rpl::start_with_next([=](QRect clip) { + QPainter(skip).fillRect(clip, st::boxBg); + }, skip->lifetime()); +}; + +void DraftOptionsBox( + not_null box, + EditDraftOptionsArgs &&args, + HistoryItem *replyItem, + WebPageData *previewData) { + box->setWidth(st::boxWideWidth); + + const auto &draft = args.draft; + struct State { + rpl::variable
shown; + rpl::lifetime shownLifetime; + rpl::variable quote; + Data::WebPageDraft webpage; + WebPageData *preview = nullptr; + QString link; + Ui::SettingsSlider *tabs = nullptr; + PreviewWrap *wrap = nullptr; + rpl::lifetime resolveLifetime; + }; + const auto state = box->lifetime().make_state(); + state->quote = draft.reply.quote; + state->webpage = draft.webpage; + state->preview = previewData; + state->shown = previewData ? Section::Link : Section::Reply; + if (replyItem && previewData) { + box->setNoContentMargin(true); + state->tabs = box->setPinnedToTopContent( + object_ptr( + box.get(), + st::defaultTabsSlider)); + state->tabs->resizeToWidth(st::boxWideWidth); + state->tabs->move(0, 0); + state->tabs->setRippleTopRoundRadius(st::boxRadius); + state->tabs->setSections({ + tr::lng_reply_header_short(tr::now), + tr::lng_link_header_short(tr::now), + }); + state->tabs->setActiveSectionFast(1); + state->tabs->sectionActivated( + ) | rpl::start_with_next([=](int section) { + state->shown = section ? Section::Link : Section::Reply; + }, box->lifetime()); + } else { + box->setTitle(previewData + ? tr::lng_link_options_header() + : draft.reply.quote.empty() + ? tr::lng_reply_options_header() + : tr::lng_reply_options_quote()); + } + + const auto bottom = box->setPinnedToBottomContent( + object_ptr(box)); + + const auto &done = args.done; + const auto &show = args.show; + const auto &highlight = args.highlight; + const auto &clearOldDraft = args.clearOldDraft; + const auto resolveReply = [=] { + auto result = draft.reply; + result.quote = state->quote.current(); + return result; + }; + const auto finish = [=]( + FullReplyTo result, + Data::WebPageDraft webpage) { + const auto weak = Ui::MakeWeak(box); + done(std::move(result), std::move(webpage)); + if (const auto strong = weak.data()) { + strong->closeBox(); + } + }; + const auto setupReplyActions = [=] { + AddFilledSkip(bottom); + + Settings::AddButton( + bottom, + tr::lng_reply_in_another_chat(), + st::settingsButton, + { &st::menuIconReplace } + )->setClickedCallback([=] { + ShowReplyToChatBox(show, resolveReply(), clearOldDraft); + }); + + Settings::AddButton( + bottom, + tr::lng_reply_show_in_chat(), + st::settingsButton, + { &st::menuIconShowInChat } + )->setClickedCallback(highlight); + + Settings::AddButton( + bottom, + tr::lng_reply_remove(), + st::settingsAttentionButtonWithIcon, + { &st::menuIconDeleteAttention } + )->setClickedCallback([=] { + finish({}, state->webpage); + }); + + if (!replyItem->originalText().empty()) { + AddFilledSkip(bottom); + Settings::AddDividerText( + bottom, + tr::lng_reply_about_quote()); + } + }; + const auto setupLinkActions = [=] { + AddFilledSkip(bottom); + + if (!draft.textWithTags.empty()) { + Settings::AddButton( + bottom, + (state->webpage.invert + ? tr::lng_link_move_down() + : tr::lng_link_move_up()), + st::settingsButton, + { state->webpage.invert + ? &st::menuIconBelow + : &st::menuIconAbove } + )->setClickedCallback([=] { + state->webpage.invert = !state->webpage.invert; + state->webpage.manual = true; + state->shown.force_assign(Section::Link); + }); + } + + if (state->preview->hasLargeMedia) { + const auto small = state->webpage.forceSmallMedia + || (!state->webpage.forceLargeMedia + && state->preview->computeDefaultSmallMedia()); + Settings::AddButton( + bottom, + (small + ? tr::lng_link_enlarge_photo() + : tr::lng_link_shrink_photo()), + st::settingsButton, + { small ? &st::menuIconEnlarge : &st::menuIconShrink } + )->setClickedCallback([=] { + if (small) { + state->webpage.forceSmallMedia = false; + state->webpage.forceLargeMedia = true; + } else { + state->webpage.forceLargeMedia = false; + state->webpage.forceSmallMedia = true; + } + state->webpage.manual = true; + state->shown.force_assign(Section::Link); + }); + } + + Settings::AddButton( + bottom, + tr::lng_link_remove(), + st::settingsAttentionButtonWithIcon, + { &st::menuIconDeleteAttention } + )->setClickedCallback([=] { + finish(resolveReply(), { .removed = true }); + }); + + if (args.links.size() > 1) { + AddFilledSkip(bottom); + Settings::AddDividerText( + bottom, + tr::lng_link_about_choose()); + } + }; + + const auto &resolver = args.resolver; + const auto performSwitch = [=](const QString &link, WebPageData *page) { + if (page) { + state->preview = page; + state->webpage.id = page->id; + state->webpage.url = page->url; + state->webpage.manual = true; + state->link = link; + state->shown.force_assign(Section::Link); + } else { + show->showToast(u"Could not generate preview for this link."_q); + } + }; + const auto switchTo = [=](const QString &link) { + if (link == state->link) { + return; + } + if (const auto value = resolver->lookup(link)) { + performSwitch(link, *value); + } else { + resolver->request(link); + state->resolveLifetime = resolver->resolved( + ) | rpl::start_with_next([=](const QString &resolved) { + if (resolved == link) { + state->resolveLifetime.destroy(); + performSwitch( + link, + resolver->lookup(link).value_or(nullptr)); + } + }); + } + }; + + state->wrap = box->addRow( + object_ptr(box, args.history), + {}); + const auto &linkRanges = args.links; + state->shown.value() | rpl::start_with_next([=](Section shown) { + bottom->clear(); + state->shownLifetime.destroy(); + if (shown == Section::Reply) { + state->quote = state->wrap->showQuoteSelector( + replyItem, + state->quote.current()); + setupReplyActions(); + } else { + state->wrap->showLinkSelector( + draft.textWithTags, + state->webpage, + linkRanges, + state->link + ) | rpl::start_with_next([=](QString link) { + switchTo(link); + }, state->shownLifetime); + setupLinkActions(); + } + }, box->lifetime()); + + auto save = rpl::combine( + state->quote.value(), + state->shown.value() + ) | rpl::map([=](const TextWithEntities "e, Section shown) { + return (quote.empty() || shown != Section::Reply) + ? tr::lng_settings_save() + : tr::lng_reply_quote_selected(); + }) | rpl::flatten_latest(); + box->addButton(std::move(save), [=] { + finish(resolveReply(), state->webpage); + }); + + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); + + if (replyItem) { + args.show->session().data().itemRemoved( + ) | rpl::filter([=](not_null removed) { + return removed == replyItem; + }) | rpl::start_with_next([=] { + if (previewData) { + state->tabs = nullptr; + box->setPinnedToTopContent( + object_ptr(nullptr)); + box->setNoContentMargin(false); + box->setTitle(state->quote.current().empty() + ? tr::lng_reply_options_header() + : tr::lng_reply_options_quote()); + state->shown = Section::Link; + } else { + box->closeBox(); + } + }, box->lifetime()); + } +} } // namespace void ShowReplyToChatBox( @@ -603,14 +963,9 @@ void ShowReplyToChatBox( ) | rpl::start_with_next(std::move(callback), state->box->lifetime()); } -void EditDraftOptions( - std::shared_ptr show, - not_null history, - Data::Draft draft, - Fn done, - Fn highlight, - Fn clearOldDraft) { - const auto session = &show->session(); +void EditDraftOptions(EditDraftOptionsArgs &&args) { + const auto &draft = args.draft; + const auto session = &args.show->session(); const auto replyItem = session->data().message(draft.reply.messageId); const auto previewDataRaw = draft.webpage.id ? session->data().webpage(draft.webpage.id).get() @@ -623,225 +978,8 @@ void EditDraftOptions( if (!replyItem && !previewData) { return; } - show->show(Box([=](not_null box) { - box->setWidth(st::boxWideWidth); - - struct State { - rpl::variable
shown; - rpl::lifetime shownLifetime; - rpl::variable quote; - Data::WebPageDraft webpage; - Ui::SettingsSlider *tabs = nullptr; - PreviewWrap *wrap = nullptr; - }; - const auto state = box->lifetime().make_state(); - state->quote = draft.reply.quote; - state->webpage = draft.webpage; - state->shown = previewData ? Section::Link : Section::Reply; - if (replyItem && previewData) { - box->setNoContentMargin(true); - state->tabs = box->setPinnedToTopContent( - object_ptr( - box.get(), - st::defaultTabsSlider)); - state->tabs->resizeToWidth(st::boxWideWidth); - state->tabs->move(0, 0); - state->tabs->setRippleTopRoundRadius(st::boxRadius); - state->tabs->setSections({ - tr::lng_reply_header_short(tr::now), - tr::lng_link_header_short(tr::now), - }); - state->tabs->setActiveSectionFast(1); - state->tabs->sectionActivated( - ) | rpl::start_with_next([=](int section) { - state->shown = section ? Section::Link : Section::Reply; - }, box->lifetime()); - } else { - box->setTitle(previewData - ? tr::lng_link_options_header() - : draft.reply.quote.empty() - ? tr::lng_reply_options_header() - : tr::lng_reply_options_quote()); - } - - const auto bottom = box->setPinnedToBottomContent( - object_ptr(box)); - const auto addSkip = [=] { - const auto skip = bottom->add(object_ptr( - bottom, - st::settingsPrivacySkipTop)); - skip->paintRequest() | rpl::start_with_next([=](QRect clip) { - QPainter(skip).fillRect(clip, st::boxBg); - }, skip->lifetime()); - }; - - const auto resolveReply = [=] { - auto result = draft.reply; - result.quote = state->quote.current(); - return result; - }; - const auto finish = [=]( - FullReplyTo result, - Data::WebPageDraft webpage) { - const auto weak = Ui::MakeWeak(box); - done(std::move(result), std::move(webpage)); - if (const auto strong = weak.data()) { - strong->closeBox(); - } - }; - const auto setupReplyActions = [=] { - addSkip(); - - Settings::AddButton( - bottom, - tr::lng_reply_in_another_chat(), - st::settingsButton, - { &st::menuIconReplace } - )->setClickedCallback([=] { - ShowReplyToChatBox(show, resolveReply(), clearOldDraft); - }); - - Settings::AddButton( - bottom, - tr::lng_reply_show_in_chat(), - st::settingsButton, - { &st::menuIconShowInChat } - )->setClickedCallback(highlight); - - Settings::AddButton( - bottom, - tr::lng_reply_remove(), - st::settingsAttentionButtonWithIcon, - { &st::menuIconDeleteAttention } - )->setClickedCallback([=] { - finish({}, state->webpage); - }); - - if (!replyItem->originalText().empty()) { - addSkip(); - Settings::AddDividerText( - bottom, - tr::lng_reply_about_quote()); - } - }; - const auto setupLinkActions = [=] { - addSkip(); - - if (!draft.textWithTags.empty()) { - Settings::AddButton( - bottom, - (state->webpage.invert - ? tr::lng_link_move_down() - : tr::lng_link_move_up()), - st::settingsButton, - { state->webpage.invert - ? &st::menuIconBelow - : &st::menuIconAbove } - )->setClickedCallback([=] { - state->webpage.invert = !state->webpage.invert; - state->webpage.manual = true; - state->shown.force_assign(Section::Link); - }); - } - - if (previewData->hasLargeMedia) { - const auto small = state->webpage.forceSmallMedia - || (!state->webpage.forceLargeMedia - && previewData->computeDefaultSmallMedia()); - Settings::AddButton( - bottom, - (small - ? tr::lng_link_enlarge_photo() - : tr::lng_link_shrink_photo()), - st::settingsButton, - { small ? &st::menuIconEnlarge : &st::menuIconShrink } - )->setClickedCallback([=] { - if (small) { - state->webpage.forceSmallMedia = false; - state->webpage.forceLargeMedia = true; - } else { - state->webpage.forceLargeMedia = false; - state->webpage.forceSmallMedia = true; - } - state->webpage.manual = true; - state->shown.force_assign(Section::Link); - }); - } - - Settings::AddButton( - bottom, - tr::lng_link_remove(), - st::settingsAttentionButtonWithIcon, - { &st::menuIconDeleteAttention } - )->setClickedCallback([=] { - finish(resolveReply(), { .removed = true }); - }); - - if (true) { - addSkip(); - Settings::AddDividerText( - bottom, - tr::lng_link_about_choose()); - } - }; - - state->wrap = box->addRow( - object_ptr(box, history), - {}); - state->shown.value() | rpl::start_with_next([=](Section shown) { - bottom->clear(); - state->shownLifetime.destroy(); - if (shown == Section::Reply) { - state->quote = state->wrap->showQuoteSelector( - replyItem, - state->quote.current()); - setupReplyActions(); - } else { - state->wrap->showLinkSelector( - draft.textWithTags, - state->webpage - ) | rpl::start_with_next([=](QString url) { - }, state->shownLifetime); - setupLinkActions(); - } - }, box->lifetime()); - - auto save = rpl::combine( - state->quote.value(), - state->shown.value() - ) | rpl::map([=](const TextWithEntities "e, Section shown) { - return (quote.empty() || shown != Section::Reply) - ? tr::lng_settings_save() - : tr::lng_reply_quote_selected(); - }) | rpl::flatten_latest(); - box->addButton(std::move(save), [=] { - finish(resolveReply(), state->webpage); - }); - - box->addButton(tr::lng_cancel(), [=] { - box->closeBox(); - }); - - if (replyItem) { - session->data().itemRemoved( - ) | rpl::filter([=](not_null removed) { - return removed == replyItem; - }) | rpl::start_with_next([=] { - if (previewData) { - state->tabs = nullptr; - box->setPinnedToTopContent( - object_ptr(nullptr)); - box->setNoContentMargin(false); - box->setTitle(state->quote.current().empty() - ? tr::lng_reply_options_header() - : tr::lng_reply_options_quote()); - state->shown = Section::Link; - } else { - box->closeBox(); - } - }, box->lifetime()); - } - })); + args.show->show( + Box(DraftOptionsBox, std::move(args), replyItem, previewData)); } } // namespace HistoryView::Controls diff --git a/Telegram/SourceFiles/history/view/controls/history_view_draft_options.h b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.h index 6e3def8cc..798e1fa0c 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_draft_options.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.h @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_drafts.h" class History; +struct MessageLinkRange; namespace ChatHelpers { class Show; @@ -21,13 +22,21 @@ class SessionController; namespace HistoryView::Controls { -void EditDraftOptions( - std::shared_ptr show, - not_null history, - Data::Draft draft, - Fn done, - Fn highlight, - Fn clearOldDraft); +class WebpageResolver; + +struct EditDraftOptionsArgs { + std::shared_ptr show; + not_null history; + Data::Draft draft; + QString usedLink; + std::vector links; + std::shared_ptr resolver; + Fn done; + Fn highlight; + Fn clearOldDraft; +}; + +void EditDraftOptions(EditDraftOptionsArgs &&args); void ShowReplyToChatBox( std::shared_ptr show, diff --git a/Telegram/SourceFiles/history/view/controls/history_view_webpage_processor.cpp b/Telegram/SourceFiles/history/view/controls/history_view_webpage_processor.cpp index c75accfb2..50abe57bd 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_webpage_processor.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_webpage_processor.cpp @@ -110,17 +110,88 @@ WebPageText ProcessWebPageData(WebPageData *page) { return previewText; } +WebpageResolver::WebpageResolver(not_null session) +: _session(session) +, _api(&session->mtp()) { +} + +std::optional WebpageResolver::lookup( + const QString &link) const { + const auto i = _cache.find(link); + return (i == end(_cache)) + ? std::optional() + : (i->second && !i->second->failed) + ? i->second + : nullptr; +} + +QString WebpageResolver::find(not_null page) const { + for (const auto &[link, cached] : _cache) { + if (cached == page) { + return link; + } + } + return QString(); +} + +void WebpageResolver::request(const QString &link) { + if (_requestLink == link) { + return; + } + const auto done = [=](const MTPDmessageMediaWebPage &data) { + const auto page = _session->data().processWebpage(data.vwebpage()); + if (page->pendingTill > 0 + && page->pendingTill < base::unixtime::now()) { + page->pendingTill = 0; + page->failed = true; + } + _cache.emplace(link, page->failed ? nullptr : page.get()); + _resolved.fire_copy(link); + }; + const auto fail = [=] { + _cache.emplace(link, nullptr); + _resolved.fire_copy(link); + }; + _requestLink = link; + _requestId = _api.request( + MTPmessages_GetWebPagePreview( + MTP_flags(0), + MTP_string(link), + MTPVector() + )).done([=](const MTPMessageMedia &result, mtpRequestId requestId) { + if (_requestId == requestId) { + _requestId = 0; + } + result.match([=](const MTPDmessageMediaWebPage &data) { + done(data); + }, [&](const auto &d) { + fail(); + }); + }).fail([=](const MTP::Error &error, mtpRequestId requestId) { + if (_requestId == requestId) { + _requestId = 0; + } + fail(); + }).send(); +} + +void WebpageResolver::cancel(const QString &link) { + if (_requestLink == link) { + _api.request(base::take(_requestId)).cancel(); + } +} + WebpageProcessor::WebpageProcessor( not_null history, not_null field) : _history(history) -, _api(&history->session().mtp()) +, _resolver(std::make_shared(&history->session())) , _parser(field) , _timer([=] { if (!ShowWebPagePreview(_data) || _link.isEmpty()) { return; } - request(); + _resolver->request(_link); }) { _history->session().downloaderTaskFinished( ) | rpl::filter([=] { @@ -141,6 +212,23 @@ WebpageProcessor::WebpageProcessor( _parsedLinks = std::move(parsed); checkPreview(); }, _lifetime); + + _resolver->resolved() | rpl::start_with_next([=](QString link) { + if (_link != link + || _draft.removed + || (_draft.manual && _draft.url != link)) { + return; + } + _data = _resolver->lookup(link).value_or(nullptr); + if (_data) { + _draft.id = _data->id; + _draft.url = _data->url; + updateFromData(); + } else { + _links = QStringList(); + checkPreview(); + } + }, _lifetime); } rpl::producer<> WebpageProcessor::repaintRequests() const { @@ -151,8 +239,20 @@ Data::WebPageDraft WebpageProcessor::draft() const { return _draft; } +std::shared_ptr WebpageProcessor::resolver() const { + return _resolver; +} + +const std::vector &WebpageProcessor::links() const { + return _parser.ranges(); +} + +QString WebpageProcessor::link() const { + return _link; +} + void WebpageProcessor::apply(Data::WebPageDraft draft, bool reparse) { - _api.request(base::take(_requestId)).cancel(); + const auto was = _link; if (draft.removed) { _draft = draft; if (_parsedLinks.empty()) { @@ -173,14 +273,21 @@ void WebpageProcessor::apply(Data::WebPageDraft draft, bool reparse) { : nullptr; if (page && page->url == draft.url) { _data = page; + if (const auto link = _resolver->find(page); !link.isEmpty()) { + _link = link; + } updateFromData(); } else { - request(); + _resolver->request(_link); + return; } } else if (!draft.manual && !_draft.manual) { _draft = draft; checkNow(reparse); } + if (_link != was) { + _resolver->cancel(was); + } } void WebpageProcessor::updateFromData() { @@ -212,56 +319,6 @@ void WebpageProcessor::updateFromData() { _repaintRequests.fire({}); } -void WebpageProcessor::request() { - const auto link = _link; - const auto done = [=](const MTPDmessageMediaWebPage &data) { - const auto page = _history->owner().processWebpage(data.vwebpage()); - if (page->pendingTill > 0 - && page->pendingTill < base::unixtime::now()) { - page->pendingTill = 0; - page->failed = true; - } - _cache.emplace(link, page->failed ? nullptr : page.get()); - if (_link == link - && !_draft.removed - && (!_draft.manual || _draft.url == link)) { - _data = (page->id && !page->failed) - ? page.get() - : nullptr; - _draft.id = page->id; - _draft.url = page->url; - updateFromData(); - } - }; - const auto fail = [=] { - _cache.emplace(link, nullptr); - if (_link == link && !_draft.removed && !_draft.manual) { - _links = QStringList(); - checkPreview(); - } - }; - _requestId = _api.request( - MTPmessages_GetWebPagePreview( - MTP_flags(0), - MTP_string(_link), - MTPVector() - )).done([=](const MTPMessageMedia &result, mtpRequestId requestId) { - if (_requestId == requestId) { - _requestId = 0; - } - result.match([=](const MTPDmessageMediaWebPage &data) { - done(data); - }, [&](const auto &d) { - fail(); - }); - }).fail([=](const MTP::Error &error, mtpRequestId requestId) { - if (_requestId == requestId) { - _requestId = 0; - } - fail(); - }).send(); -} - void WebpageProcessor::setDisabled(bool disabled) { _parser.setDisabled(disabled); if (disabled) { @@ -307,25 +364,21 @@ void WebpageProcessor::checkPreview() { auto page = (WebPageData*)nullptr; auto chosen = QString(); for (const auto &link : _links) { - const auto i = _cache.find(link); - if (i == end(_cache)) { + const auto value = _resolver->lookup(link); + if (!value) { chosen = link; break; - } else if (i->second) { - if (i->second->failed) { - i->second = nullptr; - } else { - chosen = link; - page = i->second; - break; - } + } else if (*value) { + chosen = link; + page = *value; + break; } } if (_link != chosen) { + _resolver->cancel(_link); _link = chosen; - _api.request(base::take(_requestId)).cancel(); if (!page && !_link.isEmpty()) { - request(); + _resolver->request(_link); } } if (page) { diff --git a/Telegram/SourceFiles/history/view/controls/history_view_webpage_processor.h b/Telegram/SourceFiles/history/view/controls/history_view_webpage_processor.h index 2c356eedc..f27d1ad6c 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_webpage_processor.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_webpage_processor.h @@ -7,13 +7,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -#include "base/weak_ptr.h" #include "data/data_drafts.h" #include "chat_helpers/message_field.h" #include "mtproto/sender.h" class History; +namespace Main { +class Session; +} // namespace Main + namespace Ui { class InputField; } // namespace Ui @@ -47,7 +50,33 @@ struct WebpageParsed { } }; -class WebpageProcessor final : public base::has_weak_ptr { +class WebpageResolver final { +public: + explicit WebpageResolver(not_null session); + + [[nodiscard]] std::optional lookup( + const QString &link) const; + [[nodiscard]] rpl::producer resolved() const { + return _resolved.events(); + } + + [[nodiscard]] QString find(not_null page) const; + + void request(const QString &link); + void cancel(const QString &link); + +private: + const not_null _session; + MTP::Sender _api; + base::flat_map _cache; + rpl::event_stream _resolved; + + QString _requestLink; + mtpRequestId _requestId = 0; + +}; + +class WebpageProcessor final { public: WebpageProcessor( not_null history, @@ -63,6 +92,9 @@ public: // unless preview was removed in the draft or manual. void apply(Data::WebPageDraft draft, bool reparse = true); [[nodiscard]] Data::WebPageDraft draft() const; + [[nodiscard]] std::shared_ptr resolver() const; + [[nodiscard]] const std::vector &links() const; + [[nodiscard]] QString link() const; [[nodiscard]] rpl::producer<> repaintRequests() const; [[nodiscard]] rpl::producer parsedValue() const; @@ -74,21 +106,17 @@ public: private: void updateFromData(); void checkPreview(); - void request(); const not_null _history; - MTP::Sender _api; + const std::shared_ptr _resolver; MessageLinksParser _parser; QStringList _parsedLinks; QStringList _links; QString _link; WebPageData *_data = nullptr; - base::flat_map _cache; Data::WebPageDraft _draft; - mtpRequestId _requestId = 0; - rpl::event_stream<> _repaintRequests; rpl::variable _parsed; diff --git a/Telegram/SourceFiles/history/view/history_view_cursor_state.h b/Telegram/SourceFiles/history/view/history_view_cursor_state.h index 80cec91a9..82fcda954 100644 --- a/Telegram/SourceFiles/history/view/history_view_cursor_state.h +++ b/Telegram/SourceFiles/history/view/history_view_cursor_state.h @@ -49,6 +49,7 @@ struct TextState { FullMsgId itemId; CursorState cursor = CursorState::None; ClickHandlerPtr link; + bool overMessageText = false; bool afterSymbol = false; bool customTooltip = false; uint16 symbol = 0; diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index c35d18d7a..042cfb21b 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -2195,6 +2195,7 @@ TextState Message::textState( if (_invertMedia) { result.symbol += visibleMediaTextLength(); } + result.overMessageText = true; checkBottomInfoState(); return result; } else if (point.y() >= trect.y() + trect.height()) {