mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-07-21 13:12:50 +02:00
Merge tag 'v5.16.4' into dev
This commit is contained in:
commit
5270f155ff
91 changed files with 2366 additions and 606 deletions
|
@ -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
|
||||
|
|
BIN
Telegram/Resources/animations/no_chats.tgs
Normal file
BIN
Telegram/Resources/animations/no_chats.tgs
Normal file
Binary file not shown.
Before Width: | Height: | Size: 470 B After Width: | Height: | Size: 470 B |
Before Width: | Height: | Size: 899 B After Width: | Height: | Size: 899 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
@ -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.";
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 = [=](
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -84,8 +84,6 @@ public:
|
|||
void saveActiveSubsectionThread(not_null<Thread*> thread);
|
||||
Thread *activeSubsectionThread() const;
|
||||
|
||||
[[nodiscard]] Dialogs::UnreadState unreadStateWithParentMuted() const;
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime();
|
||||
|
||||
private:
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {};
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -65,6 +65,7 @@ private:
|
|||
struct Highlight {
|
||||
FullMsgId itemId;
|
||||
TextSelection part;
|
||||
int todoListId = 0;
|
||||
|
||||
explicit operator bool() const {
|
||||
return itemId.operator bool();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -718,8 +718,7 @@ void DraftOptionsBox(
|
|||
state->link = args.usedLink;
|
||||
state->quote = SelectedQuote{
|
||||
replyItem,
|
||||
draft.reply.quote,
|
||||
draft.reply.quoteOffset,
|
||||
{ draft.reply.quote, draft.reply.quoteOffset },
|
||||
};
|
||||
state->forward = std::move(args.forward);
|
||||
state->webpage = draft.webpage;
|
||||
|
@ -783,7 +782,7 @@ void DraftOptionsBox(
|
|||
box->setTitle(hasLink
|
||||
? tr::lng_link_options_header()
|
||||
: hasReply
|
||||
? (state->quote.current().text.empty()
|
||||
? (state->quote.current().highlight.quote.empty()
|
||||
? tr::lng_reply_options_header()
|
||||
: tr::lng_reply_options_quote())
|
||||
: (forwardCount == 1)
|
||||
|
@ -807,10 +806,12 @@ void DraftOptionsBox(
|
|||
auto result = draft.reply;
|
||||
if (const auto current = state->quote.current()) {
|
||||
result.messageId = current.item->fullId();
|
||||
result.quote = current.text;
|
||||
result.quoteOffset = current.offset;
|
||||
result.quote = current.highlight.quote;
|
||||
result.quoteOffset = current.highlight.quoteOffset;
|
||||
// result.todoItemId = current.highlight.todoItemId;
|
||||
} else {
|
||||
result.quote = {};
|
||||
// result.todoItemId = 0;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
@ -1112,7 +1113,7 @@ void DraftOptionsBox(
|
|||
state->quote.value(),
|
||||
state->shown.value()
|
||||
) | rpl::map([=](const SelectedQuote "e, Section shown) {
|
||||
return (quote.text.empty() || shown != Section::Reply)
|
||||
return (quote.highlight.quote.empty() || shown != Section::Reply)
|
||||
? tr::lng_settings_save()
|
||||
: tr::lng_reply_quote_selected();
|
||||
}) | rpl::flatten_latest();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -644,8 +644,13 @@ bool AddReplyToMessageAction(
|
|||
return false;
|
||||
}
|
||||
|
||||
const auto todoListTaskId = request.link
|
||||
? request.link->property(kTodoListItemIdProperty).toInt()
|
||||
: 0;
|
||||
const auto "e = request.quote;
|
||||
auto text = (quote.text.empty()
|
||||
auto text = (todoListTaskId
|
||||
? tr::lng_context_reply_to_task
|
||||
: quote.highlight.quote.empty()
|
||||
? tr::lng_context_reply_msg
|
||||
: tr::lng_context_quote_and_reply)(
|
||||
tr::now,
|
||||
|
@ -653,8 +658,9 @@ bool AddReplyToMessageAction(
|
|||
menu->addAction(std::move(text), [=, itemId = item->fullId()] {
|
||||
list->replyToMessageRequestNotify({
|
||||
.messageId = itemId,
|
||||
.quote = quote.text,
|
||||
.quoteOffset = quote.offset,
|
||||
.quote = quote.highlight.quote,
|
||||
.quoteOffset = quote.highlight.quoteOffset,
|
||||
.todoItemId = todoListTaskId,
|
||||
}, base::IsCtrlPressed());
|
||||
}, &st::menuIconReply);
|
||||
return true;
|
||||
|
@ -680,7 +686,7 @@ bool AddTodoListAction(
|
|||
if (const auto item = controller->session().data().message(itemId)) {
|
||||
Window::PeerMenuAddTodoListTasks(controller, item);
|
||||
}
|
||||
}, &st::menuIconCreateTodoList);
|
||||
}, &st::menuIconAdd);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 "e) {
|
||||
Expects(quote.item != nullptr);
|
||||
|
||||
if (quote.text.empty()) {
|
||||
const auto &rich = quote.highlight.quote;
|
||||
if (rich.empty()) {
|
||||
return {};
|
||||
}
|
||||
const auto &original = quote.item->originalText();
|
||||
if (quote.offset == kSearchQueryOffsetHint) {
|
||||
if (quote.highlight.quoteOffset == kSearchQueryOffsetHint) {
|
||||
return ApplyModificationsFrom(
|
||||
FindSearchQueryHighlight(original.text, quote.text.text),
|
||||
FindSearchQueryHighlight(original.text, rich.text),
|
||||
text);
|
||||
}
|
||||
const auto length = int(original.text.size());
|
||||
const auto qlength = int(quote.text.text.size());
|
||||
const auto qlength = int(rich.text.size());
|
||||
const auto checkAt = [&](int offset) {
|
||||
return TextSelection{
|
||||
uint16(offset),
|
||||
|
@ -2234,7 +2245,7 @@ TextSelection Element::FindSelectionFromQuote(
|
|||
if (offset > length - qlength) {
|
||||
return TextSelection();
|
||||
}
|
||||
const auto i = original.text.indexOf(quote.text.text, offset);
|
||||
const auto i = original.text.indexOf(rich.text, offset);
|
||||
return (i >= 0) ? checkAt(i) : TextSelection();
|
||||
};
|
||||
const auto findOneBefore = [&](int offset) {
|
||||
|
@ -2243,7 +2254,7 @@ TextSelection Element::FindSelectionFromQuote(
|
|||
}
|
||||
const auto end = std::min(offset + qlength - 1, length);
|
||||
const auto from = end - length - 1;
|
||||
const auto i = original.text.lastIndexOf(quote.text.text, from);
|
||||
const auto i = original.text.lastIndexOf(rich.text, from);
|
||||
return (i >= 0) ? checkAt(i) : TextSelection();
|
||||
};
|
||||
const auto findAfter = [&](int offset) {
|
||||
|
@ -2281,7 +2292,7 @@ TextSelection Element::FindSelectionFromQuote(
|
|||
? before
|
||||
: after;
|
||||
};
|
||||
auto result = findTwoWays(quote.offset);
|
||||
auto result = findTwoWays(quote.highlight.quoteOffset);
|
||||
if (result.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
@ -2468,6 +2479,70 @@ int FindViewY(not_null<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()) {
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -490,6 +490,8 @@ void Message::initPaidInformation() {
|
|||
refreshSuggestedInfo(item, suggest, replyData);
|
||||
}
|
||||
return;
|
||||
} else if (!item->history()->peer->isUser()) {
|
||||
return;
|
||||
}
|
||||
const auto media = this->media();
|
||||
const auto mine = PaidInformation{
|
||||
|
@ -3348,7 +3350,7 @@ TextSelection Message::selectionFromQuote(
|
|||
const SelectedQuote "e) const {
|
||||
Expects(quote.item != nullptr);
|
||||
|
||||
if (quote.text.empty()) {
|
||||
if (quote.highlight.quote.empty()) {
|
||||
return {};
|
||||
}
|
||||
const auto item = quote.item;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -156,6 +156,7 @@ struct ChatPaintHighlight {
|
|||
float64 opacity = 0.;
|
||||
float64 collapsion = 0.;
|
||||
TextSelection range;
|
||||
int todoItemId = 0;
|
||||
};
|
||||
|
||||
struct ChatPaintContext {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 }};
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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();
|
||||
|
|
2
Telegram/ThirdParty/tgcalls
vendored
2
Telegram/ThirdParty/tgcalls
vendored
|
@ -1 +1 @@
|
|||
Subproject commit 1348de6aa6c07ed32354d3e26423c45304000a39
|
||||
Subproject commit d78b0507c54d76d5fe9691c8efe2638dee2c1536
|
|
@ -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
|
||||
|
|
|
@ -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
2
cmake
|
@ -1 +1 @@
|
|||
Subproject commit b032f270b622610ca3f42a83f37b3a183c9da0da
|
||||
Subproject commit f3d6471bd58dbad727d4f8fbccd0fb36632eee9e
|
Loading…
Add table
Reference in a new issue