Compare commits

..

4 Commits

Author SHA1 Message Date
John Preston
476e66d027 Version 6.3.3: Fix build with Xcode. 2025-11-21 20:37:46 +04:00
John Preston
fc11d81673 Version 6.3.3.
- Some more improvements for gift auctions.
2025-11-21 19:07:31 +04:00
John Preston
629754a353 Correctly track emoji pausing in suggestions bar. 2025-11-21 19:05:02 +04:00
John Preston
147dbee051 Implement active auctions chats list bar. 2025-11-21 18:58:03 +04:00
22 changed files with 730 additions and 81 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 935 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

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

View File

@@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
//
VS_VERSION_INFO VERSIONINFO
FILEVERSION 6,3,2,0
PRODUCTVERSION 6,3,2,0
FILEVERSION 6,3,3,0
PRODUCTVERSION 6,3,3,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@@ -62,10 +62,10 @@ BEGIN
BEGIN
VALUE "CompanyName", "Telegram FZ-LLC"
VALUE "FileDescription", "Telegram Desktop"
VALUE "FileVersion", "6.3.2.0"
VALUE "FileVersion", "6.3.3.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2025"
VALUE "ProductName", "Telegram Desktop"
VALUE "ProductVersion", "6.3.2.0"
VALUE "ProductVersion", "6.3.3.0"
END
END
BLOCK "VarFileInfo"

View File

@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
//
VS_VERSION_INFO VERSIONINFO
FILEVERSION 6,3,2,0
PRODUCTVERSION 6,3,2,0
FILEVERSION 6,3,3,0
PRODUCTVERSION 6,3,3,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@@ -53,10 +53,10 @@ BEGIN
BEGIN
VALUE "CompanyName", "Telegram FZ-LLC"
VALUE "FileDescription", "Telegram Desktop Updater"
VALUE "FileVersion", "6.3.2.0"
VALUE "FileVersion", "6.3.3.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2025"
VALUE "ProductName", "Telegram Desktop"
VALUE "ProductVersion", "6.3.2.0"
VALUE "ProductVersion", "6.3.3.0"
END
END
BLOCK "VarFileInfo"

View File

