Merge tag 'v5.16.4' into dev

This commit is contained in:
AlexeyZavar 2025-07-15 16:47:25 +03:00
commit 5270f155ff
91 changed files with 2366 additions and 606 deletions

View file

@ -1339,6 +1339,8 @@ PRIVATE
media/view/media_view_playback_controls.h media/view/media_view_playback_controls.h
media/view/media_view_playback_progress.cpp media/view/media_view_playback_progress.cpp
media/view/media_view_playback_progress.h 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.h
media/system_media_controls_manager.cpp media/system_media_controls_manager.cpp
menu/menu_antispam_validator.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-ProcessThreads-l1-1-0.dll
/DELAYLOAD:API-MS-Win-Core-Synch-l1-2-0.dll # Synchronization.lib /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-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-l1-1-0.dll
/DELAYLOAD:API-MS-Win-Core-WinRT-Error-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 /DELAYLOAD:API-MS-Win-Core-WinRT-String-l1-1-0.dll

Binary file not shown.

View file

Before

Width:  |  Height:  |  Size: 470 B

After

Width:  |  Height:  |  Size: 470 B

View file

Before

Width:  |  Height:  |  Size: 899 B

After

Width:  |  Height:  |  Size: 899 B

View file

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -434,6 +434,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_dlg_new_channel_name" = "Channel name"; "lng_dlg_new_channel_name" = "Channel name";
"lng_dlg_new_bot_name" = "Bot name"; "lng_dlg_new_bot_name" = "Bot name";
"lng_no_chats" = "Your chats will be here"; "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_chats_filter" = "No chats currently belong to this folder.";
"lng_no_saved_sublists" = "You can save messages from other chats here."; "lng_no_saved_sublists" = "You can save messages from other chats here.";
"lng_contacts_loading" = "Loading..."; "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_to_msg" = "Go To Message";
"lng_context_reply_msg" = "Reply"; "lng_context_reply_msg" = "Reply";
"lng_context_quote_and_reply" = "Quote & Reply"; "lng_context_quote_and_reply" = "Quote & Reply";
"lng_context_reply_to_task" = "Reply to Task";
"lng_context_edit_msg" = "Edit"; "lng_context_edit_msg" = "Edit";
"lng_context_add_factcheck" = "Add Fact Check"; "lng_context_add_factcheck" = "Add Fact Check";
"lng_context_edit_factcheck" = "Edit 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_inline_switch_cant" = "Sorry, no way to write here :(";
"lng_preview_reply_to" = "Reply to {name}"; "lng_preview_reply_to" = "Reply to {name}";
"lng_preview_reply_to_quote" = "Reply to quote from {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_title" = "Suggest a Post Below";
"lng_suggest_bar_text" = "Click to offer a price for publishing."; "lng_suggest_bar_text" = "Click to offer a price for publishing.";

View file

@ -38,6 +38,7 @@
<file alias="topics_tabs.tgs">../../animations/edit_peers/topics_tabs.tgs</file> <file alias="topics_tabs.tgs">../../animations/edit_peers/topics_tabs.tgs</file>
<file alias="topics_list.tgs">../../animations/edit_peers/topics_list.tgs</file> <file alias="topics_list.tgs">../../animations/edit_peers/topics_list.tgs</file>
<file alias="direct_messages.tgs">../../animations/edit_peers/direct_messages.tgs</file> <file alias="direct_messages.tgs">../../animations/edit_peers/direct_messages.tgs</file>
<file alias="no_chats.tgs">../../animations/no_chats.tgs</file>
<file alias="dice_idle.tgs">../../animations/dice/dice_idle.tgs</file> <file alias="dice_idle.tgs">../../animations/dice/dice_idle.tgs</file>
<file alias="dart_idle.tgs">../../animations/dice/dart_idle.tgs</file> <file alias="dart_idle.tgs">../../animations/dice/dart_idle.tgs</file>

View file

@ -10,7 +10,7 @@
<Identity Name="TelegramMessengerLLP.TelegramDesktop" <Identity Name="TelegramMessengerLLP.TelegramDesktop"
ProcessorArchitecture="ARCHITECTURE" ProcessorArchitecture="ARCHITECTURE"
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A" Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
Version="5.16.3.0" /> Version="5.16.4.0" />
<Properties> <Properties>
<DisplayName>Telegram Desktop</DisplayName> <DisplayName>Telegram Desktop</DisplayName>
<PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName> <PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName>

View file

@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
// //
VS_VERSION_INFO VERSIONINFO VS_VERSION_INFO VERSIONINFO
FILEVERSION 5,16,3,0 FILEVERSION 5,16,4,0
PRODUCTVERSION 5,16,3,0 PRODUCTVERSION 5,16,4,0
FILEFLAGSMASK 0x3fL FILEFLAGSMASK 0x3fL
#ifdef _DEBUG #ifdef _DEBUG
FILEFLAGS 0x1L FILEFLAGS 0x1L
@ -62,10 +62,10 @@ BEGIN
BEGIN BEGIN
VALUE "CompanyName", "Radolyn Labs" VALUE "CompanyName", "Radolyn Labs"
VALUE "FileDescription", "AyuGram Desktop" VALUE "FileDescription", "AyuGram Desktop"
VALUE "FileVersion", "5.16.3.0" VALUE "FileVersion", "5.16.4.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2025" VALUE "LegalCopyright", "Copyright (C) 2014-2025"
VALUE "ProductName", "AyuGram Desktop" VALUE "ProductName", "AyuGram Desktop"
VALUE "ProductVersion", "5.16.3.0" VALUE "ProductVersion", "5.16.4.0"
END END
END END
BLOCK "VarFileInfo" BLOCK "VarFileInfo"

View file

@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
// //
VS_VERSION_INFO VERSIONINFO VS_VERSION_INFO VERSIONINFO
FILEVERSION 5,16,3,0 FILEVERSION 5,16,4,0
PRODUCTVERSION 5,16,3,0 PRODUCTVERSION 5,16,4,0
FILEFLAGSMASK 0x3fL FILEFLAGSMASK 0x3fL
#ifdef _DEBUG #ifdef _DEBUG
FILEFLAGS 0x1L FILEFLAGS 0x1L
@ -53,10 +53,10 @@ BEGIN
BEGIN BEGIN
VALUE "CompanyName", "Radolyn Labs" VALUE "CompanyName", "Radolyn Labs"
VALUE "FileDescription", "AyuGram Desktop Updater" VALUE "FileDescription", "AyuGram Desktop Updater"
VALUE "FileVersion", "5.16.3.0" VALUE "FileVersion", "5.16.4.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2025" VALUE "LegalCopyright", "Copyright (C) 2014-2025"
VALUE "ProductName", "AyuGram Desktop" VALUE "ProductName", "AyuGram Desktop"
VALUE "ProductVersion", "5.16.3.0" VALUE "ProductVersion", "5.16.4.0"
END END
END END
BLOCK "VarFileInfo" BLOCK "VarFileInfo"

View file

@ -111,6 +111,13 @@ void AddProxyFromClipboard(
QGuiApplication::clipboard()->text()); QGuiApplication::clipboard()->text());
const auto isSingle = maybeUrls.size() == 1; const auto isSingle = maybeUrls.size() == 1;
enum class Result {
Success,
Failed,
Unsupported,
Invalid,
};
const auto proceedUrl = [=](const auto &local) { const auto proceedUrl = [=](const auto &local) {
const auto command = base::StringViewMid( const auto command = base::StringViewMid(
local, local,
@ -146,6 +153,11 @@ void AddProxyFromClipboard(
match->captured(1), match->captured(1),
qthelp::UrlParamNameTransform::ToLower); qthelp::UrlParamNameTransform::ToLower);
const auto proxy = ProxyDataFromFields(type, fields); 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 contains = controller->contains(proxy);
const auto toast = (contains const auto toast = (contains
? tr::lng_proxy_add_from_clipboard_existing_toast ? tr::lng_proxy_add_from_clipboard_existing_toast
@ -158,19 +170,29 @@ void AddProxyFromClipboard(
} }
break; break;
} }
return true; return Result::Success;
} }
return false; return Result::Failed;
}; };
auto success = false; auto success = Result::Failed;
for (const auto &maybeUrl : maybeUrls) { 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) { if (success != Result::Success) {
show->showToast( if (success == Result::Failed) {
tr::lng_proxy_add_from_clipboard_failed_toast(tr::now)); 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))));
}
} }
} }

View file

