diff --git a/Telegram/SourceFiles/api/api_bot.cpp b/Telegram/SourceFiles/api/api_bot.cpp index 42847eca3a..823047abdc 100644 --- a/Telegram/SourceFiles/api/api_bot.cpp +++ b/Telegram/SourceFiles/api/api_bot.cpp @@ -54,8 +54,7 @@ namespace { void SendBotCallbackData( not_null controller, not_null item, - int row, - int column, + BotButtonLookup lookup, std::optional password, Fn done = nullptr, Fn handleError = nullptr) { @@ -68,10 +67,7 @@ void SendBotCallbackData( const auto api = &session->api(); const auto bot = item->getMessageBot(); const auto fullId = item->fullId(); - const auto getButton = [=] { - return HistoryMessageMarkupButton::Get(owner, fullId, row, column); - }; - const auto button = getButton(); + const auto button = lookup(); if (!button || button->requestId) { return; } @@ -117,7 +113,7 @@ void SendBotCallbackData( if (!item) { return; } - if (const auto button = getButton()) { + if (const auto button = lookup()) { button->requestId = 0; owner->requestItemRepaint(item); } @@ -169,7 +165,7 @@ void SendBotCallbackData( return; } // Show error? - if (const auto button = getButton()) { + if (const auto button = lookup()) { button->requestId = 0; owner->requestItemRepaint(item); } @@ -208,16 +204,14 @@ void HideSingleUseKeyboard( void SendBotCallbackData( not_null controller, not_null item, - int row, - int column) { - SendBotCallbackData(controller, item, row, column, std::nullopt); + BotButtonLookup lookup) { + SendBotCallbackData(controller, item, std::move(lookup), std::nullopt); } void SendBotCallbackDataWithPassword( not_null controller, not_null item, - int row, - int column) { + BotButtonLookup lookup) { if (!item->isRegular()) { return; } @@ -226,21 +220,13 @@ void SendBotCallbackDataWithPassword( const auto owner = &history->owner(); const auto api = &session->api(); const auto fullId = item->fullId(); - const auto getButton = [=] { - return HistoryMessageMarkupButton::Get( - owner, - fullId, - row, - column); - }; - const auto button = getButton(); - if (!button || button->requestId) { + if (const auto button = lookup(); !button || button->requestId) { return; } api->cloudPassword().reload(); const auto weak = base::make_weak(controller); const auto show = controller->uiShow(); - SendBotCallbackData(controller, item, row, column, {}, {}, [=]( + SendBotCallbackData(controller, item, lookup, {}, {}, [=]( const QString &error) { auto box = PrePasswordErrorBox( error, @@ -252,7 +238,9 @@ void SendBotCallbackDataWithPassword( show->showBox(std::move(box), Ui::LayerOption::CloseOther); } else { auto lifetime = std::make_shared(); - button->requestId = -1; + if (const auto button = lookup()) { + button->requestId = -1; + } api->cloudPassword().state( ) | rpl::take( 1 @@ -260,7 +248,7 @@ void SendBotCallbackDataWithPassword( if (lifetime) { base::take(lifetime)->destroy(); } - if (const auto button = getButton()) { + if (const auto button = lookup()) { if (button->requestId == -1) { button->requestId = 0; } @@ -275,7 +263,7 @@ void SendBotCallbackDataWithPassword( fields.customCheckCallback = [=]( const Core::CloudPasswordResult &result, base::weak_qptr box) { - if (const auto button = getButton()) { + if (const auto button = lookup()) { if (button->requestId) { return; } @@ -287,15 +275,21 @@ void SendBotCallbackDataWithPassword( if (!strongController) { return; } - SendBotCallbackData(strongController, item, row, column, result, [=] { - if (box) { - box->closeBox(); - } - }, [=](const QString &error) { - if (box) { - box->handleCustomCheckError(error); - } - }); + SendBotCallbackData( + strongController, + item, + lookup, + result, + [=] { + if (box) { + box->closeBox(); + } + }, + [=](const QString &error) { + if (box) { + box->handleCustomCheckError(error); + } + }); } }; auto object = Box(session, fields); @@ -316,7 +310,7 @@ bool SwitchInlineBotButtonReceived( samePeerReplyTo); } -void ActivateBotCommand(ClickHandlerContext context, int row, int column) { +void ActivateBotButton(ClickHandlerContext context, BotButtonLookup lookup) { const auto strong = context.sessionWindow.get(); if (!strong) { return; @@ -326,11 +320,7 @@ void ActivateBotCommand(ClickHandlerContext context, int row, int column) { if (!item) { return; } - const auto button = HistoryMessageMarkupButton::Get( - &item->history()->owner(), - item->fullId(), - row, - column); + const auto button = lookup(); if (!button) { return; } @@ -353,11 +343,11 @@ void ActivateBotCommand(ClickHandlerContext context, int row, int column) { case ButtonType::Callback: case ButtonType::Game: { - SendBotCallbackData(controller, item, row, column); + SendBotCallbackData(controller, item, lookup); } break; case ButtonType::CallbackWithPassword: { - SendBotCallbackDataWithPassword(controller, item, row, column); + SendBotCallbackDataWithPassword(controller, item, lookup); } break; case ButtonType::Buy: { @@ -514,7 +504,7 @@ void ActivateBotCommand(ClickHandlerContext context, int row, int column) { } break; case ButtonType::Auth: - UrlAuthBox::ActivateButton(controller->uiShow(), item, row, column); + UrlAuthBox::ActivateButton(controller->uiShow(), item, lookup); break; case ButtonType::UserProfile: { @@ -636,4 +626,16 @@ void ActivateBotCommand(ClickHandlerContext context, int row, int column) { } } +void ActivateBotCommand(ClickHandlerContext context, int row, int column) { + const auto strong = context.sessionWindow.get(); + if (!strong) { + return; + } + const auto owner = &strong->session().data(); + const auto itemId = context.itemId; + ActivateBotButton(context, [=] { + return HistoryMessageMarkupButton::Get(owner, itemId, row, column); + }); +} + } // namespace Api diff --git a/Telegram/SourceFiles/api/api_bot.h b/Telegram/SourceFiles/api/api_bot.h index 7e26dd1039..e8907f54b0 100644 --- a/Telegram/SourceFiles/api/api_bot.h +++ b/Telegram/SourceFiles/api/api_bot.h @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL struct ClickHandlerContext; class HistoryItem; +struct HistoryMessageMarkupButton; namespace Window { class SessionController; @@ -16,17 +17,17 @@ class SessionController; namespace Api { +using BotButtonLookup = Fn; + void SendBotCallbackData( not_null controller, not_null item, - int row, - int column); + BotButtonLookup lookup); void SendBotCallbackDataWithPassword( not_null controller, not_null item, - int row, - int column); + BotButtonLookup lookup); bool SwitchInlineBotButtonReceived( not_null controller, @@ -34,6 +35,7 @@ bool SwitchInlineBotButtonReceived( UserData *samePeerBot = nullptr, MsgId samePeerReplyTo = 0); +void ActivateBotButton(ClickHandlerContext context, BotButtonLookup lookup); void ActivateBotCommand(ClickHandlerContext context, int row, int column); } // namespace Api diff --git a/Telegram/SourceFiles/boxes/url_auth_box.cpp b/Telegram/SourceFiles/boxes/url_auth_box.cpp index 0d027d105c..b7c0ec0ce4 100644 --- a/Telegram/SourceFiles/boxes/url_auth_box.cpp +++ b/Telegram/SourceFiles/boxes/url_auth_box.cpp @@ -206,8 +206,7 @@ void RequestButton( std::shared_ptr show, const MTPDurlAuthResultRequest &request, not_null message, - int row, - int column); + Api::BotButtonLookup lookup); void RequestUrl( std::shared_ptr show, const MTPDurlAuthResultRequest &request, @@ -218,15 +217,10 @@ void RequestUrl( void ActivateButton( std::shared_ptr show, not_null message, - int row, - int column) { + Api::BotButtonLookup lookup) { const auto itemId = message->fullId(); - const auto button = HistoryMessageMarkupButton::Get( - &message->history()->owner(), - itemId, - row, - column); - if (button->requestId || !message->isRegular()) { + const auto button = lookup(); + if (!button || button->requestId || !message->isRegular()) { return; } const auto session = &message->history()->session(); @@ -243,11 +237,7 @@ void ActivateButton( MTPstring(), // #TODO auth url MTPstring() // in_app_origin )).done([=](const MTPUrlAuthResult &result) { - const auto button = HistoryMessageMarkupButton::Get( - &session->data(), - itemId, - row, - column); + const auto button = lookup(); if (!button) { return; } @@ -261,15 +251,11 @@ void ActivateButton( HiddenUrlClickHandler::Open(url); }, [&](const MTPDurlAuthResultRequest &data) { if (const auto item = session->data().message(itemId)) { - RequestButton(show, data, item, row, column); + RequestButton(show, data, item, lookup); } }); }).fail([=] { - const auto button = HistoryMessageMarkupButton::Get( - &session->data(), - itemId, - row, - column); + const auto button = lookup(); if (!button) { return; } @@ -320,14 +306,9 @@ void RequestButton( std::shared_ptr show, const MTPDurlAuthResultRequest &request, not_null message, - int row, - int column) { + Api::BotButtonLookup lookup) { const auto itemId = message->fullId(); - const auto button = HistoryMessageMarkupButton::Get( - &message->history()->owner(), - itemId, - row, - column); + const auto button = lookup(); if (!button || button->requestId || !message->isRegular()) { return; } diff --git a/Telegram/SourceFiles/boxes/url_auth_box.h b/Telegram/SourceFiles/boxes/url_auth_box.h index 09416d0908..40337b40a7 100644 --- a/Telegram/SourceFiles/boxes/url_auth_box.h +++ b/Telegram/SourceFiles/boxes/url_auth_box.h @@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "api/api_bot.h" + class HistoryItem; struct HistoryMessageMarkupButton; @@ -24,8 +26,7 @@ namespace UrlAuthBox { void ActivateButton( std::shared_ptr show, not_null message, - int row, - int column); + Api::BotButtonLookup lookup); void ActivateUrl( std::shared_ptr show, not_null session, diff --git a/Telegram/SourceFiles/data/data_chat_participant_status.h b/Telegram/SourceFiles/data/data_chat_participant_status.h index 815baefc7f..99aa044186 100644 --- a/Telegram/SourceFiles/data/data_chat_participant_status.h +++ b/Telegram/SourceFiles/data/data_chat_participant_status.h @@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +class PeerData; + namespace ChatHelpers { class Show; } // namespace ChatHelpers diff --git a/Telegram/SourceFiles/history/history_item_reply_markup.cpp b/Telegram/SourceFiles/history/history_item_reply_markup.cpp index 0c80e9ce67..cd5ab01ac1 100644 --- a/Telegram/SourceFiles/history/history_item_reply_markup.cpp +++ b/Telegram/SourceFiles/history/history_item_reply_markup.cpp @@ -40,6 +40,86 @@ namespace { } // namespace +HistoryMessageMarkupButton::Visual ParseRichButtonVisual( + const tl::conditional &style) { + if (!style) { + return {}; + } + using Color = HistoryMessageMarkupButton::Color; + const auto &data = style->data(); + return { + .color = (data.is_bg_danger() + ? Color::Danger + : data.is_bg_primary() + ? Color::Primary + : data.is_bg_success() + ? Color::Success + : Color::Normal), + }; +} + +std::optional ParseInlineButton( + const MTPInlineButtonType &type, + const QString &text, + HistoryMessageMarkupButton::Visual visual) { + using Type = HistoryMessageMarkupButton::Type; + auto result = std::optional(); + type.match([&](const MTPDinlineButtonTypeUrl &data) { + result.emplace(Type::Url, text, visual, qba(data.vurl())); + }, [&](const MTPDinlineButtonTypeUrlAuth &data) { + result.emplace( + Type::Auth, + text, + visual, + qba(data.vurl()), + qs(data.vfwd_text().value_or_empty()), + data.vbutton_id().v); + }, [&](const MTPDinputInlineButtonTypeUrlAuth &) { + LOG(("API Error: inputInlineButtonTypeUrlAuth.")); + // Should not get those for the users. + }, [&](const MTPDinlineButtonTypeWebView &data) { + result.emplace(Type::WebView, text, visual, data.vurl().v); + }, [&](const MTPDinlineButtonTypeCallback &data) { + result.emplace( + (data.is_requires_password() + ? Type::CallbackWithPassword + : Type::Callback), + text, + visual, + qba(data.vdata())); + }, [&](const MTPDinlineButtonTypeGame &) { + result.emplace(Type::Game, text, visual); + }, [&](const MTPDinlineButtonTypeBuy &) { + result.emplace(Type::Buy, text, visual); + }, [&](const MTPDinlineButtonTypeSwitchInline &data) { + const auto samePeer = data.is_same_peer(); + result.emplace( + (samePeer ? Type::SwitchInlineSame : Type::SwitchInline), + text, + visual, + qba(data.vquery())); + if (!samePeer) { + if (const auto types = data.vpeer_types()) { + result->peerTypes = PeerTypesFromMTP(*types); + } + } + }, [&](const MTPDinlineButtonTypeUserProfile &data) { + result.emplace( + Type::UserProfile, + text, + visual, + QByteArray::number(data.vuser_id().v)); + }, [&](const MTPDinputInlineButtonTypeUserProfile &) { + LOG(("API Error: inputInlineButtonTypeUserProfile.")); + // Should not get those for the users. + }, [&](const MTPDinlineButtonTypeCopy &data) { + result.emplace(Type::CopyText, text, visual, data.vcopy_text().v); + }, [&](const MTPDinlineButtonTypeDisabled &) { + result.emplace(Type::Disabled, text, visual); + }); + return result; +} + RequestPeerQuery RequestPeerQueryFromTL( const MTPDbuttonTypeRequestPeer &query) { using Type = RequestPeerQuery::Type; @@ -116,6 +196,18 @@ HistoryMessageMarkupButton::HistoryMessageMarkupButton( , buttonId(buttonId) { } +bool operator==( + const HistoryMessageMarkupButton &a, + const HistoryMessageMarkupButton &b) { + return (a.type == b.type) + && (a.visual == b.visual) + && (a.text == b.text) + && (a.forwardText == b.forwardText) + && (a.data == b.data) + && (a.buttonId == b.buttonId) + && (a.peerTypes == b.peerTypes); +} + HistoryMessageMarkupButton *HistoryMessageMarkupButton::Get( not_null owner, FullMsgId itemId, @@ -218,80 +310,19 @@ void HistoryMessageMarkupData::fillRows( data.vurl().v); }); }, [&](const MTPDkeyboardInlineButton &data) { - const auto text = qs(data.vtext()); - const auto visual = ParseVisual(data.vstyle()); - data.vtype().match([&]( - const MTPDinlineButtonTypeUrl &data) { - row.emplace_back( - Type::Url, - text, - visual, - qba(data.vurl())); - }, [&](const MTPDinlineButtonTypeUrlAuth &data) { - row.emplace_back( - Type::Auth, - text, - visual, - qba(data.vurl()), - qs(data.vfwd_text().value_or_empty()), - data.vbutton_id().v); - }, [&](const MTPDinputInlineButtonTypeUrlAuth &) { - LOG(("API Error: inputInlineButtonTypeUrlAuth.")); - // Should not get those for the users. - }, [&](const MTPDinlineButtonTypeWebView &data) { - row.emplace_back( - Type::WebView, - text, - visual, - data.vurl().v); - }, [&](const MTPDinlineButtonTypeCallback &data) { - row.emplace_back( - (data.is_requires_password() - ? Type::CallbackWithPassword - : Type::Callback), - text, - visual, - qba(data.vdata())); - }, [&](const MTPDinlineButtonTypeGame &) { - row.emplace_back(Type::Game, text, visual); - }, [&](const MTPDinlineButtonTypeBuy &) { - row.emplace_back(Type::Buy, text, visual); - }, [&](const MTPDinlineButtonTypeSwitchInline &data) { - const auto type = data.is_same_peer() - ? Type::SwitchInlineSame - : Type::SwitchInline; - row.emplace_back( - type, - text, - visual, - qba(data.vquery())); - if (type == Type::SwitchInline) { - // Optimization flag. - // Fast check on all new messages if there is a switch button to auto-click it. - flags |= ReplyMarkupFlag::HasSwitchInlineButton; - if (const auto types = data.vpeer_types()) { - row.back().peerTypes = PeerTypesFromMTP( - *types); - } - } - }, [&](const MTPDinlineButtonTypeUserProfile &data) { - row.emplace_back( - Type::UserProfile, - text, - visual, - QByteArray::number(data.vuser_id().v)); - }, [&](const MTPDinputInlineButtonTypeUserProfile &) { - LOG(("API Error: inputInlineButtonTypeUserProfile.")); - // Should not get those for the users. - }, [&](const MTPDinlineButtonTypeCopy &data) { - row.emplace_back( - Type::CopyText, - text, - visual, - data.vcopy_text().v); - }, [&](const MTPDinlineButtonTypeDisabled &) { - row.emplace_back(Type::Disabled, text, visual); - }); + auto button = ParseInlineButton( + data.vtype(), + qs(data.vtext()), + ParseVisual(data.vstyle())); + if (!button) { + return; + } + if (button->type == Type::SwitchInline) { + // Optimization flag. + // Fast check on all new messages if there is a switch button to auto-click it. + flags |= ReplyMarkupFlag::HasSwitchInlineButton; + } + row.push_back(std::move(*button)); }); } if (!row.empty()) { diff --git a/Telegram/SourceFiles/history/history_item_reply_markup.h b/Telegram/SourceFiles/history/history_item_reply_markup.h index 808915ee23..33e82135eb 100644 --- a/Telegram/SourceFiles/history/history_item_reply_markup.h +++ b/Telegram/SourceFiles/history/history_item_reply_markup.h @@ -9,6 +9,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/flags.h" #include "data/data_chat_participant_status.h" +#include "data/data_types.h" + +#include namespace Api { struct SendOptions; @@ -111,6 +114,10 @@ struct HistoryMessageMarkupButton { struct Visual { DocumentId iconId = 0; Color color = Color::Normal; + + friend inline bool operator==( + const Visual &, + const Visual &) = default; }; HistoryMessageMarkupButton( @@ -137,6 +144,17 @@ struct HistoryMessageMarkupButton { }; +[[nodiscard]] bool operator==( + const HistoryMessageMarkupButton &a, + const HistoryMessageMarkupButton &b); + +[[nodiscard]] std::optional ParseInlineButton( + const MTPInlineButtonType &type, + const QString &text, + HistoryMessageMarkupButton::Visual visual); +[[nodiscard]] HistoryMessageMarkupButton::Visual ParseRichButtonVisual( + const tl::conditional &style); + struct HistoryMessageMarkupData { HistoryMessageMarkupData() = default; explicit HistoryMessageMarkupData(const MTPReplyMarkup *data); diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index ff35ffdab2..a3c8ca4a02 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -2029,6 +2029,10 @@ void Element::validateText() { runtime->handlerMediaActivation = {}; runtime->handlerPlaceholderId = {}; runtime->handlerPlaceholderPoint = QPoint(); + runtime->handlerButtonRow = {}; + runtime->handlerButtonRowHandler = nullptr; + runtime->pressedButtonRow = {}; + runtime->pressedButtonRowHandler = nullptr; invalidateTextSizeCache(); }; const auto item = data(); diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index 6ed99cc3aa..430eec56ee 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -2026,7 +2026,14 @@ QString ListWidget::tooltipText() const { return forwarded->text.toString(); } } else if (const auto link = ClickHandler::getActive()) { - return link->tooltip(); + if (const auto text = link->tooltip(); !text.isEmpty()) { + return text; + } + } + if (const auto view = _overElement) { + auto request = StateRequest(); + request.flags |= Ui::Text::StateRequest::Flag::LookupCustomTooltip; + return view->textState(_overState.point, request).customTooltipText; } return QString(); } @@ -4620,7 +4627,8 @@ void ListWidget::mouseActionUpdate() { } if (dragState.link || dragState.cursor == CursorState::Date - || dragState.cursor == CursorState::Forwarded) { + || dragState.cursor == CursorState::Forwarded + || dragState.customTooltip) { Ui::Tooltip::Show(1000, this); } diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index ad08d2d78b..b2116f496c 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -3344,7 +3344,9 @@ void Message::clickHandlerPressedChanged( } else if (const auto rich = richpage() ; rich && ((handler == rich->handler) - || (handler == rich->handlerHorizontalScrollPressed))) { + || (handler == rich->handlerHorizontalScrollPressed) + || (handler == rich->handlerButtonRowHandler) + || (handler == rich->pressedButtonRowHandler))) { if (pressed) { if ((handler == rich->handler) && rich->handlerHorizontalScrollHit @@ -3369,6 +3371,21 @@ void Message::clickHandlerPressedChanged( rich->article.stopPlaceholderRipple(rich->handlerPlaceholderId); } } + if (pressed) { + if ((handler == rich->handlerButtonRowHandler) + && (rich->handlerButtonRow.index >= 0)) { + rich->pressedButtonRow = rich->handlerButtonRow; + rich->pressedButtonRowHandler = handler; + rich->article.addButtonRowRipple( + rich->pressedButtonRow.id, + rich->pressedButtonRow.index, + rich->pressedButtonRow.localPoint); + } + } else if (handler == rich->pressedButtonRowHandler) { + rich->article.stopButtonRowRipple(rich->pressedButtonRow.id); + rich->pressedButtonRow = {}; + rich->pressedButtonRowHandler = nullptr; + } } else if (_reactions) { _reactions->clickHandlerPressedChanged( handler, @@ -4511,6 +4528,10 @@ bool Message::getStateText( rich->handlerHorizontalScrollHit = std::nullopt; rich->handlerHorizontalScrollPoint = {}; }; + const auto clearButtonRowHandler = [&] { + rich->handlerButtonRow = {}; + rich->handlerButtonRowHandler = nullptr; + }; const auto horizontalScrollHit = rich->article.horizontalScrollHit(local); *outResult = TextState(item); outResult->horizontalScroll = horizontalScrollHit.scrollable; @@ -4523,6 +4544,7 @@ bool Message::getStateText( rich->handlerMediaActivation = {}; rich->handlerPlaceholderId = {}; rich->handlerPlaceholderPoint = {}; + clearButtonRowHandler(); if (!rich->handlerHorizontalScrollHit || !rich->handler) { rich->handler = std::make_shared( [](ClickContext) { @@ -4536,6 +4558,7 @@ bool Message::getStateText( if (!hit.valid()) { rich->handlerCodeHeaderSegmentIndex = -1; clearHorizontalScrollHandler(); + clearButtonRowHandler(); return horizontalScrollHit.scrollable; } const auto offset = rich->article.selectionOffsetFromHit( @@ -4554,6 +4577,7 @@ bool Message::getStateText( rich->handlerMediaActivation = {}; rich->handlerPlaceholderId = {}; rich->handlerPlaceholderPoint = {}; + clearButtonRowHandler(); if (!reuse) { const auto text = rich->article.textForContext(hit); rich->handlerCodeHeaderSegmentIndex = hit.segmentIndex; @@ -4573,6 +4597,7 @@ bool Message::getStateText( rich->handlerMediaActivation = {}; rich->handlerPlaceholderId = {}; rich->handlerPlaceholderPoint = {}; + clearButtonRowHandler(); outResult->link = hit.state.link; } else if (hit.preparedLink || hit.mediaActivation.kind != MediaActivationKind::None) { @@ -4588,6 +4613,7 @@ bool Message::getStateText( clearHorizontalScrollHandler(); rich->handlerPlaceholderId = hit.mediaActivation.placeholderId; rich->handlerPlaceholderPoint = hit.placeholderLocalPoint; + clearButtonRowHandler(); if (!reuse) { rich->handlerPreparedLink = prepared; rich->handlerMediaActivation = activation; @@ -4613,6 +4639,19 @@ bool Message::getStateText( } else { rich->handlerCodeHeaderSegmentIndex = -1; clearHorizontalScrollHandler(); + if (hit.buttonRow.index >= 0) { + rich->handlerButtonRow = hit.buttonRow; + rich->handlerButtonRowHandler = hit.state.link; + } else { + clearButtonRowHandler(); + } + if (!hit.customTooltip.isEmpty()) { + outResult->customTooltip = true; + using Flag = Ui::Text::StateRequest::Flag; + if (request.flags & Flag::LookupCustomTooltip) { + outResult->customTooltipText = hit.customTooltip; + } + } outResult->link = hit.state.link; } outResult->cursor = (!outResult->link && hit.direct) diff --git a/Telegram/SourceFiles/history/view/history_view_message.h b/Telegram/SourceFiles/history/view/history_view_message.h index ab298687c3..559309b8a1 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.h +++ b/Telegram/SourceFiles/history/view/history_view_message.h @@ -112,6 +112,10 @@ struct HistoryMessageRichPage mutable Iv::Markdown::MediaActivation handlerMediaActivation; mutable Iv::Markdown::PreparedPlaceholderBlockId handlerPlaceholderId; mutable QPoint handlerPlaceholderPoint; + mutable Iv::Markdown::MarkdownArticleButtonRowHit handlerButtonRow; + mutable ClickHandlerPtr handlerButtonRowHandler; + mutable Iv::Markdown::MarkdownArticleButtonRowHit pressedButtonRow; + mutable ClickHandlerPtr pressedButtonRowHandler; }; enum class BadgeRole : uchar { diff --git a/Telegram/SourceFiles/iv/editor/iv_editor_state.cpp b/Telegram/SourceFiles/iv/editor/iv_editor_state.cpp index 3172a8d7d9..b28340211a 100644 --- a/Telegram/SourceFiles/iv/editor/iv_editor_state.cpp +++ b/Telegram/SourceFiles/iv/editor/iv_editor_state.cpp @@ -1464,6 +1464,7 @@ void MergeRichTextAnchors(RichText *target, RichText source) { case BlockKind::Unsupported: case BlockKind::Thinking: case BlockKind::AuthorDate: + case BlockKind::ButtonRow: case BlockKind::Embed: case BlockKind::EmbedPost: case BlockKind::Channel: diff --git a/Telegram/SourceFiles/iv/iv.style b/Telegram/SourceFiles/iv/iv.style index 960f04f6bc..996feb2c32 100644 --- a/Telegram/SourceFiles/iv/iv.style +++ b/Telegram/SourceFiles/iv/iv.style @@ -338,6 +338,7 @@ MarkdownBlockSkips { relatedArticle: pixels; embedPost: pixels; placeholder: pixels; + buttonRow: pixels; } MarkdownList { indent: pixels; @@ -439,6 +440,28 @@ MarkdownPlaceholder { labelFg: color; labelFgActive: color; } +MarkdownButtonRow { + height: pixels; + spacing: pixels; + padding: pixels; + labelMinPadding: pixels; + iconExtra: pixels; + iconPosition: point; + labelStyle: TextStyle; + defaultBg: color; + defaultBgOpacity: double; + defaultRipple: color; + defaultRippleOpacity: double; + defaultFg: color; + primaryBg: color; + primaryRipple: color; + successFg: color; + dangerFg: color; + tintBgOpacity: double; + tintRippleOpacity: double; + disabledOpacity: double; + disabledPrimaryOpacity: double; +} MarkdownPhoto { padding: margins; captionSkip: pixels; @@ -584,6 +607,7 @@ Markdown { relatedArticle: MarkdownRelatedArticle; groupedMedia: MarkdownGroupedMedia; failure: MarkdownFailure; + buttonRow: MarkdownButtonRow; } defaultMarkdownTextPalette: TextPalette(defaultTextPalette) { @@ -623,6 +647,7 @@ defaultMarkdownBlockSkips: MarkdownBlockSkips { relatedArticle: 0px; embedPost: 12px; placeholder: 12px; + buttonRow: 12px; } defaultMarkdownList: MarkdownList { indent: 28px; @@ -885,6 +910,34 @@ defaultMarkdownFailure: MarkdownFailure { width: 360px; skip: 12px; } +markdownButtonRowTintBgOpacity: 0.12; +markdownButtonRowTintRippleOpacity: 0.22; +defaultMarkdownButtonRowLabelStyle: TextStyle(defaultMarkdownBodyStyle) { + font: font(13px semibold); + lineHeight: 0px; +} +defaultMarkdownButtonRow: MarkdownButtonRow { + height: 26px; + spacing: 6px; + padding: 17px; + labelMinPadding: 4px; + iconExtra: 10px; + iconPosition: point(6px, 5px); + labelStyle: defaultMarkdownButtonRowLabelStyle; + defaultBg: windowBgOver; + defaultBgOpacity: 1.0; + defaultRipple: windowBgRipple; + defaultRippleOpacity: 1.0; + defaultFg: windowBoldFg; + primaryBg: activeButtonBg; + primaryRipple: activeButtonBgRipple; + successFg: historyPeer2NameFg; + dangerFg: historyPeer1NameFg; + tintBgOpacity: markdownButtonRowTintBgOpacity; + tintRippleOpacity: markdownButtonRowTintRippleOpacity; + disabledOpacity: 0.33; + disabledPrimaryOpacity: 0.6; +} markdownFootnoteBox: Box(defaultBox) { buttonPadding: margins(0px, 0px, 0px, 0px); buttonHeight: 0px; @@ -980,6 +1033,7 @@ defaultMarkdown: Markdown { relatedArticle: defaultMarkdownRelatedArticle; groupedMedia: defaultMarkdownGroupedMedia; failure: defaultMarkdownFailure; + buttonRow: defaultMarkdownButtonRow; } messageMarkdownBodyStyle: TextStyle(messageTextStyle) { font: font(13px); @@ -1052,6 +1106,7 @@ messageMarkdownBlockSkips: MarkdownBlockSkips { relatedArticle: 0px; embedPost: 8px; placeholder: 8px; + buttonRow: 8px; } messageMarkdownQuoteBgOpacity: 0.12; messageMarkdownDetailsIcon: icon{{ "history_down_arrow", msgInDateFg }}; @@ -1301,6 +1356,16 @@ messageMarkdownOut: Markdown(messageMarkdown) { failure: MarkdownFailure(messageMarkdownFailure) { label: messageMarkdownFailureLabelOut; } + buttonRow: MarkdownButtonRow(defaultMarkdownButtonRow) { + defaultBg: msgOutReplyBarColor; + defaultBgOpacity: markdownButtonRowTintBgOpacity; + defaultRipple: msgOutReplyBarColor; + defaultRippleOpacity: markdownButtonRowTintRippleOpacity; + defaultFg: msgOutReplyBarColor; + primaryBg: msgOutReplyBarColor; + primaryRipple: msgFileOutBg; + successFg: msgOutReplyBarColor; + } } messageMarkdownSelected: Markdown(messageMarkdown) { textPalette: TextPalette(inTextPaletteSelected) { @@ -1362,6 +1427,11 @@ messageMarkdownSelected: Markdown(messageMarkdown) { failure: MarkdownFailure(messageMarkdownFailure) { label: messageMarkdownFailureLabelSelected; } + buttonRow: MarkdownButtonRow(defaultMarkdownButtonRow) { + defaultFg: historyTextInFgSelected; + successFg: historyPeer2NameFgSelected; + dangerFg: historyPeer1NameFgSelected; + } } messageMarkdownOutSelected: Markdown(messageMarkdown) { textPalette: TextPalette(outTextPaletteSelected) { @@ -1423,6 +1493,17 @@ messageMarkdownOutSelected: Markdown(messageMarkdown) { failure: MarkdownFailure(messageMarkdownFailure) { label: messageMarkdownFailureLabelOutSelected; } + buttonRow: MarkdownButtonRow(defaultMarkdownButtonRow) { + defaultBg: msgOutReplyBarSelColor; + defaultBgOpacity: markdownButtonRowTintBgOpacity; + defaultRipple: msgOutReplyBarSelColor; + defaultRippleOpacity: markdownButtonRowTintRippleOpacity; + defaultFg: msgOutReplyBarSelColor; + primaryBg: msgOutReplyBarSelColor; + primaryRipple: msgFileOutBgSelected; + successFg: msgOutReplyBarSelColor; + dangerFg: historyPeer1NameFgSelected; + } } aiComposeCardMarkdown: Markdown(messageMarkdown) { textPadding: margins(12px, 8px, 12px, 8px); diff --git a/Telegram/SourceFiles/iv/iv_rich_message_serializer.cpp b/Telegram/SourceFiles/iv/iv_rich_message_serializer.cpp index 5e292ac790..c6c735cfdc 100644 --- a/Telegram/SourceFiles/iv/iv_rich_message_serializer.cpp +++ b/Telegram/SourceFiles/iv/iv_rich_message_serializer.cpp @@ -926,6 +926,7 @@ void TrimEmptyParagraphEdges(std::vector *blocks) { return RichTextHasVisibleText(block.text) || block.date != 0; case BlockKind::Divider: case BlockKind::Anchor: + case BlockKind::ButtonRow: case BlockKind::Unsupported: case BlockKind::List: case BlockKind::Embed: @@ -1648,6 +1649,7 @@ void TrimEmptyParagraphEdges(std::vector *blocks) { } case BlockKind::Unsupported: case BlockKind::AuthorDate: + case BlockKind::ButtonRow: case BlockKind::Embed: case BlockKind::EmbedPost: case BlockKind::Channel: diff --git a/Telegram/SourceFiles/iv/iv_rich_page.cpp b/Telegram/SourceFiles/iv/iv_rich_page.cpp index 8733bc51d9..283e2e6d6f 100644 --- a/Telegram/SourceFiles/iv/iv_rich_page.cpp +++ b/Telegram/SourceFiles/iv/iv_rich_page.cpp @@ -11,7 +11,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/flat_map.h" #include "base/qthelp_url.h" #include "base/unixtime.h" -#include "base/variant.h" #include "data/data_document.h" #include "data/data_peer.h" #include "data/data_photo.h" @@ -39,6 +38,7 @@ namespace { using Block = RichPage::Block; using BlockKind = RichPage::BlockKind; +using ButtonAlignment = RichPage::ButtonAlignment; using GroupedMediaIntent = RichPage::GroupedMediaIntent; using ListItem = RichPage::ListItem; using ListKind = RichPage::ListKind; @@ -189,6 +189,7 @@ enum class OrderedMarkerType { const auto PhotoLargeLevels = u"ydxcwmbsa"_q; constexpr auto kDefaultMapWidth = 400; constexpr auto kDefaultMapHeight = 200; +constexpr auto kMaxButtonRowButtons = 8; [[nodiscard]] int NonZeroMapWidth(int width) { return (width > 0) ? width : kDefaultMapWidth; @@ -240,6 +241,7 @@ struct ParseContext { }; base::flat_map documentInfos; bool dropRichTextClickHandlers = false; + bool keepRichTextFormattedDates = false; bool displayTextDiff = false; }; @@ -258,6 +260,7 @@ using TableOccupancyGrid = std::vector; enum class RichTextParseMode { Normal, DropClickHandlers, + DropClickHandlersKeepDates, }; void AccumulateTextLength( @@ -474,6 +477,9 @@ void AccumulateBlockMetrics( AccumulateTextLength(metrics, block.text); AccumulateTextLength(metrics, block.caption); AccumulateTextLength(metrics, block.formula); + for (const auto &button : block.buttons) { + AccumulateTextLength(metrics, button.text); + } if (IsMediaKind(block.kind)) { ++metrics->mediaCount; } @@ -1010,7 +1016,8 @@ void RememberWebPageMedia( if (!AppendRichText(data.vtext(), result, context, anchorId, anchorIds)) { return false; } - if (context->dropRichTextClickHandlers) { + if (context->dropRichTextClickHandlers + && !context->keepRichTextFormattedDates) { return true; } auto flags = FormattedDateFlags(); @@ -1118,8 +1125,11 @@ void RememberWebPageMedia( auto anchorId = QString(); auto anchorIds = std::vector(); const auto wasDropClickHandlers = context->dropRichTextClickHandlers; + const auto wasKeepFormattedDates = context->keepRichTextFormattedDates; context->dropRichTextClickHandlers - = (mode == RichTextParseMode::DropClickHandlers); + = (mode != RichTextParseMode::Normal); + context->keepRichTextFormattedDates + = (mode == RichTextParseMode::DropClickHandlersKeepDates); const auto parsed = AppendRichText( text, &result, @@ -1127,6 +1137,7 @@ void RememberWebPageMedia( &anchorId, &anchorIds); context->dropRichTextClickHandlers = wasDropClickHandlers; + context->keepRichTextFormattedDates = wasKeepFormattedDates; (void)parsed; result.anchorId = std::move(anchorId); result.anchorIds = std::move(anchorIds); @@ -1621,9 +1632,40 @@ void AppendBlock( parsed.caption = ParseCaption(data.vcaption(), context); AdoptAnchor(&parsed.anchorId, &parsed.caption); result->push_back(std::move(parsed)); - }, [&](const MTPDpageBlockButtonRow &) { - AssertIsDebug(); - result->push_back(MakeBlock(BlockKind::Unsupported)); + }, [&](const MTPDpageBlockButtonRow &data) { + auto parsed = MakeBlock(BlockKind::ButtonRow); + parsed.buttonAlignment = data.is_align_left() + ? ButtonAlignment::Left + : data.is_align_center() + ? ButtonAlignment::Center + : data.is_align_right() + ? ButtonAlignment::Right + : ButtonAlignment::Stretch; + const auto &list = data.vbuttons().v; + const auto count = std::min(int(list.size()), kMaxButtonRowButtons); + parsed.buttons.reserve(count); + for (auto i = 0; i != count; ++i) { + const auto &fields = list[i].data(); + auto button = ParseInlineButton( + fields.vtype(), + QString(), + ParseRichButtonVisual(fields.vstyle())); + if (!button) { + continue; + } + auto text = ParseRichText( + fields.vtext(), + context, + RichTextParseMode::DropClickHandlersKeepDates); + button->text = text.text.text; + parsed.buttons.push_back({ + .text = std::move(text), + .button = std::move(*button), + }); + } + if (!parsed.buttons.empty()) { + result->push_back(std::move(parsed)); + } }, [&](const MTPDpageBlockDocument &data) { result->push_back(MakeDocumentBlock( BlockKind::File, @@ -1645,61 +1687,11 @@ void AppendBlocks( } } -void ExpandInlineTextObjects(TextWithEntities *text, bool withIcons) { - auto &entities = text->entities; - for (auto i = entities.begin(); i != entities.end();) { - if (i->type() != EntityType::CustomEmoji) { - ++i; - continue; - } - const auto object = Markdown::ParseInlineTextObjectEntity( - i->data()); - if (!object) { - ++i; - continue; - } - const auto replacement = v::match(object->data, []( - const Markdown::InlineTextObjectFormulaData &data) { - return data.trimmedTex; - }, [](const Markdown::InlineTextObjectIvImageData &data) { - return data.replacementText; - }); - const auto offset = i->offset(); - const auto length = i->length(); - const auto delta = int(replacement.size()) - length; - text->text.replace(offset, length, replacement); - for (auto &entity : entities) { - if (&entity == &*i) { - continue; - } else if (entity.offset() > offset) { - entity.shiftRight(delta); - } else if (entity.offset() + entity.length() > offset) { - entity.shrinkFromRight(-delta); - } - } - const auto formula = (object->kind - == Markdown::InlineTextObjectKind::Formula); - if (withIcons && formula && !replacement.isEmpty()) { - const auto icon = Ui::Text::IconEmoji( - &st::ivSummaryMathIcon, - replacement); - *i = EntityInText( - EntityType::CustomEmoji, - offset, - int(replacement.size()), - icon.entities.front().data()); - ++i; - } else { - i = entities.erase(i); - } - } -} - void AppendSummaryLine( TextWithEntities *result, TextWithEntities &&line, bool withIcons) { - ExpandInlineTextObjects(&line, withIcons); + Markdown::ExpandInlineTextObjects(&line, withIcons); TextUtilities::Trim(line); if (line.empty()) { return; @@ -2040,6 +2032,11 @@ void AppendSummaryBlock( AppendSummaryLine(result, std::move(line), withIcons); return; } + case BlockKind::ButtonRow: + for (const auto &button : block.buttons) { + AppendSummaryLine(result, button.text, withIcons); + } + return; case BlockKind::List: { auto ordered = OrderedListSequenceStart(block); const auto step = block.orderedList.reversed ? -1 : 1; @@ -2274,6 +2271,7 @@ std::shared_ptr ParsePage( case BlockKind::File: case BlockKind::GroupedMedia: case BlockKind::Map: + case BlockKind::ButtonRow: return false; default: break; @@ -2542,7 +2540,7 @@ TextWithEntities FlattenRichPageToSimpleText(const RichPage &page) { // Code blocks are allowed at the top level as a Pre entity, but // their content is sent as plain text without any inline entities. auto inner = block.text.text; - ExpandInlineTextObjects(&inner, false); + Markdown::ExpandInlineTextObjects(&inner, false); inner.entities.clear(); AppendSimpleBlock( &result, diff --git a/Telegram/SourceFiles/iv/iv_rich_page.h b/Telegram/SourceFiles/iv/iv_rich_page.h index a9d04dd833..1fabe024c4 100644 --- a/Telegram/SourceFiles/iv/iv_rich_page.h +++ b/Telegram/SourceFiles/iv/iv_rich_page.h @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "base/basic_types.h" +#include "history/history_item_reply_markup.h" #include "ui/text/text_entity.h" #include @@ -48,6 +49,7 @@ struct RichPage { Code, Divider, Anchor, + ButtonRow, List, Quote, Photo, @@ -180,6 +182,23 @@ struct RichPage { const RelatedArticle &, const RelatedArticle &) = default; }; + enum class ButtonAlignment : uchar { + Stretch, + Left, + Center, + Right, + }; + struct Button { + RichText text; + HistoryMessageMarkupButton button = HistoryMessageMarkupButton( + HistoryMessageMarkupButton::Type::Disabled, + QString(), + {}); + + friend inline bool operator==( + const Button &, + const Button &) = default; + }; struct Block { BlockKind kind = BlockKind::Unsupported; QString anchorId; @@ -217,6 +236,7 @@ struct RichPage { ListKind listKind = ListKind::Bullet; OrderedListData orderedList; GroupedMediaIntent mediaIntent = GroupedMediaIntent::Collage; + ButtonAlignment buttonAlignment = ButtonAlignment::Stretch; PhotoData *photo = nullptr; DocumentData *document = nullptr; PeerData *peer = nullptr; @@ -228,6 +248,7 @@ struct RichPage { std::vector mediaItems; std::vector tableRows; std::vector relatedArticles; + std::vector