@@ -8,16 +8,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/star_gift_auction_box.h"
#include "api/api_text_entities.h"
#include "base/timer_rpl.h"
#include "base/unixtime.h"
#include "boxes/peers/replace_boost_box.h"
#include "boxes/send_credits_box.h" // CreditsEmojiSmall
#include "boxes/share_box.h"
#include "boxes/star_gift_box.h"
#include "calls/group/calls_group_common.h"
#include "core/application.h"
#include "core/credits_amount.h"
#include "core/ui_integration.h"
#include "data/components/credits.h"
#include "data/components/gift_auctions.h"
#include "data/stickers/data_custom_emoji.h"
#include "data/data_document.h"
#include "data/data_message_reactions.h"
#include "data/data_session.h"
#include "data/data_user.h"
@@ -180,6 +184,17 @@ struct BidSliderValues {
};
}
[[nodiscard]] QString NiceCountdownText(int seconds) {
const auto minutes = seconds / 60;
const auto hours = minutes / 60;
return hours
? u"%1:%2:%3"_q
.arg(hours)
.arg((minutes % 60), 2, 10, QChar('0'))
.arg((seconds % 60), 2, 10, QChar('0'))
: u"%1:%2"_q.arg(minutes).arg((seconds % 60), 2, 10, QChar('0'));
}
[[nodiscard]] object_ptr<RpWidget> MakeBidRow(
not_null<RpWidget*> parent,
std::shared_ptr<ChatHelpers::Show> show,
@@ -380,17 +395,11 @@ object_ptr<RpWidget> MakeAuctionInfoBlocks(
auto untilTitle = rpl::duplicate(
stateValue
) | rpl::map([=](const Data::GiftAuctionState &state) {
return SecondsLeftTillValue(state.nextRoundAt);
}) | rpl::flatten_latest() | rpl::map([=](int seconds) {
const auto minutes = seconds / 60;
const auto hours = minutes / 60;
return hours
? u"%1:%2:%3"_q
.arg(hours)
.arg((minutes % 60), 2, 10, QChar('0'))
.arg((seconds % 60), 2, 10, QChar('0'))
: u"%1:%2"_q.arg(minutes).arg((seconds % 60), 2, 10, QChar('0'));
}) | Text::ToWithEntities();
return SecondsLeftTillValue(state.nextRoundAt
? state.nextRoundAt
: state.endDate);
}) | rpl::flatten_latest(
) | rpl::map(NiceCountdownText) | rpl::map(tr::marked);
auto leftTitle = rpl::duplicate(
stateValue
) | rpl::map([=](const Data::GiftAuctionState &state) {
@@ -653,7 +662,10 @@ void AuctionBidBox(not_null<GenericBox*> box, AuctionBidBoxArgs &&args) {
box->closeBox();
if (const auto window = show->resolveWindow()) {
window->showPeer(update.to, ShowAtTheEndMsgId);
window->showPeerHistory(
update.to,
Window::SectionShow::Way::ClearStack,
ShowAtTheEndMsgId);
}
}
}, box->lifetime());
@@ -801,6 +813,8 @@ void AuctionBidBox(not_null<GenericBox*> box, AuctionBidBoxArgs &&args) {
lt_count,
perRound,
tr::rich),
.st = &st::auctionBidToast,
.attach = RectPart::Top,
.duration = kBidPlacedToastDuration,
});
}
@@ -1561,4 +1575,281 @@ void AuctionAboutBox(
).append(' ').append(tr::lng_auction_about_understood(tr::now))));
}
TextWithEntities ActiveAuctionsTitle(const Data::ActiveAuctions &auctions) {
const auto &list = auctions.list;
if (list.size() == 1) {
const auto auction = list.front();
return Data::SingleCustomEmoji(
auction->gift->document
).append(' ').append(tr::lng_auction_bar_active(tr::now));
}
auto result = tr::marked();
for (const auto auction : list | ranges::views::take(3)) {
result.append(Data::SingleCustomEmoji(auction->gift->document));
}
return result.append(' ').append(
tr::lng_auction_bar_active_many(tr::now, lt_count, list.size()));
}
ManyAuctionsState ActiveAuctionsState(const Data::ActiveAuctions &auctions) {
const auto &list = auctions.list;
const auto winning = [](not_null<Data::GiftAuctionState*> auction) {
const auto position = MyAuctionPosition(*auction);
return (position <= auction->gift->auctionGiftsPerRound)
? position
: 0;
};
if (list.size() == 1) {
const auto auction = list.front();
const auto position = winning(auction);
auto text = position
? tr::lng_auction_bar_winning(
tr::now,
lt_count,
position,
tr::marked)
: tr::lng_auction_bar_outbid(tr::now, tr::marked);
return { std::move(text), !position };
}
auto outbid = 0;
for (const auto auction : list) {
if (!winning(auction)) {
++outbid;
}
}
auto text = (outbid == list.size())
? tr::lng_auction_bar_outbid_all(tr::now, tr::marked)
: outbid
? tr::lng_auction_bar_outbid_some(
tr::now,
lt_count,
outbid,
tr::marked)
: tr::lng_auction_bar_winning_all(tr::now, tr::marked);
return { std::move(text), outbid != 0 };
}
rpl::producer<TextWithEntities> ActiveAuctionsButton(
const Data::ActiveAuctions &auctions) {
const auto &list = auctions.list;
const auto withIcon = [](const QString &text) {
using namespace Ui::Text;
return IconEmoji(&st::auctionBidEmoji).append(' ').append(text);
};
if (list.size() == 1) {
const auto auction = auctions.list.front();
const auto end = auction->nextRoundAt
? auction->nextRoundAt
: auction->endDate;
return SecondsLeftTillValue(end)
| rpl::map(NiceCountdownText)
| rpl::map(withIcon);
}
return tr::lng_auction_bar_view() | rpl::map(withIcon);
}
struct Single {
QString slug;
not_null<DocumentData*> document;
int round = 0;
int total = 0;
int bid = 0;
int position = 0;
int winning = 0;
TimeId ends = 0;
};
object_ptr<Ui::RpWidget> MakeActiveAuctionRow(
not_null<QWidget*> parent,
not_null<Window::SessionController*> window,
not_null<DocumentData*> document,
const QString &slug,
rpl::producer<Single> value) {
auto result = object_ptr<Ui::VerticalLayout>(parent);
const auto raw = result.data();
raw->add(object_ptr<Ui::RpWidget>(raw));
auto title = rpl::duplicate(value) | rpl::map([=](const Single &fields) {
return tr::lng_auction_bar_round(
tr::now,
lt_n,
QString::number(fields.round + 1),
lt_amount,
QString::number(fields.total));
});
raw->add(
object_ptr<Ui::FlatLabel>(
raw,
std::move(title),
st::auctionListTitle),
st::auctionListTitlePadding);
const auto tag = Data::CustomEmojiSizeTag::Isolated;
const auto sticker = std::shared_ptr<Ui::Text::CustomEmoji>(
document->owner().customEmojiManager().create(
document,
[=] { raw->update(); },
tag));
raw->paintRequest(
) | rpl::start_with_next([=] {
auto q = QPainter(raw);
sticker->paint(q, {
.textColor = st::windowFg->c,
.now = crl::now(),
.position = QPoint(),
});
}, raw->lifetime());
auto helper = Ui::Text::CustomEmojiHelper();
const auto star = helper.paletteDependent(Ui::Earn::IconCreditsEmoji());
auto text = rpl::duplicate(value) | rpl::map([=](const Single &fields) {
const auto stars = tr::marked(star).append(' ').append(
Lang::FormatCountDecimal(fields.bid));
const auto outbid = (fields.position > fields.winning);
return outbid
? tr::lng_auction_bar_bid_outbid(
tr::now,
lt_stars,
stars,
tr::rich)
: tr::lng_auction_bar_bid_ranked(
tr::now,
lt_stars,
stars,
lt_n,
tr::marked(QString::number(fields.position)),
tr::rich);
});
const auto subtitle = raw->add(
object_ptr<Ui::FlatLabel>(
raw,
std::move(text),
st::auctionListText,
st::defaultPopupMenu,
helper.context()),
st::auctionListTextPadding);
rpl::duplicate(value) | rpl::start_with_next([=](const Single &fields) {
const auto outbid = (fields.position > fields.winning);
subtitle->setTextColorOverride(outbid
? st::attentionButtonFg->c
: std::optional<QColor>());
}, subtitle->lifetime());
const auto button = raw->add(
object_ptr<Ui::RoundButton>(
raw,
rpl::single(QString()),
st::auctionListRaise),
st::auctionListRaisePadding);
button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
auto secondsLeft = rpl::duplicate(
value
) | rpl::map([=](const Single &fields) {
return SecondsLeftTillValue(fields.ends);
}) | rpl::flatten_latest();
button->setText(rpl::combine(
std::move(secondsLeft),
tr::lng_auction_bar_raise_bid()
) | rpl::map([=](int seconds, const QString &text) {
return Ui::Text::IconEmoji(
&st::auctionBidEmoji
).append(' ').append(text).append(' ').append(
Ui::Text::Colorized(NiceCountdownText(seconds)));
}));
button->setClickedCallback([=] {
window->showStarGiftAuction(slug);
});
button->setFullRadius(true);
raw->widthValue() | rpl::start_with_next([=](int width) {
button->setFullWidth(width);
}, button->lifetime());
return result;
}
Fn<void()> ActiveAuctionsCallback(
not_null<Window::SessionController*> window,
const Data::ActiveAuctions &auctions) {
const auto &list = auctions.list;
const auto count = int(list.size());
if (count == 1) {
const auto slug = list.front()->gift->auctionSlug;
return [=] {
window->showStarGiftAuction(slug);
};
}
struct Auctions {
std::vector<rpl::variable<Single>> list;
};
const auto state = std::make_shared<Auctions>();
const auto singleFrom = [](const Data::GiftAuctionState &state) {
return Single{
.slug = state.gift->auctionSlug,
.document = state.gift->document,
.round = state.currentRound,
.total = state.totalRounds,
.bid = int(state.my.bid),
.position = MyAuctionPosition(state),
.winning = state.gift->auctionGiftsPerRound,
.ends = state.nextRoundAt ? state.nextRoundAt : state.endDate,
};
};
for (const auto auction : list) {
state->list.push_back(singleFrom(*auction));
}
return [=] {
window->show(Box([=](not_null<Ui::GenericBox*> box) {
const auto rows = box->lifetime().make_state<
rpl::variable<int>
>(count);
box->setWidth(st::boxWideWidth);
box->setTitle(tr::lng_auction_bar_active_many(
lt_count,
rows->value() | tr::to_count()));
const auto auctions = &window->session().giftAuctions();
for (auto &entry : state->list) {
using Data::GiftAuctionState;
const auto &now = entry.current();
entry = auctions->state(
now.slug
) | rpl::filter([=](const GiftAuctionState &state) {
return state.my.bid != 0;
}) | rpl::map(singleFrom);
const auto skip = st::auctionListEntrySkip;
const auto row = box->addRow(
MakeActiveAuctionRow(
box,
window,
now.document,
now.slug,
entry.value()),
st::boxRowPadding + QMargins(0, skip, 0, skip));
auctions->state(
now.slug
) | rpl::start_with_next([=](const GiftAuctionState &state) {
if (!state.my.bid) {
delete row;
if (const auto now = rows->current(); now > 1) {
*rows = (now - 1);
} else {
box->closeBox();
}
}
}, row->lifetime());
}
box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); });
box->addButton(tr::lng_box_ok(), [=] { box->closeBox(); });
}));
};
}
} // namespace Ui

