From 8b8b3ebc09b3e66bb68f735ac3e1c2a7c2f925c4 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 2 Jun 2026 17:23:09 +0400 Subject: [PATCH] Support server-provided limits for rich messages. --- Telegram/Resources/langs/lang.strings | 7 + Telegram/SourceFiles/apiwrap.cpp | 11 +- Telegram/SourceFiles/boxes/send_files_box.cpp | 20 +- Telegram/SourceFiles/boxes/share_box.cpp | 19 +- Telegram/SourceFiles/config.h | 2 - .../SourceFiles/data/data_premium_limits.cpp | 14 + .../SourceFiles/data/data_premium_limits.h | 3 + Telegram/SourceFiles/data/data_session.cpp | 6 +- .../history/history_item_helpers.cpp | 11 +- .../SourceFiles/history/history_widget.cpp | 36 +- .../history_view_compose_controls.cpp | 24 +- .../controls/history_view_draft_options.cpp | 29 +- .../controls/history_view_forward_panel.cpp | 56 +++- .../controls/history_view_forward_panel.h | 12 + .../view/history_view_chat_section.cpp | 9 +- .../view/history_view_scheduled_section.cpp | 9 +- .../SourceFiles/iv/editor/iv_editor_box.cpp | 7 +- .../SourceFiles/iv/editor/iv_editor_box.h | 5 + .../iv/editor/iv_editor_session.cpp | 162 ++++++++- .../SourceFiles/iv/editor/iv_editor_state.cpp | 309 ++++++++++++++++-- .../SourceFiles/iv/editor/iv_editor_state.h | 44 ++- .../iv/editor/iv_editor_widget.cpp | 219 +++++++++---- .../SourceFiles/iv/editor/iv_editor_widget.h | 11 +- .../iv/iv_rich_message_serializer.cpp | 25 +- Telegram/SourceFiles/iv/iv_rich_page.cpp | 172 +++++++++- Telegram/SourceFiles/iv/iv_rich_page.h | 21 ++ .../business/settings_shortcut_messages.cpp | 11 +- .../SourceFiles/support/support_helper.cpp | 2 +- .../ui/controls/compose_ai_button_factory.cpp | 12 +- .../ui/controls/compose_ai_button_factory.h | 2 +- .../window/notifications_manager_default.cpp | 4 +- .../SourceFiles/window/window_peer_menu.cpp | 21 +- 32 files changed, 1081 insertions(+), 214 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index b8c177ba24..28e4b30e2a 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -7185,6 +7185,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_article_insert_details" = "Details"; "lng_article_insert_table" = "Table"; "lng_article_insert_map" = "Map"; +"lng_article_limit_length" = "The article is too long."; +"lng_article_limit_depth" = "The article is nested too deeply."; +"lng_article_limit_blocks" = "The article has too many blocks."; +"lng_article_limit_columns" = "The article table has too many columns."; +"lng_article_limit_media" = "The article has too many media files."; +"lng_article_premium_required" = "Subscribe to {link} to be able to send rich articles."; +"lng_article_premium_required_link" = "Telegram Premium"; "lng_polls_menu_item" = "Poll"; "lng_polls_create" = "Create poll"; "lng_polls_create_title" = "New poll"; diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 177472d978..528577e93b 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -50,6 +50,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_forum_topic.h" #include "data/data_forum.h" #include "data/data_message_reaction_id.h" +#include "data/data_premium_limits.h" #include "data/data_saved_messages.h" #include "data/data_saved_music.h" #include "data/data_saved_sublist.h" @@ -73,6 +74,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" #include "history/history_item_components.h" #include "history/history_item_helpers.h" +#include "history/view/controls/history_view_forward_panel.h" #include "main/main_session.h" #include "main/main_session_settings.h" #include "main/main_account.h" @@ -3543,6 +3545,10 @@ void ApiWrap::forwardMessages( } return; } + draft.options = HistoryView::Controls::NormalizeForwardOptions( + _session, + draft.items, + draft.options); struct SharedCallback { int requestsLeft = 0; @@ -4232,10 +4238,13 @@ void ApiWrap::sendMessage( HistoryItem *lastMessage = nullptr; auto &histories = history->owner().histories(); + const auto messageLengthLimit = Data::PremiumLimits( + &history->session() + ).messageLengthCurrent(); const auto exactWebPage = !message.webPage.url.isEmpty(); auto isFirst = true; - while (TextUtilities::CutPart(sending, left, MaxMessageSize) + while (TextUtilities::CutPart(sending, left, messageLengthLimit) || (isFirst && exactWebPage)) { TextUtilities::Trim(left); const auto isLast = left.empty(); diff --git a/Telegram/SourceFiles/boxes/send_files_box.cpp b/Telegram/SourceFiles/boxes/send_files_box.cpp index b40b9fde92..d142df7b89 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.cpp +++ b/Telegram/SourceFiles/boxes/send_files_box.cpp @@ -189,6 +189,18 @@ void EditFileCaptionBox( TextWithTags currentCaption, Fn apply) { box->setTitle(tr::lng_context_upload_edit_caption()); + const auto window = Core::App().findWindow(box); + const auto controller = window ? window->sessionController() : nullptr; + const auto maxCaptionLength = [&] { + if (captionToPeer) { + return Data::PremiumLimits( + &captionToPeer->session()).captionLengthCurrent(); + } else if (controller) { + return Data::PremiumLimits( + &controller->session()).captionLengthCurrent(); + } + return kMaxMessageLength; + }(); const auto wrap = box->addRow( object_ptr(box), st::boxRowPadding); @@ -197,11 +209,10 @@ void EditFileCaptionBox( st.files.caption, Ui::InputField::Mode::MultiLine, tr::lng_photo_caption()); - field->setMaxLength(kMaxMessageLength); + field->setMaxLength(maxCaptionLength); field->setSubmitSettings(Core::App().settings().sendSubmitWay()); Ui::ResizeFitChild(wrap, field); - if (const auto window = Core::App().findWindow(box)) { - const auto controller = window->sessionController(); + if (window) { const auto allow = [=](not_null emoji) { return captionToPeer && Data::AllowEmojiWithoutPremium(captionToPeer, emoji); @@ -1889,7 +1900,8 @@ void SendFilesBox::setupCaption() { } _caption->setSubmitSettings( Core::App().settings().sendSubmitWay()); - _caption->setMaxLength(kMaxMessageLength); + _caption->setMaxLength( + Data::PremiumLimits(&_show->session()).captionLengthCurrent()); _caption->heightChanges( ) | rpl::on_next([=] { diff --git a/Telegram/SourceFiles/boxes/share_box.cpp b/Telegram/SourceFiles/boxes/share_box.cpp index 8304f99ff6..8b10c34c0d 100644 --- a/Telegram/SourceFiles/boxes/share_box.cpp +++ b/Telegram/SourceFiles/boxes/share_box.cpp @@ -31,6 +31,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" #include "history/history_item.h" #include "history/history_item_helpers.h" +#include "history/view/controls/history_view_forward_panel.h" #include "history/view/history_view_element.h" #include "history/view/history_view_context_menu.h" // CopyPostLink. #include "settings/sections/settings_premium.h" @@ -746,8 +747,10 @@ void ShareBox::submit(Api::SendOptions options) { return true; }; if (const auto onstack = _descriptor.submitCallback) { - const auto forwardOptions = (_forwardOptions.captionsCount - && _forwardOptions.dropCaptions) + const auto forwardOptions = !_descriptor.forwardOptions.show + ? Data::ForwardOptions::PreserveInfo + : (_forwardOptions.captionsCount + && _forwardOptions.dropCaptions) ? Data::ForwardOptions::NoNamesAndCaptions : _forwardOptions.dropNames ? Data::ForwardOptions::NoSenderNames @@ -1705,6 +1708,12 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( if (existingIds.empty() || result.empty()) { return; } + if (HistoryView::Controls::HasRichPage(items)) { + forwardOptions = HistoryView::Controls::NormalizeForwardOptions( + &history->session(), + items, + forwardOptions); + } const auto error = GetErrorForSending( result, @@ -1975,6 +1984,9 @@ void FastShareMessage( : ranges::all_of(items, [](auto item) { return item->media() && item->media()->forceForwardedInfo(); }); + const auto canShowRichForwardOptions + = !HistoryView::Controls::HasRichPage(items) + || HistoryView::Controls::CanHideForwardAuthor(session, items); auto copyCallback = [=] { const auto item = owner->message(msgIds[0]); @@ -2030,7 +2042,8 @@ void FastShareMessage( .forwardOptions = { .sendersCount = ItemsForwardSendersCount(items), .captionsCount = ItemsForwardCaptionsCount(items), - .show = !hasOnlyForcedForwardedInfo, + .show = !hasOnlyForcedForwardedInfo + && canShowRichForwardOptions, }, .moneyRestrictionError = ShareMessageMoneyRestrictionError(), }), Ui::LayerOption::CloseOther); diff --git a/Telegram/SourceFiles/config.h b/Telegram/SourceFiles/config.h index 5d861a7412..94dc62afce 100644 --- a/Telegram/SourceFiles/config.h +++ b/Telegram/SourceFiles/config.h @@ -23,8 +23,6 @@ enum { SearchPeopleLimit = 5, - MaxMessageSize = 4096, - WebPageUserId = 701000, UpdateDelayConstPart = 8 * 3600, // 8 hour min time between update check requests diff --git a/Telegram/SourceFiles/data/data_premium_limits.cpp b/Telegram/SourceFiles/data/data_premium_limits.cpp index 5915c3da3f..cb1fe7eada 100644 --- a/Telegram/SourceFiles/data/data_premium_limits.cpp +++ b/Telegram/SourceFiles/data/data_premium_limits.cpp @@ -176,6 +176,20 @@ int PremiumLimits::captionLengthCurrent() const { : captionLengthDefault(); } +int PremiumLimits::messageLengthDefault() const { + return appConfigLimit("message_length_limit_default", 4096); +} + +int PremiumLimits::messageLengthPremium() const { + return appConfigLimit("message_length_limit_premium", 8192); +} + +int PremiumLimits::messageLengthCurrent() const { + return isPremium() + ? messageLengthPremium() + : messageLengthDefault(); +} + int PremiumLimits::uploadMaxDefault() const { return appConfigLimit("upload_max_fileparts_default", 4000); } diff --git a/Telegram/SourceFiles/data/data_premium_limits.h b/Telegram/SourceFiles/data/data_premium_limits.h index dd93b5fa25..bd499c990f 100644 --- a/Telegram/SourceFiles/data/data_premium_limits.h +++ b/Telegram/SourceFiles/data/data_premium_limits.h @@ -70,6 +70,9 @@ public: [[nodiscard]] int captionLengthDefault() const; [[nodiscard]] int captionLengthPremium() const; [[nodiscard]] int captionLengthCurrent() const; + [[nodiscard]] int messageLengthDefault() const; + [[nodiscard]] int messageLengthPremium() const; + [[nodiscard]] int messageLengthCurrent() const; [[nodiscard]] int uploadMaxDefault() const; [[nodiscard]] int uploadMaxPremium() const; diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 6585ae023e..f62f8dbecc 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -92,6 +92,7 @@ namespace Data { namespace { constexpr auto kNextForUpgradeGiftTimeout = 5 * crl::time(1000); +constexpr auto kMaxServiceNotificationMessageSize = 4096; using ViewElement = HistoryView::Element; @@ -5400,7 +5401,10 @@ void Session::insertCheckedServiceNotification( const auto localFlags = MessageFlag::ClientSideUnread | MessageFlag::Local; auto sending = TextWithEntities(), left = message; - while (TextUtilities::CutPart(sending, left, MaxMessageSize)) { + while (TextUtilities::CutPart( + sending, + left, + kMaxServiceNotificationMessageSize)) { const auto id = nextLocalMessageId(); addNewMessage( id, diff --git a/Telegram/SourceFiles/history/history_item_helpers.cpp b/Telegram/SourceFiles/history/history_item_helpers.cpp index 11dd7b40dd..1af7166acc 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.cpp +++ b/Telegram/SourceFiles/history/history_item_helpers.cpp @@ -23,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_forum_topic.h" #include "data/data_message_reactions.h" #include "data/data_poll.h" +#include "data/data_premium_limits.h" #include "data/data_session.h" #include "data/data_stories.h" #include "data/data_user.h" @@ -80,9 +81,12 @@ int ComputeSendingMessagesCount( auto prepareFlags = Ui::ItemTextOptions( history, history->session().user()).flags; + const auto messageLengthLimit = Data::PremiumLimits( + &history->session() + ).messageLengthCurrent(); TextUtilities::PrepareForSending(left, prepareFlags); - while (TextUtilities::CutPart(sending, left, MaxMessageSize)) { + while (TextUtilities::CutPart(sending, left, messageLengthLimit)) { ++result; } if (!result) { @@ -104,6 +108,9 @@ Data::SendError GetErrorForSending( const auto thread = topic ? not_null(topic) : peer->owner().history(peer); + const auto messageLengthLimit = Data::PremiumLimits( + &thread->owningHistory()->session() + ).messageLengthCurrent(); if (request.story) { if (const auto error = request.story->errorTextForForward(thread)) { return error; @@ -138,7 +145,7 @@ Data::SendError GetErrorForSending( return tr::lng_slowmode_no_many(tr::now); } } - if (request.text && request.text->text.size() > MaxMessageSize) { + if (request.text && request.text->text.size() > messageLengthLimit) { return tr::lng_slowmode_too_long(tr::now); } else if ((hasText || request.story) && count > 1) { return tr::lng_slowmode_no_many(tr::now); diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index cd28f110ba..1782d367ea 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -435,6 +435,11 @@ HistoryWidget::HistoryWidget( ) | rpl::on_next([=] { fieldChanged(); }, _field->lifetime()); + Data::AmPremiumValue(&session()) | rpl::on_next([=] { + checkCharsLimitation(); + updateAiButtonVisibility(); + updateSendAsFileVisibility(); + }, lifetime()); #ifdef Q_OS_MAC // Removed an ability to insert text from the menu bar // when the field is hidden. @@ -541,7 +546,10 @@ HistoryWidget::HistoryWidget( _field->setMimeDataHook(WrappedMessageFieldMimeHook([=]( not_null data, Ui::InputField::MimeAction action) { - const auto pasteResult = Ui::CheckLargeTextPaste(_field, data); + const auto pasteResult = Ui::CheckLargeTextPaste( + &session(), + _field, + data); if (pasteResult.exceeds) { if (action == Ui::InputField::MimeAction::Check) { return true; @@ -4831,10 +4839,11 @@ void HistoryWidget::saveEditMessage(Api::SendOptions options) { } return; } else { - const auto maxCaptionSize = !hasMediaWithCaption - ? MaxMessageSize - : Data::PremiumLimits(&session()).captionLengthCurrent(); - const auto remove = _fieldCharsCountManager.count() - maxCaptionSize; + const auto limits = Data::PremiumLimits(&session()); + const auto maxTextSize = hasMediaWithCaption + ? limits.captionLengthCurrent() + : limits.messageLengthCurrent(); + const auto remove = _fieldCharsCountManager.count() - maxTextSize; if (remove > 0) { controller()->showToast( tr::lng_edit_limit_reached(tr::now, lt_count, remove)); @@ -6407,7 +6416,8 @@ bool HistoryWidget::hasEnoughLinesForAi() const { bool HistoryWidget::textExceedsMaxSize() const { return _history && !_voiceRecordBar->isActive() - && _field->getLastText().size() > MaxMessageSize; + && (_field->getLastText().size() + > Data::PremiumLimits(&session()).messageLengthCurrent()); } void HistoryWidget::updateAiButtonVisibility() { @@ -9056,10 +9066,11 @@ void HistoryWidget::checkCharsLimitation() { } const auto hasMediaWithCaption = item->media() && item->media()->allowsEditCaption(); - const auto maxCaptionSize = !hasMediaWithCaption - ? MaxMessageSize - : Data::PremiumLimits(&session()).captionLengthCurrent(); - const auto remove = _fieldCharsCountManager.count() - maxCaptionSize; + const auto limits = Data::PremiumLimits(&session()); + const auto maxTextSize = hasMediaWithCaption + ? limits.captionLengthCurrent() + : limits.messageLengthCurrent(); + const auto remove = _fieldCharsCountManager.count() - maxTextSize; if (remove > 0) { if (!_charsLimitation) { _charsLimitation = base::make_unique_q( @@ -9067,11 +9078,6 @@ void HistoryWidget::checkCharsLimitation() { _send.get(), style::al_bottom); _charsLimitation->show(); - Data::AmPremiumValue( - &session() - ) | rpl::on_next([=] { - checkCharsLimitation(); - }, _charsLimitation->lifetime()); } _charsLimitation->setLeft(remove); } else { diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index cf3e0aa4fa..441beae8db 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -2399,6 +2399,11 @@ void ComposeControls::initField() { updateAiButtonVisibility(); updateSendAsFileVisibility(); }, _field->lifetime()); + Data::AmPremiumValue(&session()) | rpl::on_next([=] { + checkCharsLimitation(); + updateAiButtonVisibility(); + updateSendAsFileVisibility(); + }, _wrap->lifetime()); #ifdef Q_OS_MAC // Removed an ability to insert text from the menu bar // when the field is hidden. @@ -3436,7 +3441,7 @@ void ComposeControls::fireSendTextAsFile( bool ComposeControls::checkLargeTextPaste( not_null data, Ui::InputField::MimeAction action) { - const auto result = Ui::CheckLargeTextPaste(_field, data); + const auto result = Ui::CheckLargeTextPaste(&session(), _field, data); if (!result.exceeds) { return false; } @@ -3840,7 +3845,8 @@ bool ComposeControls::hasEnoughLinesForAi() const { bool ComposeControls::textExceedsMaxSize() const { return _history && !_recording.current() - && _field->getLastText().size() > MaxMessageSize; + && (_field->getLastText().size() + > Data::PremiumLimits(&session()).messageLengthCurrent()); } bool ComposeControls::updateBotCommandShown() { @@ -4648,11 +4654,12 @@ void ComposeControls::checkCharsLimitation() { } const auto hasMediaWithCaption = item->media() && item->media()->allowsEditCaption(); - const auto maxCaptionSize = !hasMediaWithCaption - ? MaxMessageSize - : Data::PremiumLimits(&session()).captionLengthCurrent(); + const auto limits = Data::PremiumLimits(&session()); + const auto maxTextSize = hasMediaWithCaption + ? limits.captionLengthCurrent() + : limits.messageLengthCurrent(); const auto remove = Ui::ComputeFieldCharacterCount(_field) - - maxCaptionSize; + - maxTextSize; if (remove > 0) { if (!_charsLimitation) { using namespace Controls; @@ -4661,11 +4668,6 @@ void ComposeControls::checkCharsLimitation() { _send.get(), style::al_bottom); _charsLimitation->show(); - Data::AmPremiumValue( - &session() - ) | rpl::on_next([=] { - checkCharsLimitation(); - }, _charsLimitation->lifetime()); } _charsLimitation->setLeft(remove); } else { 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 6a614db8f2..5bd0b2f04d 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp @@ -276,6 +276,10 @@ void PreviewWrap::showForwardSelector(Data::ResolvedForwardDraft draft) { }; const auto wasViews = base::take(_views); using Options = Data::ForwardOptions; + draft.options = NormalizeForwardOptions( + &_history->session(), + draft.items, + draft.options); const auto dropNames = (draft.options != Options::PreserveInfo); const auto dropCaptions = (draft.options == Options::NoNamesAndCaptions); for (const auto &source : draft.items) { @@ -834,7 +838,10 @@ void DraftOptionsBox( const auto weak = base::make_weak(box); auto forward = Data::ForwardDraft(); if (options) { - forward.options = *options; + forward.options = NormalizeForwardOptions( + &show->session(), + state->forward.items, + *options); for (const auto &item : state->forward.items) { forward.ids.push_back(item->fullId()); } @@ -954,15 +961,20 @@ void DraftOptionsBox( const auto setupForwardActions = [=] { using Options = Data::ForwardOptions; - const auto now = state->forward.options; const auto &items = state->forward.items; + state->forward.options = NormalizeForwardOptions( + &show->session(), + items, + state->forward.options); + const auto now = state->forward.options; const auto count = items.size(); const auto dropNames = (now != Options::PreserveInfo); const auto sendersCount = ItemsForwardSendersCount(items); const auto captionsCount = ItemsForwardCaptionsCount(items); - const auto hasOnlyForcedForwardedInfo = !captionsCount - && HasOnlyForcedForwardedInfo(items); - const auto canDropNames = !hasOnlyForcedForwardedInfo + const auto canHideAuthor = CanHideForwardAuthor( + &show->session(), + items); + const auto canDropNames = canHideAuthor && HasDropForwardedInfoSetting(items); const auto dropCaptions = (now == Options::NoNamesAndCaptions); @@ -989,7 +1001,7 @@ void DraftOptionsBox( state->shown.force_assign(Section::Forward); }); } - if (captionsCount) { + if (captionsCount && canHideAuthor) { Settings::AddButtonWithIcon( bottom, (dropCaptions @@ -1147,7 +1159,10 @@ void DraftOptionsBox( .text = { tr::lng_reply_quote_long_text(tr::now) }, }); } else { - const auto options = state->forward.options; + const auto options = NormalizeForwardOptions( + &show->session(), + state->forward.items, + state->forward.options); finish(resolveReply(), state->webpage, options); } }; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp index d17cb3e5a3..ea7c14b71f 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp @@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "data/data_media_types.h" #include "data/data_forum_topic.h" +#include "data/data_user.h" #include "main/main_session.h" #include "ui/chat/forward_options_box.h" #include "ui/effects/spoiler_mess.h" @@ -236,7 +237,9 @@ bool ForwardPanel::empty() const { void ForwardPanel::applyOptions(Data::ForwardOptions options) { if (_data.items.empty()) { return; - } else if (_data.options != options) { + } + options = NormalizeForwardOptions(&_to->session(), _data.items, options); + if (_data.options != options) { const auto topicRootId = _to->topicRootId(); const auto monoforumPeerId = _to->monoforumPeerId(); _data.options = options; @@ -250,27 +253,20 @@ void ForwardPanel::applyOptions(Data::ForwardOptions options) { void ForwardPanel::editToNextOption() { using Options = Data::ForwardOptions; - const auto captionsCount = ItemsForwardCaptionsCount(_data.items); - const auto hasOnlyForcedForwardedInfo = !captionsCount - && HasOnlyForcedForwardedInfo(_data.items); - if (hasOnlyForcedForwardedInfo) { + if (_data.items.empty()) { return; } - - const auto now = _data.options; + const auto captionsCount = ItemsForwardCaptionsCount(_data.items); + const auto now = NormalizeForwardOptions( + &_to->session(), + _data.items, + _data.options); const auto next = (now == Options::PreserveInfo) ? Options::NoSenderNames : ((now == Options::NoSenderNames) && captionsCount) ? Options::NoNamesAndCaptions : Options::PreserveInfo; - - const auto topicRootId = _to->topicRootId(); - const auto monoforumPeerId = _to->monoforumPeerId(); - _to->owningHistory()->setForwardDraft(topicRootId, monoforumPeerId, { - .ids = _to->owner().itemsToIds(_data.items), - .options = next, - }); - _repaint(); + applyOptions(next); } void ForwardPanel::paint( @@ -479,4 +475,34 @@ bool HasDropForwardedInfoSetting(const HistoryItemsList &list) { return false; } +bool HasRichPage(const HistoryItemsList &list) { + for (const auto &item : list) { + if (item->richPage()) { + return true; + } + } + return false; +} + +bool CanHideForwardAuthor( + not_null session, + const HistoryItemsList &list) { + if (list.empty()) { + return true; + } + if (HasOnlyForcedForwardedInfo(list)) { + return false; + } + return session->premium() || !HasRichPage(list); +} + +Data::ForwardOptions NormalizeForwardOptions( + not_null session, + const HistoryItemsList &list, + Data::ForwardOptions options) { + return CanHideForwardAuthor(session, list) + ? options + : Data::ForwardOptions::PreserveInfo; +} + } // namespace HistoryView::Controls diff --git a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h index b29204047d..e12c13dbbc 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h @@ -23,6 +23,10 @@ class Thread; struct WebPageDraft; } // namespace Data +namespace Main { +class Session; +} // namespace Main + namespace Window { class SessionController; } // namespace Window @@ -88,5 +92,13 @@ void EditWebPageOptions( [[nodiscard]] bool HasOnlyForcedForwardedInfo(const HistoryItemsList &list); [[nodiscard]] bool HasOnlyDroppedForwardedInfo(const HistoryItemsList &list); [[nodiscard]] bool HasDropForwardedInfoSetting(const HistoryItemsList &list); +[[nodiscard]] bool HasRichPage(const HistoryItemsList &list); +[[nodiscard]] bool CanHideForwardAuthor( + not_null session, + const HistoryItemsList &list); +[[nodiscard]] Data::ForwardOptions NormalizeForwardOptions( + not_null session, + const HistoryItemsList &list, + Data::ForwardOptions options); } // namespace HistoryView::Controls diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp index 63380c5cef..0fdb143fdb 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp @@ -1541,11 +1541,12 @@ void ChatWidget::edit( } return; } else { - const auto maxCaptionSize = !hasMediaWithCaption - ? MaxMessageSize - : Data::PremiumLimits(&session()).captionLengthCurrent(); + const auto limits = Data::PremiumLimits(&session()); + const auto maxTextSize = hasMediaWithCaption + ? limits.captionLengthCurrent() + : limits.messageLengthCurrent(); const auto remove = _composeControls->fieldCharacterCount() - - maxCaptionSize; + - maxTextSize; if (remove > 0) { controller()->showToast( tr::lng_edit_limit_reached(tr::now, lt_count, remove)); diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp index d39e4be9b5..cc6c7c952d 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp @@ -779,11 +779,12 @@ void ScheduledWidget::edit( } return; } else { - const auto maxCaptionSize = !hasMediaWithCaption - ? MaxMessageSize - : Data::PremiumLimits(&session()).captionLengthCurrent(); + const auto limits = Data::PremiumLimits(&session()); + const auto maxTextSize = hasMediaWithCaption + ? limits.captionLengthCurrent() + : limits.messageLengthCurrent(); const auto remove = _composeControls->fieldCharacterCount() - - maxCaptionSize; + - maxTextSize; if (remove > 0) { controller()->showToast( tr::lng_edit_limit_reached(tr::now, lt_count, remove)); diff --git a/Telegram/SourceFiles/iv/editor/iv_editor_box.cpp b/Telegram/SourceFiles/iv/editor/iv_editor_box.cpp index ab7bfa8126..fda64b6890 100644 --- a/Telegram/SourceFiles/iv/editor/iv_editor_box.cpp +++ b/Telegram/SourceFiles/iv/editor/iv_editor_box.cpp @@ -354,7 +354,8 @@ void SetupBox( box, descriptor.controller, descriptor.peer, - descriptor.state), + descriptor.state, + std::move(descriptor.showLimitToast)), style::margins()); const auto tooltipParent = box->getDelegate()->outerContainer(); box->setPinnedToTopContent(object_ptr( @@ -368,7 +369,9 @@ void SetupBox( const auto submit = box->addButton( rpl::single(SubmitText(descriptor)), [=, confirmed = std::move(descriptor.confirmed)] { - editor->commitInlineField(); + if (!editor->commitInlineField()) { + return; + } if ((!confirmed || confirmed()) && weak) { weak->closeBox(); } diff --git a/Telegram/SourceFiles/iv/editor/iv_editor_box.h b/Telegram/SourceFiles/iv/editor/iv_editor_box.h index fc7406944d..873880caea 100644 --- a/Telegram/SourceFiles/iv/editor/iv_editor_box.h +++ b/Telegram/SourceFiles/iv/editor/iv_editor_box.h @@ -23,6 +23,10 @@ namespace Ui { class RpWidget; } // namespace Ui +namespace Iv { +enum class RichMessageLimitError : unsigned char; +} // namespace Iv + namespace Iv::Editor { class State; @@ -44,6 +48,7 @@ struct ShowBoxDescriptor { Fn)> setupSubmitButton; Fn)> requestMedia; Fn)> requestMap; + Fn showLimitToast; }; void ShowBox(ShowBoxDescriptor descriptor); diff --git a/Telegram/SourceFiles/iv/editor/iv_editor_session.cpp b/Telegram/SourceFiles/iv/editor/iv_editor_session.cpp index 9db1922dab..ca06d2baa6 100644 --- a/Telegram/SourceFiles/iv/editor/iv_editor_session.cpp +++ b/Telegram/SourceFiles/iv/editor/iv_editor_session.cpp @@ -23,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_location.h" #include "data/data_photo.h" #include "data/data_session.h" +#include "data/data_user.h" #include "history/history.h" #include "history/history_item.h" #include "history/history_item_helpers.h" @@ -36,6 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "mainwidget.h" #include "menu/menu_send.h" +#include "settings/sections/settings_premium.h" #include "storage/file_upload.h" #include "storage/localimageloader.h" #include "storage/storage_account.h" @@ -140,6 +142,75 @@ private: || (type == PreparedFileType::Music); } +[[nodiscard]] bool CanUseRichMessages(not_null session) { + return session->premium(); +} + +void ShowRichMessagesPremiumToast( + not_null controller) { + Settings::ShowPremiumPromoToast( + controller->uiShow(), + tr::lng_article_premium_required( + tr::now, + lt_link, + tr::link(tr::bold( + tr::lng_article_premium_required_link(tr::now))), + tr::marked), + u"rich_message"_q); +} + +[[nodiscard]] bool IsRichMessageMediaKind(RichPage::BlockKind kind) { + switch (kind) { + case RichPage::BlockKind::Photo: + case RichPage::BlockKind::Video: + case RichPage::BlockKind::Audio: + return true; + default: + return false; + } +} + +void CountRichPageMedia( + const std::vector &blocks, + int *result) { + for (const auto &block : blocks) { + if (IsRichMessageMediaKind(block.kind)) { + ++(*result); + } + CountRichPageMedia(block.blocks, result); + for (const auto &item : block.listItems) { + CountRichPageMedia(item.blocks, result); + } + for (const auto &item : block.mediaItems) { + if (IsRichMessageMediaKind(item.kind)) { + ++(*result); + } + } + } +} + +[[nodiscard]] int CountRichPageMedia(const RichPage &page) { + auto result = 0; + CountRichPageMedia(page.blocks, &result); + return result; +} + +template +[[nodiscard]] int CountAcceptedPreparedFiles(const Container &files) { + auto result = 0; + for (const auto &file : files) { + if (AcceptedPreparedFileType(file.type)) { + ++result; + } + } + return result; +} + +[[nodiscard]] int CountAcceptedPreparedFiles(const PreparedList &list) { + return CountAcceptedPreparedFiles(list.files) + + CountAcceptedPreparedFiles(list.filesToProcess); +} + [[nodiscard]] RichPage::RichText ToRichText(QString text) { auto result = RichPage::RichText(); result.text.text = std::move(text); @@ -457,7 +528,28 @@ private: }, [](QString) { })) - , _state(std::make_shared(_page, _runtime)) + , _showLimitToast([controller](RichMessageLimitError error) { + switch (error) { + case RichMessageLimitError::Length: + controller->showToast(tr::lng_article_limit_length(tr::now)); + return; + case RichMessageLimitError::Blocks: + controller->showToast(tr::lng_article_limit_blocks(tr::now)); + return; + case RichMessageLimitError::Depth: + controller->showToast(tr::lng_article_limit_depth(tr::now)); + return; + case RichMessageLimitError::Media: + controller->showToast(tr::lng_article_limit_media(tr::now)); + return; + case RichMessageLimitError::TableColumns: + controller->showToast(tr::lng_article_limit_columns(tr::now)); + return; + } + controller->showToast(tr::lng_edit_error(tr::now)); + }) + , _limits(ResolveRichMessageLimits(_session)) + , _state(std::make_shared(_page, _runtime, _limits)) , _submitOptions(_composeAction ? _composeAction->options : Api::SendOptions()) { subscribeToUploader(); } @@ -466,6 +558,10 @@ private: if (_submittedPage || _submitApiRequested) { return false; } + if (!CanUseRichMessages(_session)) { + ShowRichMessagesPremiumToast(_controller); + return false; + } if (hasPendingPreparation()) { _submitDeferred = true; return false; @@ -476,6 +572,10 @@ private: } auto page = std::shared_ptr( std::make_shared(_state->richPage())); + if (const auto error = ValidateRichMessage(*page, _limits)) { + showRichMessageLimitToast(*error); + return false; + } if (!applySubmittedLocalState(page)) { _controller->showToast(tr::lng_edit_error(tr::now)); return false; @@ -833,6 +933,7 @@ private: not_null editor) { session->requestMap(editor); }, + .showLimitToast = _showLimitToast, }; ShowBox(std::move(descriptor)); } @@ -861,6 +962,11 @@ private: QPointer editor, PreparedList list, uint64 batchId) { + if (const auto accepted = CountAcceptedPreparedFiles(list); + accepted && exceedsMediaLimitWith(accepted)) { + showRichMessageLimitToast(RichMessageLimitError::Media); + return; + } for (auto &file : list.files) { applyPreparedFile(editor, std::move(file), batchId); } @@ -895,6 +1001,7 @@ private: _prepareQueue.pop_front(); const auto weak = base::make_weak(this); _preparing = true; + _preparingFileType = queued.file.type; const auto sideLimit = PhotoSideLimit(); crl::async([weak, queued = std::move(queued), sideLimit]() mutable { Storage::PrepareDetails( @@ -911,6 +1018,7 @@ private: void preparedAsyncFile(QueuedPrepare queued) { _preparing = false; + _preparingFileType = PreparedFileType::None; applyPreparedFile( queued.editor, std::move(queued.file), @@ -926,6 +1034,10 @@ private: showRejectedToast(batchId); return; } + if (exceedsMediaLimitWith(1)) { + showRichMessageLimitToast(RichMessageLimitError::Media); + return; + } prepareAttachment(editor, std::move(file)); } @@ -981,6 +1093,10 @@ private: if (!editor || !prepared) { return; } + if (exceedsMediaLimitWith(1)) { + showRichMessageLimitToast(RichMessageLimitError::Media); + return; + } _editor = editor; const auto uploadId = FullMsgId( _peer->id, @@ -1038,12 +1154,16 @@ private: } } - _session->uploader().upload(uploadId, prepared); _attachments.push_back(std::move(record)); auto &stored = _attachments.back(); - updateAttachmentProgress(stored); editor->insertPreparedBlock(makeAttachmentBlock(stored)); refreshAttachmentLocators(stored); + if (stored.blockLocators.empty()) { + _attachments.pop_back(); + return; + } + _session->uploader().upload(uploadId, prepared); + updateAttachmentProgress(stored); requestEditorUpdate(); } @@ -1406,13 +1526,32 @@ private: void refreshAttachmentLocators(AttachmentRecord &attachment) { auto locators = std::vector(); collectBlockLocators( - _page->blocks, + _state->richPage().blocks, State::BlockContainerPath(), attachment, locators); attachment.blockLocators = std::move(locators); } + [[nodiscard]] int pendingAttachmentPlaceholders() const { + auto result = _pendingAttachmentPrepareCount; + if (AcceptedPreparedFileType(_preparingFileType)) { + ++result; + } + for (const auto &queued : _prepareQueue) { + if (AcceptedPreparedFileType(queued.file.type)) { + ++result; + } + } + return result; + } + + [[nodiscard]] bool exceedsMediaLimitWith(int additionalMedia) const { + return (CountRichPageMedia(_state->richPage()) + + pendingAttachmentPlaceholders() + + additionalMedia) > _limits.maxMedia; + } + [[nodiscard]] bool hasVisibleAttachmentBlock(AttachmentRecord &attachment) { refreshAttachmentLocators(attachment); return !attachment.blockLocators.empty(); @@ -1432,6 +1571,10 @@ private: _controller->showToast(tr::lng_attach_failed(tr::now)); } + void showRichMessageLimitToast(RichMessageLimitError error) const { + _showLimitToast(error); + } + void showRejectedToast(uint64 batchId) { if (_rejectedToastBatchId == batchId) { return; @@ -1485,6 +1628,8 @@ private: const std::optional _edited; const std::shared_ptr _page; const std::shared_ptr _runtime; + const Fn _showLimitToast; + const RichMessageLimits _limits; const std::shared_ptr _state; Api::SendOptions _submitOptions; QPointer _submitButton; @@ -1499,6 +1644,7 @@ private: uint64 _rejectedToastBatchId = 0; int _pendingAttachmentPrepareCount = 0; bool _preparing = false; + PreparedFileType _preparingFileType = PreparedFileType::None; bool _submitDeferred = false; bool _submitApiRequested = false; @@ -1511,6 +1657,10 @@ void ShowComposeBox( not_null peer, Api::SendAction action, Fn sendMenuDetails) { + if (!CanUseRichMessages(&controller->session())) { + ShowRichMessagesPremiumToast(controller); + return; + } ArticleSession::ShowCompose( controller, peer, @@ -1521,6 +1671,10 @@ void ShowComposeBox( void ShowEditBox( not_null controller, not_null item) { + if (!CanUseRichMessages(&controller->session())) { + ShowRichMessagesPremiumToast(controller); + return; + } ArticleSession::ShowEdit(controller, item); } diff --git a/Telegram/SourceFiles/iv/editor/iv_editor_state.cpp b/Telegram/SourceFiles/iv/editor/iv_editor_state.cpp index 517dc199d6..874a6a71f4 100644 --- a/Telegram/SourceFiles/iv/editor/iv_editor_state.cpp +++ b/Telegram/SourceFiles/iv/editor/iv_editor_state.cpp @@ -40,6 +40,35 @@ using TableCell = RichPage::TableCell; using TaskState = RichPage::TaskState; using TextNodeDescriptor = State::TextNodeDescriptor; +constexpr auto kMaxRichTextNodeLength = 16000; +constexpr auto kMaxCommittedFieldLength = 256 * 1024; + +[[nodiscard]] TextWithEntities MakeText(QString text) { + auto result = TextWithEntities(); + result.text = std::move(text); + return result; +} + +[[nodiscard]] std::vector SplitFieldText( + TextWithEntities text) { + auto result = std::vector(); + auto left = std::move(text); + auto consumed = 0; + while (!left.text.isEmpty() && consumed < kMaxCommittedFieldLength) { + auto part = TextWithEntities(); + const auto limit = std::min( + kMaxRichTextNodeLength, + kMaxCommittedFieldLength - consumed); + if (!TextUtilities::CutPart(part, left, limit) + || part.text.isEmpty()) { + break; + } + consumed += part.text.size(); + result.push_back(std::move(part)); + } + return result; +} + [[nodiscard]] BlockContainerPath BlockChildrenContainer(BlockPath path) { auto result = std::move(path.container); result.steps.push_back({ @@ -240,14 +269,16 @@ using TextNodeDescriptor = State::TextNodeDescriptor; } // namespace State::State() -: State(std::make_shared(), nullptr) { +: State(std::make_shared(), nullptr, RichMessageLimits()) { } State::State( std::shared_ptr richPage, - std::shared_ptr mediaRuntime) + std::shared_ptr mediaRuntime, + RichMessageLimits limits) : _richPage(richPage ? std::move(richPage) : std::make_shared()) -, _mediaRuntime(std::move(mediaRuntime)) { +, _mediaRuntime(std::move(mediaRuntime)) +, _limits(std::move(limits)) { if (_richPage->blocks.empty()) { _richPage->blocks.push_back(MakeParagraphBlock()); } @@ -262,6 +293,35 @@ const Markdown::MarkdownArticleContent &State::prepared() const { return _prepared; } +template +Result State::applyCheckedMutation(Result failure, Callback &&callback) { + _lastLimitError = std::nullopt; + auto candidate = State( + std::make_shared(*_richPage), + _mediaRuntime, + _limits); + candidate._activeTextOrdinal = _activeTextOrdinal; + candidate._lastLimitError = std::nullopt; + const auto outcome = callback(candidate); + if (!outcome.apply) { + return outcome.result; + } + if (const auto error = ValidateRichMessage(*candidate._richPage, _limits)) { + _lastLimitError = error; + return failure; + } + commitCheckedMutation(std::move(candidate)); + return outcome.result; +} + +void State::commitCheckedMutation(State state) { + _richPage = std::move(state._richPage); + _prepared = std::move(state._prepared); + _textNodes = std::move(state._textNodes); + _activeTextOrdinal = state._activeTextOrdinal; + _lastLimitError = std::nullopt; +} + const std::vector &State::textNodes() const { return _textNodes; } @@ -302,20 +362,45 @@ TextWithEntities State::activeText() const { return TextWithEntities(); } -void State::applyActiveText(TextWithEntities text) { +bool State::applyActiveText(TextWithEntities text) { + _lastLimitError = std::nullopt; + return applyActiveTextWithLocalLimit(std::move(text)); +} + +bool State::applyActiveTextUnchecked(TextWithEntities text) { const auto descriptor = textNode(_activeTextOrdinal); if (!descriptor) { - return; + return false; } if (auto current = richText(descriptor->leaf)) { current->text = std::move(text); rebuild(); - return; + return true; } if (auto current = rawText(descriptor->leaf)) { *current = std::move(text.text); rebuild(); + return true; } + return false; +} + +bool State::applyActiveTextWithLocalLimit(TextWithEntities text) { + const auto descriptor = textNode(_activeTextOrdinal); + if (!descriptor) { + return false; + } + auto chunks = SplitFieldText(std::move(text)); + if (chunks.size() <= 1) { + return applyActiveTextUnchecked(chunks.empty() + ? TextWithEntities() + : std::move(chunks.front())); + } + auto first = chunks.front(); + if (applySplitParagraphText(*descriptor, std::move(chunks))) { + return true; + } + return applyActiveTextUnchecked(std::move(first)); } FieldMode State::activeFieldMode() const { @@ -381,20 +466,138 @@ QString State::activePlaceholderText() const { return QString(); } -void State::applyActiveRawText(QString text) { +bool State::applyActiveRawText(QString text) { + _lastLimitError = std::nullopt; + return applyActiveRawTextWithLocalLimit(std::move(text)); +} + +bool State::applyActiveRawTextUnchecked(QString text) { const auto descriptor = textNode(_activeTextOrdinal); if (!descriptor) { - return; + return false; } if (auto current = rawText(descriptor->leaf)) { *current = std::move(text); rebuild(); - return; + return true; } if (auto current = richText(descriptor->leaf)) { current->text = MakeText(std::move(text)); rebuild(); + return true; } + return false; +} + +bool State::applyActiveRawTextWithLocalLimit(QString text) { + auto chunks = SplitFieldText(MakeText(std::move(text))); + return applyActiveRawTextUnchecked(chunks.empty() + ? QString() + : std::move(chunks.front().text)); +} + +bool State::applySplitParagraphText( + const TextNodeDescriptor &descriptor, + std::vector chunks) { + if (chunks.empty()) { + return applyActiveTextUnchecked(TextWithEntities()); + } + const auto makeParagraph = [&](TextWithEntities text) { + auto paragraph = MakeParagraphBlock(); + paragraph.text.text = std::move(text); + return paragraph; + }; + const auto focus = [&](LeafPath leaf) { + rebuild(); + if (!activateRebuiltLeaf(leaf)) { + ensureActiveTextOrdinal(); + } + }; + if (descriptor.leaf.kind == LeafKind::BlockText) { + const auto path = descriptor.leaf.block; + if (auto owner = block(path)) { + if (owner->kind == BlockKind::Paragraph) { + auto container = blockContainer(path.container); + if (!container + || path.index < 0 + || path.index >= int(container->size())) { + return false; + } + owner->text.text = std::move(chunks.front()); + auto blocks = std::vector(); + blocks.reserve(chunks.size() - 1); + for (auto i = 1; i != int(chunks.size()); ++i) { + blocks.push_back(makeParagraph(std::move(chunks[i]))); + } + container->insert( + container->begin() + path.index + 1, + std::make_move_iterator(blocks.begin()), + std::make_move_iterator(blocks.end())); + focus(descriptor.leaf); + return true; + } else if (owner->kind == BlockKind::Quote && !owner->pullquote) { + auto firstText = std::move(owner->text); + firstText.text = std::move(chunks.front()); + auto blocks = std::vector(); + blocks.reserve(chunks.size()); + auto first = MakeParagraphBlock(); + first.text = std::move(firstText); + blocks.push_back(std::move(first)); + for (auto i = 1; i != int(chunks.size()); ++i) { + blocks.push_back(makeParagraph(std::move(chunks[i]))); + } + owner->text = RichText(); + owner->blocks.insert( + owner->blocks.begin(), + std::make_move_iterator(blocks.begin()), + std::make_move_iterator(blocks.end())); + focus({ + .kind = LeafKind::BlockText, + .block = { + .container = BlockChildrenContainer(path), + .index = 0, + }, + }); + return true; + } + } + } else if (descriptor.leaf.kind == LeafKind::ListItemText) { + const auto path = descriptor.leaf.block; + if (auto item = listItem(path, descriptor.leaf.listItemIndex)) { + auto firstText = std::move(item->text); + firstText.text = std::move(chunks.front()); + auto blocks = std::vector(); + blocks.reserve(chunks.size()); + auto first = MakeParagraphBlock(); + first.anchorId = std::move(item->anchorId); + first.text = std::move(firstText); + blocks.push_back(std::move(first)); + for (auto i = 1; i != int(chunks.size()); ++i) { + blocks.push_back(makeParagraph(std::move(chunks[i]))); + } + item->anchorId.clear(); + item->text = RichText(); + item->blocks.insert( + item->blocks.begin(), + std::make_move_iterator(blocks.begin()), + std::make_move_iterator(blocks.end())); + focus({ + .kind = LeafKind::BlockText, + .block = { + .container = ListItemChildrenContainer( + path, + descriptor.leaf.listItemIndex), + .index = 0, + }, + }); + return true; + } + } + return false; +} + +std::optional State::lastLimitError() const { + return _lastLimitError; } std::optional State::codeBlockLanguage(int ordinal) const { @@ -1710,7 +1913,17 @@ auto State::normalizeActiveListItemSurface() return surface; } -int State::ensureTrailingParagraphActive() { +std::optional State::ensureTrailingParagraphActive() { + return applyCheckedMutation(std::optional(), [](State &candidate) { + const auto result = candidate.ensureTrailingParagraphActiveUnchecked(); + return CheckedMutationResult>{ + .apply = result.has_value(), + .result = result, + }; + }); +} + +std::optional State::ensureTrailingParagraphActiveUnchecked() { if (_richPage->blocks.empty() || _richPage->blocks.back().kind != BlockKind::Paragraph) { _richPage->blocks.push_back(MakeParagraphBlock()); @@ -1727,10 +1940,22 @@ int State::ensureTrailingParagraphActive() { if (!setActiveTextByOrdinal(ordinal)) { ensureActiveTextOrdinal(); } - return _activeTextOrdinal; + return (_activeTextOrdinal >= 0) + ? std::make_optional(_activeTextOrdinal) + : std::nullopt; } std::optional State::moveActiveQuoteDown() { + return applyCheckedMutation(std::optional(), [](State &candidate) { + const auto result = candidate.moveActiveQuoteDownUnchecked(); + return CheckedMutationResult>{ + .apply = result.has_value(), + .result = result, + }; + }); +} + +std::optional State::moveActiveQuoteDownUnchecked() { const auto descriptor = textNode(_activeTextOrdinal); if (!descriptor) { return std::nullopt; @@ -1770,6 +1995,16 @@ std::optional State::moveActiveQuoteDown() { } std::optional State::handleActiveHeadingEnter() { + return applyCheckedMutation(std::optional(), [](State &candidate) { + const auto result = candidate.handleActiveHeadingEnterUnchecked(); + return CheckedMutationResult>{ + .apply = result.has_value(), + .result = result, + }; + }); +} + +std::optional State::handleActiveHeadingEnterUnchecked() { const auto descriptor = textNode(_activeTextOrdinal); if (!descriptor || descriptor->leaf.kind != LeafKind::BlockText) { return std::nullopt; @@ -1799,6 +2034,16 @@ std::optional State::handleActiveHeadingEnter() { } std::optional State::handleActiveListEnter() { + return applyCheckedMutation(std::optional(), [](State &candidate) { + const auto result = candidate.handleActiveListEnterUnchecked(); + return CheckedMutationResult>{ + .apply = result.has_value(), + .result = result, + }; + }); +} + +std::optional State::handleActiveListEnterUnchecked() { const auto surface = normalizeActiveListItemSurface(); if (!surface) { return std::nullopt; @@ -1854,37 +2099,52 @@ std::optional State::handleActiveListEnter() { } void State::insertHeading1AfterActive() { - insertBlockAfterActive({ + (void)insertBlockAfterActive({ .type = InsertBlockType::Heading, .headingLevel = 1, }); } void State::insertBlockquoteAfterActive() { - insertBlockAfterActive({ + (void)insertBlockAfterActive({ .type = InsertBlockType::Blockquote, }); } -void State::insertBlockAfterActive(InsertAction action) { - auto blocks = std::vector(); - blocks.push_back(makeBlock(action)); - insertBlocksAfterActive(std::move(blocks)); +bool State::insertBlockAfterActive(InsertAction action) { + return applyCheckedMutation(false, [action](State &candidate) { + auto blocks = std::vector(); + blocks.push_back(candidate.makeBlock(action)); + const auto applied = candidate.insertBlocksAfterActiveUnchecked( + std::move(blocks)); + return CheckedMutationResult{ + .apply = applied, + .result = applied, + }; + }); } -void State::insertPreparedBlockAfterActive(Block block) { +bool State::insertPreparedBlockAfterActive(Block block) { auto blocks = std::vector(); blocks.push_back(std::move(block)); - insertBlocksAfterActive(std::move(blocks)); + return insertPreparedBlocksAfterActive(std::move(blocks)); } -void State::insertPreparedBlocksAfterActive(std::vector blocks) { - insertBlocksAfterActive(std::move(blocks)); +bool State::insertPreparedBlocksAfterActive(std::vector blocks) { + return applyCheckedMutation(false, [blocks = std::move(blocks)]( + State &candidate) mutable { + const auto applied = candidate.insertBlocksAfterActiveUnchecked( + std::move(blocks)); + return CheckedMutationResult{ + .apply = applied, + .result = applied, + }; + }); } -void State::insertBlocksAfterActive(std::vector blocks) { +bool State::insertBlocksAfterActiveUnchecked(std::vector blocks) { if (blocks.empty()) { - return; + return false; } const auto descriptor = textNode(_activeTextOrdinal); if (descriptor && shouldReplaceActiveTextOnlyBlock(*descriptor, blocks)) { @@ -1902,7 +2162,7 @@ void State::insertBlocksAfterActive(std::vector blocks) { std::make_move_iterator(blocks.end())); rebuild(); focusInsertedBlocks(path.container, insertAt, count); - return; + return true; } } auto anchor = resolveActiveInsertionTarget(); @@ -1932,6 +2192,7 @@ void State::insertBlocksAfterActive(std::vector blocks) { std::make_move_iterator(blocks.end())); rebuild(); focusInsertedBlocks(anchor.container, insertAt, count); + return true; } std::vector *State::blockContainer(const BlockContainerPath &path) { diff --git a/Telegram/SourceFiles/iv/editor/iv_editor_state.h b/Telegram/SourceFiles/iv/editor/iv_editor_state.h index 0b989d3cb7..3afba18037 100644 --- a/Telegram/SourceFiles/iv/editor/iv_editor_state.h +++ b/Telegram/SourceFiles/iv/editor/iv_editor_state.h @@ -170,7 +170,8 @@ public: State(); State( std::shared_ptr richPage, - std::shared_ptr mediaRuntime); + std::shared_ptr mediaRuntime, + RichMessageLimits limits = {}); [[nodiscard]] const RichPage &richPage() const; [[nodiscard]] const Markdown::MarkdownArticleContent &prepared() const; @@ -181,11 +182,12 @@ public: [[nodiscard]] int activeTextOrdinal() const; [[nodiscard]] bool setActiveTextByOrdinal(int ordinal); [[nodiscard]] TextWithEntities activeText() const; - void applyActiveText(TextWithEntities text); + [[nodiscard]] bool applyActiveText(TextWithEntities text); [[nodiscard]] FieldMode activeFieldMode() const; [[nodiscard]] QString activeRawText() const; [[nodiscard]] QString activePlaceholderText() const; - void applyActiveRawText(QString text); + [[nodiscard]] bool applyActiveRawText(QString text); + [[nodiscard]] std::optional lastLimitError() const; [[nodiscard]] std::optional codeBlockLanguage(int ordinal) const; [[nodiscard]] bool setCodeBlockLanguage(int ordinal, QString language); [[nodiscard]] int activeTextLength() const; @@ -207,12 +209,13 @@ public: const Markdown::PreparedEditListItemSource &source); [[nodiscard]] bool toggleDetailsOpen( const Markdown::PreparedEditBlockSource &source); - [[nodiscard]] int ensureTrailingParagraphActive(); + [[nodiscard]] std::optional ensureTrailingParagraphActive(); void insertHeading1AfterActive(); void insertBlockquoteAfterActive(); - void insertBlockAfterActive(InsertAction action); - void insertPreparedBlockAfterActive(RichPage::Block block); - void insertPreparedBlocksAfterActive(std::vector blocks); + [[nodiscard]] bool insertBlockAfterActive(InsertAction action); + [[nodiscard]] bool insertPreparedBlockAfterActive(RichPage::Block block); + [[nodiscard]] bool insertPreparedBlocksAfterActive( + std::vector blocks); private: struct StructuralBlockRange { @@ -250,6 +253,12 @@ private: int itemIndex = -1; }; + template + struct CheckedMutationResult { + bool apply = false; + Result result; + }; + [[nodiscard]] std::optional convertBlockContainerPath( const Markdown::PreparedEditBlockContainerPath &path) const; [[nodiscard]] std::optional convertBlockPath( @@ -342,6 +351,11 @@ private: void rebuildTextNodes( const std::vector &blocks, const BlockContainerPath &container); + void commitCheckedMutation(State state); + template + [[nodiscard]] Result applyCheckedMutation( + Result failure, + Callback &&callback); [[nodiscard]] std::optional activateRebuiltLeaf( const LeafPath &path); [[nodiscard]] InsertionAnchor resolveActiveInsertionTarget() const; @@ -361,7 +375,19 @@ private: -> std::optional; [[nodiscard]] auto normalizeActiveListItemSurface() -> std::optional; - void insertBlocksAfterActive(std::vector blocks); + [[nodiscard]] bool applyActiveTextUnchecked(TextWithEntities text); + [[nodiscard]] bool applyActiveRawTextUnchecked(QString text); + [[nodiscard]] bool applyActiveTextWithLocalLimit(TextWithEntities text); + [[nodiscard]] bool applyActiveRawTextWithLocalLimit(QString text); + [[nodiscard]] bool applySplitParagraphText( + const TextNodeDescriptor &descriptor, + std::vector chunks); + [[nodiscard]] std::optional ensureTrailingParagraphActiveUnchecked(); + [[nodiscard]] std::optional moveActiveQuoteDownUnchecked(); + [[nodiscard]] std::optional handleActiveHeadingEnterUnchecked(); + [[nodiscard]] std::optional handleActiveListEnterUnchecked(); + [[nodiscard]] bool insertBlocksAfterActiveUnchecked( + std::vector blocks); void appendBlockTextNode( const BlockPath &path, LeafKind kind, @@ -446,9 +472,11 @@ private: std::shared_ptr _richPage; std::shared_ptr _mediaRuntime; + RichMessageLimits _limits; Markdown::MarkdownArticleContent _prepared; std::vector _textNodes; int _activeTextOrdinal = -1; + std::optional _lastLimitError; }; diff --git a/Telegram/SourceFiles/iv/editor/iv_editor_widget.cpp b/Telegram/SourceFiles/iv/editor/iv_editor_widget.cpp index 46a7b50e69..6171af2674 100644 --- a/Telegram/SourceFiles/iv/editor/iv_editor_widget.cpp +++ b/Telegram/SourceFiles/iv/editor/iv_editor_widget.cpp @@ -670,11 +670,13 @@ Widget::Widget( QWidget *parent, not_null controller, not_null peer, - std::shared_ptr state) + std::shared_ptr state, + Fn showLimitToast) : Ui::RpWidget(parent) , _controller(controller) , _peer(peer) , _state(std::move(state)) +, _showLimitToast(std::move(showLimitToast)) , _articleStyle(std::make_shared( CreateEditorMarkdownStyle())) , _article(std::make_shared(*_articleStyle)) @@ -789,15 +791,20 @@ bool Widget::replayImeIntoField(QInputMethodEvent *e) { return true; } -void Widget::commitInlineField() { - applyFieldTextToState(); +bool Widget::commitInlineField() { + if (applyFieldTextToState()) { + return true; + } + revertInlineFieldToState(); + showLastLimitToast(); + return false; } void Widget::hideInlineFieldAndRefresh() { if (_field->isHidden()) { return; } - commitInlineField(); + (void)commitInlineField(); _pendingOrdinal = -1; _pendingCursorOffset = 0; hideInlineField(); @@ -826,8 +833,13 @@ void Widget::syncInlineFieldGeometry() { } void Widget::insertBlock(State::InsertAction action) { - commitInlineField(); - _state->insertBlockAfterActive(action); + if (!commitInlineField()) { + return; + } + if (!_state->insertBlockAfterActive(action)) { + showLastLimitToast(); + return; + } refreshPreparedContent(); activateTextOrdinal(_state->activeTextOrdinal(), 0); } @@ -842,8 +854,13 @@ void Widget::insertPreparedBlocks(std::vector blocks) { if (blocks.empty()) { return; } - commitInlineField(); - _state->insertPreparedBlocksAfterActive(std::move(blocks)); + if (!commitInlineField()) { + return; + } + if (!_state->insertPreparedBlocksAfterActive(std::move(blocks))) { + showLastLimitToast(); + return; + } refreshPreparedContent(); activateTextOrdinal(_state->activeTextOrdinal(), 0); } @@ -1259,7 +1276,9 @@ void Widget::mouseReleaseEvent(QMouseEvent *e) { const auto controlHit = _article->editControlHitTest(articlePoint); const auto applyControlToggle = [&](auto &&toggle, auto &&afterRefresh) { const auto hadVisibleField = !_field->isHidden(); - commitInlineField(); + if (!commitInlineField()) { + return false; + } _pendingOrdinal = -1; _pendingCursorOffset = 0; hideInlineField(); @@ -1334,7 +1353,9 @@ void Widget::mouseReleaseEvent(QMouseEvent *e) { if (_field->isHidden()) { return false; } - commitInlineField(); + if (!commitInlineField()) { + return false; + } _pendingOrdinal = -1; _pendingCursorOffset = 0; hideInlineField(); @@ -1354,7 +1375,10 @@ void Widget::mouseReleaseEvent(QMouseEvent *e) { return false; } auto languageHit = hit; - if (commitVisibleInlineField()) { + if (!_field->isHidden()) { + if (!commitVisibleInlineField()) { + return true; + } languageHit = _article->hitTest( articlePoint, Ui::Text::StateRequest::Flag::LookupSymbol); @@ -1406,11 +1430,12 @@ void Widget::mouseReleaseEvent(QMouseEvent *e) { const auto selectionFrom = selection.from.offset; const auto selectionTo = selection.to.offset; clearTextSelection(); - commitVisibleInlineField(); - activateTextOrdinal( - selectionOrdinal, - selectionFrom, - selectionTo); + if (_field->isHidden() || commitVisibleInlineField()) { + activateTextOrdinal( + selectionOrdinal, + selectionFrom, + selectionTo); + } e->accept(); return; } else if (fromField) { @@ -1462,8 +1487,9 @@ void Widget::mouseReleaseEvent(QMouseEvent *e) { _field->setTextCursor(cursor); _field->setFocusFast(); } else if (targetOrdinal >= 0) { - commitVisibleInlineField(); - activateTextOrdinal(targetOrdinal, offset); + if (_field->isHidden() || commitVisibleInlineField()) { + activateTextOrdinal(targetOrdinal, offset); + } } } else if (articlePoint.y() >= _articleHeight) { activateTrailingParagraph(); @@ -1685,7 +1711,7 @@ void Widget::setupInlineField() { _field->focusedChanges( ) | rpl::on_next([=](bool focused) { if (!focused && !_settingField && !_trackingPointerPress) { - commitInlineField(); + (void)commitInlineField(); refreshPreparedContent(); } }, _field->lifetime()); @@ -1753,33 +1779,8 @@ void Widget::refreshInlineFieldPlaceholderColor() { _inlineFieldPlaceholderColorOverride->color()); } -void Widget::activateTextOrdinal(int ordinal, int cursorOffset) { - activateTextOrdinal(ordinal, cursorOffset, cursorOffset); -} - -void Widget::activateTextOrdinal( - int ordinal, - int selectionFrom, - int selectionTo) { - if (!_state->setActiveTextByOrdinal(ordinal)) { - return; - } - _boundarySelectionOrigin = std::nullopt; - _activeOrdinal = ordinal; - _pendingOrdinal = -1; - _pendingCursorOffset = 0; - - const auto segmentIndex = segmentIndexForEditableOrdinal(ordinal); - if (segmentIndex < 0) { - _activeSegmentIndex = -1; - _pendingOrdinal = ordinal; - _pendingCursorOffset = selectionTo; - hideInlineField(); - return; - } - - _activeSegmentIndex = segmentIndex; - ensureInlineFieldForSegment(segmentIndex); +void Widget::setInlineFieldFromActiveState(int selectionFrom, int selectionTo) { + ensureInlineFieldForSegment(_activeSegmentIndex); refreshInlineFieldPlaceholder(); _settingField = true; auto cursorSelectionFrom = selectionFrom; @@ -1812,6 +1813,35 @@ void Widget::activateTextOrdinal( } _field->setTextCursor(cursor); _settingField = false; +} + +void Widget::activateTextOrdinal(int ordinal, int cursorOffset) { + activateTextOrdinal(ordinal, cursorOffset, cursorOffset); +} + +void Widget::activateTextOrdinal( + int ordinal, + int selectionFrom, + int selectionTo) { + if (!_state->setActiveTextByOrdinal(ordinal)) { + return; + } + _boundarySelectionOrigin = std::nullopt; + _activeOrdinal = ordinal; + _pendingOrdinal = -1; + _pendingCursorOffset = 0; + + const auto segmentIndex = segmentIndexForEditableOrdinal(ordinal); + if (segmentIndex < 0) { + _activeSegmentIndex = -1; + _pendingOrdinal = ordinal; + _pendingCursorOffset = selectionTo; + hideInlineField(); + return; + } + + _activeSegmentIndex = segmentIndex; + setInlineFieldFromActiveState(selectionFrom, selectionTo); _field->show(); syncInlineFieldGeometry(); updateInlineFieldHeightOverride(); @@ -1855,22 +1885,47 @@ void Widget::revealActiveInlineField() { } void Widget::activateTrailingParagraph() { - commitInlineField(); + if (!commitInlineField()) { + return; + } const auto ordinal = _state->ensureTrailingParagraphActive(); + if (!ordinal) { + showLastLimitToast(); + return; + } refreshPreparedContent(); - activateTextOrdinal(ordinal, _state->activeText().text.size()); + activateTextOrdinal(*ordinal, _state->activeText().text.size()); } -void Widget::applyFieldTextToState() { - if (_settingField || _field->isHidden()) { +void Widget::revertInlineFieldToState() { + if (_field->isHidden() || _activeSegmentIndex < 0) { return; } + const auto cursor = _field->textCursor(); + setInlineFieldFromActiveState(cursor.anchor(), cursor.position()); + syncInlineFieldGeometry(); + updateInlineFieldHeightOverride(); +} + +bool Widget::applyFieldTextToState() { + if (_settingField || _field->isHidden()) { + return true; + } if (_state->activeFieldMode() == State::FieldMode::Raw) { - _state->applyActiveRawText(_field->getLastText()); - return; + return _state->applyActiveRawText(_field->getLastText()); } const auto text = _field->getTextWithAppliedMarkdown(); - _state->applyActiveText(ConvertEditorTagsToRichText(text)); + return _state->applyActiveText(ConvertEditorTagsToRichText(text)); +} + +bool Widget::showLastLimitToast() { + if (_showLimitToast) { + if (const auto error = _state->lastLimitError()) { + _showLimitToast(*error); + return true; + } + } + return false; } void Widget::hideInlineField() { @@ -1920,11 +1975,17 @@ bool Widget::handleFieldKey(QKeyEvent *e) { || key == Qt::Key_PageUp)) { handled = moveBoundary(false, false); } else if (atEnd && key == Qt::Key_Down) { - commitInlineField(); - if (const auto target = _state->moveActiveQuoteDown()) { + if (!commitInlineField()) { + handled = true; + } else if (const auto target = _state->moveActiveQuoteDown()) { refreshPreparedContent(); activateTextOrdinal(*target, 0); handled = true; + } else if (_state->lastLimitError()) { + handled = moveBoundaryAfterCommit(true, false); + if (!handled) { + handled = true; + } } else { handled = moveBoundaryAfterCommit(true, true); } @@ -1933,8 +1994,9 @@ bool Widget::handleFieldKey(QKeyEvent *e) { || key == Qt::Key_PageDown)) { handled = moveBoundary(true, true); } else if (key == Qt::Key_Return || key == Qt::Key_Enter) { - commitInlineField(); - if (const auto target = _state->handleActiveListEnter()) { + if (!commitInlineField()) { + handled = true; + } else if (const auto target = _state->handleActiveListEnter()) { refreshPreparedContent(); activateTextOrdinal(*target, 0); handled = true; @@ -1942,6 +2004,9 @@ bool Widget::handleFieldKey(QKeyEvent *e) { refreshPreparedContent(); activateTextOrdinal(*target, 0); handled = true; + } else if (_state->lastLimitError()) { + showLastLimitToast(); + handled = true; } } else if (atStart && key == Qt::Key_Backspace) { handled = removeBoundaryOwner(false); @@ -1984,7 +2049,9 @@ bool Widget::moveBoundary(bool forward, bool allowTrailing) { if (!target && !addTrailing) { return false; } - commitInlineField(); + if (!commitInlineField()) { + return true; + } if (target) { refreshPreparedContent(); if (forward) { @@ -1995,8 +2062,11 @@ bool Widget::moveBoundary(bool forward, bool allowTrailing) { return true; } const auto ordinal = _state->ensureTrailingParagraphActive(); + if (!ordinal) { + return forward && allowTrailing && _state->lastLimitError().has_value(); + } refreshPreparedContent(); - activateTextOrdinal(ordinal, 0); + activateTextOrdinal(*ordinal, 0); return true; } @@ -2015,8 +2085,11 @@ bool Widget::moveBoundaryAfterCommit(bool forward, bool allowTrailing) { } if (forward && allowTrailing && !_state->isActiveTopLevelParagraph()) { const auto ordinal = _state->ensureTrailingParagraphActive(); + if (!ordinal) { + return _state->lastLimitError().has_value(); + } refreshPreparedContent(); - activateTextOrdinal(ordinal, 0); + activateTextOrdinal(*ordinal, 0); return true; } return false; @@ -2024,7 +2097,9 @@ bool Widget::moveBoundaryAfterCommit(bool forward, bool allowTrailing) { bool Widget::moveTabBoundary(bool forward) { if (!_field->isHidden()) { - commitInlineField(); + if (!commitInlineField()) { + return true; + } } const auto target = forward ? _state->nextEditableOrdinal() @@ -2039,13 +2114,18 @@ bool Widget::moveTabBoundary(bool forward) { } clearSelection(); const auto ordinal = _state->ensureTrailingParagraphActive(); + if (!ordinal) { + return _state->lastLimitError().has_value(); + } refreshPreparedContent(); - activateTextOrdinalAtEnd(ordinal); + activateTextOrdinalAtEnd(*ordinal); return true; } bool Widget::removeBoundaryOwner(bool forward) { - commitInlineField(); + if (!commitInlineField()) { + return true; + } const auto target = _state->activeBoundaryTarget(forward); using BoundaryAction = State::BoundaryTarget::Action; switch (target.action) { @@ -2500,6 +2580,10 @@ bool Widget::handleStructuralSelectionKey(QKeyEvent *e) { return std::nullopt; }(); const auto target = removeCurrentStructuralSelection(forward); + if (hasStructuralSelection()) { + e->accept(); + return true; + } auto activatedOrigin = false; if (origin && _state->setActiveTextByOrdinal(origin->ordinal)) { const auto cursor = origin->forward ? _state->activeTextLength() : 0; @@ -2526,7 +2610,9 @@ std::optional Widget::removeCurrentStructuralSelection(bool forward) { return std::nullopt; } const auto selection = _structuralSelection; - commitInlineField(); + if (!commitInlineField()) { + return std::nullopt; + } _pendingOrdinal = -1; _pendingCursorOffset = 0; hideInlineField(); @@ -2639,7 +2725,10 @@ bool Widget::handleFieldMouseEvent(QEvent *event) { updateArticleSelection(articlePoint, hit, editHit); if (type == QEvent::MouseButtonRelease) { if (hasStructuralSelection()) { - commitInlineField(); + if (!commitInlineField()) { + mouse->accept(); + return true; + } _pendingOrdinal = -1; _pendingCursorOffset = 0; hideInlineField(); diff --git a/Telegram/SourceFiles/iv/editor/iv_editor_widget.h b/Telegram/SourceFiles/iv/editor/iv_editor_widget.h index 71f5a5a1ed..296a405b1e 100644 --- a/Telegram/SourceFiles/iv/editor/iv_editor_widget.h +++ b/Telegram/SourceFiles/iv/editor/iv_editor_widget.h @@ -50,11 +50,12 @@ public: QWidget *parent, not_null controller, not_null peer, - std::shared_ptr state); + std::shared_ptr state, + Fn showLimitToast = {}); void activateInitialNode(); void activateSegment(int segmentIndex, int cursorOffset); - void commitInlineField(); + [[nodiscard]] bool commitInlineField(); void refreshPreparedContent(); void syncInlineFieldGeometry(); void insertBlock(State::InsertAction action); @@ -173,7 +174,10 @@ private: void refreshInlineFieldPlaceholder(); void refreshInlineFieldPlaceholderColor(); void activateTrailingParagraph(); - void applyFieldTextToState(); + void setInlineFieldFromActiveState(int selectionFrom, int selectionTo); + void revertInlineFieldToState(); + [[nodiscard]] bool applyFieldTextToState(); + bool showLastLimitToast(); void hideInlineField(); void acceptInlineField(); void hideInlineFieldAndRefresh(); @@ -237,6 +241,7 @@ private: const not_null _controller; const not_null _peer; const std::shared_ptr _state; + const Fn _showLimitToast; std::shared_ptr _articleStyle; std::shared_ptr _article; base::unique_qptr _field; diff --git a/Telegram/SourceFiles/iv/iv_rich_message_serializer.cpp b/Telegram/SourceFiles/iv/iv_rich_message_serializer.cpp index 72954c9332..4556a5a029 100644 --- a/Telegram/SourceFiles/iv/iv_rich_message_serializer.cpp +++ b/Telegram/SourceFiles/iv/iv_rich_message_serializer.cpp @@ -372,7 +372,7 @@ struct SerializeContext { return MTP_textUrl( *inner, MTP_string(decoded ? decoded->url : data), - MTP_long(decoded ? decoded->webpageId : 0)); + MTP_long(0)); } case EntityType::MentionName: { const auto userId = CollectMentionUser(context, entity.data()); @@ -404,18 +404,10 @@ struct SerializeContext { case Markdown::InlineTextObjectKind::IvImage: { const auto image = std::get_if< Markdown::InlineTextObjectIvImageData>(&parsed->data); - const auto documentId = image - ? CollectDocument(context, image->documentId) - : std::nullopt; - return (image - && documentId - && image->width > 0 - && image->height > 0) - ? std::make_optional(MTP_textImage( - MTP_long(*documentId), - MTP_int(image->width), - MTP_int(image->height))) - : std::nullopt; + return std::optional(MakePlainRichText( + (image && !image->replacementText.isEmpty()) + ? image->replacementText + : u"[image]"_q)); } } } @@ -915,15 +907,12 @@ struct SerializeContext { if (block.spoiler) { flags |= Flag::f_spoiler; } - if (!block.url.isEmpty()) { - flags |= Flag::f_url; - } return MTP_pageBlockPhoto( MTP_flags(flags), MTP_long(*photoId), *caption, - (block.url.isEmpty() ? MTPstring() : MTP_string(block.url)), - (block.url.isEmpty() ? MTPlong() : MTP_long(0))); + MTPstring(), + MTPlong()); } case BlockKind::Video: { const auto documentId = CollectDocument( diff --git a/Telegram/SourceFiles/iv/iv_rich_page.cpp b/Telegram/SourceFiles/iv/iv_rich_page.cpp index 236cdbdba1..062e35e757 100644 --- a/Telegram/SourceFiles/iv/iv_rich_page.cpp +++ b/Telegram/SourceFiles/iv/iv_rich_page.cpp @@ -21,9 +21,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "iv/markdown/iv_markdown_prepare_serialize.h" #include "iv/markdown/iv_markdown_prepare_links.h" #include "lang/lang_keys.h" +#include "main/main_app_config.h" #include "main/main_session.h" #include "ui/text/text_utilities.h" +#include + #include namespace Iv { @@ -77,8 +80,21 @@ constexpr auto kDefaultMapHeight = 200; return result; } +enum class ParseSource { + InstantViewPage, + RichMessage, +}; + struct ParseContext { + ParseContext( + not_null session, + ParseSource source = ParseSource::InstantViewPage) + : session(session) + , source(source) { + } + not_null session; + ParseSource source = ParseSource::InstantViewPage; base::flat_map photos; base::flat_map documents; base::flat_map photoSizes; @@ -96,11 +112,105 @@ struct ParseContext { bool dropRichTextClickHandlers = false; }; +struct RichMessageMetrics { + int textLength = 0; + int blockCount = 0; + int maxDepth = 0; + int mediaCount = 0; + int maxTableColumns = 0; +}; + enum class RichTextParseMode { Normal, DropClickHandlers, }; +void AccumulateTextLength( + RichMessageMetrics *metrics, + const RichText &text) { + metrics->textLength += int(text.text.text.size()); +} + +void AccumulateTextLength( + RichMessageMetrics *metrics, + const QString &text) { + metrics->textLength += int(text.size()); +} + +[[nodiscard]] bool IsMediaKind(BlockKind kind) { + switch (kind) { + case BlockKind::Photo: + case BlockKind::Video: + case BlockKind::Audio: + return true; + default: + return false; + } +} + +[[nodiscard]] int EffectiveTableColumns(const TableRow &row) { + auto result = 0; + for (const auto &cell : row.cells) { + result += std::max(cell.colspan, 1); + } + return result; +} + +void AccumulateBlockMetrics( + RichMessageMetrics *metrics, + const std::vector &blocks, + int depth); + +void AccumulateBlockMetrics( + RichMessageMetrics *metrics, + const Block &block, + int depth) { + ++metrics->blockCount; + metrics->maxDepth = std::max(metrics->maxDepth, depth); + AccumulateTextLength(metrics, block.text); + AccumulateTextLength(metrics, block.caption); + AccumulateTextLength(metrics, block.formula); + if (IsMediaKind(block.kind)) { + ++metrics->mediaCount; + } + for (const auto &child : block.blocks) { + AccumulateBlockMetrics(metrics, child, depth + 1); + } + for (const auto &item : block.listItems) { + AccumulateTextLength(metrics, item.text); + AccumulateBlockMetrics(metrics, item.blocks, depth + 1); + } + for (const auto &item : block.mediaItems) { + if (IsMediaKind(item.kind)) { + ++metrics->mediaCount; + } + } + for (const auto &row : block.tableRows) { + metrics->maxTableColumns = std::max( + metrics->maxTableColumns, + EffectiveTableColumns(row)); + for (const auto &cell : row.cells) { + AccumulateTextLength(metrics, cell.text); + } + } +} + +void AccumulateBlockMetrics( + RichMessageMetrics *metrics, + const std::vector &blocks, + int depth) { + for (const auto &block : blocks) { + AccumulateBlockMetrics(metrics, block, depth); + } +} + +[[nodiscard]] RichMessageMetrics ComputeRichMessageMetrics( + const RichPage &page) { + auto result = RichMessageMetrics(); + AccumulateBlockMetrics(&result, page.blocks, 1); + return result; +} + [[nodiscard]] QString DateText(TimeId date) { return langDateTimeFull(base::unixtime::parse(date)); } @@ -154,9 +264,12 @@ void AppendRich(RichText *to, RichText &&from) { } [[nodiscard]] QString RichPageLinkEntityData( + ParseSource source, const QString &url, uint64 webpageId) { - return (webpageId && !url.isEmpty()) + return (source == ParseSource::InstantViewPage + && webpageId + && !url.isEmpty()) ? EncodeRichPageLinkUrl(url, webpageId) : url; } @@ -407,7 +520,10 @@ void RememberWebPageMedia( return true; }, [&](const MTPDtextImage &data) { const auto replacementText = u"[image]"_q; - if (!data.vdocument_id().v || data.vw().v <= 0 || data.vh().v <= 0) { + if (context->source == ParseSource::RichMessage + || !data.vdocument_id().v + || data.vw().v <= 0 + || data.vh().v <= 0) { result->text.append(replacementText); return true; } @@ -494,7 +610,10 @@ void RememberWebPageMedia( &result->text, from, EntityType::CustomUrl, - RichPageLinkEntityData(target, uint64(data.vwebpage_id().v))); + RichPageLinkEntityData( + context->source, + target, + uint64(data.vwebpage_id().v))); }, [&](const MTPDtextEmail &data) { const auto from = result->text.text.size(); if (!AppendRichText(data.vtext(), result, context, anchorId, anchorIds)) { @@ -830,7 +949,9 @@ void AppendBlock( const auto photoId = uint64(data.vphoto_id().v); const auto size = FindPhotoSize(*context, photoId); auto parsed = MakeBlock(BlockKind::Photo); - parsed.url = qs(data.vurl().value_or_empty()); + if (context->source == ParseSource::InstantViewPage) { + parsed.url = qs(data.vurl().value_or_empty()); + } parsed.width = size.width(); parsed.height = size.height(); parsed.photoId = photoId; @@ -1358,7 +1479,7 @@ std::shared_ptr ParsePage( const MTPDwebPage *webpage) { return page.match([&](const MTPDpage &data) { auto result = std::make_shared(); - auto context = ParseContext{ session }; + auto context = ParseContext(session, ParseSource::InstantViewPage); result->url = qs(data.vurl()); result->rtl = data.is_rtl(); result->part = data.is_part(); @@ -1381,6 +1502,45 @@ std::shared_ptr ParsePage( } // namespace +RichMessageLimits ResolveRichMessageLimits(not_null session) { + const auto &config = session->appConfig(); + auto result = RichMessageLimits(); + result.lengthLimit = config.get( + u"rich_message_length_limit"_q, + result.lengthLimit); + result.maxBlocks = config.get( + u"rich_message_max_blocks"_q, + result.maxBlocks); + result.maxDepth = config.get( + u"rich_message_max_depth"_q, + result.maxDepth); + result.maxMedia = config.get( + u"rich_message_max_media"_q, + result.maxMedia); + result.maxTableCols = config.get( + u"rich_message_max_table_cols"_q, + result.maxTableCols); + return result; +} + +std::optional ValidateRichMessage( + const RichPage &page, + const RichMessageLimits &limits) { + const auto metrics = ComputeRichMessageMetrics(page); + if (metrics.textLength > limits.lengthLimit) { + return RichMessageLimitError::Length; + } else if (metrics.blockCount > limits.maxBlocks) { + return RichMessageLimitError::Blocks; + } else if (metrics.maxDepth > limits.maxDepth) { + return RichMessageLimitError::Depth; + } else if (metrics.mediaCount > limits.maxMedia) { + return RichMessageLimitError::Media; + } else if (metrics.maxTableColumns > limits.maxTableCols) { + return RichMessageLimitError::TableColumns; + } + return std::nullopt; +} + QString EncodeRichPageLinkUrl( const QString &url, uint64 webpageId) { @@ -1422,7 +1582,7 @@ std::shared_ptr ParseRichPage( not_null session, const MTPRichMessage &message) { auto result = std::make_shared(); - auto context = ParseContext{ session }; + auto context = ParseContext(session, ParseSource::RichMessage); const auto &data = message.data(); result->rtl = data.is_rtl(); result->part = data.is_part(); diff --git a/Telegram/SourceFiles/iv/iv_rich_page.h b/Telegram/SourceFiles/iv/iv_rich_page.h index e9379146dc..08aa2eb2f6 100644 --- a/Telegram/SourceFiles/iv/iv_rich_page.h +++ b/Telegram/SourceFiles/iv/iv_rich_page.h @@ -177,11 +177,32 @@ struct RichPage { std::vector blocks; }; +struct RichMessageLimits { + int lengthLimit = 32768; + int maxBlocks = 500; + int maxDepth = 16; + int maxMedia = 50; + int maxTableCols = 20; +}; + +enum class RichMessageLimitError : unsigned char { + Length, + Blocks, + Depth, + Media, + TableColumns, +}; + struct RichPageLinkUrl { QString url; uint64 webpageId = 0; }; +[[nodiscard]] RichMessageLimits ResolveRichMessageLimits( + not_null session); +[[nodiscard]] std::optional ValidateRichMessage( + const RichPage &page, + const RichMessageLimits &limits); [[nodiscard]] QString EncodeRichPageLinkUrl( const QString &url, uint64 webpageId); diff --git a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp index 9d2e0d355e..767ee3264d 100644 --- a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp +++ b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp @@ -1230,10 +1230,11 @@ void ShortcutMessages::edit( const auto hasMediaWithCaption = item && item->media() && item->media()->allowsEditCaption(); - const auto maxCaptionSize = !hasMediaWithCaption - ? MaxMessageSize - : Data::PremiumLimits(_session).captionLengthCurrent(); - if (!TextUtilities::CutPart(sending, left, maxCaptionSize) + const auto limits = Data::PremiumLimits(_session); + const auto maxTextSize = hasMediaWithCaption + ? limits.captionLengthCurrent() + : limits.messageLengthCurrent(); + if (!TextUtilities::CutPart(sending, left, maxTextSize) && !hasMediaWithCaption) { if (item) { _controller->show(Box(item)); @@ -1242,7 +1243,7 @@ void ShortcutMessages::edit( } return; } else if (!left.text.isEmpty()) { - const auto remove = originalLeftSize - maxCaptionSize; + const auto remove = originalLeftSize - maxTextSize; _controller->showToast( tr::lng_edit_limit_reached(tr::now, lt_count, remove)); return; diff --git a/Telegram/SourceFiles/support/support_helper.cpp b/Telegram/SourceFiles/support/support_helper.cpp index 0b6c5531bf..e31e770866 100644 --- a/Telegram/SourceFiles/support/support_helper.cpp +++ b/Telegram/SourceFiles/support/support_helper.cpp @@ -53,7 +53,7 @@ namespace { constexpr auto kOccupyFor = TimeId(60); constexpr auto kReoccupyEach = 30 * crl::time(1000); -constexpr auto kMaxSupportInfoLength = MaxMessageSize * 4; +constexpr auto kMaxSupportInfoLength = 16 * 1024; constexpr auto kTopicRootId = MsgId(0); constexpr auto kMonoforumPeerId = PeerId(0); diff --git a/Telegram/SourceFiles/ui/controls/compose_ai_button_factory.cpp b/Telegram/SourceFiles/ui/controls/compose_ai_button_factory.cpp index ba6456db8e..6cea602d92 100644 --- a/Telegram/SourceFiles/ui/controls/compose_ai_button_factory.cpp +++ b/Telegram/SourceFiles/ui/controls/compose_ai_button_factory.cpp @@ -9,9 +9,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/options.h" #include "boxes/compose_ai_box.h" -#include "config.h" #include "core/mime_type.h" #include "data/data_ai_compose_tones.h" +#include "data/data_premium_limits.h" #include "data/data_session.h" #include "history/view/controls/history_view_compose_ai_button.h" #include "lang/lang_keys.h" @@ -52,7 +52,7 @@ bool HasEnoughLinesForAi( return false; } const auto &text = field->getLastText(); - if (text.size() > MaxMessageSize) { + if (text.size() > Data::PremiumLimits(session).messageLengthCurrent()) { return false; } for (const auto &ch : text) { @@ -78,11 +78,13 @@ PreparedList PrepareTextAsFile(const QString &text) { constexpr auto kSendAsFilePasteMultiplier = 8; -int SendAsFilePasteThreshold() { - return kSendAsFilePasteMultiplier * MaxMessageSize; +int SendAsFilePasteThreshold(not_null session) { + return kSendAsFilePasteMultiplier + * Data::PremiumLimits(session).messageLengthCurrent(); } LargeTextPasteResult CheckLargeTextPaste( + not_null session, not_null field, not_null data) { if (data->hasImage()) { @@ -99,7 +101,7 @@ LargeTextPasteResult CheckLargeTextPaste( const auto resultingSize = currentText.size() - (selEnd - selStart) + pasteText.size(); - if (resultingSize < SendAsFilePasteThreshold()) { + if (resultingSize < SendAsFilePasteThreshold(session)) { return {}; } return { diff --git a/Telegram/SourceFiles/ui/controls/compose_ai_button_factory.h b/Telegram/SourceFiles/ui/controls/compose_ai_button_factory.h index 8e6d9bb7cc..73768fee6a 100644 --- a/Telegram/SourceFiles/ui/controls/compose_ai_button_factory.h +++ b/Telegram/SourceFiles/ui/controls/compose_ai_button_factory.h @@ -49,7 +49,6 @@ void UpdateCaptionAiButtonGeometry( not_null field); [[nodiscard]] PreparedList PrepareTextAsFile(const QString &text); -[[nodiscard]] int SendAsFilePasteThreshold(); struct LargeTextPasteResult { bool exceeds = false; @@ -57,6 +56,7 @@ struct LargeTextPasteResult { }; [[nodiscard]] LargeTextPasteResult CheckLargeTextPaste( + not_null session, not_null field, not_null data); diff --git a/Telegram/SourceFiles/window/notifications_manager_default.cpp b/Telegram/SourceFiles/window/notifications_manager_default.cpp index 8fdc81c097..1cc4d549f0 100644 --- a/Telegram/SourceFiles/window/notifications_manager_default.cpp +++ b/Telegram/SourceFiles/window/notifications_manager_default.cpp @@ -23,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/painter.h" #include "ui/power_saving.h" #include "ui/ui_utility.h" +#include "data/data_premium_limits.h" #include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_forum_topic.h" @@ -1115,7 +1116,8 @@ void Notification::showReplyField() { _replyArea->moveToLeft(st::notifyBorderWidth, st::notifyMinHeight); _replyArea->show(); _replyArea->setFocus(); - _replyArea->setMaxLength(MaxMessageSize); + _replyArea->setMaxLength( + Data::PremiumLimits(&_item->history()->session()).messageLengthCurrent()); _replyArea->setSubmitSettings(Ui::InputField::SubmitSettings::Both); InitMessageFieldHandlers({ .session = &_item->history()->session(), diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 8f2d783c5e..7bde506634 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -75,6 +75,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" #include "history/history_item_helpers.h" // GetErrorForSending. #include "history/history_item_components.h" +#include "history/view/controls/history_view_forward_panel.h" #include "history/view/history_view_context_menu.h" #include "history/view/history_view_schedule_box.h" #include "window/window_separate_id.h" @@ -2869,6 +2870,16 @@ base::weak_qptr ShowForwardMessagesBox( const auto msgIds = owner->itemsToIds(itemsList); const auto sendersCount = ItemsForwardSendersCount(itemsList); const auto captionsCount = ItemsForwardCaptionsCount(itemsList); + const auto hasRichPage = HistoryView::Controls::HasRichPage(itemsList); + const auto hasOnlyForcedForwardedInfo = !captionsCount + && HistoryView::Controls::HasOnlyForcedForwardedInfo(itemsList); + const auto showForwardOptions = !hasOnlyForcedForwardedInfo + && (!hasRichPage + || HistoryView::Controls::CanHideForwardAuthor(session, itemsList)); + draft.options = HistoryView::Controls::NormalizeForwardOptions( + session, + itemsList, + draft.options); if (msgIds.empty()) { return nullptr; } @@ -3090,6 +3101,9 @@ base::weak_qptr ShowForwardMessagesBox( boxRaw->setForwardOptions({ .sendersCount = sendersCount, .captionsCount = captionsCount, + .dropNames = (draft.options != Data::ForwardOptions::PreserveInfo), + .dropCaptions = (draft.options + == Data::ForwardOptions::NoNamesAndCaptions), }); show->showBox(std::move(box)); auto state = State{ boxRaw, controllerRaw }; @@ -3217,6 +3231,10 @@ base::weak_qptr ShowForwardMessagesBox( state->submit = nullptr; return true; }; + auto forwardOptions = HistoryView::Controls::NormalizeForwardOptions( + session, + itemsList, + state->box->forwardOptionsData()); send( ranges::views::all( peers @@ -3227,7 +3245,7 @@ base::weak_qptr ShowForwardMessagesBox( checkPaid, std::move(comment), options, - state->box->forwardOptionsData()); + forwardOptions); if (!state->submit && successCallback) { successCallback(); } @@ -3252,7 +3270,6 @@ base::weak_qptr ShowForwardMessagesBox( : SendMenu::Type::Scheduled; }; - const auto showForwardOptions = true; const auto showMenu = [=](not_null parent) { if (state->menu) { state->menu = nullptr;