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