View File

@@ -13,6 +13,7 @@ class Show;
namespace Data {
struct GiftAuctionState;
struct ActiveAuctions;
} // namespace Data
namespace Info::PeerGifts {
@@ -60,4 +61,18 @@ void AuctionAboutBox(
int giftsPerRound,
Fn<void(Fn<void()> close)> understood);
[[nodiscard]] TextWithEntities ActiveAuctionsTitle(
const Data::ActiveAuctions &auctions);
struct ManyAuctionsState {
TextWithEntities text;
bool someOutbid = false;
};
[[nodiscard]] ManyAuctionsState ActiveAuctionsState(
const Data::ActiveAuctions &auctions);
[[nodiscard]] rpl::producer<TextWithEntities> ActiveAuctionsButton(
const Data::ActiveAuctions &auctions);
[[nodiscard]] Fn<void()> ActiveAuctionsCallback(
not_null<Window::SessionController*> window,
const Data::ActiveAuctions &auctions);
} // namespace Ui

View File

@@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D1ED}"_cs;
constexpr auto AppNameOld = "Telegram Win (Unofficial)"_cs;
constexpr auto AppName = "Telegram Desktop"_cs;
constexpr auto AppFile = "Telegram"_cs;
constexpr auto AppVersion = 6003002;
constexpr auto AppVersionStr = "6.3.2";
constexpr auto AppVersion = 6003003;
constexpr auto AppVersionStr = "6.3.3";
constexpr auto AppBetaVersion = false;
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;

View File

