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_progress.cpp
media/view/media_view_playback_progress.h
media/view/media_view_playback_sponsored.cpp
media/view/media_view_playback_sponsored.h
media/system_media_controls_manager.h
media/system_media_controls_manager.cpp
menu/menu_antispam_validator.cpp
@ -2071,7 +2073,7 @@ if (MSVC)
/DELAYLOAD:API-MS-Win-Core-ProcessThreads-l1-1-0.dll
/DELAYLOAD:API-MS-Win-Core-Synch-l1-2-0.dll # Synchronization.lib
/DELAYLOAD:API-MS-Win-Core-SysInfo-l1-1-0.dll
/DELAYLOAD:API-MS-Win-Core-Timezone-l1-1-0.dll
# /DELAYLOAD:API-MS-Win-Core-Timezone-l1-1-0.dll
/DELAYLOAD:API-MS-Win-Core-WinRT-l1-1-0.dll
/DELAYLOAD:API-MS-Win-Core-WinRT-Error-l1-1-0.dll
/DELAYLOAD:API-MS-Win-Core-WinRT-String-l1-1-0.dll

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_bot_name" = "Bot name";
"lng_no_chats" = "Your chats will be here";
"lng_no_conversations" = "You have no\nconversations yet.";
"lng_no_conversations_button" = "New Message";
"lng_no_conversations_subtitle" = "Your contacts on Telegram";
"lng_no_chats_filter" = "No chats currently belong to this folder.";
"lng_no_saved_sublists" = "You can save messages from other chats here.";
"lng_contacts_loading" = "Loading...";
@ -4260,6 +4263,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_context_to_msg" = "Go To Message";
"lng_context_reply_msg" = "Reply";
"lng_context_quote_and_reply" = "Quote & Reply";
"lng_context_reply_to_task" = "Reply to Task";
"lng_context_edit_msg" = "Edit";
"lng_context_add_factcheck" = "Add Fact Check";
"lng_context_edit_factcheck" = "Edit Fact Check";
@ -4450,6 +4454,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_inline_switch_cant" = "Sorry, no way to write here :(";
"lng_preview_reply_to" = "Reply to {name}";
"lng_preview_reply_to_quote" = "Reply to quote from {name}";
"lng_preview_reply_to_task" = "Reply to task from {title}";
"lng_suggest_bar_title" = "Suggest a Post Below";
"lng_suggest_bar_text" = "Click to offer a price for publishing.";

View file

@ -38,6 +38,7 @@
<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="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="dart_idle.tgs">../../animations/dice/dart_idle.tgs</file>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -127,6 +127,7 @@ constexpr auto kUpgradeDoneToastDuration = 4 * crl::time(1000);
constexpr auto kGiftsPreloadTimeout = 3 * crl::time(1000);
constexpr auto kResaleGiftsPerPage = 50;
constexpr auto kFiltersCount = 4;
constexpr auto kResellPriceCacheLifetime = 60 * crl::time(1000);
using namespace HistoryView;
using namespace Info::PeerGifts;
@ -220,6 +221,33 @@ struct GiftDetails {
bool byStars = false;
};
struct SessionResalePrices {
explicit SessionResalePrices(not_null<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 {
public:
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(
std::shared_ptr<ChatHelpers::Show> show,
std::shared_ptr<Data::UniqueGift> unique,
@ -4422,6 +4499,132 @@ void UpdateGiftSellPrice(
}).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(
std::shared_ptr<ChatHelpers::Show> show,
std::shared_ptr<Data::UniqueGift> unique,
@ -4430,125 +4633,11 @@ void ShowUniqueGiftSellBox(
if (ShowResaleGiftLater(show, unique)) {
return;
}
show->show(Box([=](not_null<Ui::GenericBox*> box) {
box->setTitle(tr::lng_gift_sell_title());
box->setStyle(st.box ? *st.box : st::upgradeGiftBox);
box->setWidth(st::boxWideWidth);
box->addTopButton(st.close ? *st.close : st::boxTitleClose, [=] {
box->closeBox();
});
const auto priceNow = unique->starsForResale;
const auto name = Data::UniqueGiftName(*unique);
const auto slug = unique->slug;
const auto session = &show->session();
AddSubsectionTitle(
box->verticalLayout(),
tr::lng_gift_sell_placeholder(),
(st::boxRowPadding - QMargins(
st::defaultSubsectionTitlePadding.left(),
0,
st::defaultSubsectionTitlePadding.right(),
st::defaultSubsectionTitlePadding.bottom())));
const auto &appConfig = session->appConfig();
const auto limit = appConfig.giftResalePriceMax();
const auto minimal = appConfig.giftResalePriceMin();
const auto thousandths = appConfig.giftResaleReceiveThousandths();
const auto wrap = box->addRow(object_ptr<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());
}));
const auto session = &show->session();
const auto &title = unique->title;
InvokeWithUniqueGiftResellPrice(session, title, [=](int price) {
show->show(Box(UniqueGiftSellBox, show, unique, savedId, price, st));
});
}
void GiftReleasedByHandler(not_null<PeerData*> peer) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -72,7 +72,7 @@ namespace {
| (data.is_send_audios() ? Flag::SendMusic : Flag())
| (data.is_send_voices() ? Flag::SendVoiceMessages : Flag())
| (data.is_send_docs() ? Flag::SendFiles : Flag())
| (data.is_send_messages() ? Flag::SendOther : Flag())
| (data.is_send_plain() ? Flag::SendOther : Flag())
| (data.is_embed_links() ? Flag::EmbedLinks : Flag())
| (data.is_change_info() ? Flag::ChangeInfo : Flag())
| (data.is_invite_users() ? Flag::AddParticipants : Flag())
@ -142,7 +142,7 @@ MTPChatBannedRights RestrictionsToMTP(ChatRestrictionsInfo info) {
| ((flags & R::SendMusic) ? Flag::f_send_audios : Flag())
| ((flags & R::SendVoiceMessages) ? Flag::f_send_voices : Flag())
| ((flags & R::SendFiles) ? Flag::f_send_docs : Flag())
| ((flags & R::SendOther) ? Flag::f_send_messages : Flag())
| ((flags & R::SendOther) ? Flag::f_send_plain : Flag())
| ((flags & R::EmbedLinks) ? Flag::f_embed_links : Flag())
| ((flags & R::ChangeInfo) ? Flag::f_change_info : Flag())
| ((flags & R::AddParticipants) ? Flag::f_invite_users : Flag())

View file

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

View file

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

View file

@ -96,17 +96,6 @@ Thread *SavedMessages::activeSubsectionThread() const {
return _activeSubsectionSublist;
}
Dialogs::UnreadState SavedMessages::unreadStateWithParentMuted() const {
auto result = _chatsList.unreadState();
if (_owningHistory->muted()) {
result.chatsMuted = result.chats;
result.marksMuted = result.marks;
result.messagesMuted = result.messages;
result.reactionsMuted = result.reactions;
}
return result;
}
SavedMessages::~SavedMessages() {
clear();
}
@ -458,6 +447,9 @@ void SavedMessages::applySublistDeleted(not_null<PeerData*> sublistPeer) {
if (ranges::contains(_lastSublists, not_null(raw))) {
reorderLastSublists();
}
if (_activeSubsectionSublist == raw) {
_activeSubsectionSublist = nullptr;
}
_sublistDestroyed.fire(raw);
session().changes().sublistUpdated(

View file

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

View file

@ -217,7 +217,28 @@ void SavedSublist::applyItemRemoved(MsgId id) {
if (const auto chatListItem = _chatListMessage.value_or(nullptr)) {
if (chatListItem->id == id) {
_chatListMessage = std::nullopt;
requestChatListMessage();
crl::on_main(this, [=] {
// We didn't yet update _list here.
if (_chatListMessage.has_value()) {
return;
} else if (_skippedAfter == 0) {
if (!_list.empty()) {
applyMaybeLast(owner().message(
owningHistory()->peer,
_list.front()));
return;
} else if (_skippedBefore == 0) {
setLastServerMessage(nullptr);
updateChatListExistence();
return;
}
}
if (_parent->parentChat()) {
requestChatListMessage();
} else {
loadAround(0);
}
});
}
}
}
@ -1110,6 +1131,10 @@ void SavedSublist::loadAround(MsgId id) {
_list.clear();
if (processMessagesIsEmpty(result)) {
_fullCount = _skippedBefore = _skippedAfter = 0;
if (!_parent->parentChat() && !_chatListMessage) {
setLastServerMessage(nullptr);
updateChatListExistence();
}
} else if (id) {
Assert(!_list.empty());
if (_list.front() <= id) {
@ -1117,6 +1142,11 @@ void SavedSublist::loadAround(MsgId id) {
} else if (_list.back() >= id) {
_skippedBefore = 0;
}
} else if (!_parent->parentChat() && !_chatListMessage) {
Assert(!_list.empty());
applyMaybeLast(owner().message(
owningHistory()->peer,
_list.front()));
}
checkReadTillEnd();
}).fail([=](const MTP::Error &error) {

View file

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

View file

@ -244,6 +244,17 @@ dialogsEmptyLabel: FlatLabel(defaultFlatLabel) {
align: align(top);
textFg: windowSubTextFg;
}
dialogEmptyButton: RoundButton(defaultActiveButton) {
}
dialogEmptyButtonSkip: 12px;
dialogEmptyButtonLabel: FlatLabel(defaultFlatLabel) {
style: TextStyle(defaultTextStyle) {
font: font(boxFontSize semibold);
}
minWidth: 32px;
align: align(top);
textFg: windowFg;
}
dialogsMenuToggle: IconButton {
width: 40px;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -964,12 +964,26 @@ void HistoryItem::updateServiceDependent(bool force) {
}
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->peerId
? _history->owner().peer(dependent->peerId)
: _history->peer),
dependent->msgId,
fullId());
fullId(),
{ .todoItemId = todoItemId });
}
auto gotDependencyItem = false;
if (!dependent->msg) {
@ -1858,7 +1872,10 @@ bool HistoryItem::isAyuNoForwards() const {
}
bool HistoryItem::canLookupMessageAuthor() const {
return isRegular() && _history->amMonoforumAdmin() && _from->isChannel();
return isRegular()
&& !isService()
&& _history->amMonoforumAdmin()
&& _from->isChannel();
}
bool HistoryItem::skipNotification() const {
@ -4392,6 +4409,7 @@ void HistoryItem::createComponentsHelper(HistoryItemCommonFields &&fields) {
: replyTo.monoforumPeerId
? replyTo.monoforumPeerId
: PeerId();
config.reply.todoItemId = replyTo.todoItemId;
const auto replyToTop = replyTo.topicRootId
? replyTo.topicRootId
: LookupReplyToTop(_history, to);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -490,6 +490,8 @@ void Message::initPaidInformation() {
refreshSuggestedInfo(item, suggest, replyData);
}
return;
} else if (!item->history()->peer->isUser()) {
return;
}
const auto media = this->media();
const auto mine = PaidInformation{
@ -3348,7 +3350,7 @@ TextSelection Message::selectionFromQuote(
const SelectedQuote &quote) const {
Expects(quote.item != nullptr);
if (quote.text.empty()) {
if (quote.highlight.quote.empty()) {
return {};
}
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_session.h"
#include "data/data_story.h"
#include "data/data_todo_list.h"
#include "data/data_user.h"
#include "history/view/history_view_item_preview.h"
#include "history/history.h"
@ -42,6 +43,85 @@ namespace {
constexpr auto kNonExpandedLinesLimit = 5;
[[nodiscard]] QImage MakeTaskImage() {
const auto diameter = st::normalFont->ascent;
const auto line = st::historyPollRadio.thickness;
const auto size = 2 * line + diameter;
const auto ratio = style::DevicePixelRatio();
auto result = QImage(
QSize(size, size) * ratio,
QImage::Format_ARGB32_Premultiplied);
result.fill(Qt::transparent);
result.setDevicePixelRatio(ratio);
auto p = QPainter(&result);
PainterHighQualityEnabler hq(p);
p.setOpacity(st::historyPollRadioOpacity);
const auto rect = QRectF(line, line, diameter, diameter).marginsRemoved(
QMarginsF(line / 2., line / 2., line / 2., line / 2.));
auto pen = QPen(QColor(255, 255, 255));
pen.setWidth(line);
p.setPen(pen);
p.drawEllipse(rect);
p.end();
return result;
}
[[nodiscard]] QImage MakeTaskDoneImage() {
const auto white = QColor(255, 255, 255);
const auto black = QColor(0, 0, 0);
const auto diameter = st::normalFont->ascent;
const auto line = st::historyPollRadio.thickness;
const auto size = 2 * line + diameter;
const auto ratio = style::DevicePixelRatio();
auto result = QImage(
QSize(size, size) * ratio,
QImage::Format_ARGB32_Premultiplied);
result.fill(black);
result.setDevicePixelRatio(ratio);
auto p = QPainter(&result);
PainterHighQualityEnabler hq(p);
const auto rect = QRectF(line, line, diameter, diameter).marginsRemoved(
QMarginsF(line / 2., line / 2., line / 2., line / 2.));
auto pen = QPen(white);
pen.setWidth(line);
p.setPen(pen);
p.setBrush(white);
p.drawEllipse(rect);
const auto &icon = st::historyPollInChoiceRight;
icon.paint(
p,
line + (diameter - icon.width()) / 2,
line + (diameter - icon.height()) / 2,
size,
black);
p.end();
return style::colorizeImage(result, white);
}
[[nodiscard]] TextWithEntities TaskDoneIcon(
not_null<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
void ValidateBackgroundEmoji(
@ -197,6 +277,22 @@ void Reply::update(
const auto item = view->data();
const auto &fields = data->fields();
const auto message = data->resolvedMessage.get();
const auto messageMedia = (message && fields.todoItemId)
? message->media()
: nullptr;
const auto messageTodoList = messageMedia
? messageMedia->todolist()
: nullptr;
const auto taskIndex = messageTodoList
? int(ranges::find(
messageTodoList->items,
fields.todoItemId,
&TodoListItem::id) - begin(messageTodoList->items))
: -1;
const auto task = (taskIndex >= 0
&& taskIndex < messageTodoList->items.size())
? &messageTodoList->items[taskIndex]
: nullptr;
const auto story = data->resolvedStory.get();
const auto externalMedia = fields.externalMedia.get();
if (!_externalSender) {
@ -214,7 +310,6 @@ void Reply::update(
_hiddenSenderColorIndexPlusOne = (!_colorPeer && message)
? (message->originalHiddenSenderInfo()->colorIndex + 1)
: 0;
const auto hasPreview = (story && story->hasReplyPreview())
|| (message
&& message->media()
@ -229,8 +324,13 @@ void Reply::update(
&& !fields.quote.empty();
_hasQuoteIcon = hasQuoteIcon ? 1 : 0;
const auto session = &view->history()->session();
const auto text = (!_displaying && data->unavailable())
? TextWithEntities()
: task
? Ui::Text::Colorized(task->completionDate
? TaskDoneIcon(session)
: TaskIcon(session)).append(task->text)
: (message && (fields.quote.empty() || !fields.manualQuote))
? message->inReplyText()
: !fields.quote.empty()
@ -288,10 +388,11 @@ void Reply::setLinkFrom(
const auto &fields = data->fields();
const auto externalChannelId = peerToChannel(fields.externalPeerId);
const auto messageId = fields.messageId;
const auto quote = fields.manualQuote
? fields.quote
: TextWithEntities();
const auto quoteOffset = fields.quoteOffset;
const auto highlight = MessageHighlightId{
.quote = fields.manualQuote ? fields.quote : TextWithEntities(),
.quoteOffset = int(fields.quoteOffset),
.todoItemId = fields.todoItemId,
};
const auto returnToId = view->data()->fullId();
const auto externalLink = [=](ClickContext context) {
const auto my = context.other.value<ClickHandlerContext>();
@ -314,8 +415,7 @@ void Reply::setLinkFrom(
channel,
messageId,
returnToId,
quote,
quoteOffset
highlight
)->onClick(context);
} else {
controller->showPeerInfo(channel);
@ -336,7 +436,7 @@ void Reply::setLinkFrom(
const auto message = data->resolvedMessage.get();
const auto story = data->resolvedStory.get();
_link = message
? JumpToMessageClickHandler(message, returnToId, quote, quoteOffset)
? JumpToMessageClickHandler(message, returnToId, highlight)
: story
? JumpToStoryClickHandler(story)
: (data->external()
@ -873,18 +973,28 @@ TextWithEntities Reply::ForwardEmoji(not_null<Data::Session*> owner) {
TextWithEntities Reply::ComposePreviewName(
not_null<History*> history,
not_null<HistoryItem*> to,
bool quote) {
const FullReplyTo &replyTo) {
const auto sender = [&] {
if (const auto from = to->displayFrom()) {
return not_null(from);
}
return to->author();
}();
if (const auto media = replyTo.todoItemId ? to->media() : nullptr) {
if (const auto todolist = media->todolist()) {
return tr::lng_preview_reply_to_task(
tr::now,
lt_title,
todolist->title,
Ui::Text::WithEntities);
}
}
const auto toPeer = to->history()->peer;
const auto displayAsExternal = (to->history() != history);
const auto groupNameAdded = displayAsExternal
&& (toPeer != sender)
&& (toPeer->isChat() || toPeer->isMegagroup());
const auto quote = replyTo && !replyTo.quote.empty();
const auto shorten = groupNameAdded || quote;
auto nameFull = TextWithEntities();

View file

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

View file

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

View file

@ -463,17 +463,16 @@ QSize Service::performCountCurrentSize(int newWidth) {
const auto media = this->media();
const auto mediaDisplayed = media && media->isDisplayed();
auto contentWidth = newWidth;
if (delegate()->elementChatMode() == ElementChatMode::Wide) {
accumulate_min(contentWidth, st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left());
}
contentWidth -= st::msgServiceMargin.left() + st::msgServiceMargin.left(); // two small margins
if (contentWidth < st::msgServicePadding.left() + st::msgServicePadding.right() + 1) {
contentWidth = st::msgServicePadding.left() + st::msgServicePadding.right() + 1;
}
if (mediaDisplayed && media->hideServiceText()) {
newHeight += media->resizeGetHeight(newWidth) + marginBottom();
} else if (!text().isEmpty()) {
if (delegate()->elementChatMode() == ElementChatMode::Wide) {
accumulate_min(contentWidth, st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left());
}
contentWidth -= st::msgServiceMargin.left() + st::msgServiceMargin.left(); // two small margins
if (contentWidth < st::msgServicePadding.left() + st::msgServicePadding.right() + 1) {
contentWidth = st::msgServicePadding.left() + st::msgServicePadding.right() + 1;
}
auto nwidth = qMax(contentWidth - st::msgServicePadding.left() - st::msgServicePadding.right(), 0);
newHeight += (contentWidth >= maxWidth())
? minHeight()

View file

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

View file

@ -334,9 +334,11 @@ void TodoList::updateTasks(bool skipAnimations) {
ClickHandlerPtr TodoList::createTaskClickHandler(
const Task &task) {
const auto id = task.id;
return std::make_shared<LambdaClickHandler>(crl::guard(this, [=] {
auto result = std::make_shared<LambdaClickHandler>(crl::guard(this, [=] {
toggleCompletion(id);
}));
result->setProperty(kTodoListItemIdProperty, id);
return result;
}
void TodoList::startToggleAnimation(Task &task) {
@ -375,11 +377,24 @@ void TodoList::toggleCompletion(int id) {
if (i == end(_tasks)) {
return;
}
const auto selected = (i->completionDate != 0);
i->completionDate = selected ? TimeId() : base::unixtime::now();
if (!selected) {
i->setCompletedBy(_parent->history()->session().user());
}
const auto parentMedia = _parent->data()->media();
const auto baseList = parentMedia ? parentMedia->todolist() : nullptr;
if (baseList) {
const auto j = ranges::find(baseList->items, id, &TodoListItem::id);
if (j != end(baseList->items)) {
j->completionDate = i->completionDate;
j->completedBy = i->completedBy;
}
history()->owner().updateDependentMessages(_parent->data());
}
startToggleAnimation(*i);
repaint();
@ -467,6 +482,7 @@ void TodoList::draw(Painter &p, const PaintContext &context) const {
paintw,
width(),
context);
appendTaskHighlight(task.id, tshift, height, context);
if (was) {
heavy = true;
} else if (!task.userpic.null()) {
@ -561,6 +577,33 @@ int TodoList::paintTask(
return height;
}
void TodoList::appendTaskHighlight(
int id,
int top,
int height,
const PaintContext &context) const {
if (context.highlight.todoItemId != id
|| context.highlight.collapsion <= 0.) {
return;
}
const auto to = context.highlightInterpolateTo;
const auto toProgress = (1. - context.highlight.collapsion);
if (toProgress >= 1.) {
context.highlightPathCache->addRect(to);
} else if (toProgress <= 0.) {
context.highlightPathCache->addRect(0, top, width(), height);
} else {
const auto lerp = [=](int from, int to) {
return from + (to - from) * toProgress;
};
context.highlightPathCache->addRect(
lerp(0, to.x()),
lerp(top, to.y()),
lerp(width(), to.width()),
lerp(height, to.height()));
}
}
void TodoList::paintRadio(
Painter &p,
const Task &task,

View file

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

View file

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

View file

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

View file

@ -949,7 +949,20 @@ QString FormatCountDecimal(int64 number) {
}
QString FormatExactCountDecimal(float64 number) {
return QLocale().toString(number, 'f', QLocale::FloatingPointShortest);
const auto locale = QLocale();
if (qFuzzyCompare(number, base::SafeRound(number))) {
return locale.toString(int64(base::SafeRound(number)));
}
// Somehow using QLocale::FloatingPointShortest sometimes produces
// "0.8500000000000001" on some systems / locales,
// so I want to stick to 6 digits max (default third argument value).
auto result = locale.toString(number, 'f');
const auto zero = locale.zeroDigit();
while (result.endsWith(zero)) {
result.chop(1);
}
return result;
}
ShortenedCount FormatCreditsAmountToShort(CreditsAmount amount) {

View file

@ -1087,3 +1087,34 @@ mediaviewSponsoredButton: RoundButton(defaultActiveButton) {
ripple: universalRippleAnimation;
}
mediaSponsoredSkip: 16px;
mediaSponsoredShift: 16px;
mediaSponsoredPadding: margins(12px, 8px, 8px, 8px);
mediaSponsoredThumb: 48px;
mediaSponsoredCloseTwice: 3px;
mediaSponsoredCloseSmall: 3px;
mediaSponsoredCloseSize: 11px;
mediaSponsoredCloseCorner: 6px;
mediaSponsoredCloseFull: 64px;
mediaSponsoredCloseStroke: 2px;
mediaSponsoredCloseRipple: 36px;
mediaSponsoredCloseDiameter: 24px;
mediaSponsoredCloseFont: font(12px bold);
mediaSponsoredAbout: RoundButton(defaultActiveButton) {
textFg: windowActiveTextFg;
textFgOver: windowActiveTextFg;
textBg: lightButtonBgOver;
textBgOver: lightButtonBgOver;
width: -12px;
height: 18px;
radius: 9px;
textTop: 0px;
style: TextStyle(defaultTextStyle) {
font: font(12px);
}
ripple: RippleAnimation(defaultRippleAnimation) {
color: lightButtonBgRipple;
}
}

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

View file

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

View file

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

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

View file

@ -33,23 +33,25 @@ enum class SponsoredPhrases {
Search,
};
struct SponsoredMenuSettings {
bool dark = false;
bool skipAbout = false;
bool skipInfo = false;
};
void FillSponsored(
not_null<Ui::RpWidget*> parent,
const Ui::Menu::MenuCallback &addAction,
std::shared_ptr<ChatHelpers::Show> show,
SponsoredPhrases phrases,
const Data::SponsoredMessageDetails &details,
Data::SponsoredReportAction report,
bool mediaViewer,
bool skipAbout);
SponsoredMenuSettings settings = {});
void FillSponsored(
not_null<Ui::RpWidget*> parent,
const Ui::Menu::MenuCallback &addAction,
std::shared_ptr<ChatHelpers::Show> show,
const FullMsgId &fullId,
bool mediaViewer,
bool skipAbout = false);
SponsoredMenuSettings settings = {});
void ShowSponsored(
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;
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;
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;
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;
inputReplyToMonoForum#69d66c45 monoforum_peer_id:InputPeer = InputReplyTo;
@ -2720,4 +2720,4 @@ smsjobs.finishJob#4f1ebf24 flags:# job_id:string error:flags.0?string = Bool;
fragment.getCollectibleInfo#be1e85ba collectible:InputCollectible = fragment.CollectibleInfo;
// LAYER 207
// LAYER 209

View file

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

View file

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

View file

@ -324,9 +324,8 @@ void PointDetailsWidget::setXIndex(int xIndex) {
nullptr,
{ float64(xIndex), float64(xIndex) }).parts
: std::vector<PiePartData::Part>();
const auto multiplier = float64(kOneStarInNano);
const auto isCredits
= _chartData.currency == Data::StatisticalCurrency::Credits;
= (_chartData.currency == Data::StatisticalCurrency::Credits);
for (auto i = 0; i < _chartData.lines.size(); i++) {
const auto &dataLine = _chartData.lines[i];
auto textLine = Line();
@ -350,19 +349,23 @@ void PointDetailsWidget::setXIndex(int xIndex) {
? tr::lng_channel_earn_chart_overriden_detail_credits
: tr::lng_channel_earn_chart_overriden_detail_currency)(
tr::now));
const auto provided = dataLine.y[xIndex];
const auto value = isCredits
? CreditsAmount(provided, CreditsType::Stars)
: CreditsAmount(
provided / kOneStarInNano,
provided % kOneStarInNano,
CreditsType::Ton);
copy.value.setText(
_textStyle,
Lang::FormatExactCountDecimal(
dataLine.y[xIndex] / multiplier));
Lang::FormatCreditsAmountDecimal(value));
_lines.push_back(std::move(copy));
textLine.name.setText(
_textStyle,
tr::lng_channel_earn_chart_overriden_detail_usd(tr::now));
textLine.value.setText(
_textStyle,
Info::ChannelEarn::ToUsd(
dataLine.y[xIndex] / multiplier,
_chartData.currencyRate, 0));
Info::ChannelEarn::ToUsd(value, _chartData.currencyRate, 0));
}
_lines.push_back(std::move(textLine));
}

View file

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

View file

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

View file

@ -81,7 +81,7 @@ public:
void setSections(
SubsectionTabs sections,
Fn<bool()> paused);
void setActiveSectionFast(int active);
void setActiveSectionFast(int active, bool ignoreScroll = false);
[[nodiscard]] int sectionsCount() 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 }};
menuIconOrderDate: icon {{ "menu/order_date", menuIconColor }};
menuIconOrderNumber: icon {{ "menu/order_number", menuIconColor }};
menuIconAdd: icon{{ "menu/add", menuIconColor }};
menuIconTTLAny: icon {{ "menu/auto_delete_plain", menuIconColor }};
menuIconTTLAnyTextPosition: point(11px, 22px);
@ -204,6 +205,7 @@ menuBlueIconGroupCreate: icon {{ "menu/groups_create", lightButtonFg }};
mediaMenuIconStickers: icon {{ "menu/stickers", mediaviewMenuFg }};
mediaMenuIconCancel: icon {{ "menu/cancel", mediaviewMenuFg }};
mediaMenuIconChannel: icon {{ "menu/channel", mediaviewMenuFg }};
mediaMenuIconShowInChat: icon {{ "menu/show_in_chat", mediaviewMenuFg }};
mediaMenuIconShowInFolder: icon {{ "menu/show_in_folder", mediaviewMenuFg }};
mediaMenuIconDownload: icon {{ "menu/download", mediaviewMenuFg }};

View file

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

View file

@ -53,6 +53,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_peer_values.h"
#include "data/data_premium_limits.h"
#include "data/data_web_page.h"
#include "dialogs/ui/chat_search_in.h"
#include "passport/passport_form_controller.h"
#include "chat_helpers/tabbed_selector.h"
#include "chat_helpers/emoji_interactions.h"
@ -361,6 +362,14 @@ void DateClickHandler::onClick(ClickContext context) const {
}
}
MessageHighlightId SearchHighlightId(const QString &query) {
auto result = MessageHighlightId{ .quote = { query } };
if (!result.quote.empty()) {
result.quoteOffset = kSearchQueryOffsetHint;
}
return result;
}
SessionNavigation::SessionNavigation(not_null<Main::Session*> session)
: _session(session)
, _api(&_session->mtp()) {
@ -1149,8 +1158,7 @@ void SessionNavigation::showRepliesForMessage(
.repliesRootId = rootId,
},
commentId,
params.highlightPart,
params.highlightPartOffsetHint);
params.highlight);
memento->setFromTopic(topic);
showSection(std::move(memento), params);
return;
@ -1272,8 +1280,7 @@ void SessionNavigation::showSublist(
.sublist = sublist,
},
itemId,
params.highlightPart,
params.highlightPartOffsetHint);
params.highlight);
showSection(std::move(memento), params);
}
@ -1811,10 +1818,13 @@ void SessionController::activateFirstChatsFilter() {
}
}
bool SessionController::uniqueChatsInSearchResults() const {
bool SessionController::uniqueChatsInSearchResults(
const Dialogs::SearchState &state) const {
const auto global = (state.tab == Dialogs::ChatSearchTab::MyMessages)
|| (state.tab == Dialogs::ChatSearchTab::PublicPosts);
return session().supportMode()
&& !session().settings().supportAllSearchResults()
&& !_searchInChat.current();
&& (global || !state.inChat);
}
bool SessionController::openFolderInDifferentWindow(

View file

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

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

View file

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

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

2
cmake

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