@ -361,9 +361,14 @@ void CreateModerateMessagesBox(
}); });
} }
if (allCanBan) { if (allCanBan) {
auto ownedWrap = object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( const auto peer = items.front()->history()->peer;
inner, auto ownedWrap = peer->isMonoforum()
object_ptr<Ui::VerticalLayout>(inner)); ? nullptr
: object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
inner,
object_ptr<Ui::VerticalLayout>(inner));
auto computeRestrictions = Fn<ChatRestrictions()>();
const auto wrap = ownedWrap.data();
Ui::AddSkip(inner); Ui::AddSkip(inner);
Ui::AddSkip(inner); Ui::AddSkip(inner);
@ -371,7 +376,9 @@ void CreateModerateMessagesBox(
object_ptr<Ui::Checkbox>( object_ptr<Ui::Checkbox>(
box, box,
rpl::conditional( rpl::conditional(
ownedWrap->toggledValue(), (ownedWrap
? ownedWrap->toggledValue()
: rpl::single(false) | rpl::type_erased()),
tr::lng_restrict_user( tr::lng_restrict_user(
lt_count, lt_count,
rpl::single(participants.size()) | tr::to_count()), rpl::single(participants.size()) | tr::to_count()),
@ -390,136 +397,141 @@ void CreateModerateMessagesBox(
Ui::AddSkip(inner); Ui::AddSkip(inner);
Ui::AddSkip(inner); Ui::AddSkip(inner);
const auto wrap = inner->add(std::move(ownedWrap)); if (ownedWrap) {
const auto container = wrap->entity(); inner->add(std::move(ownedWrap));
wrap->toggle(false, anim::type::instant);
const auto session = &participants.front()->session(); const auto container = wrap->entity();
const auto emojiMargin = QMargins( wrap->toggle(false, anim::type::instant);
-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 label = object_ptr<Ui::FlatLabel>( const auto session = &participants.front()->session();
inner, const auto emojiMargin = QMargins(
QString(), -st::moderateBoxExpandInnerSkip,
st::moderateBoxDividerLabel); -st::moderateBoxExpandInnerSkip / 2,
const auto raw = label.data(); 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(); auto label = object_ptr<Ui::FlatLabel>(
const auto scrollLifetime = lifetime.make_state<rpl::lifetime>(); inner,
label->setClickHandlerFilter([=]( QString(),
const ClickHandlerPtr &handler, st::moderateBoxDividerLabel);
Qt::MouseButton button) { const auto raw = label.data();
if (button != Qt::LeftButton) {
return false; auto &lifetime = wrap->lifetime();
} const auto scrollLifetime = lifetime.make_state<rpl::lifetime>();
wrap->toggle(!wrap->toggled(), anim::type::normal); label->setClickHandlerFilter([=](
{ const ClickHandlerPtr &handler,
inner->heightValue() | rpl::start_with_next([=] { Qt::MouseButton button) {
if (!wrap->animating()) { if (button != Qt::LeftButton) {
scrollLifetime->destroy(); return false;
Ui::PostponeCall(crl::guard(box, [=] { }
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<int>::max());
}));
} else {
box->scrollToY(std::numeric_limits<int>::max()); box->scrollToY(std::numeric_limits<int>::max());
})); }
} else { }, *scrollLifetime);
box->scrollToY(std::numeric_limits<int>::max()); }
} return true;
}, *scrollLifetime); });
} wrap->toggledValue(
return true; ) | rpl::map([isSingle, emojiUp, emojiDown](bool toggled) {
}); return ((toggled && isSingle)
wrap->toggledValue( ? tr::lng_restrict_user_part
) | rpl::map([isSingle, emojiUp, emojiDown](bool toggled) { : (toggled && !isSingle)
return ((toggled && isSingle) ? tr::lng_restrict_users_part
? tr::lng_restrict_user_part : isSingle
: (toggled && !isSingle) ? tr::lng_restrict_user_full
? tr::lng_restrict_users_part : tr::lng_restrict_users_full)(
: isSingle lt_emoji,
? tr::lng_restrict_user_full rpl::single(toggled ? emojiUp : emojiDown),
: tr::lng_restrict_users_full)( Ui::Text::WithEntities);
lt_emoji, }) | rpl::flatten_latest(
rpl::single(toggled ? emojiUp : emojiDown), ) | rpl::start_with_next([=](const TextWithEntities &text) {
Ui::Text::WithEntities); raw->setMarkedText(
}) | rpl::flatten_latest( Ui::Text::Link(text, u"internal:"_q),
) | rpl::start_with_next([=](const TextWithEntities &text) { Core::TextContext({ .session = session }));
raw->setMarkedText( }, label->lifetime());
Ui::Text::Link(text, u"internal:"_q),
Core::TextContext({ .session = session }));
}, label->lifetime());
Ui::AddSkip(inner); Ui::AddSkip(inner);
inner->add(object_ptr<Ui::DividerLabel>( inner->add(object_ptr<Ui::DividerLabel>(
inner, inner,
std::move(label), std::move(label),
st::defaultBoxDividerLabelPadding, st::defaultBoxDividerLabelPadding,
RectPart::Top | RectPart::Bottom)); RectPart::Top | RectPart::Bottom));
using Flag = ChatRestriction; using Flag = ChatRestriction;
using Flags = ChatRestrictions; using Flags = ChatRestrictions;
const auto peer = items.front()->history()->peer; const auto chat = peer->asChat();
const auto chat = peer->asChat(); const auto channel = peer->asChannel();
const auto channel = peer->asChannel(); const auto defaultRestrictions = chat
const auto defaultRestrictions = chat ? chat->defaultRestrictions()
? chat->defaultRestrictions() : channel->defaultRestrictions();
: channel->defaultRestrictions(); const auto prepareFlags = FixDependentRestrictions(
const auto prepareFlags = FixDependentRestrictions( defaultRestrictions
defaultRestrictions | ((channel && channel->isPublic())
| ((channel && channel->isPublic()) ? (Flag::ChangeInfo | Flag::PinMessages)
? (Flag::ChangeInfo | Flag::PinMessages) : Flags(0)));
: Flags(0))); const auto disabledMessages = [&] {
const auto disabledMessages = [&] { auto result = base::flat_map<Flags, QString>();
auto result = base::flat_map<Flags, QString>(); {
{ const auto disabled = FixDependentRestrictions(
const auto disabled = FixDependentRestrictions( defaultRestrictions
defaultRestrictions | ((channel && channel->isPublic())
| ((channel && channel->isPublic()) ? (Flag::ChangeInfo | Flag::PinMessages)
? (Flag::ChangeInfo | Flag::PinMessages) : Flags(0)));
: Flags(0))); result.emplace(
result.emplace( disabled,
disabled, tr::lng_rights_restriction_for_all(tr::now));
tr::lng_rights_restriction_for_all(tr::now)); }
} return result;
return result; }();
}();
Ui::AddSubsectionTitle( Ui::AddSubsectionTitle(
inner, inner,
rpl::conditional( rpl::conditional(
rpl::single(isSingle), rpl::single(isSingle),
tr::lng_restrict_users_part_single_header(), tr::lng_restrict_users_part_single_header(),
tr::lng_restrict_users_part_header( tr::lng_restrict_users_part_header(
lt_count, lt_count,
rpl::single(participants.size()) | tr::to_count()))); rpl::single(participants.size()) | tr::to_count())));
auto [checkboxes, getRestrictions, changes] = CreateEditRestrictions( auto [checkboxes, getRestrictions, changes] = CreateEditRestrictions(
box, box,
prepareFlags, prepareFlags,
disabledMessages, disabledMessages,
{ .isForum = peer->isForum() }); { .isForum = peer->isForum() });
std::move(changes) | rpl::start_with_next([=] { computeRestrictions = getRestrictions;
ban->setChecked(true); std::move(changes) | rpl::start_with_next([=] {
}, ban->lifetime()); ban->setChecked(true);
Ui::AddSkip(container); }, ban->lifetime());
Ui::AddDivider(container); Ui::AddSkip(container);
Ui::AddSkip(container); Ui::AddDivider(container);
container->add(std::move(checkboxes)); Ui::AddSkip(container);
container->add(std::move(checkboxes));
}
// Handle confirmation manually. // Handle confirmation manually.
confirms->events() | rpl::start_with_next([=] { confirms->events() | rpl::start_with_next([=] {
if (ban->checked() && controller->collectRequests) { if (ban->checked() && controller->collectRequests) {
const auto kick = !wrap->toggled(); const auto kick = !wrap || !wrap->toggled();
const auto restrictions = getRestrictions(); const auto restrictions = computeRestrictions
? computeRestrictions()
: ChatRestrictions();
const auto request = [=]( const auto request = [=](
not_null<PeerData*> peer, not_null<PeerData*> peer,
not_null<ChannelData*> channel) { not_null<ChannelData*> channel) {
@ -532,10 +544,15 @@ void CreateModerateMessagesBox(
nullptr, nullptr,
nullptr); nullptr);
} else { } else {
channel->session().api().chatParticipants().kick( const auto block = channel->isMonoforum()
channel, ? channel->monoforumBroadcast()
peer, : channel.get();
{ channel->restrictions(), 0 }); if (block) {
block->session().api().chatParticipants().kick(
block,
peer,
{ block->restrictions(), 0 });
}
} }
}; };
sequentiallyRequest(request, controller->collectRequests()); sequentiallyRequest(request, controller->collectRequests());

View file

@ -127,6 +127,7 @@ constexpr auto kUpgradeDoneToastDuration = 4 * crl::time(1000);
constexpr auto kGiftsPreloadTimeout = 3 * crl::time(1000); constexpr auto kGiftsPreloadTimeout = 3 * crl::time(1000);
constexpr auto kResaleGiftsPerPage = 50; constexpr auto kResaleGiftsPerPage = 50;
constexpr auto kFiltersCount = 4; constexpr auto kFiltersCount = 4;
constexpr auto kResellPriceCacheLifetime = 60 * crl::time(1000);
using namespace HistoryView; using namespace HistoryView;
using namespace Info::PeerGifts; using namespace Info::PeerGifts;
@ -220,6 +221,33 @@ struct GiftDetails {
bool byStars = false; bool byStars = false;
}; };
struct SessionResalePrices {
explicit SessionResalePrices(not_null<Main::Session*> session)
: api(std::make_unique<Api::PremiumGiftCodeOptions>(session->user())) {
}
std::unique_ptr<Api::PremiumGiftCodeOptions> api;
base::flat_map<QString, int> prices;
std::vector<Fn<void()>> waiting;
rpl::lifetime requestLifetime;
crl::time lastReceived = 0;
};
[[nodiscard]] not_null<SessionResalePrices*> ResalePrices(
not_null<Main::Session*> session) {
static auto result = base::flat_map<
not_null<Main::Session*>,
std::unique_ptr<SessionResalePrices>>();
if (const auto i = result.find(session); i != end(result)) {
return i->second.get();
}
const auto i = result.emplace(
session,
std::make_unique<SessionResalePrices>(session)).first;
session->lifetime().add([session] { result.remove(session); });
return i->second.get();
}
class PeerRow final : public PeerListRow { class PeerRow final : public PeerListRow {
public: public:
using PeerListRow::PeerListRow; using PeerListRow::PeerListRow;
@ -4381,6 +4409,55 @@ void ShowUniqueGiftWearBox(
})); }));
} }
void PreloadUniqueGiftResellPrices(not_null<Main::Session*> 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<Main::Session*> session,
const QString &title,
Fn<void(int)> 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( void UpdateGiftSellPrice(
std::shared_ptr<ChatHelpers::Show> show, std::shared_ptr<ChatHelpers::Show> show,
std::shared_ptr<Data::UniqueGift> unique, std::shared_ptr<Data::UniqueGift> unique,
@ -4422,6 +4499,132 @@ void UpdateGiftSellPrice(
}).send(); }).send();
} }
void UniqueGiftSellBox(
not_null<Ui::GenericBox*> box,
std::shared_ptr<ChatHelpers::Show> show,
std::shared_ptr<Data::UniqueGift> 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<Ui::FixedHeightWidget>(
box,
st::editTagField.heightMin));
auto owned = object_ptr<Ui::NumberInput>(
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<Ui::FlatLabel>(
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( void ShowUniqueGiftSellBox(
std::shared_ptr<ChatHelpers::Show> show, std::shared_ptr<ChatHelpers::Show> show,
std::shared_ptr<Data::UniqueGift> unique, std::shared_ptr<Data::UniqueGift> unique,
@ -4430,125 +4633,11 @@ void ShowUniqueGiftSellBox(
if (ShowResaleGiftLater(show, unique)) { if (ShowResaleGiftLater(show, unique)) {
return; return;
} }
show->show(Box([=](not_null<Ui::GenericBox*> box) { const auto session = &show->session();
box->setTitle(tr::lng_gift_sell_title()); const auto &title = unique->title;
box->setStyle(st.box ? *st.box : st::upgradeGiftBox); InvokeWithUniqueGiftResellPrice(session, title, [=](int price) {
box->setWidth(st::boxWideWidth); show->show(Box(UniqueGiftSellBox, show, unique, savedId, price, st));
});
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<Ui::FixedHeightWidget>(
box,
st::editTagField.heightMin));
auto owned = object_ptr<Ui::NumberInput>(
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<Ui::FlatLabel>(
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 GiftReleasedByHandler(not_null<PeerData*> peer) { void GiftReleasedByHandler(not_null<PeerData*> peer) {

View file

@ -21,6 +21,7 @@ class SavedStarGiftId;
} // namespace Data } // namespace Data
namespace Main { namespace Main {
class Session;
class SessionShow; class SessionShow;
} // namespace Main } // namespace Main
@ -71,6 +72,8 @@ void ShowUniqueGiftWearBox(
const Data::UniqueGift &gift, const Data::UniqueGift &gift,
Settings::GiftWearBoxStyleOverride st); Settings::GiftWearBoxStyleOverride st);
void PreloadUniqueGiftResellPrices(not_null<Main::Session*> session);
void UpdateGiftSellPrice( void UpdateGiftSellPrice(
std::shared_ptr<ChatHelpers::Show> show, std::shared_ptr<ChatHelpers::Show> show,
std::shared_ptr<Data::UniqueGift> unique, std::shared_ptr<Data::UniqueGift> unique,

View file

@ -425,8 +425,6 @@ void TransferGift(
Data::SavedStarGiftId savedId, Data::SavedStarGiftId savedId,
Fn<void(Payments::CheckoutResult)> done, Fn<void(Payments::CheckoutResult)> done,
bool skipPaymentForm = false) { bool skipPaymentForm = false) {
Expects(to->isUser());
const auto session = &window->session(); const auto session = &window->session();
const auto weak = base::make_weak(window); const auto weak = base::make_weak(window);
auto formDone = [=]( auto formDone = [=](

View file

@ -17,6 +17,7 @@ constexpr auto kSendReactionEmojiProperty = 0x04;
constexpr auto kReactionsCountEmojiProperty = 0x05; constexpr auto kReactionsCountEmojiProperty = 0x05;
constexpr auto kDocumentFilenameTooltipProperty = 0x06; constexpr auto kDocumentFilenameTooltipProperty = 0x06;
constexpr auto kPhoneNumberLinkProperty = 0x07; constexpr auto kPhoneNumberLinkProperty = 0x07;
constexpr auto kTodoListItemIdProperty = 0x08;
namespace Ui { namespace Ui {
class Show; class Show;

View file

@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D666}"_cs;
constexpr auto AppNameOld = "AyuGram for Windows"_cs; constexpr auto AppNameOld = "AyuGram for Windows"_cs;
constexpr auto AppName = "AyuGram Desktop"_cs; constexpr auto AppName = "AyuGram Desktop"_cs;
constexpr auto AppFile = "AyuGram"_cs; constexpr auto AppFile = "AyuGram"_cs;
constexpr auto AppVersion = 5016003; constexpr auto AppVersion = 5016004;
constexpr auto AppVersionStr = "5.16.3"; constexpr auto AppVersionStr = "5.16.4";
constexpr auto AppBetaVersion = false; constexpr auto AppBetaVersion = false;
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION; constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;

View file

@ -32,8 +32,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Data { namespace Data {
namespace { namespace {
constexpr auto kMs = crl::time(1000);
constexpr auto kRequestTimeLimit = 5 * 60 * crl::time(1000); constexpr auto kRequestTimeLimit = 5 * 60 * crl::time(1000);
const auto kFlaggedPreload = ((MediaPreload*)quintptr(0x01));
[[nodiscard]] bool TooEarlyForRequest(crl::time received) { [[nodiscard]] bool TooEarlyForRequest(crl::time received) {
return (received > 0) && (received + kRequestTimeLimit > crl::now()); return (received > 0) && (received + kRequestTimeLimit > crl::now());
} }
@ -77,17 +80,21 @@ void SponsoredMessages::clear() {
void SponsoredMessages::clearOldRequests() { void SponsoredMessages::clearOldRequests() {
const auto now = crl::now(); const auto now = crl::now();
while (true) { const auto clear = [&](auto &requests) {
const auto i = ranges::find_if(_requests, [&](const auto &value) { while (true) {
const auto &request = value.second; const auto i = ranges::find_if(requests, [&](const auto &value) {
return !request.requestId const auto &request = value.second;
&& (request.lastReceived + kRequestTimeLimit <= now); return !request.requestId
}); && (request.lastReceived + kRequestTimeLimit <= now);
if (i == end(_requests)) { });
break; if (i == end(requests)) {
break;
}
requests.erase(i);
} }
_requests.erase(i); };
} clear(_requests);
clear(_requestsForVideo);
} }
SponsoredMessages::AppendResult SponsoredMessages::append( SponsoredMessages::AppendResult SponsoredMessages::append(
@ -241,6 +248,11 @@ bool SponsoredMessages::canHaveFor(not_null<History*> history) const {
return false; return false;
} }
bool SponsoredMessages::canHaveFor(not_null<HistoryItem*> item) const {
return item->history()->peer->isBroadcast()
&& item->isRegular();
}
bool SponsoredMessages::isTopBarFor(not_null<History*> history) const { bool SponsoredMessages::isTopBarFor(not_null<History*> history) const {
const auto &settings = AyuSettings::getInstance(); const auto &settings = AyuSettings::getInstance();
if (settings.disableAds) { if (settings.disableAds) {
@ -291,6 +303,78 @@ void SponsoredMessages::request(not_null<History*> history, Fn<void()> done) {
}).send(); }).send();
} }
void SponsoredMessages::requestForVideo(
not_null<HistoryItem*> item,
Fn<void(SponsoredForVideo)> 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( void SponsoredMessages::parse(
not_null<History*> history, not_null<History*> history,
const MTPmessages_sponsoredMessages &list) { const MTPmessages_sponsoredMessages &list) {
@ -306,12 +390,9 @@ void SponsoredMessages::parse(
_session->data().processChats(data.vchats()); _session->data().processChats(data.vchats());
const auto &messages = data.vmessages().v; 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.entries.clear();
list.received = crl::now(); list.received = crl::now();
for (const auto &message : messages) {
append(history, list, message);
}
if (const auto postsBetween = data.vposts_between()) { if (const auto postsBetween = data.vposts_between()) {
list.postsBetween = postsBetween->v; list.postsBetween = postsBetween->v;
list.state = State::InjectToMiddle; list.state = State::InjectToMiddle;
@ -320,10 +401,61 @@ void SponsoredMessages::parse(
? State::AppendToEnd ? State::AppendToEnd
: State::AppendToTopBar; : State::AppendToTopBar;
} }
for (const auto &message : messages) {
append([=] {
return &_data[history].entries;
}, history, message);
}
}, [](const MTPDmessages_sponsoredMessagesEmpty &) { }, [](const MTPDmessages_sponsoredMessagesEmpty &) {
}); });
} }
void SponsoredMessages::parseForVideo(
not_null<PeerData*> 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<PeerData*> 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( FullMsgId SponsoredMessages::fillTopBar(
not_null<History*> history, not_null<History*> history,
not_null<Ui::RpWidget*> widget) { not_null<Ui::RpWidget*> widget) {
@ -373,8 +505,8 @@ rpl::producer<> SponsoredMessages::itemRemoved(const FullMsgId &fullId) {
} }
void SponsoredMessages::append( void SponsoredMessages::append(
Fn<not_null<std::vector<Entry>*>()> entries,
not_null<History*> history, not_null<History*> history,
List &list,
const MTPSponsoredMessage &message) { const MTPSponsoredMessage &message) {
const auto &data = message.data(); const auto &data = message.data();
const auto randomId = data.vrandom_id().v; const auto randomId = data.vrandom_id().v;
@ -385,14 +517,14 @@ void SponsoredMessages::append(
data.vmedia()->match([&](const MTPDmessageMediaPhoto &media) { data.vmedia()->match([&](const MTPDmessageMediaPhoto &media) {
if (const auto tlPhoto = media.vphoto()) { if (const auto tlPhoto = media.vphoto()) {
tlPhoto->match([&](const MTPDphoto &data) { tlPhoto->match([&](const MTPDphoto &data) {
mediaPhoto = history->owner().processPhoto(data); mediaPhoto = _session->data().processPhoto(data);
}, [](const MTPDphotoEmpty &) { }, [](const MTPDphotoEmpty &) {
}); });
} }
}, [&](const MTPDmessageMediaDocument &media) { }, [&](const MTPDmessageMediaDocument &media) {
if (const auto tlDocument = media.vdocument()) { if (const auto tlDocument = media.vdocument()) {
tlDocument->match([&](const MTPDdocument &data) { tlDocument->match([&](const MTPDdocument &data) {
const auto d = history->owner().processDocument( const auto d = _session->data().processDocument(
data, data,
media.valt_documents()); media.valt_documents());
if (d->isVideoFile() if (d->isVideoFile()
@ -413,7 +545,7 @@ void SponsoredMessages::append(
.link = qs(data.vurl()), .link = qs(data.vurl()),
.buttonText = qs(data.vbutton_text()), .buttonText = qs(data.vbutton_text()),
.photoId = data.vphoto() .photoId = data.vphoto()
? history->session().data().processPhoto(*data.vphoto())->id ? _session->data().processPhoto(*data.vphoto())->id
: PhotoId(0), : PhotoId(0),
.mediaPhotoId = (mediaPhoto ? mediaPhoto->id : 0), .mediaPhotoId = (mediaPhoto ? mediaPhoto->id : 0),
.mediaDocumentId = (mediaDocument ? mediaDocument->id : 0), .mediaDocumentId = (mediaDocument ? mediaDocument->id : 0),
@ -449,25 +581,24 @@ void SponsoredMessages::append(
.link = from.link, .link = from.link,
.sponsorInfo = std::move(sponsorInfo), .sponsorInfo = std::move(sponsorInfo),
.additionalInfo = std::move(additionalInfo), .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({ const auto itemId = FullMsgId(
.sponsored = std::move(sharedMessage),
});
auto &entry = list.entries.back();
const auto itemId = entry.itemFullId = FullMsgId(
history->peer->id, history->peer->id,
_session->data().nextLocalMessageId()); _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. const auto fileOrigin = FileOrigin(); // No way to refresh in ads.
static const auto kFlaggedPreload = ((MediaPreload*)quintptr(0x01));
const auto preloaded = [=] { const auto preloaded = [=] {
const auto i = _data.find(history); const auto list = entries();
if (i == end(_data)) { const auto j = ranges::find(*list, itemId, &Entry::itemFullId);
return; if (j == end(*list)) {
}
auto &entries = i->second.entries;
const auto j = ranges::find(entries, itemId, &Entry::itemFullId);
if (j == end(entries)) {
return; return;
} }
auto &entry = *j; auto &entry = *j;
@ -565,7 +696,11 @@ SponsoredMessages::Details SponsoredMessages::lookupDetails(
if (!entryPtr) { if (!entryPtr) {
return {}; return {};
} }
const auto &data = entryPtr->sponsored; return lookupDetails(entryPtr->sponsored);
}
SponsoredMessages::Details SponsoredMessages::lookupDetails(
const SponsoredMessage &data) const {
return { return {
.info = Prepare(data), .info = Prepare(data),
.link = data.link, .link = data.link,

View file

@ -67,10 +67,12 @@ struct SponsoredMessage {
QByteArray randomId; QByteArray randomId;
SponsoredFrom from; SponsoredFrom from;
TextWithEntities textWithEntities; TextWithEntities textWithEntities;
History *history = nullptr; not_null<History*> history;
QString link; QString link;
TextWithEntities sponsorInfo; TextWithEntities sponsorInfo;
TextWithEntities additionalInfo; TextWithEntities additionalInfo;
crl::time durationMin = 0;
crl::time durationMax = 0;
}; };
struct SponsoredMessageDetails { struct SponsoredMessageDetails {
@ -92,6 +94,23 @@ struct SponsoredReportAction {
Fn<void(Data::SponsoredReportResult)>)> callback; Fn<void(Data::SponsoredReportResult)>)> callback;
}; };
struct SponsoredForVideoState {
int itemIndex = 0;
crl::time leftTillShow = 0;
[[nodiscard]] bool initial() const {
return !itemIndex && !leftTillShow;
}
};
struct SponsoredForVideo {
std::vector<SponsoredMessage> list;
crl::time startDelay = 0;
crl::time betweenDelay = 0;
SponsoredForVideoState state;
};
class SponsoredMessages final { class SponsoredMessages final {
public: public:
enum class AppendResult { enum class AppendResult {
@ -111,10 +130,18 @@ public:
~SponsoredMessages(); ~SponsoredMessages();
[[nodiscard]] bool canHaveFor(not_null<History*> history) const; [[nodiscard]] bool canHaveFor(not_null<History*> history) const;
[[nodiscard]] bool canHaveFor(not_null<HistoryItem*> item) const;
[[nodiscard]] bool isTopBarFor(not_null<History*> history) const; [[nodiscard]] bool isTopBarFor(not_null<History*> history) const;
void request(not_null<History*> history, Fn<void()> done); void request(not_null<History*> history, Fn<void()> done);
void requestForVideo(
not_null<HistoryItem*> item,
Fn<void(SponsoredForVideo)> done);
void updateForVideo(
FullMsgId itemId,
SponsoredForVideoState state);
void clearItems(not_null<History*> history); void clearItems(not_null<History*> history);
[[nodiscard]] Details lookupDetails(const FullMsgId &fullId) const; [[nodiscard]] Details lookupDetails(const FullMsgId &fullId) const;
[[nodiscard]] Details lookupDetails(const SponsoredMessage &data) const;
[[nodiscard]] Details lookupDetails( [[nodiscard]] Details lookupDetails(
const Api::SponsoredSearchResult &data) const; const Api::SponsoredSearchResult &data) const;
void clicked(const FullMsgId &fullId, bool isMedia, bool isFullscreen); void clicked(const FullMsgId &fullId, bool isMedia, bool isFullscreen);
@ -166,18 +193,35 @@ private:
int postsBetween = 0; int postsBetween = 0;
State state = State::None; State state = State::None;
}; };
struct ListForVideo {
std::vector<Entry> entries;
crl::time received = 0;
crl::time startDelay = 0;
crl::time betweenDelay = 0;
SponsoredForVideoState state;
};
struct Request { struct Request {
mtpRequestId requestId = 0; mtpRequestId requestId = 0;
crl::time lastReceived = 0; crl::time lastReceived = 0;
}; };
struct RequestForVideo {
std::vector<Fn<void(SponsoredForVideo)>> callbacks;
mtpRequestId requestId = 0;
crl::time lastReceived = 0;
};
void parse( void parse(
not_null<History*> history, not_null<History*> history,
const MTPmessages_sponsoredMessages &list); const MTPmessages_sponsoredMessages &list);
void parseForVideo(
not_null<PeerData*> peer,
const MTPmessages_sponsoredMessages &list);
void append( void append(
Fn<not_null<std::vector<Entry>*>()> entries,
not_null<History*> history, not_null<History*> history,
List &list,
const MTPSponsoredMessage &message); const MTPSponsoredMessage &message);
[[nodiscard]] SponsoredForVideo prepareForVideo(
not_null<PeerData*> peer);
void clearOldRequests(); void clearOldRequests();
const Entry *find(const FullMsgId &fullId) const; const Entry *find(const FullMsgId &fullId) const;
@ -189,6 +233,9 @@ private:
base::flat_map<not_null<History*>, Request> _requests; base::flat_map<not_null<History*>, Request> _requests;
base::flat_map<RandomId, Request> _viewRequests; base::flat_map<RandomId, Request> _viewRequests;
base::flat_map<not_null<PeerData*>, ListForVideo> _dataForVideo;
base::flat_map<not_null<PeerData*>, RequestForVideo> _requestsForVideo;
rpl::event_stream<FullMsgId> _itemRemoved; rpl::event_stream<FullMsgId> _itemRemoved;
rpl::lifetime _lifetime; rpl::lifetime _lifetime;

View file

@ -72,7 +72,7 @@ namespace {
| (data.is_send_audios() ? Flag::SendMusic : Flag()) | (data.is_send_audios() ? Flag::SendMusic : Flag())
| (data.is_send_voices() ? Flag::SendVoiceMessages : Flag()) | (data.is_send_voices() ? Flag::SendVoiceMessages : Flag())
| (data.is_send_docs() ? Flag::SendFiles : 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_embed_links() ? Flag::EmbedLinks : Flag())
| (data.is_change_info() ? Flag::ChangeInfo : Flag()) | (data.is_change_info() ? Flag::ChangeInfo : Flag())
| (data.is_invite_users() ? Flag::AddParticipants : 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::SendMusic) ? Flag::f_send_audios : Flag())
| ((flags & R::SendVoiceMessages) ? Flag::f_send_voices : Flag()) | ((flags & R::SendVoiceMessages) ? Flag::f_send_voices : Flag())
| ((flags & R::SendFiles) ? Flag::f_send_docs : 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::EmbedLinks) ? Flag::f_embed_links : Flag())
| ((flags & R::ChangeInfo) ? Flag::f_change_info : Flag()) | ((flags & R::ChangeInfo) ? Flag::f_change_info : Flag())
| ((flags & R::AddParticipants) ? Flag::f_invite_users : Flag()) | ((flags & R::AddParticipants) ? Flag::f_invite_users : Flag())

View file

@ -92,7 +92,8 @@ MTPInputReplyTo ReplyToForMTP(
: Flag()) : Flag())
| (quoteEntities.v.isEmpty() | (quoteEntities.v.isEmpty()
? Flag() ? 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.messageId ? replyTo.messageId.msg : 0),
MTP_int(replyTo.topicRootId), MTP_int(replyTo.topicRootId),
(external (external
@ -103,7 +104,8 @@ MTPInputReplyTo ReplyToForMTP(
MTP_int(replyTo.quoteOffset), MTP_int(replyTo.quoteOffset),
(replyToMonoforumPeerId (replyToMonoforumPeerId
? history->owner().peer(replyToMonoforumPeerId)->input ? history->owner().peer(replyToMonoforumPeerId)->input
: MTPInputPeer())); : MTPInputPeer()),
MTP_int(replyTo.todoItemId));
} else if (history->peer->amMonoforumAdmin() } else if (history->peer->amMonoforumAdmin()
&& replyTo.monoforumPeerId) { && replyTo.monoforumPeerId) {
const auto replyToMonoforumPeer = replyTo.monoforumPeerId const auto replyToMonoforumPeer = replyTo.monoforumPeerId

View file

@ -172,6 +172,19 @@ inline QDebug operator<<(QDebug debug, const FullMsgId &fullMsgId) {
Q_DECLARE_METATYPE(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 { struct FullReplyTo {
FullMsgId messageId; FullMsgId messageId;
TextWithEntities quote; TextWithEntities quote;
@ -179,7 +192,11 @@ struct FullReplyTo {
MsgId topicRootId = 0; MsgId topicRootId = 0;
PeerId monoforumPeerId = 0; PeerId monoforumPeerId = 0;
int quoteOffset = 0; int quoteOffset = 0;
int todoItemId = 0;
[[nodiscard]] MessageHighlightId highlight() const {
return { quote, quoteOffset, todoItemId };
}
[[nodiscard]] bool replying() const { [[nodiscard]] bool replying() const {
return messageId || (storyId && storyId.peer); return messageId || (storyId && storyId.peer);
} }

View file

@ -96,17 +96,6 @@ Thread *SavedMessages::activeSubsectionThread() const {
return _activeSubsectionSublist; 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() { SavedMessages::~SavedMessages() {
clear(); clear();
} }
@ -458,6 +447,9 @@ void SavedMessages::applySublistDeleted(not_null<PeerData*> sublistPeer) {
if (ranges::contains(_lastSublists, not_null(raw))) { if (ranges::contains(_lastSublists, not_null(raw))) {
reorderLastSublists(); reorderLastSublists();
} }
if (_activeSubsectionSublist == raw) {
_activeSubsectionSublist = nullptr;
}
_sublistDestroyed.fire(raw); _sublistDestroyed.fire(raw);
session().changes().sublistUpdated( session().changes().sublistUpdated(

View file

@ -84,8 +84,6 @@ public:
void saveActiveSubsectionThread(not_null<Thread*> thread); void saveActiveSubsectionThread(not_null<Thread*> thread);
Thread *activeSubsectionThread() const; Thread *activeSubsectionThread() const;
[[nodiscard]] Dialogs::UnreadState unreadStateWithParentMuted() const;
[[nodiscard]] rpl::lifetime &lifetime(); [[nodiscard]] rpl::lifetime &lifetime();
private: private:

View file

@ -217,7 +217,28 @@ void SavedSublist::applyItemRemoved(MsgId id) {
if (const auto chatListItem = _chatListMessage.value_or(nullptr)) { if (const auto chatListItem = _chatListMessage.value_or(nullptr)) {
if (chatListItem->id == id) { if (chatListItem->id == id) {
_chatListMessage = std::nullopt; _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(); _list.clear();
if (processMessagesIsEmpty(result)) { if (processMessagesIsEmpty(result)) {
_fullCount = _skippedBefore = _skippedAfter = 0; _fullCount = _skippedBefore = _skippedAfter = 0;
if (!_parent->parentChat() && !_chatListMessage) {
setLastServerMessage(nullptr);
updateChatListExistence();
}
} else if (id) { } else if (id) {
Assert(!_list.empty()); Assert(!_list.empty());
if (_list.front() <= id) { if (_list.front() <= id) {
@ -1117,6 +1142,11 @@ void SavedSublist::loadAround(MsgId id) {
} else if (_list.back() >= id) { } else if (_list.back() >= id) {
_skippedBefore = 0; _skippedBefore = 0;
} }
} else if (!_parent->parentChat() && !_chatListMessage) {
Assert(!_list.empty());
applyMaybeLast(owner().message(
owningHistory()->peer,
_list.front()));
} }
checkReadTillEnd(); checkReadTillEnd();
}).fail([=](const MTP::Error &error) { }).fail([=](const MTP::Error &error) {

View file

@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/unixtime.h" #include "base/unixtime.h"
#include "apiwrap.h" #include "apiwrap.h"
#include "core/application.h" #include "core/application.h"
#include "data/components/top_peers.h"
#include "data/data_changes.h" #include "data/data_changes.h"
#include "data/data_channel.h" #include "data/data_channel.h"
#include "data/data_document.h" #include "data/data_document.h"
@ -425,10 +426,7 @@ void Stories::parseAndApply(const MTPPeerStories &stories) {
}; };
if (result.peer->isSelf() if (result.peer->isSelf()
|| (result.peer->isChannel() && result.peer->asChannel()->amIn()) || (result.peer->isChannel() && result.peer->asChannel()->amIn())
|| (result.peer->isUser() || result.peer->isUser()) {
&& (result.peer->asUser()->isBot()
|| result.peer->asUser()->isContact()))
|| result.peer->isServiceUser()) {
const auto hidden = result.peer->hasStoriesHidden(); const auto hidden = result.peer->hasStoriesHidden();
using List = StorySourcesList; using List = StorySourcesList;
add(hidden ? List::Hidden : List::NotHidden); add(hidden ? List::Hidden : List::NotHidden);
@ -1197,7 +1195,11 @@ void Stories::toggleHidden(
bool hidden, bool hidden,
std::shared_ptr<Ui::Show> show) { std::shared_ptr<Ui::Show> show) {
const auto peer = _owner->peer(peerId); 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 (peer->hasStoriesHidden() != hidden) {
if (!justRemove) { if (!justRemove) {
peer->setStoriesHidden(hidden); peer->setStoriesHidden(hidden);
@ -1206,6 +1208,9 @@ void Stories::toggleHidden(
peer->input, peer->input,
MTP_bool(hidden) MTP_bool(hidden)
)).send(); )).send();
if (byHints) {
peer->session().topPeers().remove(peer);
}
} }
const auto name = peer->shortName(); const auto name = peer->shortName();

View file

@ -244,6 +244,17 @@ dialogsEmptyLabel: FlatLabel(defaultFlatLabel) {
align: align(top); align: align(top);
textFg: windowSubTextFg; 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 { dialogsMenuToggle: IconButton {
width: 40px; width: 40px;

View file

@ -31,6 +31,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/text/text_utilities.h" #include "ui/text/text_utilities.h"
#include "ui/text/text_options.h" #include "ui/text/text_options.h"
#include "ui/dynamic_thumbnails.h" #include "ui/dynamic_thumbnails.h"
#include "ui/vertical_list.h"
#include "ui/painter.h" #include "ui/painter.h"
#include "ui/rect.h" #include "ui/rect.h"
#include "ui/ui_utility.h" #include "ui/ui_utility.h"
@ -58,8 +59,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/options.h" #include "base/options.h"
#include "lang/lang_keys.h" #include "lang/lang_keys.h"
#include "lottie/lottie_icon.h" #include "lottie/lottie_icon.h"
#include "mainwindow.h" #include "settings/settings_common.h"
#include "mainwidget.h"
#include "storage/storage_account.h" #include "storage/storage_account.h"
#include "apiwrap.h" #include "apiwrap.h"
#include "main/main_session.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 "api/api_chat_filters.h"
#include "base/qt/qt_common_adapters.h" #include "base/qt/qt_common_adapters.h"
#include "styles/style_dialogs.h" #include "styles/style_dialogs.h"
#include "styles/style_boxes.h"
#include "styles/style_chat.h" // popupMenuExpandedSeparator #include "styles/style_chat.h" // popupMenuExpandedSeparator
#include "styles/style_chat_helpers.h" #include "styles/style_chat_helpers.h"
#include "styles/style_color_indices.h" #include "styles/style_color_indices.h"
@ -3081,6 +3082,11 @@ void InnerWidget::clearSelection() {
} }
void InnerWidget::fillSupportSearchMenu(not_null<Ui::PopupMenu*> menu) { void InnerWidget::fillSupportSearchMenu(not_null<Ui::PopupMenu*> 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 all = session().settings().supportAllSearchResults();
const auto text = all ? "Only one from chat" : "Show all messages"; const auto text = all ? "Only one from chat" : "Show all messages";
menu->addAction(text, [=] { menu->addAction(text, [=] {
@ -3091,9 +3097,11 @@ void InnerWidget::fillSupportSearchMenu(not_null<Ui::PopupMenu*> menu) {
void InnerWidget::fillArchiveSearchMenu(not_null<Ui::PopupMenu*> menu) { void InnerWidget::fillArchiveSearchMenu(not_null<Ui::PopupMenu*> menu) {
const auto folder = session().data().folderLoaded(Data::Folder::kId); const auto folder = session().data().folderLoaded(Data::Folder::kId);
const auto globalSearch = (_searchState.tab == ChatSearchTab::MyMessages)
|| (_searchState.tab == ChatSearchTab::PublicPosts);
if (!folder if (!folder
|| !folder->chatsList()->fullSize().current() || !folder->chatsList()->fullSize().current()
|| _searchState.inChat) { || (!globalSearch && _searchState.inChat)) {
return; return;
} }
const auto skip = session().settings().skipArchiveInSearch(); const auto skip = session().settings().skipArchiveInSearch();
@ -3263,16 +3271,13 @@ void InnerWidget::showSponsoredMenu(int peerSearchIndex, QPoint globalPos) {
refresh(); refresh();
}); });
Menu::FillSponsored( Menu::FillSponsored(
this,
Ui::Menu::CreateAddActionCallback(_menu), Ui::Menu::CreateAddActionCallback(_menu),
_controller->uiShow(), _controller->uiShow(),
Menu::SponsoredPhrases::Search, Menu::SponsoredPhrases::Search,
session().sponsoredMessages().lookupDetails(entry->sponsored->data), session().sponsoredMessages().lookupDetails(entry->sponsored->data),
session().sponsoredMessages().createReportCallback( session().sponsoredMessages().createReportCallback(
entry->sponsored->data.randomId, entry->sponsored->data.randomId,
remove), remove));
false,
false);
QObject::connect(_menu.get(), &QObject::destroyed, [=] { QObject::connect(_menu.get(), &QObject::destroyed, [=] {
if (_peerSearchMenu >= 0 if (_peerSearchMenu >= 0
&& _peerSearchMenu < _peerSearchResults.size()) { && _peerSearchMenu < _peerSearchResults.size()) {
@ -3811,7 +3816,7 @@ void InnerWidget::itemRemoved(not_null<const HistoryItem*> item) {
} }
bool InnerWidget::uniqueSearchResults() const { bool InnerWidget::uniqueSearchResults() const {
return _controller->uniqueChatsInSearchResults(); return _controller->uniqueChatsInSearchResults(_searchState);
} }
bool InnerWidget::hasHistoryInResults(not_null<History*> history) const { bool InnerWidget::hasHistoryInResults(not_null<History*> history) const {
@ -3869,7 +3874,8 @@ void InnerWidget::searchReceived(
? _searchState.inChat ? _searchState.inChat
: Key(_openedForum->history()); : Key(_openedForum->history());
if (inject if (inject
&& (!_searchState.inChat && (globalSearch
|| !_searchState.inChat
|| inject->history() == _searchState.inChat.history())) { || inject->history() == _searchState.inChat.history())) {
Assert(_searchResults.empty()); Assert(_searchResults.empty());
Assert(!toPreview); Assert(!toPreview);
@ -4082,9 +4088,18 @@ void InnerWidget::refreshEmpty() {
if (state == EmptyState::None) { if (state == EmptyState::None) {
_emptyState = state; _emptyState = state;
_empty.destroy(); _empty.destroy();
_emptyList.destroy();
_emptyButton.destroy();
return; return;
} else if (_emptyState == state) { } else if (_emptyState == state) {
_empty->setVisible(_state == WidgetState::Default); _empty->setVisible(_state == WidgetState::Default);
if (_emptyList) {
_emptyList->setVisible(_state == WidgetState::Default);
_empty->setVisible(!_emptyList->isVisible());
}
if (_emptyButton) {
_emptyButton->setVisible(_state == WidgetState::Default);
}
return; return;
} }
_emptyState = state; _emptyState = state;
@ -4115,7 +4130,6 @@ void InnerWidget::refreshEmpty() {
return result; return result;
}); });
_empty.create(this, std::move(full), st::dialogsEmptyLabel); _empty.create(this, std::move(full), st::dialogsEmptyLabel);
resizeEmpty();
_empty->overrideLinkClickHandler([=] { _empty->overrideLinkClickHandler([=] {
if (_emptyState == EmptyState::NoContacts) { if (_emptyState == EmptyState::NoContacts) {
_controller->showAddContact(); _controller->showAddContact();
@ -4127,6 +4141,58 @@ void InnerWidget::refreshEmpty() {
} }
}); });
_empty->setVisible(_state == WidgetState::Default); _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<Ui::CenterWrap<>>(_emptyList, std::move(icon.widget)));
Ui::AddSkip(_emptyList);
_emptyList->add(
object_ptr<Ui::FlatLabel>(
_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() { void InnerWidget::resizeEmpty() {
@ -4135,6 +4201,13 @@ void InnerWidget::resizeEmpty() {
_empty->resizeToWidth(width() - 2 * skip); _empty->resizeToWidth(width() - 2 * skip);
_empty->move(skip, (st::dialogsEmptyHeight - _empty->height()) / 2); _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) { if (_searchEmpty) {
_searchEmpty->resizeToWidth(width()); _searchEmpty->resizeToWidth(width());
_searchEmpty->move(0, searchedOffset()); _searchEmpty->move(0, searchedOffset());

View file

@ -43,6 +43,8 @@ namespace Ui {
class IconButton; class IconButton;
class PopupMenu; class PopupMenu;
class FlatLabel; class FlatLabel;
class VerticalLayout;
class RoundButton;
struct ScrollToRequest; struct ScrollToRequest;
namespace Controls { namespace Controls {
enum class QuickDialogAction; enum class QuickDialogAction;
@ -619,6 +621,8 @@ private:
object_ptr<SearchEmpty> _searchEmpty = { nullptr }; object_ptr<SearchEmpty> _searchEmpty = { nullptr };
SearchState _searchEmptyState; SearchState _searchEmptyState;
object_ptr<Ui::FlatLabel> _empty = { nullptr }; object_ptr<Ui::FlatLabel> _empty = { nullptr };
object_ptr<Ui::VerticalLayout> _emptyList = { nullptr };
object_ptr<Ui::RoundButton> _emptyButton = { nullptr };
Ui::DraggingScrollManager _draggingScroll; Ui::DraggingScrollManager _draggingScroll;

View file

@ -120,6 +120,10 @@ void MainList::unreadStateChanged(
const auto notify = !useClouded || wasState.known; const auto notify = !useClouded || wasState.known;
const auto notifier = unreadStateChangeNotifier(notify); const auto notifier = unreadStateChangeNotifier(notify);
_unreadState += nowState - wasState; _unreadState += nowState - wasState;
if (_unreadState.chatsMuted > _unreadState.chats
|| _unreadState.messagesMuted > _unreadState.messages) {
[[maybe_unused]] int a = 0;
}
if (updateCloudUnread) { if (updateCloudUnread) {
// Assert(nowState.known); // Assert(nowState.known);
_cloudUnreadState += nowState - wasState; _cloudUnreadState += nowState - wasState;
@ -145,6 +149,10 @@ void MainList::unreadEntryChanged(
} else { } else {
_unreadState -= state; _unreadState -= state;
} }
if (_unreadState.chatsMuted > _unreadState.chats
|| _unreadState.messagesMuted > _unreadState.messages) {
[[maybe_unused]] int a = 0;
}
if (updateCloudUnread) { if (updateCloudUnread) {
if (added) { if (added) {
_cloudUnreadState += state; _cloudUnreadState += state;

View file

@ -903,10 +903,7 @@ void Widget::chosenRow(const ChosenRow &row) {
} else if (const auto topic = row.key.topic()) { } else if (const auto topic = row.key.topic()) {
auto params = Window::SectionShow( auto params = Window::SectionShow(
Window::SectionShow::Way::ClearStack); Window::SectionShow::Way::ClearStack);
params.highlightPart.text = _searchState.query; params.highlight = Window::SearchHighlightId(_searchState.query);
if (!params.highlightPart.empty()) {
params.highlightPartOffsetHint = kSearchQueryOffsetHint;
}
if (row.newWindow) { if (row.newWindow) {
controller()->showInNewWindow( controller()->showInNewWindow(
Window::SeparateId(topic), Window::SeparateId(topic),
@ -973,15 +970,12 @@ void Widget::chosenRow(const ChosenRow &row) {
return; return;
} else if (history) { } else if (history) {
const auto peer = history->peer; const auto peer = history->peer;
const auto showAtMsgId = controller()->uniqueChatsInSearchResults() const auto showAtMsgId = controller()->uniqueChatsInSearchResults(
? ShowAtUnreadMsgId _searchState
: row.message.fullId.msg; ) ? ShowAtUnreadMsgId : row.message.fullId.msg;
auto params = Window::SectionShow( auto params = Window::SectionShow(
Window::SectionShow::Way::ClearStack); Window::SectionShow::Way::ClearStack);
params.highlightPart.text = _searchState.query; params.highlight = Window::SearchHighlightId(_searchState.query);
if (!params.highlightPart.empty()) {
params.highlightPartOffsetHint = kSearchQueryOffsetHint;
}
if (row.newWindow) { if (row.newWindow) {
controller()->showInNewWindow(peer, showAtMsgId); controller()->showInNewWindow(peer, showAtMsgId);
} else { } else {

View file

@ -1167,8 +1167,15 @@ Chat ParseChat(const MTPChat &data) {
result.colorIndex = (color && color->data().vcolor()) result.colorIndex = (color && color->data().vcolor())
? color->data().vcolor()->v ? color->data().vcolor()->v
: PeerColorIndex(result.bareId); : PeerColorIndex(result.bareId);
result.isMonoforum = data.is_monoforum();
result.isBroadcast = data.is_broadcast(); result.isBroadcast = data.is_broadcast();
result.isSupergroup = data.is_megagroup(); 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()); result.title = ParseString(data.vtitle());
if (const auto username = data.vusername()) { if (const auto username = data.vusername()) {
result.username = ParseString(*username); result.username = ParseString(*username);
@ -1188,15 +1195,6 @@ Chat ParseChat(const MTPChat &data) {
return result; return result;
} }
std::map<PeerId, Chat> ParseChatsList(const MTPVector<MTPChat> &data) {
auto result = std::map<PeerId, Chat>();
for (const auto &chat : data.v) {
auto parsed = ParseChat(chat);
result.emplace(parsed.id(), std::move(parsed));
}
return result;
}
Utf8String ContactInfo::name() const { Utf8String ContactInfo::name() const {
return firstName.isEmpty() return firstName.isEmpty()
? (lastName.isEmpty() ? (lastName.isEmpty()
@ -1273,6 +1271,20 @@ std::map<PeerId, Peer> ParsePeersLists(
auto parsed = ParseChat(chat); auto parsed = ParseChat(chat);
result.emplace(parsed.id(), Peer{ std::move(parsed) }); result.emplace(parsed.id(), Peer{ std::move(parsed) });
} }
for (auto &[peerId, parsed] : result) {
if (const auto chat = std::get_if<Chat>(&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; return result;
} }
@ -2191,7 +2203,13 @@ const DialogInfo *DialogsInfo::item(int index) const {
DialogInfo::Type DialogTypeFromChat(const Chat &chat) { DialogInfo::Type DialogTypeFromChat(const Chat &chat) {
using Type = DialogInfo::Type; 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 ? (chat.isBroadcast
? Type::PrivateChannel ? Type::PrivateChannel
: chat.isSupergroup : chat.isSupergroup
@ -2252,6 +2270,11 @@ DialogsInfo ParseDialogsInfo(const MTPmessages_Dialogs &data) {
info.migratedToChannelId = peer.chat() info.migratedToChannelId = peer.chat()
? peer.chat()->migratedToChannelId ? peer.chat()->migratedToChannelId
: 0; : 0;
info.isMonoforum = peer.chat()
&& peer.chat()->isMonoforum;
info.monoforumBroadcastInput = peer.chat()
? peer.chat()->monoforumBroadcastInput
: MTPInputPeer(MTP_inputPeerEmpty());
} }
info.topMessageId = fields.vtop_message().v; info.topMessageId = fields.vtop_message().v;
const auto messageIt = messages.find(MessageId{ const auto messageIt = messages.find(MessageId{
@ -2290,6 +2313,10 @@ DialogInfo DialogInfoFromChat(const Chat &data) {
result.topMessageId = 0; result.topMessageId = 0;
result.type = DialogTypeFromChat(data); result.type = DialogTypeFromChat(data);
result.migratedToChannelId = data.migratedToChannelId; result.migratedToChannelId = data.migratedToChannelId;
result.isMonoforum = data.isMonoforum;
if (data.isMonoforumAdmin) {
result.monoforumBroadcastInput = data.monoforumBroadcastInput;
}
return result; return result;
} }
@ -2424,7 +2451,8 @@ void FinalizeDialogsInfo(DialogsInfo &info, const Settings &settings) {
} }
Unexpected("Type in ApiWrap::onlyMyMessages."); 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); ranges::sort(dialog.splits);
} }

View file

@ -319,14 +319,19 @@ struct Chat {
Utf8String title; Utf8String title;
Utf8String username; Utf8String username;
uint8 colorIndex = 0; uint8 colorIndex = 0;
bool isMonoforum = false;
bool isBroadcast = false; bool isBroadcast = false;
bool isSupergroup = false; bool isSupergroup = false;
bool isMonoforumAdmin = false;
bool hasMonoforumAdminRights = false;
bool isMonoforumOfPublicBroadcast = false;
BareId monoforumLinkId = 0;
MTPInputPeer input = MTP_inputPeerEmpty(); MTPInputPeer input = MTP_inputPeerEmpty();
MTPInputPeer monoforumBroadcastInput = MTP_inputPeerEmpty();
}; };
Chat ParseChat(const MTPChat &data); Chat ParseChat(const MTPChat &data);
std::map<PeerId, Chat> ParseChatsList(const MTPVector<MTPChat> &data);
struct Peer { struct Peer {
PeerId id() const; PeerId id() const;
@ -952,12 +957,15 @@ struct DialogInfo {
MTPInputPeer migratedFromInput = MTP_inputPeerEmpty(); MTPInputPeer migratedFromInput = MTP_inputPeerEmpty();
ChannelId migratedToChannelId = 0; ChannelId migratedToChannelId = 0;
MTPInputPeer monoforumBroadcastInput = MTP_inputPeerEmpty();
// User messages splits which contained that dialog. // User messages splits which contained that dialog.
std::vector<int> splits; std::vector<int> splits;
// Filled after the whole dialogs list is accumulated. // Filled after the whole dialogs list is accumulated.
bool onlyMyMessages = false; bool onlyMyMessages = false;
bool isLeftChannel = false; bool isLeftChannel = false;
bool isMonoforum = false;
QString relativePath; QString relativePath;
// Filled when requesting dialog messages. // Filled when requesting dialog messages.

View file

@ -1370,7 +1370,7 @@ void ApiWrap::appendSinglePeerDialogs(Data::DialogsInfo &&info) {
if (isSupergroupType(info.type) && !migratedRequestId) { if (isSupergroupType(info.type) && !migratedRequestId) {
migratedRequestId = requestSinglePeerMigrated(info); migratedRequestId = requestSinglePeerMigrated(info);
continue; continue;
} else if (isChannelType(info.type)) { } else if (isChannelType(info.type) || info.isMonoforum) {
continue; continue;
} }
for (auto i = last; i != 0; --i) { for (auto i = last; i != 0; --i) {
@ -1642,6 +1642,9 @@ void ApiWrap::requestChatMessages(
const auto realPeerInput = (splitIndex >= 0) const auto realPeerInput = (splitIndex >= 0)
? _chatProcess->info.input ? _chatProcess->info.input
: _chatProcess->info.migratedFromInput; : _chatProcess->info.migratedFromInput;
const auto outgoingInput = _chatProcess->info.isMonoforum
? _chatProcess->info.monoforumBroadcastInput
: MTP_inputPeerSelf();
const auto realSplitIndex = (splitIndex >= 0) const auto realSplitIndex = (splitIndex >= 0)
? splitIndex ? splitIndex
: (splitsCount + splitIndex); : (splitsCount + splitIndex);
@ -1650,7 +1653,7 @@ void ApiWrap::requestChatMessages(
MTP_flags(MTPmessages_Search::Flag::f_from_id), MTP_flags(MTPmessages_Search::Flag::f_from_id),
realPeerInput, realPeerInput,
MTP_string(), // query MTP_string(), // query
MTP_inputPeerSelf(), outgoingInput,
MTPInputPeer(), // saved_peer_id MTPInputPeer(), // saved_peer_id
MTPVector<MTPReaction>(), // saved_reaction MTPVector<MTPReaction>(), // saved_reaction
MTPint(), // top_msg_id MTPint(), // top_msg_id

View file

@ -111,7 +111,8 @@ std::optional<MTPMessageReplyHeader> PrepareLogReply(
MTP_int(topId), MTP_int(topId),
MTPstring(), // quote_text MTPstring(), // quote_text
MTPVector<MTPMessageEntity>(), // quote_entities MTPVector<MTPMessageEntity>(), // quote_entities
MTPint()); // quote_offset MTPint(), // quote_offset
MTPint()); // todo_item_id
} }
} }
return {}; return {};

View file

@ -2380,7 +2380,7 @@ Dialogs::UnreadState History::chatListUnreadState() const {
return AdjustedForumUnreadState(forum->topicsList()->unreadState()); return AdjustedForumUnreadState(forum->topicsList()->unreadState());
} else if (const auto monoforum = peer->monoforum()) { } else if (const auto monoforum = peer->monoforum()) {
return AdjustedForumUnreadState( return AdjustedForumUnreadState(
monoforum->unreadStateWithParentMuted()); withMyMuted(monoforum->chatsList()->unreadState()));;
} }
return computeUnreadState(); return computeUnreadState();
} }
@ -2395,7 +2395,7 @@ Dialogs::BadgesState History::chatListBadgesState() const {
} else if (const auto monoforum = peer->monoforum()) { } else if (const auto monoforum = peer->monoforum()) {
return adjustBadgesStateByFolder( return adjustBadgesStateByFolder(
Dialogs::BadgesForUnread( Dialogs::BadgesForUnread(
monoforum->unreadStateWithParentMuted(), withMyMuted(monoforum->chatsList()->unreadState()),
Dialogs::CountInBadge::Chats, Dialogs::CountInBadge::Chats,
Dialogs::IncludeInBadge::All)); Dialogs::IncludeInBadge::All));
} }
@ -2440,6 +2440,16 @@ Dialogs::UnreadState History::computeUnreadState() const {
return result; 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() { void History::allowChatListMessageResolve() {
if (_flags & Flag::ResolveChatListMessage) { if (_flags & Flag::ResolveChatListMessage) {
return; return;
@ -3368,7 +3378,8 @@ bool History::isForum() const {
void History::monoforumChanged(Data::SavedMessages *old) { void History::monoforumChanged(Data::SavedMessages *old) {
if (inChatList()) { if (inChatList()) {
notifyUnreadStateChange(old notifyUnreadStateChange(old
? AdjustedForumUnreadState(old->chatsList()->unreadState()) ? AdjustedForumUnreadState(
withMyMuted(old->chatsList()->unreadState()))
: computeUnreadState()); : computeUnreadState());
} }
@ -3378,9 +3389,9 @@ void History::monoforumChanged(Data::SavedMessages *old) {
monoforum->chatsList()->unreadStateChanges( monoforum->chatsList()->unreadStateChanges(
) | rpl::filter([=] { ) | rpl::filter([=] {
return (_flags & Flag::IsMonoforumAdmin) && inChatList(); return (_flags & Flag::IsMonoforumAdmin) && inChatList();
}) | rpl::map( }) | rpl::map([=](const Dialogs::UnreadState &was) {
AdjustedForumUnreadState return AdjustedForumUnreadState(withMyMuted(was));
) | rpl::start_with_next([=](const Dialogs::UnreadState &old) { }) | rpl::start_with_next([=](const Dialogs::UnreadState &old) {
notifyUnreadStateChange(old); notifyUnreadStateChange(old);
}, monoforum->lifetime()); }, monoforum->lifetime());

View file

@ -602,6 +602,8 @@ private:
[[nodiscard]] Dialogs::BadgesState adjustBadgesStateByFolder( [[nodiscard]] Dialogs::BadgesState adjustBadgesStateByFolder(
Dialogs::BadgesState state) const; Dialogs::BadgesState state) const;
[[nodiscard]] Dialogs::UnreadState computeUnreadState() const; [[nodiscard]] Dialogs::UnreadState computeUnreadState() const;
[[nodiscard]] Dialogs::UnreadState withMyMuted(
Dialogs::UnreadState state) const;
void setFolderPointer(Data::Folder *folder); void setFolderPointer(Data::Folder *folder);
void hasUnreadMentionChanged(bool has) override; void hasUnreadMentionChanged(bool has) override;

View file

@ -674,10 +674,10 @@ void HistoryInner::setupSwipeReplyAndBack() {
: still)->fullId(); : still)->fullId();
_widget->replyToMessage({ _widget->replyToMessage({
.messageId = replyToItemId, .messageId = replyToItemId,
.quote = selected.text, .quote = selected.highlight.quote,
.quoteOffset = selected.offset, .quoteOffset = selected.highlight.quoteOffset,
}); });
if (!selected.text.empty()) { if (!selected.highlight.quote.empty()) {
_widget->clearSelected(); _widget->clearSelected();
} }
}; };
@ -2409,6 +2409,9 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
const auto linkUserpicPeerId = (link && _dragStateUserpic) const auto linkUserpicPeerId = (link && _dragStateUserpic)
? link->property(kPeerLinkPeerIdProperty).toULongLong() ? link->property(kPeerLinkPeerIdProperty).toULongLong()
: 0; : 0;
const auto todoListTaskId = link
? link->property(kTodoListItemIdProperty).toInt()
: 0;
const auto session = &this->session(); const auto session = &this->session();
_whoReactedMenuLifetime.destroy(); _whoReactedMenuLifetime.destroy();
if (!clickedReaction.empty() if (!clickedReaction.empty()
@ -2777,20 +2780,21 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
const auto selected = selectedQuote(item); const auto selected = selectedQuote(item);
auto text = (selected auto text = (selected
? tr::lng_context_quote_and_reply ? tr::lng_context_quote_and_reply
: todoListTaskId
? tr::lng_context_reply_to_task
: tr::lng_context_reply_msg)( : tr::lng_context_reply_msg)(
tr::now, tr::now,
Ui::Text::FixAmpersandInAction); Ui::Text::FixAmpersandInAction);
const auto replyToItem = selected.item ? selected.item : item; const auto replyToItem = selected.item ? selected.item : item;
const auto itemId = replyToItem->fullId(); const auto itemId = replyToItem->fullId();
const auto quote = selected.text;
const auto quoteOffset = selected.offset;
_menu->addAction(std::move(text), [=] { _menu->addAction(std::move(text), [=] {
_widget->replyToMessage({ _widget->replyToMessage({
.messageId = itemId, .messageId = itemId,
.quote = quote, .quote = selected.highlight.quote,
.quoteOffset = quoteOffset, .quoteOffset = selected.highlight.quoteOffset,
.todoItemId = todoListTaskId,
}); });
if (!quote.empty()) { if (!selected.highlight.quote.empty()) {
_widget->clearSelected(); _widget->clearSelected();
} }
}, &st::menuIconReply); }, &st::menuIconReply);
@ -2809,7 +2813,7 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
Window::PeerMenuAddTodoListTasks(_controller, item); Window::PeerMenuAddTodoListTasks(_controller, item);
} }
}), }),
&st::menuIconCreateTodoList); &st::menuIconAdd);
}; };
const auto lnkPhoto = link const auto lnkPhoto = link
? reinterpret_cast<PhotoData*>( ? reinterpret_cast<PhotoData*>(
@ -2955,11 +2959,9 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
: nullptr; : nullptr;
if (sponsored) { if (sponsored) {
Menu::FillSponsored( Menu::FillSponsored(
this,
Ui::Menu::CreateAddActionCallback(_menu), Ui::Menu::CreateAddActionCallback(_menu),
controller->uiShow(), controller->uiShow(),
sponsored->fullId(), sponsored->fullId());
false);
} }
if (isUponSelected > 0) { if (isUponSelected > 0) {
addReplyAction(item); addReplyAction(item);

View file

@ -964,12 +964,26 @@ void HistoryItem::updateServiceDependent(bool force) {
} }
if (!dependent->lnk) { if (!dependent->lnk) {
auto todoItemId = 0;
if (const auto done = Get<HistoryServiceTodoCompletions>()) {
const auto &items = !done->completed.empty()
? done->completed
: done->incompleted;
if (items.size() == 1) {
todoItemId = items.front();
}
} else if (const auto append = Get<HistoryServiceTodoAppendTasks>()) {
if (append->list.size() == 1) {
todoItemId = append->list.front().id;
}
}
dependent->lnk = JumpToMessageClickHandler( dependent->lnk = JumpToMessageClickHandler(
(dependent->peerId (dependent->peerId
? _history->owner().peer(dependent->peerId) ? _history->owner().peer(dependent->peerId)
: _history->peer), : _history->peer),
dependent->msgId, dependent->msgId,
fullId()); fullId(),
{ .todoItemId = todoItemId });
} }
auto gotDependencyItem = false; auto gotDependencyItem = false;
if (!dependent->msg) { if (!dependent->msg) {
@ -1858,7 +1872,10 @@ bool HistoryItem::isAyuNoForwards() const {
} }
bool HistoryItem::canLookupMessageAuthor() const { bool HistoryItem::canLookupMessageAuthor() const {
return isRegular() && _history->amMonoforumAdmin() && _from->isChannel(); return isRegular()
&& !isService()
&& _history->amMonoforumAdmin()
&& _from->isChannel();
} }
bool HistoryItem::skipNotification() const { bool HistoryItem::skipNotification() const {
@ -4392,6 +4409,7 @@ void HistoryItem::createComponentsHelper(HistoryItemCommonFields &&fields) {
: replyTo.monoforumPeerId : replyTo.monoforumPeerId
? replyTo.monoforumPeerId ? replyTo.monoforumPeerId
: PeerId(); : PeerId();
config.reply.todoItemId = replyTo.todoItemId;
const auto replyToTop = replyTo.topicRootId const auto replyToTop = replyTo.topicRootId
? replyTo.topicRootId ? replyTo.topicRootId
: LookupReplyToTop(_history, to); : LookupReplyToTop(_history, to);

View file

@ -390,6 +390,7 @@ ReplyFields ReplyFieldsFromMTP(
= data.vreply_to_top_id().value_or(result.messageId.bare); = data.vreply_to_top_id().value_or(result.messageId.bare);
result.topicPost = data.is_forum_topic() ? 1 : 0; result.topicPost = data.is_forum_topic() ? 1 : 0;
} }
result.todoItemId = data.vtodo_item_id().value_or_empty();
if (const auto header = data.vreply_from()) { if (const auto header = data.vreply_from()) {
const auto &data = header->data(); const auto &data = header->data();
result.externalPostAuthor result.externalPostAuthor

View file

@ -277,6 +277,7 @@ struct ReplyFields {
MsgId messageId = 0; MsgId messageId = 0;
MsgId topMessageId = 0; MsgId topMessageId = 0;
StoryId storyId = 0; StoryId storyId = 0;
int todoItemId = 0;
uint32 quoteOffset : 30 = 0; uint32 quoteOffset : 30 = 0;
uint32 manualQuote : 1 = 0; uint32 manualQuote : 1 = 0;
uint32 topicPost : 1 = 0; uint32 topicPost : 1 = 0;

View file

@ -722,22 +722,19 @@ bool IsItemScheduledUntilOnline(not_null<const HistoryItem*> item) {
ClickHandlerPtr JumpToMessageClickHandler( ClickHandlerPtr JumpToMessageClickHandler(
not_null<HistoryItem*> item, not_null<HistoryItem*> item,
FullMsgId returnToId, FullMsgId returnToId,
TextWithEntities highlightPart, MessageHighlightId highlight) {
int highlightPartOffsetHint) {
return JumpToMessageClickHandler( return JumpToMessageClickHandler(
item->history()->peer, item->history()->peer,
item->id, item->id,
returnToId, returnToId,
std::move(highlightPart), std::move(highlight));
highlightPartOffsetHint);
} }
ClickHandlerPtr JumpToMessageClickHandler( ClickHandlerPtr JumpToMessageClickHandler(
not_null<PeerData*> peer, not_null<PeerData*> peer,
MsgId msgId, MsgId msgId,
FullMsgId returnToId, FullMsgId returnToId,
TextWithEntities highlightPart, MessageHighlightId highlight) {
int highlightPartOffsetHint) {
return std::make_shared<LambdaClickHandler>([=] { return std::make_shared<LambdaClickHandler>([=] {
const auto separate = Core::App().separateWindowFor(peer); const auto separate = Core::App().separateWindowFor(peer);
const auto controller = separate const auto controller = separate
@ -747,8 +744,7 @@ ClickHandlerPtr JumpToMessageClickHandler(
auto params = Window::SectionShow{ auto params = Window::SectionShow{
Window::SectionShow::Way::Forward Window::SectionShow::Way::Forward
}; };
params.highlightPart = highlightPart; params.highlight = highlight;
params.highlightPartOffsetHint = highlightPartOffsetHint;
params.origin = Window::SectionShow::OriginMessage{ params.origin = Window::SectionShow::OriginMessage{
returnToId returnToId
}; };
@ -910,7 +906,8 @@ MTPMessageReplyHeader NewMessageReplyHeader(const Api::SendAction &action) {
| Flag::f_quote_offset)) | Flag::f_quote_offset))
| (quoteEntities.v.empty() | (quoteEntities.v.empty()
? Flag() ? Flag()
: Flag::f_quote_entities)), : Flag::f_quote_entities)
| (replyTo.todoItemId ? Flag::f_todo_item_id : Flag())),
MTP_int(replyTo.messageId.msg), MTP_int(replyTo.messageId.msg),
peerToMTP(externalPeerId), peerToMTP(externalPeerId),
MTPMessageFwdHeader(), // reply_from MTPMessageFwdHeader(), // reply_from
@ -918,7 +915,8 @@ MTPMessageReplyHeader NewMessageReplyHeader(const Api::SendAction &action) {
MTP_int(replyToTop), MTP_int(replyToTop),
MTP_string(replyTo.quote.text), MTP_string(replyTo.quote.text),
quoteEntities, quoteEntities,
MTP_int(replyTo.quoteOffset)); MTP_int(replyTo.quoteOffset),
MTP_int(replyTo.todoItemId));
} }
return MTPMessageReplyHeader(); return MTPMessageReplyHeader();
} }

View file

@ -229,13 +229,11 @@ private:
not_null<PeerData*> peer, not_null<PeerData*> peer,
MsgId msgId, MsgId msgId,
FullMsgId returnToId = FullMsgId(), FullMsgId returnToId = FullMsgId(),
TextWithEntities highlightPart = {}, MessageHighlightId highlight = {});
int highlightPartOffsetHint = 0);
[[nodiscard]] ClickHandlerPtr JumpToMessageClickHandler( [[nodiscard]] ClickHandlerPtr JumpToMessageClickHandler(
not_null<HistoryItem*> item, not_null<HistoryItem*> item,
FullMsgId returnToId = FullMsgId(), FullMsgId returnToId = FullMsgId(),
TextWithEntities highlightPart = {}, MessageHighlightId highlight = {});
int highlightPartOffsetHint = 0);
[[nodiscard]] ClickHandlerPtr JumpToStoryClickHandler( [[nodiscard]] ClickHandlerPtr JumpToStoryClickHandler(
not_null<Data::Story*> story); not_null<Data::Story*> story);
ClickHandlerPtr JumpToStoryClickHandler( ClickHandlerPtr JumpToStoryClickHandler(

View file

@ -65,6 +65,7 @@ Ui::ChatPaintHighlight ElementHighlighter::state(
if (item->fullId() == _highlighted.itemId) { if (item->fullId() == _highlighted.itemId) {
auto result = _animation.state(); auto result = _animation.state();
result.range = _highlighted.part; result.range = _highlighted.part;
result.todoItemId = _highlighted.todoListId;
return result; return result;
} }
return {}; return {};
@ -82,19 +83,27 @@ ElementHighlighter::Highlight ElementHighlighter::computeHighlight(
const auto i = ranges::find(group->items, item); const auto i = ranges::find(group->items, item);
if (i != end(group->items)) { if (i != end(group->items)) {
const auto index = int(i - begin(group->items)); const auto index = int(i - begin(group->items));
if (quote.text.empty()) { if (quote.highlight.empty()) {
return { leaderId, AddGroupItemSelection({}, index) }; return { leaderId, AddGroupItemSelection({}, index) };
} else if (const auto leaderView = _viewForItem(leader)) { } else if (const auto leaderView = _viewForItem(leader)) {
return { leaderId, leaderView->selectionFromQuote(quote) }; return {
leaderId,
leaderView->selectionFromQuote(quote),
quote.highlight.todoItemId,
};
} }
} }
return { leaderId }; return { leaderId, {}, quote.highlight.todoItemId };
} else if (quote.text.empty()) { } else if (quote.highlight.quote.empty()) {
return { item->fullId() }; return { item->fullId(), {}, quote.highlight.todoItemId };
} else if (const auto view = _viewForItem(item)) { } 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) { void ElementHighlighter::highlight(Highlight data) {
@ -108,7 +117,7 @@ void ElementHighlighter::highlight(Highlight data) {
} }
} }
_highlighted = data; _highlighted = data;
_animation.start(!data.part.empty() _animation.start((!data.part.empty() || data.todoListId)
&& !IsSubGroupSelection(data.part)); && !IsSubGroupSelection(data.part));
repaintHighlightedItem(view); repaintHighlightedItem(view);

View file

@ -65,6 +65,7 @@ private:
struct Highlight { struct Highlight {
FullMsgId itemId; FullMsgId itemId;
TextSelection part; TextSelection part;
int todoListId = 0;
explicit operator bool() const { explicit operator bool() const {
return itemId.operator bool(); return itemId.operator bool();

View file

@ -68,6 +68,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_changes.h" #include "data/data_changes.h"
#include "data/data_drafts.h" #include "data/data_drafts.h"
#include "data/data_session.h" #include "data/data_session.h"
#include "data/data_todo_list.h"
#include "data/data_web_page.h" #include "data/data_web_page.h"
#include "data/data_document.h" #include "data/data_document.h"
#include "data/data_photo.h" #include "data/data_photo.h"
@ -1575,12 +1576,21 @@ int HistoryWidget::itemTopForHighlight(
const auto heightLeft = (visibleAreaHeight - viewHeight); const auto heightLeft = (visibleAreaHeight - viewHeight);
if (heightLeft >= 0) { if (heightLeft >= 0) {
return std::max(itemTop - (heightLeft / 2), 0); return std::max(itemTop - (heightLeft / 2), 0);
} else if (const auto sel = itemHighlight(item).range } else if (const auto highlight = itemHighlight(item)
; !sel.empty() && !IsSubGroupSelection(sel)) { ; (!highlight.range.empty() || highlight.todoItemId)
&& !IsSubGroupSelection(highlight.range)) {
const auto sel = highlight.range;
const auto single = st::messageTextStyle.font->height; const auto single = st::messageTextStyle.font->height;
const auto begin = HistoryView::FindViewY(view, sel.from) - single; const auto todoy = sel.empty()
const auto end = HistoryView::FindViewY(view, sel.to, begin + single) ? HistoryView::FindViewTaskY(view, highlight.todoItemId)
+ 2 * single; : 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; auto result = itemTop;
if (end > visibleAreaHeight) { if (end > visibleAreaHeight) {
result = std::max(result, itemTop + end - visibleAreaHeight); result = std::max(result, itemTop + end - visibleAreaHeight);
@ -5797,8 +5807,7 @@ void HistoryWidget::switchToSearch(QString query) {
const auto item = activation.item; const auto item = activation.item;
auto params = ::Window::SectionShow( auto params = ::Window::SectionShow(
::Window::SectionShow::Way::ClearStack); ::Window::SectionShow::Way::ClearStack);
params.highlightPart = { activation.query }; params.highlight = Window::SearchHighlightId(activation.query);
params.highlightPartOffsetHint = kSearchQueryOffsetHint;
controller()->showPeerHistory( controller()->showPeerHistory(
item->history()->peer->id, item->history()->peer->id,
params, params,
@ -6907,8 +6916,7 @@ int HistoryWidget::countInitialScrollTop() {
enqueueMessageHighlight({ enqueueMessageHighlight({
item, item,
base::take(_showAtMsgParams.highlightPart), base::take(_showAtMsgParams.highlight),
base::take(_showAtMsgParams.highlightPartOffsetHint),
}); });
const auto result = itemTopForHighlight(view); const auto result = itemTopForHighlight(view);
createUnreadBarIfBelowVisibleArea(result); createUnreadBarIfBelowVisibleArea(result);
@ -7670,12 +7678,7 @@ void HistoryWidget::editDraftOptions() {
void HistoryWidget::jumpToReply(FullReplyTo to) { void HistoryWidget::jumpToReply(FullReplyTo to) {
if (const auto item = session().data().message(to.messageId)) { if (const auto item = session().data().message(to.messageId)) {
JumpToMessageClickHandler( JumpToMessageClickHandler(item, {}, to.highlight())->onClick({});
item,
{},
to.quote,
to.quoteOffset
)->onClick({});
} }
} }
@ -8718,7 +8721,7 @@ void HistoryWidget::clearFieldText(
void HistoryWidget::replyToMessage(FullReplyTo id) { void HistoryWidget::replyToMessage(FullReplyTo id) {
if (const auto item = session().data().message(id.messageId)) { if (const auto item = session().data().message(id.messageId)) {
if (CanSendReply(item) && !base::IsCtrlPressed()) { if (CanSendReply(item) && !base::IsCtrlPressed()) {
replyToMessage(item, id.quote, id.quoteOffset); replyToMessage(item, id);
} else if (item->allowsForward()) { } else if (item->allowsForward()) {
const auto show = controller()->uiShow(); const auto show = controller()->uiShow();
HistoryView::Controls::ShowReplyToChatBox(show, id); HistoryView::Controls::ShowReplyToChatBox(show, id);
@ -8731,16 +8734,12 @@ void HistoryWidget::replyToMessage(FullReplyTo id) {
void HistoryWidget::replyToMessage( void HistoryWidget::replyToMessage(
not_null<HistoryItem*> item, not_null<HistoryItem*> item,
TextWithEntities quote, FullReplyTo fields) {
int quoteOffset) {
if (isJoinChannel()) { if (isJoinChannel()) {
return; return;
} }
_processingReplyTo = { fields.messageId = item->fullId();
.messageId = item->fullId(), _processingReplyTo = fields;
.quote = quote,
.quoteOffset = quoteOffset,
};
_processingReplyItem = item; _processingReplyItem = item;
processReply(); processReply();
} }
@ -9429,11 +9428,24 @@ void HistoryWidget::updateReplyEditText(not_null<HistoryItem*> item) {
.session = &session(), .session = &session(),
.repaint = [=] { updateField(); }, .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( _replyEditMsgText.setMarkedText(
st::defaultTextStyle, st::defaultTextStyle,
((_editMsgId || _replyTo.quote.empty()) text,
? item->inReplyText()
: _replyTo.quote),
Ui::DialogTextOptions(), Ui::DialogTextOptions(),
context); context);
if (fieldOrDisabledShown() || isRecording()) { if (fieldOrDisabledShown() || isRecording()) {
@ -9519,10 +9531,9 @@ void HistoryWidget::updateReplyToName() {
.customEmojiLoopLimit = 1, .customEmojiLoopLimit = 1,
}); });
const auto to = _replyEditMsg ? _replyEditMsg : _kbReplyTo; const auto to = _replyEditMsg ? _replyEditMsg : _kbReplyTo;
const auto replyToQuote = _replyTo && !_replyTo.quote.empty();
_replyToName.setMarkedText( _replyToName.setMarkedText(
st::fwdTextStyle, st::fwdTextStyle,
HistoryView::Reply::ComposePreviewName(_history, to, replyToQuote), HistoryView::Reply::ComposePreviewName(_history, to, _replyTo),
Ui::NameTextOptions(), Ui::NameTextOptions(),
context); context);
} }

View file

@ -205,8 +205,7 @@ public:
void replyToMessage(FullReplyTo id); void replyToMessage(FullReplyTo id);
void replyToMessage( void replyToMessage(
not_null<HistoryItem*> item, not_null<HistoryItem*> item,
TextWithEntities quote = {}, FullReplyTo fields = {});
int quoteOffset = 0);
void editMessage( void editMessage(
not_null<HistoryItem*> item, not_null<HistoryItem*> item,
const TextSelection &selection); const TextSelection &selection);

View file

@ -492,10 +492,9 @@ void FieldHeader::setShownMessage(HistoryItem *item) {
.customEmojiLoopLimit = 1, .customEmojiLoopLimit = 1,
}); });
const auto replyTo = _replyTo.current(); const auto replyTo = _replyTo.current();
const auto quote = replyTo && !replyTo.quote.empty();
_shownMessageName.setMarkedText( _shownMessageName.setMarkedText(
st::fwdTextStyle, st::fwdTextStyle,
HistoryView::Reply::ComposePreviewName(_history, item, quote), HistoryView::Reply::ComposePreviewName(_history, item, replyTo),
Ui::NameTextOptions(), Ui::NameTextOptions(),
context); context);
} else { } else {

View file

@ -718,8 +718,7 @@ void DraftOptionsBox(
state->link = args.usedLink; state->link = args.usedLink;
state->quote = SelectedQuote{ state->quote = SelectedQuote{
replyItem, replyItem,
draft.reply.quote, { draft.reply.quote, draft.reply.quoteOffset },
draft.reply.quoteOffset,
}; };
state->forward = std::move(args.forward); state->forward = std::move(args.forward);
state->webpage = draft.webpage; state->webpage = draft.webpage;
@ -783,7 +782,7 @@ void DraftOptionsBox(
box->setTitle(hasLink box->setTitle(hasLink
? tr::lng_link_options_header() ? tr::lng_link_options_header()
: hasReply : hasReply
? (state->quote.current().text.empty() ? (state->quote.current().highlight.quote.empty()
? tr::lng_reply_options_header() ? tr::lng_reply_options_header()
: tr::lng_reply_options_quote()) : tr::lng_reply_options_quote())
: (forwardCount == 1) : (forwardCount == 1)
@ -807,10 +806,12 @@ void DraftOptionsBox(
auto result = draft.reply; auto result = draft.reply;
if (const auto current = state->quote.current()) { if (const auto current = state->quote.current()) {
result.messageId = current.item->fullId(); result.messageId = current.item->fullId();
result.quote = current.text; result.quote = current.highlight.quote;
result.quoteOffset = current.offset; result.quoteOffset = current.highlight.quoteOffset;
// result.todoItemId = current.highlight.todoItemId;
} else { } else {
result.quote = {}; result.quote = {};
// result.todoItemId = 0;
} }
return result; return result;
}; };
@ -1112,7 +1113,7 @@ void DraftOptionsBox(
state->quote.value(), state->quote.value(),
state->shown.value() state->shown.value()
) | rpl::map([=](const SelectedQuote &quote, Section shown) { ) | rpl::map([=](const SelectedQuote &quote, Section shown) {
return (quote.text.empty() || shown != Section::Reply) return (quote.highlight.quote.empty() || shown != Section::Reply)
? tr::lng_settings_save() ? tr::lng_settings_save()
: tr::lng_reply_quote_selected(); : tr::lng_reply_quote_selected();
}) | rpl::flatten_latest(); }) | rpl::flatten_latest();

View file

@ -123,12 +123,10 @@ rpl::producer<Ui::MessageBarContent> RootViewContent(
ChatMemento::ChatMemento( ChatMemento::ChatMemento(
ChatViewId id, ChatViewId id,
MsgId highlightId, MsgId highlightId,
const TextWithEntities &highlightPart, MessageHighlightId highlight)
int highlightPartOffsetHint)
: _id(id) : _id(id)
, _highlightPart(highlightPart) , _highlightId(highlightId)
, _highlightPartOffsetHint(highlightPartOffsetHint) , _highlight(std::move(highlight)) {
, _highlightId(highlightId) {
if (highlightId || _id.sublist) { if (highlightId || _id.sublist) {
_list.setAroundPosition({ _list.setAroundPosition({
.fullId = FullMsgId(_id.history->peer->id, highlightId), .fullId = FullMsgId(_id.history->peer->id, highlightId),
@ -884,12 +882,7 @@ void ChatWidget::setupComposeControls() {
_composeControls->jumpToItemRequests( _composeControls->jumpToItemRequests(
) | rpl::start_with_next([=](FullReplyTo to) { ) | rpl::start_with_next([=](FullReplyTo to) {
if (const auto item = session().data().message(to.messageId)) { if (const auto item = session().data().message(to.messageId)) {
JumpToMessageClickHandler( JumpToMessageClickHandler(item, {}, to.highlight())->onClick({});
item,
{},
to.quote,
to.quoteOffset
)->onClick({});
} }
}, lifetime()); }, lifetime());
@ -1047,8 +1040,9 @@ void ChatWidget::setupSwipeReplyAndBack() {
: still)->fullId(); : still)->fullId();
_inner->replyToMessageRequestNotify({ _inner->replyToMessageRequestNotify({
.messageId = replyToItemId, .messageId = replyToItemId,
.quote = selected.text, .quote = selected.highlight.quote,
.quoteOffset = selected.offset, .quoteOffset = selected.highlight.quoteOffset,
.todoItemId = selected.highlight.todoItemId,
}); });
}; };
return result; return result;
@ -2648,8 +2642,7 @@ void ChatWidget::restoreState(not_null<ChatMemento*> memento) {
auto params = Window::SectionShow( auto params = Window::SectionShow(
Window::SectionShow::Way::Forward, Window::SectionShow::Way::Forward,
anim::type::instant); anim::type::instant);
params.highlightPart = memento->highlightPart(); params.highlight = memento->highlight();
params.highlightPartOffsetHint = memento->highlightPartOffsetHint();
showAtPosition(Data::MessagePosition{ showAtPosition(Data::MessagePosition{
.fullId = FullMsgId(_peer->id, highlight), .fullId = FullMsgId(_peer->id, highlight),
.date = TimeId(0), .date = TimeId(0),
@ -3452,8 +3445,7 @@ bool ChatWidget::searchInChatEmbedded(
const auto item = activation.item; const auto item = activation.item;
auto params = ::Window::SectionShow( auto params = ::Window::SectionShow(
::Window::SectionShow::Way::ClearStack); ::Window::SectionShow::Way::ClearStack);
params.highlightPart = { activation.query }; params.highlight = Window::SearchHighlightId(activation.query);
params.highlightPartOffsetHint = kSearchQueryOffsetHint;
controller()->showPeerHistory( controller()->showPeerHistory(
item->history()->peer->id, item->history()->peer->id,
params, params,

View file

@ -461,8 +461,7 @@ public:
explicit ChatMemento( explicit ChatMemento(
ChatViewId id, ChatViewId id,
MsgId highlightId = 0, MsgId highlightId = 0,
const TextWithEntities &highlightPart = {}, MessageHighlightId highlight = {});
int highlightPartOffsetHint = 0);
struct Comments { struct Comments {
}; };
@ -511,20 +510,16 @@ public:
[[nodiscard]] MsgId highlightId() const { [[nodiscard]] MsgId highlightId() const {
return _highlightId; return _highlightId;
} }
[[nodiscard]] const TextWithEntities &highlightPart() const { [[nodiscard]] const MessageHighlightId &highlight() const {
return _highlightPart; return _highlight;
}
[[nodiscard]] int highlightPartOffsetHint() const {
return _highlightPartOffsetHint;
} }
private: private:
void setupTopicViewer(); void setupTopicViewer();
ChatViewId _id; ChatViewId _id;
const TextWithEntities _highlightPart;
const int _highlightPartOffsetHint = 0;
const MsgId _highlightId = 0; const MsgId _highlightId = 0;
const MessageHighlightId _highlight;
ListMemento _list; ListMemento _list;
std::shared_ptr<Data::RepliesList> _replies; std::shared_ptr<Data::RepliesList> _replies;
QVector<FullMsgId> _replyReturns; QVector<FullMsgId> _replyReturns;

View file

@ -644,8 +644,13 @@ bool AddReplyToMessageAction(
return false; return false;
} }
const auto todoListTaskId = request.link
? request.link->property(kTodoListItemIdProperty).toInt()
: 0;
const auto &quote = request.quote; const auto &quote = 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_reply_msg
: tr::lng_context_quote_and_reply)( : tr::lng_context_quote_and_reply)(
tr::now, tr::now,
@ -653,8 +658,9 @@ bool AddReplyToMessageAction(
menu->addAction(std::move(text), [=, itemId = item->fullId()] { menu->addAction(std::move(text), [=, itemId = item->fullId()] {
list->replyToMessageRequestNotify({ list->replyToMessageRequestNotify({
.messageId = itemId, .messageId = itemId,
.quote = quote.text, .quote = quote.highlight.quote,
.quoteOffset = quote.offset, .quoteOffset = quote.highlight.quoteOffset,
.todoItemId = todoListTaskId,
}, base::IsCtrlPressed()); }, base::IsCtrlPressed());
}, &st::menuIconReply); }, &st::menuIconReply);
return true; return true;
@ -680,7 +686,7 @@ bool AddTodoListAction(
if (const auto item = controller->session().data().message(itemId)) { if (const auto item = controller->session().data().message(itemId)) {
Window::PeerMenuAddTodoListTasks(controller, item); Window::PeerMenuAddTodoListTasks(controller, item);
} }
}, &st::menuIconCreateTodoList); }, &st::menuIconAdd);
return true; return true;
} }

View file

@ -46,6 +46,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_channel.h" #include "data/data_channel.h"
#include "data/data_saved_sublist.h" #include "data/data_saved_sublist.h"
#include "data/data_session.h" #include "data/data_session.h"
#include "data/data_todo_list.h"
#include "data/data_forum.h" #include "data/data_forum.h"
#include "data/data_forum_topic.h" #include "data/data_forum_topic.h"
#include "data/data_message_reactions.h" #include "data/data_message_reactions.h"
@ -1360,9 +1361,18 @@ void Element::validateText() {
if (const auto done = item->Get<HistoryServiceTodoCompletions>()) { if (const auto done = item->Get<HistoryServiceTodoCompletions>()) {
if (!done->completed.empty() && !done->incompleted.empty()) { if (!done->completed.empty() && !done->incompleted.empty()) {
const auto todoItemId = (done->incompleted.size() == 1)
? done->incompleted.front()
: 0;
setServicePreMessage( setServicePreMessage(
item->composeTodoIncompleted(done), item->composeTodoIncompleted(done),
done->lnk); JumpToMessageClickHandler(
(done->peerId
? history()->owner().peer(done->peerId)
: history()->peer),
done->msgId,
item->fullId(),
{ .todoItemId = todoItemId }));
} else { } else {
setServicePreMessage({}); setServicePreMessage({});
} }
@ -2205,7 +2215,7 @@ SelectedQuote Element::FindSelectedQuote(
++i; ++i;
} }
} }
return { item, result, modified.from, overflown }; return { item, { result, modified.from }, overflown };
} }
TextSelection Element::FindSelectionFromQuote( TextSelection Element::FindSelectionFromQuote(
@ -2213,17 +2223,18 @@ TextSelection Element::FindSelectionFromQuote(
const SelectedQuote &quote) { const SelectedQuote &quote) {
Expects(quote.item != nullptr); Expects(quote.item != nullptr);
if (quote.text.empty()) { const auto &rich = quote.highlight.quote;
if (rich.empty()) {
return {}; return {};
} }
const auto &original = quote.item->originalText(); const auto &original = quote.item->originalText();
if (quote.offset == kSearchQueryOffsetHint) { if (quote.highlight.quoteOffset == kSearchQueryOffsetHint) {
return ApplyModificationsFrom( return ApplyModificationsFrom(
FindSearchQueryHighlight(original.text, quote.text.text), FindSearchQueryHighlight(original.text, rich.text),
text); text);
} }
const auto length = int(original.text.size()); 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) { const auto checkAt = [&](int offset) {
return TextSelection{ return TextSelection{
uint16(offset), uint16(offset),
@ -2234,7 +2245,7 @@ TextSelection Element::FindSelectionFromQuote(
if (offset > length - qlength) { if (offset > length - qlength) {
return TextSelection(); 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(); return (i >= 0) ? checkAt(i) : TextSelection();
}; };
const auto findOneBefore = [&](int offset) { const auto findOneBefore = [&](int offset) {
@ -2243,7 +2254,7 @@ TextSelection Element::FindSelectionFromQuote(
} }
const auto end = std::min(offset + qlength - 1, length); const auto end = std::min(offset + qlength - 1, length);
const auto from = end - length - 1; 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(); return (i >= 0) ? checkAt(i) : TextSelection();
}; };
const auto findAfter = [&](int offset) { const auto findAfter = [&](int offset) {
@ -2281,7 +2292,7 @@ TextSelection Element::FindSelectionFromQuote(
? before ? before
: after; : after;
}; };
auto result = findTwoWays(quote.offset); auto result = findTwoWays(quote.highlight.quoteOffset);
if (result.empty()) { if (result.empty()) {
return {}; return {};
} }
@ -2468,6 +2479,70 @@ int FindViewY(not_null<Element*> view, uint16 symbol, int yfrom) {
return origin.y() + (yfrom + ytill) / 2; return origin.y() + (yfrom + ytill) / 2;
} }
int FindViewTaskY(not_null<Element*> 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) { Window::SessionController *ExtractController(const ClickContext &context) {
const auto my = context.other.value<ClickHandlerContext>(); const auto my = context.other.value<ClickHandlerContext>();
if (const auto controller = my.sessionWindow.get()) { if (const auto controller = my.sessionWindow.get()) {

View file

@ -357,12 +357,11 @@ struct TopicButton {
struct SelectedQuote { struct SelectedQuote {
HistoryItem *item = nullptr; HistoryItem *item = nullptr;
TextWithEntities text; MessageHighlightId highlight;
int offset = 0;
bool overflown = false; bool overflown = false;
explicit operator bool() const { explicit operator bool() const {
return item && !text.empty(); return item && !highlight.quote.empty();
} }
friend inline bool operator==(SelectedQuote, SelectedQuote) = default; friend inline bool operator==(SelectedQuote, SelectedQuote) = default;
}; };
@ -748,6 +747,11 @@ private:
uint16 symbol, uint16 symbol,
int yfrom = 0); int yfrom = 0);
[[nodiscard]] int FindViewTaskY(
not_null<Element*> view,
int taskId,
int yfrom = 0);
[[nodiscard]] Window::SessionController *ExtractController( [[nodiscard]] Window::SessionController *ExtractController(
const ClickContext &context); const ClickContext &context);

View file

@ -720,12 +720,21 @@ std::optional<int> ListWidget::scrollTopForView(
const auto heightLeft = (available - height); const auto heightLeft = (available - height);
if (heightLeft >= 0) { if (heightLeft >= 0) {
return std::max(top - (heightLeft / 2), 0); return std::max(top - (heightLeft / 2), 0);
} else if (const auto sel = _highlighter.state(view->data()).range } else if (const auto highlight = _highlighter.state(view->data())
; !sel.empty() && !IsSubGroupSelection(sel)) { ; (!highlight.range.empty() || highlight.todoItemId)
&& !IsSubGroupSelection(highlight.range)) {
const auto sel = highlight.range;
const auto single = st::messageTextStyle.font->height; const auto single = st::messageTextStyle.font->height;
const auto begin = HistoryView::FindViewY(view, sel.from) - single; const auto todoy = sel.empty()
const auto end = HistoryView::FindViewY(view, sel.to, begin + single) ? HistoryView::FindViewTaskY(view, highlight.todoItemId)
+ 2 * single; : 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; auto result = top;
if (end > available) { if (end > available) {
result = std::max(result, top + end - available); result = std::max(result, top + end - available);
@ -822,10 +831,9 @@ bool ListWidget::isBelowPosition(Data::MessagePosition position) const {
void ListWidget::highlightMessage( void ListWidget::highlightMessage(
FullMsgId itemId, FullMsgId itemId,
const TextWithEntities &part, const MessageHighlightId &highlight) {
int partOffsetHint) {
if (const auto view = viewForItem(itemId)) { 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 if (position != Data::MaxMessagePosition
&& position != Data::UnreadMessagePosition) { && position != Data::UnreadMessagePosition) {
const auto hasHighlight = !params.highlightPart.empty(); const auto hasHighlight = !params.highlight.empty();
highlightMessage( highlightMessage(position.fullId, params.highlight);
position.fullId,
params.highlightPart,
params.highlightPartOffsetHint);
if (hasHighlight) { if (hasHighlight) {
// We may want to scroll to a different part of the message. // We may want to scroll to a different part of the message.
scrollTop = scrollTopForPosition(position); scrollTop = scrollTopForPosition(position);

View file

@ -314,8 +314,7 @@ public:
bool isBelowPosition(Data::MessagePosition position) const; bool isBelowPosition(Data::MessagePosition position) const;
void highlightMessage( void highlightMessage(
FullMsgId itemId, FullMsgId itemId,
const TextWithEntities &part, const MessageHighlightId &highlight);
int partOffsetHint);
void showAtPosition( void showAtPosition(
Data::MessagePosition position, Data::MessagePosition position,

View file

@ -490,6 +490,8 @@ void Message::initPaidInformation() {
refreshSuggestedInfo(item, suggest, replyData); refreshSuggestedInfo(item, suggest, replyData);
} }
return; return;
} else if (!item->history()->peer->isUser()) {
return;
} }
const auto media = this->media(); const auto media = this->media();
const auto mine = PaidInformation{ const auto mine = PaidInformation{
@ -3348,7 +3350,7 @@ TextSelection Message::selectionFromQuote(
const SelectedQuote &quote) const { const SelectedQuote &quote) const {
Expects(quote.item != nullptr); Expects(quote.item != nullptr);
if (quote.text.empty()) { if (quote.highlight.quote.empty()) {
return {}; return {};
} }
const auto item = quote.item; const auto item = quote.item;

View file

@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_peer.h" #include "data/data_peer.h"
#include "data/data_session.h" #include "data/data_session.h"
#include "data/data_story.h" #include "data/data_story.h"
#include "data/data_todo_list.h"
#include "data/data_user.h" #include "data/data_user.h"
#include "history/view/history_view_item_preview.h" #include "history/view/history_view_item_preview.h"
#include "history/history.h" #include "history/history.h"
@ -42,6 +43,85 @@ namespace {
constexpr auto kNonExpandedLinesLimit = 5; 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<Main::Session*> session) {
return Ui::Text::SingleCustomEmoji(
session->data().customEmojiManager().registerInternalEmoji(
MakeTaskDoneImage(),
QMargins(0, st::lineWidth, st::lineWidth, 0)));
}
[[nodiscard]] TextWithEntities TaskIcon(not_null<Main::Session*> session) {
return Ui::Text::SingleCustomEmoji(
session->data().customEmojiManager().registerInternalEmoji(
MakeTaskImage(),
QMargins(0, st::lineWidth, st::lineWidth, 0)));
}
} // namespace } // namespace
void ValidateBackgroundEmoji( void ValidateBackgroundEmoji(
@ -197,6 +277,22 @@ void Reply::update(
const auto item = view->data(); const auto item = view->data();
const auto &fields = data->fields(); const auto &fields = data->fields();
const auto message = data->resolvedMessage.get(); 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 story = data->resolvedStory.get();
const auto externalMedia = fields.externalMedia.get(); const auto externalMedia = fields.externalMedia.get();
if (!_externalSender) { if (!_externalSender) {
@ -214,7 +310,6 @@ void Reply::update(
_hiddenSenderColorIndexPlusOne = (!_colorPeer && message) _hiddenSenderColorIndexPlusOne = (!_colorPeer && message)
? (message->originalHiddenSenderInfo()->colorIndex + 1) ? (message->originalHiddenSenderInfo()->colorIndex + 1)
: 0; : 0;
const auto hasPreview = (story && story->hasReplyPreview()) const auto hasPreview = (story && story->hasReplyPreview())
|| (message || (message
&& message->media() && message->media()
@ -229,8 +324,13 @@ void Reply::update(
&& !fields.quote.empty(); && !fields.quote.empty();
_hasQuoteIcon = hasQuoteIcon ? 1 : 0; _hasQuoteIcon = hasQuoteIcon ? 1 : 0;
const auto session = &view->history()->session();
const auto text = (!_displaying && data->unavailable()) const auto text = (!_displaying && data->unavailable())
? TextWithEntities() ? TextWithEntities()
: task
? Ui::Text::Colorized(task->completionDate
? TaskDoneIcon(session)
: TaskIcon(session)).append(task->text)
: (message && (fields.quote.empty() || !fields.manualQuote)) : (message && (fields.quote.empty() || !fields.manualQuote))
? message->inReplyText() ? message->inReplyText()
: !fields.quote.empty() : !fields.quote.empty()
@ -288,10 +388,11 @@ void Reply::setLinkFrom(
const auto &fields = data->fields(); const auto &fields = data->fields();
const auto externalChannelId = peerToChannel(fields.externalPeerId); const auto externalChannelId = peerToChannel(fields.externalPeerId);
const auto messageId = fields.messageId; const auto messageId = fields.messageId;
const auto quote = fields.manualQuote const auto highlight = MessageHighlightId{
? fields.quote .quote = fields.manualQuote ? fields.quote : TextWithEntities(),
: TextWithEntities(); .quoteOffset = int(fields.quoteOffset),
const auto quoteOffset = fields.quoteOffset; .todoItemId = fields.todoItemId,
};
const auto returnToId = view->data()->fullId(); const auto returnToId = view->data()->fullId();
const auto externalLink = [=](ClickContext context) { const auto externalLink = [=](ClickContext context) {
const auto my = context.other.value<ClickHandlerContext>(); const auto my = context.other.value<ClickHandlerContext>();
@ -314,8 +415,7 @@ void Reply::setLinkFrom(
channel, channel,
messageId, messageId,
returnToId, returnToId,
quote, highlight
quoteOffset
)->onClick(context); )->onClick(context);
} else { } else {
controller->showPeerInfo(channel); controller->showPeerInfo(channel);
@ -336,7 +436,7 @@ void Reply::setLinkFrom(
const auto message = data->resolvedMessage.get(); const auto message = data->resolvedMessage.get();
const auto story = data->resolvedStory.get(); const auto story = data->resolvedStory.get();
_link = message _link = message
? JumpToMessageClickHandler(message, returnToId, quote, quoteOffset) ? JumpToMessageClickHandler(message, returnToId, highlight)
: story : story
? JumpToStoryClickHandler(story) ? JumpToStoryClickHandler(story)
: (data->external() : (data->external()
@ -873,18 +973,28 @@ TextWithEntities Reply::ForwardEmoji(not_null<Data::Session*> owner) {
TextWithEntities Reply::ComposePreviewName( TextWithEntities Reply::ComposePreviewName(
not_null<History*> history, not_null<History*> history,
not_null<HistoryItem*> to, not_null<HistoryItem*> to,
bool quote) { const FullReplyTo &replyTo) {
const auto sender = [&] { const auto sender = [&] {
if (const auto from = to->displayFrom()) { if (const auto from = to->displayFrom()) {
return not_null(from); return not_null(from);
} }
return to->author(); 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 toPeer = to->history()->peer;
const auto displayAsExternal = (to->history() != history); const auto displayAsExternal = (to->history() != history);
const auto groupNameAdded = displayAsExternal const auto groupNameAdded = displayAsExternal
&& (toPeer != sender) && (toPeer != sender)
&& (toPeer->isChat() || toPeer->isMegagroup()); && (toPeer->isChat() || toPeer->isMegagroup());
const auto quote = replyTo && !replyTo.quote.empty();
const auto shorten = groupNameAdded || quote; const auto shorten = groupNameAdded || quote;
auto nameFull = TextWithEntities(); auto nameFull = TextWithEntities();

View file

@ -110,7 +110,7 @@ public:
[[nodiscard]] static TextWithEntities ComposePreviewName( [[nodiscard]] static TextWithEntities ComposePreviewName(
not_null<History*> history, not_null<History*> history,
not_null<HistoryItem*> to, not_null<HistoryItem*> to,
bool quote); const FullReplyTo &replyTo);
private: private:
[[nodiscard]] Ui::Text::GeometryDescriptor textGeometry( [[nodiscard]] Ui::Text::GeometryDescriptor textGeometry(

View file

@ -438,12 +438,8 @@ void ScheduledWidget::setupComposeControls() {
if (item->isScheduled() && item->history() == _history) { if (item->isScheduled() && item->history() == _history) {
showAtPosition(item->position()); showAtPosition(item->position());
} else { } else {
JumpToMessageClickHandler( const auto highlight = to.highlight();
item, JumpToMessageClickHandler(item, {}, highlight)->onClick({});
{},
to.quote,
to.quoteOffset
)->onClick({});
} }
} }
}, lifetime()); }, lifetime());

View file

@ -463,17 +463,16 @@ QSize Service::performCountCurrentSize(int newWidth) {
const auto media = this->media(); const auto media = this->media();
const auto mediaDisplayed = media && media->isDisplayed(); const auto mediaDisplayed = media && media->isDisplayed();
auto contentWidth = newWidth; 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()) { if (mediaDisplayed && media->hideServiceText()) {
newHeight += media->resizeGetHeight(newWidth) + marginBottom(); newHeight += media->resizeGetHeight(newWidth) + marginBottom();
} else if (!text().isEmpty()) { } 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); auto nwidth = qMax(contentWidth - st::msgServicePadding.left() - st::msgServicePadding.right(), 0);
newHeight += (contentWidth >= maxWidth()) newHeight += (contentWidth >= maxWidth())
? minHeight() ? minHeight()

View file

@ -432,10 +432,14 @@ void SubsectionTabs::setupSlider(
.session = &session(), .session = &session(),
}), }),
}, paused); }, paused);
slider->setActiveSectionFast(activeIndex);
const auto ignoreActiveScroll = (scrollSavingIndex >= 0);
slider->setActiveSectionFast(activeIndex, ignoreActiveScroll);
_sectionsSlice = _slice; _sectionsSlice = _slice;
if (scrollSavingIndex >= 0) { Assert(slider->sectionsCount() == _slice.size());
if (ignoreActiveScroll) {
Assert(scrollSavingIndex < slider->sectionsCount());
const auto position = scrollSavingShift const auto position = scrollSavingShift
+ slider->lookupSectionPosition(scrollSavingIndex); + slider->lookupSectionPosition(scrollSavingIndex);
if (vertical) { if (vertical) {
@ -702,6 +706,8 @@ void SubsectionTabs::refreshSlice() {
if (_slice != slice) { if (_slice != slice) {
_slice = std::move(slice); _slice = std::move(slice);
_refreshed.fire({}); _refreshed.fire({});
Assert((!_horizontal && !_vertical)
|| (_slice.size() == _sectionsSlice.size()));
} }
}); });
const auto push = [&](not_null<Data::Thread*> thread) { const auto push = [&](not_null<Data::Thread*> thread) {

View file

@ -334,9 +334,11 @@ void TodoList::updateTasks(bool skipAnimations) {
ClickHandlerPtr TodoList::createTaskClickHandler( ClickHandlerPtr TodoList::createTaskClickHandler(
const Task &task) { const Task &task) {
const auto id = task.id; const auto id = task.id;
return std::make_shared<LambdaClickHandler>(crl::guard(this, [=] { auto result = std::make_shared<LambdaClickHandler>(crl::guard(this, [=] {
toggleCompletion(id); toggleCompletion(id);
})); }));
result->setProperty(kTodoListItemIdProperty, id);
return result;
} }
void TodoList::startToggleAnimation(Task &task) { void TodoList::startToggleAnimation(Task &task) {
@ -375,11 +377,24 @@ void TodoList::toggleCompletion(int id) {
if (i == end(_tasks)) { if (i == end(_tasks)) {
return; return;
} }
const auto selected = (i->completionDate != 0); const auto selected = (i->completionDate != 0);
i->completionDate = selected ? TimeId() : base::unixtime::now(); i->completionDate = selected ? TimeId() : base::unixtime::now();
if (!selected) { if (!selected) {
i->setCompletedBy(_parent->history()->session().user()); 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); startToggleAnimation(*i);
repaint(); repaint();
@ -467,6 +482,7 @@ void TodoList::draw(Painter &p, const PaintContext &context) const {
paintw, paintw,
width(), width(),
context); context);
appendTaskHighlight(task.id, tshift, height, context);
if (was) { if (was) {
heavy = true; heavy = true;
} else if (!task.userpic.null()) { } else if (!task.userpic.null()) {
@ -561,6 +577,33 @@ int TodoList::paintTask(
return height; 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( void TodoList::paintRadio(
Painter &p, Painter &p,
const Task &task, const Task &task,

View file

@ -117,6 +117,11 @@ private:
int top, int top,
int paintw, int paintw,
const PaintContext &context) const; const PaintContext &context) const;
void appendTaskHighlight(
int id,
int top,
int height,
const PaintContext &context) const;
void radialAnimationCallback() const; void radialAnimationCallback() const;

View file

@ -731,8 +731,8 @@ manageDeleteGroupButton: SettingsCountButton(manageGroupNoIconButton) {
manageGroupReactions: IconButton(defaultIconButton) { manageGroupReactions: IconButton(defaultIconButton) {
width: 24px; width: 24px;
height: 36px; height: 36px;
icon: icon{{ "info/edit/stickers_add", historyComposeIconFg }}; icon: icon{{ "menu/add", historyComposeIconFg }};
iconOver: icon{{ "info/edit/stickers_add", historyComposeIconFgOver }}; iconOver: icon{{ "menu/add", historyComposeIconFgOver }};
} }
manageGroupReactionsField: InputField(defaultInputField) { manageGroupReactionsField: InputField(defaultInputField) {
textMargins: margins(1px, 12px, 24px, 8px); textMargins: margins(1px, 12px, 24px, 8px);

View file

@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_premium.h" #include "api/api_premium.h"
#include "apiwrap.h" #include "apiwrap.h"
#include "boxes/star_gift_box.h"
#include "data/data_channel.h" #include "data/data_channel.h"
#include "data/data_credits.h" #include "data/data_credits.h"
#include "data/data_session.h" #include "data/data_session.h"
@ -380,6 +381,7 @@ void InnerWidget::loadMore() {
_entries.clear(); _entries.clear();
} }
_entries.reserve(_entries.size() + data.vgifts().v.size()); _entries.reserve(_entries.size() + data.vgifts().v.size());
auto hasUnique = false;
for (const auto &gift : data.vgifts().v) { for (const auto &gift : data.vgifts().v) {
if (auto parsed = Api::FromTL(_peer, gift)) { if (auto parsed = Api::FromTL(_peer, gift)) {
auto descriptor = DescriptorForGift(_peer, *parsed); auto descriptor = DescriptorForGift(_peer, *parsed);
@ -387,10 +389,15 @@ void InnerWidget::loadMore() {
.gift = std::move(*parsed), .gift = std::move(*parsed),
.descriptor = std::move(descriptor), .descriptor = std::move(descriptor),
}); });
hasUnique = (parsed->info.unique != nullptr);
} }
} }
refreshButtons(); refreshButtons();
refreshAbout(); refreshAbout();
if (hasUnique) {
Ui::PreloadUniqueGiftResellPrices(&_peer->session());
}
}).fail([=] { }).fail([=] {
_loadMoreRequestId = 0; _loadMoreRequestId = 0;
_allLoaded = true; _allLoaded = true;

View file

@ -949,7 +949,20 @@ QString FormatCountDecimal(int64 number) {
} }
QString FormatExactCountDecimal(float64 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) { ShortenedCount FormatCreditsAmountToShort(CreditsAmount amount) {

View file

@ -1087,3 +1087,34 @@ mediaviewSponsoredButton: RoundButton(defaultActiveButton) {
ripple: universalRippleAnimation; 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;
}
}

View file

@ -51,6 +51,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "media/view/media_view_pip.h" #include "media/view/media_view_pip.h"
#include "media/view/media_view_overlay_raster.h" #include "media/view/media_view_overlay_raster.h"
#include "media/view/media_view_overlay_opengl.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_share.h"
#include "media/stories/media_stories_view.h" #include "media/stories/media_stories_view.h"
#include "media/streaming/media_streaming_document.h" #include "media/streaming/media_streaming_document.h"
@ -339,6 +340,7 @@ struct OverlayWidget::Streamed {
Streaming::Instance instance; Streaming::Instance instance;
std::unique_ptr<PlaybackControls> controls; std::unique_ptr<PlaybackControls> controls;
std::unique_ptr<PlaybackSponsored> sponsored;
std::unique_ptr<base::PowerSaveBlocker> powerSaveBlocker; std::unique_ptr<base::PowerSaveBlocker> powerSaveBlocker;
bool ready = false; bool ready = false;
@ -1617,7 +1619,11 @@ void OverlayWidget::fillContextMenuActions(
if (const auto window = findWindow()) { if (const auto window = findWindow()) {
const auto show = window->uiShow(); const auto show = window->uiShow();
const auto fullId = _message->fullId(); const auto fullId = _message->fullId();
Menu::FillSponsored(_body, addAction, show, fullId, true); Menu::FillSponsored(
addAction,
show,
fullId,
{ .dark = true, .skipInfo = true });
} }
return; return;
} }
@ -3981,7 +3987,11 @@ bool OverlayWidget::initStreaming(const StartStreaming &startStreaming) {
&& !_streamed->instance.player().finished())) { && !_streamed->instance.player().finished())) {
startStreamingPlayer(startStreaming); startStreamingPlayer(startStreaming);
} else { } else {
_streamed->ready = _streamed->instance.player().ready(); if (_streamed->instance.player().ready()) {
markStreamedReady();
} else {
_streamed->ready = false;
}
updatePlaybackState(); updatePlaybackState();
} }
return true; return true;
@ -3994,7 +4004,7 @@ void OverlayWidget::startStreamingPlayer(
const auto &player = _streamed->instance.player(); const auto &player = _streamed->instance.player();
if (player.playing()) { if (player.playing()) {
if (!_streamed->withSound) { if (!_streamed->withSound) {
_streamed->ready = true; markStreamedReady();
return; return;
} }
_pip = nullptr; _pip = nullptr;
@ -4012,6 +4022,18 @@ void OverlayWidget::startStreamingPlayer(
restartAtSeekPosition(_streamedPosition); 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() { void OverlayWidget::initStreamingThumbnail() {
Expects(_photo || _document); Expects(_photo || _document);
@ -4083,7 +4105,7 @@ void OverlayWidget::initStreamingThumbnail() {
} }
void OverlayWidget::streamingReady(Streaming::Information &&info) { void OverlayWidget::streamingReady(Streaming::Information &&info) {
_streamed->ready = true; markStreamedReady();
if (videoShown()) { if (videoShown()) {
applyVideoSize(); applyVideoSize();
_streamedQualityChangeFrame = QImage(); _streamedQualityChangeFrame = QImage();
@ -4105,6 +4127,7 @@ void OverlayWidget::applyVideoSize() {
bool OverlayWidget::createStreamingObjects() { bool OverlayWidget::createStreamingObjects() {
Expects(_photo || _document); Expects(_photo || _document);
Expects(!_streamed);
const auto origin = fileOrigin(); const auto origin = fileOrigin();
const auto callback = [=] { waitingAnimationCallback(); }; const auto callback = [=] { waitingAnimationCallback(); };
@ -4137,6 +4160,18 @@ bool OverlayWidget::createStreamingObjects() {
_body, _body,
static_cast<PlaybackControls::Delegate*>(this)); static_cast<PlaybackControls::Delegate*>(this));
_streamed->controls->show(); _streamed->controls->show();
_streamed->sponsored = PlaybackSponsored::Has(_message)
? std::make_unique<PlaybackSponsored>(
_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(); refreshClipControllerGeometry();
} }
return true; return true;

View file

@ -78,6 +78,7 @@ struct ContentLayout;
namespace Media::View { namespace Media::View {
class PlaybackSponsored;
class GroupThumbs; class GroupThumbs;
class Pip; class Pip;
@ -412,6 +413,7 @@ private:
const StartStreaming &startStreaming = StartStreaming()); const StartStreaming &startStreaming = StartStreaming());
void startStreamingPlayer(const StartStreaming &startStreaming); void startStreamingPlayer(const StartStreaming &startStreaming);
void initStreamingThumbnail(); void initStreamingThumbnail();
void markStreamedReady();
void streamingReady(Streaming::Information &&info); void streamingReady(Streaming::Information &&info);
[[nodiscard]] bool createStreamingObjects(); [[nodiscard]] bool createStreamingObjects();
void handleStreamingUpdate(Streaming::Update &&update); void handleStreamingUpdate(Streaming::Update &&update);

View file

@ -19,14 +19,13 @@ class MediaSlider;
class PopupMenu; class PopupMenu;
} // namespace Ui } // namespace Ui
namespace Media { namespace Media::Player {
namespace Player {
struct TrackState; struct TrackState;
class SettingsButton; class SettingsButton;
class SpeedController; class SpeedController;
} // namespace Player } // namespace Media::Player
namespace View { namespace Media::View {
class PlaybackProgress; class PlaybackProgress;
@ -131,5 +130,4 @@ private:
}; };
} // namespace View } // namespace Media::View
} // namespace Media

View file

@ -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<QWidget*> parent,
const style::RippleAnimation &st,
rpl::producer<crl::time> allowCloseAt);
[[nodiscard]] rpl::producer<Action> actions() const;
private:
QPoint prepareRippleStartPosition() const override;
QImage prepareRippleMask() const override;
void paintEvent(QPaintEvent *e) override;
void updateProgress(crl::time now);
rpl::event_stream<Action> _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<QWidget*> parent,
const style::RippleAnimation &st,
rpl::producer<crl::time> 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<Action> 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<ChatHelpers::Show> show,
const Data::SponsoredMessage &data,
rpl::producer<crl::time> allowCloseAt);
[[nodiscard]] rpl::producer<Action> actions() const;
void setFinalPosition(int x, int y);
void fadeIn();
void fadeOut(Fn<void()> 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<void()> finished = nullptr);
void startFade(Fn<void()> finished);
const not_null<Main::Session*> _session;
const std::shared_ptr<ChatHelpers::Show> _show;
const Data::SponsoredMessage _data;
style::RoundButton _aboutSt;
std::unique_ptr<Ui::RoundButton> _about;
std::unique_ptr<Close> _close;
base::unique_qptr<Ui::PopupMenu> _menu;
rpl::event_stream<Action> _actions;
std::shared_ptr<Data::PhotoMedia> _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<ChatHelpers::Show> show,
const Data::SponsoredMessage &data,
rpl::producer<crl::time> allowCloseAt)
: RpWidget(parent)
, _session(&data.history->session())
, _show(std::move(show))
, _data(data)
, _aboutSt(PrepareAboutStyle())
, _about(std::make_unique<Ui::RoundButton>(
this,
tr::lng_search_sponsored_button(),
_aboutSt))
, _close(
std::make_unique<Close>(
this,
_aboutSt.ripple,
std::move(allowCloseAt))) {
_about->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
setMouseTracking(true);
populate();
hide();
}
rpl::producer<Action> 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<void()> hidden) {
if (!_shown) {
if (const auto onstack = hidden) {
onstack();
}
return;
}
_shown = false;
startFade(std::move(hidden));
}
void PlaybackSponsored::Message::startFade(Fn<void()> 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<void()> 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<Ui::PopupMenu>(
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<Ui::RpWidget*> controls,
std::shared_ptr<ChatHelpers::Show> show,
not_null<HistoryItem*> 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<Message>(
_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

View file

@ -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<Ui::RpWidget*> controls,
std::shared_ptr<ChatHelpers::Show> show,
not_null<HistoryItem*> 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<QWidget*> _parent;
const not_null<Main::Session*> _session;
const std::shared_ptr<ChatHelpers::Show> _show;
const FullMsgId _itemId;
rpl::variable<QRect> _controlsGeometry;
std::unique_ptr<Message> _widget;
rpl::variable<crl::time> _allowCloseAt;
crl::time _start = 0;
bool _started = false;
bool _paused = false;
bool _pausedInside = false;
bool _pausedOutside = false;
base::Timer _timer;
std::optional<Data::SponsoredForVideo> _data;
rpl::lifetime _lifetime;
};
} // namespace Media::View

View file

@ -287,14 +287,12 @@ void AboutBox(
top->setForceRippled(false); top->setForceRippled(false);
}); });
FillSponsored( FillSponsored(
top,
Ui::Menu::CreateAddActionCallback(menu->get()), Ui::Menu::CreateAddActionCallback(menu->get()),
show, show,
phrases, phrases,
details, details,
report, report,
false, { .skipAbout = true });
true);
const auto global = top->mapToGlobal( const auto global = top->mapToGlobal(
QPoint(top->width() / 4 * 3, top->height() / 2)); QPoint(top->width() / 4 * 3, top->height() / 2));
raw->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight); raw->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight);
@ -390,18 +388,17 @@ void ShowReportSponsoredBox(
} // namespace } // namespace
void FillSponsored( void FillSponsored(
not_null<Ui::RpWidget*> parent,
const Ui::Menu::MenuCallback &addAction, const Ui::Menu::MenuCallback &addAction,
std::shared_ptr<ChatHelpers::Show> show, std::shared_ptr<ChatHelpers::Show> show,
SponsoredPhrases phrases, SponsoredPhrases phrases,
const Data::SponsoredMessages::Details &details, const Data::SponsoredMessages::Details &details,
Data::SponsoredReportAction report, Data::SponsoredReportAction report,
bool mediaViewer, SponsoredMenuSettings settings) {
bool skipAbout) {
const auto session = &show->session(); const auto session = &show->session();
const auto &info = details.info; const auto &info = details.info;
const auto dark = settings.dark;
if (!mediaViewer && !info.empty()) { if (!settings.skipInfo && !info.empty()) {
auto fillSubmenu = [&](not_null<Ui::PopupMenu*> menu) { auto fillSubmenu = [&](not_null<Ui::PopupMenu*> menu) {
const auto allText = ranges::accumulate( const auto allText = ranges::accumulate(
info, info,
@ -416,8 +413,10 @@ void FillSponsored(
for (const auto &i : info) { for (const auto &i : info) {
auto item = base::make_unique_q<Ui::Menu::MultilineAction>( auto item = base::make_unique_q<Ui::Menu::MultilineAction>(
menu, menu,
st::defaultMenu, dark ? st::storiesMenu : st::defaultMenu,
st::historySponsorInfoItem, (dark
? st::historySponsorInfoItemDark
: st::historySponsorInfoItem),
st::historyHasCustomEmojiPosition, st::historyHasCustomEmojiPosition,
base::duplicate(i)); base::duplicate(i));
item->clicks( item->clicks(
@ -431,27 +430,31 @@ void FillSponsored(
addAction({ addAction({
.text = tr::lng_sponsored_info_menu(tr::now), .text = tr::lng_sponsored_info_menu(tr::now),
.handler = nullptr, .handler = nullptr,
.icon = &st::menuIconChannel, .icon = (dark
? &st::mediaMenuIconChannel
: &st::menuIconChannel),
.fillSubmenu = std::move(fillSubmenu), .fillSubmenu = std::move(fillSubmenu),
}); });
addAction({ addAction({
.separatorSt = &st::expandedMenuSeparator, .separatorSt = (dark
? &st::mediaviewMenuSeparator
: &st::expandedMenuSeparator),
.isSeparator = true, .isSeparator = true,
}); });
} }
if (details.canReport) { if (details.canReport) {
if (!skipAbout) { if (!settings.skipAbout) {
addAction(tr::lng_sponsored_menu_revenued_about(tr::now), [=] { addAction(tr::lng_sponsored_menu_revenued_about(tr::now), [=] {
show->show(Box(AboutBox, show, phrases, details, report)); 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), [=] { addAction(tr::lng_sponsored_menu_revenued_report(tr::now), [=] {
ShowReportSponsoredBox(show, report); ShowReportSponsoredBox(show, report);
}, (mediaViewer ? &st::mediaMenuIconBlock : &st::menuIconBlock)); }, (dark ? &st::mediaMenuIconBlock : &st::menuIconBlock));
addAction({ addAction({
.separatorSt = (mediaViewer .separatorSt = (dark
? &st::mediaviewMenuSeparator ? &st::mediaviewMenuSeparator
: &st::expandedMenuSeparator), : &st::expandedMenuSeparator),
.isSeparator = true, .isSeparator = true,
@ -464,26 +467,22 @@ void FillSponsored(
} else { } else {
ShowPremiumPreviewBox(show, PremiumFeature::NoAds); ShowPremiumPreviewBox(show, PremiumFeature::NoAds);
} }
}, (mediaViewer ? &st::mediaMenuIconCancel : &st::menuIconCancel)); }, (dark ? &st::mediaMenuIconCancel : &st::menuIconCancel));
} }
void FillSponsored( void FillSponsored(
not_null<Ui::RpWidget*> parent,
const Ui::Menu::MenuCallback &addAction, const Ui::Menu::MenuCallback &addAction,
std::shared_ptr<ChatHelpers::Show> show, std::shared_ptr<ChatHelpers::Show> show,
const FullMsgId &fullId, const FullMsgId &fullId,
bool mediaViewer, SponsoredMenuSettings settings) {
bool skipAbout) {
const auto session = &show->session(); const auto session = &show->session();
FillSponsored( FillSponsored(
parent,
addAction, addAction,
show, show,
PhrasesForMessage(fullId), PhrasesForMessage(fullId),
session->sponsoredMessages().lookupDetails(fullId), session->sponsoredMessages().lookupDetails(fullId),
session->sponsoredMessages().createReportCallback(fullId), session->sponsoredMessages().createReportCallback(fullId),
mediaViewer, settings);
skipAbout);
} }
void ShowSponsored( void ShowSponsored(
@ -495,11 +494,9 @@ void ShowSponsored(
st::popupMenuWithIcons); st::popupMenuWithIcons);
FillSponsored( FillSponsored(
parent,
Ui::Menu::CreateAddActionCallback(menu), Ui::Menu::CreateAddActionCallback(menu),
show, show,
fullId, fullId);
false);
menu->popup(QCursor::pos()); menu->popup(QCursor::pos());
} }

View file

@ -33,23 +33,25 @@ enum class SponsoredPhrases {
Search, Search,
}; };
struct SponsoredMenuSettings {
bool dark = false;
bool skipAbout = false;
bool skipInfo = false;
};
void FillSponsored( void FillSponsored(
not_null<Ui::RpWidget*> parent,
const Ui::Menu::MenuCallback &addAction, const Ui::Menu::MenuCallback &addAction,
std::shared_ptr<ChatHelpers::Show> show, std::shared_ptr<ChatHelpers::Show> show,
SponsoredPhrases phrases, SponsoredPhrases phrases,
const Data::SponsoredMessageDetails &details, const Data::SponsoredMessageDetails &details,
Data::SponsoredReportAction report, Data::SponsoredReportAction report,
bool mediaViewer, SponsoredMenuSettings settings = {});
bool skipAbout);
void FillSponsored( void FillSponsored(
not_null<Ui::RpWidget*> parent,
const Ui::Menu::MenuCallback &addAction, const Ui::Menu::MenuCallback &addAction,
std::shared_ptr<ChatHelpers::Show> show, std::shared_ptr<ChatHelpers::Show> show,
const FullMsgId &fullId, const FullMsgId &fullId,
bool mediaViewer, SponsoredMenuSettings settings = {});
bool skipAbout = false);
void ShowSponsored( void ShowSponsored(
not_null<Ui::RpWidget*> parent, not_null<Ui::RpWidget*> parent,

View file

@ -1344,7 +1344,7 @@ messages.messageViews#b6c4f543 views:Vector<MessageViews> chats:Vector<Chat> use
messages.discussionMessage#a6341782 flags:# messages:Vector<Message> max_id:flags.0?int read_inbox_max_id:flags.1?int read_outbox_max_id:flags.2?int unread_count:int chats:Vector<Chat> users:Vector<User> = messages.DiscussionMessage; messages.discussionMessage#a6341782 flags:# messages:Vector<Message> max_id:flags.0?int read_inbox_max_id:flags.1?int read_outbox_max_id:flags.2?int unread_count:int chats:Vector<Chat> users:Vector<User> = 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<MessageEntity> 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<MessageEntity> quote_offset:flags.10?int todo_item_id:flags.11?int = MessageReplyHeader;
messageReplyStoryHeader#e5af939 peer:Peer story_id: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<Peer> channel_id:flags.0?long max_id:flags.2?int read_max_id:flags.3?int = MessageReplies; messageReplies#83d60fc2 flags:# comments:flags.0?true replies:int replies_pts:int recent_repliers:flags.1?Vector<Peer> 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<StoryViews> users:Vector<User> = stories.StoryViews; stories.storyViews#de9eed1d views:Vector<StoryViews> users:Vector<User> = 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<MessageEntity> 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<MessageEntity> 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; inputReplyToStory#5881323a peer:InputPeer story_id:int = InputReplyTo;
inputReplyToMonoForum#69d66c45 monoforum_peer_id:InputPeer = 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; fragment.getCollectibleInfo#be1e85ba collectible:InputCollectible = fragment.CollectibleInfo;
// LAYER 207 // LAYER 209

View file

@ -939,7 +939,8 @@ void FillUniqueGiftMenu(
&& e.id.isEmpty() && e.id.isEmpty()
&& (e.in || (giftChannel && giftChannel->canManageGifts())) && (e.in || (giftChannel && giftChannel->canManageGifts()))
&& !e.giftTransferred && !e.giftTransferred
&& !e.giftRefunded; && !e.giftRefunded
&& !e.converted;
const auto unique = e.uniqueGift; const auto unique = e.uniqueGift;
if (unique if (unique
@ -1148,7 +1149,6 @@ void GenericCreditsEntryBox(
const auto isStarGift = e.stargift || e.soldOutInfo; const auto isStarGift = e.stargift || e.soldOutInfo;
const auto creditsHistoryStarGift = isStarGift && !e.id.isEmpty(); const auto creditsHistoryStarGift = isStarGift && !e.id.isEmpty();
const auto sentStarGift = creditsHistoryStarGift && !e.in; const auto sentStarGift = creditsHistoryStarGift && !e.in;
const auto convertedStarGift = creditsHistoryStarGift && e.converted;
const auto giftToSelf = isStarGift const auto giftToSelf = isStarGift
&& (e.barePeerId == selfPeerId) && (e.barePeerId == selfPeerId)
&& (e.in || e.bareGiftOwnerId == selfPeerId); && (e.in || e.bareGiftOwnerId == selfPeerId);
@ -1164,7 +1164,8 @@ void GenericCreditsEntryBox(
const auto starGiftCanManage = isStarGift const auto starGiftCanManage = isStarGift
&& !creditsHistoryStarGift && !creditsHistoryStarGift
&& (e.in || giftToChannelCanManage) && (e.in || giftToChannelCanManage)
&& !e.fromGiftSlug; && !e.fromGiftSlug
&& !e.converted;
const auto starGiftCanTransfer = isStarGift const auto starGiftCanTransfer = isStarGift
&& !creditsHistoryStarGift && !creditsHistoryStarGift
&& (e.in || giftToChannelCanTransfer); && (e.in || giftToChannelCanTransfer);
@ -1250,12 +1251,13 @@ void GenericCreditsEntryBox(
EntryToSavedStarGiftId(session, e), EntryToSavedStarGiftId(session, e),
style); style);
}; };
const auto canResell = CanResellGift(session, e);
AddUniqueGiftCover( AddUniqueGiftCover(
content, content,
rpl::single(*uniqueGift), rpl::single(*uniqueGift),
{}, {},
std::move(price), std::move(price),
CanResellGift(session, e) ? std::move(change) : Fn<void()>()); canResell ? std::move(change) : Fn<void()>());
AddSkip(content, st::defaultVerticalListSkip * 2); AddSkip(content, st::defaultVerticalListSkip * 2);
@ -1263,6 +1265,10 @@ void GenericCreditsEntryBox(
const auto type = SavedStarGiftMenuType::View; const auto type = SavedStarGiftMenuType::View;
FillUniqueGiftMenu(show, menu, e, type, st); FillUniqueGiftMenu(show, menu, e, type, st);
}); });
if (canResell) {
Ui::PreloadUniqueGiftResellPrices(session);
}
} else if (const auto callback = Ui::PaintPreviewCallback(session, e)) { } else if (const auto callback = Ui::PaintPreviewCallback(session, e)) {
const auto thumb = content->add(object_ptr<Ui::CenterWrap<>>( const auto thumb = content->add(object_ptr<Ui::CenterWrap<>>(
content, content,
@ -1419,7 +1425,7 @@ void GenericCreditsEntryBox(
? tr::lng_credits_box_history_entry_gift_unavailable(tr::now) ? tr::lng_credits_box_history_entry_gift_unavailable(tr::now)
: sentStarGift : sentStarGift
? tr::lng_credits_box_history_entry_gift_sent(tr::now) ? tr::lng_credits_box_history_entry_gift_sent(tr::now)
: convertedStarGift : e.converted
? tr::lng_credits_box_history_entry_gift_converted(tr::now) ? tr::lng_credits_box_history_entry_gift_converted(tr::now)
: (isStarGift && !starGiftCanManage) : (isStarGift && !starGiftCanManage)
? tr::lng_gift_link_label_gift(tr::now) ? tr::lng_gift_link_label_gift(tr::now)
@ -1622,7 +1628,7 @@ void GenericCreditsEntryBox(
} }
const auto arrow = Ui::Text::IconEmoji(&st::textMoreIconEmoji); const auto arrow = Ui::Text::IconEmoji(&st::textMoreIconEmoji);
if (!uniqueGift && starGiftCanManage) { if (!uniqueGift && (starGiftCanManage || e.converted)) {
Ui::AddSkip(content); Ui::AddSkip(content);
const auto about = box->addRow( const auto about = box->addRow(
object_ptr<Ui::CenterWrap<Ui::FlatLabel>>( object_ptr<Ui::CenterWrap<Ui::FlatLabel>>(
@ -1751,7 +1757,8 @@ void GenericCreditsEntryBox(
const auto canToggle = starGiftCanManage const auto canToggle = starGiftCanManage
&& !e.giftTransferred && !e.giftTransferred
&& !e.giftRefunded; && !e.giftRefunded
&& !e.converted;
const auto toggleVisibility = [=, weak = Ui::MakeWeak(box)](bool save) { const auto toggleVisibility = [=, weak = Ui::MakeWeak(box)](bool save) {
const auto showSection = !e.fromGiftsList; const auto showSection = !e.fromGiftsList;
const auto savedId = EntryToSavedStarGiftId(&show->session(), e); const auto savedId = EntryToSavedStarGiftId(&show->session(), e);

View file

@ -843,7 +843,6 @@ void SetupPremium(
button->addClickHandler([=] { button->addClickHandler([=] {
showOther(BusinessId()); showOther(BusinessId());
}); });
Ui::NewBadge::AddToRight(button);
if (controller->session().premiumCanBuy()) { if (controller->session().premiumCanBuy()) {
const auto button = AddButtonWithIcon( const auto button = AddButtonWithIcon(
@ -852,6 +851,8 @@ void SetupPremium(
st::settingsButton, st::settingsButton,
{ .icon = &st::menuIconGiftPremium } { .icon = &st::menuIconGiftPremium }
); );
Ui::NewBadge::AddToRight(button);
button->addClickHandler([=] { button->addClickHandler([=] {
Ui::ChooseStarGiftRecipient(controller); Ui::ChooseStarGiftRecipient(controller);
}); });

View file

@ -324,9 +324,8 @@ void PointDetailsWidget::setXIndex(int xIndex) {
nullptr, nullptr,
{ float64(xIndex), float64(xIndex) }).parts { float64(xIndex), float64(xIndex) }).parts
: std::vector<PiePartData::Part>(); : std::vector<PiePartData::Part>();
const auto multiplier = float64(kOneStarInNano);
const auto isCredits const auto isCredits
= _chartData.currency == Data::StatisticalCurrency::Credits; = (_chartData.currency == Data::StatisticalCurrency::Credits);
for (auto i = 0; i < _chartData.lines.size(); i++) { for (auto i = 0; i < _chartData.lines.size(); i++) {
const auto &dataLine = _chartData.lines[i]; const auto &dataLine = _chartData.lines[i];
auto textLine = Line(); 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_credits
: tr::lng_channel_earn_chart_overriden_detail_currency)( : tr::lng_channel_earn_chart_overriden_detail_currency)(
tr::now)); 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( copy.value.setText(
_textStyle, _textStyle,
Lang::FormatExactCountDecimal( Lang::FormatCreditsAmountDecimal(value));
dataLine.y[xIndex] / multiplier));
_lines.push_back(std::move(copy)); _lines.push_back(std::move(copy));
textLine.name.setText( textLine.name.setText(
_textStyle, _textStyle,
tr::lng_channel_earn_chart_overriden_detail_usd(tr::now)); tr::lng_channel_earn_chart_overriden_detail_usd(tr::now));
textLine.value.setText( textLine.value.setText(
_textStyle, _textStyle,
Info::ChannelEarn::ToUsd( Info::ChannelEarn::ToUsd(value, _chartData.currencyRate, 0));
dataLine.y[xIndex] / multiplier,
_chartData.currencyRate, 0));
} }
_lines.push_back(std::move(textLine)); _lines.push_back(std::move(textLine));
} }

View file

@ -947,6 +947,9 @@ historySponsorInfoItem: FlatLabel(defaultFlatLabel) {
minWidth: 136px; minWidth: 136px;
maxHeight: 120px; maxHeight: 120px;
} }
historySponsorInfoItemDark: FlatLabel(historySponsorInfoItem) {
textFg: mediaviewControlFg;
}
historyHasCustomEmoji: FlatLabel(defaultFlatLabel) { historyHasCustomEmoji: FlatLabel(defaultFlatLabel) {
style: TextStyle(defaultTextStyle) { style: TextStyle(defaultTextStyle) {
font: font(11px); font: font(11px);

View file

@ -156,6 +156,7 @@ struct ChatPaintHighlight {
float64 opacity = 0.; float64 opacity = 0.;
float64 collapsion = 0.; float64 collapsion = 0.;
TextSelection range; TextSelection range;
int todoItemId = 0;
}; };
struct ChatPaintContext { struct ChatPaintContext {

View file

@ -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())); Expects(active < int(_tabs.size()));
if (_active == active) { if (_active == active) {
@ -403,8 +403,10 @@ void SubsectionSlider::setActiveSectionFast(int active) {
_active = active; _active = active;
_activeFrom.stop(); _activeFrom.stop();
_activeSize.stop(); _activeSize.stop();
const auto now = getFinalActiveRange(); if (_active >= 0 && !ignoreScroll) {
_requestShown.fire({ now.from, now.from + now.size }); const auto now = getFinalActiveRange();
_requestShown.fire({ now.from, now.from + now.size });
}
_bar->update(); _bar->update();
} }
@ -425,6 +427,7 @@ rpl::producer<int> SubsectionSlider::sectionContextMenu() const {
} }
int SubsectionSlider::lookupSectionPosition(int index) const { int SubsectionSlider::lookupSectionPosition(int index) const {
Expects(!_tabs.empty());
Expects(index >= 0 && index < _tabs.size()); Expects(index >= 0 && index < _tabs.size());
return _vertical ? _tabs[index]->y() : _tabs[index]->x(); return _vertical ? _tabs[index]->y() : _tabs[index]->x();

View file

@ -81,7 +81,7 @@ public:
void setSections( void setSections(
SubsectionTabs sections, SubsectionTabs sections,
Fn<bool()> paused); Fn<bool()> paused);
void setActiveSectionFast(int active); void setActiveSectionFast(int active, bool ignoreScroll = false);
[[nodiscard]] int sectionsCount() const; [[nodiscard]] int sectionsCount() const;
[[nodiscard]] rpl::producer<int> sectionActivated() const; [[nodiscard]] rpl::producer<int> sectionActivated() const;

View file

@ -185,6 +185,7 @@ menuIconPayment: icon {{ "payments/payment_card", menuIconColor }};
menuIconOrderPrice: icon {{ "menu/order_price", menuIconColor }}; menuIconOrderPrice: icon {{ "menu/order_price", menuIconColor }};
menuIconOrderDate: icon {{ "menu/order_date", menuIconColor }}; menuIconOrderDate: icon {{ "menu/order_date", menuIconColor }};
menuIconOrderNumber: icon {{ "menu/order_number", menuIconColor }}; menuIconOrderNumber: icon {{ "menu/order_number", menuIconColor }};
menuIconAdd: icon{{ "menu/add", menuIconColor }};
menuIconTTLAny: icon {{ "menu/auto_delete_plain", menuIconColor }}; menuIconTTLAny: icon {{ "menu/auto_delete_plain", menuIconColor }};
menuIconTTLAnyTextPosition: point(11px, 22px); menuIconTTLAnyTextPosition: point(11px, 22px);
@ -204,6 +205,7 @@ menuBlueIconGroupCreate: icon {{ "menu/groups_create", lightButtonFg }};
mediaMenuIconStickers: icon {{ "menu/stickers", mediaviewMenuFg }}; mediaMenuIconStickers: icon {{ "menu/stickers", mediaviewMenuFg }};
mediaMenuIconCancel: icon {{ "menu/cancel", mediaviewMenuFg }}; mediaMenuIconCancel: icon {{ "menu/cancel", mediaviewMenuFg }};
mediaMenuIconChannel: icon {{ "menu/channel", mediaviewMenuFg }};
mediaMenuIconShowInChat: icon {{ "menu/show_in_chat", mediaviewMenuFg }}; mediaMenuIconShowInChat: icon {{ "menu/show_in_chat", mediaviewMenuFg }};
mediaMenuIconShowInFolder: icon {{ "menu/show_in_folder", mediaviewMenuFg }}; mediaMenuIconShowInFolder: icon {{ "menu/show_in_folder", mediaviewMenuFg }};
mediaMenuIconDownload: icon {{ "menu/download", mediaviewMenuFg }}; mediaMenuIconDownload: icon {{ "menu/download", mediaviewMenuFg }};

View file

@ -3835,7 +3835,7 @@ void PeerMenuConfirmToggleFee(
MTP_flags((refund ? Flag::f_refund_charged : Flag()) MTP_flags((refund ? Flag::f_refund_charged : Flag())
| (removeFee ? Flag() : Flag::f_require_payment) | (removeFee ? Flag() : Flag::f_require_payment)
| (parent ? Flag::f_parent_peer : Flag())), | (parent ? Flag::f_parent_peer : Flag())),
parent->input, (parent ? parent->input : MTPInputPeer()),
user->inputUser user->inputUser
)).done([=] { )).done([=] {
if (!parent) { if (!parent) {

View file

@ -53,6 +53,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_peer_values.h" #include "data/data_peer_values.h"
#include "data/data_premium_limits.h" #include "data/data_premium_limits.h"
#include "data/data_web_page.h" #include "data/data_web_page.h"
#include "dialogs/ui/chat_search_in.h"
#include "passport/passport_form_controller.h" #include "passport/passport_form_controller.h"
#include "chat_helpers/tabbed_selector.h" #include "chat_helpers/tabbed_selector.h"
#include "chat_helpers/emoji_interactions.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<Main::Session*> session) SessionNavigation::SessionNavigation(not_null<Main::Session*> session)
: _session(session) : _session(session)
, _api(&_session->mtp()) { , _api(&_session->mtp()) {
@ -1149,8 +1158,7 @@ void SessionNavigation::showRepliesForMessage(
.repliesRootId = rootId, .repliesRootId = rootId,
}, },
commentId, commentId,
params.highlightPart, params.highlight);
params.highlightPartOffsetHint);
memento->setFromTopic(topic); memento->setFromTopic(topic);
showSection(std::move(memento), params); showSection(std::move(memento), params);
return; return;
@ -1272,8 +1280,7 @@ void SessionNavigation::showSublist(
.sublist = sublist, .sublist = sublist,
}, },
itemId, itemId,
params.highlightPart, params.highlight);
params.highlightPartOffsetHint);
showSection(std::move(memento), params); 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() return session().supportMode()
&& !session().settings().supportAllSearchResults() && !session().settings().supportAllSearchResults()
&& !_searchInChat.current(); && (global || !state.inChat);
} }
bool SessionController::openFolderInDifferentWindow( bool SessionController::openFolderInDifferentWindow(

View file

@ -30,6 +30,10 @@ class SavedMessages;
enum class StorySourcesList : uchar; enum class StorySourcesList : uchar;
} // namespace Data } // namespace Data
namespace Dialogs {
struct SearchState;
} // namespace Dialogs
namespace ChatHelpers { namespace ChatHelpers {
class TabbedSelector; class TabbedSelector;
class EmojiInteractions; class EmojiInteractions;
@ -162,8 +166,9 @@ struct SectionShow {
return copy; return copy;
} }
TextWithEntities highlightPart; MessageHighlightId highlight;
int highlightPartOffsetHint = 0; int highlightPartOffsetHint = 0;
int highlightTodoItemId = 0;
std::optional<TimeId> videoTimestamp; std::optional<TimeId> videoTimestamp;
Way way = Way::Forward; Way way = Way::Forward;
anim::type animated = anim::type::normal; anim::type animated = anim::type::normal;
@ -178,6 +183,8 @@ struct SectionShow {
}; };
[[nodiscard]] MessageHighlightId SearchHighlightId(const QString &query);
class SessionController; class SessionController;
class SessionNavigation : public base::has_weak_ptr { class SessionNavigation : public base::has_weak_ptr {
@ -404,7 +411,7 @@ public:
void setSearchInChat(Dialogs::Key value) { void setSearchInChat(Dialogs::Key value) {
_searchInChat = value; _searchInChat = value;
} }
bool uniqueChatsInSearchResults() const; bool uniqueChatsInSearchResults(const Dialogs::SearchState &state) const;
void openFolder(not_null<Data::Folder*> folder); void openFolder(not_null<Data::Folder*> folder);
void closeFolder(); void closeFolder();

@ -1 +1 @@
Subproject commit 1348de6aa6c07ed32354d3e26423c45304000a39 Subproject commit d78b0507c54d76d5fe9691c8efe2638dee2c1536

View file

@ -1,7 +1,7 @@
AppVersion 5016003 AppVersion 5016004
AppVersionStrMajor 5.16 AppVersionStrMajor 5.16
AppVersionStrSmall 5.16.3 AppVersionStrSmall 5.16.4
AppVersionStr 5.16.3 AppVersionStr 5.16.4
BetaChannel 0 BetaChannel 0
AlphaVersion 0 AlphaVersion 0
AppVersionOriginal 5.16.3 AppVersionOriginal 5.16.4

View file

@ -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) 5.16.3 (08.07.25)
- Allow removing / charging fee in channel direct messages. - Allow removing / charging fee in channel direct messages.

2
cmake

@ -1 +1 @@
Subproject commit b032f270b622610ca3f42a83f37b3a183c9da0da Subproject commit f3d6471bd58dbad727d4f8fbccd0fb36632eee9e