@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "data/components/gift_auctions.h"
#include "api/api_hash.h"
#include "api/api_premium.h"
#include "api/api_text_entities.h"
#include "apiwrap.h"
@@ -18,6 +19,16 @@ namespace Data {
GiftAuctions::GiftAuctions(not_null<Main::Session*> session)
: _session(session)
, _timer([=] { checkSubscriptions(); }) {
crl::on_main(_session, [=] {
rpl::merge(
_session->data().chatsListChanges(),
_session->data().chatsListLoadedEvents()
) | rpl::filter(
!rpl::mappers::_1
) | rpl::take(1) | rpl::start_with_next([=] {
requestActive();
}, _lifetime);
});
}
GiftAuctions::~GiftAuctions() = default;
@@ -50,15 +61,27 @@ rpl::producer<GiftAuctionState> GiftAuctions::state(const QString &slug) {
void GiftAuctions::apply(const MTPDupdateStarGiftAuctionState &data) {
if (const auto entry = find(data.vgift_id().v)) {
const auto was = myStateKey(entry->state);
apply(entry, data.vstate());
entry->changes.fire({});
if (was != myStateKey(entry->state)) {
_activeChanged.fire({});
}
} else {
requestActive();
}
}
void GiftAuctions::apply(const MTPDupdateStarGiftAuctionUserState &data) {
if (const auto entry = find(data.vgift_id().v)) {
const auto was = myStateKey(entry->state);
apply(entry, data.vuser_state());
entry->changes.fire({});
if (was != myStateKey(entry->state)) {
_activeChanged.fire({});
}
} else {
requestActive();
}
}
@@ -106,6 +129,37 @@ void GiftAuctions::requestAcquired(
}).send();
}
rpl::producer<ActiveAuctions> GiftAuctions::active() const {
return _activeChanged.events_starting_with_copy(
rpl::empty
) | rpl::map([=] {
return collectActive();
});
}
rpl::producer<bool> GiftAuctions::hasActiveChanges() const {
const auto has = hasActive();
return _activeChanged.events(
) | rpl::map([=] {
return hasActive();
}) | rpl::combine_previous(
has
) | rpl::filter([=](bool previous, bool current) {
return previous != current;
}) | rpl::map([=](bool previous, bool current) {
return current;
});
}
bool GiftAuctions::hasActive() const {
for (const auto &[slug, entry] : _map) {
if (myStateKey(entry->state)) {
return true;
}
}
return false;
}
void GiftAuctions::checkSubscriptions() {
const auto now = crl::now();
auto next = crl::time();
@@ -128,6 +182,101 @@ void GiftAuctions::checkSubscriptions() {
}
}
auto GiftAuctions::myStateKey(const GiftAuctionState &state) const
-> MyStateKey {
if (!state.my.bid) {
return {};
}
auto min = 0;
for (const auto &level : state.bidLevels) {
if (level.position > state.gift->auctionGiftsPerRound) {
break;
} else if (!min || min > level.amount) {
min = level.amount;
}
}
return {
.bid = int(state.my.bid),
.position = MyAuctionPosition(state),
.version = state.version,
};
}
ActiveAuctions GiftAuctions::collectActive() const {
auto result = ActiveAuctions();
result.list.reserve(_map.size());
for (const auto &[slug, entry] : _map) {
const auto raw = &entry->state;
if (raw->gift && raw->my.date) {
result.list.push_back(raw);
}
}
return result;
}
uint64 GiftAuctions::countActiveHash() const {
auto result = Api::HashInit();
for (const auto &active : collectActive().list) {
Api::HashUpdate(result, active->version);
Api::HashUpdate(result, active->my.date);
}
return Api::HashFinalize(result);
}
void GiftAuctions::requestActive() {
if (_activeRequestId) {
return;
}
_activeRequestId = _session->api().request(
MTPpayments_GetStarGiftActiveAuctions(MTP_long(countActiveHash()))
).done([=](const MTPpayments_StarGiftActiveAuctions &result) {
result.match([=](const MTPDpayments_starGiftActiveAuctions &data) {
const auto owner = &_session->data();
owner->processUsers(data.vusers());
auto giftsFound = base::flat_set<QString>();
const auto &list = data.vauctions().v;
giftsFound.reserve(list.size());
for (const auto &auction : list) {
const auto &data = auction.data();
auto gift = Api::FromTL(_session, data.vgift());
const auto slug = gift ? gift->auctionSlug : QString();
if (slug.isEmpty()) {
LOG(("Api Error: Bad auction gift."));
continue;
}
auto &entry = _map[slug];
if (!entry) {
entry = std::make_unique<Entry>();
}
const auto raw = entry.get();
if (!raw->state.gift) {
raw->state.gift = std::move(gift);
}
apply(raw, data.vstate());
apply(raw, data.vuser_state());
giftsFound.emplace(slug);
}
for (const auto &[slug, entry] : _map) {
const auto my = &entry->state.my;
if (my->date && !giftsFound.contains(slug)) {
my->to = nullptr;
my->minBidAmount = 0;
my->bid = 0;
my->date = 0;
my->returned = false;
giftsFound.emplace(slug);
}
}
for (const auto &slug : giftsFound) {
_map[slug]->changes.fire({});
}
_activeChanged.fire({});
}, [](const MTPDpayments_starGiftActiveAuctionsNotModified &) {
});
}).send();
}
void GiftAuctions::request(const QString &slug) {
auto &entry = _map[slug];
Assert(entry != nullptr);
@@ -144,6 +293,8 @@ void GiftAuctions::request(const QString &slug) {
raw->requested = false;
const auto &data = result.data();
_session->data().processUsers(data.vusers());
raw->state.gift = Api::FromTL(_session, data.vgift());
if (!raw->state.gift) {
return;
@@ -152,8 +303,7 @@ void GiftAuctions::request(const QString &slug) {
const auto ms = timeout * crl::time(1000);
raw->state.subscribedTill = ms ? (crl::now() + ms) : -1;
_session->data().processUsers(data.vusers());
const auto was = myStateKey(raw->state);
apply(raw, data.vstate());
apply(raw, data.vuser_state());
if (raw->changes.has_consumers()) {
@@ -162,6 +312,9 @@ void GiftAuctions::request(const QString &slug) {
_timer.callOnce(ms);
}
}
if (was != myStateKey(raw->state)) {
_activeChanged.fire({});
}
}).send();
}
@@ -177,49 +330,54 @@ GiftAuctions::Entry *GiftAuctions::find(uint64 giftId) const {
void GiftAuctions::apply(
not_null<Entry*> entry,
const MTPStarGiftAuctionState &state) {
Expects(entry->state.gift.has_value());
apply(&entry->state, state);
}
void GiftAuctions::apply(
not_null<GiftAuctionState*> entry,
const MTPStarGiftAuctionState &state) {
Expects(entry->gift.has_value());
const auto raw = &entry->state;
state.match([&](const MTPDstarGiftAuctionState &data) {
const auto version = data.vversion().v;
if (raw->version >= version) {
if (entry->version >= version) {
return;
}
const auto owner = &_session->data();
raw->startDate = data.vstart_date().v;
raw->endDate = data.vend_date().v;
raw->minBidAmount = data.vmin_bid_amount().v;
entry->startDate = data.vstart_date().v;
entry->endDate = data.vend_date().v;
entry->minBidAmount = data.vmin_bid_amount().v;
const auto &levels = data.vbid_levels().v;
raw->bidLevels.clear();
raw->bidLevels.reserve(levels.size());
entry->bidLevels.clear();
entry->bidLevels.reserve(levels.size());
for (const auto &level : levels) {
auto &entry = raw->bidLevels.emplace_back();
auto &bid = entry->bidLevels.emplace_back();
const auto &data = level.data();
entry.amount = data.vamount().v;
entry.position = data.vpos().v;
entry.date = data.vdate().v;
bid.amount = data.vamount().v;
bid.position = data.vpos().v;
bid.date = data.vdate().v;
}
const auto &top = data.vtop_bidders().v;
raw->topBidders.clear();
raw->topBidders.reserve(top.size());
entry->topBidders.clear();
entry->topBidders.reserve(top.size());
for (const auto &user : top) {
raw->topBidders.push_back(owner->user(UserId(user.v)));
entry->topBidders.push_back(owner->user(UserId(user.v)));
}
raw->nextRoundAt = data.vnext_round_at().v;
raw->giftsLeft = data.vgifts_left().v;
raw->currentRound = data.vcurrent_round().v;
raw->totalRounds = data.vtotal_rounds().v;
raw->averagePrice = 0;
entry->nextRoundAt = data.vnext_round_at().v;
entry->giftsLeft = data.vgifts_left().v;
entry->currentRound = data.vcurrent_round().v;
entry->totalRounds = data.vtotal_rounds().v;
entry->averagePrice = 0;
}, [&](const MTPDstarGiftAuctionStateFinished &data) {
raw->averagePrice = data.vaverage_price().v;
raw->startDate = data.vstart_date().v;
raw->endDate = data.vend_date().v;
raw->minBidAmount = 0;
raw->nextRoundAt
= raw->currentRound
= raw->totalRounds
= raw->giftsLeft
= raw->version
entry->averagePrice = data.vaverage_price().v;
entry->startDate = data.vstart_date().v;
entry->endDate = data.vend_date().v;
entry->minBidAmount = 0;
entry->nextRoundAt
= entry->currentRound
= entry->totalRounds
= entry->giftsLeft
= entry->version
= 0;
}, [&](const MTPDstarGiftAuctionStateNotModified &data) {
});
@@ -228,16 +386,32 @@ void GiftAuctions::apply(
void GiftAuctions::apply(
not_null<Entry*> entry,
const MTPStarGiftAuctionUserState &state) {
apply(&entry->state.my, state);
}
void GiftAuctions::apply(
not_null<StarGiftAuctionMyState*> entry,
const MTPStarGiftAuctionUserState &state) {
const auto &data = state.data();
const auto raw = &entry->state.my;
raw->to = data.vbid_peer()
entry->to = data.vbid_peer()
? _session->data().peer(peerFromMTP(*data.vbid_peer())).get()
: nullptr;
raw->minBidAmount = data.vmin_bid_amount().value_or(0);
raw->bid = data.vbid_amount().value_or(0);
raw->date = data.vbid_date().value_or(0);
raw->gotCount = data.vacquired_count().v;
raw->returned = data.is_returned();
entry->minBidAmount = data.vmin_bid_amount().value_or(0);
entry->bid = data.vbid_amount().value_or(0);
entry->date = data.vbid_date().value_or(0);
entry->gotCount = data.vacquired_count().v;
entry->returned = data.is_returned();
}
int MyAuctionPosition(const GiftAuctionState &state) {
const auto &levels = state.bidLevels;
for (auto i = begin(levels), e = end(levels); i != e; ++i) {
if (i->amount < state.my.bid
|| (i->amount == state.my.bid && i->date > state.my.date)) {
return i->position;
}
}
return (levels.empty() ? 0 : levels.back().position) + 1;
}
} // namespace Data

View File

@@ -62,6 +62,10 @@ struct GiftAcquired {
bool nameHidden = false;
};
struct ActiveAuctions {
std::vector<not_null<GiftAuctionState*>> list;
};
class GiftAuctions final {
public:
explicit GiftAuctions(not_null<Main::Session*> session);
@@ -73,31 +77,63 @@ public:
void apply(const MTPDupdateStarGiftAuctionUserState &data);
void requestAcquired(
uint64 giftId,
uint64 giftId,
Fn<void(std::vector<Data::GiftAcquired>)> done);
[[nodiscard]] rpl::producer<ActiveAuctions> active() const;
[[nodiscard]] rpl::producer<bool> hasActiveChanges() const;
[[nodiscard]] bool hasActive() const;
private:
struct Entry {
GiftAuctionState state;
rpl::event_stream<> changes;
bool requested = false;
};
struct MyStateKey {
int bid = 0;
int position = 0;
int version = 0;
explicit operator bool() const {
return bid != 0;
}
friend inline bool operator==(MyStateKey, MyStateKey) = default;
};
void request(const QString &slug);
Entry *find(uint64 giftId) const;
void apply(
not_null<Entry*> entry,
const MTPStarGiftAuctionState &state);
void apply(
not_null<GiftAuctionState*> entry,
const MTPStarGiftAuctionState &state);
void apply(
not_null<Entry*> entry,
const MTPStarGiftAuctionUserState &state);
void apply(
not_null<StarGiftAuctionMyState*> entry,
const MTPStarGiftAuctionUserState &state);
void checkSubscriptions();
[[nodiscard]] MyStateKey myStateKey(const GiftAuctionState &state) const;
[[nodiscard]] ActiveAuctions collectActive() const;
[[nodiscard]] uint64 countActiveHash() const;
void requestActive();
const not_null<Main::Session*> _session;
base::Timer _timer;
base::flat_map<QString, std::unique_ptr<Entry>> _map;
rpl::event_stream<> _activeChanged;
mtpRequestId _activeRequestId = 0;
rpl::lifetime _lifetime;
};
[[nodiscard]] int MyAuctionPosition(const GiftAuctionState &state);
} // namespace Data

View File

@@ -129,6 +129,11 @@ dialogRowOpenBot: DialogRightButton {
dialogRowOpenBotRecent: DialogRightButton(dialogRowOpenBot) {
margin: margins(0px, 32px, 16px, 0px);
}
dialogsTopBarRightButton: RoundButton(defaultActiveButton) {
width: -16px;
height: 22px;
textTop: 2px;
}
forumDialogJumpArrow: icon{{ "dialogs/dialogs_topic_arrow", dialogsTextFg }};
forumDialogJumpArrowOver: icon{{ "dialogs/dialogs_topic_arrow", dialogsTextFgOver }};

View File

@@ -14,9 +14,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "apiwrap.h"
#include "base/call_delayed.h"
#include "boxes/star_gift_box.h" // ShowStarGiftBox.
#include "boxes/star_gift_auction_box.h"
#include "core/application.h"
#include "core/click_handler_types.h"
#include "core/ui_integration.h"
#include "data/components/gift_auctions.h"
#include "data/components/promo_suggestions.h"
#include "data/data_birthday.h"
#include "data/data_changes.h"
@@ -184,6 +186,7 @@ rpl::producer<Ui::SlideWrap<Ui::RpWidget>*> TopBarSuggestionValue(
rpl::lifetime userpicLifetime;
rpl::lifetime giftsLifetime;
rpl::lifetime creditsLifetime;
rpl::lifetime auctionsLifetime;
std::unique_ptr<Api::CreditsHistory> creditsHistory;
};
@@ -193,8 +196,11 @@ rpl::producer<Ui::SlideWrap<Ui::RpWidget>*> TopBarSuggestionValue(
rpl::single(st::dialogsTopBarLeftPadding));
const auto ensureContent = [=] {
if (!state->content) {
const auto window = FindSessionController(parent);
state->content = Ui::CreateChild<TopBarSuggestionContent>(
parent);
parent,
[=] { return window->isGifPausedAtLeastFor(
Window::GifPauseReason::Layer); });
rpl::combine(
parent->widthValue(),
state->content->desiredHeightValue()
@@ -229,6 +235,7 @@ rpl::producer<Ui::SlideWrap<Ui::RpWidget>*> TopBarSuggestionValue(
state->userpicLifetime.destroy();
state->giftsLifetime.destroy();
state->creditsLifetime.destroy();
state->auctionsLifetime.destroy();
if (!session->api().authorizations().unreviewed().empty()) {
state->content = nullptr;
@@ -273,7 +280,50 @@ rpl::producer<Ui::SlideWrap<Ui::RpWidget>*> TopBarSuggestionValue(
const auto wrap = state->wrap.get();
using RightIcon = TopBarSuggestionContent::RightIcon;
const auto promo = &session->promoSuggestions();
if (const auto custom = promo->custom()) {
const auto auctions = &session->giftAuctions();
if (auctions->hasActive()) {
using namespace Data;
struct Button {
rpl::variable<TextWithEntities> text;
Fn<void()> callback;
base::has_weak_ptr guard;
};
auto &lifetime = state->auctionsLifetime;
const auto button = lifetime.template make_state<Button>();
const auto window = FindSessionController(parent);
auctions->active(
) | rpl::start_with_next([=](ActiveAuctions &&active) {
const auto empty = active.list.empty();
state->desiredWrapToggle.force_assign(
Toggle{ !empty, anim::type::normal });
if (empty) {
return;
}
auto text = Ui::ActiveAuctionsState(active);
const auto textColorOverride = text.someOutbid
? st::attentionButtonFg->c
: std::optional<QColor>();
content->setContent(
Ui::ActiveAuctionsTitle(active),
std::move(text.text),
Core::TextContext({ .session = session }),
textColorOverride);
button->text = Ui::ActiveAuctionsButton(active);
button->callback = Ui::ActiveAuctionsCallback(
window,
active);
}, state->auctionsLifetime);
const auto callback = crl::guard(&button->guard, [=] {
button->callback();
});
content->setRightButton(button->text.value(), callback);
content->setClickedCallback(callback);
content->setLeftPadding(state->leftPadding.value());
state->desiredWrapToggle.force_assign(
Toggle{ true, anim::type::normal });
return;
} else if (const auto custom = promo->custom()) {
content->setRightIcon(RightIcon::Close);
content->setLeftPadding(state->leftPadding.value());
content->setClickedCallback([=] {
@@ -733,12 +783,14 @@ rpl::producer<Ui::SlideWrap<Ui::RpWidget>*> TopBarSuggestionValue(
rpl::merge(
session->promoSuggestions().value(),
session->api().authorizations().unreviewedChanges(),
Data::AmPremiumValue(session) | rpl::skip(1) | rpl::to_empty
Data::AmPremiumValue(session) | rpl::skip(1) | rpl::to_empty,
session->giftAuctions().hasActiveChanges() | rpl::to_empty
) | rpl::start_with_next([=] {
const auto was = state->wrap.get();
const auto weak = base::make_weak(was);
processCurrentSuggestion(processCurrentSuggestion);
if (was != state->wrap) {
consumer.put_next_copy(state->wrap);
if (was != state->wrap || (was && !weak)) {
consumer.put_next_copy(state->wrap.get());
}
}, lifetime);

View File

@@ -156,15 +156,19 @@ not_null<Ui::SlideWrap<Ui::VerticalLayout>*> CreateUnconfirmedAuthContent(
return wrap;
}
TopBarSuggestionContent::TopBarSuggestionContent(not_null<Ui::RpWidget*> p)
: Ui::RippleButton(p, st::defaultRippleAnimationBgOver)
TopBarSuggestionContent::TopBarSuggestionContent(
not_null<Ui::RpWidget*> parent,
Fn<bool()> emojiPaused)
: Ui::RippleButton(parent, st::defaultRippleAnimationBgOver)
, _titleSt(st::semiboldTextStyle)
, _contentTitleSt(st::dialogsTopBarSuggestionTitleStyle)
, _contentTextSt(st::dialogsTopBarSuggestionAboutStyle) {
, _contentTextSt(st::dialogsTopBarSuggestionAboutStyle)
, _emojiPaused(std::move(emojiPaused)) {
setRightIcon(RightIcon::Close);
}
void TopBarSuggestionContent::setRightIcon(RightIcon icon) {
_rightButton = nullptr;
if (icon == _rightIcon) {
return;
}
@@ -201,6 +205,35 @@ void TopBarSuggestionContent::setRightIcon(RightIcon icon) {
}
}
void TopBarSuggestionContent::setRightButton(
rpl::producer<TextWithEntities> text,
Fn<void()> callback) {
_rightHide = nullptr;
_rightArrow = nullptr;
_rightIcon = RightIcon::None;
if (!text) {
_rightButton = nullptr;
return;
}
using namespace Ui;
_rightButton = base::make_unique_q<RoundButton>(
this,
rpl::single(QString()),
st::dialogsTopBarRightButton);
_rightButton->setText(std::move(text));
rpl::combine(
sizeValue(),
_rightButton->sizeValue()
) | rpl::start_with_next([=](QSize outer, QSize inner) {
const auto top = (outer.height() - inner.height()) / 2;
_rightButton->moveToRight(top, top, outer.width());
}, _rightButton->lifetime());
_rightButton->setFullRadius(true);
_rightButton->setTextTransform(RoundButton::TextTransform::NoTransform);
_rightButton->setClickedCallback(std::move(callback));
_rightButton->show();
}
void TopBarSuggestionContent::draw(QPainter &p) {
const auto kLinesForPhoto = 3;
@@ -226,6 +259,8 @@ void TopBarSuggestionContent::draw(QPainter &p) {
- (_rightHide ? _rightHide->width() : 0);
const auto titleRight = leftPadding;
const auto hasSecondLineTitle = availableWidth < _contentTitle.maxWidth();
const auto paused = On(PowerSaving::kEmojiChat)
|| (_emojiPaused && _emojiPaused());
p.setPen(st::windowActiveTextFg);
p.setPen(st::windowFg);
{
@@ -237,7 +272,7 @@ void TopBarSuggestionContent::draw(QPainter &p) {
? availableWidth
: (availableWidth - titleRight),
.availableWidth = availableWidth,
.pausedEmoji = On(PowerSaving::kEmojiChat),
.pausedEmoji = paused,
.elisionLines = hasSecondLineTitle ? 2 : 1,
});
}
@@ -270,7 +305,7 @@ void TopBarSuggestionContent::draw(QPainter &p) {
: availableWidth,
};
};
p.setPen(st::windowSubTextFg);
p.setPen(_descriptionColorOverride.value_or(st::windowSubTextFg->c));
_contentText.draw(p, {
.position = QPoint(left, top),
.outerWidth = availableWidth,
@@ -278,7 +313,7 @@ void TopBarSuggestionContent::draw(QPainter &p) {
.geometry = Ui::Text::GeometryDescriptor{
.layout = std::move(lineLayout),
},
.pausedEmoji = On(PowerSaving::kEmojiChat),
.pausedEmoji = paused,
});
_lastPaintedContentTop = top;
_lastPaintedContentLineAmount = lastContentLineAmount;
@@ -288,7 +323,9 @@ void TopBarSuggestionContent::draw(QPainter &p) {
void TopBarSuggestionContent::setContent(
TextWithEntities title,
TextWithEntities description,
std::optional<Ui::Text::MarkedContext> context) {
std::optional<Ui::Text::MarkedContext> context,
std::optional<QColor> descriptionColorOverride) {
_descriptionColorOverride = descriptionColorOverride;
if (context) {
context->repaint = [=] { update(); };
_contentTitle.setMarkedText(
@@ -305,6 +342,7 @@ void TopBarSuggestionContent::setContent(
_contentTitle.setMarkedText(_contentTitleSt, std::move(title));
_contentText.setMarkedText(_contentTextSt, std::move(description));
}
update();
}
void TopBarSuggestionContent::paintEvent(QPaintEvent *) {

View File

@@ -40,17 +40,23 @@ public:
Arrow,
};
TopBarSuggestionContent(not_null<Ui::RpWidget*>);
TopBarSuggestionContent(
not_null<Ui::RpWidget*> parent,
Fn<bool()> emojiPaused = nullptr);
void setContent(
TextWithEntities title,
TextWithEntities description,
std::optional<Ui::Text::MarkedContext> context = std::nullopt);
std::optional<Ui::Text::MarkedContext> context = std::nullopt,
std::optional<QColor> descriptionColorOverride = std::nullopt);
[[nodiscard]] rpl::producer<int> desiredHeightValue() const override;
void setHideCallback(Fn<void()>);
void setRightIcon(RightIcon);
void setRightButton(
rpl::producer<TextWithEntities> text,
Fn<void()> callback);
void setLeftPadding(rpl::producer<int>);
[[nodiscard]] const style::TextStyle &contentTitleSt() const;
@@ -69,10 +75,13 @@ private:
Ui::Text::String _contentText;
rpl::variable<int> _lastPaintedContentLineAmount = 0;
rpl::variable<int> _lastPaintedContentTop = 0;
std::optional<QColor> _descriptionColorOverride;
base::unique_qptr<Ui::IconButton> _rightHide;
base::unique_qptr<Ui::IconButton> _rightArrow;
base::unique_qptr<Ui::RoundButton> _rightButton;
Fn<void()> _hideCallback;
Fn<bool()> _emojiPaused;
int _leftPadding = 0;

View File

@@ -444,6 +444,15 @@ videoStreamStarsCover: PremiumCover(creditsLowBalancePremiumCover) {
auctionInfoPreviewMargin: margins(0px, 24px, 0px, 8px);
auctionInfoSubtitleSkip: 8px;
auctionInfoTableMargin: margins(0px, 12px, 0px, 12px);
auctionBidEmoji: IconEmoji {
icon: icon {{ "settings/button_auction", windowFg }};
padding: margins(-4px, -2px, -4px, 0px);
}
auctionBidToast: Toast(defaultToast) {
padding: margins(54px, 13px, 19px, 12px);
icon: icon {{ "settings/toast_auction", toastFg }};
iconPosition: point(18px, 18px);
}
auctionAboutLogo: icon {{ "settings/large_auctions", windowFgActive }};
auctionAboutLogoPadding: margins(8px, 8px, 8px, 8px);
auctionCenteredSubtitle: FlatLabel(defaultFlatLabel) {
@@ -470,3 +479,19 @@ auctionBidStars: FlatLabel(defaultFlatLabel) {
textFg: windowSubTextFg;
}
auctionBidSkip: 10px;
auctionListEntrySkip: 12px;
auctionListTitle: FlatLabel(defaultFlatLabel) {
style: TextStyle(defaultTextStyle) {
font: font(15px semibold);
}
textFg: windowBoldFg;
}
auctionListTitlePadding: margins(50px, 0px, 0px, 0px);
auctionListText: FlatLabel(defaultFlatLabel) {
}
auctionListTextPadding: margins(50px, 4px, 0px, 0px);
auctionListRaise: RoundButton(defaultActiveButton) {
width: 0px;
}
auctionListRaisePadding: margins(0px, 8px, 0px, 0px);

View File

@@ -1,7 +1,7 @@
AppVersion 6003002
AppVersion 6003003
AppVersionStrMajor 6.3
AppVersionStrSmall 6.3.2
AppVersionStr 6.3.2
AppVersionStrSmall 6.3.3
AppVersionStr 6.3.3
BetaChannel 0
AlphaVersion 0
AppVersionOriginal 6.3.2
AppVersionOriginal 6.3.3

View File

@@ -1,3 +1,7 @@
6.3.3 (21.11.25)
- Some more improvements for gift auctions.
6.3.2 (20.11.25)
- Improved support for gift auctions.