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
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:
271
Telegram/SourceFiles/data/components/credits.cpp
Normal file
271
Telegram/SourceFiles/data/components/credits.cpp
Normal 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());
|
||||
}
|
||||
84
Telegram/SourceFiles/data/components/credits.h
Normal file
84
Telegram/SourceFiles/data/components/credits.h
Normal 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
|
||||
218
Telegram/SourceFiles/data/components/factchecks.cpp
Normal file
218
Telegram/SourceFiles/data/components/factchecks.cpp
Normal 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
|
||||
70
Telegram/SourceFiles/data/components/factchecks.h
Normal file
70
Telegram/SourceFiles/data/components/factchecks.h
Normal 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
|
||||
481
Telegram/SourceFiles/data/components/gift_auctions.cpp
Normal file
481
Telegram/SourceFiles/data/components/gift_auctions.cpp
Normal 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
|
||||
157
Telegram/SourceFiles/data/components/gift_auctions.h
Normal file
157
Telegram/SourceFiles/data/components/gift_auctions.h
Normal 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
|
||||
44
Telegram/SourceFiles/data/components/location_pickers.cpp
Normal file
44
Telegram/SourceFiles/data/components/location_pickers.cpp
Normal 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
|
||||
39
Telegram/SourceFiles/data/components/location_pickers.h
Normal file
39
Telegram/SourceFiles/data/components/location_pickers.h
Normal 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
|
||||
186
Telegram/SourceFiles/data/components/passkeys.cpp
Normal file
186
Telegram/SourceFiles/data/components/passkeys.cpp
Normal 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
|
||||
79
Telegram/SourceFiles/data/components/passkeys.h
Normal file
79
Telegram/SourceFiles/data/components/passkeys.h
Normal 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
|
||||
364
Telegram/SourceFiles/data/components/promo_suggestions.cpp
Normal file
364
Telegram/SourceFiles/data/components/promo_suggestions.cpp
Normal 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
|
||||
107
Telegram/SourceFiles/data/components/promo_suggestions.h
Normal file
107
Telegram/SourceFiles/data/components/promo_suggestions.h
Normal 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
|
||||
166
Telegram/SourceFiles/data/components/recent_peers.cpp
Normal file
166
Telegram/SourceFiles/data/components/recent_peers.cpp
Normal 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
|
||||
55
Telegram/SourceFiles/data/components/recent_peers.h
Normal file
55
Telegram/SourceFiles/data/components/recent_peers.h
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
662
Telegram/SourceFiles/data/components/scheduled_messages.cpp
Normal file
662
Telegram/SourceFiles/data/components/scheduled_messages.cpp
Normal 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
|
||||
100
Telegram/SourceFiles/data/components/scheduled_messages.h
Normal file
100
Telegram/SourceFiles/data/components/scheduled_messages.h
Normal 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
|
||||
832
Telegram/SourceFiles/data/components/sponsored_messages.cpp
Normal file
832
Telegram/SourceFiles/data/components/sponsored_messages.cpp
Normal 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
|
||||
245
Telegram/SourceFiles/data/components/sponsored_messages.h
Normal file
245
Telegram/SourceFiles/data/components/sponsored_messages.h
Normal 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
|
||||
328
Telegram/SourceFiles/data/components/top_peers.cpp
Normal file
328
Telegram/SourceFiles/data/components/top_peers.cpp
Normal 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
|
||||
64
Telegram/SourceFiles/data/components/top_peers.h
Normal file
64
Telegram/SourceFiles/data/components/top_peers.h
Normal 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
|
||||
Reference in New Issue
Block a user