init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

View File

@@ -0,0 +1,271 @@
/*
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 "data/components/credits.h"
#include "apiwrap.h"
#include "api/api_credits.h"
#include "data/data_user.h"
#include "lang/lang_keys.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
namespace Data {
namespace {
constexpr auto kReloadThreshold = 60 * crl::time(1000);
} // namespace
Credits::Credits(not_null<Main::Session*> session)
: _session(session)
, _reload([=] { load(true); }) {
}
Credits::~Credits() = default;
void Credits::apply(const MTPDupdateStarsBalance &data) {
apply(CreditsAmountFromTL(data.vbalance()));
}
rpl::producer<float64> Credits::rateValue(
not_null<PeerData*> ownedBotOrChannel) {
return rpl::single(_session->appConfig().starsWithdrawRate());
}
float64 Credits::usdRate() const {
return _session->appConfig().currencyWithdrawRate();
}
void Credits::load(bool force) {
if (_loader
|| (!force
&& _lastLoaded
&& _lastLoaded + kReloadThreshold > crl::now())) {
return;
}
const auto self = _session->user();
_loader = std::make_unique<rpl::lifetime>();
_loader->make_state<Api::CreditsStatus>(self)->request({}, [=](
Data::CreditsStatusSlice slice) {
const auto balance = slice.balance;
const auto apiStats
= _loader->make_state<Api::CreditsEarnStatistics>(self);
const auto finish = [=](bool statsEnabled) {
_statsEnabled = statsEnabled;
apply(balance);
_loader = nullptr;
};
apiStats->request() | rpl::on_error_done([=] {
finish(false);
}, [=] {
finish(true);
}, *_loader);
});
}
bool Credits::loaded() const {
return _lastLoaded != 0;
}
rpl::producer<bool> Credits::loadedValue() const {
if (loaded()) {
return rpl::single(true);
}
return rpl::single(
false
) | rpl::then(_loadedChanges.events() | rpl::map_to(true));
}
CreditsAmount Credits::balance() const {
return _nonLockedBalance.current();
}
CreditsAmount Credits::balance(PeerId peerId) const {
const auto it = _cachedPeerBalances.find(peerId);
return (it != _cachedPeerBalances.end()) ? it->second : CreditsAmount();
}
CreditsAmount Credits::balanceCurrency(PeerId peerId) const {
const auto it = _cachedPeerCurrencyBalances.find(peerId);
return (it != _cachedPeerCurrencyBalances.end())
? it->second
: CreditsAmount(0, 0, CreditsType::Ton);
}
rpl::producer<CreditsAmount> Credits::balanceValue() const {
return _nonLockedBalance.value();
}
void Credits::tonLoad(bool force) {
if (_tonRequestId
|| (!force
&& _tonLastLoaded
&& _tonLastLoaded + kReloadThreshold > crl::now())) {
return;
}
_tonRequestId = _session->api().request(MTPpayments_GetStarsStatus(
MTP_flags(MTPpayments_GetStarsStatus::Flag::f_ton),
MTP_inputPeerSelf()
)).done([=](const MTPpayments_StarsStatus &result) {
_tonRequestId = 0;
const auto amount = CreditsAmountFromTL(result.data().vbalance());
if (amount.ton()) {
apply(amount);
} else if (amount.empty()) {
apply(CreditsAmount(0, CreditsType::Ton));
} else {
LOG(("API Error: Got weird balance."));
}
}).fail([=](const MTP::Error &error) {
_tonRequestId = 0;
LOG(("API Error: Couldn't get TON balance, error: %1"
).arg(error.type()));
}).send();
}
bool Credits::tonLoaded() const {
return _tonLastLoaded != 0;
}
rpl::producer<bool> Credits::tonLoadedValue() const {
if (tonLoaded()) {
return rpl::single(true);
}
return rpl::single(
false
) | rpl::then(_tonLoadedChanges.events() | rpl::map_to(true));
}
CreditsAmount Credits::tonBalance() const {
return _tonBalance.current();
}
rpl::producer<CreditsAmount> Credits::tonBalanceValue() const {
return _tonBalance.value();
}
void Credits::updateNonLockedValue() {
_nonLockedBalance = (_balance >= _locked)
? (_balance - _locked)
: CreditsAmount();
}
void Credits::lock(CreditsAmount count) {
Expects(loaded());
Expects(count >= CreditsAmount(0));
Expects(_locked + count <= _balance);
_locked += count;
updateNonLockedValue();
}
void Credits::unlock(CreditsAmount count) {
Expects(count >= CreditsAmount(0));
Expects(_locked >= count);
_locked -= count;
updateNonLockedValue();
}
void Credits::withdrawLocked(CreditsAmount count) {
Expects(count >= CreditsAmount(0));
Expects(_locked >= count);
_locked -= count;
apply(_balance >= count ? (_balance - count) : CreditsAmount(0));
invalidate();
}
void Credits::invalidate() {
_reload.call();
}
void Credits::apply(CreditsAmount balance) {
if (balance.ton()) {
_tonBalance = balance;
const auto was = std::exchange(_tonLastLoaded, crl::now());
if (!was) {
_tonLoadedChanges.fire({});
}
} else {
_balance = balance;
updateNonLockedValue();
const auto was = std::exchange(_lastLoaded, crl::now());
if (!was) {
_loadedChanges.fire({});
}
}
}
void Credits::apply(PeerId peerId, CreditsAmount balance) {
_cachedPeerBalances[peerId] = balance;
_refreshedByPeerId.fire_copy(peerId);
}
void Credits::applyCurrency(PeerId peerId, CreditsAmount balance) {
_cachedPeerCurrencyBalances[peerId] = balance;
_refreshedByPeerId.fire_copy(peerId);
}
rpl::producer<> Credits::refreshedByPeerId(PeerId peerId) {
return _refreshedByPeerId.events(
) | rpl::filter(rpl::mappers::_1 == peerId) | rpl::to_empty;
}
bool Credits::statsEnabled() const {
return _statsEnabled;
}
} // namespace Data
CreditsAmount CreditsAmountFromTL(const MTPStarsAmount &amount) {
return amount.match([&](const MTPDstarsAmount &data) {
return CreditsAmount(
data.vamount().v,
data.vnanos().v,
CreditsType::Stars);
}, [&](const MTPDstarsTonAmount &data) {
const auto isNegative = (static_cast<int64_t>(data.vamount().v) < 0);
const auto absValue = isNegative
? uint64(~data.vamount().v + 1)
: data.vamount().v;
const auto result = CreditsAmount(
int64(absValue / 1'000'000'000),
absValue % 1'000'000'000,
CreditsType::Ton);
return isNegative
? CreditsAmount(0, CreditsType::Ton) - result
: result;
});
}
CreditsAmount CreditsAmountFromTL(const MTPStarsAmount *amount) {
return amount ? CreditsAmountFromTL(*amount) : CreditsAmount();
}
MTPStarsAmount StarsAmountToTL(CreditsAmount amount) {
return amount.ton() ? MTP_starsTonAmount(
MTP_long(amount.whole() * uint64(1'000'000'000) + amount.nano())
) : MTP_starsAmount(MTP_long(amount.whole()), MTP_int(amount.nano()));
}
QString PrepareCreditsAmountText(CreditsAmount amount) {
return amount.stars()
? tr::lng_action_gift_for_stars(
tr::now,
lt_count_decimal,
amount.value())
: tr::lng_action_gift_for_ton(
tr::now,
lt_count_decimal,
amount.value());
}

View File

@@ -0,0 +1,84 @@
/*
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
namespace Main {
class Session;
} // namespace Main
namespace Data {
class Credits final {
public:
explicit Credits(not_null<Main::Session*> session);
~Credits();
void load(bool force = false);
[[nodiscard]] bool loaded() const;
[[nodiscard]] rpl::producer<bool> loadedValue() const;
[[nodiscard]] CreditsAmount balance() const;
[[nodiscard]] CreditsAmount balance(PeerId peerId) const;
[[nodiscard]] rpl::producer<CreditsAmount> balanceValue() const;
[[nodiscard]] float64 usdRate() const;
[[nodiscard]] rpl::producer<float64> rateValue(
not_null<PeerData*> ownedBotOrChannel);
[[nodiscard]] rpl::producer<> refreshedByPeerId(PeerId peerId);
void tonLoad(bool force = false);
[[nodiscard]] bool tonLoaded() const;
[[nodiscard]] rpl::producer<bool> tonLoadedValue() const;
[[nodiscard]] CreditsAmount tonBalance() const;
[[nodiscard]] rpl::producer<CreditsAmount> tonBalanceValue() const;
void apply(CreditsAmount balance);
void apply(PeerId peerId, CreditsAmount balance);
[[nodiscard]] bool statsEnabled() const;
void applyCurrency(PeerId peerId, CreditsAmount balance);
[[nodiscard]] CreditsAmount balanceCurrency(PeerId peerId) const;
void lock(CreditsAmount count);
void unlock(CreditsAmount count);
void withdrawLocked(CreditsAmount count);
void invalidate();
void apply(const MTPDupdateStarsBalance &data);
private:
void updateNonLockedValue();
const not_null<Main::Session*> _session;
std::unique_ptr<rpl::lifetime> _loader;
base::flat_map<PeerId, CreditsAmount> _cachedPeerBalances;
base::flat_map<PeerId, CreditsAmount> _cachedPeerCurrencyBalances;
CreditsAmount _balance;
CreditsAmount _locked;
rpl::variable<CreditsAmount> _nonLockedBalance;
rpl::event_stream<> _loadedChanges;
crl::time _lastLoaded = 0;
float64 _rate = 0.;
rpl::variable<CreditsAmount> _tonBalance;
rpl::event_stream<> _tonLoadedChanges;
crl::time _tonLastLoaded = false;
mtpRequestId _tonRequestId = 0;
bool _statsEnabled = false;
rpl::event_stream<PeerId> _refreshedByPeerId;
SingleQueuedInvokation _reload;
};
} // namespace Data

View File

@@ -0,0 +1,218 @@
/*
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 "data/components/factchecks.h"
#include "api/api_text_entities.h"
#include "apiwrap.h"
#include "base/random.h"
#include "data/data_session.h"
#include "data/data_web_page.h"
#include "history/view/media/history_view_web_page.h"
#include "history/view/history_view_message.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/history_item_components.h"
#include "lang/lang_keys.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "ui/layers/show.h"
namespace Data {
namespace {
constexpr auto kRequestDelay = crl::time(1000);
} // namespace
Factchecks::Factchecks(not_null<Main::Session*> session)
: _session(session)
, _requestTimer([=] { request(); }) {
}
void Factchecks::requestFor(not_null<HistoryItem*> item) {
subscribeIfNotYet();
if (const auto factcheck = item->Get<HistoryMessageFactcheck>()) {
factcheck->requested = true;
}
if (!_requestTimer.isActive()) {
_requestTimer.callOnce(kRequestDelay);
}
const auto changed = !_pending.empty()
&& (_pending.front()->history() != item->history());
const auto added = _pending.emplace(item).second;
if (changed) {
request();
} else if (added && _pending.size() == 1) {
_requestTimer.callOnce(kRequestDelay);
}
}
void Factchecks::subscribeIfNotYet() {
if (_subscribed) {
return;
}
_subscribed = true;
_session->data().itemRemoved(
) | rpl::on_next([=](not_null<const HistoryItem*> item) {
_pending.remove(item);
const auto i = ranges::find(_requested, item.get());
if (i != end(_requested)) {
*i = nullptr;
}
}, _lifetime);
}
void Factchecks::request() {
_requestTimer.cancel();
if (!_requested.empty() || _pending.empty()) {
return;
}
_session->api().request(base::take(_requestId)).cancel();
auto ids = QVector<MTPint>();
ids.reserve(_pending.size());
const auto history = _pending.front()->history();
for (auto i = begin(_pending); i != end(_pending);) {
const auto &item = *i;
if (item->history() == history) {
_requested.push_back(item);
ids.push_back(MTP_int(item->id.bare));
i = _pending.erase(i);
} else {
++i;
}
}
_requestId = _session->api().request(MTPmessages_GetFactCheck(
history->peer->input(),
MTP_vector<MTPint>(std::move(ids))
)).done([=](const MTPVector<MTPFactCheck> &result) {
_requestId = 0;
const auto &list = result.v;
auto index = 0;
for (const auto &item : base::take(_requested)) {
if (!item) {
} else if (index >= list.size()) {
item->setFactcheck({});
} else {
item->setFactcheck(FromMTP(item, &list[index]));
}
++index;
}
if (!_pending.empty()) {
request();
}
}).fail([=] {
_requestId = 0;
for (const auto &item : base::take(_requested)) {
if (item) {
item->setFactcheck({});
}
}
if (!_pending.empty()) {
request();
}
}).send();
}
std::unique_ptr<HistoryView::WebPage> Factchecks::makeMedia(
not_null<HistoryView::Message*> view,
not_null<HistoryMessageFactcheck*> factcheck) {
if (!factcheck->page) {
factcheck->page = view->history()->owner().webpage(
base::RandomValue<WebPageId>(),
tr::lng_factcheck_title(tr::now),
factcheck->data.text);
factcheck->page->type = WebPageType::Factcheck;
}
return std::make_unique<HistoryView::WebPage>(
view,
factcheck->page,
MediaWebPageFlags());
}
bool Factchecks::canEdit(not_null<HistoryItem*> item) const {
if (!canEdit()
|| !item->isRegular()
|| !item->history()->peer->isBroadcast()) {
return false;
}
const auto media = item->media();
if (!media || media->webpage() || media->photo()) {
return true;
} else if (const auto document = media->document()) {
return !document->isVideoMessage() && !document->sticker();
}
return false;
}
bool Factchecks::canEdit() const {
return _session->appConfig().get<bool>(u"can_edit_factcheck"_q, false);
}
int Factchecks::lengthLimit() const {
return _session->appConfig().get<int>(u"factcheck_length_limit"_q, 1024);
}
void Factchecks::save(
FullMsgId itemId,
TextWithEntities text,
Fn<void(QString)> done) {
const auto item = _session->data().message(itemId);
if (!item) {
return;
} else if (text.empty()) {
_session->api().request(MTPmessages_DeleteFactCheck(
item->history()->peer->input(),
MTP_int(item->id.bare)
)).done([=](const MTPUpdates &result) {
_session->api().applyUpdates(result);
done(QString());
}).fail([=](const MTP::Error &error) {
done(error.type());
}).send();
} else {
_session->api().request(MTPmessages_EditFactCheck(
item->history()->peer->input(),
MTP_int(item->id.bare),
MTP_textWithEntities(
MTP_string(text.text),
Api::EntitiesToMTP(
_session,
text.entities,
Api::ConvertOption::SkipLocal))
)).done([=](const MTPUpdates &result) {
_session->api().applyUpdates(result);
done(QString());
}).fail([=](const MTP::Error &error) {
done(error.type());
}).send();
}
}
void Factchecks::save(
FullMsgId itemId,
const TextWithEntities &was,
TextWithEntities text,
std::shared_ptr<Ui::Show> show) {
const auto wasEmpty = was.empty();
const auto textEmpty = text.empty();
save(itemId, std::move(text), [=](QString error) {
show->showToast(!error.isEmpty()
? error
: textEmpty
? tr::lng_factcheck_remove_done(tr::now)
: wasEmpty
? tr::lng_factcheck_add_done(tr::now)
: tr::lng_factcheck_edit_done(tr::now));
});
}
} // namespace Data

View File

@@ -0,0 +1,70 @@
/*
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"
class HistoryItem;
struct HistoryMessageFactcheck;
namespace HistoryView {
class Message;
class WebPage;
} // namespace HistoryView
namespace Main {
class Session;
} // namespace Main
namespace Ui {
class Show;
} // namespace Ui
namespace Data {
class Factchecks final {
public:
explicit Factchecks(not_null<Main::Session*> session);
void requestFor(not_null<HistoryItem*> item);
[[nodiscard]] std::unique_ptr<HistoryView::WebPage> makeMedia(
not_null<HistoryView::Message*> view,
not_null<HistoryMessageFactcheck*> factcheck);
[[nodiscard]] bool canEdit(not_null<HistoryItem*> item) const;
[[nodiscard]] int lengthLimit() const;
void save(
FullMsgId itemId,
TextWithEntities text,
Fn<void(QString)> done);
void save(
FullMsgId itemId,
const TextWithEntities &was,
TextWithEntities text,
std::shared_ptr<Ui::Show> show);
private:
[[nodiscard]] bool canEdit() const;
void subscribeIfNotYet();
void request();
const not_null<Main::Session*> _session;
base::Timer _requestTimer;
base::flat_set<not_null<HistoryItem*>> _pending;
std::vector<HistoryItem*> _requested;
mtpRequestId _requestId = 0;
bool _subscribed = false;
rpl::lifetime _lifetime;
};
} // namespace Data

View File

@@ -0,0 +1,481 @@
/*
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 "data/components/gift_auctions.h"
#include "api/api_hash.h"
#include "api/api_premium.h"
#include "api/api_text_entities.h"
#include "apiwrap.h"
#include "data/data_session.h"
#include "main/main_session.h"
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::on_next([=] {
requestActive();
}, _lifetime);
});
}
GiftAuctions::~GiftAuctions() = default;
rpl::producer<GiftAuctionState> GiftAuctions::state(const QString &slug) {
return [=](auto consumer) {
auto lifetime = rpl::lifetime();
auto &entry = _map[slug];
if (!entry) {
entry = std::make_unique<Entry>();
}
const auto raw = entry.get();
raw->changes.events() | rpl::on_next([=] {
consumer.put_next_copy(raw->state);
}, lifetime);
const auto now = crl::now();
if (raw->state.subscribedTill < 0
|| raw->state.subscribedTill >= now) {
consumer.put_next_copy(raw->state);
} else if (raw->state.subscribedTill >= 0) {
request(slug);
}
return lifetime;
};
}
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();
}
}
void GiftAuctions::requestAcquired(
uint64 giftId,
Fn<void(std::vector<Data::GiftAcquired>)> done) {
Expects(done != nullptr);
_session->api().request(MTPpayments_GetStarGiftAuctionAcquiredGifts(
MTP_long(giftId)
)).done([=](const MTPpayments_StarGiftAuctionAcquiredGifts &result) {
const auto &data = result.data();
const auto owner = &_session->data();
owner->processUsers(data.vusers());
owner->processChats(data.vchats());
const auto &list = data.vgifts().v;
auto gifts = std::vector<Data::GiftAcquired>();
gifts.reserve(list.size());
for (const auto &gift : list) {
const auto &data = gift.data();
gifts.push_back({
.to = owner->peer(peerFromMTP(data.vpeer())),
.message = (data.vmessage()
? Api::ParseTextWithEntities(_session, *data.vmessage())
: TextWithEntities()),
.date = data.vdate().v,
.bidAmount = int64(data.vbid_amount().v),
.round = data.vround().v,
.number = data.vgift_num().value_or_empty(),
.position = data.vpos().v,
.nameHidden = data.is_name_hidden(),
});
}
if (const auto entry = find(giftId)) {
const auto count = int(gifts.size());
if (entry->state.my.gotCount != count) {
entry->state.my.gotCount = count;
entry->changes.fire({});
}
}
done(std::move(gifts));
}).fail([=] {
done({});
}).send();
}
std::optional<Data::UniqueGiftAttributes> GiftAuctions::attributes(
uint64 giftId) const {
const auto i = _attributes.find(giftId);
return (i != end(_attributes) && i->second.waiters.empty())
? i->second.lists
: std::optional<Data::UniqueGiftAttributes>();
}
void GiftAuctions::requestAttributes(uint64 giftId, Fn<void()> ready) {
auto &entry = _attributes[giftId];
entry.waiters.push_back(std::move(ready));
if (entry.waiters.size() > 1) {
return;
}
_session->api().request(MTPpayments_GetStarGiftUpgradeAttributes(
MTP_long(giftId)
)).done([=](const MTPpayments_StarGiftUpgradeAttributes &result) {
const auto &attributes = result.data().vattributes().v;
auto &entry = _attributes[giftId];
auto &info = entry.lists;
info.models.reserve(attributes.size());
info.patterns.reserve(attributes.size());
info.backdrops.reserve(attributes.size());
for (const auto &attribute : attributes) {
attribute.match([&](const MTPDstarGiftAttributeModel &data) {
info.models.push_back(Api::FromTL(_session, data));
}, [&](const MTPDstarGiftAttributePattern &data) {
info.patterns.push_back(Api::FromTL(_session, data));
}, [&](const MTPDstarGiftAttributeBackdrop &data) {
info.backdrops.push_back(Api::FromTL(data));
}, [](const MTPDstarGiftAttributeOriginalDetails &data) {
});
}
for (const auto &ready : base::take(entry.waiters)) {
ready();
}
}).fail([=] {
for (const auto &ready : base::take(_attributes[giftId].waiters)) {
ready();
}
}).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();
for (const auto &[slug, entry] : _map) {
const auto raw = entry.get();
const auto till = raw->state.subscribedTill;
if (till <= 0 || !raw->changes.has_consumers()) {
continue;
} else if (till <= now) {
request(slug);
} else {
const auto timeout = till - now;
if (!next || timeout < next) {
next = timeout;
}
}
}
if (next) {
_timer.callOnce(next);
}
}
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());
owner->processChats(data.vchats());
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);
const auto raw = entry.get();
if (raw->requested) {
return;
}
raw->requested = true;
_session->api().request(MTPpayments_GetStarGiftAuctionState(
MTP_inputStarGiftAuctionSlug(MTP_string(slug)),
MTP_int(raw->state.version)
)).done([=](const MTPpayments_StarGiftAuctionState &result) {
raw->requested = false;
const auto &data = result.data();
_session->data().processUsers(data.vusers());
_session->data().processChats(data.vchats());
raw->state.gift = Api::FromTL(_session, data.vgift());
if (!raw->state.gift) {
return;
}
const auto timeout = data.vtimeout().v;
const auto ms = timeout * crl::time(1000);
raw->state.subscribedTill = ms ? (crl::now() + ms) : -1;
const auto was = myStateKey(raw->state);
apply(raw, data.vstate());
apply(raw, data.vuser_state());
if (raw->changes.has_consumers()) {
raw->changes.fire({});
if (ms && (!_timer.isActive() || _timer.remainingTime() > ms)) {
_timer.callOnce(ms);
}
}
if (was != myStateKey(raw->state)) {
_activeChanged.fire({});
}
}).send();
}
GiftAuctions::Entry *GiftAuctions::find(uint64 giftId) const {
for (const auto &[slug, entry] : _map) {
if (entry->state.gift && entry->state.gift->id == giftId) {
return entry.get();
}
}
return nullptr;
}
void GiftAuctions::apply(
not_null<Entry*> entry,
const MTPStarGiftAuctionState &state) {
apply(&entry->state, state);
}
void GiftAuctions::apply(
not_null<GiftAuctionState*> entry,
const MTPStarGiftAuctionState &state) {
Expects(entry->gift.has_value());
state.match([&](const MTPDstarGiftAuctionState &data) {
const auto version = data.vversion().v;
if (entry->version >= version) {
return;
}
const auto owner = &_session->data();
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;
entry->bidLevels.clear();
entry->bidLevels.reserve(levels.size());
for (const auto &level : levels) {
auto &bid = entry->bidLevels.emplace_back();
const auto &data = level.data();
bid.amount = data.vamount().v;
bid.position = data.vpos().v;
bid.date = data.vdate().v;
}
const auto &top = data.vtop_bidders().v;
entry->topBidders.clear();
entry->topBidders.reserve(top.size());
for (const auto &user : top) {
entry->topBidders.push_back(owner->user(UserId(user.v)));
}
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;
const auto &rounds = data.vrounds().v;
entry->roundParameters.clear();
entry->roundParameters.reserve(rounds.size());
for (const auto &round : rounds) {
round.match([&](const MTPDstarGiftAuctionRound &data) {
entry->roundParameters.push_back({
.number = data.vnum().v,
.duration = data.vduration().v,
});
}, [&](const MTPDstarGiftAuctionRoundExtendable &data) {
entry->roundParameters.push_back({
.number = data.vnum().v,
.duration = data.vduration().v,
.extendTop = data.vextend_top().v,
.extendDuration = data.vextend_window().v,
});
});
}
entry->averagePrice = 0;
}, [&](const MTPDstarGiftAuctionStateFinished &data) {
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) {
});
}
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();
entry->to = data.vbid_peer()
? _session->data().peer(peerFromMTP(*data.vbid_peer())).get()
: nullptr;
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

@@ -0,0 +1,157 @@
/*
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 "data/data_star_gift.h"
namespace Main {
class Session;
} // namespace Main
namespace Data {
struct GiftAuctionBidLevel {
int64 amount = 0;
int position = 0;
TimeId date = 0;
};
struct StarGiftAuctionMyState {
PeerData *to = nullptr;
int64 minBidAmount = 0;
int64 bid = 0;
TimeId date = 0;
int gotCount = 0;
bool returned = false;
};
struct GiftAuctionRound {
int number = 0;
TimeId duration = 0;
int extendTop = 0;
TimeId extendDuration = 0;
};
struct GiftAuctionState {
std::optional<StarGift> gift;
StarGiftAuctionMyState my;
std::vector<GiftAuctionBidLevel> bidLevels;
std::vector<not_null<UserData*>> topBidders;
std::vector<GiftAuctionRound> roundParameters;
crl::time subscribedTill = 0;
int64 minBidAmount = 0;
int64 averagePrice = 0;
TimeId startDate = 0;
TimeId endDate = 0;
TimeId nextRoundAt = 0;
int currentRound = 0;
int totalRounds = 0;
int giftsLeft = 0;
int version = 0;
[[nodiscard]] bool finished() const {
return (averagePrice != 0);
}
};
struct GiftAcquired {
not_null<PeerData*> to;
TextWithEntities message;
TimeId date = 0;
int64 bidAmount = 0;
int round = 0;
int number = 0;
int position = 0;
bool nameHidden = false;
};
struct ActiveAuctions {
std::vector<not_null<GiftAuctionState*>> list;
};
class GiftAuctions final {
public:
explicit GiftAuctions(not_null<Main::Session*> session);
~GiftAuctions();
[[nodiscard]] rpl::producer<GiftAuctionState> state(const QString &slug);
void apply(const MTPDupdateStarGiftAuctionState &data);
void apply(const MTPDupdateStarGiftAuctionUserState &data);
void requestAcquired(
uint64 giftId,
Fn<void(std::vector<Data::GiftAcquired>)> done);
[[nodiscard]] std::optional<Data::UniqueGiftAttributes> attributes(
uint64 giftId) const;
void requestAttributes(uint64 giftId, Fn<void()> ready);
[[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;
};
struct Attributes {
Data::UniqueGiftAttributes lists;
std::vector<Fn<void()>> waiters;
};
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;
base::flat_map<uint64, Attributes> _attributes;
rpl::event_stream<> _activeChanged;
mtpRequestId _activeRequestId = 0;
rpl::lifetime _lifetime;
};
[[nodiscard]] int MyAuctionPosition(const GiftAuctionState &state);
} // namespace Data

View File

@@ -0,0 +1,44 @@
/*
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 "data/components/location_pickers.h"
#include "api/api_common.h"
#include "ui/controls/location_picker.h"
namespace Data {
struct LocationPickers::Entry {
Api::SendAction action;
base::weak_ptr<Ui::LocationPicker> picker;
};
LocationPickers::LocationPickers() = default;
LocationPickers::~LocationPickers() = default;
Ui::LocationPicker *LocationPickers::lookup(const Api::SendAction &action) {
for (auto i = begin(_pickers); i != end(_pickers);) {
if (const auto strong = i->picker.get()) {
if (i->action == action) {
return strong;
}
++i;
} else {
i = _pickers.erase(i);
}
}
return nullptr;
}
void LocationPickers::emplace(
const Api::SendAction &action,
not_null<Ui::LocationPicker*> picker) {
_pickers.push_back({ action, picker });
}
} // namespace Data

View File

@@ -0,0 +1,39 @@
/*
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/weak_ptr.h"
namespace Api {
struct SendAction;
} // namespace Api
namespace Ui {
class LocationPicker;
} // namespace Ui
namespace Data {
class LocationPickers final {
public:
LocationPickers();
~LocationPickers();
Ui::LocationPicker *lookup(const Api::SendAction &action);
void emplace(
const Api::SendAction &action,
not_null<Ui::LocationPicker*> picker);
private:
struct Entry;
std::vector<Entry> _pickers;
};
} // namespace Data

View File

@@ -0,0 +1,186 @@
/*
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 "data/components/passkeys.h"
#include "apiwrap.h"
#include "data/data_passkey_deserialize.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "platform/platform_webauthn.h"
namespace Data {
namespace {
constexpr auto kTimeoutMs = 5000;
[[nodiscard]] PasskeyEntry FromTL(const MTPDpasskey &data) {
return PasskeyEntry{
.id = qs(data.vid()),
.name = qs(data.vname()),
.date = data.vdate().v,
.softwareEmojiId = data.vsoftware_emoji_id().value_or(0),
.lastUsageDate = data.vlast_usage_date().value_or(0),
};
}
} // namespace
Passkeys::Passkeys(not_null<Main::Session*> session)
: _session(session) {
}
Passkeys::~Passkeys() = default;
void Passkeys::initRegistration(
Fn<void(const Data::Passkey::RegisterData&)> done) {
_session->api().request(MTPaccount_InitPasskeyRegistration(
)).done([=](const MTPaccount_PasskeyRegistrationOptions &result) {
const auto &data = result.data();
const auto jsonData = data.voptions().data().vdata().v;
if (const auto p = Data::Passkey::DeserializeRegisterData(jsonData)) {
done(*p);
}
}).send();
}
void Passkeys::registerPasskey(
const Platform::WebAuthn::RegisterResult &result,
Fn<void()> done) {
const auto credentialIdBase64 = QString::fromUtf8(
result.credentialId.toBase64(QByteArray::Base64UrlEncoding));
_session->api().request(MTPaccount_RegisterPasskey(
MTP_inputPasskeyCredentialPublicKey(
MTP_string(credentialIdBase64),
MTP_string(credentialIdBase64),
MTP_inputPasskeyResponseRegister(
MTP_dataJSON(MTP_bytes(result.clientDataJSON)),
MTP_bytes(result.attestationObject)))
)).done([=](const MTPPasskey &result) {
_passkeys.emplace_back(FromTL(result.data()));
_listUpdated.fire({});
done();
}).send();
}
void Passkeys::deletePasskey(
const QString &id,
Fn<void()> done,
Fn<void(QString)> fail) {
_session->api().request(MTPaccount_DeletePasskey(
MTP_string(id)
)).done([=] {
_lastRequestTime = 0;
_listKnown = false;
loadList();
done();
}).fail([=](const MTP::Error &error) {
fail(error.type());
}).send();
}
rpl::producer<> Passkeys::requestList() {
if (crl::now() - _lastRequestTime > kTimeoutMs) {
if (!_listRequestId) {
loadList();
}
return _listUpdated.events();
} else {
return _listUpdated.events_starting_with(rpl::empty_value());
}
}
const std::vector<PasskeyEntry> &Passkeys::list() const {
return _passkeys;
}
bool Passkeys::listKnown() const {
return _listKnown;
}
void Passkeys::loadList() {
_lastRequestTime = crl::now();
_listRequestId = _session->api().request(MTPaccount_GetPasskeys(
)).done([=](const MTPaccount_Passkeys &result) {
_listRequestId = 0;
_listKnown = true;
const auto &data = result.data();
_passkeys.clear();
_passkeys.reserve(data.vpasskeys().v.size());
for (const auto &passkey : data.vpasskeys().v) {
_passkeys.emplace_back(FromTL(passkey.data()));
}
_listUpdated.fire({});
}).fail([=] {
_listRequestId = 0;
}).send();
}
bool Passkeys::canRegister() const {
const auto max = _session->appConfig().passkeysAccountPasskeysMax();
return Platform::WebAuthn::IsSupported() && _passkeys.size() < max;
}
bool Passkeys::possible() const {
return _session->appConfig().settingsDisplayPasskeys();
}
void InitPasskeyLogin(
MTP::Sender &api,
Fn<void(const Data::Passkey::LoginData&)> done) {
api.request(MTPauth_InitPasskeyLogin(
MTP_int(ApiId),
MTP_string(ApiHash)
)).done([=](const MTPauth_PasskeyLoginOptions &result) {
const auto &data = result.data();
if (const auto p = Passkey::DeserializeLoginData(
data.voptions().data().vdata().v)) {
done(*p);
}
}).send();
}
void FinishPasskeyLogin(
MTP::Sender &api,
int initialDc,
const Platform::WebAuthn::LoginResult &result,
Fn<void(const MTPauth_Authorization&)> done,
Fn<void(QString)> fail) {
const auto userHandleStr = QString::fromUtf8(result.userHandle);
const auto parts = userHandleStr.split(':');
if (parts.size() != 2) {
return;
}
const auto userDc = parts[0].toInt();
const auto credentialIdBase64 = result.credentialId.toBase64(
QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
const auto credential = MTP_inputPasskeyCredentialPublicKey(
MTP_string(credentialIdBase64.toStdString()),
MTP_string(credentialIdBase64.toStdString()),
MTP_inputPasskeyResponseLogin(
MTP_dataJSON(MTP_bytes(result.clientDataJSON)),
MTP_bytes(result.authenticatorData),
MTP_bytes(result.signature),
MTP_string(userHandleStr.toStdString())
)
);
const auto flags = (userDc != initialDc)
? MTPauth_finishPasskeyLogin::Flag::f_from_dc_id
: MTPauth_finishPasskeyLogin::Flags(0);
api.request(MTPauth_FinishPasskeyLogin(
MTP_flags(flags),
credential,
MTP_int(initialDc),
MTP_long(0)
)).toDC(
userDc
).done(done).fail([=](const MTP::Error &error) {
fail(error.type());
}).send();
}
} // namespace Data

View File

@@ -0,0 +1,79 @@
/*
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
namespace Data::Passkey {
struct RegisterData;
struct LoginData;
} // namespace Data::Passkey
namespace Platform::WebAuthn {
struct RegisterResult;
struct LoginResult;
} // namespace Platform::WebAuthn
namespace Main {
class Session;
} // namespace Main
namespace MTP {
class Sender;
} // namespace MTP
namespace Data {
struct PasskeyEntry {
QString id;
QString name;
TimeId date = 0;
DocumentId softwareEmojiId = 0;
TimeId lastUsageDate = 0;
};
class Passkeys final {
public:
explicit Passkeys(not_null<Main::Session*> session);
~Passkeys();
void initRegistration(Fn<void(const Data::Passkey::RegisterData&)> done);
void registerPasskey(
const Platform::WebAuthn::RegisterResult &result,
Fn<void()> done);
void deletePasskey(
const QString &id,
Fn<void()> done,
Fn<void(QString)> fail);
[[nodiscard]] rpl::producer<> requestList();
[[nodiscard]] const std::vector<PasskeyEntry> &list() const;
[[nodiscard]] bool listKnown() const;
[[nodiscard]] bool canRegister() const;
[[nodiscard]] bool possible() const;
private:
void loadList();
const not_null<Main::Session*> _session;
std::vector<PasskeyEntry> _passkeys;
rpl::event_stream<> _listUpdated;
crl::time _lastRequestTime = 0;
mtpRequestId _listRequestId = 0;
bool _listKnown = false;
};
void InitPasskeyLogin(
MTP::Sender &api,
Fn<void(const Data::Passkey::LoginData&)> done);
void FinishPasskeyLogin(
MTP::Sender &api,
int initialDc,
const Platform::WebAuthn::LoginResult &result,
Fn<void(const MTPauth_Authorization&)> done,
Fn<void(QString)> fail);
} // namespace Data

View File

@@ -0,0 +1,364 @@
/*
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 "data/components/promo_suggestions.h"
#include "api/api_text_entities.h"
#include "apiwrap.h"
#include "base/unixtime.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "data/data_changes.h"
#include "data/data_histories.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "history/history.h"
#include "main/main_session.h"
#include "main/main_session_settings.h"
namespace Data {
namespace {
using UserIds = std::vector<UserId>;
constexpr auto kTopPromotionInterval = TimeId(60 * 60);
constexpr auto kTopPromotionMinDelay = TimeId(10);
[[nodiscard]] CustomSuggestion CustomFromTL(
not_null<Main::Session*> session,
const MTPPendingSuggestion &r) {
return CustomSuggestion({
.suggestion = qs(r.data().vsuggestion()),
.title = Api::ParseTextWithEntities(session, r.data().vtitle()),
.description = Api::ParseTextWithEntities(
session,
r.data().vdescription()),
.url = qs(r.data().vurl()),
});
}
} // namespace
PromoSuggestions::PromoSuggestions(
not_null<Main::Session*> session,
Fn<void()> firstPromoLoaded)
: _session(session)
, _topPromotionTimer([=] { refreshTopPromotion(); })
, _firstPromoLoaded(std::move(firstPromoLoaded)) {
Core::App().settings().proxy().connectionTypeValue(
) | rpl::on_next([=] {
refreshTopPromotion();
}, _lifetime);
}
PromoSuggestions::~PromoSuggestions() = default;
void PromoSuggestions::refreshTopPromotion() {
const auto now = base::unixtime::now();
const auto next = (_topPromotionNextRequestTime != 0)
? _topPromotionNextRequestTime
: now;
if (_topPromotionRequestId) {
topPromotionDelayed(now, next);
return;
}
const auto key = [&]() -> std::pair<QString, uint32> {
if (!Core::App().settings().proxy().isEnabled()) {
return {};
}
const auto &proxy = Core::App().settings().proxy().selected();
if (proxy.type != MTP::ProxyData::Type::Mtproto) {
return {};
}
return { proxy.host, proxy.port };
}();
if (_topPromotionKey == key && now < next) {
topPromotionDelayed(now, next);
return;
}
_topPromotionKey = key;
_topPromotionRequestId = _session->api().request(MTPhelp_GetPromoData(
)).done([=](const MTPhelp_PromoData &result) {
_topPromotionRequestId = 0;
_topPromotionNextRequestTime = result.match([&](const auto &data) {
return data.vexpires().v;
});
topPromotionDelayed(
base::unixtime::now(),
_topPromotionNextRequestTime);
result.match([&](const MTPDhelp_promoDataEmpty &data) {
setTopPromoted(nullptr, QString(), QString());
}, [&](const MTPDhelp_promoData &data) {
_session->data().processChats(data.vchats());
_session->data().processUsers(data.vusers());
auto changedPendingSuggestions = false;
auto pendingSuggestions = ranges::views::all(
data.vpending_suggestions().v
) | ranges::views::transform([](const auto &suggestion) {
return qs(suggestion);
}) | ranges::to_vector;
for (const auto &suggestion : pendingSuggestions) {
if (suggestion == u"SETUP_LOGIN_EMAIL_NOSKIP"_q) {
_setupEmailState = SetupEmailState::SetupNoSkip;
break;
}
if (suggestion == u"SETUP_LOGIN_EMAIL"_q) {
_setupEmailState = SetupEmailState::Setup;
break;
}
}
if (!ranges::equal(_pendingSuggestions, pendingSuggestions)) {
_pendingSuggestions = std::move(pendingSuggestions);
changedPendingSuggestions = true;
}
auto changedDismissedSuggestions = false;
for (const auto &suggestion : data.vdismissed_suggestions().v) {
changedDismissedSuggestions
|= _dismissedSuggestions.emplace(qs(suggestion)).second;
}
if (const auto peer = data.vpeer()) {
const auto peerId = peerFromMTP(*peer);
const auto history = _session->data().history(peerId);
setTopPromoted(
history,
data.vpsa_type().value_or_empty(),
data.vpsa_message().value_or_empty());
} else {
setTopPromoted(nullptr, QString(), QString());
}
auto changedCustom = false;
auto custom = data.vcustom_pending_suggestion()
? std::make_optional(
CustomFromTL(
_session,
*data.vcustom_pending_suggestion()))
: std::nullopt;
if (_custom != custom) {
_custom = std::move(custom);
changedCustom = true;
}
const auto changedContactBirthdaysLastDayRequest =
_contactBirthdaysLastDayRequest != -1
&& _contactBirthdaysLastDayRequest
!= QDate::currentDate().day();
if (changedPendingSuggestions
|| changedDismissedSuggestions
|| changedCustom
|| changedContactBirthdaysLastDayRequest) {
_refreshed.fire({});
}
});
if (_firstPromoLoaded) {
base::take(_firstPromoLoaded)();
}
}).fail([=] {
_topPromotionRequestId = 0;
const auto now = base::unixtime::now();
const auto next = _topPromotionNextRequestTime = now
+ kTopPromotionInterval;
if (!_topPromotionTimer.isActive()) {
topPromotionDelayed(now, next);
}
}).send();
}
void PromoSuggestions::topPromotionDelayed(TimeId now, TimeId next) {
_topPromotionTimer.callOnce(std::min(
std::max(next - now, kTopPromotionMinDelay),
kTopPromotionInterval) * crl::time(1000));
};
rpl::producer<> PromoSuggestions::value() const {
return _refreshed.events_starting_with({});
}
void PromoSuggestions::setTopPromoted(
History *promoted,
const QString &type,
const QString &message) {
const auto changed = (_topPromoted != promoted);
if (!changed
&& (!promoted || promoted->topPromotionMessage() == message)) {
return;
}
if (changed) {
if (_topPromoted) {
_topPromoted->cacheTopPromotion(false, QString(), QString());
}
}
const auto old = std::exchange(_topPromoted, promoted);
if (_topPromoted) {
_session->data().histories().requestDialogEntry(_topPromoted);
_topPromoted->cacheTopPromotion(true, type, message);
_topPromoted->requestChatListMessage();
_session->changes().historyUpdated(
_topPromoted,
HistoryUpdate::Flag::TopPromoted);
}
if (changed && old) {
_session->changes().historyUpdated(
old,
HistoryUpdate::Flag::TopPromoted);
}
}
bool PromoSuggestions::current(const QString &key) const {
if (key == u"BIRTHDAY_CONTACTS_TODAY"_q) {
if (_dismissedSuggestions.contains(key)) {
return false;
} else {
const auto known
= PromoSuggestions::knownBirthdaysToday();
if (!known) {
return true;
}
return !known->empty();
}
}
return !_dismissedSuggestions.contains(key)
&& ranges::contains(_pendingSuggestions, key);
}
rpl::producer<> PromoSuggestions::requested(const QString &key) const {
return value() | rpl::filter([=] { return current(key); });
}
void PromoSuggestions::dismiss(const QString &key) {
if (!_dismissedSuggestions.emplace(key).second) {
return;
}
_session->api().request(MTPhelp_DismissSuggestion(
MTP_inputPeerEmpty(),
MTP_string(key)
)).send();
}
void PromoSuggestions::dismissSetupEmail(Fn<void()> done) {
auto key = QString();
if (_setupEmailState == SetupEmailState::SettingUp) {
key = u"SETUP_LOGIN_EMAIL"_q;
} else if (_setupEmailState == SetupEmailState::SettingUpNoSkip) {
key = u"SETUP_LOGIN_EMAIL_NOSKIP"_q;
} else {
return;
}
_session->api().request(MTPhelp_DismissSuggestion(
MTP_inputPeerEmpty(),
MTP_string(key)
)).done([=](const MTPBool &) {
_setupEmailState = SetupEmailState::None;
done();
}).send();
}
void PromoSuggestions::invalidate() {
if (_topPromotionRequestId) {
_session->api().request(_topPromotionRequestId).cancel();
}
_topPromotionNextRequestTime = 0;
_topPromotionTimer.callOnce(crl::time(200));
}
std::optional<CustomSuggestion> PromoSuggestions::custom() const {
return (_custom && !_dismissedSuggestions.contains(_custom->suggestion))
? _custom
: std::nullopt;
}
void PromoSuggestions::requestContactBirthdays(Fn<void()> done, bool force) {
if ((_contactBirthdaysLastDayRequest != -1)
&& (_contactBirthdaysLastDayRequest == QDate::currentDate().day())
&& !force) {
return done();
}
if (_contactBirthdaysRequestId) {
_session->api().request(_contactBirthdaysRequestId).cancel();
}
_contactBirthdaysRequestId = _session->api().request(
MTPcontacts_GetBirthdays()
).done([=](const MTPcontacts_ContactBirthdays &result) {
_contactBirthdaysRequestId = 0;
_contactBirthdaysLastDayRequest = QDate::currentDate().day();
auto users = UserIds();
auto today = UserIds();
_session->data().processUsers(result.data().vusers());
for (const auto &tlContact : result.data().vcontacts().v) {
const auto peerId = tlContact.data().vcontact_id().v;
if (const auto user = _session->data().user(peerId)) {
const auto &data = tlContact.data().vbirthday().data();
user->setBirthday(Data::Birthday(
data.vday().v,
data.vmonth().v,
data.vyear().value_or_empty()));
if (user->isSelf()
|| user->isInaccessible()
|| user->isBlocked()) {
continue;
}
if (Data::IsBirthdayToday(user->birthday())) {
today.push_back(peerToUser(user->id));
}
users.push_back(peerToUser(user->id));
}
}
_contactBirthdays = std::move(users);
_contactBirthdaysToday = std::move(today);
done();
}).fail([=](const MTP::Error &error) {
_contactBirthdaysRequestId = 0;
_contactBirthdaysLastDayRequest = QDate::currentDate().day();
_contactBirthdays = {};
_contactBirthdaysToday = {};
done();
}).send();
}
std::optional<UserIds> PromoSuggestions::knownContactBirthdays() const {
if ((_contactBirthdaysLastDayRequest == -1)
|| (_contactBirthdaysLastDayRequest != QDate::currentDate().day())) {
return std::nullopt;
}
return _contactBirthdays;
}
std::optional<UserIds> PromoSuggestions::knownBirthdaysToday() const {
if ((_contactBirthdaysLastDayRequest == -1)
|| (_contactBirthdaysLastDayRequest != QDate::currentDate().day())) {
return std::nullopt;
}
return _contactBirthdaysToday;
}
QString PromoSuggestions::SugValidatePassword() {
static const auto key = u"VALIDATE_PASSWORD"_q;
return key;
}
void PromoSuggestions::setSetupEmailState(SetupEmailState state) {
if (_setupEmailState != state) {
_setupEmailState = state;
_setupEmailStateChanges.fire_copy(state);
}
}
SetupEmailState PromoSuggestions::setupEmailState() const {
return _setupEmailState;
}
rpl::producer<SetupEmailState> PromoSuggestions::setupEmailStateValue() const {
return _setupEmailStateChanges.events_starting_with_copy(_setupEmailState);
}
} // namespace Data

View File

@@ -0,0 +1,107 @@
/*
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"
class History;
namespace Main {
class Session;
} // namespace Main
namespace Data {
enum class SetupEmailState {
None,
Setup,
SetupNoSkip,
SettingUp,
SettingUpNoSkip,
};
struct CustomSuggestion final {
QString suggestion;
TextWithEntities title;
TextWithEntities description;
QString url;
friend inline auto operator<=>(
const CustomSuggestion &,
const CustomSuggestion &) = default;
};
class PromoSuggestions final {
public:
explicit PromoSuggestions(
not_null<Main::Session*> session,
Fn<void()> firstPromoLoaded = nullptr);
~PromoSuggestions();
[[nodiscard]] bool current(const QString &key) const;
[[nodiscard]] std::optional<CustomSuggestion> custom() const;
[[nodiscard]] rpl::producer<> requested(const QString &key) const;
void dismiss(const QString &key);
void dismissSetupEmail(Fn<void()> done);
void refreshTopPromotion();
void invalidate();
rpl::producer<> value() const;
// Create rpl::producer<> refreshed() const; on memand.
void requestContactBirthdays(Fn<void()> done, bool force = false);
[[nodiscard]] auto knownContactBirthdays() const
-> std::optional<std::vector<UserId>>;
[[nodiscard]] auto knownBirthdaysToday() const
-> std::optional<std::vector<UserId>>;
[[nodiscard]] static QString SugValidatePassword();
void setSetupEmailState(SetupEmailState state);
[[nodiscard]] SetupEmailState setupEmailState() const;
[[nodiscard]] rpl::producer<SetupEmailState> setupEmailStateValue() const;
private:
void setTopPromoted(
History *promoted,
const QString &type,
const QString &message);
void topPromotionDelayed(TimeId now, TimeId next);
const not_null<Main::Session*> _session;
base::flat_set<QString> _dismissedSuggestions;
std::vector<QString> _pendingSuggestions;
std::optional<CustomSuggestion> _custom;
History *_topPromoted = nullptr;
mtpRequestId _contactBirthdaysRequestId = 0;
int _contactBirthdaysLastDayRequest = -1;
std::vector<UserId> _contactBirthdays;
std::vector<UserId> _contactBirthdaysToday;
mtpRequestId _topPromotionRequestId = 0;
std::pair<QString, uint32> _topPromotionKey;
TimeId _topPromotionNextRequestTime = TimeId(0);
base::Timer _topPromotionTimer;
SetupEmailState _setupEmailState = SetupEmailState::None;
rpl::event_stream<> _refreshed;
rpl::event_stream<SetupEmailState> _setupEmailStateChanges;
Fn<void()> _firstPromoLoaded;
rpl::lifetime _lifetime;
};
} // namespace Data

View File

@@ -0,0 +1,166 @@
/*
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 "data/components/recent_peers.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "history/history.h"
#include "main/main_session.h"
#include "storage/serialize_common.h"
#include "storage/serialize_peer.h"
#include "storage/storage_account.h"
namespace Data {
namespace {
constexpr auto kLimit = 48;
constexpr auto kMaxRememberedOpenChats = 32;
} // namespace
RecentPeers::RecentPeers(not_null<Main::Session*> session)
: _session(session) {
}
RecentPeers::~RecentPeers() = default;
const std::vector<not_null<PeerData*>> &RecentPeers::list() const {
_session->local().readSearchSuggestions();
return _list;
}
rpl::producer<> RecentPeers::updates() const {
return _updates.events();
}
void RecentPeers::remove(not_null<PeerData*> peer) {
const auto i = ranges::find(_list, peer);
if (i != end(_list)) {
_list.erase(i);
_updates.fire({});
}
_session->local().writeSearchSuggestionsDelayed();
}
void RecentPeers::bump(not_null<PeerData*> peer) {
_session->local().readSearchSuggestions();
if (!_list.empty() && _list.front() == peer) {
return;
}
auto i = ranges::find(_list, peer);
if (i == end(_list)) {
_list.push_back(peer);
i = end(_list) - 1;
}
ranges::rotate(begin(_list), i, i + 1);
_updates.fire({});
_session->local().writeSearchSuggestionsDelayed();
}
void RecentPeers::clear() {
_session->local().readSearchSuggestions();
_list.clear();
_updates.fire({});
_session->local().writeSearchSuggestionsDelayed();
}
QByteArray RecentPeers::serialize() const {
_session->local().readSearchSuggestions();
if (_list.empty()) {
return {};
}
auto size = 2 * sizeof(quint32); // AppVersion, count
const auto count = std::min(int(_list.size()), kLimit);
auto &&list = _list | ranges::views::take(count);
for (const auto &peer : list) {
size += Serialize::peerSize(peer);
}
auto stream = Serialize::ByteArrayWriter(size);
stream
<< quint32(AppVersion)
<< quint32(count);
for (const auto &peer : list) {
Serialize::writePeer(stream, peer);
}
return std::move(stream).result();
}
void RecentPeers::applyLocal(QByteArray serialized) {
_list.clear();
if (serialized.isEmpty()) {
DEBUG_LOG(("Suggestions: Bad RecentPeers local, empty."));
return;
}
auto stream = Serialize::ByteArrayReader(serialized);
auto streamAppVersion = quint32();
auto count = quint32();
stream >> streamAppVersion >> count;
if (!stream.ok()) {
DEBUG_LOG(("Suggestions: Bad RecentPeers local, not ok."));
return;
}
DEBUG_LOG(("Suggestions: "
"Start RecentPeers read, count: %1, version: %2."
).arg(count
).arg(streamAppVersion));
_list.reserve(count);
for (auto i = 0; i != int(count); ++i) {
const auto streamPosition = stream.underlying().device()->pos();
const auto peer = Serialize::readPeer(
_session,
streamAppVersion,
stream);
if (stream.ok() && peer) {
_list.push_back(peer);
} else {
_list.clear();
DEBUG_LOG(("Suggestions: Failed RecentPeers reading %1 / %2."
).arg(i + 1
).arg(count));
DEBUG_LOG(("Failed bytes: %1.").arg(
QString::fromUtf8(serialized.mid(streamPosition).toHex())));
return;
}
}
DEBUG_LOG(
("Suggestions: RecentPeers read OK, count: %1").arg(_list.size()));
}
std::vector<not_null<Thread*>> RecentPeers::collectChatOpenHistory() const {
_session->local().readSearchSuggestions();
return _opens;
}
void RecentPeers::chatOpenPush(not_null<Thread*> thread) {
const auto i = ranges::find(_opens, thread);
if (i == end(_opens)) {
while (_opens.size() >= kMaxRememberedOpenChats) {
_opens.pop_back();
}
_opens.insert(begin(_opens), thread);
} else if (i != begin(_opens)) {
ranges::rotate(begin(_opens), i, i + 1);
}
}
void RecentPeers::chatOpenRemove(not_null<Thread*> thread) {
_opens.erase(ranges::remove(_opens, thread), end(_opens));
}
void RecentPeers::chatOpenKeepUserpics(
base::flat_map<not_null<PeerData*>, Ui::PeerUserpicView> userpics) {
_chatOpenUserpicsCache = std::move(userpics);
}
} // namespace Data

View File

@@ -0,0 +1,55 @@
/*
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 "ui/userpic_view.h"
namespace Main {
class Session;
} // namespace Main
namespace Data {
class Thread;
class RecentPeers final {
public:
explicit RecentPeers(not_null<Main::Session*> session);
~RecentPeers();
[[nodiscard]] const std::vector<not_null<PeerData*>> &list() const;
[[nodiscard]] rpl::producer<> updates() const;
void remove(not_null<PeerData*> peer);
void bump(not_null<PeerData*> peer);
void clear();
[[nodiscard]] QByteArray serialize() const;
void applyLocal(QByteArray serialized);
[[nodiscard]] auto collectChatOpenHistory() const
-> std::vector<not_null<Thread*>>;
void chatOpenPush(not_null<Thread*> thread);
void chatOpenRemove(not_null<Thread*> thread);
void chatOpenKeepUserpics(
base::flat_map<not_null<PeerData*>, Ui::PeerUserpicView> userpics);
private:
const not_null<Main::Session*> _session;
std::vector<not_null<PeerData*>> _list;
std::vector<not_null<Thread*>> _opens;
base::flat_map<
not_null<PeerData*>,
Ui::PeerUserpicView> _chatOpenUserpicsCache;
rpl::event_stream<> _updates;
};
} // namespace Data

View File

@@ -0,0 +1,268 @@
/*
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 "data/components/recent_shared_media_gifts.h"
#include "api/api_credits.h" // InputSavedStarGiftId
#include "api/api_premium.h"
#include "apiwrap.h"
#include "chat_helpers/compose/compose_show.h"
#include "data/data_document.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "lang/lang_keys.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
namespace Data {
namespace {
constexpr auto kReloadThreshold = 60 * crl::time(1000);
constexpr auto kMaxGifts = 3;
constexpr auto kMaxPinnedGifts = 6;
} // namespace
RecentSharedMediaGifts::RecentSharedMediaGifts(
not_null<Main::Session*> session)
: _session(session) {
}
RecentSharedMediaGifts::~RecentSharedMediaGifts() = default;
std::vector<Data::SavedStarGift> RecentSharedMediaGifts::filterGifts(
const std::deque<Data::SavedStarGift> &gifts,
bool onlyPinnedToTop) {
auto result = std::vector<Data::SavedStarGift>();
const auto maxCount = onlyPinnedToTop ? kMaxPinnedGifts : kMaxGifts;
for (const auto &gift : gifts) {
if (!onlyPinnedToTop || gift.pinned) {
result.push_back(gift);
if (result.size() >= maxCount) {
break;
}
}
}
return result;
}
void RecentSharedMediaGifts::request(
not_null<PeerData*> peer,
Fn<void(std::vector<SavedStarGift>)> done,
bool onlyPinnedToTop) {
const auto it = _recent.find(peer->id);
if (it != _recent.end()) {
auto &entry = it->second;
if (entry.lastRequestTime
&& entry.lastRequestTime + kReloadThreshold > crl::now()) {
done(filterGifts(entry.gifts, onlyPinnedToTop));
return;
}
if (entry.requestId) {
entry.pendingCallbacks.push_back([=] {
const auto it = _recent.find(peer->id);
if (it != _recent.end()) {
done(filterGifts(it->second.gifts, onlyPinnedToTop));
}
});
return;
}
}
_recent[peer->id].requestId = peer->session().api().request(
MTPpayments_GetSavedStarGifts(
MTP_flags(0),
peer->input(),
MTP_int(0), // collection_id
MTP_string(QString()),
MTP_int(kMaxPinnedGifts)
)).done([=](const MTPpayments_SavedStarGifts &result) {
const auto &data = result.data();
const auto owner = &peer->owner();
owner->processUsers(data.vusers());
owner->processChats(data.vchats());
auto &entry = _recent[peer->id];
entry.lastRequestTime = crl::now();
entry.requestId = 0;
entry.gifts.clear();
for (const auto &gift : data.vgifts().v) {
if (auto parsed = Api::FromTL(peer, gift)) {
entry.gifts.push_back(std::move(*parsed));
}
}
done(filterGifts(entry.gifts, onlyPinnedToTop));
for (const auto &callback : entry.pendingCallbacks) {
callback();
}
entry.pendingCallbacks.clear();
}).send();
}
void RecentSharedMediaGifts::clearLastRequestTime(
not_null<PeerData*> peer) {
const auto it = _recent.find(peer->id);
if (it != _recent.end()) {
it->second.lastRequestTime = 0;
}
}
void RecentSharedMediaGifts::updatePinnedOrder(
std::shared_ptr<ChatHelpers::Show> show,
not_null<PeerData*> peer,
const std::vector<SavedStarGift> &gifts,
const std::vector<Data::SavedStarGiftId> &manageIds,
Fn<void()> done) {
auto inputs = QVector<MTPInputSavedStarGift>();
inputs.reserve(manageIds.size());
for (const auto &id : manageIds) {
inputs.push_back(Api::InputSavedStarGiftId(id));
}
_session->api().request(MTPpayments_ToggleStarGiftsPinnedToTop(
peer->input(),
MTP_vector<MTPInputSavedStarGift>(std::move(inputs))
)).done([=] {
auto result = std::deque<SavedStarGift>();
for (const auto &id : manageIds) {
for (const auto &gift : gifts) {
if (gift.manageId == id) {
result.push_back(gift);
break;
}
}
}
_recent[peer->id].gifts = std::move(result);
if (done) {
done();
}
}).fail([=](const MTP::Error &error) {
show->showToast(error.type());
}).send();
}
void RecentSharedMediaGifts::togglePinned(
std::shared_ptr<ChatHelpers::Show> show,
not_null<PeerData*> peer,
const Data::SavedStarGiftId &manageId,
bool pinned,
std::shared_ptr<Data::UniqueGift> uniqueData,
std::shared_ptr<Data::UniqueGift> replacingData) {
const auto performToggle = [=](const std::vector<SavedStarGift> &gifts) {
const auto limit = _session->appConfig().pinnedGiftsLimit();
auto manageIds = std::vector<Data::SavedStarGiftId>();
if (pinned) {
for (const auto &gift : gifts) {
if (gift.pinned && gift.manageId != manageId) {
manageIds.push_back(gift.manageId);
if (manageIds.size() >= limit - 1) {
break;
}
}
}
manageIds.push_back(manageId);
} else {
for (const auto &gift : gifts) {
if (gift.pinned && gift.manageId != manageId) {
manageIds.push_back(gift.manageId);
}
}
}
const auto updateLocal = [=] {
using GiftAction = Data::GiftUpdate::Action;
_session->data().notifyGiftUpdate({
.id = manageId,
.action = (pinned ? GiftAction::Pin : GiftAction::Unpin),
});
if (pinned) {
show->showToast({
.title = (uniqueData
? tr::lng_gift_pinned_done_title(
tr::now,
lt_gift,
Data::UniqueGiftName(*uniqueData))
: QString()),
.text = (replacingData
? tr::lng_gift_pinned_done_replaced(
tr::now,
lt_gift,
TextWithEntities{
Data::UniqueGiftName(*replacingData),
},
tr::marked)
: tr::lng_gift_pinned_done(
tr::now,
tr::marked)),
.duration = Ui::Toast::kDefaultDuration * 2,
});
}
};
if (!pinned) {
updatePinnedOrder(show, peer, gifts, manageIds, updateLocal);
} else {
_session->api().request(MTPpayments_GetSavedStarGift(
MTP_vector<MTPInputSavedStarGift>(
1,
Api::InputSavedStarGiftId(manageId))
)).done([=](const MTPpayments_SavedStarGifts &result) {
const auto &tlGift = result.data().vgifts().v.front();
if (auto parsed = Api::FromTL(peer, tlGift)) {
auto updatedGifts = std::vector<SavedStarGift>();
for (const auto &gift : gifts) {
if (gift.pinned && gift.manageId != manageId) {
updatedGifts.push_back(gift);
}
}
parsed->pinned = true;
updatedGifts.push_back(*parsed);
updatePinnedOrder(
show,
peer,
updatedGifts,
manageIds,
updateLocal);
}
}).send();
}
};
request(peer, performToggle, true);
}
void RecentSharedMediaGifts::reorderPinned(
std::shared_ptr<ChatHelpers::Show> show,
not_null<PeerData*> peer,
int oldPosition,
int newPosition) {
const auto performReorder = [=](const std::vector<SavedStarGift> &gifts) {
if (oldPosition < 0 || oldPosition >= gifts.size()
|| newPosition < 0 || newPosition >= gifts.size()
|| oldPosition == newPosition) {
return;
}
auto manageIds = std::vector<Data::SavedStarGiftId>();
manageIds.reserve(gifts.size());
for (const auto &gift : gifts) {
manageIds.push_back(gift.manageId);
}
base::reorder(manageIds, oldPosition, newPosition);
updatePinnedOrder(show, peer, gifts, manageIds, nullptr);
};
request(peer, performReorder, true);
}
} // namespace Data

View File

@@ -0,0 +1,73 @@
/*
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 "data/data_star_gift.h"
namespace ChatHelpers {
class Show;
} // namespace ChatHelpers
namespace Main {
class Session;
} // namespace Main
namespace Data {
class RecentSharedMediaGifts final {
public:
explicit RecentSharedMediaGifts(not_null<Main::Session*> session);
~RecentSharedMediaGifts();
void request(
not_null<PeerData*> peer,
Fn<void(std::vector<Data::SavedStarGift>)> done,
bool onlyPinnedToTop = false);
void clearLastRequestTime(not_null<PeerData*> peer);
void togglePinned(
std::shared_ptr<ChatHelpers::Show> show,
not_null<PeerData*> peer,
const Data::SavedStarGiftId &manageId,
bool pinned,
std::shared_ptr<Data::UniqueGift> uniqueData,
std::shared_ptr<Data::UniqueGift> replacingData = nullptr);
void reorderPinned(
std::shared_ptr<ChatHelpers::Show> show,
not_null<PeerData*> peer,
int oldPosition,
int newPosition);
private:
void updatePinnedOrder(
std::shared_ptr<ChatHelpers::Show> show,
not_null<PeerData*> peer,
const std::vector<SavedStarGift> &gifts,
const std::vector<Data::SavedStarGiftId> &manageIds,
Fn<void()> done);
[[nodiscard]] std::vector<Data::SavedStarGift> filterGifts(
const std::deque<SavedStarGift> &gifts,
bool onlyPinnedToTop);
struct Entry {
std::deque<SavedStarGift> gifts;
crl::time lastRequestTime = 0;
mtpRequestId requestId = 0;
std::vector<Fn<void()>> pendingCallbacks;
};
const not_null<Main::Session*> _session;
base::flat_map<PeerId, Entry> _recent;
};
} // namespace Data

View File

@@ -0,0 +1,662 @@
/*
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 "data/components/scheduled_messages.h"
#include "base/unixtime.h"
#include "data/data_forum_topic.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "api/api_hash.h"
#include "api/api_text_entities.h"
#include "main/main_session.h"
#include "history/history.h"
#include "history/history_item_components.h"
#include "history/history_item_helpers.h"
#include "apiwrap.h"
namespace Data {
namespace {
constexpr auto kRequestTimeLimit = 60 * crl::time(1000);
[[nodiscard]] MsgId RemoteToLocalMsgId(MsgId id) {
Expects(IsServerMsgId(id));
return ServerMaxMsgId + id + 1;
}
[[nodiscard]] MsgId LocalToRemoteMsgId(MsgId id) {
Expects(IsScheduledMsgId(id));
return (id - ServerMaxMsgId - 1);
}
[[nodiscard]] bool TooEarlyForRequest(crl::time received) {
return (received > 0) && (received + kRequestTimeLimit > crl::now());
}
[[nodiscard]] bool HasScheduledDate(not_null<HistoryItem*> item) {
return (item->date() != Api::kScheduledUntilOnlineTimestamp)
&& (item->date() > base::unixtime::now());
}
[[nodiscard]] MTPMessage PrepareMessage(const MTPMessage &message) {
return message.match([&](const MTPDmessageEmpty &data) {
return MTP_messageEmpty(
data.vflags(),
data.vid(),
data.vpeer_id() ? *data.vpeer_id() : MTPPeer());
}, [&](const MTPDmessageService &data) {
return MTP_messageService(
MTP_flags(data.vflags().v
| MTPDmessageService::Flag(
MTPDmessage::Flag::f_from_scheduled)),
data.vid(),
data.vfrom_id() ? *data.vfrom_id() : MTPPeer(),
data.vpeer_id(),
data.vsaved_peer_id() ? *data.vsaved_peer_id() : MTPPeer(),
data.vreply_to() ? *data.vreply_to() : MTPMessageReplyHeader(),
data.vdate(),
data.vaction(),
data.vreactions() ? *data.vreactions() : MTPMessageReactions(),
MTP_int(data.vttl_period().value_or_empty()));
}, [&](const MTPDmessage &data) {
return MTP_message(
MTP_flags(data.vflags().v | MTPDmessage::Flag::f_from_scheduled),
data.vid(),
data.vfrom_id() ? *data.vfrom_id() : MTPPeer(),
MTPint(), // from_boosts_applied
data.vpeer_id(),
data.vsaved_peer_id() ? *data.vsaved_peer_id() : MTPPeer(),
data.vfwd_from() ? *data.vfwd_from() : MTPMessageFwdHeader(),
MTP_long(data.vvia_bot_id().value_or_empty()),
MTP_long(data.vvia_business_bot_id().value_or_empty()),
data.vreply_to() ? *data.vreply_to() : MTPMessageReplyHeader(),
data.vdate(),
data.vmessage(),
data.vmedia() ? *data.vmedia() : MTPMessageMedia(),
data.vreply_markup() ? *data.vreply_markup() : MTPReplyMarkup(),
(data.ventities()
? *data.ventities()
: MTPVector<MTPMessageEntity>()),
MTP_int(data.vviews().value_or_empty()),
MTP_int(data.vforwards().value_or_empty()),
data.vreplies() ? *data.vreplies() : MTPMessageReplies(),
MTP_int(data.vedit_date().value_or_empty()),
MTP_bytes(data.vpost_author().value_or_empty()),
MTP_long(data.vgrouped_id().value_or_empty()),
MTPMessageReactions(),
MTPVector<MTPRestrictionReason>(),
MTP_int(data.vttl_period().value_or_empty()),
MTPint(), // quick_reply_shortcut_id
MTP_long(data.veffect().value_or_empty()), // effect
data.vfactcheck() ? *data.vfactcheck() : MTPFactCheck(),
MTP_int(data.vreport_delivery_until_date().value_or_empty()),
MTP_long(data.vpaid_message_stars().value_or_empty()),
(data.vsuggested_post()
? *data.vsuggested_post()
: MTPSuggestedPost()),
MTP_int(data.vschedule_repeat_period().value_or_empty()));
});
}
} // namespace
bool IsScheduledMsgId(MsgId id) {
return (id > ServerMaxMsgId) && (id < ScheduledMaxMsgId);
}
ScheduledMessages::ScheduledMessages(not_null<Main::Session*> session)
: _session(session)
, _clearTimer([=] { clearOldRequests(); }) {
_session->data().itemRemoved(
) | rpl::filter([](not_null<const HistoryItem*> item) {
return item->isScheduled();
}) | rpl::on_next([=](not_null<const HistoryItem*> item) {
remove(item);
}, _lifetime);
}
ScheduledMessages::~ScheduledMessages() {
Expects(_data.empty());
Expects(_requests.empty());
}
void ScheduledMessages::clear() {
_lifetime.destroy();
for (const auto &request : base::take(_requests)) {
_session->api().request(request.second.requestId).cancel();
}
base::take(_data);
}
void ScheduledMessages::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;
}
_requests.erase(i);
}
}
MsgId ScheduledMessages::localMessageId(MsgId remoteId) const {
return RemoteToLocalMsgId(remoteId);
}
MsgId ScheduledMessages::lookupId(not_null<const HistoryItem*> item) const {
Expects(item->isScheduled());
Expects(!item->isSending());
Expects(!item->hasFailed());
return LocalToRemoteMsgId(item->id);
}
HistoryItem *ScheduledMessages::lookupItem(PeerId peer, MsgId msg) const {
const auto history = _session->data().historyLoaded(peer);
if (!history) {
return nullptr;
}
const auto i = _data.find(history);
if (i == end(_data)) {
return nullptr;
}
const auto &items = i->second.items;
const auto j = ranges::find_if(items, [&](auto &item) {
return item->id == msg;
});
if (j == end(items)) {
return nullptr;
}
return (*j).get();
}
HistoryItem *ScheduledMessages::lookupItem(FullMsgId itemId) const {
return lookupItem(itemId.peer, itemId.msg);
}
int ScheduledMessages::count(not_null<History*> history) const {
const auto i = _data.find(history);
return (i != end(_data)) ? i->second.items.size() : 0;
}
bool ScheduledMessages::hasFor(not_null<Data::ForumTopic*> topic) const {
const auto i = _data.find(topic->owningHistory());
if (i == end(_data)) {
return false;
}
return ranges::any_of(i->second.items, [&](const OwnedItem &item) {
return item->topic() == topic;
});
}
void ScheduledMessages::sendNowSimpleMessage(
const MTPDupdateShortSentMessage &update,
not_null<HistoryItem*> local) {
Expects(local->isSending());
Expects(local->isScheduled());
if (HasScheduledDate(local)) {
LOG(("Error: trying to put to history a new local message, "
"that has scheduled date."));
return;
}
// When the user sends a text message scheduled until online
// while the recipient is already online, the server sends
// updateShortSentMessage to the client and the client calls this method.
// Since such messages can only be sent to recipients,
// we know for sure that a message can't have fields such as the author,
// views count, etc.
const auto history = local->history();
auto action = Api::SendAction(history);
action.replyTo = local->replyTo();
const auto replyHeader = NewMessageReplyHeader(action);
const auto localFlags = NewMessageFlags(history->peer)
& ~MessageFlag::BeingSent;
const auto flags = MTPDmessage::Flag::f_entities
| MTPDmessage::Flag::f_from_id
| (action.replyTo
? MTPDmessage::Flag::f_reply_to
: MTPDmessage::Flag(0))
| (update.vttl_period()
? MTPDmessage::Flag::f_ttl_period
: MTPDmessage::Flag(0))
| ((localFlags & MessageFlag::Outgoing)
? MTPDmessage::Flag::f_out
: MTPDmessage::Flag(0))
| (local->effectId()
? MTPDmessage::Flag::f_effect
: MTPDmessage::Flag(0));
const auto views = 1;
const auto forwards = 0;
history->addNewMessage(
update.vid().v,
MTP_message(
MTP_flags(flags),
update.vid(),
peerToMTP(local->from()->id),
MTPint(), // from_boosts_applied
peerToMTP(history->peer->id),
MTPPeer(), // saved_peer_id
MTPMessageFwdHeader(),
MTPlong(), // via_bot_id
MTPlong(), // via_business_bot_id
replyHeader,
update.vdate(),
MTP_string(local->originalText().text),
MTP_messageMediaEmpty(),
MTPReplyMarkup(),
Api::EntitiesToMTP(
&history->session(),
local->originalText().entities),
MTP_int(views),
MTP_int(forwards),
MTPMessageReplies(),
MTPint(), // edit_date
MTP_string(),
MTPlong(),
MTPMessageReactions(),
MTPVector<MTPRestrictionReason>(),
MTP_int(update.vttl_period().value_or_empty()),
MTPint(), // quick_reply_shortcut_id
MTP_long(local->effectId()), // effect
MTPFactCheck(),
MTPint(), // report_delivery_until_date
MTPlong(), // paid_message_stars
MTPSuggestedPost(),
MTPint()), // schedule_repeat_period
localFlags,
NewMessageType::Unread);
local->destroy();
}
void ScheduledMessages::apply(const MTPDupdateNewScheduledMessage &update) {
const auto &message = update.vmessage();
const auto peer = PeerFromMessage(message);
if (!peer) {
return;
}
const auto history = _session->data().historyLoaded(peer);
if (!history) {
return;
}
auto &list = _data[history];
append(history, list, message);
sort(list);
_updates.fire_copy(history);
}
void ScheduledMessages::checkEntitiesAndUpdate(const MTPDmessage &data) {
// When the user sends a message with a media scheduled until online
// while the recipient is already online, or scheduled message
// is already due and is sent immediately, the server sends
// updateNewMessage or updateNewChannelMessage to the client
// and the client calls this method.
const auto peer = peerFromMTP(data.vpeer_id());
const auto history = _session->data().historyLoaded(peer);
if (!history) {
return;
}
const auto i = _data.find(history);
if (i == end(_data)) {
return;
}
const auto &itemMap = i->second.itemById;
const auto j = itemMap.find(data.vid().v);
if (j == end(itemMap)) {
return;
}
const auto existing = j->second;
if (!HasScheduledDate(existing)) {
// Destroy a local message, that should be in history.
existing->updateSentContent({
qs(data.vmessage()),
Api::EntitiesFromMTP(_session, data.ventities().value_or_empty())
}, data.vmedia());
existing->updateReplyMarkup(
HistoryMessageMarkupData(data.vreply_markup()));
existing->updateForwardedInfo(data.vfwd_from());
_session->data().requestItemTextRefresh(existing);
existing->destroy();
}
}
void ScheduledMessages::apply(
const MTPDupdateDeleteScheduledMessages &update) {
const auto peer = peerFromMTP(update.vpeer());
if (!peer) {
return;
}
const auto history = _session->data().historyLoaded(peer);
if (!history) {
return;
}
auto i = _data.find(history);
if (i == end(_data)) {
return;
}
const auto sent = update.vsent_messages();
const auto &ids = update.vmessages().v;
for (auto k = 0, count = int(ids.size()); k != count; ++k) {
const auto id = ids[k].v;
const auto &list = i->second;
const auto j = list.itemById.find(id);
if (j != end(list.itemById)) {
if (sent && k < sent->v.size()) {
const auto &sentId = sent->v[k];
_session->data().sentFromScheduled({
.item = j->second,
.sentId = sentId.v,
});
}
j->second->destroy();
i = _data.find(history);
if (i == end(_data)) {
break;
}
}
}
_updates.fire_copy(history);
}
void ScheduledMessages::apply(
const MTPDupdateMessageID &update,
not_null<HistoryItem*> local) {
const auto id = update.vid().v;
const auto i = _data.find(local->history());
Assert(i != end(_data));
auto &list = i->second;
const auto j = list.itemById.find(id);
if (j != end(list.itemById) || !IsServerMsgId(id)) {
local->destroy();
} else {
Assert(!list.itemById.contains(local->id));
local->setRealId(localMessageId(id));
list.itemById.emplace(id, local);
}
}
void ScheduledMessages::appendSending(not_null<HistoryItem*> item) {
Expects(item->isSending());
Expects(item->isScheduled());
const auto history = item->history();
auto &list = _data[history];
list.items.emplace_back(item);
sort(list);
_updates.fire_copy(history);
}
void ScheduledMessages::removeSending(not_null<HistoryItem*> item) {
Expects(item->isSending() || item->hasFailed());
Expects(item->isScheduled());
item->destroy();
}
rpl::producer<> ScheduledMessages::updates(not_null<History*> history) {
request(history);
return _updates.events(
) | rpl::filter([=](not_null<History*> value) {
return (value == history);
}) | rpl::to_empty;
}
Data::MessagesSlice ScheduledMessages::list(
not_null<History*> history) const {
auto result = Data::MessagesSlice();
const auto i = _data.find(history);
if (i == end(_data)) {
const auto i = _requests.find(history);
if (i == end(_requests)) {
return result;
}
result.fullCount = result.skippedAfter = result.skippedBefore = 0;
return result;
}
const auto &list = i->second.items;
result.skippedAfter = result.skippedBefore = 0;
result.fullCount = int(list.size());
result.ids = ranges::views::all(
list
) | ranges::views::transform(
&HistoryItem::fullId
) | ranges::to_vector;
return result;
}
Data::MessagesSlice ScheduledMessages::list(
not_null<const Data::ForumTopic*> topic) const {
auto result = Data::MessagesSlice();
const auto i = _data.find(topic->Data::Thread::owningHistory());
if (i == end(_data)) {
const auto i = _requests.find(topic->Data::Thread::owningHistory());
if (i == end(_requests)) {
return result;
}
result.fullCount = result.skippedAfter = result.skippedBefore = 0;
return result;
}
const auto &list = i->second.items;
result.skippedAfter = result.skippedBefore = 0;
result.fullCount = int(list.size());
result.ids = ranges::views::all(
list
) | ranges::views::filter([&](const OwnedItem &item) {
return item->topic() == topic;
}) | ranges::views::transform(
&HistoryItem::fullId
) | ranges::to_vector;
return result;
}
void ScheduledMessages::request(not_null<History*> history) {
const auto peer = history->peer;
if (peer->isBroadcast() && !Data::CanSendAnything(peer)) {
return;
}
auto &request = _requests[history];
if (request.requestId || TooEarlyForRequest(request.lastReceived)) {
return;
}
const auto i = _data.find(history);
const auto hash = (i != end(_data))
? countListHash(i->second)
: uint64(0);
request.requestId = _session->api().request(
MTPmessages_GetScheduledHistory(peer->input(), MTP_long(hash))
).done([=](const MTPmessages_Messages &result) {
parse(history, result);
}).fail([=] {
_requests.remove(history);
}).send();
}
void ScheduledMessages::parse(
not_null<History*> history,
const MTPmessages_Messages &list) {
auto &request = _requests[history];
request.lastReceived = crl::now();
request.requestId = 0;
if (!_clearTimer.isActive()) {
_clearTimer.callOnce(kRequestTimeLimit * 2);
}
list.match([&](const MTPDmessages_messagesNotModified &data) {
}, [&](const auto &data) {
_session->data().processUsers(data.vusers());
_session->data().processChats(data.vchats());
const auto &messages = data.vmessages().v;
if (messages.isEmpty()) {
clearNotSending(history);
return;
}
auto received = base::flat_set<not_null<HistoryItem*>>();
auto clear = base::flat_set<not_null<HistoryItem*>>();
auto &list = _data.emplace(history, List()).first->second;
for (const auto &message : messages) {
if (const auto item = append(history, list, message)) {
received.emplace(item);
}
}
for (const auto &owned : list.items) {
const auto item = owned.get();
if (!item->isSending() && !received.contains(item)) {
clear.emplace(item);
}
}
updated(history, received, clear);
});
}
HistoryItem *ScheduledMessages::append(
not_null<History*> history,
List &list,
const MTPMessage &message) {
const auto id = message.match([&](const auto &data) {
return data.vid().v;
});
const auto i = list.itemById.find(id);
if (i != end(list.itemById)) {
const auto existing = i->second;
message.match([&](const MTPDmessage &data) {
// Scheduled messages never have an edit date,
// so if we receive a flag about it,
// probably this message was edited.
if (data.is_edit_hide()) {
existing->applyEdition(HistoryMessageEdition(_session, data));
} else {
existing->updateSentContent({
qs(data.vmessage()),
Api::EntitiesFromMTP(
_session,
data.ventities().value_or_empty())
}, data.vmedia());
existing->updateReplyMarkup(
HistoryMessageMarkupData(data.vreply_markup()));
existing->updateForwardedInfo(data.vfwd_from());
}
existing->updateDate(data.vdate().v);
history->owner().requestItemTextRefresh(existing);
}, [&](const auto &data) {});
return existing;
}
if (!IsServerMsgId(id)) {
LOG(("API Error: Bad id in scheduled messages: %1.").arg(id));
return nullptr;
}
const auto item = _session->data().addNewMessage(
localMessageId(id),
PrepareMessage(message),
MessageFlags(), // localFlags
NewMessageType::Existing);
if (!item || item->history() != history) {
LOG(("API Error: Bad data received in scheduled messages."));
return nullptr;
}
list.items.emplace_back(item);
list.itemById.emplace(id, item);
return item;
}
void ScheduledMessages::clearNotSending(not_null<History*> history) {
const auto i = _data.find(history);
if (i == end(_data)) {
return;
}
auto clear = base::flat_set<not_null<HistoryItem*>>();
for (const auto &owned : i->second.items) {
if (!owned->isSending() && !owned->hasFailed()) {
clear.emplace(owned.get());
}
}
updated(history, {}, clear);
}
void ScheduledMessages::updated(
not_null<History*> history,
const base::flat_set<not_null<HistoryItem*>> &added,
const base::flat_set<not_null<HistoryItem*>> &clear) {
if (!clear.empty()) {
for (const auto &item : clear) {
item->destroy();
}
}
const auto i = _data.find(history);
if (i != end(_data)) {
sort(i->second);
}
if (!added.empty() || !clear.empty()) {
_updates.fire_copy(history);
}
}
void ScheduledMessages::sort(List &list) {
ranges::sort(list.items, ranges::less(), &HistoryItem::position);
}
void ScheduledMessages::remove(not_null<const HistoryItem*> item) {
const auto history = item->history();
const auto i = _data.find(history);
Assert(i != end(_data));
auto &list = i->second;
if (!item->isSending() && !item->hasFailed()) {
list.itemById.remove(lookupId(item));
}
const auto k = ranges::find(list.items, item, &OwnedItem::get);
Assert(k != list.items.end());
k->release();
list.items.erase(k);
if (list.items.empty()) {
_data.erase(i);
}
_updates.fire_copy(history);
}
uint64 ScheduledMessages::countListHash(const List &list) const {
using namespace Api;
auto hash = HashInit();
auto &&serverside = ranges::views::all(
list.items
) | ranges::views::filter([](const OwnedItem &item) {
return !item->isSending() && !item->hasFailed();
}) | ranges::views::reverse;
for (const auto &item : serverside) {
HashUpdate(hash, lookupId(item.get()).bare);
if (const auto edited = item->Get<HistoryMessageEdited>()) {
HashUpdate(hash, edited->date);
} else {
HashUpdate(hash, TimeId(0));
}
HashUpdate(hash, item->date());
}
return HashFinalize(hash);
}
} // namespace Data

View File

@@ -0,0 +1,100 @@
/*
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 "history/history_item.h"
#include "base/timer.h"
class History;
namespace Main {
class Session;
} // namespace Main
namespace Data {
struct MessagesSlice;
[[nodiscard]] bool IsScheduledMsgId(MsgId id);
class ScheduledMessages final {
public:
explicit ScheduledMessages(not_null<Main::Session*> session);
ScheduledMessages(const ScheduledMessages &other) = delete;
ScheduledMessages &operator=(const ScheduledMessages &other) = delete;
~ScheduledMessages();
[[nodiscard]] MsgId lookupId(not_null<const HistoryItem*> item) const;
[[nodiscard]] HistoryItem *lookupItem(PeerId peer, MsgId msg) const;
[[nodiscard]] HistoryItem *lookupItem(FullMsgId itemId) const;
[[nodiscard]] int count(not_null<History*> history) const;
[[nodiscard]] bool hasFor(not_null<Data::ForumTopic*> topic) const;
[[nodiscard]] MsgId localMessageId(MsgId remoteId) const;
void checkEntitiesAndUpdate(const MTPDmessage &data);
void apply(const MTPDupdateNewScheduledMessage &update);
void apply(const MTPDupdateDeleteScheduledMessages &update);
void apply(
const MTPDupdateMessageID &update,
not_null<HistoryItem*> local);
void appendSending(not_null<HistoryItem*> item);
void removeSending(not_null<HistoryItem*> item);
void sendNowSimpleMessage(
const MTPDupdateShortSentMessage &update,
not_null<HistoryItem*> local);
[[nodiscard]] rpl::producer<> updates(not_null<History*> history);
[[nodiscard]] Data::MessagesSlice list(not_null<History*> history) const;
[[nodiscard]] Data::MessagesSlice list(
not_null<const Data::ForumTopic*> topic) const;
void clear();
private:
using OwnedItem = std::unique_ptr<HistoryItem, HistoryItem::Destroyer>;
struct List {
std::vector<OwnedItem> items;
base::flat_map<MsgId, not_null<HistoryItem*>> itemById;
};
struct Request {
mtpRequestId requestId = 0;
crl::time lastReceived = 0;
};
void request(not_null<History*> history);
void parse(
not_null<History*> history,
const MTPmessages_Messages &list);
HistoryItem *append(
not_null<History*> history,
List &list,
const MTPMessage &message);
void clearNotSending(not_null<History*> history);
void updated(
not_null<History*> history,
const base::flat_set<not_null<HistoryItem*>> &added,
const base::flat_set<not_null<HistoryItem*>> &clear);
void sort(List &list);
void remove(not_null<const HistoryItem*> item);
[[nodiscard]] uint64 countListHash(const List &list) const;
void clearOldRequests();
const not_null<Main::Session*> _session;
base::Timer _clearTimer;
base::flat_map<not_null<History*>, List> _data;
base::flat_map<not_null<History*>, Request> _requests;
rpl::event_stream<not_null<History*>> _updates;
rpl::lifetime _lifetime;
};
} // namespace Data

View File

@@ -0,0 +1,832 @@
/*
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 "data/components/sponsored_messages.h"
#include "api/api_text_entities.h"
#include "api/api_peer_search.h" // SponsoredSearchResult
#include "apiwrap.h"
#include "core/click_handler_types.h"
#include "data/data_channel.h"
#include "data/data_document.h"
#include "data/data_file_origin.h"
#include "data/data_media_preload.h"
#include "data/data_peer_values.h"
#include "data/data_photo.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "history/history.h"
#include "history/view/history_view_element.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/chat/sponsored_message_bar.h"
#include "ui/text/text_utilities.h" // tr::rich.
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());
}
template <typename Fields>
[[nodiscard]] std::vector<TextWithEntities> Prepare(const Fields &fields) {
using InfoList = std::vector<TextWithEntities>;
return (!fields.sponsorInfo.text.isEmpty()
&& !fields.additionalInfo.text.isEmpty())
? InfoList{ fields.sponsorInfo, fields.additionalInfo }
: !fields.sponsorInfo.text.isEmpty()
? InfoList{ fields.sponsorInfo }
: !fields.additionalInfo.text.isEmpty()
? InfoList{ fields.additionalInfo }
: InfoList{};
}
} // namespace
SponsoredMessages::SponsoredMessages(not_null<Main::Session*> session)
: _session(session)
, _clearTimer([=] { clearOldRequests(); }) {
Data::AmPremiumValue(
_session
) | rpl::on_next([=](bool premium) {
if (premium) {
clear();
}
}, _lifetime);
}
SponsoredMessages::~SponsoredMessages() {
Expects(_data.empty());
Expects(_requests.empty());
Expects(_viewRequests.empty());
}
void SponsoredMessages::clear() {
_lifetime.destroy();
for (const auto &request : base::take(_requests)) {
_session->api().request(request.second.requestId).cancel();
}
for (const auto &request : base::take(_viewRequests)) {
_session->api().request(request.second.requestId).cancel();
}
base::take(_data);
}
void SponsoredMessages::clearOldRequests() {
const auto now = crl::now();
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);
}
};
clear(_requests);
clear(_requestsForVideo);
}
SponsoredMessages::AppendResult SponsoredMessages::append(
not_null<History*> history) {
if (isTopBarFor(history)) {
return SponsoredMessages::AppendResult::None;
}
const auto it = _data.find(history);
if (it == end(_data)) {
return SponsoredMessages::AppendResult::None;
}
auto &list = it->second;
if (list.showedAll
|| !TooEarlyForRequest(list.received)
|| list.postsBetween) {
return SponsoredMessages::AppendResult::None;
}
const auto entryIt = ranges::find_if(list.entries, [](const Entry &e) {
return e.item == nullptr;
});
if (entryIt == end(list.entries)) {
list.showedAll = true;
return SponsoredMessages::AppendResult::None;
} else if (entryIt->preload) {
return SponsoredMessages::AppendResult::MediaLoading;
}
entryIt->item.reset(history->addSponsoredMessage(
entryIt->itemFullId.msg,
entryIt->sponsored.from,
entryIt->sponsored.textWithEntities));
return SponsoredMessages::AppendResult::Appended;
}
void SponsoredMessages::inject(
not_null<History*> history,
MsgId injectAfterMsgId,
int betweenHeight,
int fallbackWidth) {
if (!canHaveFor(history)) {
return;
}
const auto it = _data.find(history);
if (it == end(_data)) {
return;
}
auto &list = it->second;
if (!list.postsBetween || (list.entries.size() == list.injectedCount)) {
return;
}
while (true) {
const auto entryIt = ranges::find_if(list.entries, [](const auto &e) {
return e.item == nullptr;
});
if (entryIt == end(list.entries)) {
list.showedAll = true;
return;
}
const auto lastView = (entryIt != begin(list.entries))
? (entryIt - 1)->item->mainView()
: (injectAfterMsgId == ShowAtUnreadMsgId)
? history->firstUnreadMessage()
: [&] {
const auto message = history->peer->owner().message(
history->peer->id,
injectAfterMsgId);
return message ? message->mainView() : nullptr;
}();
if (!lastView || !lastView->block()) {
return;
}
auto summaryBetween = 0;
auto summaryHeight = 0;
using BlockPtr = std::unique_ptr<HistoryBlock>;
using ViewPtr = std::unique_ptr<HistoryView::Element>;
auto blockIt = ranges::find(
history->blocks,
lastView->block(),
&BlockPtr::get);
if (blockIt == end(history->blocks)) {
return;
}
const auto messages = [&]() -> const std::vector<ViewPtr>& {
return (*blockIt)->messages;
};
auto lastViewIt = ranges::find(messages(), lastView, &ViewPtr::get);
auto appendAtLeastToEnd = false;
while ((summaryBetween < list.postsBetween)
|| (summaryHeight < betweenHeight)) {
lastViewIt++;
if (lastViewIt == end(messages())) {
blockIt++;
if (blockIt != end(history->blocks)) {
lastViewIt = begin(messages());
} else {
if (!list.injectedCount) {
appendAtLeastToEnd = true;
break;
}
return;
}
}
summaryBetween++;
const auto viewHeight = (*lastViewIt)->height();
summaryHeight += viewHeight
? viewHeight
: (*lastViewIt)->resizeGetHeight(fallbackWidth);
}
// SponsoredMessages::Details can be requested within
// the constructor of HistoryItem, so itemFullId is used as a key.
entryIt->itemFullId = FullMsgId(
history->peer->id,
_session->data().nextLocalMessageId());
if (appendAtLeastToEnd) {
entryIt->item.reset(history->addSponsoredMessage(
entryIt->itemFullId.msg,
entryIt->sponsored.from,
entryIt->sponsored.textWithEntities));
} else {
const auto makedMessage = history->makeMessage(
entryIt->itemFullId.msg,
entryIt->sponsored.from,
entryIt->sponsored.textWithEntities,
(*lastViewIt)->data());
entryIt->item.reset(makedMessage.get());
history->addNewInTheMiddle(
makedMessage.get(),
std::distance(begin(history->blocks), blockIt),
std::distance(begin(messages()), lastViewIt) + 1);
messages().back().get()->setPendingResize();
}
list.injectedCount++;
}
}
bool SponsoredMessages::canHaveFor(not_null<History*> history) const {
if (history->peer->isChannel()) {
return true;
} else if (const auto user = history->peer->asUser()) {
return user->isBot();
}
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 {
if (peerIsUser(history->peer->id)) {
if (const auto user = history->peer->asUser()) {
return user->isBot();
}
}
return false;
}
void SponsoredMessages::request(not_null<History*> history, Fn<void()> done) {
if (!canHaveFor(history)) {
return;
}
auto &request = _requests[history];
if (request.requestId || TooEarlyForRequest(request.lastReceived)) {
return;
}
{
const auto it = _data.find(history);
if (it != end(_data)) {
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;
}
}
}
request.requestId = _session->api().request(
MTPmessages_GetSponsoredMessages(
MTP_flags(0),
history->peer->input(),
MTPint()) // msg_id
).done([=](const MTPmessages_sponsoredMessages &result) {
parse(history, result);
if (done) {
done();
}
}).fail([=] {
_requests.remove(history);
}).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) {
auto &request = _requests[history];
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 &messages = data.vmessages().v;
auto &list = _data.emplace(history).first->second;
list.entries.clear();
list.received = crl::now();
if (const auto postsBetween = data.vposts_between()) {
list.postsBetween = postsBetween->v;
list.state = State::InjectToMiddle;
} else {
list.state = history->peer->isChannel()
? 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) {
const auto it = _data.find(history);
if (it != end(_data)) {
auto &list = it->second;
if (!list.entries.empty()) {
const auto &entry = list.entries.front();
const auto fullId = entry.itemFullId;
Ui::FillSponsoredMessageBar(
widget,
_session,
fullId,
entry.sponsored.from,
entry.sponsored.textWithEntities);
return fullId;
}
}
return {};
}
rpl::producer<> SponsoredMessages::itemRemoved(const FullMsgId &fullId) {
if (IsServerMsgId(fullId.msg) || !fullId) {
return rpl::never<>();
}
const auto history = _session->data().history(fullId.peer);
const auto it = _data.find(history);
if (it == end(_data)) {
return rpl::never<>();
}
auto &list = it->second;
const auto entryIt = ranges::find_if(list.entries, [&](const Entry &e) {
return e.itemFullId == fullId;
});
if (entryIt == end(list.entries)) {
return rpl::never<>();
}
if (!entryIt->optionalDestructionNotifier) {
entryIt->optionalDestructionNotifier
= std::make_unique<rpl::lifetime>();
entryIt->optionalDestructionNotifier->add([this, fullId] {
_itemRemoved.fire_copy(fullId);
});
}
return _itemRemoved.events(
) | rpl::filter(rpl::mappers::_1 == fullId) | rpl::to_empty;
}
void SponsoredMessages::append(
Fn<not_null<std::vector<Entry>*>()> entries,
not_null<History*> history,
const MTPSponsoredMessage &message) {
const auto &data = message.data();
const auto randomId = data.vrandom_id().v;
auto mediaPhoto = (PhotoData*)nullptr;
auto mediaDocument = (DocumentData*)nullptr;
{
if (data.vmedia()) {
data.vmedia()->match([&](const MTPDmessageMediaPhoto &media) {
if (const auto tlPhoto = media.vphoto()) {
tlPhoto->match([&](const MTPDphoto &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 = _session->data().processDocument(
data,
media.valt_documents());
if (d->isVideoFile()
|| d->isSilentVideo()
|| d->isAnimation()
|| d->isGifv()) {
mediaDocument = d;
}
}, [](const MTPDdocumentEmpty &) {
});
}
}, [](const auto &) {
});
}
};
const auto from = SponsoredFrom{
.title = qs(data.vtitle()),
.link = qs(data.vurl()),
.buttonText = qs(data.vbutton_text()),
.photoId = data.vphoto()
? _session->data().processPhoto(*data.vphoto())->id
: PhotoId(0),
.mediaPhotoId = (mediaPhoto ? mediaPhoto->id : 0),
.mediaDocumentId = (mediaDocument ? mediaDocument->id : 0),
.backgroundEmojiId = BackgroundEmojiIdFromColor(data.vcolor()),
.colorIndex = ColorIndexFromColor(data.vcolor()),
.isLinkInternal = !UrlRequiresConfirmation(qs(data.vurl())),
.isRecommended = data.is_recommended(),
.canReport = data.is_can_report(),
};
auto sponsorInfo = data.vsponsor_info()
? tr::lng_sponsored_info_submenu(
tr::now,
lt_text,
{ .text = qs(*data.vsponsor_info()) },
tr::rich)
: TextWithEntities();
auto additionalInfo = TextWithEntities::Simple(
data.vadditional_info() ? qs(*data.vadditional_info()) : QString());
auto sharedMessage = SponsoredMessage{
.randomId = randomId,
.from = from,
.textWithEntities = {
.text = qs(data.vmessage()),
.entities = Api::EntitiesFromMTP(
_session,
data.ventities().value_or_empty()),
},
.history = history,
.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,
};
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.
const auto preloaded = [=] {
const auto list = entries();
const auto j = ranges::find(*list, itemId, &Entry::itemFullId);
if (j == end(*list)) {
return;
}
auto &entry = *j;
if (entry.preload.get() == kFlaggedPreload) {
entry.preload.release();
} else {
entry.preload = nullptr;
}
};
auto preload = std::unique_ptr<MediaPreload>();
entry.preload.reset(kFlaggedPreload);
if (mediaPhoto) {
preload = std::make_unique<PhotoPreload>(
mediaPhoto,
fileOrigin,
preloaded);
} else if (mediaDocument && VideoPreload::Can(mediaDocument)) {
preload = std::make_unique<VideoPreload>(
mediaDocument,
fileOrigin,
preloaded);
}
// Preload constructor may have called preloaded(), which zero-ed
// entry.preload, that way we're ready and don't need to save it.
// Otherwise we're preloading and need to save the task.
if (entry.preload.get() == kFlaggedPreload) {
entry.preload.release();
if (preload) {
entry.preload = std::move(preload);
}
}
}
void SponsoredMessages::clearItems(not_null<History*> history) {
const auto it = _data.find(history);
if (it == end(_data)) {
return;
}
auto &list = it->second;
for (auto &entry : list.entries) {
entry.item.reset();
}
list.showedAll = false;
list.injectedCount = 0;
}
const SponsoredMessages::Entry *SponsoredMessages::find(
const FullMsgId &fullId) const {
if (!peerIsChannel(fullId.peer) && !peerIsUser(fullId.peer)) {
return nullptr;
}
const auto history = _session->data().history(fullId.peer);
const auto it = _data.find(history);
if (it == end(_data)) {
return nullptr;
}
auto &list = it->second;
const auto entryIt = ranges::find_if(list.entries, [&](const Entry &e) {
return e.itemFullId == fullId;
});
if (entryIt == end(list.entries)) {
return nullptr;
}
return &*entryIt;
}
void SponsoredMessages::view(const FullMsgId &fullId) {
const auto entryPtr = find(fullId);
if (!entryPtr) {
return;
}
view(entryPtr->sponsored.randomId);
}
void SponsoredMessages::view(const QByteArray &randomId) {
auto &request = _viewRequests[randomId];
if (request.requestId || TooEarlyForRequest(request.lastReceived)) {
return;
}
request.requestId = _session->api().request(
MTPmessages_ViewSponsoredMessage(MTP_bytes(randomId))
).done([=] {
auto &request = _viewRequests[randomId];
request.lastReceived = crl::now();
request.requestId = 0;
}).fail([=] {
_viewRequests.remove(randomId);
}).send();
}
SponsoredMessages::Details SponsoredMessages::lookupDetails(
const FullMsgId &fullId) const {
const auto entryPtr = find(fullId);
if (!entryPtr) {
return {};
}
return lookupDetails(entryPtr->sponsored);
}
SponsoredMessages::Details SponsoredMessages::lookupDetails(
const SponsoredMessage &data) const {
return {
.info = Prepare(data),
.link = data.link,
.buttonText = data.from.buttonText,
.photoId = data.from.photoId,
.mediaPhotoId = data.from.mediaPhotoId,
.mediaDocumentId = data.from.mediaDocumentId,
.backgroundEmojiId = data.from.backgroundEmojiId,
.colorIndex = data.from.colorIndex,
.isLinkInternal = data.from.isLinkInternal,
.canReport = data.from.canReport,
};
}
SponsoredMessages::Details SponsoredMessages::lookupDetails(
const Api::SponsoredSearchResult &data) const {
return {
.info = Prepare(data),
.canReport = true,
};
}
void SponsoredMessages::clicked(
const FullMsgId &fullId,
bool isMedia,
bool isFullscreen) {
const auto entryPtr = find(fullId);
if (!entryPtr) {
return;
}
clicked(entryPtr->sponsored.randomId, isMedia, isFullscreen);
}
void SponsoredMessages::clicked(
const QByteArray &randomId,
bool isMedia,
bool isFullscreen) {
using Flag = MTPmessages_ClickSponsoredMessage::Flag;
_session->api().request(MTPmessages_ClickSponsoredMessage(
MTP_flags(Flag(0)
| (isMedia ? Flag::f_media : Flag(0))
| (isFullscreen ? Flag::f_fullscreen : Flag(0))),
MTP_bytes(randomId)
)).send();
}
SponsoredReportAction SponsoredMessages::createReportCallback(
const FullMsgId &fullId) {
const auto entry = find(fullId);
if (!entry) {
return { .callback = [=](const auto &...) {} };
}
const auto history = _session->data().history(fullId.peer);
const auto erase = [=] {
const auto it = _data.find(history);
if (it != end(_data)) {
auto &list = it->second.entries;
const auto proj = [&](const Entry &e) {
return e.itemFullId == fullId;
};
list.erase(ranges::remove_if(list, proj), end(list));
}
};
return createReportCallback(entry->sponsored.randomId, erase);
}
SponsoredReportAction SponsoredMessages::createReportCallback(
const QByteArray &randomId,
Fn<void()> erase) {
using TLChoose = MTPDchannels_sponsoredMessageReportResultChooseOption;
using TLAdsHidden = MTPDchannels_sponsoredMessageReportResultAdsHidden;
using TLReported = MTPDchannels_sponsoredMessageReportResultReported;
using Result = SponsoredReportResult;
struct State final {
#ifdef _DEBUG
~State() {
qDebug() << "SponsoredMessages Report ~State().";
}
#endif
mtpRequestId requestId = 0;
};
const auto state = std::make_shared<State>();
return { .callback = [=](Result::Id optionId, Fn<void(Result)> done) {
if (optionId == Result::Id("-1")) {
erase();
return;
}
state->requestId = _session->api().request(
MTPmessages_ReportSponsoredMessage(
MTP_bytes(randomId),
MTP_bytes(optionId))
).done([=](
const MTPchannels_SponsoredMessageReportResult &result,
mtpRequestId requestId) {
if (state->requestId != requestId) {
return;
}
state->requestId = 0;
done(result.match([&](const TLChoose &data) {
const auto t = qs(data.vtitle());
auto list = Result::Options();
list.reserve(data.voptions().v.size());
for (const auto &tl : data.voptions().v) {
list.emplace_back(Result::Option{
.id = tl.data().voption().v,
.text = qs(tl.data().vtext()),
});
}
return Result{ .options = std::move(list), .title = t };
}, [](const TLAdsHidden &data) -> Result {
return { .result = Result::FinalStep::Hidden };
}, [&](const TLReported &data) -> Result {
erase();
if (optionId == Result::Id("1")) { // I don't like it.
return { .result = Result::FinalStep::Silence };
}
return { .result = Result::FinalStep::Reported };
}));
}).fail([=](const MTP::Error &error) {
state->requestId = 0;
if (error.type() == u"PREMIUM_ACCOUNT_REQUIRED"_q) {
done({ .result = Result::FinalStep::Premium });
} else {
done({ .error = error.type() });
}
}).send();
} };
}
SponsoredMessages::State SponsoredMessages::state(
not_null<History*> history) const {
const auto it = _data.find(history);
return (it == end(_data)) ? State::None : it->second.state;
}
} // namespace Data

View File

@@ -0,0 +1,245 @@
/*
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 "history/history_item.h"
#include "ui/image/image_location.h"
#include "window/window_session_controller_link_info.h"
class History;
namespace Api {
struct SponsoredSearchResult;
} // namespace Api
namespace Main {
class Session;
} // namespace Main
namespace Ui {
class RpWidget;
} // namespace Ui
namespace Data {
class MediaPreload;
struct SponsoredReportResult final {
using Id = QByteArray;
struct Option final {
Id id = 0;
QString text;
};
using Options = std::vector<Option>;
enum class FinalStep {
Hidden,
Reported,
Premium,
Silence,
};
Options options;
QString title;
QString error;
FinalStep result;
};
struct SponsoredFrom {
QString title;
QString link;
QString buttonText;
PhotoId photoId = PhotoId(0);
PhotoId mediaPhotoId = PhotoId(0);
DocumentId mediaDocumentId = DocumentId(0);
uint64 backgroundEmojiId = 0;
uint8 colorIndex : 6 = 0;
bool isLinkInternal = false;
bool isRecommended = false;
bool canReport = false;
};
struct SponsoredMessage {
QByteArray randomId;
SponsoredFrom from;
TextWithEntities textWithEntities;
not_null<History*> history;
QString link;
TextWithEntities sponsorInfo;
TextWithEntities additionalInfo;
crl::time durationMin = 0;
crl::time durationMax = 0;
};
struct SponsoredMessageDetails {
std::vector<TextWithEntities> info;
QString link;
QString buttonText;
PhotoId photoId = PhotoId(0);
PhotoId mediaPhotoId = PhotoId(0);
DocumentId mediaDocumentId = DocumentId(0);
uint64 backgroundEmojiId = 0;
uint8 colorIndex : 6 = 0;
bool isLinkInternal = false;
bool canReport = false;
};
struct SponsoredReportAction {
Fn<void(
Data::SponsoredReportResult::Id,
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 {
None,
Appended,
MediaLoading,
};
enum class State {
None,
AppendToEnd,
InjectToMiddle,
AppendToTopBar,
};
using Details = SponsoredMessageDetails;
using RandomId = QByteArray;
explicit SponsoredMessages(not_null<Main::Session*> session);
~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);
void clicked(
const QByteArray &randomId,
bool isMedia,
bool isFullscreen);
[[nodiscard]] FullMsgId fillTopBar(
not_null<History*> history,
not_null<Ui::RpWidget*> widget);
[[nodiscard]] rpl::producer<> itemRemoved(const FullMsgId &);
[[nodiscard]] AppendResult append(not_null<History*> history);
void inject(
not_null<History*> history,
MsgId injectAfterMsgId,
int betweenHeight,
int fallbackWidth);
void view(const FullMsgId &fullId);
void view(const QByteArray &randomId);
[[nodiscard]] State state(not_null<History*> history) const;
[[nodiscard]] SponsoredReportAction createReportCallback(
const FullMsgId &fullId);
[[nodiscard]] SponsoredReportAction createReportCallback(
const QByteArray &randomId,
Fn<void()> erase);
void clear();
private:
using OwnedItem = std::unique_ptr<HistoryItem, HistoryItem::Destroyer>;
struct Entry {
OwnedItem item;
FullMsgId itemFullId;
SponsoredMessage sponsored;
std::unique_ptr<MediaPreload> preload;
std::unique_ptr<rpl::lifetime> optionalDestructionNotifier;
};
struct List {
std::vector<Entry> entries;
// Data between history displays.
size_t injectedCount = 0;
bool showedAll = false;
//
crl::time received = 0;
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,
const MTPSponsoredMessage &message);
[[nodiscard]] SponsoredForVideo prepareForVideo(
not_null<PeerData*> peer);
void clearOldRequests();
const Entry *find(const FullMsgId &fullId) const;
const not_null<Main::Session*> _session;
base::Timer _clearTimer;
base::flat_map<not_null<History*>, List> _data;
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;
};
} // namespace Data

View File

@@ -0,0 +1,328 @@
/*
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 "data/components/top_peers.h"
#include "api/api_hash.h"
#include "apiwrap.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "main/main_session.h"
#include "mtproto/mtproto_config.h"
#include "storage/serialize_common.h"
#include "storage/serialize_peer.h"
#include "storage/storage_account.h"
namespace Data {
namespace {
constexpr auto kLimit = 64;
constexpr auto kRequestTimeLimit = 10 * crl::time(1000);
[[nodiscard]] float64 RatingDelta(TimeId now, TimeId was, int decay) {
return std::exp((now - was) * 1. / decay);
}
[[nodiscard]] quint64 SerializeRating(float64 rating) {
return quint64(
base::SafeRound(std::clamp(rating, 0., 1'000'000.) * 1'000'000.));
}
[[nodiscard]] float64 DeserializeRating(quint64 rating) {
return std::clamp(
rating,
quint64(),
quint64(1'000'000'000'000ULL)
) / 1'000'000.;
}
[[nodiscard]] MTPTopPeerCategory TypeToCategory(TopPeerType type) {
switch (type) {
case TopPeerType::Chat: return MTP_topPeerCategoryCorrespondents();
case TopPeerType::BotApp: return MTP_topPeerCategoryBotsApp();
}
Unexpected("Type in TypeToCategory.");
}
[[nodiscard]] auto TypeToGetFlags(TopPeerType type) {
using Flag = MTPcontacts_GetTopPeers::Flag;
switch (type) {
case TopPeerType::Chat: return Flag::f_correspondents;
case TopPeerType::BotApp: return Flag::f_bots_app;
}
Unexpected("Type in TypeToGetFlags.");
}
} // namespace
TopPeers::TopPeers(not_null<Main::Session*> session, TopPeerType type)
: _session(session)
, _type(type) {
if (_type == TopPeerType::Chat) {
loadAfterChats();
}
}
void TopPeers::loadAfterChats() {
using namespace rpl::mappers;
crl::on_main(_session, [=] {
_session->data().chatsListLoadedEvents(
) | rpl::filter(_1 == nullptr) | rpl::on_next([=] {
crl::on_main(_session, [=] {
request();
});
}, _session->lifetime());
});
}
TopPeers::~TopPeers() = default;
std::vector<not_null<PeerData*>> TopPeers::list() const {
_session->local().readSearchSuggestions();
return _list
| ranges::view::transform(&TopPeer::peer)
| ranges::to_vector;
}
bool TopPeers::disabled() const {
_session->local().readSearchSuggestions();
return _disabled;
}
rpl::producer<> TopPeers::updates() const {
return _updates.events();
}
void TopPeers::remove(not_null<PeerData*> peer) {
const auto i = ranges::find(_list, peer, &TopPeer::peer);
if (i != end(_list)) {
_list.erase(i);
updated();
}
_requestId = _session->api().request(MTPcontacts_ResetTopPeerRating(
TypeToCategory(_type),
peer->input()
)).send();
}
void TopPeers::increment(not_null<PeerData*> peer, TimeId date) {
_session->local().readSearchSuggestions();
if (_disabled || date <= _lastReceivedDate) {
return;
}
if (const auto user = peer->asUser(); user && !user->isBot()) {
auto changed = false;
auto i = ranges::find(_list, peer, &TopPeer::peer);
if (i == end(_list)) {
_list.push_back({ .peer = peer });
i = end(_list) - 1;
changed = true;
}
const auto &config = peer->session().mtp().config();
const auto decay = config.values().ratingDecay;
i->rating += RatingDelta(date, _lastReceivedDate, decay);
for (; i != begin(_list); --i) {
if (i->rating >= (i - 1)->rating) {
changed = true;
std::swap(*i, *(i - 1));
} else {
break;
}
}
if (changed) {
updated();
} else {
_session->local().writeSearchSuggestionsDelayed();
}
}
}
void TopPeers::reload() {
if (_requestId
|| (_lastReceived
&& _lastReceived + kRequestTimeLimit > crl::now())) {
return;
}
request();
}
void TopPeers::toggleDisabled(bool disabled) {
_session->local().readSearchSuggestions();
if (disabled) {
if (!_disabled || !_list.empty()) {
_disabled = true;
_list.clear();
updated();
}
} else if (_disabled) {
_disabled = false;
updated();
}
_session->api().request(MTPcontacts_ToggleTopPeers(
MTP_bool(!disabled)
)).done([=] {
if (!_disabled) {
request();
}
}).send();
}
void TopPeers::request() {
if (_requestId) {
return;
}
_requestId = _session->api().request(MTPcontacts_GetTopPeers(
MTP_flags(TypeToGetFlags(_type)),
MTP_int(0),
MTP_int(kLimit),
MTP_long(countHash())
)).done([=](
const MTPcontacts_TopPeers &result,
const MTP::Response &response) {
_lastReceivedDate = TimeId(response.outerMsgId >> 32);
_lastReceived = crl::now();
_requestId = 0;
result.match([&](const MTPDcontacts_topPeers &data) {
_disabled = false;
const auto owner = &_session->data();
owner->processUsers(data.vusers());
owner->processChats(data.vchats());
for (const auto &category : data.vcategories().v) {
const auto &data = category.data();
const auto cons = (_type == TopPeerType::Chat)
? mtpc_topPeerCategoryCorrespondents
: mtpc_topPeerCategoryBotsApp;
if (data.vcategory().type() != cons) {
LOG(("API Error: Unexpected top peer category."));
continue;
}
_list = ranges::views::all(
data.vpeers().v
) | ranges::views::transform([&](
const MTPTopPeer &top) {
return TopPeer{
owner->peer(peerFromMTP(top.data().vpeer())),
top.data().vrating().v,
};
}) | ranges::to_vector;
}
updated();
}, [&](const MTPDcontacts_topPeersDisabled &) {
if (!_disabled) {
_list.clear();
_disabled = true;
updated();
}
}, [](const MTPDcontacts_topPeersNotModified &) {
});
}).fail([=] {
_lastReceived = crl::now();
_requestId = 0;
}).send();
}
uint64 TopPeers::countHash() const {
using namespace Api;
auto hash = HashInit();
for (const auto &top : _list | ranges::views::take(kLimit)) {
HashUpdate(hash, peerToUser(top.peer->id).bare);
}
return HashFinalize(hash);
}
void TopPeers::updated() {
_updates.fire({});
_session->local().writeSearchSuggestionsDelayed();
}
QByteArray TopPeers::serialize() const {
_session->local().readSearchSuggestions();
if (!_disabled && _list.empty()) {
return {};
}
auto size = 3 * sizeof(quint32); // AppVersion, disabled, count
const auto count = std::min(int(_list.size()), kLimit);
auto &&list = _list | ranges::views::take(count);
for (const auto &top : list) {
size += Serialize::peerSize(top.peer) + sizeof(quint64);
}
auto stream = Serialize::ByteArrayWriter(size);
stream
<< quint32(AppVersion)
<< quint32(_disabled ? 1 : 0)
<< quint32(count);
for (const auto &top : list) {
Serialize::writePeer(stream, top.peer);
stream << SerializeRating(top.rating);
}
return std::move(stream).result();
}
void TopPeers::applyLocal(QByteArray serialized) {
if (_lastReceived) {
DEBUG_LOG(("Suggestions: Skipping TopPeers local, got already."));
return;
}
_list.clear();
_disabled = false;
if (serialized.isEmpty()) {
DEBUG_LOG(("Suggestions: Bad TopPeers local, empty."));
return;
}
auto stream = Serialize::ByteArrayReader(serialized);
auto streamAppVersion = quint32();
auto disabled = quint32();
auto count = quint32();
stream >> streamAppVersion >> disabled >> count;
if (!stream.ok()) {
DEBUG_LOG(("Suggestions: Bad TopPeers local, not ok."));
return;
}
DEBUG_LOG(("Suggestions: "
"Start TopPeers read, count: %1, version: %2, disabled: %3."
).arg(count
).arg(streamAppVersion
).arg(disabled));
_list.reserve(count);
for (auto i = 0; i != int(count); ++i) {
auto rating = quint64();
const auto streamPosition = stream.underlying().device()->pos();
const auto peer = Serialize::readPeer(
_session,
streamAppVersion,
stream);
stream >> rating;
if (stream.ok() && peer) {
_list.push_back({
.peer = peer,
.rating = DeserializeRating(rating),
});
} else {
DEBUG_LOG(("Suggestions: "
"Failed TopPeers reading %1 / %2.").arg(i + 1).arg(count));
DEBUG_LOG(("Failed bytes: %1.").arg(
QString::fromUtf8(serialized.mid(streamPosition).toHex())));
_list.clear();
return;
}
}
_disabled = (disabled == 1);
DEBUG_LOG(
("Suggestions: TopPeers read OK, count: %1").arg(_list.size()));
}
} // namespace Data

View File

@@ -0,0 +1,64 @@
/*
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
namespace Main {
class Session;
} // namespace Main
namespace Data {
enum class TopPeerType {
Chat,
BotApp,
};
class TopPeers final {
public:
TopPeers(not_null<Main::Session*> session, TopPeerType type);
~TopPeers();
[[nodiscard]] std::vector<not_null<PeerData*>> list() const;
[[nodiscard]] bool disabled() const;
[[nodiscard]] rpl::producer<> updates() const;
void remove(not_null<PeerData*> peer);
void increment(not_null<PeerData*> peer, TimeId date);
void reload();
void toggleDisabled(bool disabled);
[[nodiscard]] QByteArray serialize() const;
void applyLocal(QByteArray serialized);
private:
struct TopPeer {
not_null<PeerData*> peer;
float64 rating = 0.;
};
void loadAfterChats();
void request();
[[nodiscard]] uint64 countHash() const;
void updated();
const not_null<Main::Session*> _session;
const TopPeerType _type = {};
std::vector<TopPeer> _list;
rpl::event_stream<> _updates;
crl::time _lastReceived = 0;
TimeId _lastReceivedDate = 0;
mtpRequestId _requestId = 0;
bool _disabled = false;
bool _received = false;
};
} // namespace Data