diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index fd47cfb6ba..4ef120b1af 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1339,6 +1339,8 @@ PRIVATE media/view/media_view_playback_controls.h media/view/media_view_playback_progress.cpp media/view/media_view_playback_progress.h + media/view/media_view_playback_sponsored.cpp + media/view/media_view_playback_sponsored.h media/system_media_controls_manager.h media/system_media_controls_manager.cpp menu/menu_antispam_validator.cpp @@ -2071,7 +2073,7 @@ if (MSVC) /DELAYLOAD:API-MS-Win-Core-ProcessThreads-l1-1-0.dll /DELAYLOAD:API-MS-Win-Core-Synch-l1-2-0.dll # Synchronization.lib /DELAYLOAD:API-MS-Win-Core-SysInfo-l1-1-0.dll - /DELAYLOAD:API-MS-Win-Core-Timezone-l1-1-0.dll + # /DELAYLOAD:API-MS-Win-Core-Timezone-l1-1-0.dll /DELAYLOAD:API-MS-Win-Core-WinRT-l1-1-0.dll /DELAYLOAD:API-MS-Win-Core-WinRT-Error-l1-1-0.dll /DELAYLOAD:API-MS-Win-Core-WinRT-String-l1-1-0.dll diff --git a/Telegram/Resources/animations/no_chats.tgs b/Telegram/Resources/animations/no_chats.tgs new file mode 100644 index 0000000000..a30673b48a Binary files /dev/null and b/Telegram/Resources/animations/no_chats.tgs differ diff --git a/Telegram/Resources/icons/info/edit/stickers_add.png b/Telegram/Resources/icons/menu/add.png similarity index 100% rename from Telegram/Resources/icons/info/edit/stickers_add.png rename to Telegram/Resources/icons/menu/add.png diff --git a/Telegram/Resources/icons/info/edit/stickers_add@2x.png b/Telegram/Resources/icons/menu/add@2x.png similarity index 100% rename from Telegram/Resources/icons/info/edit/stickers_add@2x.png rename to Telegram/Resources/icons/menu/add@2x.png diff --git a/Telegram/Resources/icons/info/edit/stickers_add@3x.png b/Telegram/Resources/icons/menu/add@3x.png similarity index 100% rename from Telegram/Resources/icons/info/edit/stickers_add@3x.png rename to Telegram/Resources/icons/menu/add@3x.png diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index caac333013..ac5ef1dec2 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -434,6 +434,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_dlg_new_channel_name" = "Channel name"; "lng_dlg_new_bot_name" = "Bot name"; "lng_no_chats" = "Your chats will be here"; +"lng_no_conversations" = "You have no\nconversations yet."; +"lng_no_conversations_button" = "New Message"; +"lng_no_conversations_subtitle" = "Your contacts on Telegram"; "lng_no_chats_filter" = "No chats currently belong to this folder."; "lng_no_saved_sublists" = "You can save messages from other chats here."; "lng_contacts_loading" = "Loading..."; @@ -4260,6 +4263,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_context_to_msg" = "Go To Message"; "lng_context_reply_msg" = "Reply"; "lng_context_quote_and_reply" = "Quote & Reply"; +"lng_context_reply_to_task" = "Reply to Task"; "lng_context_edit_msg" = "Edit"; "lng_context_add_factcheck" = "Add Fact Check"; "lng_context_edit_factcheck" = "Edit Fact Check"; @@ -4450,6 +4454,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_inline_switch_cant" = "Sorry, no way to write here :("; "lng_preview_reply_to" = "Reply to {name}"; "lng_preview_reply_to_quote" = "Reply to quote from {name}"; +"lng_preview_reply_to_task" = "Reply to task from {title}"; "lng_suggest_bar_title" = "Suggest a Post Below"; "lng_suggest_bar_text" = "Click to offer a price for publishing."; diff --git a/Telegram/Resources/qrc/telegram/animations.qrc b/Telegram/Resources/qrc/telegram/animations.qrc index 9beaa522d3..cc9c2313ed 100644 --- a/Telegram/Resources/qrc/telegram/animations.qrc +++ b/Telegram/Resources/qrc/telegram/animations.qrc @@ -38,6 +38,7 @@ ../../animations/edit_peers/topics_tabs.tgs ../../animations/edit_peers/topics_list.tgs ../../animations/edit_peers/direct_messages.tgs + ../../animations/no_chats.tgs ../../animations/dice/dice_idle.tgs ../../animations/dice/dart_idle.tgs diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml index d83d286216..d63d433f73 100644 --- a/Telegram/Resources/uwp/AppX/AppxManifest.xml +++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml @@ -10,7 +10,7 @@ + Version="5.16.4.0" /> Telegram Desktop Telegram Messenger LLP diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc index b96320c234..218aa82970 100644 --- a/Telegram/Resources/winrc/Telegram.rc +++ b/Telegram/Resources/winrc/Telegram.rc @@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico" // VS_VERSION_INFO VERSIONINFO - FILEVERSION 5,16,3,0 - PRODUCTVERSION 5,16,3,0 + FILEVERSION 5,16,4,0 + PRODUCTVERSION 5,16,4,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -62,10 +62,10 @@ BEGIN BEGIN VALUE "CompanyName", "Radolyn Labs" VALUE "FileDescription", "AyuGram Desktop" - VALUE "FileVersion", "5.16.3.0" + VALUE "FileVersion", "5.16.4.0" VALUE "LegalCopyright", "Copyright (C) 2014-2025" VALUE "ProductName", "AyuGram Desktop" - VALUE "ProductVersion", "5.16.3.0" + VALUE "ProductVersion", "5.16.4.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc index e1293d48f7..81f44604c6 100644 --- a/Telegram/Resources/winrc/Updater.rc +++ b/Telegram/Resources/winrc/Updater.rc @@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US // VS_VERSION_INFO VERSIONINFO - FILEVERSION 5,16,3,0 - PRODUCTVERSION 5,16,3,0 + FILEVERSION 5,16,4,0 + PRODUCTVERSION 5,16,4,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -53,10 +53,10 @@ BEGIN BEGIN VALUE "CompanyName", "Radolyn Labs" VALUE "FileDescription", "AyuGram Desktop Updater" - VALUE "FileVersion", "5.16.3.0" + VALUE "FileVersion", "5.16.4.0" VALUE "LegalCopyright", "Copyright (C) 2014-2025" VALUE "ProductName", "AyuGram Desktop" - VALUE "ProductVersion", "5.16.3.0" + VALUE "ProductVersion", "5.16.4.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/SourceFiles/boxes/connection_box.cpp b/Telegram/SourceFiles/boxes/connection_box.cpp index e5833d5f85..898c049d66 100644 --- a/Telegram/SourceFiles/boxes/connection_box.cpp +++ b/Telegram/SourceFiles/boxes/connection_box.cpp @@ -111,6 +111,13 @@ void AddProxyFromClipboard( QGuiApplication::clipboard()->text()); const auto isSingle = maybeUrls.size() == 1; + enum class Result { + Success, + Failed, + Unsupported, + Invalid, + }; + const auto proceedUrl = [=](const auto &local) { const auto command = base::StringViewMid( local, @@ -146,6 +153,11 @@ void AddProxyFromClipboard( match->captured(1), qthelp::UrlParamNameTransform::ToLower); const auto proxy = ProxyDataFromFields(type, fields); + if (!proxy) { + return (proxy.status() == ProxyData::Status::Unsupported) + ? Result::Unsupported + : Result::Invalid; + } const auto contains = controller->contains(proxy); const auto toast = (contains ? tr::lng_proxy_add_from_clipboard_existing_toast @@ -158,19 +170,29 @@ void AddProxyFromClipboard( } break; } - return true; + return Result::Success; } - return false; + return Result::Failed; }; - auto success = false; + auto success = Result::Failed; for (const auto &maybeUrl : maybeUrls) { - success |= proceedUrl(Core::TryConvertUrlToLocal(maybeUrl)); + const auto result = proceedUrl(Core::TryConvertUrlToLocal(maybeUrl)); + if (success != Result::Success) { + success = result; + } } - if (!success) { - show->showToast( - tr::lng_proxy_add_from_clipboard_failed_toast(tr::now)); + if (success != Result::Success) { + if (success == Result::Failed) { + show->showToast( + tr::lng_proxy_add_from_clipboard_failed_toast(tr::now)); + } else { + show->showBox(Ui::MakeInformBox( + (success == Result::Unsupported + ? tr::lng_proxy_unsupported(tr::now) + : tr::lng_proxy_invalid(tr::now)))); + } } } diff --git a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp index 071b9dd684..bf8c08c3ea 100644 --- a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp +++ b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp @@ -361,9 +361,14 @@ void CreateModerateMessagesBox( }); } if (allCanBan) { - auto ownedWrap = object_ptr>( - inner, - object_ptr(inner)); + const auto peer = items.front()->history()->peer; + auto ownedWrap = peer->isMonoforum() + ? nullptr + : object_ptr>( + inner, + object_ptr(inner)); + auto computeRestrictions = Fn(); + const auto wrap = ownedWrap.data(); Ui::AddSkip(inner); Ui::AddSkip(inner); @@ -371,7 +376,9 @@ void CreateModerateMessagesBox( object_ptr( box, rpl::conditional( - ownedWrap->toggledValue(), + (ownedWrap + ? ownedWrap->toggledValue() + : rpl::single(false) | rpl::type_erased()), tr::lng_restrict_user( lt_count, rpl::single(participants.size()) | tr::to_count()), @@ -390,136 +397,141 @@ void CreateModerateMessagesBox( Ui::AddSkip(inner); Ui::AddSkip(inner); - const auto wrap = inner->add(std::move(ownedWrap)); - const auto container = wrap->entity(); - wrap->toggle(false, anim::type::instant); + if (ownedWrap) { + inner->add(std::move(ownedWrap)); - const auto session = &participants.front()->session(); - const auto emojiMargin = QMargins( - -st::moderateBoxExpandInnerSkip, - -st::moderateBoxExpandInnerSkip / 2, - 0, - 0); - const auto emojiUp = Ui::Text::SingleCustomEmoji( - session->data().customEmojiManager().registerInternalEmoji( - st::moderateBoxExpandIcon, - emojiMargin, - false)); - const auto emojiDown = Ui::Text::SingleCustomEmoji( - session->data().customEmojiManager().registerInternalEmoji( - st::moderateBoxExpandIconDown, - emojiMargin, - false)); + const auto container = wrap->entity(); + wrap->toggle(false, anim::type::instant); - auto label = object_ptr( - inner, - QString(), - st::moderateBoxDividerLabel); - const auto raw = label.data(); + const auto session = &participants.front()->session(); + const auto emojiMargin = QMargins( + -st::moderateBoxExpandInnerSkip, + -st::moderateBoxExpandInnerSkip / 2, + 0, + 0); + const auto emojiUp = Ui::Text::SingleCustomEmoji( + session->data().customEmojiManager().registerInternalEmoji( + st::moderateBoxExpandIcon, + emojiMargin, + false)); + const auto emojiDown = Ui::Text::SingleCustomEmoji( + session->data().customEmojiManager().registerInternalEmoji( + st::moderateBoxExpandIconDown, + emojiMargin, + false)); - auto &lifetime = wrap->lifetime(); - const auto scrollLifetime = lifetime.make_state(); - label->setClickHandlerFilter([=]( - const ClickHandlerPtr &handler, - Qt::MouseButton button) { - if (button != Qt::LeftButton) { - return false; - } - wrap->toggle(!wrap->toggled(), anim::type::normal); - { - inner->heightValue() | rpl::start_with_next([=] { - if (!wrap->animating()) { - scrollLifetime->destroy(); - Ui::PostponeCall(crl::guard(box, [=] { + auto label = object_ptr( + inner, + QString(), + st::moderateBoxDividerLabel); + const auto raw = label.data(); + + auto &lifetime = wrap->lifetime(); + const auto scrollLifetime = lifetime.make_state(); + label->setClickHandlerFilter([=]( + const ClickHandlerPtr &handler, + Qt::MouseButton button) { + if (button != Qt::LeftButton) { + return false; + } + wrap->toggle(!wrap->toggled(), anim::type::normal); + { + inner->heightValue() | rpl::start_with_next([=] { + if (!wrap->animating()) { + scrollLifetime->destroy(); + Ui::PostponeCall(crl::guard(box, [=] { + box->scrollToY(std::numeric_limits::max()); + })); + } else { box->scrollToY(std::numeric_limits::max()); - })); - } else { - box->scrollToY(std::numeric_limits::max()); - } - }, *scrollLifetime); - } - return true; - }); - wrap->toggledValue( - ) | rpl::map([isSingle, emojiUp, emojiDown](bool toggled) { - return ((toggled && isSingle) - ? tr::lng_restrict_user_part - : (toggled && !isSingle) - ? tr::lng_restrict_users_part - : isSingle - ? tr::lng_restrict_user_full - : tr::lng_restrict_users_full)( - lt_emoji, - rpl::single(toggled ? emojiUp : emojiDown), - Ui::Text::WithEntities); - }) | rpl::flatten_latest( - ) | rpl::start_with_next([=](const TextWithEntities &text) { - raw->setMarkedText( - Ui::Text::Link(text, u"internal:"_q), - Core::TextContext({ .session = session })); - }, label->lifetime()); + } + }, *scrollLifetime); + } + return true; + }); + wrap->toggledValue( + ) | rpl::map([isSingle, emojiUp, emojiDown](bool toggled) { + return ((toggled && isSingle) + ? tr::lng_restrict_user_part + : (toggled && !isSingle) + ? tr::lng_restrict_users_part + : isSingle + ? tr::lng_restrict_user_full + : tr::lng_restrict_users_full)( + lt_emoji, + rpl::single(toggled ? emojiUp : emojiDown), + Ui::Text::WithEntities); + }) | rpl::flatten_latest( + ) | rpl::start_with_next([=](const TextWithEntities &text) { + raw->setMarkedText( + Ui::Text::Link(text, u"internal:"_q), + Core::TextContext({ .session = session })); + }, label->lifetime()); - Ui::AddSkip(inner); - inner->add(object_ptr( - inner, - std::move(label), - st::defaultBoxDividerLabelPadding, - RectPart::Top | RectPart::Bottom)); + Ui::AddSkip(inner); + inner->add(object_ptr( + inner, + std::move(label), + st::defaultBoxDividerLabelPadding, + RectPart::Top | RectPart::Bottom)); - using Flag = ChatRestriction; - using Flags = ChatRestrictions; - const auto peer = items.front()->history()->peer; - const auto chat = peer->asChat(); - const auto channel = peer->asChannel(); - const auto defaultRestrictions = chat - ? chat->defaultRestrictions() - : channel->defaultRestrictions(); - const auto prepareFlags = FixDependentRestrictions( - defaultRestrictions - | ((channel && channel->isPublic()) - ? (Flag::ChangeInfo | Flag::PinMessages) - : Flags(0))); - const auto disabledMessages = [&] { - auto result = base::flat_map(); - { - const auto disabled = FixDependentRestrictions( - defaultRestrictions - | ((channel && channel->isPublic()) - ? (Flag::ChangeInfo | Flag::PinMessages) - : Flags(0))); - result.emplace( - disabled, - tr::lng_rights_restriction_for_all(tr::now)); - } - return result; - }(); + using Flag = ChatRestriction; + using Flags = ChatRestrictions; + const auto chat = peer->asChat(); + const auto channel = peer->asChannel(); + const auto defaultRestrictions = chat + ? chat->defaultRestrictions() + : channel->defaultRestrictions(); + const auto prepareFlags = FixDependentRestrictions( + defaultRestrictions + | ((channel && channel->isPublic()) + ? (Flag::ChangeInfo | Flag::PinMessages) + : Flags(0))); + const auto disabledMessages = [&] { + auto result = base::flat_map(); + { + const auto disabled = FixDependentRestrictions( + defaultRestrictions + | ((channel && channel->isPublic()) + ? (Flag::ChangeInfo | Flag::PinMessages) + : Flags(0))); + result.emplace( + disabled, + tr::lng_rights_restriction_for_all(tr::now)); + } + return result; + }(); - Ui::AddSubsectionTitle( - inner, - rpl::conditional( - rpl::single(isSingle), - tr::lng_restrict_users_part_single_header(), - tr::lng_restrict_users_part_header( - lt_count, - rpl::single(participants.size()) | tr::to_count()))); - auto [checkboxes, getRestrictions, changes] = CreateEditRestrictions( - box, - prepareFlags, - disabledMessages, - { .isForum = peer->isForum() }); - std::move(changes) | rpl::start_with_next([=] { - ban->setChecked(true); - }, ban->lifetime()); - Ui::AddSkip(container); - Ui::AddDivider(container); - Ui::AddSkip(container); - container->add(std::move(checkboxes)); + Ui::AddSubsectionTitle( + inner, + rpl::conditional( + rpl::single(isSingle), + tr::lng_restrict_users_part_single_header(), + tr::lng_restrict_users_part_header( + lt_count, + rpl::single(participants.size()) | tr::to_count()))); + auto [checkboxes, getRestrictions, changes] = CreateEditRestrictions( + box, + prepareFlags, + disabledMessages, + { .isForum = peer->isForum() }); + computeRestrictions = getRestrictions; + std::move(changes) | rpl::start_with_next([=] { + ban->setChecked(true); + }, ban->lifetime()); + Ui::AddSkip(container); + Ui::AddDivider(container); + Ui::AddSkip(container); + container->add(std::move(checkboxes)); + } // Handle confirmation manually. confirms->events() | rpl::start_with_next([=] { if (ban->checked() && controller->collectRequests) { - const auto kick = !wrap->toggled(); - const auto restrictions = getRestrictions(); + const auto kick = !wrap || !wrap->toggled(); + const auto restrictions = computeRestrictions + ? computeRestrictions() + : ChatRestrictions(); const auto request = [=]( not_null peer, not_null channel) { @@ -532,10 +544,15 @@ void CreateModerateMessagesBox( nullptr, nullptr); } else { - channel->session().api().chatParticipants().kick( - channel, - peer, - { channel->restrictions(), 0 }); + const auto block = channel->isMonoforum() + ? channel->monoforumBroadcast() + : channel.get(); + if (block) { + block->session().api().chatParticipants().kick( + block, + peer, + { block->restrictions(), 0 }); + } } }; sequentiallyRequest(request, controller->collectRequests()); diff --git a/Telegram/SourceFiles/boxes/star_gift_box.cpp b/Telegram/SourceFiles/boxes/star_gift_box.cpp index 64ce31f976..5ba5079c4f 100644 --- a/Telegram/SourceFiles/boxes/star_gift_box.cpp +++ b/Telegram/SourceFiles/boxes/star_gift_box.cpp @@ -127,6 +127,7 @@ constexpr auto kUpgradeDoneToastDuration = 4 * crl::time(1000); constexpr auto kGiftsPreloadTimeout = 3 * crl::time(1000); constexpr auto kResaleGiftsPerPage = 50; constexpr auto kFiltersCount = 4; +constexpr auto kResellPriceCacheLifetime = 60 * crl::time(1000); using namespace HistoryView; using namespace Info::PeerGifts; @@ -220,6 +221,33 @@ struct GiftDetails { bool byStars = false; }; +struct SessionResalePrices { + explicit SessionResalePrices(not_null session) + : api(std::make_unique(session->user())) { + } + + std::unique_ptr api; + base::flat_map prices; + std::vector> waiting; + rpl::lifetime requestLifetime; + crl::time lastReceived = 0; +}; + +[[nodiscard]] not_null ResalePrices( + not_null session) { + static auto result = base::flat_map< + not_null, + std::unique_ptr>(); + if (const auto i = result.find(session); i != end(result)) { + return i->second.get(); + } + const auto i = result.emplace( + session, + std::make_unique(session)).first; + session->lifetime().add([session] { result.remove(session); }); + return i->second.get(); +} + class PeerRow final : public PeerListRow { public: using PeerListRow::PeerListRow; @@ -4381,6 +4409,55 @@ void ShowUniqueGiftWearBox( })); } +void PreloadUniqueGiftResellPrices(not_null session) { + const auto entry = ResalePrices(session); + const auto now = crl::now(); + const auto makeRequest = entry->prices.empty() + || (now - entry->lastReceived >= kResellPriceCacheLifetime); + if (!makeRequest || entry->requestLifetime) { + return; + } + const auto finish = [=] { + entry->requestLifetime.destroy(); + entry->lastReceived = crl::now(); + for (const auto &callback : base::take(entry->waiting)) { + callback(); + } + }; + entry->requestLifetime = entry->api->requestStarGifts( + ) | rpl::start_with_error_done(finish, [=] { + const auto &gifts = entry->api->starGifts(); + entry->prices.reserve(gifts.size()); + for (auto &gift : gifts) { + if (!gift.resellTitle.isEmpty() && gift.starsResellMin > 0) { + entry->prices[gift.resellTitle] = gift.starsResellMin; + } + } + finish(); + }); +} + +void InvokeWithUniqueGiftResellPrice( + not_null session, + const QString &title, + Fn callback) { + PreloadUniqueGiftResellPrices(session); + + const auto finish = [=] { + const auto entry = ResalePrices(session); + Assert(entry->lastReceived != 0); + + const auto i = entry->prices.find(title); + callback((i != end(entry->prices)) ? i->second : 0); + }; + const auto entry = ResalePrices(session); + if (entry->lastReceived) { + finish(); + } else { + entry->waiting.push_back(finish); + } +} + void UpdateGiftSellPrice( std::shared_ptr show, std::shared_ptr unique, @@ -4422,6 +4499,132 @@ void UpdateGiftSellPrice( }).send(); } +void UniqueGiftSellBox( + not_null box, + std::shared_ptr show, + std::shared_ptr unique, + Data::SavedStarGiftId savedId, + int price, + Settings::GiftWearBoxStyleOverride st) { + box->setTitle(tr::lng_gift_sell_title()); + box->setStyle(st.box ? *st.box : st::upgradeGiftBox); + box->setWidth(st::boxWideWidth); + + box->addTopButton(st.close ? *st.close : st::boxTitleClose, [=] { + box->closeBox(); + }); + const auto priceNow = unique->starsForResale; + const auto name = Data::UniqueGiftName(*unique); + const auto slug = unique->slug; + + const auto session = &show->session(); + AddSubsectionTitle( + box->verticalLayout(), + tr::lng_gift_sell_placeholder(), + (st::boxRowPadding - QMargins( + st::defaultSubsectionTitlePadding.left(), + 0, + st::defaultSubsectionTitlePadding.right(), + st::defaultSubsectionTitlePadding.bottom()))); + const auto &appConfig = session->appConfig(); + const auto limit = appConfig.giftResalePriceMax(); + const auto minimal = appConfig.giftResalePriceMin(); + const auto thousandths = appConfig.giftResaleReceiveThousandths(); + const auto wrap = box->addRow(object_ptr( + box, + st::editTagField.heightMin)); + auto owned = object_ptr( + wrap, + st::editTagField, + rpl::single(QString()), + QString::number(priceNow ? priceNow : price ? price : minimal), + limit); + const auto field = owned.data(); + wrap->widthValue() | rpl::start_with_next([=](int width) { + field->move(0, 0); + field->resize(width, field->height()); + wrap->resize(width, field->height()); + }, wrap->lifetime()); + field->paintRequest() | rpl::start_with_next([=](QRect clip) { + auto p = QPainter(field); + st::paidStarIcon.paint(p, 0, st::paidStarIconTop, field->width()); + }, field->lifetime()); + field->selectAll(); + box->setFocusCallback([=] { + field->setFocusFast(); + }); + + const auto errors = box->lifetime().make_state< + rpl::event_stream<> + >(); + auto goods = rpl::merge( + rpl::single(rpl::empty) | rpl::map_to(true), + base::qt_signal_producer( + field, + &Ui::NumberInput::changed + ) | rpl::map_to(true), + errors->events() | rpl::map_to(false) + ) | rpl::start_spawning(box->lifetime()); + auto text = rpl::duplicate(goods) | rpl::map([=](bool good) { + const auto value = field->getLastText().toInt(); + const auto receive = (int64(value) * thousandths) / 1000; + return !good + ? tr::lng_gift_sell_min_price( + tr::now, + lt_count, + minimal, + Ui::Text::RichLangValue) + : (value >= minimal) + ? tr::lng_gift_sell_amount( + tr::now, + lt_count, + receive, + Ui::Text::RichLangValue) + : tr::lng_gift_sell_about( + tr::now, + lt_percent, + TextWithEntities{ u"%1%"_q.arg(thousandths / 10.) }, + Ui::Text::RichLangValue); + }); + const auto details = box->addRow(object_ptr( + box, + std::move(text) | rpl::after_next([=] { + box->verticalLayout()->resizeToWidth(box->width()); + }), + st::boxLabel)); + Ui::AddSkip(box->verticalLayout()); + + rpl::duplicate(goods) | rpl::start_with_next([=](bool good) { + details->setTextColorOverride( + good ? st::windowSubTextFg->c : st::boxTextFgError->c); + }, details->lifetime()); + + QObject::connect(field, &NumberInput::submitted, [=] { + const auto count = field->getLastText().toInt(); + if (count < minimal) { + field->showError(); + errors->fire({}); + return; + } + box->closeBox(); + UpdateGiftSellPrice(show, unique, savedId, count); + }); + const auto button = box->addButton(priceNow + ? tr::lng_gift_sell_update() + : tr::lng_gift_sell_put(), [=] { field->submitted({}); }); + rpl::combine( + box->widthValue(), + button->widthValue() + ) | rpl::start_with_next([=](int outer, int inner) { + const auto padding = st::giftBox.buttonPadding; + const auto wanted = outer - padding.left() - padding.right(); + if (inner != wanted) { + button->resizeToWidth(wanted); + button->moveToLeft(padding.left(), padding.top()); + } + }, box->lifetime()); +} + void ShowUniqueGiftSellBox( std::shared_ptr show, std::shared_ptr unique, @@ -4430,125 +4633,11 @@ void ShowUniqueGiftSellBox( if (ShowResaleGiftLater(show, unique)) { return; } - show->show(Box([=](not_null box) { - box->setTitle(tr::lng_gift_sell_title()); - box->setStyle(st.box ? *st.box : st::upgradeGiftBox); - box->setWidth(st::boxWideWidth); - - box->addTopButton(st.close ? *st.close : st::boxTitleClose, [=] { - box->closeBox(); - }); - const auto priceNow = unique->starsForResale; - const auto name = Data::UniqueGiftName(*unique); - const auto slug = unique->slug; - - const auto session = &show->session(); - AddSubsectionTitle( - box->verticalLayout(), - tr::lng_gift_sell_placeholder(), - (st::boxRowPadding - QMargins( - st::defaultSubsectionTitlePadding.left(), - 0, - st::defaultSubsectionTitlePadding.right(), - st::defaultSubsectionTitlePadding.bottom()))); - const auto &appConfig = session->appConfig(); - const auto limit = appConfig.giftResalePriceMax(); - const auto minimal = appConfig.giftResalePriceMin(); - const auto thousandths = appConfig.giftResaleReceiveThousandths(); - const auto wrap = box->addRow(object_ptr( - box, - st::editTagField.heightMin)); - auto owned = object_ptr( - wrap, - st::editTagField, - rpl::single(QString()), - QString::number(priceNow ? priceNow : minimal), - limit); - const auto field = owned.data(); - wrap->widthValue() | rpl::start_with_next([=](int width) { - field->move(0, 0); - field->resize(width, field->height()); - wrap->resize(width, field->height()); - }, wrap->lifetime()); - field->paintRequest() | rpl::start_with_next([=](QRect clip) { - auto p = QPainter(field); - st::paidStarIcon.paint(p, 0, st::paidStarIconTop, field->width()); - }, field->lifetime()); - field->selectAll(); - box->setFocusCallback([=] { - field->setFocusFast(); - }); - - const auto errors = box->lifetime().make_state< - rpl::event_stream<> - >(); - auto goods = rpl::merge( - rpl::single(rpl::empty) | rpl::map_to(true), - base::qt_signal_producer( - field, - &Ui::NumberInput::changed - ) | rpl::map_to(true), - errors->events() | rpl::map_to(false) - ) | rpl::start_spawning(box->lifetime()); - auto text = rpl::duplicate(goods) | rpl::map([=](bool good) { - const auto value = field->getLastText().toInt(); - const auto receive = (int64(value) * thousandths) / 1000; - return !good - ? tr::lng_gift_sell_min_price( - tr::now, - lt_count, - minimal, - Ui::Text::RichLangValue) - : (value >= minimal) - ? tr::lng_gift_sell_amount( - tr::now, - lt_count, - receive, - Ui::Text::RichLangValue) - : tr::lng_gift_sell_about( - tr::now, - lt_percent, - TextWithEntities{ u"%1%"_q.arg(thousandths / 10.) }, - Ui::Text::RichLangValue); - }); - const auto details = box->addRow(object_ptr( - box, - std::move(text) | rpl::after_next([=] { - box->verticalLayout()->resizeToWidth(box->width()); - }), - st::boxLabel)); - Ui::AddSkip(box->verticalLayout()); - - rpl::duplicate(goods) | rpl::start_with_next([=](bool good) { - details->setTextColorOverride( - good ? st::windowSubTextFg->c : st::boxTextFgError->c); - }, details->lifetime()); - - QObject::connect(field, &NumberInput::submitted, [=] { - const auto count = field->getLastText().toInt(); - if (count < minimal) { - field->showError(); - errors->fire({}); - return; - } - box->closeBox(); - UpdateGiftSellPrice(show, unique, savedId, count); - }); - const auto button = box->addButton(priceNow - ? tr::lng_gift_sell_update() - : tr::lng_gift_sell_put(), [=] { field->submitted({}); }); - rpl::combine( - box->widthValue(), - button->widthValue() - ) | rpl::start_with_next([=](int outer, int inner) { - const auto padding = st::giftBox.buttonPadding; - const auto wanted = outer - padding.left() - padding.right(); - if (inner != wanted) { - button->resizeToWidth(wanted); - button->moveToLeft(padding.left(), padding.top()); - } - }, box->lifetime()); - })); + const auto session = &show->session(); + const auto &title = unique->title; + InvokeWithUniqueGiftResellPrice(session, title, [=](int price) { + show->show(Box(UniqueGiftSellBox, show, unique, savedId, price, st)); + }); } void GiftReleasedByHandler(not_null peer) { diff --git a/Telegram/SourceFiles/boxes/star_gift_box.h b/Telegram/SourceFiles/boxes/star_gift_box.h index 78ee14a028..b3df5b5b87 100644 --- a/Telegram/SourceFiles/boxes/star_gift_box.h +++ b/Telegram/SourceFiles/boxes/star_gift_box.h @@ -21,6 +21,7 @@ class SavedStarGiftId; } // namespace Data namespace Main { +class Session; class SessionShow; } // namespace Main @@ -71,6 +72,8 @@ void ShowUniqueGiftWearBox( const Data::UniqueGift &gift, Settings::GiftWearBoxStyleOverride st); +void PreloadUniqueGiftResellPrices(not_null session); + void UpdateGiftSellPrice( std::shared_ptr show, std::shared_ptr unique, diff --git a/Telegram/SourceFiles/boxes/transfer_gift_box.cpp b/Telegram/SourceFiles/boxes/transfer_gift_box.cpp index 6566f68703..d94c9d01e7 100644 --- a/Telegram/SourceFiles/boxes/transfer_gift_box.cpp +++ b/Telegram/SourceFiles/boxes/transfer_gift_box.cpp @@ -425,8 +425,6 @@ void TransferGift( Data::SavedStarGiftId savedId, Fn done, bool skipPaymentForm = false) { - Expects(to->isUser()); - const auto session = &window->session(); const auto weak = base::make_weak(window); auto formDone = [=]( diff --git a/Telegram/SourceFiles/core/click_handler_types.h b/Telegram/SourceFiles/core/click_handler_types.h index 43295e1965..520b1282a1 100644 --- a/Telegram/SourceFiles/core/click_handler_types.h +++ b/Telegram/SourceFiles/core/click_handler_types.h @@ -17,6 +17,7 @@ constexpr auto kSendReactionEmojiProperty = 0x04; constexpr auto kReactionsCountEmojiProperty = 0x05; constexpr auto kDocumentFilenameTooltipProperty = 0x06; constexpr auto kPhoneNumberLinkProperty = 0x07; +constexpr auto kTodoListItemIdProperty = 0x08; namespace Ui { class Show; diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h index 424c724925..9c475823cd 100644 --- a/Telegram/SourceFiles/core/version.h +++ b/Telegram/SourceFiles/core/version.h @@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D666}"_cs; constexpr auto AppNameOld = "AyuGram for Windows"_cs; constexpr auto AppName = "AyuGram Desktop"_cs; constexpr auto AppFile = "AyuGram"_cs; -constexpr auto AppVersion = 5016003; -constexpr auto AppVersionStr = "5.16.3"; +constexpr auto AppVersion = 5016004; +constexpr auto AppVersionStr = "5.16.4"; constexpr auto AppBetaVersion = false; constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION; diff --git a/Telegram/SourceFiles/data/components/sponsored_messages.cpp b/Telegram/SourceFiles/data/components/sponsored_messages.cpp index a0d308f814..bab9af1daa 100644 --- a/Telegram/SourceFiles/data/components/sponsored_messages.cpp +++ b/Telegram/SourceFiles/data/components/sponsored_messages.cpp @@ -32,8 +32,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Data { namespace { +constexpr auto kMs = crl::time(1000); constexpr auto kRequestTimeLimit = 5 * 60 * crl::time(1000); +const auto kFlaggedPreload = ((MediaPreload*)quintptr(0x01)); + [[nodiscard]] bool TooEarlyForRequest(crl::time received) { return (received > 0) && (received + kRequestTimeLimit > crl::now()); } @@ -77,17 +80,21 @@ void SponsoredMessages::clear() { void SponsoredMessages::clearOldRequests() { const auto now = crl::now(); - while (true) { - const auto i = ranges::find_if(_requests, [&](const auto &value) { - const auto &request = value.second; - return !request.requestId - && (request.lastReceived + kRequestTimeLimit <= now); - }); - if (i == end(_requests)) { - break; + const auto clear = [&](auto &requests) { + while (true) { + const auto i = ranges::find_if(requests, [&](const auto &value) { + const auto &request = value.second; + return !request.requestId + && (request.lastReceived + kRequestTimeLimit <= now); + }); + if (i == end(requests)) { + break; + } + requests.erase(i); } - _requests.erase(i); - } + }; + clear(_requests); + clear(_requestsForVideo); } SponsoredMessages::AppendResult SponsoredMessages::append( @@ -241,6 +248,11 @@ bool SponsoredMessages::canHaveFor(not_null history) const { return false; } +bool SponsoredMessages::canHaveFor(not_null item) const { + return item->history()->peer->isBroadcast() + && item->isRegular(); +} + bool SponsoredMessages::isTopBarFor(not_null history) const { const auto &settings = AyuSettings::getInstance(); if (settings.disableAds) { @@ -291,6 +303,78 @@ void SponsoredMessages::request(not_null history, Fn done) { }).send(); } +void SponsoredMessages::requestForVideo( + not_null item, + Fn done) { + Expects(done != nullptr); + + if (!canHaveFor(item)) { + done({}); + return; + } + const auto peer = item->history()->peer; + auto &request = _requestsForVideo[peer]; + if (TooEarlyForRequest(request.lastReceived)) { + auto prepared = prepareForVideo(peer); + if (prepared.list.empty() + || prepared.state.itemIndex < prepared.list.size() + || prepared.state.leftTillShow > 0) { + done(std::move(prepared)); + return; + } + } + request.callbacks.push_back(std::move(done)); + if (request.requestId) { + return; + } + { + const auto it = _dataForVideo.find(peer); + if (it != end(_dataForVideo)) { + auto &list = it->second; + // Don't rebuild currently displayed messages. + const auto proj = [](const Entry &e) { + return e.item != nullptr; + }; + if (ranges::any_of(list.entries, proj)) { + return; + } + } + } + const auto finish = [=] { + const auto i = _requestsForVideo.find(peer); + if (i != end(_requestsForVideo)) { + for (const auto &callback : base::take(i->second.callbacks)) { + callback(prepareForVideo(peer)); + } + } + }; + using Flag = MTPmessages_GetSponsoredMessages::Flag; + request.requestId = _session->api().request( + MTPmessages_GetSponsoredMessages( + MTP_flags(Flag::f_msg_id), + peer->input, + MTP_int(item->id.bare)) + ).done([=](const MTPmessages_sponsoredMessages &result) { + parseForVideo(peer, result); + finish(); + }).fail([=] { + _requestsForVideo.remove(peer); + finish(); + }).send(); +} + +void SponsoredMessages::updateForVideo( + FullMsgId itemId, + SponsoredForVideoState state) { + if (state.initial()) { + return; + } + const auto i = _dataForVideo.find(_session->data().peer(itemId.peer)); + if (i != end(_dataForVideo)) { + i->second.state = state; + } +} + void SponsoredMessages::parse( not_null history, const MTPmessages_sponsoredMessages &list) { @@ -306,12 +390,9 @@ void SponsoredMessages::parse( _session->data().processChats(data.vchats()); const auto &messages = data.vmessages().v; - auto &list = _data.emplace(history, List()).first->second; + auto &list = _data.emplace(history).first->second; list.entries.clear(); list.received = crl::now(); - for (const auto &message : messages) { - append(history, list, message); - } if (const auto postsBetween = data.vposts_between()) { list.postsBetween = postsBetween->v; list.state = State::InjectToMiddle; @@ -320,10 +401,61 @@ void SponsoredMessages::parse( ? State::AppendToEnd : State::AppendToTopBar; } + for (const auto &message : messages) { + append([=] { + return &_data[history].entries; + }, history, message); + } }, [](const MTPDmessages_sponsoredMessagesEmpty &) { }); } +void SponsoredMessages::parseForVideo( + not_null peer, + const MTPmessages_sponsoredMessages &list) { + auto &request = _requestsForVideo[peer]; + request.lastReceived = crl::now(); + request.requestId = 0; + if (!_clearTimer.isActive()) { + _clearTimer.callOnce(kRequestTimeLimit * 2); + } + + list.match([&](const MTPDmessages_sponsoredMessages &data) { + _session->data().processUsers(data.vusers()); + _session->data().processChats(data.vchats()); + + const auto history = _session->data().history(peer); + const auto &messages = data.vmessages().v; + auto &list = _dataForVideo.emplace(peer).first->second; + list.entries.clear(); + list.received = crl::now(); + list.startDelay = data.vstart_delay().value_or_empty() * kMs; + list.betweenDelay = data.vbetween_delay().value_or_empty() * kMs; + for (const auto &message : messages) { + append([=] { + return &_dataForVideo[peer].entries; + }, history, message); + } + }, [](const MTPDmessages_sponsoredMessagesEmpty &) { + }); +} + +SponsoredForVideo SponsoredMessages::prepareForVideo( + not_null peer) { + const auto i = _dataForVideo.find(peer); + if (i == end(_dataForVideo) || i->second.entries.empty()) { + return {}; + } + return SponsoredForVideo{ + .list = i->second.entries | ranges::views::transform( + &Entry::sponsored + ) | ranges::to_vector, + .startDelay = i->second.startDelay, + .betweenDelay = i->second.betweenDelay, + .state = i->second.state, + }; +} + FullMsgId SponsoredMessages::fillTopBar( not_null history, not_null widget) { @@ -373,8 +505,8 @@ rpl::producer<> SponsoredMessages::itemRemoved(const FullMsgId &fullId) { } void SponsoredMessages::append( + Fn*>()> entries, not_null history, - List &list, const MTPSponsoredMessage &message) { const auto &data = message.data(); const auto randomId = data.vrandom_id().v; @@ -385,14 +517,14 @@ void SponsoredMessages::append( data.vmedia()->match([&](const MTPDmessageMediaPhoto &media) { if (const auto tlPhoto = media.vphoto()) { tlPhoto->match([&](const MTPDphoto &data) { - mediaPhoto = history->owner().processPhoto(data); + mediaPhoto = _session->data().processPhoto(data); }, [](const MTPDphotoEmpty &) { }); } }, [&](const MTPDmessageMediaDocument &media) { if (const auto tlDocument = media.vdocument()) { tlDocument->match([&](const MTPDdocument &data) { - const auto d = history->owner().processDocument( + const auto d = _session->data().processDocument( data, media.valt_documents()); if (d->isVideoFile() @@ -413,7 +545,7 @@ void SponsoredMessages::append( .link = qs(data.vurl()), .buttonText = qs(data.vbutton_text()), .photoId = data.vphoto() - ? history->session().data().processPhoto(*data.vphoto())->id + ? _session->data().processPhoto(*data.vphoto())->id : PhotoId(0), .mediaPhotoId = (mediaPhoto ? mediaPhoto->id : 0), .mediaDocumentId = (mediaDocument ? mediaDocument->id : 0), @@ -449,25 +581,24 @@ void SponsoredMessages::append( .link = from.link, .sponsorInfo = std::move(sponsorInfo), .additionalInfo = std::move(additionalInfo), + .durationMin = data.vmin_display_duration().value_or_empty() * kMs, + .durationMax = data.vmax_display_duration().value_or_empty() * kMs, }; - list.entries.push_back({ - .sponsored = std::move(sharedMessage), - }); - auto &entry = list.entries.back(); - const auto itemId = entry.itemFullId = FullMsgId( + const auto itemId = FullMsgId( history->peer->id, _session->data().nextLocalMessageId()); + const auto list = entries(); + list->push_back({ + .itemFullId = itemId, + .sponsored = std::move(sharedMessage), + }); + auto &entry = list->back(); const auto fileOrigin = FileOrigin(); // No way to refresh in ads. - static const auto kFlaggedPreload = ((MediaPreload*)quintptr(0x01)); const auto preloaded = [=] { - const auto i = _data.find(history); - if (i == end(_data)) { - return; - } - auto &entries = i->second.entries; - const auto j = ranges::find(entries, itemId, &Entry::itemFullId); - if (j == end(entries)) { + const auto list = entries(); + const auto j = ranges::find(*list, itemId, &Entry::itemFullId); + if (j == end(*list)) { return; } auto &entry = *j; @@ -565,7 +696,11 @@ SponsoredMessages::Details SponsoredMessages::lookupDetails( if (!entryPtr) { return {}; } - const auto &data = entryPtr->sponsored; + return lookupDetails(entryPtr->sponsored); +} + +SponsoredMessages::Details SponsoredMessages::lookupDetails( + const SponsoredMessage &data) const { return { .info = Prepare(data), .link = data.link, diff --git a/Telegram/SourceFiles/data/components/sponsored_messages.h b/Telegram/SourceFiles/data/components/sponsored_messages.h index 6c875c081c..b2a17ad0cb 100644 --- a/Telegram/SourceFiles/data/components/sponsored_messages.h +++ b/Telegram/SourceFiles/data/components/sponsored_messages.h @@ -67,10 +67,12 @@ struct SponsoredMessage { QByteArray randomId; SponsoredFrom from; TextWithEntities textWithEntities; - History *history = nullptr; + not_null history; QString link; TextWithEntities sponsorInfo; TextWithEntities additionalInfo; + crl::time durationMin = 0; + crl::time durationMax = 0; }; struct SponsoredMessageDetails { @@ -92,6 +94,23 @@ struct SponsoredReportAction { Fn)> callback; }; +struct SponsoredForVideoState { + int itemIndex = 0; + crl::time leftTillShow = 0; + + [[nodiscard]] bool initial() const { + return !itemIndex && !leftTillShow; + } +}; + +struct SponsoredForVideo { + std::vector list; + crl::time startDelay = 0; + crl::time betweenDelay = 0; + + SponsoredForVideoState state; +}; + class SponsoredMessages final { public: enum class AppendResult { @@ -111,10 +130,18 @@ public: ~SponsoredMessages(); [[nodiscard]] bool canHaveFor(not_null history) const; + [[nodiscard]] bool canHaveFor(not_null item) const; [[nodiscard]] bool isTopBarFor(not_null history) const; void request(not_null history, Fn done); + void requestForVideo( + not_null item, + Fn done); + void updateForVideo( + FullMsgId itemId, + SponsoredForVideoState state); void clearItems(not_null history); [[nodiscard]] Details lookupDetails(const FullMsgId &fullId) const; + [[nodiscard]] Details lookupDetails(const SponsoredMessage &data) const; [[nodiscard]] Details lookupDetails( const Api::SponsoredSearchResult &data) const; void clicked(const FullMsgId &fullId, bool isMedia, bool isFullscreen); @@ -166,18 +193,35 @@ private: int postsBetween = 0; State state = State::None; }; + struct ListForVideo { + std::vector entries; + crl::time received = 0; + crl::time startDelay = 0; + crl::time betweenDelay = 0; + SponsoredForVideoState state; + }; struct Request { mtpRequestId requestId = 0; crl::time lastReceived = 0; }; + struct RequestForVideo { + std::vector> callbacks; + mtpRequestId requestId = 0; + crl::time lastReceived = 0; + }; void parse( not_null history, const MTPmessages_sponsoredMessages &list); + void parseForVideo( + not_null peer, + const MTPmessages_sponsoredMessages &list); void append( + Fn*>()> entries, not_null history, - List &list, const MTPSponsoredMessage &message); + [[nodiscard]] SponsoredForVideo prepareForVideo( + not_null peer); void clearOldRequests(); const Entry *find(const FullMsgId &fullId) const; @@ -189,6 +233,9 @@ private: base::flat_map, Request> _requests; base::flat_map _viewRequests; + base::flat_map, ListForVideo> _dataForVideo; + base::flat_map, RequestForVideo> _requestsForVideo; + rpl::event_stream _itemRemoved; rpl::lifetime _lifetime; diff --git a/Telegram/SourceFiles/data/data_chat_participant_status.cpp b/Telegram/SourceFiles/data/data_chat_participant_status.cpp index e18a18e7d3..964082f2cc 100644 --- a/Telegram/SourceFiles/data/data_chat_participant_status.cpp +++ b/Telegram/SourceFiles/data/data_chat_participant_status.cpp @@ -72,7 +72,7 @@ namespace { | (data.is_send_audios() ? Flag::SendMusic : Flag()) | (data.is_send_voices() ? Flag::SendVoiceMessages : Flag()) | (data.is_send_docs() ? Flag::SendFiles : Flag()) - | (data.is_send_messages() ? Flag::SendOther : Flag()) + | (data.is_send_plain() ? Flag::SendOther : Flag()) | (data.is_embed_links() ? Flag::EmbedLinks : Flag()) | (data.is_change_info() ? Flag::ChangeInfo : Flag()) | (data.is_invite_users() ? Flag::AddParticipants : Flag()) @@ -142,7 +142,7 @@ MTPChatBannedRights RestrictionsToMTP(ChatRestrictionsInfo info) { | ((flags & R::SendMusic) ? Flag::f_send_audios : Flag()) | ((flags & R::SendVoiceMessages) ? Flag::f_send_voices : Flag()) | ((flags & R::SendFiles) ? Flag::f_send_docs : Flag()) - | ((flags & R::SendOther) ? Flag::f_send_messages : Flag()) + | ((flags & R::SendOther) ? Flag::f_send_plain : Flag()) | ((flags & R::EmbedLinks) ? Flag::f_embed_links : Flag()) | ((flags & R::ChangeInfo) ? Flag::f_change_info : Flag()) | ((flags & R::AddParticipants) ? Flag::f_invite_users : Flag()) diff --git a/Telegram/SourceFiles/data/data_histories.cpp b/Telegram/SourceFiles/data/data_histories.cpp index eb85f931b1..2ebbc0dfdf 100644 --- a/Telegram/SourceFiles/data/data_histories.cpp +++ b/Telegram/SourceFiles/data/data_histories.cpp @@ -92,7 +92,8 @@ MTPInputReplyTo ReplyToForMTP( : Flag()) | (quoteEntities.v.isEmpty() ? Flag() - : Flag::f_quote_entities)), + : Flag::f_quote_entities) + | (replyTo.todoItemId ? Flag::f_todo_item_id : Flag())), MTP_int(replyTo.messageId ? replyTo.messageId.msg : 0), MTP_int(replyTo.topicRootId), (external @@ -103,7 +104,8 @@ MTPInputReplyTo ReplyToForMTP( MTP_int(replyTo.quoteOffset), (replyToMonoforumPeerId ? history->owner().peer(replyToMonoforumPeerId)->input - : MTPInputPeer())); + : MTPInputPeer()), + MTP_int(replyTo.todoItemId)); } else if (history->peer->amMonoforumAdmin() && replyTo.monoforumPeerId) { const auto replyToMonoforumPeer = replyTo.monoforumPeerId diff --git a/Telegram/SourceFiles/data/data_msg_id.h b/Telegram/SourceFiles/data/data_msg_id.h index bee8324194..3627fd4d0c 100644 --- a/Telegram/SourceFiles/data/data_msg_id.h +++ b/Telegram/SourceFiles/data/data_msg_id.h @@ -172,6 +172,19 @@ inline QDebug operator<<(QDebug debug, const FullMsgId &fullMsgId) { Q_DECLARE_METATYPE(FullMsgId); +struct MessageHighlightId { + TextWithEntities quote; + int quoteOffset = 0; + int todoItemId = 0; + + [[nodiscard]] bool empty() const { + return quote.empty() && !todoItemId; + } + [[nodiscard]] friend inline bool operator==( + const MessageHighlightId &a, + const MessageHighlightId &b) = default; +}; + struct FullReplyTo { FullMsgId messageId; TextWithEntities quote; @@ -179,7 +192,11 @@ struct FullReplyTo { MsgId topicRootId = 0; PeerId monoforumPeerId = 0; int quoteOffset = 0; + int todoItemId = 0; + [[nodiscard]] MessageHighlightId highlight() const { + return { quote, quoteOffset, todoItemId }; + } [[nodiscard]] bool replying() const { return messageId || (storyId && storyId.peer); } diff --git a/Telegram/SourceFiles/data/data_saved_messages.cpp b/Telegram/SourceFiles/data/data_saved_messages.cpp index 1cae8972db..a7ee009257 100644 --- a/Telegram/SourceFiles/data/data_saved_messages.cpp +++ b/Telegram/SourceFiles/data/data_saved_messages.cpp @@ -96,17 +96,6 @@ Thread *SavedMessages::activeSubsectionThread() const { return _activeSubsectionSublist; } -Dialogs::UnreadState SavedMessages::unreadStateWithParentMuted() const { - auto result = _chatsList.unreadState(); - if (_owningHistory->muted()) { - result.chatsMuted = result.chats; - result.marksMuted = result.marks; - result.messagesMuted = result.messages; - result.reactionsMuted = result.reactions; - } - return result; -} - SavedMessages::~SavedMessages() { clear(); } @@ -458,6 +447,9 @@ void SavedMessages::applySublistDeleted(not_null sublistPeer) { if (ranges::contains(_lastSublists, not_null(raw))) { reorderLastSublists(); } + if (_activeSubsectionSublist == raw) { + _activeSubsectionSublist = nullptr; + } _sublistDestroyed.fire(raw); session().changes().sublistUpdated( diff --git a/Telegram/SourceFiles/data/data_saved_messages.h b/Telegram/SourceFiles/data/data_saved_messages.h index 65f0345c7b..fe77fbb232 100644 --- a/Telegram/SourceFiles/data/data_saved_messages.h +++ b/Telegram/SourceFiles/data/data_saved_messages.h @@ -84,8 +84,6 @@ public: void saveActiveSubsectionThread(not_null thread); Thread *activeSubsectionThread() const; - [[nodiscard]] Dialogs::UnreadState unreadStateWithParentMuted() const; - [[nodiscard]] rpl::lifetime &lifetime(); private: diff --git a/Telegram/SourceFiles/data/data_saved_sublist.cpp b/Telegram/SourceFiles/data/data_saved_sublist.cpp index 64ef0886df..7eab828dd4 100644 --- a/Telegram/SourceFiles/data/data_saved_sublist.cpp +++ b/Telegram/SourceFiles/data/data_saved_sublist.cpp @@ -217,7 +217,28 @@ void SavedSublist::applyItemRemoved(MsgId id) { if (const auto chatListItem = _chatListMessage.value_or(nullptr)) { if (chatListItem->id == id) { _chatListMessage = std::nullopt; - requestChatListMessage(); + crl::on_main(this, [=] { + // We didn't yet update _list here. + if (_chatListMessage.has_value()) { + return; + } else if (_skippedAfter == 0) { + if (!_list.empty()) { + applyMaybeLast(owner().message( + owningHistory()->peer, + _list.front())); + return; + } else if (_skippedBefore == 0) { + setLastServerMessage(nullptr); + updateChatListExistence(); + return; + } + } + if (_parent->parentChat()) { + requestChatListMessage(); + } else { + loadAround(0); + } + }); } } } @@ -1110,6 +1131,10 @@ void SavedSublist::loadAround(MsgId id) { _list.clear(); if (processMessagesIsEmpty(result)) { _fullCount = _skippedBefore = _skippedAfter = 0; + if (!_parent->parentChat() && !_chatListMessage) { + setLastServerMessage(nullptr); + updateChatListExistence(); + } } else if (id) { Assert(!_list.empty()); if (_list.front() <= id) { @@ -1117,6 +1142,11 @@ void SavedSublist::loadAround(MsgId id) { } else if (_list.back() >= id) { _skippedBefore = 0; } + } else if (!_parent->parentChat() && !_chatListMessage) { + Assert(!_list.empty()); + applyMaybeLast(owner().message( + owningHistory()->peer, + _list.front())); } checkReadTillEnd(); }).fail([=](const MTP::Error &error) { diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index ef8d55c959..f72efbc1ee 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/unixtime.h" #include "apiwrap.h" #include "core/application.h" +#include "data/components/top_peers.h" #include "data/data_changes.h" #include "data/data_channel.h" #include "data/data_document.h" @@ -425,10 +426,7 @@ void Stories::parseAndApply(const MTPPeerStories &stories) { }; if (result.peer->isSelf() || (result.peer->isChannel() && result.peer->asChannel()->amIn()) - || (result.peer->isUser() - && (result.peer->asUser()->isBot() - || result.peer->asUser()->isContact())) - || result.peer->isServiceUser()) { + || result.peer->isUser()) { const auto hidden = result.peer->hasStoriesHidden(); using List = StorySourcesList; add(hidden ? List::Hidden : List::NotHidden); @@ -1197,7 +1195,11 @@ void Stories::toggleHidden( bool hidden, std::shared_ptr show) { const auto peer = _owner->peer(peerId); - const auto justRemove = peer->isServiceUser() && hidden; + const auto byHints = peer->isUser() + && !peer->asUser()->isBot() + && !peer->asUser()->isContact() + && !peer->asUser()->isServiceUser(); + const auto justRemove = (byHints || peer->isServiceUser()) && hidden; if (peer->hasStoriesHidden() != hidden) { if (!justRemove) { peer->setStoriesHidden(hidden); @@ -1206,6 +1208,9 @@ void Stories::toggleHidden( peer->input, MTP_bool(hidden) )).send(); + if (byHints) { + peer->session().topPeers().remove(peer); + } } const auto name = peer->shortName(); diff --git a/Telegram/SourceFiles/dialogs/dialogs.style b/Telegram/SourceFiles/dialogs/dialogs.style index 6780fefa54..9436d4e7c9 100644 --- a/Telegram/SourceFiles/dialogs/dialogs.style +++ b/Telegram/SourceFiles/dialogs/dialogs.style @@ -244,6 +244,17 @@ dialogsEmptyLabel: FlatLabel(defaultFlatLabel) { align: align(top); textFg: windowSubTextFg; } +dialogEmptyButton: RoundButton(defaultActiveButton) { +} +dialogEmptyButtonSkip: 12px; +dialogEmptyButtonLabel: FlatLabel(defaultFlatLabel) { + style: TextStyle(defaultTextStyle) { + font: font(boxFontSize semibold); + } + minWidth: 32px; + align: align(top); + textFg: windowFg; +} dialogsMenuToggle: IconButton { width: 40px; diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index db2dcd57aa..19894fd4c0 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -31,6 +31,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/text/text_utilities.h" #include "ui/text/text_options.h" #include "ui/dynamic_thumbnails.h" +#include "ui/vertical_list.h" #include "ui/painter.h" #include "ui/rect.h" #include "ui/ui_utility.h" @@ -58,8 +59,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/options.h" #include "lang/lang_keys.h" #include "lottie/lottie_icon.h" -#include "mainwindow.h" -#include "mainwidget.h" +#include "settings/settings_common.h" #include "storage/storage_account.h" #include "apiwrap.h" #include "main/main_session.h" @@ -80,6 +80,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_chat_filters.h" #include "base/qt/qt_common_adapters.h" #include "styles/style_dialogs.h" +#include "styles/style_boxes.h" #include "styles/style_chat.h" // popupMenuExpandedSeparator #include "styles/style_chat_helpers.h" #include "styles/style_color_indices.h" @@ -3081,6 +3082,11 @@ void InnerWidget::clearSelection() { } void InnerWidget::fillSupportSearchMenu(not_null menu) { + const auto globalSearch = (_searchState.tab == ChatSearchTab::MyMessages) + || (_searchState.tab == ChatSearchTab::PublicPosts); + if (!globalSearch && _searchState.inChat) { + return; + } const auto all = session().settings().supportAllSearchResults(); const auto text = all ? "Only one from chat" : "Show all messages"; menu->addAction(text, [=] { @@ -3091,9 +3097,11 @@ void InnerWidget::fillSupportSearchMenu(not_null menu) { void InnerWidget::fillArchiveSearchMenu(not_null menu) { const auto folder = session().data().folderLoaded(Data::Folder::kId); + const auto globalSearch = (_searchState.tab == ChatSearchTab::MyMessages) + || (_searchState.tab == ChatSearchTab::PublicPosts); if (!folder || !folder->chatsList()->fullSize().current() - || _searchState.inChat) { + || (!globalSearch && _searchState.inChat)) { return; } const auto skip = session().settings().skipArchiveInSearch(); @@ -3263,16 +3271,13 @@ void InnerWidget::showSponsoredMenu(int peerSearchIndex, QPoint globalPos) { refresh(); }); Menu::FillSponsored( - this, Ui::Menu::CreateAddActionCallback(_menu), _controller->uiShow(), Menu::SponsoredPhrases::Search, session().sponsoredMessages().lookupDetails(entry->sponsored->data), session().sponsoredMessages().createReportCallback( entry->sponsored->data.randomId, - remove), - false, - false); + remove)); QObject::connect(_menu.get(), &QObject::destroyed, [=] { if (_peerSearchMenu >= 0 && _peerSearchMenu < _peerSearchResults.size()) { @@ -3811,7 +3816,7 @@ void InnerWidget::itemRemoved(not_null item) { } bool InnerWidget::uniqueSearchResults() const { - return _controller->uniqueChatsInSearchResults(); + return _controller->uniqueChatsInSearchResults(_searchState); } bool InnerWidget::hasHistoryInResults(not_null history) const { @@ -3869,7 +3874,8 @@ void InnerWidget::searchReceived( ? _searchState.inChat : Key(_openedForum->history()); if (inject - && (!_searchState.inChat + && (globalSearch + || !_searchState.inChat || inject->history() == _searchState.inChat.history())) { Assert(_searchResults.empty()); Assert(!toPreview); @@ -4082,9 +4088,18 @@ void InnerWidget::refreshEmpty() { if (state == EmptyState::None) { _emptyState = state; _empty.destroy(); + _emptyList.destroy(); + _emptyButton.destroy(); return; } else if (_emptyState == state) { _empty->setVisible(_state == WidgetState::Default); + if (_emptyList) { + _emptyList->setVisible(_state == WidgetState::Default); + _empty->setVisible(!_emptyList->isVisible()); + } + if (_emptyButton) { + _emptyButton->setVisible(_state == WidgetState::Default); + } return; } _emptyState = state; @@ -4115,7 +4130,6 @@ void InnerWidget::refreshEmpty() { return result; }); _empty.create(this, std::move(full), st::dialogsEmptyLabel); - resizeEmpty(); _empty->overrideLinkClickHandler([=] { if (_emptyState == EmptyState::NoContacts) { _controller->showAddContact(); @@ -4127,6 +4141,58 @@ void InnerWidget::refreshEmpty() { } }); _empty->setVisible(_state == WidgetState::Default); + + if (state == EmptyState::NoContacts) { + const auto isListVisible = _state == WidgetState::Default; + _emptyList.create(this); + _emptyList->setVisible(isListVisible); + + auto icon = ::Settings::CreateLottieIcon( + _emptyList, + { + .name = u"no_chats"_q, + .sizeOverride = Size(st::changePhoneIconSize), + }); + _emptyList->add( + object_ptr>(_emptyList, std::move(icon.widget))); + Ui::AddSkip(_emptyList); + _emptyList->add( + object_ptr( + _emptyList, + tr::lng_no_conversations(), + st::dialogEmptyButtonLabel)); + if (_state == WidgetState::Default) { + icon.animate(anim::repeat::once); + } + _emptyButton.create( + this, + tr::lng_no_conversations_button(), + st::dialogEmptyButton); + _emptyButton->setTextTransform( + Ui::RoundButton::TextTransform::NoTransform); + _emptyButton->setVisible(isListVisible); + _emptyButton->setClickedCallback([=, window = _controller] { + window->show(PrepareContactsBox(window)); + }); + geometryValue() | rpl::start_with_next([=](const QRect &r) { + const auto top = r.height() + - _emptyButton->height() + - st::dialogEmptyButtonSkip; + _emptyButton->moveToLeft(st::dialogEmptyButtonSkip, top); + }, _emptyButton->lifetime()); + geometryValue() | rpl::start_with_next([=](const QRect &r) { + const auto bottom = _emptyButton + ? (_emptyButton->height() + st::dialogEmptyButtonSkip) + : 0; + _emptyList->moveToLeft( + 0, + ((r.height() - bottom) - _emptyList->height()) / 2); + }, _emptyList->lifetime()); + + _empty->setVisible(!_emptyList->isVisible()); + } + + resizeEmpty(); } void InnerWidget::resizeEmpty() { @@ -4135,6 +4201,13 @@ void InnerWidget::resizeEmpty() { _empty->resizeToWidth(width() - 2 * skip); _empty->move(skip, (st::dialogsEmptyHeight - _empty->height()) / 2); } + if (_emptyList) { + _emptyList->resizeToWidth(width()); + } + if (_emptyButton) { + const auto skip = st::dialogEmptyButtonSkip; + _emptyButton->resizeToWidth(width() - 2 * skip); + } if (_searchEmpty) { _searchEmpty->resizeToWidth(width()); _searchEmpty->move(0, searchedOffset()); diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h index c3c4a6b033..e54955ee2e 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h @@ -43,6 +43,8 @@ namespace Ui { class IconButton; class PopupMenu; class FlatLabel; +class VerticalLayout; +class RoundButton; struct ScrollToRequest; namespace Controls { enum class QuickDialogAction; @@ -619,6 +621,8 @@ private: object_ptr _searchEmpty = { nullptr }; SearchState _searchEmptyState; object_ptr _empty = { nullptr }; + object_ptr _emptyList = { nullptr }; + object_ptr _emptyButton = { nullptr }; Ui::DraggingScrollManager _draggingScroll; diff --git a/Telegram/SourceFiles/dialogs/dialogs_main_list.cpp b/Telegram/SourceFiles/dialogs/dialogs_main_list.cpp index 5440f38843..605edf3824 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_main_list.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_main_list.cpp @@ -120,6 +120,10 @@ void MainList::unreadStateChanged( const auto notify = !useClouded || wasState.known; const auto notifier = unreadStateChangeNotifier(notify); _unreadState += nowState - wasState; + if (_unreadState.chatsMuted > _unreadState.chats + || _unreadState.messagesMuted > _unreadState.messages) { + [[maybe_unused]] int a = 0; + } if (updateCloudUnread) { // Assert(nowState.known); _cloudUnreadState += nowState - wasState; @@ -145,6 +149,10 @@ void MainList::unreadEntryChanged( } else { _unreadState -= state; } + if (_unreadState.chatsMuted > _unreadState.chats + || _unreadState.messagesMuted > _unreadState.messages) { + [[maybe_unused]] int a = 0; + } if (updateCloudUnread) { if (added) { _cloudUnreadState += state; diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index 0c77f7eedd..757da6b31e 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -903,10 +903,7 @@ void Widget::chosenRow(const ChosenRow &row) { } else if (const auto topic = row.key.topic()) { auto params = Window::SectionShow( Window::SectionShow::Way::ClearStack); - params.highlightPart.text = _searchState.query; - if (!params.highlightPart.empty()) { - params.highlightPartOffsetHint = kSearchQueryOffsetHint; - } + params.highlight = Window::SearchHighlightId(_searchState.query); if (row.newWindow) { controller()->showInNewWindow( Window::SeparateId(topic), @@ -973,15 +970,12 @@ void Widget::chosenRow(const ChosenRow &row) { return; } else if (history) { const auto peer = history->peer; - const auto showAtMsgId = controller()->uniqueChatsInSearchResults() - ? ShowAtUnreadMsgId - : row.message.fullId.msg; + const auto showAtMsgId = controller()->uniqueChatsInSearchResults( + _searchState + ) ? ShowAtUnreadMsgId : row.message.fullId.msg; auto params = Window::SectionShow( Window::SectionShow::Way::ClearStack); - params.highlightPart.text = _searchState.query; - if (!params.highlightPart.empty()) { - params.highlightPartOffsetHint = kSearchQueryOffsetHint; - } + params.highlight = Window::SearchHighlightId(_searchState.query); if (row.newWindow) { controller()->showInNewWindow(peer, showAtMsgId); } else { diff --git a/Telegram/SourceFiles/export/data/export_data_types.cpp b/Telegram/SourceFiles/export/data/export_data_types.cpp index e4ef4a0a6f..68e8d37cbe 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.cpp +++ b/Telegram/SourceFiles/export/data/export_data_types.cpp @@ -1167,8 +1167,15 @@ Chat ParseChat(const MTPChat &data) { result.colorIndex = (color && color->data().vcolor()) ? color->data().vcolor()->v : PeerColorIndex(result.bareId); + result.isMonoforum = data.is_monoforum(); result.isBroadcast = data.is_broadcast(); result.isSupergroup = data.is_megagroup(); + result.hasMonoforumAdminRights = data.is_broadcast() + && (data.is_creator() + || (data.vadmin_rights() + && data.vadmin_rights()->data().is_manage_direct_messages())); + result.monoforumLinkId + = data.vlinked_monoforum_id().value_or_empty(); result.title = ParseString(data.vtitle()); if (const auto username = data.vusername()) { result.username = ParseString(*username); @@ -1188,15 +1195,6 @@ Chat ParseChat(const MTPChat &data) { return result; } -std::map ParseChatsList(const MTPVector &data) { - auto result = std::map(); - for (const auto &chat : data.v) { - auto parsed = ParseChat(chat); - result.emplace(parsed.id(), std::move(parsed)); - } - return result; -} - Utf8String ContactInfo::name() const { return firstName.isEmpty() ? (lastName.isEmpty() @@ -1273,6 +1271,20 @@ std::map ParsePeersLists( auto parsed = ParseChat(chat); result.emplace(parsed.id(), Peer{ std::move(parsed) }); } + for (auto &[peerId, parsed] : result) { + if (const auto chat = std::get_if(&parsed.data)) { + if (chat->isMonoforum) { + const auto i = result.find( + PeerId(ChannelId(chat->monoforumLinkId))); + if (i != end(result)) { + chat->isMonoforumAdmin + = i->second.chat()->hasMonoforumAdminRights; + chat->isMonoforumOfPublicBroadcast + = !i->second.chat()->username.isEmpty(); + } + } + } + } return result; } @@ -2191,7 +2203,13 @@ const DialogInfo *DialogsInfo::item(int index) const { DialogInfo::Type DialogTypeFromChat(const Chat &chat) { using Type = DialogInfo::Type; - return chat.username.isEmpty() + return (chat.isMonoforum && !chat.isMonoforumAdmin) + ? Type::Personal + : (chat.isMonoforumAdmin && chat.isMonoforumOfPublicBroadcast) + ? Type::PublicSupergroup + : chat.isMonoforumAdmin + ? Type::PrivateSupergroup + : chat.username.isEmpty() ? (chat.isBroadcast ? Type::PrivateChannel : chat.isSupergroup @@ -2252,6 +2270,11 @@ DialogsInfo ParseDialogsInfo(const MTPmessages_Dialogs &data) { info.migratedToChannelId = peer.chat() ? peer.chat()->migratedToChannelId : 0; + info.isMonoforum = peer.chat() + && peer.chat()->isMonoforum; + info.monoforumBroadcastInput = peer.chat() + ? peer.chat()->monoforumBroadcastInput + : MTPInputPeer(MTP_inputPeerEmpty()); } info.topMessageId = fields.vtop_message().v; const auto messageIt = messages.find(MessageId{ @@ -2290,6 +2313,10 @@ DialogInfo DialogInfoFromChat(const Chat &data) { result.topMessageId = 0; result.type = DialogTypeFromChat(data); result.migratedToChannelId = data.migratedToChannelId; + result.isMonoforum = data.isMonoforum; + if (data.isMonoforumAdmin) { + result.monoforumBroadcastInput = data.monoforumBroadcastInput; + } return result; } @@ -2424,7 +2451,8 @@ void FinalizeDialogsInfo(DialogsInfo &info, const Settings &settings) { } Unexpected("Type in ApiWrap::onlyMyMessages."); }(); - dialog.onlyMyMessages = ((settings.fullChats & setting) != setting); + dialog.onlyMyMessages = (dialog.type != DialogType::Personal) + && ((settings.fullChats & setting) != setting); ranges::sort(dialog.splits); } diff --git a/Telegram/SourceFiles/export/data/export_data_types.h b/Telegram/SourceFiles/export/data/export_data_types.h index 2c650871a3..86e376fdd8 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.h +++ b/Telegram/SourceFiles/export/data/export_data_types.h @@ -319,14 +319,19 @@ struct Chat { Utf8String title; Utf8String username; uint8 colorIndex = 0; + bool isMonoforum = false; bool isBroadcast = false; bool isSupergroup = false; + bool isMonoforumAdmin = false; + bool hasMonoforumAdminRights = false; + bool isMonoforumOfPublicBroadcast = false; + BareId monoforumLinkId = 0; MTPInputPeer input = MTP_inputPeerEmpty(); + MTPInputPeer monoforumBroadcastInput = MTP_inputPeerEmpty(); }; Chat ParseChat(const MTPChat &data); -std::map ParseChatsList(const MTPVector &data); struct Peer { PeerId id() const; @@ -952,12 +957,15 @@ struct DialogInfo { MTPInputPeer migratedFromInput = MTP_inputPeerEmpty(); ChannelId migratedToChannelId = 0; + MTPInputPeer monoforumBroadcastInput = MTP_inputPeerEmpty(); + // User messages splits which contained that dialog. std::vector splits; // Filled after the whole dialogs list is accumulated. bool onlyMyMessages = false; bool isLeftChannel = false; + bool isMonoforum = false; QString relativePath; // Filled when requesting dialog messages. diff --git a/Telegram/SourceFiles/export/export_api_wrap.cpp b/Telegram/SourceFiles/export/export_api_wrap.cpp index 4fc78bad52..b25c130b7e 100644 --- a/Telegram/SourceFiles/export/export_api_wrap.cpp +++ b/Telegram/SourceFiles/export/export_api_wrap.cpp @@ -1370,7 +1370,7 @@ void ApiWrap::appendSinglePeerDialogs(Data::DialogsInfo &&info) { if (isSupergroupType(info.type) && !migratedRequestId) { migratedRequestId = requestSinglePeerMigrated(info); continue; - } else if (isChannelType(info.type)) { + } else if (isChannelType(info.type) || info.isMonoforum) { continue; } for (auto i = last; i != 0; --i) { @@ -1642,6 +1642,9 @@ void ApiWrap::requestChatMessages( const auto realPeerInput = (splitIndex >= 0) ? _chatProcess->info.input : _chatProcess->info.migratedFromInput; + const auto outgoingInput = _chatProcess->info.isMonoforum + ? _chatProcess->info.monoforumBroadcastInput + : MTP_inputPeerSelf(); const auto realSplitIndex = (splitIndex >= 0) ? splitIndex : (splitsCount + splitIndex); @@ -1650,7 +1653,7 @@ void ApiWrap::requestChatMessages( MTP_flags(MTPmessages_Search::Flag::f_from_id), realPeerInput, MTP_string(), // query - MTP_inputPeerSelf(), + outgoingInput, MTPInputPeer(), // saved_peer_id MTPVector(), // saved_reaction MTPint(), // top_msg_id diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp index 0f656950b4..618af25507 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp @@ -111,7 +111,8 @@ std::optional PrepareLogReply( MTP_int(topId), MTPstring(), // quote_text MTPVector(), // quote_entities - MTPint()); // quote_offset + MTPint(), // quote_offset + MTPint()); // todo_item_id } } return {}; diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index 98740671f4..bcb39ac3c8 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -2380,7 +2380,7 @@ Dialogs::UnreadState History::chatListUnreadState() const { return AdjustedForumUnreadState(forum->topicsList()->unreadState()); } else if (const auto monoforum = peer->monoforum()) { return AdjustedForumUnreadState( - monoforum->unreadStateWithParentMuted()); + withMyMuted(monoforum->chatsList()->unreadState()));; } return computeUnreadState(); } @@ -2395,7 +2395,7 @@ Dialogs::BadgesState History::chatListBadgesState() const { } else if (const auto monoforum = peer->monoforum()) { return adjustBadgesStateByFolder( Dialogs::BadgesForUnread( - monoforum->unreadStateWithParentMuted(), + withMyMuted(monoforum->chatsList()->unreadState()), Dialogs::CountInBadge::Chats, Dialogs::IncludeInBadge::All)); } @@ -2440,6 +2440,16 @@ Dialogs::UnreadState History::computeUnreadState() const { return result; } +Dialogs::UnreadState History::withMyMuted(Dialogs::UnreadState state) const { + if (muted()) { + state.chatsMuted = state.chats; + state.marksMuted = state.marks; + state.messagesMuted = state.messages; + state.reactionsMuted = state.reactions; + } + return state; +} + void History::allowChatListMessageResolve() { if (_flags & Flag::ResolveChatListMessage) { return; @@ -3368,7 +3378,8 @@ bool History::isForum() const { void History::monoforumChanged(Data::SavedMessages *old) { if (inChatList()) { notifyUnreadStateChange(old - ? AdjustedForumUnreadState(old->chatsList()->unreadState()) + ? AdjustedForumUnreadState( + withMyMuted(old->chatsList()->unreadState())) : computeUnreadState()); } @@ -3378,9 +3389,9 @@ void History::monoforumChanged(Data::SavedMessages *old) { monoforum->chatsList()->unreadStateChanges( ) | rpl::filter([=] { return (_flags & Flag::IsMonoforumAdmin) && inChatList(); - }) | rpl::map( - AdjustedForumUnreadState - ) | rpl::start_with_next([=](const Dialogs::UnreadState &old) { + }) | rpl::map([=](const Dialogs::UnreadState &was) { + return AdjustedForumUnreadState(withMyMuted(was)); + }) | rpl::start_with_next([=](const Dialogs::UnreadState &old) { notifyUnreadStateChange(old); }, monoforum->lifetime()); diff --git a/Telegram/SourceFiles/history/history.h b/Telegram/SourceFiles/history/history.h index a36d2000c6..7f556bbc71 100644 --- a/Telegram/SourceFiles/history/history.h +++ b/Telegram/SourceFiles/history/history.h @@ -602,6 +602,8 @@ private: [[nodiscard]] Dialogs::BadgesState adjustBadgesStateByFolder( Dialogs::BadgesState state) const; [[nodiscard]] Dialogs::UnreadState computeUnreadState() const; + [[nodiscard]] Dialogs::UnreadState withMyMuted( + Dialogs::UnreadState state) const; void setFolderPointer(Data::Folder *folder); void hasUnreadMentionChanged(bool has) override; diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index afadf42c55..13391112e6 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -674,10 +674,10 @@ void HistoryInner::setupSwipeReplyAndBack() { : still)->fullId(); _widget->replyToMessage({ .messageId = replyToItemId, - .quote = selected.text, - .quoteOffset = selected.offset, + .quote = selected.highlight.quote, + .quoteOffset = selected.highlight.quoteOffset, }); - if (!selected.text.empty()) { + if (!selected.highlight.quote.empty()) { _widget->clearSelected(); } }; @@ -2409,6 +2409,9 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { const auto linkUserpicPeerId = (link && _dragStateUserpic) ? link->property(kPeerLinkPeerIdProperty).toULongLong() : 0; + const auto todoListTaskId = link + ? link->property(kTodoListItemIdProperty).toInt() + : 0; const auto session = &this->session(); _whoReactedMenuLifetime.destroy(); if (!clickedReaction.empty() @@ -2777,20 +2780,21 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { const auto selected = selectedQuote(item); auto text = (selected ? tr::lng_context_quote_and_reply + : todoListTaskId + ? tr::lng_context_reply_to_task : tr::lng_context_reply_msg)( tr::now, Ui::Text::FixAmpersandInAction); const auto replyToItem = selected.item ? selected.item : item; const auto itemId = replyToItem->fullId(); - const auto quote = selected.text; - const auto quoteOffset = selected.offset; _menu->addAction(std::move(text), [=] { _widget->replyToMessage({ .messageId = itemId, - .quote = quote, - .quoteOffset = quoteOffset, + .quote = selected.highlight.quote, + .quoteOffset = selected.highlight.quoteOffset, + .todoItemId = todoListTaskId, }); - if (!quote.empty()) { + if (!selected.highlight.quote.empty()) { _widget->clearSelected(); } }, &st::menuIconReply); @@ -2809,7 +2813,7 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { Window::PeerMenuAddTodoListTasks(_controller, item); } }), - &st::menuIconCreateTodoList); + &st::menuIconAdd); }; const auto lnkPhoto = link ? reinterpret_cast( @@ -2955,11 +2959,9 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { : nullptr; if (sponsored) { Menu::FillSponsored( - this, Ui::Menu::CreateAddActionCallback(_menu), controller->uiShow(), - sponsored->fullId(), - false); + sponsored->fullId()); } if (isUponSelected > 0) { addReplyAction(item); diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 6d96224f21..4bc8eb5b6a 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -964,12 +964,26 @@ void HistoryItem::updateServiceDependent(bool force) { } if (!dependent->lnk) { + auto todoItemId = 0; + if (const auto done = Get()) { + const auto &items = !done->completed.empty() + ? done->completed + : done->incompleted; + if (items.size() == 1) { + todoItemId = items.front(); + } + } else if (const auto append = Get()) { + if (append->list.size() == 1) { + todoItemId = append->list.front().id; + } + } dependent->lnk = JumpToMessageClickHandler( (dependent->peerId ? _history->owner().peer(dependent->peerId) : _history->peer), dependent->msgId, - fullId()); + fullId(), + { .todoItemId = todoItemId }); } auto gotDependencyItem = false; if (!dependent->msg) { @@ -1858,7 +1872,10 @@ bool HistoryItem::isAyuNoForwards() const { } bool HistoryItem::canLookupMessageAuthor() const { - return isRegular() && _history->amMonoforumAdmin() && _from->isChannel(); + return isRegular() + && !isService() + && _history->amMonoforumAdmin() + && _from->isChannel(); } bool HistoryItem::skipNotification() const { @@ -4392,6 +4409,7 @@ void HistoryItem::createComponentsHelper(HistoryItemCommonFields &&fields) { : replyTo.monoforumPeerId ? replyTo.monoforumPeerId : PeerId(); + config.reply.todoItemId = replyTo.todoItemId; const auto replyToTop = replyTo.topicRootId ? replyTo.topicRootId : LookupReplyToTop(_history, to); diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp index 0c7d1f0cff..87375f1d49 100644 --- a/Telegram/SourceFiles/history/history_item_components.cpp +++ b/Telegram/SourceFiles/history/history_item_components.cpp @@ -390,6 +390,7 @@ ReplyFields ReplyFieldsFromMTP( = data.vreply_to_top_id().value_or(result.messageId.bare); result.topicPost = data.is_forum_topic() ? 1 : 0; } + result.todoItemId = data.vtodo_item_id().value_or_empty(); if (const auto header = data.vreply_from()) { const auto &data = header->data(); result.externalPostAuthor diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index 4ca58bde84..6b1cfbb495 100644 --- a/Telegram/SourceFiles/history/history_item_components.h +++ b/Telegram/SourceFiles/history/history_item_components.h @@ -277,6 +277,7 @@ struct ReplyFields { MsgId messageId = 0; MsgId topMessageId = 0; StoryId storyId = 0; + int todoItemId = 0; uint32 quoteOffset : 30 = 0; uint32 manualQuote : 1 = 0; uint32 topicPost : 1 = 0; diff --git a/Telegram/SourceFiles/history/history_item_helpers.cpp b/Telegram/SourceFiles/history/history_item_helpers.cpp index f274356674..bc0f4824b9 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.cpp +++ b/Telegram/SourceFiles/history/history_item_helpers.cpp @@ -722,22 +722,19 @@ bool IsItemScheduledUntilOnline(not_null item) { ClickHandlerPtr JumpToMessageClickHandler( not_null item, FullMsgId returnToId, - TextWithEntities highlightPart, - int highlightPartOffsetHint) { + MessageHighlightId highlight) { return JumpToMessageClickHandler( item->history()->peer, item->id, returnToId, - std::move(highlightPart), - highlightPartOffsetHint); + std::move(highlight)); } ClickHandlerPtr JumpToMessageClickHandler( not_null peer, MsgId msgId, FullMsgId returnToId, - TextWithEntities highlightPart, - int highlightPartOffsetHint) { + MessageHighlightId highlight) { return std::make_shared([=] { const auto separate = Core::App().separateWindowFor(peer); const auto controller = separate @@ -747,8 +744,7 @@ ClickHandlerPtr JumpToMessageClickHandler( auto params = Window::SectionShow{ Window::SectionShow::Way::Forward }; - params.highlightPart = highlightPart; - params.highlightPartOffsetHint = highlightPartOffsetHint; + params.highlight = highlight; params.origin = Window::SectionShow::OriginMessage{ returnToId }; @@ -910,7 +906,8 @@ MTPMessageReplyHeader NewMessageReplyHeader(const Api::SendAction &action) { | Flag::f_quote_offset)) | (quoteEntities.v.empty() ? Flag() - : Flag::f_quote_entities)), + : Flag::f_quote_entities) + | (replyTo.todoItemId ? Flag::f_todo_item_id : Flag())), MTP_int(replyTo.messageId.msg), peerToMTP(externalPeerId), MTPMessageFwdHeader(), // reply_from @@ -918,7 +915,8 @@ MTPMessageReplyHeader NewMessageReplyHeader(const Api::SendAction &action) { MTP_int(replyToTop), MTP_string(replyTo.quote.text), quoteEntities, - MTP_int(replyTo.quoteOffset)); + MTP_int(replyTo.quoteOffset), + MTP_int(replyTo.todoItemId)); } return MTPMessageReplyHeader(); } diff --git a/Telegram/SourceFiles/history/history_item_helpers.h b/Telegram/SourceFiles/history/history_item_helpers.h index d1c472dd58..f6906136c8 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.h +++ b/Telegram/SourceFiles/history/history_item_helpers.h @@ -229,13 +229,11 @@ private: not_null peer, MsgId msgId, FullMsgId returnToId = FullMsgId(), - TextWithEntities highlightPart = {}, - int highlightPartOffsetHint = 0); + MessageHighlightId highlight = {}); [[nodiscard]] ClickHandlerPtr JumpToMessageClickHandler( not_null item, FullMsgId returnToId = FullMsgId(), - TextWithEntities highlightPart = {}, - int highlightPartOffsetHint = 0); + MessageHighlightId highlight = {}); [[nodiscard]] ClickHandlerPtr JumpToStoryClickHandler( not_null story); ClickHandlerPtr JumpToStoryClickHandler( diff --git a/Telegram/SourceFiles/history/history_view_highlight_manager.cpp b/Telegram/SourceFiles/history/history_view_highlight_manager.cpp index ee07a1571f..5ff3f71a6d 100644 --- a/Telegram/SourceFiles/history/history_view_highlight_manager.cpp +++ b/Telegram/SourceFiles/history/history_view_highlight_manager.cpp @@ -65,6 +65,7 @@ Ui::ChatPaintHighlight ElementHighlighter::state( if (item->fullId() == _highlighted.itemId) { auto result = _animation.state(); result.range = _highlighted.part; + result.todoItemId = _highlighted.todoListId; return result; } return {}; @@ -82,19 +83,27 @@ ElementHighlighter::Highlight ElementHighlighter::computeHighlight( const auto i = ranges::find(group->items, item); if (i != end(group->items)) { const auto index = int(i - begin(group->items)); - if (quote.text.empty()) { + if (quote.highlight.empty()) { return { leaderId, AddGroupItemSelection({}, index) }; } else if (const auto leaderView = _viewForItem(leader)) { - return { leaderId, leaderView->selectionFromQuote(quote) }; + return { + leaderId, + leaderView->selectionFromQuote(quote), + quote.highlight.todoItemId, + }; } } - return { leaderId }; - } else if (quote.text.empty()) { - return { item->fullId() }; + return { leaderId, {}, quote.highlight.todoItemId }; + } else if (quote.highlight.quote.empty()) { + return { item->fullId(), {}, quote.highlight.todoItemId }; } else if (const auto view = _viewForItem(item)) { - return { item->fullId(), view->selectionFromQuote(quote) }; + return { + item->fullId(), + view->selectionFromQuote(quote), + quote.highlight.todoItemId, + }; } - return { item->fullId() }; + return { item->fullId(), {}, quote.highlight.todoItemId }; } void ElementHighlighter::highlight(Highlight data) { @@ -108,7 +117,7 @@ void ElementHighlighter::highlight(Highlight data) { } } _highlighted = data; - _animation.start(!data.part.empty() + _animation.start((!data.part.empty() || data.todoListId) && !IsSubGroupSelection(data.part)); repaintHighlightedItem(view); diff --git a/Telegram/SourceFiles/history/history_view_highlight_manager.h b/Telegram/SourceFiles/history/history_view_highlight_manager.h index 5a3b43ca8e..08ac68e44d 100644 --- a/Telegram/SourceFiles/history/history_view_highlight_manager.h +++ b/Telegram/SourceFiles/history/history_view_highlight_manager.h @@ -65,6 +65,7 @@ private: struct Highlight { FullMsgId itemId; TextSelection part; + int todoListId = 0; explicit operator bool() const { return itemId.operator bool(); diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 1cc3d28412..9d82d767d0 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -68,6 +68,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_changes.h" #include "data/data_drafts.h" #include "data/data_session.h" +#include "data/data_todo_list.h" #include "data/data_web_page.h" #include "data/data_document.h" #include "data/data_photo.h" @@ -1575,12 +1576,21 @@ int HistoryWidget::itemTopForHighlight( const auto heightLeft = (visibleAreaHeight - viewHeight); if (heightLeft >= 0) { return std::max(itemTop - (heightLeft / 2), 0); - } else if (const auto sel = itemHighlight(item).range - ; !sel.empty() && !IsSubGroupSelection(sel)) { + } else if (const auto highlight = itemHighlight(item) + ; (!highlight.range.empty() || highlight.todoItemId) + && !IsSubGroupSelection(highlight.range)) { + const auto sel = highlight.range; const auto single = st::messageTextStyle.font->height; - const auto begin = HistoryView::FindViewY(view, sel.from) - single; - const auto end = HistoryView::FindViewY(view, sel.to, begin + single) - + 2 * single; + const auto todoy = sel.empty() + ? HistoryView::FindViewTaskY(view, highlight.todoItemId) + : 0; + const auto begin = sel.empty() + ? (todoy - 4 * single) + : HistoryView::FindViewY(view, sel.from) - single; + const auto end = sel.empty() + ? (todoy + 4 * single) + : (HistoryView::FindViewY(view, sel.to, begin + single) + + 2 * single); auto result = itemTop; if (end > visibleAreaHeight) { result = std::max(result, itemTop + end - visibleAreaHeight); @@ -5797,8 +5807,7 @@ void HistoryWidget::switchToSearch(QString query) { const auto item = activation.item; auto params = ::Window::SectionShow( ::Window::SectionShow::Way::ClearStack); - params.highlightPart = { activation.query }; - params.highlightPartOffsetHint = kSearchQueryOffsetHint; + params.highlight = Window::SearchHighlightId(activation.query); controller()->showPeerHistory( item->history()->peer->id, params, @@ -6907,8 +6916,7 @@ int HistoryWidget::countInitialScrollTop() { enqueueMessageHighlight({ item, - base::take(_showAtMsgParams.highlightPart), - base::take(_showAtMsgParams.highlightPartOffsetHint), + base::take(_showAtMsgParams.highlight), }); const auto result = itemTopForHighlight(view); createUnreadBarIfBelowVisibleArea(result); @@ -7670,12 +7678,7 @@ void HistoryWidget::editDraftOptions() { void HistoryWidget::jumpToReply(FullReplyTo to) { if (const auto item = session().data().message(to.messageId)) { - JumpToMessageClickHandler( - item, - {}, - to.quote, - to.quoteOffset - )->onClick({}); + JumpToMessageClickHandler(item, {}, to.highlight())->onClick({}); } } @@ -8718,7 +8721,7 @@ void HistoryWidget::clearFieldText( void HistoryWidget::replyToMessage(FullReplyTo id) { if (const auto item = session().data().message(id.messageId)) { if (CanSendReply(item) && !base::IsCtrlPressed()) { - replyToMessage(item, id.quote, id.quoteOffset); + replyToMessage(item, id); } else if (item->allowsForward()) { const auto show = controller()->uiShow(); HistoryView::Controls::ShowReplyToChatBox(show, id); @@ -8731,16 +8734,12 @@ void HistoryWidget::replyToMessage(FullReplyTo id) { void HistoryWidget::replyToMessage( not_null item, - TextWithEntities quote, - int quoteOffset) { + FullReplyTo fields) { if (isJoinChannel()) { return; } - _processingReplyTo = { - .messageId = item->fullId(), - .quote = quote, - .quoteOffset = quoteOffset, - }; + fields.messageId = item->fullId(); + _processingReplyTo = fields; _processingReplyItem = item; processReply(); } @@ -9429,11 +9428,24 @@ void HistoryWidget::updateReplyEditText(not_null item) { .session = &session(), .repaint = [=] { updateField(); }, }); + const auto text = [&] { + const auto media = _replyTo.todoItemId ? item->media() : nullptr; + if (const auto todolist = media ? media->todolist() : nullptr) { + const auto i = ranges::find( + todolist->items, + _replyTo.todoItemId, + &TodoListItem::id); + if (i != end(todolist->items)) { + return i->text; + } + } + return (_editMsgId || _replyTo.quote.empty()) + ? item->inReplyText() + : _replyTo.quote; + }(); _replyEditMsgText.setMarkedText( st::defaultTextStyle, - ((_editMsgId || _replyTo.quote.empty()) - ? item->inReplyText() - : _replyTo.quote), + text, Ui::DialogTextOptions(), context); if (fieldOrDisabledShown() || isRecording()) { @@ -9519,10 +9531,9 @@ void HistoryWidget::updateReplyToName() { .customEmojiLoopLimit = 1, }); const auto to = _replyEditMsg ? _replyEditMsg : _kbReplyTo; - const auto replyToQuote = _replyTo && !_replyTo.quote.empty(); _replyToName.setMarkedText( st::fwdTextStyle, - HistoryView::Reply::ComposePreviewName(_history, to, replyToQuote), + HistoryView::Reply::ComposePreviewName(_history, to, _replyTo), Ui::NameTextOptions(), context); } diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index 37ac772c18..0d181fa3f4 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -205,8 +205,7 @@ public: void replyToMessage(FullReplyTo id); void replyToMessage( not_null item, - TextWithEntities quote = {}, - int quoteOffset = 0); + FullReplyTo fields = {}); void editMessage( not_null item, const TextSelection &selection); 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 6cca420dbf..f00742f305 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -492,10 +492,9 @@ void FieldHeader::setShownMessage(HistoryItem *item) { .customEmojiLoopLimit = 1, }); const auto replyTo = _replyTo.current(); - const auto quote = replyTo && !replyTo.quote.empty(); _shownMessageName.setMarkedText( st::fwdTextStyle, - HistoryView::Reply::ComposePreviewName(_history, item, quote), + HistoryView::Reply::ComposePreviewName(_history, item, replyTo), Ui::NameTextOptions(), context); } 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 f398211636..15125a480b 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp @@ -718,8 +718,7 @@ void DraftOptionsBox( state->link = args.usedLink; state->quote = SelectedQuote{ replyItem, - draft.reply.quote, - draft.reply.quoteOffset, + { draft.reply.quote, draft.reply.quoteOffset }, }; state->forward = std::move(args.forward); state->webpage = draft.webpage; @@ -783,7 +782,7 @@ void DraftOptionsBox( box->setTitle(hasLink ? tr::lng_link_options_header() : hasReply - ? (state->quote.current().text.empty() + ? (state->quote.current().highlight.quote.empty() ? tr::lng_reply_options_header() : tr::lng_reply_options_quote()) : (forwardCount == 1) @@ -807,10 +806,12 @@ void DraftOptionsBox( auto result = draft.reply; if (const auto current = state->quote.current()) { result.messageId = current.item->fullId(); - result.quote = current.text; - result.quoteOffset = current.offset; + result.quote = current.highlight.quote; + result.quoteOffset = current.highlight.quoteOffset; +// result.todoItemId = current.highlight.todoItemId; } else { result.quote = {}; +// result.todoItemId = 0; } return result; }; @@ -1112,7 +1113,7 @@ void DraftOptionsBox( state->quote.value(), state->shown.value() ) | rpl::map([=](const SelectedQuote "e, Section shown) { - return (quote.text.empty() || shown != Section::Reply) + return (quote.highlight.quote.empty() || shown != Section::Reply) ? tr::lng_settings_save() : tr::lng_reply_quote_selected(); }) | rpl::flatten_latest(); diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp index 12dff183d2..72c2c2e58c 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp @@ -123,12 +123,10 @@ rpl::producer RootViewContent( ChatMemento::ChatMemento( ChatViewId id, MsgId highlightId, - const TextWithEntities &highlightPart, - int highlightPartOffsetHint) + MessageHighlightId highlight) : _id(id) -, _highlightPart(highlightPart) -, _highlightPartOffsetHint(highlightPartOffsetHint) -, _highlightId(highlightId) { +, _highlightId(highlightId) +, _highlight(std::move(highlight)) { if (highlightId || _id.sublist) { _list.setAroundPosition({ .fullId = FullMsgId(_id.history->peer->id, highlightId), @@ -884,12 +882,7 @@ void ChatWidget::setupComposeControls() { _composeControls->jumpToItemRequests( ) | rpl::start_with_next([=](FullReplyTo to) { if (const auto item = session().data().message(to.messageId)) { - JumpToMessageClickHandler( - item, - {}, - to.quote, - to.quoteOffset - )->onClick({}); + JumpToMessageClickHandler(item, {}, to.highlight())->onClick({}); } }, lifetime()); @@ -1047,8 +1040,9 @@ void ChatWidget::setupSwipeReplyAndBack() { : still)->fullId(); _inner->replyToMessageRequestNotify({ .messageId = replyToItemId, - .quote = selected.text, - .quoteOffset = selected.offset, + .quote = selected.highlight.quote, + .quoteOffset = selected.highlight.quoteOffset, + .todoItemId = selected.highlight.todoItemId, }); }; return result; @@ -2648,8 +2642,7 @@ void ChatWidget::restoreState(not_null memento) { auto params = Window::SectionShow( Window::SectionShow::Way::Forward, anim::type::instant); - params.highlightPart = memento->highlightPart(); - params.highlightPartOffsetHint = memento->highlightPartOffsetHint(); + params.highlight = memento->highlight(); showAtPosition(Data::MessagePosition{ .fullId = FullMsgId(_peer->id, highlight), .date = TimeId(0), @@ -3452,8 +3445,7 @@ bool ChatWidget::searchInChatEmbedded( const auto item = activation.item; auto params = ::Window::SectionShow( ::Window::SectionShow::Way::ClearStack); - params.highlightPart = { activation.query }; - params.highlightPartOffsetHint = kSearchQueryOffsetHint; + params.highlight = Window::SearchHighlightId(activation.query); controller()->showPeerHistory( item->history()->peer->id, params, diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.h b/Telegram/SourceFiles/history/view/history_view_chat_section.h index 87b4604c04..97030e58e1 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.h +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.h @@ -461,8 +461,7 @@ public: explicit ChatMemento( ChatViewId id, MsgId highlightId = 0, - const TextWithEntities &highlightPart = {}, - int highlightPartOffsetHint = 0); + MessageHighlightId highlight = {}); struct Comments { }; @@ -511,20 +510,16 @@ public: [[nodiscard]] MsgId highlightId() const { return _highlightId; } - [[nodiscard]] const TextWithEntities &highlightPart() const { - return _highlightPart; - } - [[nodiscard]] int highlightPartOffsetHint() const { - return _highlightPartOffsetHint; + [[nodiscard]] const MessageHighlightId &highlight() const { + return _highlight; } private: void setupTopicViewer(); ChatViewId _id; - const TextWithEntities _highlightPart; - const int _highlightPartOffsetHint = 0; const MsgId _highlightId = 0; + const MessageHighlightId _highlight; ListMemento _list; std::shared_ptr _replies; QVector _replyReturns; diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index 859608d249..4e48d08eff 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -644,8 +644,13 @@ bool AddReplyToMessageAction( return false; } + const auto todoListTaskId = request.link + ? request.link->property(kTodoListItemIdProperty).toInt() + : 0; const auto "e = request.quote; - auto text = (quote.text.empty() + auto text = (todoListTaskId + ? tr::lng_context_reply_to_task + : quote.highlight.quote.empty() ? tr::lng_context_reply_msg : tr::lng_context_quote_and_reply)( tr::now, @@ -653,8 +658,9 @@ bool AddReplyToMessageAction( menu->addAction(std::move(text), [=, itemId = item->fullId()] { list->replyToMessageRequestNotify({ .messageId = itemId, - .quote = quote.text, - .quoteOffset = quote.offset, + .quote = quote.highlight.quote, + .quoteOffset = quote.highlight.quoteOffset, + .todoItemId = todoListTaskId, }, base::IsCtrlPressed()); }, &st::menuIconReply); return true; @@ -680,7 +686,7 @@ bool AddTodoListAction( if (const auto item = controller->session().data().message(itemId)) { Window::PeerMenuAddTodoListTasks(controller, item); } - }, &st::menuIconCreateTodoList); + }, &st::menuIconAdd); return true; } diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index a6f6f5ac78..586c2033df 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -46,6 +46,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_channel.h" #include "data/data_saved_sublist.h" #include "data/data_session.h" +#include "data/data_todo_list.h" #include "data/data_forum.h" #include "data/data_forum_topic.h" #include "data/data_message_reactions.h" @@ -1360,9 +1361,18 @@ void Element::validateText() { if (const auto done = item->Get()) { if (!done->completed.empty() && !done->incompleted.empty()) { + const auto todoItemId = (done->incompleted.size() == 1) + ? done->incompleted.front() + : 0; setServicePreMessage( item->composeTodoIncompleted(done), - done->lnk); + JumpToMessageClickHandler( + (done->peerId + ? history()->owner().peer(done->peerId) + : history()->peer), + done->msgId, + item->fullId(), + { .todoItemId = todoItemId })); } else { setServicePreMessage({}); } @@ -2205,7 +2215,7 @@ SelectedQuote Element::FindSelectedQuote( ++i; } } - return { item, result, modified.from, overflown }; + return { item, { result, modified.from }, overflown }; } TextSelection Element::FindSelectionFromQuote( @@ -2213,17 +2223,18 @@ TextSelection Element::FindSelectionFromQuote( const SelectedQuote "e) { Expects(quote.item != nullptr); - if (quote.text.empty()) { + const auto &rich = quote.highlight.quote; + if (rich.empty()) { return {}; } const auto &original = quote.item->originalText(); - if (quote.offset == kSearchQueryOffsetHint) { + if (quote.highlight.quoteOffset == kSearchQueryOffsetHint) { return ApplyModificationsFrom( - FindSearchQueryHighlight(original.text, quote.text.text), + FindSearchQueryHighlight(original.text, rich.text), text); } const auto length = int(original.text.size()); - const auto qlength = int(quote.text.text.size()); + const auto qlength = int(rich.text.size()); const auto checkAt = [&](int offset) { return TextSelection{ uint16(offset), @@ -2234,7 +2245,7 @@ TextSelection Element::FindSelectionFromQuote( if (offset > length - qlength) { return TextSelection(); } - const auto i = original.text.indexOf(quote.text.text, offset); + const auto i = original.text.indexOf(rich.text, offset); return (i >= 0) ? checkAt(i) : TextSelection(); }; const auto findOneBefore = [&](int offset) { @@ -2243,7 +2254,7 @@ TextSelection Element::FindSelectionFromQuote( } const auto end = std::min(offset + qlength - 1, length); const auto from = end - length - 1; - const auto i = original.text.lastIndexOf(quote.text.text, from); + const auto i = original.text.lastIndexOf(rich.text, from); return (i >= 0) ? checkAt(i) : TextSelection(); }; const auto findAfter = [&](int offset) { @@ -2281,7 +2292,7 @@ TextSelection Element::FindSelectionFromQuote( ? before : after; }; - auto result = findTwoWays(quote.offset); + auto result = findTwoWays(quote.highlight.quoteOffset); if (result.empty()) { return {}; } @@ -2468,6 +2479,70 @@ int FindViewY(not_null view, uint16 symbol, int yfrom) { return origin.y() + (yfrom + ytill) / 2; } +int FindViewTaskY(not_null view, int taskId, int yfrom) { + auto request = HistoryView::StateRequest(); + request.flags = Ui::Text::StateRequest::Flag::LookupLink; + const auto single = st::messageTextStyle.font->height; + const auto inner = view->innerGeometry(); + const auto origin = inner.topLeft(); + const auto top = 0; + const auto bottom = view->height(); + if (origin.y() < top + || origin.y() + inner.height() > bottom + || inner.height() <= 0) { + return yfrom; + } + const auto media = view->data()->media(); + const auto todolist = media ? media->todolist() : nullptr; + if (!todolist) { + return yfrom; + } + const auto &items = todolist->items; + const auto indexOf = [&](int id) -> int { + return ranges::find(items, id, &TodoListItem::id) - begin(items); + }; + const auto index = indexOf(taskId); + const auto count = int(items.size()); + if (index == count) { + return yfrom; + } + yfrom = std::max(yfrom - origin.y(), 0); + auto ytill = inner.height() - 1; + const auto middle = (yfrom + ytill) / 2; + const auto fory = [&](int y) { + const auto state = view->textState(origin + QPoint(0, y), request); + const auto &link = state.link; + const auto id = link + ? link->property(kTodoListItemIdProperty).toInt() + : -1; + const auto index = (id >= 0) ? indexOf(id) : int(items.size()); + return (index < count) ? index : (y < middle) ? -1 : count; + }; + auto indexfrom = fory(yfrom); + auto indextill = fory(ytill); + if ((yfrom >= ytill) || (indexfrom >= index)) { + return origin.y() + yfrom; + } else if (indextill <= index) { + return origin.y() + ytill; + } + while (ytill - yfrom >= 2 * single) { + const auto middle = (yfrom + ytill) / 2; + const auto found = fory(middle); + if (found == index + || indexfrom > found + || indextill < found) { + return origin.y() + middle; + } else if (found < index) { + yfrom = middle; + indexfrom = found; + } else { + ytill = middle; + indextill = found; + } + } + return origin.y() + (yfrom + ytill) / 2; +} + Window::SessionController *ExtractController(const ClickContext &context) { const auto my = context.other.value(); if (const auto controller = my.sessionWindow.get()) { diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h index 6f7a4edea0..34bc8679ce 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.h +++ b/Telegram/SourceFiles/history/view/history_view_element.h @@ -357,12 +357,11 @@ struct TopicButton { struct SelectedQuote { HistoryItem *item = nullptr; - TextWithEntities text; - int offset = 0; + MessageHighlightId highlight; bool overflown = false; explicit operator bool() const { - return item && !text.empty(); + return item && !highlight.quote.empty(); } friend inline bool operator==(SelectedQuote, SelectedQuote) = default; }; @@ -748,6 +747,11 @@ private: uint16 symbol, int yfrom = 0); +[[nodiscard]] int FindViewTaskY( + not_null view, + int taskId, + int yfrom = 0); + [[nodiscard]] Window::SessionController *ExtractController( const ClickContext &context); diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index b7477cddfa..0217ef24e3 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -720,12 +720,21 @@ std::optional ListWidget::scrollTopForView( const auto heightLeft = (available - height); if (heightLeft >= 0) { return std::max(top - (heightLeft / 2), 0); - } else if (const auto sel = _highlighter.state(view->data()).range - ; !sel.empty() && !IsSubGroupSelection(sel)) { + } else if (const auto highlight = _highlighter.state(view->data()) + ; (!highlight.range.empty() || highlight.todoItemId) + && !IsSubGroupSelection(highlight.range)) { + const auto sel = highlight.range; const auto single = st::messageTextStyle.font->height; - const auto begin = HistoryView::FindViewY(view, sel.from) - single; - const auto end = HistoryView::FindViewY(view, sel.to, begin + single) - + 2 * single; + const auto todoy = sel.empty() + ? HistoryView::FindViewTaskY(view, highlight.todoItemId) + : 0; + const auto begin = sel.empty() + ? (todoy - 4 * single) + : HistoryView::FindViewY(view, sel.from) - single; + const auto end = sel.empty() + ? (todoy + 4 * single) + : (HistoryView::FindViewY(view, sel.to, begin + single) + + 2 * single); auto result = top; if (end > available) { result = std::max(result, top + end - available); @@ -822,10 +831,9 @@ bool ListWidget::isBelowPosition(Data::MessagePosition position) const { void ListWidget::highlightMessage( FullMsgId itemId, - const TextWithEntities &part, - int partOffsetHint) { + const MessageHighlightId &highlight) { if (const auto view = viewForItem(itemId)) { - _highlighter.highlight({ view->data(), part, partOffsetHint }); + _highlighter.highlight({ view->data(), highlight }); } } @@ -903,11 +911,8 @@ bool ListWidget::showAtPositionNow( } if (position != Data::MaxMessagePosition && position != Data::UnreadMessagePosition) { - const auto hasHighlight = !params.highlightPart.empty(); - highlightMessage( - position.fullId, - params.highlightPart, - params.highlightPartOffsetHint); + const auto hasHighlight = !params.highlight.empty(); + highlightMessage(position.fullId, params.highlight); if (hasHighlight) { // We may want to scroll to a different part of the message. scrollTop = scrollTopForPosition(position); diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.h b/Telegram/SourceFiles/history/view/history_view_list_widget.h index 8f4a354b8c..f313810626 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.h +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.h @@ -314,8 +314,7 @@ public: bool isBelowPosition(Data::MessagePosition position) const; void highlightMessage( FullMsgId itemId, - const TextWithEntities &part, - int partOffsetHint); + const MessageHighlightId &highlight); void showAtPosition( Data::MessagePosition position, diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 3dde8462fc..708127f9b9 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -490,6 +490,8 @@ void Message::initPaidInformation() { refreshSuggestedInfo(item, suggest, replyData); } return; + } else if (!item->history()->peer->isUser()) { + return; } const auto media = this->media(); const auto mine = PaidInformation{ @@ -3348,7 +3350,7 @@ TextSelection Message::selectionFromQuote( const SelectedQuote "e) const { Expects(quote.item != nullptr); - if (quote.text.empty()) { + if (quote.highlight.quote.empty()) { return {}; } const auto item = quote.item; diff --git a/Telegram/SourceFiles/history/view/history_view_reply.cpp b/Telegram/SourceFiles/history/view/history_view_reply.cpp index 6c3f680bca..d69108bfdd 100644 --- a/Telegram/SourceFiles/history/view/history_view_reply.cpp +++ b/Telegram/SourceFiles/history/view/history_view_reply.cpp @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_peer.h" #include "data/data_session.h" #include "data/data_story.h" +#include "data/data_todo_list.h" #include "data/data_user.h" #include "history/view/history_view_item_preview.h" #include "history/history.h" @@ -42,6 +43,85 @@ namespace { constexpr auto kNonExpandedLinesLimit = 5; +[[nodiscard]] QImage MakeTaskImage() { + const auto diameter = st::normalFont->ascent; + const auto line = st::historyPollRadio.thickness; + const auto size = 2 * line + diameter; + const auto ratio = style::DevicePixelRatio(); + auto result = QImage( + QSize(size, size) * ratio, + QImage::Format_ARGB32_Premultiplied); + result.fill(Qt::transparent); + result.setDevicePixelRatio(ratio); + + auto p = QPainter(&result); + PainterHighQualityEnabler hq(p); + + p.setOpacity(st::historyPollRadioOpacity); + + const auto rect = QRectF(line, line, diameter, diameter).marginsRemoved( + QMarginsF(line / 2., line / 2., line / 2., line / 2.)); + auto pen = QPen(QColor(255, 255, 255)); + pen.setWidth(line); + p.setPen(pen); + p.drawEllipse(rect); + + p.end(); + + return result; +} + +[[nodiscard]] QImage MakeTaskDoneImage() { + const auto white = QColor(255, 255, 255); + const auto black = QColor(0, 0, 0); + + const auto diameter = st::normalFont->ascent; + const auto line = st::historyPollRadio.thickness; + const auto size = 2 * line + diameter; + const auto ratio = style::DevicePixelRatio(); + auto result = QImage( + QSize(size, size) * ratio, + QImage::Format_ARGB32_Premultiplied); + result.fill(black); + result.setDevicePixelRatio(ratio); + + auto p = QPainter(&result); + PainterHighQualityEnabler hq(p); + + const auto rect = QRectF(line, line, diameter, diameter).marginsRemoved( + QMarginsF(line / 2., line / 2., line / 2., line / 2.)); + auto pen = QPen(white); + pen.setWidth(line); + p.setPen(pen); + p.setBrush(white); + p.drawEllipse(rect); + const auto &icon = st::historyPollInChoiceRight; + icon.paint( + p, + line + (diameter - icon.width()) / 2, + line + (diameter - icon.height()) / 2, + size, + black); + p.end(); + + return style::colorizeImage(result, white); +} + +[[nodiscard]] TextWithEntities TaskDoneIcon( + not_null session) { + return Ui::Text::SingleCustomEmoji( + session->data().customEmojiManager().registerInternalEmoji( + MakeTaskDoneImage(), + QMargins(0, st::lineWidth, st::lineWidth, 0))); +} + +[[nodiscard]] TextWithEntities TaskIcon(not_null session) { + return Ui::Text::SingleCustomEmoji( + session->data().customEmojiManager().registerInternalEmoji( + MakeTaskImage(), + QMargins(0, st::lineWidth, st::lineWidth, 0))); +} + } // namespace void ValidateBackgroundEmoji( @@ -197,6 +277,22 @@ void Reply::update( const auto item = view->data(); const auto &fields = data->fields(); const auto message = data->resolvedMessage.get(); + const auto messageMedia = (message && fields.todoItemId) + ? message->media() + : nullptr; + const auto messageTodoList = messageMedia + ? messageMedia->todolist() + : nullptr; + const auto taskIndex = messageTodoList + ? int(ranges::find( + messageTodoList->items, + fields.todoItemId, + &TodoListItem::id) - begin(messageTodoList->items)) + : -1; + const auto task = (taskIndex >= 0 + && taskIndex < messageTodoList->items.size()) + ? &messageTodoList->items[taskIndex] + : nullptr; const auto story = data->resolvedStory.get(); const auto externalMedia = fields.externalMedia.get(); if (!_externalSender) { @@ -214,7 +310,6 @@ void Reply::update( _hiddenSenderColorIndexPlusOne = (!_colorPeer && message) ? (message->originalHiddenSenderInfo()->colorIndex + 1) : 0; - const auto hasPreview = (story && story->hasReplyPreview()) || (message && message->media() @@ -229,8 +324,13 @@ void Reply::update( && !fields.quote.empty(); _hasQuoteIcon = hasQuoteIcon ? 1 : 0; + const auto session = &view->history()->session(); const auto text = (!_displaying && data->unavailable()) ? TextWithEntities() + : task + ? Ui::Text::Colorized(task->completionDate + ? TaskDoneIcon(session) + : TaskIcon(session)).append(task->text) : (message && (fields.quote.empty() || !fields.manualQuote)) ? message->inReplyText() : !fields.quote.empty() @@ -288,10 +388,11 @@ void Reply::setLinkFrom( const auto &fields = data->fields(); const auto externalChannelId = peerToChannel(fields.externalPeerId); const auto messageId = fields.messageId; - const auto quote = fields.manualQuote - ? fields.quote - : TextWithEntities(); - const auto quoteOffset = fields.quoteOffset; + const auto highlight = MessageHighlightId{ + .quote = fields.manualQuote ? fields.quote : TextWithEntities(), + .quoteOffset = int(fields.quoteOffset), + .todoItemId = fields.todoItemId, + }; const auto returnToId = view->data()->fullId(); const auto externalLink = [=](ClickContext context) { const auto my = context.other.value(); @@ -314,8 +415,7 @@ void Reply::setLinkFrom( channel, messageId, returnToId, - quote, - quoteOffset + highlight )->onClick(context); } else { controller->showPeerInfo(channel); @@ -336,7 +436,7 @@ void Reply::setLinkFrom( const auto message = data->resolvedMessage.get(); const auto story = data->resolvedStory.get(); _link = message - ? JumpToMessageClickHandler(message, returnToId, quote, quoteOffset) + ? JumpToMessageClickHandler(message, returnToId, highlight) : story ? JumpToStoryClickHandler(story) : (data->external() @@ -873,18 +973,28 @@ TextWithEntities Reply::ForwardEmoji(not_null owner) { TextWithEntities Reply::ComposePreviewName( not_null history, not_null to, - bool quote) { + const FullReplyTo &replyTo) { const auto sender = [&] { if (const auto from = to->displayFrom()) { return not_null(from); } return to->author(); }(); + if (const auto media = replyTo.todoItemId ? to->media() : nullptr) { + if (const auto todolist = media->todolist()) { + return tr::lng_preview_reply_to_task( + tr::now, + lt_title, + todolist->title, + Ui::Text::WithEntities); + } + } const auto toPeer = to->history()->peer; const auto displayAsExternal = (to->history() != history); const auto groupNameAdded = displayAsExternal && (toPeer != sender) && (toPeer->isChat() || toPeer->isMegagroup()); + const auto quote = replyTo && !replyTo.quote.empty(); const auto shorten = groupNameAdded || quote; auto nameFull = TextWithEntities(); diff --git a/Telegram/SourceFiles/history/view/history_view_reply.h b/Telegram/SourceFiles/history/view/history_view_reply.h index 416f4c0097..3e1a55addc 100644 --- a/Telegram/SourceFiles/history/view/history_view_reply.h +++ b/Telegram/SourceFiles/history/view/history_view_reply.h @@ -110,7 +110,7 @@ public: [[nodiscard]] static TextWithEntities ComposePreviewName( not_null history, not_null to, - bool quote); + const FullReplyTo &replyTo); private: [[nodiscard]] Ui::Text::GeometryDescriptor textGeometry( diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp index d888247952..f362110b0b 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp @@ -438,12 +438,8 @@ void ScheduledWidget::setupComposeControls() { if (item->isScheduled() && item->history() == _history) { showAtPosition(item->position()); } else { - JumpToMessageClickHandler( - item, - {}, - to.quote, - to.quoteOffset - )->onClick({}); + const auto highlight = to.highlight(); + JumpToMessageClickHandler(item, {}, highlight)->onClick({}); } } }, lifetime()); diff --git a/Telegram/SourceFiles/history/view/history_view_service_message.cpp b/Telegram/SourceFiles/history/view/history_view_service_message.cpp index 47dd11eb8d..ce283943a0 100644 --- a/Telegram/SourceFiles/history/view/history_view_service_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_service_message.cpp @@ -463,17 +463,16 @@ QSize Service::performCountCurrentSize(int newWidth) { const auto media = this->media(); const auto mediaDisplayed = media && media->isDisplayed(); auto contentWidth = newWidth; + if (delegate()->elementChatMode() == ElementChatMode::Wide) { + accumulate_min(contentWidth, st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left()); + } + contentWidth -= st::msgServiceMargin.left() + st::msgServiceMargin.left(); // two small margins + if (contentWidth < st::msgServicePadding.left() + st::msgServicePadding.right() + 1) { + contentWidth = st::msgServicePadding.left() + st::msgServicePadding.right() + 1; + } if (mediaDisplayed && media->hideServiceText()) { newHeight += media->resizeGetHeight(newWidth) + marginBottom(); } else if (!text().isEmpty()) { - if (delegate()->elementChatMode() == ElementChatMode::Wide) { - accumulate_min(contentWidth, st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left()); - } - contentWidth -= st::msgServiceMargin.left() + st::msgServiceMargin.left(); // two small margins - if (contentWidth < st::msgServicePadding.left() + st::msgServicePadding.right() + 1) { - contentWidth = st::msgServicePadding.left() + st::msgServicePadding.right() + 1; - } - auto nwidth = qMax(contentWidth - st::msgServicePadding.left() - st::msgServicePadding.right(), 0); newHeight += (contentWidth >= maxWidth()) ? minHeight() diff --git a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp index 234bbca061..19036b8788 100644 --- a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp +++ b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp @@ -432,10 +432,14 @@ void SubsectionTabs::setupSlider( .session = &session(), }), }, paused); - slider->setActiveSectionFast(activeIndex); + + const auto ignoreActiveScroll = (scrollSavingIndex >= 0); + slider->setActiveSectionFast(activeIndex, ignoreActiveScroll); _sectionsSlice = _slice; - if (scrollSavingIndex >= 0) { + Assert(slider->sectionsCount() == _slice.size()); + if (ignoreActiveScroll) { + Assert(scrollSavingIndex < slider->sectionsCount()); const auto position = scrollSavingShift + slider->lookupSectionPosition(scrollSavingIndex); if (vertical) { @@ -702,6 +706,8 @@ void SubsectionTabs::refreshSlice() { if (_slice != slice) { _slice = std::move(slice); _refreshed.fire({}); + Assert((!_horizontal && !_vertical) + || (_slice.size() == _sectionsSlice.size())); } }); const auto push = [&](not_null thread) { diff --git a/Telegram/SourceFiles/history/view/media/history_view_todo_list.cpp b/Telegram/SourceFiles/history/view/media/history_view_todo_list.cpp index 4f37d36d0c..e468a46eae 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_todo_list.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_todo_list.cpp @@ -334,9 +334,11 @@ void TodoList::updateTasks(bool skipAnimations) { ClickHandlerPtr TodoList::createTaskClickHandler( const Task &task) { const auto id = task.id; - return std::make_shared(crl::guard(this, [=] { + auto result = std::make_shared(crl::guard(this, [=] { toggleCompletion(id); })); + result->setProperty(kTodoListItemIdProperty, id); + return result; } void TodoList::startToggleAnimation(Task &task) { @@ -375,11 +377,24 @@ void TodoList::toggleCompletion(int id) { if (i == end(_tasks)) { return; } + const auto selected = (i->completionDate != 0); i->completionDate = selected ? TimeId() : base::unixtime::now(); if (!selected) { i->setCompletedBy(_parent->history()->session().user()); } + + const auto parentMedia = _parent->data()->media(); + const auto baseList = parentMedia ? parentMedia->todolist() : nullptr; + if (baseList) { + const auto j = ranges::find(baseList->items, id, &TodoListItem::id); + if (j != end(baseList->items)) { + j->completionDate = i->completionDate; + j->completedBy = i->completedBy; + } + history()->owner().updateDependentMessages(_parent->data()); + } + startToggleAnimation(*i); repaint(); @@ -467,6 +482,7 @@ void TodoList::draw(Painter &p, const PaintContext &context) const { paintw, width(), context); + appendTaskHighlight(task.id, tshift, height, context); if (was) { heavy = true; } else if (!task.userpic.null()) { @@ -561,6 +577,33 @@ int TodoList::paintTask( return height; } +void TodoList::appendTaskHighlight( + int id, + int top, + int height, + const PaintContext &context) const { + if (context.highlight.todoItemId != id + || context.highlight.collapsion <= 0.) { + return; + } + const auto to = context.highlightInterpolateTo; + const auto toProgress = (1. - context.highlight.collapsion); + if (toProgress >= 1.) { + context.highlightPathCache->addRect(to); + } else if (toProgress <= 0.) { + context.highlightPathCache->addRect(0, top, width(), height); + } else { + const auto lerp = [=](int from, int to) { + return from + (to - from) * toProgress; + }; + context.highlightPathCache->addRect( + lerp(0, to.x()), + lerp(top, to.y()), + lerp(width(), to.width()), + lerp(height, to.height())); + } +} + void TodoList::paintRadio( Painter &p, const Task &task, diff --git a/Telegram/SourceFiles/history/view/media/history_view_todo_list.h b/Telegram/SourceFiles/history/view/media/history_view_todo_list.h index 733c5c2ab0..b17e7853cc 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_todo_list.h +++ b/Telegram/SourceFiles/history/view/media/history_view_todo_list.h @@ -117,6 +117,11 @@ private: int top, int paintw, const PaintContext &context) const; + void appendTaskHighlight( + int id, + int top, + int height, + const PaintContext &context) const; void radialAnimationCallback() const; diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style index e20c3d861e..3eae2fe12d 100644 --- a/Telegram/SourceFiles/info/info.style +++ b/Telegram/SourceFiles/info/info.style @@ -731,8 +731,8 @@ manageDeleteGroupButton: SettingsCountButton(manageGroupNoIconButton) { manageGroupReactions: IconButton(defaultIconButton) { width: 24px; height: 36px; - icon: icon{{ "info/edit/stickers_add", historyComposeIconFg }}; - iconOver: icon{{ "info/edit/stickers_add", historyComposeIconFgOver }}; + icon: icon{{ "menu/add", historyComposeIconFg }}; + iconOver: icon{{ "menu/add", historyComposeIconFgOver }}; } manageGroupReactionsField: InputField(defaultInputField) { textMargins: margins(1px, 12px, 24px, 8px); diff --git a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp index 86b7d7b527..8fd731a9e5 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_premium.h" #include "apiwrap.h" +#include "boxes/star_gift_box.h" #include "data/data_channel.h" #include "data/data_credits.h" #include "data/data_session.h" @@ -380,6 +381,7 @@ void InnerWidget::loadMore() { _entries.clear(); } _entries.reserve(_entries.size() + data.vgifts().v.size()); + auto hasUnique = false; for (const auto &gift : data.vgifts().v) { if (auto parsed = Api::FromTL(_peer, gift)) { auto descriptor = DescriptorForGift(_peer, *parsed); @@ -387,10 +389,15 @@ void InnerWidget::loadMore() { .gift = std::move(*parsed), .descriptor = std::move(descriptor), }); + hasUnique = (parsed->info.unique != nullptr); } } refreshButtons(); refreshAbout(); + + if (hasUnique) { + Ui::PreloadUniqueGiftResellPrices(&_peer->session()); + } }).fail([=] { _loadMoreRequestId = 0; _allLoaded = true; diff --git a/Telegram/SourceFiles/lang/lang_tag.cpp b/Telegram/SourceFiles/lang/lang_tag.cpp index eb6f938bf8..02620f2e1d 100644 --- a/Telegram/SourceFiles/lang/lang_tag.cpp +++ b/Telegram/SourceFiles/lang/lang_tag.cpp @@ -949,7 +949,20 @@ QString FormatCountDecimal(int64 number) { } QString FormatExactCountDecimal(float64 number) { - return QLocale().toString(number, 'f', QLocale::FloatingPointShortest); + const auto locale = QLocale(); + if (qFuzzyCompare(number, base::SafeRound(number))) { + return locale.toString(int64(base::SafeRound(number))); + } + + // Somehow using QLocale::FloatingPointShortest sometimes produces + // "0.8500000000000001" on some systems / locales, + // so I want to stick to 6 digits max (default third argument value). + auto result = locale.toString(number, 'f'); + const auto zero = locale.zeroDigit(); + while (result.endsWith(zero)) { + result.chop(1); + } + return result; } ShortenedCount FormatCreditsAmountToShort(CreditsAmount amount) { diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index 2513a792cb..73fd0fa725 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -1087,3 +1087,34 @@ mediaviewSponsoredButton: RoundButton(defaultActiveButton) { ripple: universalRippleAnimation; } + +mediaSponsoredSkip: 16px; +mediaSponsoredShift: 16px; +mediaSponsoredPadding: margins(12px, 8px, 8px, 8px); +mediaSponsoredThumb: 48px; +mediaSponsoredCloseTwice: 3px; +mediaSponsoredCloseSmall: 3px; +mediaSponsoredCloseSize: 11px; +mediaSponsoredCloseCorner: 6px; +mediaSponsoredCloseFull: 64px; +mediaSponsoredCloseStroke: 2px; +mediaSponsoredCloseRipple: 36px; +mediaSponsoredCloseDiameter: 24px; +mediaSponsoredCloseFont: font(12px bold); + +mediaSponsoredAbout: RoundButton(defaultActiveButton) { + textFg: windowActiveTextFg; + textFgOver: windowActiveTextFg; + textBg: lightButtonBgOver; + textBgOver: lightButtonBgOver; + width: -12px; + height: 18px; + radius: 9px; + textTop: 0px; + style: TextStyle(defaultTextStyle) { + font: font(12px); + } + ripple: RippleAnimation(defaultRippleAnimation) { + color: lightButtonBgRipple; + } +} diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index afd69eb966..f6d64f14c6 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -51,6 +51,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/view/media_view_pip.h" #include "media/view/media_view_overlay_raster.h" #include "media/view/media_view_overlay_opengl.h" +#include "media/view/media_view_playback_sponsored.h" #include "media/stories/media_stories_share.h" #include "media/stories/media_stories_view.h" #include "media/streaming/media_streaming_document.h" @@ -339,6 +340,7 @@ struct OverlayWidget::Streamed { Streaming::Instance instance; std::unique_ptr controls; + std::unique_ptr sponsored; std::unique_ptr powerSaveBlocker; bool ready = false; @@ -1617,7 +1619,11 @@ void OverlayWidget::fillContextMenuActions( if (const auto window = findWindow()) { const auto show = window->uiShow(); const auto fullId = _message->fullId(); - Menu::FillSponsored(_body, addAction, show, fullId, true); + Menu::FillSponsored( + addAction, + show, + fullId, + { .dark = true, .skipInfo = true }); } return; } @@ -3981,7 +3987,11 @@ bool OverlayWidget::initStreaming(const StartStreaming &startStreaming) { && !_streamed->instance.player().finished())) { startStreamingPlayer(startStreaming); } else { - _streamed->ready = _streamed->instance.player().ready(); + if (_streamed->instance.player().ready()) { + markStreamedReady(); + } else { + _streamed->ready = false; + } updatePlaybackState(); } return true; @@ -3994,7 +4004,7 @@ void OverlayWidget::startStreamingPlayer( const auto &player = _streamed->instance.player(); if (player.playing()) { if (!_streamed->withSound) { - _streamed->ready = true; + markStreamedReady(); return; } _pip = nullptr; @@ -4012,6 +4022,18 @@ void OverlayWidget::startStreamingPlayer( restartAtSeekPosition(_streamedPosition); } +void OverlayWidget::markStreamedReady() { + Expects(_streamed != nullptr); + + if (_streamed->ready) { + return; + } + _streamed->ready = true; + if (const auto sponsored = _streamed->sponsored.get()) { + sponsored->start(); + } +} + void OverlayWidget::initStreamingThumbnail() { Expects(_photo || _document); @@ -4083,7 +4105,7 @@ void OverlayWidget::initStreamingThumbnail() { } void OverlayWidget::streamingReady(Streaming::Information &&info) { - _streamed->ready = true; + markStreamedReady(); if (videoShown()) { applyVideoSize(); _streamedQualityChangeFrame = QImage(); @@ -4105,6 +4127,7 @@ void OverlayWidget::applyVideoSize() { bool OverlayWidget::createStreamingObjects() { Expects(_photo || _document); + Expects(!_streamed); const auto origin = fileOrigin(); const auto callback = [=] { waitingAnimationCallback(); }; @@ -4137,6 +4160,18 @@ bool OverlayWidget::createStreamingObjects() { _body, static_cast(this)); _streamed->controls->show(); + _streamed->sponsored = PlaybackSponsored::Has(_message) + ? std::make_unique( + _streamed->controls.get(), + uiShow(), + _message) + : nullptr; + if (const auto sponsored = _streamed->sponsored.get()) { + _layerBg->layerShownValue( + ) | rpl::start_with_next([=](bool shown) { + sponsored->setPaused(shown); + }, sponsored->lifetime()); + } refreshClipControllerGeometry(); } return true; diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h index 8787be47e4..2044d9caae 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h @@ -78,6 +78,7 @@ struct ContentLayout; namespace Media::View { +class PlaybackSponsored; class GroupThumbs; class Pip; @@ -412,6 +413,7 @@ private: const StartStreaming &startStreaming = StartStreaming()); void startStreamingPlayer(const StartStreaming &startStreaming); void initStreamingThumbnail(); + void markStreamedReady(); void streamingReady(Streaming::Information &&info); [[nodiscard]] bool createStreamingObjects(); void handleStreamingUpdate(Streaming::Update &&update); diff --git a/Telegram/SourceFiles/media/view/media_view_playback_controls.h b/Telegram/SourceFiles/media/view/media_view_playback_controls.h index a91b8ad2e9..be3e84c824 100644 --- a/Telegram/SourceFiles/media/view/media_view_playback_controls.h +++ b/Telegram/SourceFiles/media/view/media_view_playback_controls.h @@ -19,14 +19,13 @@ class MediaSlider; class PopupMenu; } // namespace Ui -namespace Media { -namespace Player { +namespace Media::Player { struct TrackState; class SettingsButton; class SpeedController; -} // namespace Player +} // namespace Media::Player -namespace View { +namespace Media::View { class PlaybackProgress; @@ -131,5 +130,4 @@ private: }; -} // namespace View -} // namespace Media +} // namespace Media::View diff --git a/Telegram/SourceFiles/media/view/media_view_playback_sponsored.cpp b/Telegram/SourceFiles/media/view/media_view_playback_sponsored.cpp new file mode 100644 index 0000000000..895b4c70b4 --- /dev/null +++ b/Telegram/SourceFiles/media/view/media_view_playback_sponsored.cpp @@ -0,0 +1,767 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "media/view/media_view_playback_sponsored.h" + +#include "boxes/premium_preview_box.h" +#include "data/components/sponsored_messages.h" +#include "data/data_file_origin.h" +#include "data/data_photo.h" +#include "data/data_photo_media.h" +#include "data/data_session.h" +#include "history/history.h" +#include "history/history_item.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "menu/menu_sponsored.h" +#include "ui/effects/numbers_animation.h" +#include "ui/effects/ripple_animation.h" +#include "ui/widgets/menu/menu_add_action_callback.h" +#include "ui/widgets/menu/menu_add_action_callback_factory.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/popup_menu.h" +#include "ui/basic_click_handlers.h" +#include "ui/painter.h" +#include "ui/ui_utility.h" +#include "ui/cached_round_corners.h" +#include "styles/style_chat.h" +#include "styles/style_media_view.h" + +namespace Media::View { +namespace { + +constexpr auto kStartDelayMin = crl::time(1000); +constexpr auto kDurationMin = 5 * crl::time(1000); + +enum class Action { + Close, + PromotePremium, + Pause, + Unpause, +}; + +class Close final : public Ui::RippleButton { +public: + Close( + not_null parent, + const style::RippleAnimation &st, + rpl::producer allowCloseAt); + + [[nodiscard]] rpl::producer actions() const; + +private: + QPoint prepareRippleStartPosition() const override; + QImage prepareRippleMask() const override; + void paintEvent(QPaintEvent *e) override; + + void updateProgress(crl::time now); + + rpl::event_stream _actions; + + Ui::NumbersAnimation _countdown; + Ui::Animations::Basic _progress; + base::Timer _noAnimationTimer; + crl::time _allowCloseAt = 0; + crl::time _startedAt = 0; + crl::time _pausedAt = 0; + int _secondsTill = 0; + int _rippleSize = 0; + QPoint _rippleOrigin; + bool _allowClose = false; + +}; + +Close::Close( + not_null parent, + const style::RippleAnimation &st, + rpl::producer allowCloseAt) +: RippleButton(parent, st) +, _countdown(st::mediaSponsoredCloseFont, [=] { update(); }) +, _progress([=](crl::time now) { updateProgress(now); }) +, _noAnimationTimer([=] { updateProgress(crl::now()); }) +, _startedAt(crl::now()) { + resize(st::mediaSponsoredCloseFull, st::mediaSponsoredCloseFull); + + const auto size = st::mediaSponsoredCloseRipple; + const auto cut = int(base::SafeRound((width() - size) / 2.)); + _rippleSize = std::min(width() - 2 * cut, height() - 2 * cut); + _rippleOrigin = QPoint( + (width() - _rippleSize) / 2, + (height() - _rippleSize) / 2); + + std::move( + allowCloseAt + ) | rpl::start_with_next([=](crl::time at) { + const auto now = crl::now(); + if (!at) { + updateProgress(now); + _pausedAt = now; + _progress.stop(); + } else { + if (_pausedAt) { + _startedAt += now - base::take(_pausedAt); + } + _allowCloseAt = at; + updateProgress(now); + if (!anim::Disabled()) { + _progress.start(); + } else if (!_allowClose) { + _noAnimationTimer.callEach(crl::time(200)); + } + } + }, lifetime()); + updateProgress(_startedAt); + + setClickedCallback([=] { + _actions.fire(_allowClose ? Action::Close : Action::PromotePremium); + }); +} + +rpl::producer Close::actions() const { + return _actions.events(); +} + +void Close::updateProgress(crl::time now) { + update(); +} + +QPoint Close::prepareRippleStartPosition() const { + return mapFromGlobal(QCursor::pos()) - _rippleOrigin; +} + +QImage Close::prepareRippleMask() const { + return Ui::RippleAnimation::EllipseMask({ _rippleSize, _rippleSize }); +} + +void Close::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + + paintRipple(p, _rippleOrigin); + + const auto now = crl::now(); + if (!_pausedAt) { + _allowClose = (now >= _allowCloseAt); + } + const auto msTill = _allowCloseAt - (_pausedAt ? _pausedAt : now); + const auto msFull = _allowCloseAt - _startedAt; + const auto secondsTill = (std::max(msTill, crl::time()) + 999) / 1000; + const auto secondsFull = (std::max(msFull, crl::time()) + 999) / 1000; + const auto allowCloseLeft = anim::Disabled() + ? (secondsFull ? (secondsTill / float64(secondsFull)) : 0) + : std::max(msFull ? (msTill / float64(msFull)) : 0., 0.); + const auto duration = crl::time(st::fadeWrapDuration); + const auto allowedProgress = anim::Disabled() + ? (secondsTill ? 0. : 1.) + : std::clamp(-msTill, crl::time(), duration) / float64(duration); + + if (_secondsTill != secondsTill) { + const auto initial = !_secondsTill; + _secondsTill = secondsTill; + _countdown.setText(QString::number(_secondsTill), _secondsTill); + if (initial) { + _countdown.finishAnimating(); + } + } + + auto pen = st::mediaviewTextLinkFg->p; + if (allowedProgress < 1.) { + if (allowedProgress > 0.) { + p.setOpacity(1. - allowedProgress); + } + p.setPen(pen); + + const auto inner = QRect( + (width() - st::mediaSponsoredCloseDiameter) / 2, + (height() - st::mediaSponsoredCloseDiameter) / 2, + st::mediaSponsoredCloseDiameter, + st::mediaSponsoredCloseDiameter); + p.setFont(st::mediaSponsoredCloseFont); + _countdown.paint( + p, + inner.x() + (inner.width() - _countdown.countWidth()) / 2, + (inner.y() + + (inner.height() + - st::mediaSponsoredCloseFont->height) / 2), + width()); + + const auto skip = 0.23; + const auto len = int(base::SafeRound( + arc::kFullLength * (1. - skip) * allowCloseLeft)); + if (len > 0) { + const auto from = arc::kFullLength / 4; + auto hq = PainterHighQualityEnabler(p); + pen.setWidthF(st::mediaSponsoredCloseStroke); + pen.setCapStyle(Qt::RoundCap); + p.setPen(pen); + p.drawArc(inner, from, len); + } + + p.setOpacity(1.); + } + + const auto sizeFinal = st::mediaSponsoredCloseSize; + const auto sizeSmall = st::mediaSponsoredCloseCorner; + const auto twiceFinal = st::mediaSponsoredCloseTwice; + const auto twiceSmall = st::mediaSponsoredCloseSmall; + const auto size = sizeSmall + allowedProgress * (sizeFinal - sizeSmall); + const auto twice = twiceSmall + + allowedProgress * (twiceFinal - twiceSmall); + const auto leftFinal = (width() - size) / 2.; + const auto leftSmall = (width() + st::mediaSponsoredCloseDiameter) / 2. + - (st::mediaSponsoredCloseStroke / 2.) + - sizeSmall; + const auto topFinal = (height() - size) / 2.; + const auto topSmall = (height() - st::mediaSponsoredCloseDiameter) / 2.; + const auto left = leftSmall + allowedProgress * (leftFinal - leftSmall); + const auto top = topSmall + allowedProgress * (topFinal - topSmall); + + auto hq = PainterHighQualityEnabler(p); + pen.setWidthF(twice / 2.); + p.setPen(pen); + p.drawLine(QPointF(left, top), QPointF(left + size, top + size)); + p.drawLine(QPointF(left + size, top), QPointF(left, top + size)); +} + +[[nodiscard]] style::RoundButton PrepareAboutStyle() { + static auto textBg = style::complex_color([] { + auto result = st::mediaviewTextLinkFg->c; + result.setAlphaF(result.alphaF() * 0.1); + return result; + }); + static auto textBgOver = style::complex_color([] { + auto result = st::mediaviewTextLinkFg->c; + result.setAlphaF(result.alphaF() * 0.15); + return result; + }); + static auto rippleColor = style::complex_color([] { + auto result = st::mediaviewTextLinkFg->c; + result.setAlphaF(result.alphaF() * 0.2); + return result; + }); + + auto result = st::mediaSponsoredAbout; + result.textFg = st::mediaviewTextLinkFg; + result.textFgOver = st::mediaviewTextLinkFg; + result.textBg = textBg.color(); + result.textBgOver = textBgOver.color(); + result.ripple.color = rippleColor.color(); + return result; +} + +} // namespace + +class PlaybackSponsored::Message final : public Ui::RpWidget { +public: + Message( + QWidget *parent, + std::shared_ptr show, + const Data::SponsoredMessage &data, + rpl::producer allowCloseAt); + + [[nodiscard]] rpl::producer actions() const; + + void setFinalPosition(int x, int y); + + void fadeIn(); + void fadeOut(Fn hidden); + +private: + void paintEvent(QPaintEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; + + int resizeGetHeight(int newWidth) override; + + void populate(); + void startFadeIn(); + void updateShown(Fn finished = nullptr); + void startFade(Fn finished); + + const not_null _session; + const std::shared_ptr _show; + const Data::SponsoredMessage _data; + + style::RoundButton _aboutSt; + std::unique_ptr _about; + std::unique_ptr _close; + + base::unique_qptr _menu; + rpl::event_stream _actions; + + std::shared_ptr _photo; + Ui::Text::String _title; + Ui::Text::String _text; + + QPoint _finalPosition; + int _left = 0; + int _top = 0; + int _titleHeight = 0; + int _textHeight = 0; + + QImage _cache; + Ui::Animations::Simple _showAnimation; + bool _shown = false; + bool _over = false; + bool _pressed = false; + + rpl::lifetime _photoLifetime; + +}; + +PlaybackSponsored::Message::Message( + QWidget *parent, + std::shared_ptr show, + const Data::SponsoredMessage &data, + rpl::producer allowCloseAt) +: RpWidget(parent) +, _session(&data.history->session()) +, _show(std::move(show)) +, _data(data) +, _aboutSt(PrepareAboutStyle()) +, _about(std::make_unique( + this, + tr::lng_search_sponsored_button(), + _aboutSt)) +, _close( + std::make_unique( + this, + _aboutSt.ripple, + std::move(allowCloseAt))) { + _about->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + setMouseTracking(true); + populate(); + hide(); +} + +rpl::producer PlaybackSponsored::Message::actions() const { + return rpl::merge(_actions.events(), _close->actions()); +} + +void PlaybackSponsored::Message::setFinalPosition(int x, int y) { + _finalPosition = { x, y }; + if (_shown) { + updateShown(); + } +} + +void PlaybackSponsored::Message::fadeIn() { + _shown = true; + if (!_photo || _photo->loaded()) { + startFadeIn(); + return; + } + _photo->owner()->session().downloaderTaskFinished( + ) | rpl::filter([=] { + return _photo->loaded(); + }) | rpl::start_with_next([=] { + _photoLifetime.destroy(); + startFadeIn(); + }, _photoLifetime); +} + +void PlaybackSponsored::Message::startFadeIn() { + if (!_shown) { + return; + } + startFade([=] { + _session->sponsoredMessages().view(_data.randomId); + }); + show(); +} + +void PlaybackSponsored::Message::fadeOut(Fn hidden) { + if (!_shown) { + if (const auto onstack = hidden) { + onstack(); + } + return; + } + _shown = false; + startFade(std::move(hidden)); +} + +void PlaybackSponsored::Message::startFade(Fn finished) { + _cache = Ui::GrabWidgetToImage(this); + _about->hide(); + _close->hide(); + const auto from = _shown ? 0. : 1.; + const auto till = _shown ? 1. : 0.; + _showAnimation.start([=] { + updateShown(finished); + }, from, till, st::fadeWrapDuration); +} + +void PlaybackSponsored::Message::updateShown(Fn finished) { + const auto shown = _showAnimation.value(_shown ? 1. : 0.); + const auto shift = anim::interpolate(st::mediaSponsoredShift, 0, shown); + move(_finalPosition.x(), _finalPosition.y() + shift); + update(); + if (!_showAnimation.animating()) { + _cache = QImage(); + _close->show(); + _about->show(); + if (const auto onstack = finished) { + onstack(); + } + } +} + +void PlaybackSponsored::Message::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + + const auto shown = _showAnimation.value(_shown ? 1. : 0.); + if (!_cache.isNull()) { + p.setOpacity(shown); + p.drawImage(0, 0, _cache); + return; + } + + Ui::FillRoundRect( + p, + rect(), + st::mediaviewSaveMsgBg, + Ui::MediaviewSaveCorners); + + const auto &padding = st::mediaSponsoredPadding; + if (_photo) { + if (const auto image = _photo->image(Data::PhotoSize::Large)) { + const auto size = st::mediaSponsoredThumb; + const auto x = padding.left(); + const auto y = (height() - size) / 2; + p.drawPixmap( + x, + y, + image->pixSingle( + size, + size, + { .options = Images::Option::RoundCircle })); + } + } + + p.setPen(st::mediaviewControlFg); + + _title.draw(p, { + .position = { _left, _top }, + .availableWidth = _about->x() - _left, + .palette = &st::mediaviewTextPalette, + }); + + _text.draw(p, { + .position = { _left, _top + _titleHeight }, + .availableWidth = _close->x() - _left, + .palette = &st::mediaviewTextPalette, + }); +} + +void PlaybackSponsored::Message::mouseMoveEvent(QMouseEvent *e) { + const auto &padding = st::mediaSponsoredPadding; + const auto point = e->pos(); + const auto about = _about->geometry(); + const auto close = _close->geometry(); + const auto over = !about.marginsAdded(padding).contains(point) + && !close.marginsAdded(padding).contains(point); + if (_over != over) { + _over = over; + setCursor(_over ? style::cur_pointer : style::cur_default); + } +} + +void PlaybackSponsored::Message::mousePressEvent(QMouseEvent *e) { + if (_over) { + _pressed = true; + } +} + +void PlaybackSponsored::Message::mouseReleaseEvent(QMouseEvent *e) { + if (base::take(_pressed) && _over) { + _session->sponsoredMessages().clicked(_data.randomId, false, false); + UrlClickHandler::Open(_data.link); + } +} + +int PlaybackSponsored::Message::resizeGetHeight(int newWidth) { + const auto &padding = st::mediaSponsoredPadding; + const auto userpic = st::mediaSponsoredThumb; + const auto innerWidth = newWidth - _left - _close->width(); + const auto titleWidth = innerWidth - _about->width() - padding.right(); + _titleHeight = _title.countHeight(titleWidth); + _textHeight = _text.countHeight(innerWidth); + + const auto use = std::max(_titleHeight + _textHeight, userpic); + + const auto height = padding.top() + use + padding.bottom(); + _left = padding.left() + (_photo ? (userpic + padding.left()) : 0); + _top = padding.top() + (use - _titleHeight - _textHeight) / 2; + + _about->move( + _left + std::min(titleWidth, _title.maxWidth()) + padding.right(), + _top); + _close->move( + newWidth - _close->width(), + (height - _close->height()) / 2); + + return height; +} + +void PlaybackSponsored::Message::populate() { + const auto &from = _data.from; + const auto photo = from.photoId + ? _data.history->owner().photo(from.photoId).get() + : nullptr; + if (photo) { + _photo = photo->createMediaView(); + photo->load({}, LoadFromCloudOrLocal, true); + } + _title = Ui::Text::String( + st::semiboldTextStyle, + from.title, + kDefaultTextOptions, + st::msgMinWidth); + _text = Ui::Text::String( + st::defaultTextStyle, + _data.textWithEntities, + kMarkupTextOptions, + st::msgMinWidth); + + _about->setClickedCallback([=] { + _menu = nullptr; + const auto parent = parentWidget(); + _menu = base::make_unique_q( + parent, + st::mediaviewPopupMenu); + const auto raw = _menu.get(); + const auto addAction = Ui::Menu::CreateAddActionCallback(raw); + Menu::FillSponsored( + addAction, + _show, + Menu::SponsoredPhrases::Channel, + _session->sponsoredMessages().lookupDetails(_data), + _session->sponsoredMessages().createReportCallback( + _data.randomId, + crl::guard(this, [=] { _actions.fire(Action::Close); })), + { .dark = true }); + _actions.fire(Action::Pause); + Ui::Connect(raw, &QObject::destroyed, this, [=] { + _actions.fire(Action::Unpause); + }); + raw->popup(QCursor::pos()); + }); +} + +PlaybackSponsored::PlaybackSponsored( + not_null controls, + std::shared_ptr show, + not_null item) +: _parent(controls->parentWidget()) +, _session(&item->history()->session()) +, _show(std::move(show)) +, _itemId(item->fullId()) +, _controlsGeometry(controls->geometryValue()) +, _timer([=] { update(); }) { + _session->sponsoredMessages().requestForVideo(item, crl::guard(this, [=]( + Data::SponsoredForVideo data) { + if (data.list.empty()) { + return; + } + _data = std::move(data); + if (_data->state.initial() + || (_data->state.itemIndex > _data->list.size()) + || (_data->state.itemIndex == _data->list.size() + && _data->state.leftTillShow <= 0)) { + _data->state.itemIndex = 0; + _data->state.leftTillShow = std::max( + _data->startDelay, + kStartDelayMin); + } + update(); + })); +} + +PlaybackSponsored::~PlaybackSponsored() { + saveState(); +} + +void PlaybackSponsored::start() { + _started = true; + if (!_paused) { + _start = crl::now(); + update(); + } +} + +void PlaybackSponsored::setPaused(bool paused) { + setPausedOutside(paused); +} + +void PlaybackSponsored::updatePaused() { + const auto paused = _pausedInside || _pausedOutside; + if (_paused == paused) { + return; + } else if (_started && paused) { + update(); + } + _paused = paused; + if (!_started) { + return; + } else if (_paused) { + _start = 0; + _timer.cancel(); + _allowCloseAt = 0; + } else { + _start = crl::now(); + update(); + } +} + +void PlaybackSponsored::setPausedInside(bool paused) { + if (_pausedInside == paused) { + return; + } + _pausedInside = paused; + updatePaused(); +} + +void PlaybackSponsored::setPausedOutside(bool paused) { + if (_pausedOutside == paused) { + return; + } + _pausedOutside = paused; + updatePaused(); +} + +void PlaybackSponsored::finish() { + _timer.cancel(); + if (_data) { + saveState(); + _data = std::nullopt; + } +} + +void PlaybackSponsored::update() { + if (!_data || !_start) { + return; + } + + const auto [now, state] = computeState(); + const auto message = (_data->state.itemIndex < _data->list.size()) + ? &_data->list[state.itemIndex] + : nullptr; + const auto duration = message + ? std::max( + message->durationMin + kDurationMin, + message->durationMax) + : crl::time(0); + if (_data->state.leftTillShow > 0 && state.leftTillShow <= 0) { + _data->state.leftTillShow = 0; + if (duration) { + _allowCloseAt = now + message->durationMin; + show(*message); + + _start = now; + _timer.callOnce(duration); + saveState(); + } else { + finish(); + } + } else if (_data->state.leftTillShow <= 0 + && state.leftTillShow <= -duration) { + hide(now); + } else { + if (state.leftTillShow <= 0 && duration) { + _allowCloseAt = now + state.leftTillShow + message->durationMin; + if (!_widget) { + show(*message); + } + } + _data->state = state; + _timer.callOnce((state.leftTillShow > 0) + ? state.leftTillShow + : (state.leftTillShow + duration)); + } +} + + +void PlaybackSponsored::show(const Data::SponsoredMessage &data) { + _widget = std::make_unique( + _parent, + _show, + data, + _allowCloseAt.value()); + const auto raw = _widget.get(); + + _controlsGeometry.value() | rpl::start_with_next([=](QRect controls) { + raw->resizeToWidth(controls.width()); + raw->setFinalPosition( + controls.x(), + controls.y() - st::mediaSponsoredSkip - raw->height()); + }, raw->lifetime()); + + raw->actions() | rpl::start_with_next([=](Action action) { + switch (action) { + case Action::Close: hide(crl::now()); break; + case Action::PromotePremium: showPremiumPromo(); break; + case Action::Pause: setPausedInside(true); break; + case Action::Unpause: setPausedInside(false); break; + } + }, raw->lifetime()); + + raw->fadeIn(); +} + +void PlaybackSponsored::showPremiumPromo() { + ShowPremiumPreviewBox(_show, PremiumFeature::NoAds); +} + +void PlaybackSponsored::hide(crl::time now) { + Expects(_widget != nullptr); + + _widget->fadeOut([this, raw = _widget.get()] { + if (_widget.get() == raw) { + _widget = nullptr; + } + }); + + ++_data->state.itemIndex; + _data->state.leftTillShow = std::max( + _data->betweenDelay, + kStartDelayMin); + _start = now; + _timer.callOnce(_data->state.leftTillShow); + saveState(); +} + +void PlaybackSponsored::saveState() { + _session->sponsoredMessages().updateForVideo( + _itemId, + computeState().data); +} + +PlaybackSponsored::State PlaybackSponsored::computeState() const { + auto result = State{ crl::now() }; + if (!_data) { + return result; + } + result.data = _data->state; + if (!_start) { + return result; + } + const auto elapsed = result.now - _start; + result.data.leftTillShow -= elapsed; + return result; +} + +rpl::lifetime &PlaybackSponsored::lifetime() { + return _lifetime; +} + +bool PlaybackSponsored::Has(HistoryItem *item) { + return item + && item->history()->session().sponsoredMessages().canHaveFor(item); +} + +} // namespace Media::View diff --git a/Telegram/SourceFiles/media/view/media_view_playback_sponsored.h b/Telegram/SourceFiles/media/view/media_view_playback_sponsored.h new file mode 100644 index 0000000000..84fe4b9c42 --- /dev/null +++ b/Telegram/SourceFiles/media/view/media_view_playback_sponsored.h @@ -0,0 +1,83 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "base/timer.h" +#include "base/weak_ptr.h" +#include "data/components/sponsored_messages.h" + +namespace ChatHelpers { +class Show; +} // namespace ChatHelpers + +namespace Main { +class Session; +} // namespace Main + +namespace Ui { +class RpWidget; +} // namespace Ui + +namespace Media::View { + +class PlaybackSponsored final : public base::has_weak_ptr { +public: + PlaybackSponsored( + not_null controls, + std::shared_ptr show, + not_null item); + ~PlaybackSponsored(); + + void start(); + void setPaused(bool paused); + + [[nodiscard]] rpl::lifetime &lifetime(); + + [[nodiscard]] static bool Has(HistoryItem *item); + +private: + class Message; + struct State { + crl::time now = 0; + Data::SponsoredForVideoState data; + }; + + void update(); + void finish(); + void updatePaused(); + void showPremiumPromo(); + void setPausedInside(bool paused); + void setPausedOutside(bool paused); + void show(const Data::SponsoredMessage &data); + void hide(crl::time now); + [[nodiscard]] State computeState() const; + void saveState(); + + const not_null _parent; + const not_null _session; + const std::shared_ptr _show; + const FullMsgId _itemId; + + rpl::variable _controlsGeometry; + std::unique_ptr _widget; + + rpl::variable _allowCloseAt; + crl::time _start = 0; + bool _started = false; + bool _paused = false; + bool _pausedInside = false; + bool _pausedOutside = false; + base::Timer _timer; + + std::optional _data; + + rpl::lifetime _lifetime; + +}; + +} // namespace Media::View diff --git a/Telegram/SourceFiles/menu/menu_sponsored.cpp b/Telegram/SourceFiles/menu/menu_sponsored.cpp index ec65ef28db..2073239170 100644 --- a/Telegram/SourceFiles/menu/menu_sponsored.cpp +++ b/Telegram/SourceFiles/menu/menu_sponsored.cpp @@ -287,14 +287,12 @@ void AboutBox( top->setForceRippled(false); }); FillSponsored( - top, Ui::Menu::CreateAddActionCallback(menu->get()), show, phrases, details, report, - false, - true); + { .skipAbout = true }); const auto global = top->mapToGlobal( QPoint(top->width() / 4 * 3, top->height() / 2)); raw->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight); @@ -390,18 +388,17 @@ void ShowReportSponsoredBox( } // namespace void FillSponsored( - not_null parent, const Ui::Menu::MenuCallback &addAction, std::shared_ptr show, SponsoredPhrases phrases, const Data::SponsoredMessages::Details &details, Data::SponsoredReportAction report, - bool mediaViewer, - bool skipAbout) { + SponsoredMenuSettings settings) { const auto session = &show->session(); const auto &info = details.info; + const auto dark = settings.dark; - if (!mediaViewer && !info.empty()) { + if (!settings.skipInfo && !info.empty()) { auto fillSubmenu = [&](not_null menu) { const auto allText = ranges::accumulate( info, @@ -416,8 +413,10 @@ void FillSponsored( for (const auto &i : info) { auto item = base::make_unique_q( menu, - st::defaultMenu, - st::historySponsorInfoItem, + dark ? st::storiesMenu : st::defaultMenu, + (dark + ? st::historySponsorInfoItemDark + : st::historySponsorInfoItem), st::historyHasCustomEmojiPosition, base::duplicate(i)); item->clicks( @@ -431,27 +430,31 @@ void FillSponsored( addAction({ .text = tr::lng_sponsored_info_menu(tr::now), .handler = nullptr, - .icon = &st::menuIconChannel, + .icon = (dark + ? &st::mediaMenuIconChannel + : &st::menuIconChannel), .fillSubmenu = std::move(fillSubmenu), }); addAction({ - .separatorSt = &st::expandedMenuSeparator, + .separatorSt = (dark + ? &st::mediaviewMenuSeparator + : &st::expandedMenuSeparator), .isSeparator = true, }); } if (details.canReport) { - if (!skipAbout) { + if (!settings.skipAbout) { addAction(tr::lng_sponsored_menu_revenued_about(tr::now), [=] { show->show(Box(AboutBox, show, phrases, details, report)); - }, (mediaViewer ? &st::mediaMenuIconInfo : &st::menuIconInfo)); + }, (dark ? &st::mediaMenuIconInfo : &st::menuIconInfo)); } addAction(tr::lng_sponsored_menu_revenued_report(tr::now), [=] { ShowReportSponsoredBox(show, report); - }, (mediaViewer ? &st::mediaMenuIconBlock : &st::menuIconBlock)); + }, (dark ? &st::mediaMenuIconBlock : &st::menuIconBlock)); addAction({ - .separatorSt = (mediaViewer + .separatorSt = (dark ? &st::mediaviewMenuSeparator : &st::expandedMenuSeparator), .isSeparator = true, @@ -464,26 +467,22 @@ void FillSponsored( } else { ShowPremiumPreviewBox(show, PremiumFeature::NoAds); } - }, (mediaViewer ? &st::mediaMenuIconCancel : &st::menuIconCancel)); + }, (dark ? &st::mediaMenuIconCancel : &st::menuIconCancel)); } void FillSponsored( - not_null parent, const Ui::Menu::MenuCallback &addAction, std::shared_ptr show, const FullMsgId &fullId, - bool mediaViewer, - bool skipAbout) { + SponsoredMenuSettings settings) { const auto session = &show->session(); FillSponsored( - parent, addAction, show, PhrasesForMessage(fullId), session->sponsoredMessages().lookupDetails(fullId), session->sponsoredMessages().createReportCallback(fullId), - mediaViewer, - skipAbout); + settings); } void ShowSponsored( @@ -495,11 +494,9 @@ void ShowSponsored( st::popupMenuWithIcons); FillSponsored( - parent, Ui::Menu::CreateAddActionCallback(menu), show, - fullId, - false); + fullId); menu->popup(QCursor::pos()); } diff --git a/Telegram/SourceFiles/menu/menu_sponsored.h b/Telegram/SourceFiles/menu/menu_sponsored.h index 106509e2e6..987630e4bb 100644 --- a/Telegram/SourceFiles/menu/menu_sponsored.h +++ b/Telegram/SourceFiles/menu/menu_sponsored.h @@ -33,23 +33,25 @@ enum class SponsoredPhrases { Search, }; +struct SponsoredMenuSettings { + bool dark = false; + bool skipAbout = false; + bool skipInfo = false; +}; + void FillSponsored( - not_null parent, const Ui::Menu::MenuCallback &addAction, std::shared_ptr show, SponsoredPhrases phrases, const Data::SponsoredMessageDetails &details, Data::SponsoredReportAction report, - bool mediaViewer, - bool skipAbout); + SponsoredMenuSettings settings = {}); void FillSponsored( - not_null parent, const Ui::Menu::MenuCallback &addAction, std::shared_ptr show, const FullMsgId &fullId, - bool mediaViewer, - bool skipAbout = false); + SponsoredMenuSettings settings = {}); void ShowSponsored( not_null parent, diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index e33dd8366b..c6e4c22b2c 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -1344,7 +1344,7 @@ messages.messageViews#b6c4f543 views:Vector chats:Vector use messages.discussionMessage#a6341782 flags:# messages:Vector max_id:flags.0?int read_inbox_max_id:flags.1?int read_outbox_max_id:flags.2?int unread_count:int chats:Vector users:Vector = messages.DiscussionMessage; -messageReplyHeader#afbc09db flags:# reply_to_scheduled:flags.2?true forum_topic:flags.3?true quote:flags.9?true reply_to_msg_id:flags.4?int reply_to_peer_id:flags.0?Peer reply_from:flags.5?MessageFwdHeader reply_media:flags.8?MessageMedia reply_to_top_id:flags.1?int quote_text:flags.6?string quote_entities:flags.7?Vector quote_offset:flags.10?int = MessageReplyHeader; +messageReplyHeader#6917560b flags:# reply_to_scheduled:flags.2?true forum_topic:flags.3?true quote:flags.9?true reply_to_msg_id:flags.4?int reply_to_peer_id:flags.0?Peer reply_from:flags.5?MessageFwdHeader reply_media:flags.8?MessageMedia reply_to_top_id:flags.1?int quote_text:flags.6?string quote_entities:flags.7?Vector quote_offset:flags.10?int todo_item_id:flags.11?int = MessageReplyHeader; messageReplyStoryHeader#e5af939 peer:Peer story_id:int = MessageReplyHeader; messageReplies#83d60fc2 flags:# comments:flags.0?true replies:int replies_pts:int recent_repliers:flags.1?Vector channel_id:flags.0?long max_id:flags.2?int read_max_id:flags.3?int = MessageReplies; @@ -1649,7 +1649,7 @@ stories.storyViewsList#59d78fc5 flags:# count:int views_count:int forwards_count stories.storyViews#de9eed1d views:Vector users:Vector = stories.StoryViews; -inputReplyToMessage#b07038b0 flags:# reply_to_msg_id:int top_msg_id:flags.0?int reply_to_peer_id:flags.1?InputPeer quote_text:flags.2?string quote_entities:flags.3?Vector quote_offset:flags.4?int monoforum_peer_id:flags.5?InputPeer = InputReplyTo; +inputReplyToMessage#869fbe10 flags:# reply_to_msg_id:int top_msg_id:flags.0?int reply_to_peer_id:flags.1?InputPeer quote_text:flags.2?string quote_entities:flags.3?Vector quote_offset:flags.4?int monoforum_peer_id:flags.5?InputPeer todo_item_id:flags.6?int = InputReplyTo; inputReplyToStory#5881323a peer:InputPeer story_id:int = InputReplyTo; inputReplyToMonoForum#69d66c45 monoforum_peer_id:InputPeer = InputReplyTo; @@ -2720,4 +2720,4 @@ smsjobs.finishJob#4f1ebf24 flags:# job_id:string error:flags.0?string = Bool; fragment.getCollectibleInfo#be1e85ba collectible:InputCollectible = fragment.CollectibleInfo; -// LAYER 207 +// LAYER 209 diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp index afb9b9c64b..2850e85a01 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp @@ -939,7 +939,8 @@ void FillUniqueGiftMenu( && e.id.isEmpty() && (e.in || (giftChannel && giftChannel->canManageGifts())) && !e.giftTransferred - && !e.giftRefunded; + && !e.giftRefunded + && !e.converted; const auto unique = e.uniqueGift; if (unique @@ -1148,7 +1149,6 @@ void GenericCreditsEntryBox( const auto isStarGift = e.stargift || e.soldOutInfo; const auto creditsHistoryStarGift = isStarGift && !e.id.isEmpty(); const auto sentStarGift = creditsHistoryStarGift && !e.in; - const auto convertedStarGift = creditsHistoryStarGift && e.converted; const auto giftToSelf = isStarGift && (e.barePeerId == selfPeerId) && (e.in || e.bareGiftOwnerId == selfPeerId); @@ -1164,7 +1164,8 @@ void GenericCreditsEntryBox( const auto starGiftCanManage = isStarGift && !creditsHistoryStarGift && (e.in || giftToChannelCanManage) - && !e.fromGiftSlug; + && !e.fromGiftSlug + && !e.converted; const auto starGiftCanTransfer = isStarGift && !creditsHistoryStarGift && (e.in || giftToChannelCanTransfer); @@ -1250,12 +1251,13 @@ void GenericCreditsEntryBox( EntryToSavedStarGiftId(session, e), style); }; + const auto canResell = CanResellGift(session, e); AddUniqueGiftCover( content, rpl::single(*uniqueGift), {}, std::move(price), - CanResellGift(session, e) ? std::move(change) : Fn()); + canResell ? std::move(change) : Fn()); AddSkip(content, st::defaultVerticalListSkip * 2); @@ -1263,6 +1265,10 @@ void GenericCreditsEntryBox( const auto type = SavedStarGiftMenuType::View; FillUniqueGiftMenu(show, menu, e, type, st); }); + + if (canResell) { + Ui::PreloadUniqueGiftResellPrices(session); + } } else if (const auto callback = Ui::PaintPreviewCallback(session, e)) { const auto thumb = content->add(object_ptr>( content, @@ -1419,7 +1425,7 @@ void GenericCreditsEntryBox( ? tr::lng_credits_box_history_entry_gift_unavailable(tr::now) : sentStarGift ? tr::lng_credits_box_history_entry_gift_sent(tr::now) - : convertedStarGift + : e.converted ? tr::lng_credits_box_history_entry_gift_converted(tr::now) : (isStarGift && !starGiftCanManage) ? tr::lng_gift_link_label_gift(tr::now) @@ -1622,7 +1628,7 @@ void GenericCreditsEntryBox( } const auto arrow = Ui::Text::IconEmoji(&st::textMoreIconEmoji); - if (!uniqueGift && starGiftCanManage) { + if (!uniqueGift && (starGiftCanManage || e.converted)) { Ui::AddSkip(content); const auto about = box->addRow( object_ptr>( @@ -1751,7 +1757,8 @@ void GenericCreditsEntryBox( const auto canToggle = starGiftCanManage && !e.giftTransferred - && !e.giftRefunded; + && !e.giftRefunded + && !e.converted; const auto toggleVisibility = [=, weak = Ui::MakeWeak(box)](bool save) { const auto showSection = !e.fromGiftsList; const auto savedId = EntryToSavedStarGiftId(&show->session(), e); diff --git a/Telegram/SourceFiles/settings/settings_main.cpp b/Telegram/SourceFiles/settings/settings_main.cpp index ef7252be56..e4a5ae6045 100644 --- a/Telegram/SourceFiles/settings/settings_main.cpp +++ b/Telegram/SourceFiles/settings/settings_main.cpp @@ -843,7 +843,6 @@ void SetupPremium( button->addClickHandler([=] { showOther(BusinessId()); }); - Ui::NewBadge::AddToRight(button); if (controller->session().premiumCanBuy()) { const auto button = AddButtonWithIcon( @@ -852,6 +851,8 @@ void SetupPremium( st::settingsButton, { .icon = &st::menuIconGiftPremium } ); + Ui::NewBadge::AddToRight(button); + button->addClickHandler([=] { Ui::ChooseStarGiftRecipient(controller); }); diff --git a/Telegram/SourceFiles/statistics/widgets/point_details_widget.cpp b/Telegram/SourceFiles/statistics/widgets/point_details_widget.cpp index 97704994aa..c000d919b9 100644 --- a/Telegram/SourceFiles/statistics/widgets/point_details_widget.cpp +++ b/Telegram/SourceFiles/statistics/widgets/point_details_widget.cpp @@ -324,9 +324,8 @@ void PointDetailsWidget::setXIndex(int xIndex) { nullptr, { float64(xIndex), float64(xIndex) }).parts : std::vector(); - const auto multiplier = float64(kOneStarInNano); const auto isCredits - = _chartData.currency == Data::StatisticalCurrency::Credits; + = (_chartData.currency == Data::StatisticalCurrency::Credits); for (auto i = 0; i < _chartData.lines.size(); i++) { const auto &dataLine = _chartData.lines[i]; auto textLine = Line(); @@ -350,19 +349,23 @@ void PointDetailsWidget::setXIndex(int xIndex) { ? tr::lng_channel_earn_chart_overriden_detail_credits : tr::lng_channel_earn_chart_overriden_detail_currency)( tr::now)); + const auto provided = dataLine.y[xIndex]; + const auto value = isCredits + ? CreditsAmount(provided, CreditsType::Stars) + : CreditsAmount( + provided / kOneStarInNano, + provided % kOneStarInNano, + CreditsType::Ton); copy.value.setText( _textStyle, - Lang::FormatExactCountDecimal( - dataLine.y[xIndex] / multiplier)); + Lang::FormatCreditsAmountDecimal(value)); _lines.push_back(std::move(copy)); textLine.name.setText( _textStyle, tr::lng_channel_earn_chart_overriden_detail_usd(tr::now)); textLine.value.setText( _textStyle, - Info::ChannelEarn::ToUsd( - dataLine.y[xIndex] / multiplier, - _chartData.currencyRate, 0)); + Info::ChannelEarn::ToUsd(value, _chartData.currencyRate, 0)); } _lines.push_back(std::move(textLine)); } diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index c45b937ab7..c5d072ac11 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -947,6 +947,9 @@ historySponsorInfoItem: FlatLabel(defaultFlatLabel) { minWidth: 136px; maxHeight: 120px; } +historySponsorInfoItemDark: FlatLabel(historySponsorInfoItem) { + textFg: mediaviewControlFg; +} historyHasCustomEmoji: FlatLabel(defaultFlatLabel) { style: TextStyle(defaultTextStyle) { font: font(11px); diff --git a/Telegram/SourceFiles/ui/chat/chat_style.h b/Telegram/SourceFiles/ui/chat/chat_style.h index 606cf1d74c..4e75c5cecf 100644 --- a/Telegram/SourceFiles/ui/chat/chat_style.h +++ b/Telegram/SourceFiles/ui/chat/chat_style.h @@ -156,6 +156,7 @@ struct ChatPaintHighlight { float64 opacity = 0.; float64 collapsion = 0.; TextSelection range; + int todoItemId = 0; }; struct ChatPaintContext { diff --git a/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.cpp b/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.cpp index a057463507..68c64708d7 100644 --- a/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.cpp +++ b/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.cpp @@ -394,7 +394,7 @@ void SubsectionSlider::activate(int index) { } } -void SubsectionSlider::setActiveSectionFast(int active) { +void SubsectionSlider::setActiveSectionFast(int active, bool ignoreScroll) { Expects(active < int(_tabs.size())); if (_active == active) { @@ -403,8 +403,10 @@ void SubsectionSlider::setActiveSectionFast(int active) { _active = active; _activeFrom.stop(); _activeSize.stop(); - const auto now = getFinalActiveRange(); - _requestShown.fire({ now.from, now.from + now.size }); + if (_active >= 0 && !ignoreScroll) { + const auto now = getFinalActiveRange(); + _requestShown.fire({ now.from, now.from + now.size }); + } _bar->update(); } @@ -425,6 +427,7 @@ rpl::producer SubsectionSlider::sectionContextMenu() const { } int SubsectionSlider::lookupSectionPosition(int index) const { + Expects(!_tabs.empty()); Expects(index >= 0 && index < _tabs.size()); return _vertical ? _tabs[index]->y() : _tabs[index]->x(); diff --git a/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.h b/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.h index 2c82684ef9..704169b92f 100644 --- a/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.h +++ b/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.h @@ -81,7 +81,7 @@ public: void setSections( SubsectionTabs sections, Fn paused); - void setActiveSectionFast(int active); + void setActiveSectionFast(int active, bool ignoreScroll = false); [[nodiscard]] int sectionsCount() const; [[nodiscard]] rpl::producer sectionActivated() const; diff --git a/Telegram/SourceFiles/ui/menu_icons.style b/Telegram/SourceFiles/ui/menu_icons.style index fcd6d2bbb8..3c37626681 100644 --- a/Telegram/SourceFiles/ui/menu_icons.style +++ b/Telegram/SourceFiles/ui/menu_icons.style @@ -185,6 +185,7 @@ menuIconPayment: icon {{ "payments/payment_card", menuIconColor }}; menuIconOrderPrice: icon {{ "menu/order_price", menuIconColor }}; menuIconOrderDate: icon {{ "menu/order_date", menuIconColor }}; menuIconOrderNumber: icon {{ "menu/order_number", menuIconColor }}; +menuIconAdd: icon{{ "menu/add", menuIconColor }}; menuIconTTLAny: icon {{ "menu/auto_delete_plain", menuIconColor }}; menuIconTTLAnyTextPosition: point(11px, 22px); @@ -204,6 +205,7 @@ menuBlueIconGroupCreate: icon {{ "menu/groups_create", lightButtonFg }}; mediaMenuIconStickers: icon {{ "menu/stickers", mediaviewMenuFg }}; mediaMenuIconCancel: icon {{ "menu/cancel", mediaviewMenuFg }}; +mediaMenuIconChannel: icon {{ "menu/channel", mediaviewMenuFg }}; mediaMenuIconShowInChat: icon {{ "menu/show_in_chat", mediaviewMenuFg }}; mediaMenuIconShowInFolder: icon {{ "menu/show_in_folder", mediaviewMenuFg }}; mediaMenuIconDownload: icon {{ "menu/download", mediaviewMenuFg }}; diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index fa9565e9d6..b2705ad026 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -3835,7 +3835,7 @@ void PeerMenuConfirmToggleFee( MTP_flags((refund ? Flag::f_refund_charged : Flag()) | (removeFee ? Flag() : Flag::f_require_payment) | (parent ? Flag::f_parent_peer : Flag())), - parent->input, + (parent ? parent->input : MTPInputPeer()), user->inputUser )).done([=] { if (!parent) { diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index c849a249f9..80ddb4607d 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -53,6 +53,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_peer_values.h" #include "data/data_premium_limits.h" #include "data/data_web_page.h" +#include "dialogs/ui/chat_search_in.h" #include "passport/passport_form_controller.h" #include "chat_helpers/tabbed_selector.h" #include "chat_helpers/emoji_interactions.h" @@ -361,6 +362,14 @@ void DateClickHandler::onClick(ClickContext context) const { } } +MessageHighlightId SearchHighlightId(const QString &query) { + auto result = MessageHighlightId{ .quote = { query } }; + if (!result.quote.empty()) { + result.quoteOffset = kSearchQueryOffsetHint; + } + return result; +} + SessionNavigation::SessionNavigation(not_null session) : _session(session) , _api(&_session->mtp()) { @@ -1149,8 +1158,7 @@ void SessionNavigation::showRepliesForMessage( .repliesRootId = rootId, }, commentId, - params.highlightPart, - params.highlightPartOffsetHint); + params.highlight); memento->setFromTopic(topic); showSection(std::move(memento), params); return; @@ -1272,8 +1280,7 @@ void SessionNavigation::showSublist( .sublist = sublist, }, itemId, - params.highlightPart, - params.highlightPartOffsetHint); + params.highlight); showSection(std::move(memento), params); } @@ -1811,10 +1818,13 @@ void SessionController::activateFirstChatsFilter() { } } -bool SessionController::uniqueChatsInSearchResults() const { +bool SessionController::uniqueChatsInSearchResults( + const Dialogs::SearchState &state) const { + const auto global = (state.tab == Dialogs::ChatSearchTab::MyMessages) + || (state.tab == Dialogs::ChatSearchTab::PublicPosts); return session().supportMode() && !session().settings().supportAllSearchResults() - && !_searchInChat.current(); + && (global || !state.inChat); } bool SessionController::openFolderInDifferentWindow( diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h index 9af6714b60..7bde45c28d 100644 --- a/Telegram/SourceFiles/window/window_session_controller.h +++ b/Telegram/SourceFiles/window/window_session_controller.h @@ -30,6 +30,10 @@ class SavedMessages; enum class StorySourcesList : uchar; } // namespace Data +namespace Dialogs { +struct SearchState; +} // namespace Dialogs + namespace ChatHelpers { class TabbedSelector; class EmojiInteractions; @@ -162,8 +166,9 @@ struct SectionShow { return copy; } - TextWithEntities highlightPart; + MessageHighlightId highlight; int highlightPartOffsetHint = 0; + int highlightTodoItemId = 0; std::optional videoTimestamp; Way way = Way::Forward; anim::type animated = anim::type::normal; @@ -178,6 +183,8 @@ struct SectionShow { }; +[[nodiscard]] MessageHighlightId SearchHighlightId(const QString &query); + class SessionController; class SessionNavigation : public base::has_weak_ptr { @@ -404,7 +411,7 @@ public: void setSearchInChat(Dialogs::Key value) { _searchInChat = value; } - bool uniqueChatsInSearchResults() const; + bool uniqueChatsInSearchResults(const Dialogs::SearchState &state) const; void openFolder(not_null folder); void closeFolder(); diff --git a/Telegram/ThirdParty/tgcalls b/Telegram/ThirdParty/tgcalls index 1348de6aa6..d78b0507c5 160000 --- a/Telegram/ThirdParty/tgcalls +++ b/Telegram/ThirdParty/tgcalls @@ -1 +1 @@ -Subproject commit 1348de6aa6c07ed32354d3e26423c45304000a39 +Subproject commit d78b0507c54d76d5fe9691c8efe2638dee2c1536 diff --git a/Telegram/build/version b/Telegram/build/version index 4d8e1cfc6a..ea436f7a45 100644 --- a/Telegram/build/version +++ b/Telegram/build/version @@ -1,7 +1,7 @@ -AppVersion 5016003 +AppVersion 5016004 AppVersionStrMajor 5.16 -AppVersionStrSmall 5.16.3 -AppVersionStr 5.16.3 +AppVersionStrSmall 5.16.4 +AppVersionStr 5.16.4 BetaChannel 0 AlphaVersion 0 -AppVersionOriginal 5.16.3 +AppVersionOriginal 5.16.4 diff --git a/changelog.txt b/changelog.txt index 3474436d1e..ac227269eb 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,9 @@ +5.16.4 (11.07.25) + +- Fix problem with negative unread counters. +- Fix stars values display in statistics. +- Fix crash in messages fee disabling. + 5.16.3 (08.07.25) - Allow removing / charging fee in channel direct messages. diff --git a/cmake b/cmake index b032f270b6..f3d6471bd5 160000 --- a/cmake +++ b/cmake @@ -1 +1 @@ -Subproject commit b032f270b622610ca3f42a83f37b3a183c9da0da +Subproject commit f3d6471bd58dbad727d4f8fbccd0fb36632eee9e