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:
217
Telegram/SourceFiles/data/business/data_business_chatbots.cpp
Normal file
217
Telegram/SourceFiles/data/business/data_business_chatbots.cpp
Normal file
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
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/business/data_business_chatbots.h"
|
||||
|
||||
#include "apiwrap.h"
|
||||
#include "boxes/peers/edit_peer_permissions_box.h"
|
||||
#include "data/business/data_business_common.h"
|
||||
#include "data/business/data_business_info.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_user.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "main/main_session.h"
|
||||
|
||||
namespace Data {
|
||||
|
||||
Chatbots::Chatbots(not_null<Session*> owner)
|
||||
: _owner(owner) {
|
||||
}
|
||||
|
||||
Chatbots::~Chatbots() = default;
|
||||
|
||||
void Chatbots::preload() {
|
||||
if (_loaded || _requestId) {
|
||||
return;
|
||||
}
|
||||
_requestId = _owner->session().api().request(
|
||||
MTPaccount_GetConnectedBots()
|
||||
).done([=](const MTPaccount_ConnectedBots &result) {
|
||||
_requestId = 0;
|
||||
_loaded = true;
|
||||
|
||||
const auto &data = result.data();
|
||||
_owner->processUsers(data.vusers());
|
||||
const auto &list = data.vconnected_bots().v;
|
||||
if (!list.isEmpty()) {
|
||||
const auto &bot = list.front().data();
|
||||
const auto botId = bot.vbot_id().v;
|
||||
_settings = ChatbotsSettings{
|
||||
.bot = _owner->session().data().user(botId),
|
||||
.recipients = FromMTP(_owner, bot.vrecipients()),
|
||||
.permissions = FromMTP(bot.vrights()),
|
||||
};
|
||||
} else {
|
||||
_settings.force_assign(ChatbotsSettings());
|
||||
}
|
||||
}).fail([=](const MTP::Error &error) {
|
||||
_requestId = 0;
|
||||
LOG(("API Error: Could not get connected bots %1 (%2)"
|
||||
).arg(error.code()
|
||||
).arg(error.type()));
|
||||
}).send();
|
||||
}
|
||||
|
||||
bool Chatbots::loaded() const {
|
||||
return _loaded;
|
||||
}
|
||||
|
||||
const ChatbotsSettings &Chatbots::current() const {
|
||||
return _settings.current();
|
||||
}
|
||||
|
||||
rpl::producer<ChatbotsSettings> Chatbots::changes() const {
|
||||
return _settings.changes();
|
||||
}
|
||||
|
||||
rpl::producer<ChatbotsSettings> Chatbots::value() const {
|
||||
return _settings.value();
|
||||
}
|
||||
|
||||
void Chatbots::save(
|
||||
ChatbotsSettings settings,
|
||||
Fn<void()> done,
|
||||
Fn<void(QString)> fail) {
|
||||
const auto was = _settings.current();
|
||||
if (was == settings) {
|
||||
return;
|
||||
} else if (was.bot || settings.bot) {
|
||||
using Flag = MTPaccount_UpdateConnectedBot::Flag;
|
||||
const auto api = &_owner->session().api();
|
||||
api->request(MTPaccount_UpdateConnectedBot(
|
||||
MTP_flags(!settings.bot ? Flag::f_deleted : Flag::f_rights),
|
||||
ToMTP(settings.permissions),
|
||||
(settings.bot ? settings.bot : was.bot)->inputUser(),
|
||||
ForBotsToMTP(settings.recipients)
|
||||
)).done([=](const MTPUpdates &result) {
|
||||
api->applyUpdates(result);
|
||||
if (done) {
|
||||
done();
|
||||
}
|
||||
}).fail([=](const MTP::Error &error) {
|
||||
_settings = was;
|
||||
if (fail) {
|
||||
fail(error.type());
|
||||
}
|
||||
}).send();
|
||||
}
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
void Chatbots::togglePaused(not_null<PeerData*> peer, bool paused) {
|
||||
const auto type = paused
|
||||
? SentRequestType::Pause
|
||||
: SentRequestType::Unpause;
|
||||
const auto api = &_owner->session().api();
|
||||
const auto i = _sentRequests.find(peer);
|
||||
if (i != end(_sentRequests)) {
|
||||
const auto already = i->second.type;
|
||||
if (already == SentRequestType::Remove || already == type) {
|
||||
return;
|
||||
}
|
||||
api->request(i->second.requestId).cancel();
|
||||
_sentRequests.erase(i);
|
||||
}
|
||||
const auto id = api->request(MTPaccount_ToggleConnectedBotPaused(
|
||||
peer->input(),
|
||||
MTP_bool(paused)
|
||||
)).done([=] {
|
||||
if (_sentRequests[peer].type != type) {
|
||||
return;
|
||||
} else if (const auto settings = peer->barSettings()) {
|
||||
peer->setBarSettings(paused
|
||||
? ((*settings | PeerBarSetting::BusinessBotPaused)
|
||||
& ~PeerBarSetting::BusinessBotCanReply)
|
||||
: ((*settings & ~PeerBarSetting::BusinessBotPaused)
|
||||
| PeerBarSetting::BusinessBotCanReply));
|
||||
} else {
|
||||
api->requestPeerSettings(peer);
|
||||
}
|
||||
_sentRequests.remove(peer);
|
||||
}).fail([=] {
|
||||
if (_sentRequests[peer].type != type) {
|
||||
return;
|
||||
}
|
||||
api->requestPeerSettings(peer);
|
||||
_sentRequests.remove(peer);
|
||||
}).send();
|
||||
_sentRequests[peer] = SentRequest{ type, id };
|
||||
}
|
||||
|
||||
void Chatbots::removeFrom(not_null<PeerData*> peer) {
|
||||
const auto type = SentRequestType::Remove;
|
||||
const auto api = &_owner->session().api();
|
||||
const auto i = _sentRequests.find(peer);
|
||||
if (i != end(_sentRequests)) {
|
||||
const auto already = i->second.type;
|
||||
if (already == type) {
|
||||
return;
|
||||
}
|
||||
api->request(i->second.requestId).cancel();
|
||||
_sentRequests.erase(i);
|
||||
}
|
||||
const auto id = api->request(MTPaccount_DisablePeerConnectedBot(
|
||||
peer->input()
|
||||
)).done([=] {
|
||||
if (_sentRequests[peer].type != type) {
|
||||
return;
|
||||
} else if (const auto settings = peer->barSettings()) {
|
||||
peer->clearBusinessBot();
|
||||
} else {
|
||||
api->requestPeerSettings(peer);
|
||||
}
|
||||
_sentRequests.remove(peer);
|
||||
reload();
|
||||
}).fail([=] {
|
||||
api->requestPeerSettings(peer);
|
||||
_sentRequests.remove(peer);
|
||||
}).send();
|
||||
_sentRequests[peer] = SentRequest{ type, id };
|
||||
}
|
||||
|
||||
void Chatbots::reload() {
|
||||
_loaded = false;
|
||||
_owner->session().api().request(base::take(_requestId)).cancel();
|
||||
preload();
|
||||
}
|
||||
|
||||
EditFlagsDescriptor<ChatbotsPermissions> ChatbotsPermissionsLabels() {
|
||||
using Flag = ChatbotsPermission;
|
||||
|
||||
using PermissionLabel = EditFlagsLabel<ChatbotsPermissions>;
|
||||
auto messages = std::vector<PermissionLabel>{
|
||||
{ Flag::ViewMessages, tr::lng_chatbots_read(tr::now) },
|
||||
{ Flag::ReplyToMessages, tr::lng_chatbots_reply(tr::now) },
|
||||
{ Flag::MarkAsRead, tr::lng_chatbots_mark_as_read(tr::now) },
|
||||
{ Flag::DeleteSent, tr::lng_chatbots_delete_sent(tr::now) },
|
||||
{ Flag::DeleteReceived, tr::lng_chatbots_delete_received(tr::now) },
|
||||
};
|
||||
auto manage = std::vector<PermissionLabel>{
|
||||
{ Flag::EditName, tr::lng_chatbots_edit_name(tr::now) },
|
||||
{ Flag::EditBio, tr::lng_chatbots_edit_bio(tr::now) },
|
||||
{ Flag::EditUserpic, tr::lng_chatbots_edit_userpic(tr::now) },
|
||||
{ Flag::EditUsername, tr::lng_chatbots_edit_username(tr::now) },
|
||||
};
|
||||
auto gifts = std::vector<PermissionLabel>{
|
||||
{ Flag::ViewGifts, tr::lng_chatbots_view_gifts(tr::now) },
|
||||
{ Flag::SellGifts, tr::lng_chatbots_sell_gifts(tr::now) },
|
||||
{ Flag::GiftSettings, tr::lng_chatbots_gift_settings(tr::now) },
|
||||
{ Flag::TransferGifts, tr::lng_chatbots_transfer_gifts(tr::now) },
|
||||
{ Flag::TransferStars, tr::lng_chatbots_transfer_stars(tr::now) },
|
||||
};
|
||||
auto stories = std::vector<PermissionLabel>{
|
||||
{ Flag::ManageStories, tr::lng_chatbots_manage_stories(tr::now) },
|
||||
};
|
||||
return { .labels = {
|
||||
{ tr::lng_chatbots_manage_messages(), std::move(messages) },
|
||||
{ tr::lng_chatbots_manage_profile(), std::move(manage) },
|
||||
{ tr::lng_chatbots_manage_gifts(), std::move(gifts) },
|
||||
{ std::nullopt, std::move(stories) },
|
||||
}, .st = nullptr };
|
||||
}
|
||||
|
||||
} // namespace Data
|
||||
76
Telegram/SourceFiles/data/business/data_business_chatbots.h
Normal file
76
Telegram/SourceFiles/data/business/data_business_chatbots.h
Normal file
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
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/business/data_business_common.h"
|
||||
|
||||
class UserData;
|
||||
|
||||
template <typename Flags>
|
||||
struct EditFlagsDescriptor;
|
||||
|
||||
namespace Data {
|
||||
|
||||
class Session;
|
||||
|
||||
struct ChatbotsSettings {
|
||||
UserData *bot = nullptr;
|
||||
BusinessRecipients recipients;
|
||||
ChatbotsPermissions permissions;
|
||||
|
||||
friend inline bool operator==(
|
||||
const ChatbotsSettings &,
|
||||
const ChatbotsSettings &) = default;
|
||||
};
|
||||
|
||||
class Chatbots final {
|
||||
public:
|
||||
explicit Chatbots(not_null<Session*> owner);
|
||||
~Chatbots();
|
||||
|
||||
void preload();
|
||||
[[nodiscard]] bool loaded() const;
|
||||
[[nodiscard]] const ChatbotsSettings ¤t() const;
|
||||
[[nodiscard]] rpl::producer<ChatbotsSettings> changes() const;
|
||||
[[nodiscard]] rpl::producer<ChatbotsSettings> value() const;
|
||||
|
||||
void save(
|
||||
ChatbotsSettings settings,
|
||||
Fn<void()> done,
|
||||
Fn<void(QString)> fail);
|
||||
|
||||
void togglePaused(not_null<PeerData*> peer, bool paused);
|
||||
void removeFrom(not_null<PeerData*> peer);
|
||||
|
||||
private:
|
||||
enum class SentRequestType {
|
||||
Pause,
|
||||
Unpause,
|
||||
Remove,
|
||||
};
|
||||
struct SentRequest {
|
||||
SentRequestType type = SentRequestType::Pause;
|
||||
mtpRequestId requestId = 0;
|
||||
};
|
||||
|
||||
void reload();
|
||||
|
||||
const not_null<Session*> _owner;
|
||||
|
||||
rpl::variable<ChatbotsSettings> _settings;
|
||||
mtpRequestId _requestId = 0;
|
||||
bool _loaded = false;
|
||||
|
||||
base::flat_map<not_null<PeerData*>, SentRequest> _sentRequests;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] auto ChatbotsPermissionsLabels()
|
||||
-> EditFlagsDescriptor<ChatbotsPermissions>;
|
||||
|
||||
} // namespace Data
|
||||
398
Telegram/SourceFiles/data/business/data_business_common.cpp
Normal file
398
Telegram/SourceFiles/data/business/data_business_common.cpp
Normal file
@@ -0,0 +1,398 @@
|
||||
/*
|
||||
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/business/data_business_common.h"
|
||||
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_user.h"
|
||||
|
||||
namespace Data {
|
||||
namespace {
|
||||
|
||||
constexpr auto kDay = WorkingInterval::kDay;
|
||||
constexpr auto kWeek = WorkingInterval::kWeek;
|
||||
constexpr auto kInNextDayMax = WorkingInterval::kInNextDayMax;
|
||||
|
||||
[[nodiscard]] WorkingIntervals SortAndMerge(WorkingIntervals intervals) {
|
||||
auto &list = intervals.list;
|
||||
ranges::sort(list, ranges::less(), &WorkingInterval::start);
|
||||
for (auto i = 0, count = int(list.size()); i != count; ++i) {
|
||||
if (i && list[i] && list[i -1] && list[i].start <= list[i - 1].end) {
|
||||
list[i - 1] = list[i - 1].united(list[i]);
|
||||
list[i] = {};
|
||||
}
|
||||
if (!list[i]) {
|
||||
list.erase(list.begin() + i);
|
||||
--i;
|
||||
--count;
|
||||
}
|
||||
}
|
||||
return intervals;
|
||||
}
|
||||
|
||||
[[nodiscard]] WorkingIntervals MoveTailToFront(WorkingIntervals intervals) {
|
||||
auto &list = intervals.list;
|
||||
auto after = WorkingInterval{ kWeek, kWeek + kDay };
|
||||
while (!list.empty()) {
|
||||
if (const auto tail = list.back().intersected(after)) {
|
||||
list.back().end = tail.start;
|
||||
if (!list.back()) {
|
||||
list.pop_back();
|
||||
}
|
||||
list.insert(begin(list), tail.shifted(-kWeek));
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return intervals;
|
||||
}
|
||||
|
||||
template <typename Flag>
|
||||
auto RecipientsFlags(const BusinessRecipients &data) {
|
||||
const auto &chats = data.allButExcluded
|
||||
? data.excluded
|
||||
: data.included;
|
||||
using Type = BusinessChatType;
|
||||
return Flag()
|
||||
| ((chats.types & Type::NewChats) ? Flag::f_new_chats : Flag())
|
||||
| ((chats.types & Type::ExistingChats)
|
||||
? Flag::f_existing_chats
|
||||
: Flag())
|
||||
| ((chats.types & Type::Contacts) ? Flag::f_contacts : Flag())
|
||||
| ((chats.types & Type::NonContacts) ? Flag::f_non_contacts : Flag())
|
||||
| (chats.list.empty() ? Flag() : Flag::f_users)
|
||||
| (data.allButExcluded ? Flag::f_exclude_selected : Flag());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
BusinessRecipients BusinessRecipients::MakeValid(BusinessRecipients value) {
|
||||
if (value.included.empty()) {
|
||||
value.allButExcluded = true;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
MTPInputBusinessRecipients ForMessagesToMTP(const BusinessRecipients &data) {
|
||||
using Flag = MTPDinputBusinessRecipients::Flag;
|
||||
const auto &chats = data.allButExcluded ? data.excluded : data.included;
|
||||
return MTP_inputBusinessRecipients(
|
||||
MTP_flags(RecipientsFlags<Flag>(data)),
|
||||
MTP_vector_from_range(chats.list
|
||||
| ranges::views::transform(&UserData::inputUser)));
|
||||
}
|
||||
|
||||
MTPInputBusinessBotRecipients ForBotsToMTP(const BusinessRecipients &data) {
|
||||
using Flag = MTPDinputBusinessBotRecipients::Flag;
|
||||
const auto &chats = data.allButExcluded ? data.excluded : data.included;
|
||||
return MTP_inputBusinessBotRecipients(
|
||||
MTP_flags(RecipientsFlags<Flag>(data)
|
||||
| ((data.allButExcluded || data.excluded.empty())
|
||||
? Flag()
|
||||
: Flag::f_exclude_users)),
|
||||
MTP_vector_from_range(chats.list
|
||||
| ranges::views::transform(&UserData::inputUser)),
|
||||
MTP_vector_from_range(data.excluded.list
|
||||
| ranges::views::transform(&UserData::inputUser)));
|
||||
}
|
||||
|
||||
BusinessRecipients FromMTP(
|
||||
not_null<Session*> owner,
|
||||
const MTPBusinessRecipients &recipients) {
|
||||
using Type = BusinessChatType;
|
||||
|
||||
const auto &data = recipients.data();
|
||||
auto result = BusinessRecipients{
|
||||
.allButExcluded = data.is_exclude_selected(),
|
||||
};
|
||||
auto &chats = result.allButExcluded
|
||||
? result.excluded
|
||||
: result.included;
|
||||
chats.types = Type()
|
||||
| (data.is_new_chats() ? Type::NewChats : Type())
|
||||
| (data.is_existing_chats() ? Type::ExistingChats : Type())
|
||||
| (data.is_contacts() ? Type::Contacts : Type())
|
||||
| (data.is_non_contacts() ? Type::NonContacts : Type());
|
||||
if (const auto users = data.vusers()) {
|
||||
for (const auto &userId : users->v) {
|
||||
chats.list.push_back(owner->user(UserId(userId.v)));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
BusinessRecipients FromMTP(
|
||||
not_null<Session*> owner,
|
||||
const MTPBusinessBotRecipients &recipients) {
|
||||
using Type = BusinessChatType;
|
||||
|
||||
const auto &data = recipients.data();
|
||||
auto result = BusinessRecipients{
|
||||
.allButExcluded = data.is_exclude_selected(),
|
||||
};
|
||||
auto &chats = result.allButExcluded
|
||||
? result.excluded
|
||||
: result.included;
|
||||
chats.types = Type()
|
||||
| (data.is_new_chats() ? Type::NewChats : Type())
|
||||
| (data.is_existing_chats() ? Type::ExistingChats : Type())
|
||||
| (data.is_contacts() ? Type::Contacts : Type())
|
||||
| (data.is_non_contacts() ? Type::NonContacts : Type());
|
||||
if (const auto users = data.vusers()) {
|
||||
for (const auto &userId : users->v) {
|
||||
chats.list.push_back(owner->user(UserId(userId.v)));
|
||||
}
|
||||
}
|
||||
if (!result.allButExcluded) {
|
||||
if (const auto excluded = data.vexclude_users()) {
|
||||
for (const auto &userId : excluded->v) {
|
||||
result.excluded.list.push_back(
|
||||
owner->user(UserId(userId.v)));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
ChatbotsPermissions FromMTP(const MTPBusinessBotRights &rights) {
|
||||
using Flag = ChatbotsPermission;
|
||||
const auto &data = rights.data();
|
||||
|
||||
return Flag::ViewMessages
|
||||
| (data.is_reply() ? Flag::ReplyToMessages : Flag())
|
||||
| (data.is_read_messages() ? Flag::MarkAsRead : Flag())
|
||||
| (data.is_delete_sent_messages() ? Flag::DeleteSent : Flag())
|
||||
| (data.is_delete_received_messages() ? Flag::DeleteReceived : Flag())
|
||||
| (data.is_edit_name() ? Flag::EditName : Flag())
|
||||
| (data.is_edit_bio() ? Flag::EditBio : Flag())
|
||||
| (data.is_edit_profile_photo() ? Flag::EditUserpic : Flag())
|
||||
| (data.is_edit_username() ? Flag::EditUsername : Flag())
|
||||
| (data.is_view_gifts() ? Flag::ViewGifts : Flag())
|
||||
| (data.is_sell_gifts() ? Flag::SellGifts : Flag())
|
||||
| (data.is_change_gift_settings() ? Flag::GiftSettings : Flag())
|
||||
| (data.is_transfer_and_upgrade_gifts() ? Flag::TransferGifts : Flag())
|
||||
| (data.is_transfer_stars() ? Flag::TransferStars : Flag())
|
||||
| (data.is_manage_stories() ? Flag::ManageStories : Flag());
|
||||
}
|
||||
|
||||
MTPBusinessBotRights ToMTP(ChatbotsPermissions rights) {
|
||||
using Flag = MTPDbusinessBotRights::Flag;
|
||||
using Right = ChatbotsPermission;
|
||||
return MTP_businessBotRights(MTP_flags(Flag()
|
||||
| ((rights & Right::ReplyToMessages) ? Flag::f_reply : Flag())
|
||||
| ((rights & Right::MarkAsRead) ? Flag::f_read_messages : Flag())
|
||||
| ((rights & Right::DeleteSent) ? Flag::f_delete_sent_messages : Flag())
|
||||
| ((rights & Right::DeleteReceived) ? Flag::f_delete_received_messages : Flag())
|
||||
| ((rights & Right::EditName) ? Flag::f_edit_name : Flag())
|
||||
| ((rights & Right::EditBio) ? Flag::f_edit_bio : Flag())
|
||||
| ((rights & Right::EditUserpic) ? Flag::f_edit_profile_photo : Flag())
|
||||
| ((rights & Right::EditUsername) ? Flag::f_edit_username : Flag())
|
||||
| ((rights & Right::ViewGifts) ? Flag::f_view_gifts : Flag())
|
||||
| ((rights & Right::SellGifts) ? Flag::f_sell_gifts : Flag())
|
||||
| ((rights & Right::GiftSettings) ? Flag::f_change_gift_settings : Flag())
|
||||
| ((rights & Right::TransferGifts) ? Flag::f_transfer_and_upgrade_gifts : Flag())
|
||||
| ((rights & Right::TransferStars) ? Flag::f_transfer_stars : Flag())
|
||||
| ((rights & Right::ManageStories) ? Flag::f_manage_stories : Flag())));
|
||||
}
|
||||
|
||||
BusinessDetails FromMTP(
|
||||
not_null<Session*> owner,
|
||||
const tl::conditional<MTPBusinessWorkHours> &hours,
|
||||
const tl::conditional<MTPBusinessLocation> &location,
|
||||
const tl::conditional<MTPBusinessIntro> &intro) {
|
||||
auto result = BusinessDetails();
|
||||
if (hours) {
|
||||
const auto &data = hours->data();
|
||||
result.hours.timezoneId = qs(data.vtimezone_id());
|
||||
result.hours.intervals.list = ranges::views::all(
|
||||
data.vweekly_open().v
|
||||
) | ranges::views::transform([](const MTPBusinessWeeklyOpen &open) {
|
||||
const auto &data = open.data();
|
||||
return WorkingInterval{
|
||||
data.vstart_minute().v * 60,
|
||||
data.vend_minute().v * 60,
|
||||
};
|
||||
}) | ranges::to_vector;
|
||||
}
|
||||
if (location) {
|
||||
const auto &data = location->data();
|
||||
result.location.address = qs(data.vaddress());
|
||||
if (const auto point = data.vgeo_point()) {
|
||||
point->match([&](const MTPDgeoPoint &data) {
|
||||
result.location.point = LocationPoint(data);
|
||||
}, [&](const MTPDgeoPointEmpty &) {
|
||||
});
|
||||
}
|
||||
}
|
||||
if (intro) {
|
||||
const auto &data = intro->data();
|
||||
result.intro.title = qs(data.vtitle());
|
||||
result.intro.description = qs(data.vdescription());
|
||||
if (const auto document = data.vsticker()) {
|
||||
result.intro.sticker = owner->processDocument(*document);
|
||||
if (!result.intro.sticker->sticker()) {
|
||||
result.intro.sticker = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] AwaySettings FromMTP(
|
||||
not_null<Session*> owner,
|
||||
const tl::conditional<MTPBusinessAwayMessage> &message) {
|
||||
if (!message) {
|
||||
return AwaySettings();
|
||||
}
|
||||
const auto &data = message->data();
|
||||
auto result = AwaySettings{
|
||||
.recipients = FromMTP(owner, data.vrecipients()),
|
||||
.shortcutId = data.vshortcut_id().v,
|
||||
.offlineOnly = data.is_offline_only(),
|
||||
};
|
||||
data.vschedule().match([&](
|
||||
const MTPDbusinessAwayMessageScheduleAlways &) {
|
||||
result.schedule.type = AwayScheduleType::Always;
|
||||
}, [&](const MTPDbusinessAwayMessageScheduleOutsideWorkHours &) {
|
||||
result.schedule.type = AwayScheduleType::OutsideWorkingHours;
|
||||
}, [&](const MTPDbusinessAwayMessageScheduleCustom &data) {
|
||||
result.schedule.type = AwayScheduleType::Custom;
|
||||
result.schedule.customInterval = WorkingInterval{
|
||||
data.vstart_date().v,
|
||||
data.vend_date().v,
|
||||
};
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] GreetingSettings FromMTP(
|
||||
not_null<Session*> owner,
|
||||
const tl::conditional<MTPBusinessGreetingMessage> &message) {
|
||||
if (!message) {
|
||||
return GreetingSettings();
|
||||
}
|
||||
const auto &data = message->data();
|
||||
return GreetingSettings{
|
||||
.recipients = FromMTP(owner, data.vrecipients()),
|
||||
.noActivityDays = data.vno_activity_days().v,
|
||||
.shortcutId = data.vshortcut_id().v,
|
||||
};
|
||||
}
|
||||
|
||||
WorkingIntervals WorkingIntervals::normalized() const {
|
||||
return SortAndMerge(MoveTailToFront(SortAndMerge(*this)));
|
||||
}
|
||||
|
||||
WorkingIntervals ExtractDayIntervals(
|
||||
const WorkingIntervals &intervals,
|
||||
int dayIndex) {
|
||||
Expects(dayIndex >= 0 && dayIndex < 7);
|
||||
|
||||
auto result = WorkingIntervals();
|
||||
auto &list = result.list;
|
||||
for (const auto &interval : intervals.list) {
|
||||
const auto now = interval.intersected(
|
||||
{ (dayIndex - 1) * kDay, (dayIndex + 2) * kDay });
|
||||
const auto after = interval.intersected(
|
||||
{ (dayIndex + 6) * kDay, (dayIndex + 9) * kDay });
|
||||
const auto before = interval.intersected(
|
||||
{ (dayIndex - 8) * kDay, (dayIndex - 5) * kDay });
|
||||
if (now) {
|
||||
list.push_back(now.shifted(-dayIndex * kDay));
|
||||
}
|
||||
if (after) {
|
||||
list.push_back(after.shifted(-(dayIndex + 7) * kDay));
|
||||
}
|
||||
if (before) {
|
||||
list.push_back(before.shifted(-(dayIndex - 7) * kDay));
|
||||
}
|
||||
}
|
||||
result = result.normalized();
|
||||
|
||||
const auto outside = [&](WorkingInterval interval) {
|
||||
return (interval.end <= 0) || (interval.start >= kDay);
|
||||
};
|
||||
list.erase(ranges::remove_if(list, outside), end(list));
|
||||
|
||||
if (!list.empty() && list.back().start <= 0 && list.back().end >= kDay) {
|
||||
list.back() = { 0, kDay };
|
||||
} else if (!list.empty() && (list.back().end > kDay + kInNextDayMax)) {
|
||||
list.back() = list.back().intersected({ 0, kDay });
|
||||
}
|
||||
if (!list.empty() && list.front().start <= 0) {
|
||||
if (list.front().start < 0
|
||||
&& list.front().end <= kInNextDayMax
|
||||
&& list.front().start > -kDay) {
|
||||
list.erase(begin(list));
|
||||
} else {
|
||||
list.front() = list.front().intersected({ 0, kDay });
|
||||
if (!list.front()) {
|
||||
list.erase(begin(list));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool IsFullOpen(const WorkingIntervals &extractedDay) {
|
||||
return extractedDay // 00:00-23:59 or 00:00-00:00 (next day)
|
||||
&& (extractedDay.list.front() == WorkingInterval{ 0, kDay - 60 }
|
||||
|| extractedDay.list.front() == WorkingInterval{ 0, kDay });
|
||||
}
|
||||
|
||||
WorkingIntervals RemoveDayIntervals(
|
||||
const WorkingIntervals &intervals,
|
||||
int dayIndex) {
|
||||
auto result = intervals.normalized();
|
||||
auto &list = result.list;
|
||||
const auto day = WorkingInterval{ 0, kDay };
|
||||
const auto shifted = day.shifted(dayIndex * kDay);
|
||||
auto before = WorkingInterval{ 0, shifted.start };
|
||||
auto after = WorkingInterval{ shifted.end, kWeek };
|
||||
for (auto i = 0, count = int(list.size()); i != count; ++i) {
|
||||
if (list[i].end <= shifted.start || list[i].start >= shifted.end) {
|
||||
continue;
|
||||
} else if (list[i].end <= shifted.start + kInNextDayMax
|
||||
&& (list[i].start < shifted.start
|
||||
|| (!dayIndex // This 'Sunday' finishing on next day <= 6:00.
|
||||
&& list[i].start == shifted.start
|
||||
&& list.back().end >= kWeek))) {
|
||||
continue;
|
||||
} else if (const auto first = list[i].intersected(before)) {
|
||||
list[i] = first;
|
||||
if (const auto second = list[i].intersected(after)) {
|
||||
list.push_back(second);
|
||||
}
|
||||
} else if (const auto second = list[i].intersected(after)) {
|
||||
list[i] = second;
|
||||
} else {
|
||||
list.erase(list.begin() + i);
|
||||
--i;
|
||||
--count;
|
||||
}
|
||||
}
|
||||
return result.normalized();
|
||||
}
|
||||
|
||||
WorkingIntervals ReplaceDayIntervals(
|
||||
const WorkingIntervals &intervals,
|
||||
int dayIndex,
|
||||
WorkingIntervals replacement) {
|
||||
auto result = RemoveDayIntervals(intervals, dayIndex);
|
||||
const auto first = result.list.insert(
|
||||
end(result.list),
|
||||
begin(replacement.list),
|
||||
end(replacement.list));
|
||||
for (auto &interval : ranges::make_subrange(first, end(result.list))) {
|
||||
interval = interval.shifted(dayIndex * kDay);
|
||||
}
|
||||
return result.normalized();
|
||||
}
|
||||
|
||||
} // namespace Data
|
||||
302
Telegram/SourceFiles/data/business/data_business_common.h
Normal file
302
Telegram/SourceFiles/data/business/data_business_common.h
Normal file
@@ -0,0 +1,302 @@
|
||||
/*
|
||||
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/flags.h"
|
||||
#include "data/data_location.h"
|
||||
|
||||
class UserData;
|
||||
|
||||
namespace Data {
|
||||
|
||||
class Session;
|
||||
|
||||
enum class BusinessChatType {
|
||||
NewChats = (1 << 0),
|
||||
ExistingChats = (1 << 1),
|
||||
Contacts = (1 << 2),
|
||||
NonContacts = (1 << 3),
|
||||
};
|
||||
inline constexpr bool is_flag_type(BusinessChatType) { return true; }
|
||||
|
||||
using BusinessChatTypes = base::flags<BusinessChatType>;
|
||||
|
||||
struct BusinessChats {
|
||||
BusinessChatTypes types;
|
||||
std::vector<not_null<UserData*>> list;
|
||||
|
||||
[[nodiscard]] bool empty() const {
|
||||
return !types && list.empty();
|
||||
}
|
||||
|
||||
friend inline bool operator==(
|
||||
const BusinessChats &a,
|
||||
const BusinessChats &b) = default;
|
||||
};
|
||||
|
||||
struct BusinessRecipients {
|
||||
BusinessChats included;
|
||||
BusinessChats excluded;
|
||||
bool allButExcluded = false;
|
||||
|
||||
[[nodiscard]] static BusinessRecipients MakeValid(
|
||||
BusinessRecipients value);
|
||||
|
||||
friend inline bool operator==(
|
||||
const BusinessRecipients &a,
|
||||
const BusinessRecipients &b) = default;
|
||||
};
|
||||
|
||||
enum class BusinessRecipientsType : uchar {
|
||||
Messages,
|
||||
Bots,
|
||||
};
|
||||
|
||||
enum class ChatbotsPermission {
|
||||
ViewMessages = 0x0001,
|
||||
ReplyToMessages = 0x0002,
|
||||
MarkAsRead = 0x0004,
|
||||
DeleteSent = 0x0008,
|
||||
DeleteReceived = 0x0010,
|
||||
EditName = 0x0020,
|
||||
EditBio = 0x0040,
|
||||
EditUserpic = 0x0080,
|
||||
EditUsername = 0x0100,
|
||||
ViewGifts = 0x0200,
|
||||
SellGifts = 0x0400,
|
||||
GiftSettings = 0x0800,
|
||||
TransferGifts = 0x1000,
|
||||
TransferStars = 0x2000,
|
||||
ManageStories = 0x4000,
|
||||
};
|
||||
inline constexpr bool is_flag_type(ChatbotsPermission) { return true; }
|
||||
using ChatbotsPermissions = base::flags<ChatbotsPermission>;
|
||||
|
||||
[[nodiscard]] MTPInputBusinessRecipients ForMessagesToMTP(
|
||||
const BusinessRecipients &data);
|
||||
[[nodiscard]] MTPInputBusinessBotRecipients ForBotsToMTP(
|
||||
const BusinessRecipients &data);
|
||||
[[nodiscard]] BusinessRecipients FromMTP(
|
||||
not_null<Session*> owner,
|
||||
const MTPBusinessRecipients &recipients);
|
||||
[[nodiscard]] BusinessRecipients FromMTP(
|
||||
not_null<Session*> owner,
|
||||
const MTPBusinessBotRecipients &recipients);
|
||||
[[nodiscard]] ChatbotsPermissions FromMTP(
|
||||
const MTPBusinessBotRights &rights);
|
||||
[[nodiscard]] MTPBusinessBotRights ToMTP(ChatbotsPermissions rights);
|
||||
|
||||
struct Timezone {
|
||||
QString id;
|
||||
QString name;
|
||||
TimeId utcOffset = 0;
|
||||
|
||||
friend inline bool operator==(
|
||||
const Timezone &a,
|
||||
const Timezone &b) = default;
|
||||
};
|
||||
|
||||
struct Timezones {
|
||||
std::vector<Timezone> list;
|
||||
|
||||
friend inline bool operator==(
|
||||
const Timezones &a,
|
||||
const Timezones &b) = default;
|
||||
};;
|
||||
|
||||
struct WorkingInterval {
|
||||
static constexpr auto kDay = 24 * 3600;
|
||||
static constexpr auto kWeek = 7 * kDay;
|
||||
static constexpr auto kInNextDayMax = 6 * 3600;
|
||||
|
||||
TimeId start = 0;
|
||||
TimeId end = 0;
|
||||
|
||||
explicit operator bool() const {
|
||||
return start < end;
|
||||
}
|
||||
|
||||
[[nodiscard]] WorkingInterval shifted(TimeId offset) const {
|
||||
return { start + offset, end + offset };
|
||||
}
|
||||
[[nodiscard]] WorkingInterval united(WorkingInterval other) const {
|
||||
if (!*this) {
|
||||
return other;
|
||||
} else if (!other) {
|
||||
return *this;
|
||||
}
|
||||
return {
|
||||
std::min(start, other.start),
|
||||
std::max(end, other.end),
|
||||
};
|
||||
}
|
||||
[[nodiscard]] WorkingInterval intersected(WorkingInterval other) const {
|
||||
const auto result = WorkingInterval{
|
||||
std::max(start, other.start),
|
||||
std::min(end, other.end),
|
||||
};
|
||||
return result ? result : WorkingInterval();
|
||||
}
|
||||
|
||||
friend inline bool operator==(
|
||||
const WorkingInterval &a,
|
||||
const WorkingInterval &b) = default;
|
||||
};
|
||||
|
||||
struct WorkingIntervals {
|
||||
std::vector<WorkingInterval> list;
|
||||
|
||||
[[nodiscard]] WorkingIntervals normalized() const;
|
||||
|
||||
explicit operator bool() const {
|
||||
for (const auto &interval : list) {
|
||||
if (interval) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
friend inline bool operator==(
|
||||
const WorkingIntervals &a,
|
||||
const WorkingIntervals &b) = default;
|
||||
};
|
||||
|
||||
struct WorkingHours {
|
||||
WorkingIntervals intervals;
|
||||
QString timezoneId;
|
||||
|
||||
[[nodiscard]] WorkingHours normalized() const {
|
||||
return { intervals.normalized(), timezoneId };
|
||||
}
|
||||
|
||||
explicit operator bool() const {
|
||||
return !timezoneId.isEmpty() && !intervals.list.empty();
|
||||
}
|
||||
|
||||
friend inline bool operator==(
|
||||
const WorkingHours &a,
|
||||
const WorkingHours &b) = default;
|
||||
};
|
||||
|
||||
[[nodiscard]] WorkingIntervals ExtractDayIntervals(
|
||||
const WorkingIntervals &intervals,
|
||||
int dayIndex);
|
||||
[[nodiscard]] bool IsFullOpen(const WorkingIntervals &extractedDay);
|
||||
[[nodiscard]] WorkingIntervals RemoveDayIntervals(
|
||||
const WorkingIntervals &intervals,
|
||||
int dayIndex);
|
||||
[[nodiscard]] WorkingIntervals ReplaceDayIntervals(
|
||||
const WorkingIntervals &intervals,
|
||||
int dayIndex,
|
||||
WorkingIntervals replacement);
|
||||
|
||||
struct BusinessLocation {
|
||||
QString address;
|
||||
std::optional<LocationPoint> point;
|
||||
|
||||
explicit operator bool() const {
|
||||
return !address.isEmpty();
|
||||
}
|
||||
|
||||
friend inline bool operator==(
|
||||
const BusinessLocation &a,
|
||||
const BusinessLocation &b) = default;
|
||||
};
|
||||
|
||||
struct ChatIntro {
|
||||
QString title;
|
||||
QString description;
|
||||
DocumentData *sticker = nullptr;
|
||||
|
||||
[[nodiscard]] bool customPhrases() const {
|
||||
return !title.isEmpty() || !description.isEmpty();
|
||||
}
|
||||
|
||||
explicit operator bool() const {
|
||||
return customPhrases() || sticker;
|
||||
}
|
||||
|
||||
friend inline bool operator==(
|
||||
const ChatIntro &a,
|
||||
const ChatIntro &b) = default;
|
||||
};
|
||||
|
||||
struct BusinessDetails {
|
||||
WorkingHours hours;
|
||||
BusinessLocation location;
|
||||
ChatIntro intro;
|
||||
|
||||
explicit operator bool() const {
|
||||
return hours || location || intro;
|
||||
}
|
||||
|
||||
friend inline bool operator==(
|
||||
const BusinessDetails &a,
|
||||
const BusinessDetails &b) = default;
|
||||
};
|
||||
|
||||
[[nodiscard]] BusinessDetails FromMTP(
|
||||
not_null<Session*> owner,
|
||||
const tl::conditional<MTPBusinessWorkHours> &hours,
|
||||
const tl::conditional<MTPBusinessLocation> &location,
|
||||
const tl::conditional<MTPBusinessIntro> &intro);
|
||||
|
||||
enum class AwayScheduleType : uchar {
|
||||
Never = 0,
|
||||
Always = 1,
|
||||
OutsideWorkingHours = 2,
|
||||
Custom = 3,
|
||||
};
|
||||
|
||||
struct AwaySchedule {
|
||||
AwayScheduleType type = AwayScheduleType::Never;
|
||||
WorkingInterval customInterval;
|
||||
|
||||
friend inline bool operator==(
|
||||
const AwaySchedule &a,
|
||||
const AwaySchedule &b) = default;
|
||||
};
|
||||
|
||||
struct AwaySettings {
|
||||
BusinessRecipients recipients;
|
||||
AwaySchedule schedule;
|
||||
BusinessShortcutId shortcutId = 0;
|
||||
bool offlineOnly = false;
|
||||
|
||||
explicit operator bool() const {
|
||||
return schedule.type != AwayScheduleType::Never;
|
||||
}
|
||||
|
||||
friend inline bool operator==(
|
||||
const AwaySettings &a,
|
||||
const AwaySettings &b) = default;
|
||||
};
|
||||
|
||||
[[nodiscard]] AwaySettings FromMTP(
|
||||
not_null<Session*> owner,
|
||||
const tl::conditional<MTPBusinessAwayMessage> &message);
|
||||
|
||||
struct GreetingSettings {
|
||||
BusinessRecipients recipients;
|
||||
int noActivityDays = 0;
|
||||
BusinessShortcutId shortcutId = 0;
|
||||
|
||||
explicit operator bool() const {
|
||||
return noActivityDays > 0;
|
||||
}
|
||||
|
||||
friend inline bool operator==(
|
||||
const GreetingSettings &a,
|
||||
const GreetingSettings &b) = default;
|
||||
};
|
||||
|
||||
[[nodiscard]] GreetingSettings FromMTP(
|
||||
not_null<Session*> owner,
|
||||
const tl::conditional<MTPBusinessGreetingMessage> &message);
|
||||
|
||||
} // namespace Data
|
||||
318
Telegram/SourceFiles/data/business/data_business_info.cpp
Normal file
318
Telegram/SourceFiles/data/business/data_business_info.cpp
Normal file
@@ -0,0 +1,318 @@
|
||||
/*
|
||||
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/business/data_business_info.h"
|
||||
|
||||
#include "apiwrap.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "data/business/data_business_common.h"
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_user.h"
|
||||
#include "main/main_session.h"
|
||||
|
||||
namespace Data {
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] MTPBusinessWorkHours ToMTP(const WorkingHours &data) {
|
||||
const auto list = data.intervals.normalized().list;
|
||||
const auto proj = [](const WorkingInterval &data) {
|
||||
return MTPBusinessWeeklyOpen(MTP_businessWeeklyOpen(
|
||||
MTP_int(data.start / 60),
|
||||
MTP_int(data.end / 60)));
|
||||
};
|
||||
return MTP_businessWorkHours(
|
||||
MTP_flags(0),
|
||||
MTP_string(data.timezoneId),
|
||||
MTP_vector_from_range(list | ranges::views::transform(proj)));
|
||||
}
|
||||
|
||||
[[nodiscard]] MTPBusinessAwayMessageSchedule ToMTP(
|
||||
const AwaySchedule &data) {
|
||||
Expects(data.type != AwayScheduleType::Never);
|
||||
|
||||
return (data.type == AwayScheduleType::Always)
|
||||
? MTP_businessAwayMessageScheduleAlways()
|
||||
: (data.type == AwayScheduleType::OutsideWorkingHours)
|
||||
? MTP_businessAwayMessageScheduleOutsideWorkHours()
|
||||
: MTP_businessAwayMessageScheduleCustom(
|
||||
MTP_int(data.customInterval.start),
|
||||
MTP_int(data.customInterval.end));
|
||||
}
|
||||
|
||||
[[nodiscard]] MTPInputBusinessAwayMessage ToMTP(const AwaySettings &data) {
|
||||
using Flag = MTPDinputBusinessAwayMessage::Flag;
|
||||
return MTP_inputBusinessAwayMessage(
|
||||
MTP_flags(data.offlineOnly ? Flag::f_offline_only : Flag()),
|
||||
MTP_int(data.shortcutId),
|
||||
ToMTP(data.schedule),
|
||||
ForMessagesToMTP(data.recipients));
|
||||
}
|
||||
|
||||
[[nodiscard]] MTPInputBusinessGreetingMessage ToMTP(
|
||||
const GreetingSettings &data) {
|
||||
return MTP_inputBusinessGreetingMessage(
|
||||
MTP_int(data.shortcutId),
|
||||
ForMessagesToMTP(data.recipients),
|
||||
MTP_int(data.noActivityDays));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
BusinessInfo::BusinessInfo(not_null<Session*> owner)
|
||||
: _owner(owner) {
|
||||
}
|
||||
|
||||
BusinessInfo::~BusinessInfo() = default;
|
||||
|
||||
void BusinessInfo::saveWorkingHours(
|
||||
WorkingHours data,
|
||||
Fn<void(QString)> fail) {
|
||||
const auto session = &_owner->session();
|
||||
auto details = session->user()->businessDetails();
|
||||
const auto &was = details.hours;
|
||||
if (was == data) {
|
||||
return;
|
||||
}
|
||||
|
||||
using Flag = MTPaccount_UpdateBusinessWorkHours::Flag;
|
||||
session->api().request(MTPaccount_UpdateBusinessWorkHours(
|
||||
MTP_flags(data ? Flag::f_business_work_hours : Flag()),
|
||||
ToMTP(data)
|
||||
)).fail([=](const MTP::Error &error) {
|
||||
auto details = session->user()->businessDetails();
|
||||
details.hours = was;
|
||||
session->user()->setBusinessDetails(std::move(details));
|
||||
if (fail) {
|
||||
fail(error.type());
|
||||
}
|
||||
}).send();
|
||||
|
||||
details.hours = std::move(data);
|
||||
session->user()->setBusinessDetails(std::move(details));
|
||||
}
|
||||
|
||||
void BusinessInfo::saveChatIntro(ChatIntro data, Fn<void(QString)> fail) {
|
||||
const auto session = &_owner->session();
|
||||
auto details = session->user()->businessDetails();
|
||||
const auto &was = details.intro;
|
||||
if (was == data) {
|
||||
return;
|
||||
} else {
|
||||
const auto session = &_owner->session();
|
||||
using Flag = MTPaccount_UpdateBusinessIntro::Flag;
|
||||
session->api().request(MTPaccount_UpdateBusinessIntro(
|
||||
MTP_flags(data ? Flag::f_intro : Flag()),
|
||||
MTP_inputBusinessIntro(
|
||||
MTP_flags(data.sticker
|
||||
? MTPDinputBusinessIntro::Flag::f_sticker
|
||||
: MTPDinputBusinessIntro::Flag()),
|
||||
MTP_string(data.title),
|
||||
MTP_string(data.description),
|
||||
(data.sticker
|
||||
? data.sticker->mtpInput()
|
||||
: MTP_inputDocumentEmpty()))
|
||||
)).fail([=](const MTP::Error &error) {
|
||||
auto details = session->user()->businessDetails();
|
||||
details.intro = was;
|
||||
session->user()->setBusinessDetails(std::move(details));
|
||||
if (fail) {
|
||||
fail(error.type());
|
||||
}
|
||||
}).send();
|
||||
}
|
||||
|
||||
details.intro = std::move(data);
|
||||
session->user()->setBusinessDetails(std::move(details));
|
||||
}
|
||||
|
||||
void BusinessInfo::saveLocation(
|
||||
BusinessLocation data,
|
||||
Fn<void(QString)> fail) {
|
||||
const auto session = &_owner->session();
|
||||
auto details = session->user()->businessDetails();
|
||||
const auto &was = details.location;
|
||||
if (was == data) {
|
||||
return;
|
||||
} else {
|
||||
const auto session = &_owner->session();
|
||||
using Flag = MTPaccount_UpdateBusinessLocation::Flag;
|
||||
session->api().request(MTPaccount_UpdateBusinessLocation(
|
||||
MTP_flags((data.point ? Flag::f_geo_point : Flag())
|
||||
| (data.address.isEmpty() ? Flag() : Flag::f_address)),
|
||||
(data.point
|
||||
? MTP_inputGeoPoint(
|
||||
MTP_flags(0),
|
||||
MTP_double(data.point->lat()),
|
||||
MTP_double(data.point->lon()),
|
||||
MTPint()) // accuracy_radius
|
||||
: MTP_inputGeoPointEmpty()),
|
||||
MTP_string(data.address)
|
||||
)).fail([=](const MTP::Error &error) {
|
||||
auto details = session->user()->businessDetails();
|
||||
details.location = was;
|
||||
session->user()->setBusinessDetails(std::move(details));
|
||||
if (fail) {
|
||||
fail(error.type());
|
||||
}
|
||||
}).send();
|
||||
}
|
||||
|
||||
details.location = std::move(data);
|
||||
session->user()->setBusinessDetails(std::move(details));
|
||||
}
|
||||
|
||||
void BusinessInfo::applyAwaySettings(AwaySettings data) {
|
||||
if (_awaySettings == data) {
|
||||
return;
|
||||
}
|
||||
_awaySettings = data;
|
||||
_awaySettingsChanged.fire({});
|
||||
}
|
||||
|
||||
void BusinessInfo::saveAwaySettings(
|
||||
AwaySettings data,
|
||||
Fn<void(QString)> fail) {
|
||||
const auto &was = _awaySettings;
|
||||
if (was == data) {
|
||||
return;
|
||||
} else if (!data || data.shortcutId) {
|
||||
using Flag = MTPaccount_UpdateBusinessAwayMessage::Flag;
|
||||
const auto session = &_owner->session();
|
||||
session->api().request(MTPaccount_UpdateBusinessAwayMessage(
|
||||
MTP_flags(data ? Flag::f_message : Flag()),
|
||||
data ? ToMTP(data) : MTPInputBusinessAwayMessage()
|
||||
)).fail([=](const MTP::Error &error) {
|
||||
_awaySettings = was;
|
||||
_awaySettingsChanged.fire({});
|
||||
if (fail) {
|
||||
fail(error.type());
|
||||
}
|
||||
}).send();
|
||||
}
|
||||
_awaySettings = std::move(data);
|
||||
_awaySettingsChanged.fire({});
|
||||
}
|
||||
|
||||
bool BusinessInfo::awaySettingsLoaded() const {
|
||||
return _awaySettings.has_value();
|
||||
}
|
||||
|
||||
AwaySettings BusinessInfo::awaySettings() const {
|
||||
return _awaySettings.value_or(AwaySettings());
|
||||
}
|
||||
|
||||
rpl::producer<> BusinessInfo::awaySettingsChanged() const {
|
||||
return _awaySettingsChanged.events();
|
||||
}
|
||||
|
||||
void BusinessInfo::applyGreetingSettings(GreetingSettings data) {
|
||||
if (_greetingSettings == data) {
|
||||
return;
|
||||
}
|
||||
_greetingSettings = data;
|
||||
_greetingSettingsChanged.fire({});
|
||||
}
|
||||
|
||||
void BusinessInfo::saveGreetingSettings(
|
||||
GreetingSettings data,
|
||||
Fn<void(QString)> fail) {
|
||||
const auto &was = _greetingSettings;
|
||||
if (was == data) {
|
||||
return;
|
||||
} else if (!data || data.shortcutId) {
|
||||
using Flag = MTPaccount_UpdateBusinessGreetingMessage::Flag;
|
||||
_owner->session().api().request(
|
||||
MTPaccount_UpdateBusinessGreetingMessage(
|
||||
MTP_flags(data ? Flag::f_message : Flag()),
|
||||
data ? ToMTP(data) : MTPInputBusinessGreetingMessage())
|
||||
).fail([=](const MTP::Error &error) {
|
||||
_greetingSettings = was;
|
||||
_greetingSettingsChanged.fire({});
|
||||
if (fail) {
|
||||
fail(error.type());
|
||||
}
|
||||
}).send();
|
||||
}
|
||||
_greetingSettings = std::move(data);
|
||||
_greetingSettingsChanged.fire({});
|
||||
}
|
||||
|
||||
bool BusinessInfo::greetingSettingsLoaded() const {
|
||||
return _greetingSettings.has_value();
|
||||
}
|
||||
|
||||
GreetingSettings BusinessInfo::greetingSettings() const {
|
||||
return _greetingSettings.value_or(GreetingSettings());
|
||||
}
|
||||
|
||||
rpl::producer<> BusinessInfo::greetingSettingsChanged() const {
|
||||
return _greetingSettingsChanged.events();
|
||||
}
|
||||
|
||||
void BusinessInfo::preload() {
|
||||
preloadTimezones();
|
||||
}
|
||||
|
||||
void BusinessInfo::preloadTimezones() {
|
||||
if (!_timezones.current().list.empty() || _timezonesRequestId) {
|
||||
return;
|
||||
}
|
||||
_timezonesRequestId = _owner->session().api().request(
|
||||
MTPhelp_GetTimezonesList(MTP_int(_timezonesHash))
|
||||
).done([=](const MTPhelp_TimezonesList &result) {
|
||||
result.match([&](const MTPDhelp_timezonesList &data) {
|
||||
_timezonesHash = data.vhash().v;
|
||||
const auto proj = [](const MTPtimezone &result) {
|
||||
return Timezone{
|
||||
.id = qs(result.data().vid()),
|
||||
.name = qs(result.data().vname()),
|
||||
.utcOffset = result.data().vutc_offset().v,
|
||||
};
|
||||
};
|
||||
_timezones = Timezones{
|
||||
.list = ranges::views::all(
|
||||
data.vtimezones().v
|
||||
) | ranges::views::transform(
|
||||
proj
|
||||
) | ranges::to_vector,
|
||||
};
|
||||
}, [](const MTPDhelp_timezonesListNotModified &) {
|
||||
});
|
||||
}).send();
|
||||
}
|
||||
|
||||
rpl::producer<Timezones> BusinessInfo::timezonesValue() const {
|
||||
const_cast<BusinessInfo*>(this)->preloadTimezones();
|
||||
return _timezones.value();
|
||||
}
|
||||
|
||||
bool BusinessInfo::timezonesLoaded() const {
|
||||
return !_timezones.current().list.empty();
|
||||
}
|
||||
|
||||
QString FindClosestTimezoneId(const std::vector<Timezone> &list) {
|
||||
const auto local = QDateTime::currentDateTime();
|
||||
const auto utc = QDateTime(local.date(), local.time(), Qt::UTC);
|
||||
const auto shift = base::unixtime::now() - (TimeId)::time(nullptr);
|
||||
const auto delta = int(utc.toSecsSinceEpoch())
|
||||
- int(local.toSecsSinceEpoch())
|
||||
- shift;
|
||||
const auto proj = [&](const Timezone &value) {
|
||||
auto distance = value.utcOffset - delta;
|
||||
while (distance > 12 * 3600) {
|
||||
distance -= 24 * 3600;
|
||||
}
|
||||
while (distance < -12 * 3600) {
|
||||
distance += 24 * 3600;
|
||||
}
|
||||
return std::abs(distance);
|
||||
};
|
||||
return ranges::min_element(list, ranges::less(), proj)->id;
|
||||
}
|
||||
|
||||
} // namespace Data
|
||||
64
Telegram/SourceFiles/data/business/data_business_info.h
Normal file
64
Telegram/SourceFiles/data/business/data_business_info.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
|
||||
|
||||
#include "data/business/data_business_common.h"
|
||||
|
||||
namespace Data {
|
||||
|
||||
class Session;
|
||||
|
||||
class BusinessInfo final {
|
||||
public:
|
||||
explicit BusinessInfo(not_null<Session*> owner);
|
||||
~BusinessInfo();
|
||||
|
||||
void preload();
|
||||
|
||||
void saveWorkingHours(WorkingHours data, Fn<void(QString)> fail);
|
||||
void saveChatIntro(ChatIntro data, Fn<void(QString)> fail);
|
||||
void saveLocation(BusinessLocation data, Fn<void(QString)> fail);
|
||||
|
||||
void saveAwaySettings(AwaySettings data, Fn<void(QString)> fail);
|
||||
void applyAwaySettings(AwaySettings data);
|
||||
[[nodiscard]] AwaySettings awaySettings() const;
|
||||
[[nodiscard]] bool awaySettingsLoaded() const;
|
||||
[[nodiscard]] rpl::producer<> awaySettingsChanged() const;
|
||||
|
||||
void saveGreetingSettings(
|
||||
GreetingSettings data,
|
||||
Fn<void(QString)> fail);
|
||||
void applyGreetingSettings(GreetingSettings data);
|
||||
[[nodiscard]] GreetingSettings greetingSettings() const;
|
||||
[[nodiscard]] bool greetingSettingsLoaded() const;
|
||||
[[nodiscard]] rpl::producer<> greetingSettingsChanged() const;
|
||||
|
||||
void preloadTimezones();
|
||||
[[nodiscard]] bool timezonesLoaded() const;
|
||||
[[nodiscard]] rpl::producer<Timezones> timezonesValue() const;
|
||||
|
||||
private:
|
||||
const not_null<Session*> _owner;
|
||||
|
||||
rpl::variable<Timezones> _timezones;
|
||||
|
||||
std::optional<AwaySettings> _awaySettings;
|
||||
rpl::event_stream<> _awaySettingsChanged;
|
||||
|
||||
std::optional<GreetingSettings> _greetingSettings;
|
||||
rpl::event_stream<> _greetingSettingsChanged;
|
||||
|
||||
mtpRequestId _timezonesRequestId = 0;
|
||||
int32 _timezonesHash = 0;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] QString FindClosestTimezoneId(
|
||||
const std::vector<Timezone> &list);
|
||||
|
||||
} // namespace Data
|
||||
786
Telegram/SourceFiles/data/business/data_shortcut_messages.cpp
Normal file
786
Telegram/SourceFiles/data/business/data_shortcut_messages.cpp
Normal file
@@ -0,0 +1,786 @@
|
||||
/*
|
||||
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/business/data_shortcut_messages.h"
|
||||
|
||||
#include "api/api_hash.h"
|
||||
#include "apiwrap.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_session.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 ScheduledMaxMsgId + id + 1;
|
||||
}
|
||||
|
||||
[[nodiscard]] MsgId LocalToRemoteMsgId(MsgId id) {
|
||||
Expects(IsShortcutMsgId(id));
|
||||
|
||||
return (id - ScheduledMaxMsgId - 1);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool TooEarlyForRequest(crl::time received) {
|
||||
return (received > 0) && (received + kRequestTimeLimit > crl::now());
|
||||
}
|
||||
|
||||
[[nodiscard]] MTPMessage PrepareMessage(
|
||||
BusinessShortcutId shortcutId,
|
||||
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(
|
||||
data.vflags(),
|
||||
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_quick_reply_shortcut_id),
|
||||
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()),
|
||||
MTP_int(shortcutId),
|
||||
MTP_long(data.veffect().value_or_empty()),
|
||||
(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 IsShortcutMsgId(MsgId id) {
|
||||
return (id > ScheduledMaxMsgId) && (id < ShortcutMaxMsgId);
|
||||
}
|
||||
|
||||
ShortcutMessages::ShortcutMessages(not_null<Session*> owner)
|
||||
: _session(&owner->session())
|
||||
, _history(owner->history(_session->userPeerId()))
|
||||
, _clearTimer([=] { clearOldRequests(); }) {
|
||||
owner->itemRemoved(
|
||||
) | rpl::filter([](not_null<const HistoryItem*> item) {
|
||||
return item->isBusinessShortcut();
|
||||
}) | rpl::on_next([=](not_null<const HistoryItem*> item) {
|
||||
remove(item);
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
ShortcutMessages::~ShortcutMessages() {
|
||||
for (const auto &request : _requests) {
|
||||
_session->api().request(request.second.requestId).cancel();
|
||||
}
|
||||
}
|
||||
|
||||
void ShortcutMessages::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);
|
||||
}
|
||||
}
|
||||
|
||||
void ShortcutMessages::updateShortcuts(const QVector<MTPQuickReply> &list) {
|
||||
auto shortcuts = parseShortcuts(list);
|
||||
auto changes = std::vector<ShortcutIdChange>();
|
||||
for (auto &[id, shortcut] : _shortcuts.list) {
|
||||
if (shortcuts.list.contains(id)) {
|
||||
continue;
|
||||
}
|
||||
auto foundId = BusinessShortcutId();
|
||||
for (auto &[realId, real] : shortcuts.list) {
|
||||
if (real.name == shortcut.name) {
|
||||
foundId = realId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (foundId) {
|
||||
mergeMessagesFromTo(id, foundId);
|
||||
changes.push_back({ .oldId = id, .newId = foundId });
|
||||
} else {
|
||||
shortcuts.list.emplace(id, shortcut);
|
||||
}
|
||||
}
|
||||
const auto changed = !_shortcutsLoaded
|
||||
|| (shortcuts != _shortcuts);
|
||||
if (changed) {
|
||||
_shortcuts = std::move(shortcuts);
|
||||
_shortcutsLoaded = true;
|
||||
for (const auto &change : changes) {
|
||||
_shortcutIdChanges.fire_copy(change);
|
||||
}
|
||||
_shortcutsChanged.fire({});
|
||||
} else {
|
||||
Assert(changes.empty());
|
||||
}
|
||||
}
|
||||
|
||||
void ShortcutMessages::mergeMessagesFromTo(
|
||||
BusinessShortcutId fromId,
|
||||
BusinessShortcutId toId) {
|
||||
auto &to = _data[toId];
|
||||
const auto i = _data.find(fromId);
|
||||
if (i == end(_data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto &from = i->second;
|
||||
auto destroy = base::flat_set<not_null<HistoryItem*>>();
|
||||
for (auto &item : from.items) {
|
||||
if (item->isSending() || item->hasFailed()) {
|
||||
item->setRealShortcutId(toId);
|
||||
to.items.push_back(std::move(item));
|
||||
} else {
|
||||
destroy.emplace(item.get());
|
||||
}
|
||||
}
|
||||
for (const auto &item : destroy) {
|
||||
item->destroy();
|
||||
}
|
||||
_data.remove(fromId);
|
||||
|
||||
cancelRequest(fromId);
|
||||
|
||||
_updates.fire_copy(toId);
|
||||
if (!destroy.empty()) {
|
||||
cancelRequest(toId);
|
||||
request(toId);
|
||||
}
|
||||
}
|
||||
|
||||
Shortcuts ShortcutMessages::parseShortcuts(
|
||||
const QVector<MTPQuickReply> &list) const {
|
||||
auto result = Shortcuts();
|
||||
for (const auto &reply : list) {
|
||||
const auto shortcut = parseShortcut(reply);
|
||||
result.list.emplace(shortcut.id, shortcut);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Shortcut ShortcutMessages::parseShortcut(const MTPQuickReply &reply) const {
|
||||
const auto &data = reply.data();
|
||||
return Shortcut{
|
||||
.id = BusinessShortcutId(data.vshortcut_id().v),
|
||||
.count = data.vcount().v,
|
||||
.name = qs(data.vshortcut()),
|
||||
.topMessageId = localMessageId(data.vtop_message().v),
|
||||
};
|
||||
}
|
||||
|
||||
MsgId ShortcutMessages::localMessageId(MsgId remoteId) const {
|
||||
return RemoteToLocalMsgId(remoteId);
|
||||
}
|
||||
|
||||
MsgId ShortcutMessages::lookupId(not_null<const HistoryItem*> item) const {
|
||||
Expects(item->isBusinessShortcut());
|
||||
Expects(!item->isSending());
|
||||
Expects(!item->hasFailed());
|
||||
|
||||
return LocalToRemoteMsgId(item->id);
|
||||
}
|
||||
|
||||
int ShortcutMessages::count(BusinessShortcutId shortcutId) const {
|
||||
const auto i = _data.find(shortcutId);
|
||||
return (i != end(_data)) ? i->second.items.size() : 0;
|
||||
}
|
||||
|
||||
void ShortcutMessages::apply(const MTPDupdateQuickReplies &update) {
|
||||
updateShortcuts(update.vquick_replies().v);
|
||||
scheduleShortcutsReload();
|
||||
}
|
||||
|
||||
void ShortcutMessages::scheduleShortcutsReload() {
|
||||
const auto hasUnknownMessages = [&] {
|
||||
const auto selfId = _session->userPeerId();
|
||||
for (const auto &[id, shortcut] : _shortcuts.list) {
|
||||
if (!_session->data().message({ selfId, shortcut.topMessageId })) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
if (hasUnknownMessages()) {
|
||||
_shortcutsLoaded = false;
|
||||
const auto cancelledId = base::take(_shortcutsRequestId);
|
||||
_session->api().request(cancelledId).cancel();
|
||||
crl::on_main(_session, [=] {
|
||||
if (cancelledId || hasUnknownMessages()) {
|
||||
preloadShortcuts();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void ShortcutMessages::apply(const MTPDupdateNewQuickReply &update) {
|
||||
const auto &reply = update.vquick_reply();
|
||||
auto foundId = BusinessShortcutId();
|
||||
const auto shortcut = parseShortcut(reply);
|
||||
for (auto &[id, existing] : _shortcuts.list) {
|
||||
if (id == shortcut.id) {
|
||||
foundId = id;
|
||||
break;
|
||||
} else if (existing.name == shortcut.name) {
|
||||
foundId = id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (foundId == shortcut.id) {
|
||||
auto &already = _shortcuts.list[shortcut.id];
|
||||
if (already != shortcut) {
|
||||
already = shortcut;
|
||||
_shortcutsChanged.fire({});
|
||||
}
|
||||
return;
|
||||
} else if (foundId) {
|
||||
_shortcuts.list.emplace(shortcut.id, shortcut);
|
||||
mergeMessagesFromTo(foundId, shortcut.id);
|
||||
_shortcuts.list.remove(foundId);
|
||||
_shortcutIdChanges.fire({ foundId, shortcut.id });
|
||||
_shortcutsChanged.fire({});
|
||||
}
|
||||
}
|
||||
|
||||
void ShortcutMessages::apply(const MTPDupdateQuickReplyMessage &update) {
|
||||
const auto &message = update.vmessage();
|
||||
const auto shortcutId = BusinessShortcutIdFromMessage(message);
|
||||
if (!shortcutId) {
|
||||
return;
|
||||
}
|
||||
const auto loaded = _data.contains(shortcutId);
|
||||
auto &list = _data[shortcutId];
|
||||
append(shortcutId, list, message);
|
||||
sort(list);
|
||||
_updates.fire_copy(shortcutId);
|
||||
updateCount(shortcutId);
|
||||
if (!loaded) {
|
||||
request(shortcutId);
|
||||
}
|
||||
}
|
||||
|
||||
void ShortcutMessages::updateCount(BusinessShortcutId shortcutId) {
|
||||
const auto i = _data.find(shortcutId);
|
||||
const auto j = _shortcuts.list.find(shortcutId);
|
||||
if (j == end(_shortcuts.list)) {
|
||||
return;
|
||||
}
|
||||
const auto count = (i != end(_data))
|
||||
? int(i->second.itemById.size())
|
||||
: 0;
|
||||
if (j->second.count != count) {
|
||||
_shortcuts.list[shortcutId].count = count;
|
||||
_shortcutsChanged.fire({});
|
||||
}
|
||||
}
|
||||
|
||||
void ShortcutMessages::apply(
|
||||
const MTPDupdateDeleteQuickReplyMessages &update) {
|
||||
const auto shortcutId = update.vshortcut_id().v;
|
||||
if (!shortcutId) {
|
||||
return;
|
||||
}
|
||||
auto i = _data.find(shortcutId);
|
||||
if (i == end(_data)) {
|
||||
return;
|
||||
}
|
||||
for (const auto &id : update.vmessages().v) {
|
||||
const auto &list = i->second;
|
||||
const auto j = list.itemById.find(id.v);
|
||||
if (j != end(list.itemById)) {
|
||||
j->second->destroy();
|
||||
i = _data.find(shortcutId);
|
||||
if (i == end(_data)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
_updates.fire_copy(shortcutId);
|
||||
updateCount(shortcutId);
|
||||
|
||||
cancelRequest(shortcutId);
|
||||
request(shortcutId);
|
||||
}
|
||||
|
||||
void ShortcutMessages::apply(const MTPDupdateDeleteQuickReply &update) {
|
||||
const auto shortcutId = update.vshortcut_id().v;
|
||||
if (!shortcutId) {
|
||||
return;
|
||||
}
|
||||
auto i = _data.find(shortcutId);
|
||||
while (i != end(_data) && !i->second.itemById.empty()) {
|
||||
i->second.itemById.back().second->destroy();
|
||||
i = _data.find(shortcutId);
|
||||
}
|
||||
_updates.fire_copy(shortcutId);
|
||||
if (_data.contains(shortcutId)) {
|
||||
updateCount(shortcutId);
|
||||
} else {
|
||||
_shortcuts.list.remove(shortcutId);
|
||||
_shortcutIdChanges.fire({ shortcutId, 0 });
|
||||
}
|
||||
}
|
||||
|
||||
void ShortcutMessages::apply(
|
||||
const MTPDupdateMessageID &update,
|
||||
not_null<HistoryItem*> local) {
|
||||
const auto id = update.vid().v;
|
||||
const auto i = _data.find(local->shortcutId());
|
||||
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 ShortcutMessages::appendSending(not_null<HistoryItem*> item) {
|
||||
Expects(item->isSending());
|
||||
Expects(item->isBusinessShortcut());
|
||||
|
||||
const auto shortcutId = item->shortcutId();
|
||||
auto &list = _data[shortcutId];
|
||||
list.items.emplace_back(item);
|
||||
sort(list);
|
||||
_updates.fire_copy(shortcutId);
|
||||
}
|
||||
|
||||
void ShortcutMessages::removeSending(not_null<HistoryItem*> item) {
|
||||
Expects(item->isSending() || item->hasFailed());
|
||||
Expects(item->isBusinessShortcut());
|
||||
|
||||
item->destroy();
|
||||
}
|
||||
|
||||
rpl::producer<> ShortcutMessages::updates(BusinessShortcutId shortcutId) {
|
||||
request(shortcutId);
|
||||
|
||||
return _updates.events(
|
||||
) | rpl::filter([=](BusinessShortcutId value) {
|
||||
return (value == shortcutId);
|
||||
}) | rpl::to_empty;
|
||||
}
|
||||
|
||||
Data::MessagesSlice ShortcutMessages::list(BusinessShortcutId shortcutId) {
|
||||
auto result = Data::MessagesSlice();
|
||||
const auto i = _data.find(shortcutId);
|
||||
if (i == end(_data)) {
|
||||
const auto i = _requests.find(shortcutId);
|
||||
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;
|
||||
}
|
||||
|
||||
void ShortcutMessages::preloadShortcuts() {
|
||||
if (_shortcutsLoaded || _shortcutsRequestId) {
|
||||
return;
|
||||
}
|
||||
const auto owner = &_session->data();
|
||||
_shortcutsRequestId = owner->session().api().request(
|
||||
MTPmessages_GetQuickReplies(MTP_long(_shortcutsHash))
|
||||
).done([=](const MTPmessages_QuickReplies &result) {
|
||||
result.match([&](const MTPDmessages_quickReplies &data) {
|
||||
owner->processUsers(data.vusers());
|
||||
owner->processChats(data.vchats());
|
||||
updateShortcuts(data.vquick_replies().v);
|
||||
}, [&](const MTPDmessages_quickRepliesNotModified &) {
|
||||
if (!_shortcutsLoaded) {
|
||||
_shortcutsLoaded = true;
|
||||
_shortcutsChanged.fire({});
|
||||
}
|
||||
});
|
||||
}).send();
|
||||
}
|
||||
|
||||
const Shortcuts &ShortcutMessages::shortcuts() const {
|
||||
return _shortcuts;
|
||||
}
|
||||
|
||||
bool ShortcutMessages::shortcutsLoaded() const {
|
||||
return _shortcutsLoaded;
|
||||
}
|
||||
|
||||
rpl::producer<> ShortcutMessages::shortcutsChanged() const {
|
||||
return _shortcutsChanged.events();
|
||||
}
|
||||
|
||||
auto ShortcutMessages::shortcutIdChanged() const
|
||||
-> rpl::producer<ShortcutIdChange> {
|
||||
return _shortcutIdChanges.events();
|
||||
}
|
||||
|
||||
BusinessShortcutId ShortcutMessages::emplaceShortcut(QString name) {
|
||||
Expects(_shortcutsLoaded);
|
||||
|
||||
for (auto &[id, shortcut] : _shortcuts.list) {
|
||||
if (shortcut.name == name) {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
const auto result = --_localShortcutId;
|
||||
_shortcuts.list.emplace(result, Shortcut{ .id = result, .name = name });
|
||||
return result;
|
||||
}
|
||||
|
||||
Shortcut ShortcutMessages::lookupShortcut(BusinessShortcutId id) const {
|
||||
const auto i = _shortcuts.list.find(id);
|
||||
|
||||
Ensures(i != end(_shortcuts.list));
|
||||
return i->second;
|
||||
}
|
||||
|
||||
BusinessShortcutId ShortcutMessages::lookupShortcutId(
|
||||
const QString &name) const {
|
||||
for (const auto &[id, shortcut] : _shortcuts.list) {
|
||||
if (!shortcut.name.compare(name, Qt::CaseInsensitive)) {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void ShortcutMessages::editShortcut(
|
||||
BusinessShortcutId id,
|
||||
QString name,
|
||||
Fn<void()> done,
|
||||
Fn<void(QString)> fail) {
|
||||
name = name.trimmed();
|
||||
if (name.isEmpty()) {
|
||||
fail(QString());
|
||||
return;
|
||||
}
|
||||
const auto finish = [=] {
|
||||
const auto i = _shortcuts.list.find(id);
|
||||
if (i != end(_shortcuts.list)) {
|
||||
i->second.name = name;
|
||||
_shortcutsChanged.fire({});
|
||||
}
|
||||
done();
|
||||
};
|
||||
for (const auto &[existingId, shortcut] : _shortcuts.list) {
|
||||
if (shortcut.name == name) {
|
||||
if (existingId == id) {
|
||||
//done();
|
||||
//return;
|
||||
break;
|
||||
} else if (_data[existingId].items.empty() && !shortcut.count) {
|
||||
removeShortcut(existingId);
|
||||
break;
|
||||
} else {
|
||||
fail(u"SHORTCUT_OCCUPIED"_q);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
_session->api().request(MTPmessages_EditQuickReplyShortcut(
|
||||
MTP_int(id),
|
||||
MTP_string(name)
|
||||
)).done(finish).fail([=](const MTP::Error &error) {
|
||||
const auto type = error.type();
|
||||
if (type == u"SHORTCUT_ID_INVALID"_q) {
|
||||
// Not on the server (yet).
|
||||
finish();
|
||||
} else {
|
||||
fail(type);
|
||||
}
|
||||
}).send();
|
||||
}
|
||||
|
||||
void ShortcutMessages::removeShortcut(BusinessShortcutId shortcutId) {
|
||||
auto i = _data.find(shortcutId);
|
||||
while (i != end(_data)) {
|
||||
if (i->second.items.empty()) {
|
||||
_data.erase(i);
|
||||
} else {
|
||||
i->second.items.front()->destroy();
|
||||
}
|
||||
i = _data.find(shortcutId);
|
||||
}
|
||||
_shortcuts.list.remove(shortcutId);
|
||||
_shortcutIdChanges.fire({ shortcutId, 0 });
|
||||
|
||||
_session->api().request(MTPmessages_DeleteQuickReplyShortcut(
|
||||
MTP_int(shortcutId)
|
||||
)).send();
|
||||
}
|
||||
|
||||
void ShortcutMessages::cancelRequest(BusinessShortcutId shortcutId) {
|
||||
const auto j = _requests.find(shortcutId);
|
||||
if (j != end(_requests)) {
|
||||
_session->api().request(j->second.requestId).cancel();
|
||||
_requests.erase(j);
|
||||
}
|
||||
}
|
||||
|
||||
void ShortcutMessages::request(BusinessShortcutId shortcutId) {
|
||||
auto &request = _requests[shortcutId];
|
||||
if (request.requestId || TooEarlyForRequest(request.lastReceived)) {
|
||||
return;
|
||||
}
|
||||
const auto i = _data.find(shortcutId);
|
||||
const auto hash = (i != end(_data))
|
||||
? countListHash(i->second)
|
||||
: uint64(0);
|
||||
request.requestId = _session->api().request(
|
||||
MTPmessages_GetQuickReplyMessages(
|
||||
MTP_flags(0),
|
||||
MTP_int(shortcutId),
|
||||
MTPVector<MTPint>(),
|
||||
MTP_long(hash))
|
||||
).done([=](const MTPmessages_Messages &result) {
|
||||
parse(shortcutId, result);
|
||||
}).fail([=] {
|
||||
_requests.remove(shortcutId);
|
||||
}).send();
|
||||
}
|
||||
|
||||
void ShortcutMessages::parse(
|
||||
BusinessShortcutId shortcutId,
|
||||
const MTPmessages_Messages &list) {
|
||||
auto &request = _requests[shortcutId];
|
||||
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(shortcutId);
|
||||
return;
|
||||
}
|
||||
auto received = base::flat_set<not_null<HistoryItem*>>();
|
||||
auto clear = base::flat_set<not_null<HistoryItem*>>();
|
||||
auto &list = _data.emplace(shortcutId, List()).first->second;
|
||||
for (const auto &message : messages) {
|
||||
if (const auto item = append(shortcutId, 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(shortcutId, received, clear);
|
||||
});
|
||||
}
|
||||
|
||||
HistoryItem *ShortcutMessages::append(
|
||||
BusinessShortcutId shortcutId,
|
||||
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) {
|
||||
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 quick reply messages: %1.").arg(id));
|
||||
return nullptr;
|
||||
}
|
||||
const auto item = _session->data().addNewMessage(
|
||||
localMessageId(id),
|
||||
PrepareMessage(shortcutId, message),
|
||||
MessageFlags(), // localFlags
|
||||
NewMessageType::Existing);
|
||||
if (!item
|
||||
|| item->history() != _history
|
||||
|| item->shortcutId() != shortcutId) {
|
||||
LOG(("API Error: Bad data received in quick reply messages."));
|
||||
return nullptr;
|
||||
}
|
||||
list.items.emplace_back(item);
|
||||
list.itemById.emplace(id, item);
|
||||
return item;
|
||||
}
|
||||
|
||||
void ShortcutMessages::clearNotSending(BusinessShortcutId shortcutId) {
|
||||
const auto i = _data.find(shortcutId);
|
||||
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(shortcutId, {}, clear);
|
||||
}
|
||||
|
||||
void ShortcutMessages::updated(
|
||||
BusinessShortcutId shortcutId,
|
||||
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(shortcutId);
|
||||
if (i != end(_data)) {
|
||||
sort(i->second);
|
||||
}
|
||||
if (!added.empty() || !clear.empty()) {
|
||||
_updates.fire_copy(shortcutId);
|
||||
}
|
||||
}
|
||||
|
||||
void ShortcutMessages::sort(List &list) {
|
||||
ranges::sort(list.items, ranges::less(), &HistoryItem::position);
|
||||
}
|
||||
|
||||
void ShortcutMessages::remove(not_null<const HistoryItem*> item) {
|
||||
const auto shortcutId = item->shortcutId();
|
||||
const auto i = _data.find(shortcutId);
|
||||
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(shortcutId);
|
||||
updateCount(shortcutId);
|
||||
}
|
||||
|
||||
uint64 ShortcutMessages::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));
|
||||
}
|
||||
}
|
||||
return HashFinalize(hash);
|
||||
}
|
||||
|
||||
MTPInputQuickReplyShortcut ShortcutIdToMTP(
|
||||
not_null<Main::Session*> session,
|
||||
BusinessShortcutId id) {
|
||||
return id
|
||||
? MTP_inputQuickReplyShortcut(MTP_string(
|
||||
session->data().shortcutMessages().lookupShortcut(id).name))
|
||||
: MTPInputQuickReplyShortcut();
|
||||
}
|
||||
|
||||
} // namespace Data
|
||||
154
Telegram/SourceFiles/data/business/data_shortcut_messages.h
Normal file
154
Telegram/SourceFiles/data/business/data_shortcut_messages.h
Normal file
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
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 {
|
||||
|
||||
class Session;
|
||||
struct MessagesSlice;
|
||||
|
||||
struct Shortcut {
|
||||
BusinessShortcutId id = 0;
|
||||
int count = 0;
|
||||
QString name;
|
||||
MsgId topMessageId = 0;
|
||||
|
||||
friend inline bool operator==(
|
||||
const Shortcut &a,
|
||||
const Shortcut &b) = default;
|
||||
};
|
||||
|
||||
struct ShortcutIdChange {
|
||||
BusinessShortcutId oldId = 0;
|
||||
BusinessShortcutId newId = 0;
|
||||
};
|
||||
|
||||
struct Shortcuts {
|
||||
base::flat_map<BusinessShortcutId, Shortcut> list;
|
||||
|
||||
friend inline bool operator==(
|
||||
const Shortcuts &a,
|
||||
const Shortcuts &b) = default;
|
||||
};
|
||||
|
||||
[[nodiscard]] bool IsShortcutMsgId(MsgId id);
|
||||
|
||||
class ShortcutMessages final {
|
||||
public:
|
||||
explicit ShortcutMessages(not_null<Session*> owner);
|
||||
~ShortcutMessages();
|
||||
|
||||
[[nodiscard]] MsgId lookupId(not_null<const HistoryItem*> item) const;
|
||||
[[nodiscard]] int count(BusinessShortcutId shortcutId) const;
|
||||
[[nodiscard]] MsgId localMessageId(MsgId remoteId) const;
|
||||
|
||||
void apply(const MTPDupdateQuickReplies &update);
|
||||
void apply(const MTPDupdateNewQuickReply &update);
|
||||
void apply(const MTPDupdateQuickReplyMessage &update);
|
||||
void apply(const MTPDupdateDeleteQuickReplyMessages &update);
|
||||
void apply(const MTPDupdateDeleteQuickReply &update);
|
||||
void apply(
|
||||
const MTPDupdateMessageID &update,
|
||||
not_null<HistoryItem*> local);
|
||||
|
||||
void appendSending(not_null<HistoryItem*> item);
|
||||
void removeSending(not_null<HistoryItem*> item);
|
||||
|
||||
[[nodiscard]] rpl::producer<> updates(BusinessShortcutId shortcutId);
|
||||
[[nodiscard]] Data::MessagesSlice list(BusinessShortcutId shortcutId);
|
||||
|
||||
void preloadShortcuts();
|
||||
[[nodiscard]] const Shortcuts &shortcuts() const;
|
||||
[[nodiscard]] bool shortcutsLoaded() const;
|
||||
[[nodiscard]] rpl::producer<> shortcutsChanged() const;
|
||||
[[nodiscard]] rpl::producer<ShortcutIdChange> shortcutIdChanged() const;
|
||||
[[nodiscard]] BusinessShortcutId emplaceShortcut(QString name);
|
||||
[[nodiscard]] Shortcut lookupShortcut(BusinessShortcutId id) const;
|
||||
[[nodiscard]] BusinessShortcutId lookupShortcutId(
|
||||
const QString &name) const;
|
||||
void editShortcut(
|
||||
BusinessShortcutId id,
|
||||
QString name,
|
||||
Fn<void()> done,
|
||||
Fn<void(QString)> fail);
|
||||
void removeShortcut(BusinessShortcutId shortcutId);
|
||||
|
||||
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(BusinessShortcutId shortcutId);
|
||||
void parse(
|
||||
BusinessShortcutId shortcutId,
|
||||
const MTPmessages_Messages &list);
|
||||
HistoryItem *append(
|
||||
BusinessShortcutId shortcutId,
|
||||
List &list,
|
||||
const MTPMessage &message);
|
||||
void clearNotSending(BusinessShortcutId shortcutId);
|
||||
void updated(
|
||||
BusinessShortcutId shortcutId,
|
||||
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();
|
||||
void cancelRequest(BusinessShortcutId shortcutId);
|
||||
void updateCount(BusinessShortcutId shortcutId);
|
||||
|
||||
void scheduleShortcutsReload();
|
||||
void mergeMessagesFromTo(
|
||||
BusinessShortcutId fromId,
|
||||
BusinessShortcutId toId);
|
||||
void updateShortcuts(const QVector<MTPQuickReply> &list);
|
||||
[[nodiscard]] Shortcut parseShortcut(const MTPQuickReply &reply) const;
|
||||
[[nodiscard]] Shortcuts parseShortcuts(
|
||||
const QVector<MTPQuickReply> &list) const;
|
||||
|
||||
const not_null<Main::Session*> _session;
|
||||
const not_null<History*> _history;
|
||||
|
||||
base::Timer _clearTimer;
|
||||
base::flat_map<BusinessShortcutId, List> _data;
|
||||
base::flat_map<BusinessShortcutId, Request> _requests;
|
||||
rpl::event_stream<BusinessShortcutId> _updates;
|
||||
|
||||
Shortcuts _shortcuts;
|
||||
rpl::event_stream<> _shortcutsChanged;
|
||||
rpl::event_stream<ShortcutIdChange> _shortcutIdChanges;
|
||||
BusinessShortcutId _localShortcutId = 0;
|
||||
uint64 _shortcutsHash = 0;
|
||||
mtpRequestId _shortcutsRequestId = 0;
|
||||
bool _shortcutsLoaded = false;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] MTPInputQuickReplyShortcut ShortcutIdToMTP(
|
||||
not_null<Main::Session*> session,
|
||||
BusinessShortcutId id);
|
||||
|
||||
} // namespace Data
|
||||
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
|
||||
83
Telegram/SourceFiles/data/data_abstract_sparse_ids.h
Normal file
83
Telegram/SourceFiles/data/data_abstract_sparse_ids.h
Normal file
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
template <typename IdsContainer>
|
||||
class AbstractSparseIds {
|
||||
public:
|
||||
using Id = typename IdsContainer::value_type;
|
||||
|
||||
AbstractSparseIds() = default;
|
||||
AbstractSparseIds(
|
||||
const IdsContainer &ids,
|
||||
std::optional<int> fullCount,
|
||||
std::optional<int> skippedBefore,
|
||||
std::optional<int> skippedAfter)
|
||||
: _ids(ids)
|
||||
, _fullCount(fullCount)
|
||||
, _skippedBefore(skippedBefore)
|
||||
, _skippedAfter(skippedAfter) {
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<int> fullCount() const {
|
||||
return _fullCount;
|
||||
}
|
||||
[[nodiscard]] std::optional<int> skippedBefore() const {
|
||||
return _skippedBefore;
|
||||
}
|
||||
[[nodiscard]] std::optional<int> skippedAfter() const {
|
||||
return _skippedAfter;
|
||||
}
|
||||
[[nodiscard]] std::optional<int> indexOf(Id id) const {
|
||||
const auto it = ranges::find(_ids, id);
|
||||
if (it != _ids.end()) {
|
||||
return (it - _ids.begin());
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
[[nodiscard]] int size() const {
|
||||
return _ids.size();
|
||||
}
|
||||
[[nodiscard]] Id operator[](int index) const {
|
||||
Expects(index >= 0 && index < size());
|
||||
|
||||
return *(_ids.begin() + index);
|
||||
}
|
||||
[[nodiscard]] std::optional<int> distance(Id a, Id b) const {
|
||||
if (const auto i = indexOf(a)) {
|
||||
if (const auto j = indexOf(b)) {
|
||||
return *j - *i;
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
[[nodiscard]] std::optional<Id> nearest(Id id) const {
|
||||
static_assert(std::is_same_v<IdsContainer, base::flat_set<Id>>);
|
||||
if (const auto it = ranges::lower_bound(_ids, id); it != _ids.end()) {
|
||||
return *it;
|
||||
} else if (_ids.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return _ids.back();
|
||||
}
|
||||
void reverse() {
|
||||
ranges::reverse(_ids);
|
||||
std::swap(_skippedBefore, _skippedAfter);
|
||||
}
|
||||
|
||||
friend inline bool operator==(
|
||||
const AbstractSparseIds&,
|
||||
const AbstractSparseIds&) = default;
|
||||
|
||||
private:
|
||||
IdsContainer _ids;
|
||||
std::optional<int> _fullCount;
|
||||
std::optional<int> _skippedBefore;
|
||||
std::optional<int> _skippedAfter;
|
||||
|
||||
};
|
||||
38
Telegram/SourceFiles/data/data_abstract_structure.cpp
Normal file
38
Telegram/SourceFiles/data/data_abstract_structure.cpp
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
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/data_abstract_structure.h"
|
||||
|
||||
#include "base/never_freed_pointer.h"
|
||||
|
||||
namespace Data {
|
||||
namespace {
|
||||
|
||||
using DataStructures = OrderedSet<AbstractStructure**>;
|
||||
base::NeverFreedPointer<DataStructures> structures;
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace internal {
|
||||
|
||||
void registerAbstractStructure(AbstractStructure **p) {
|
||||
structures.createIfNull();
|
||||
structures->insert(p);
|
||||
}
|
||||
|
||||
} // namespace internal
|
||||
|
||||
void clearGlobalStructures() {
|
||||
if (!structures) return;
|
||||
for (auto &p : *structures) {
|
||||
delete (*p);
|
||||
*p = nullptr;
|
||||
}
|
||||
structures.clear();
|
||||
}
|
||||
|
||||
} // namespace Data
|
||||
70
Telegram/SourceFiles/data/data_abstract_structure.h
Normal file
70
Telegram/SourceFiles/data/data_abstract_structure.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
|
||||
|
||||
namespace Data {
|
||||
|
||||
// This module suggests a way to hold global data structures, that are
|
||||
// created on demand and deleted at the end of the app launch.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// class MyData : public Data::AbstractStruct { .. data .. };
|
||||
// Data::GlobalStructurePointer<MyData> myData;
|
||||
// .. somewhere when needed ..
|
||||
// myData.createIfNull();
|
||||
|
||||
class AbstractStructure {
|
||||
public:
|
||||
virtual ~AbstractStructure() = 0;
|
||||
};
|
||||
inline AbstractStructure::~AbstractStructure() = default;
|
||||
|
||||
namespace internal {
|
||||
|
||||
void registerAbstractStructure(AbstractStructure **p);
|
||||
|
||||
} // namespace
|
||||
|
||||
// Must be created in global scope!
|
||||
// Structure is derived from AbstractStructure.
|
||||
template <typename Structure>
|
||||
class GlobalStructurePointer {
|
||||
public:
|
||||
GlobalStructurePointer() = default;
|
||||
GlobalStructurePointer(const GlobalStructurePointer<Structure> &other) = delete;
|
||||
GlobalStructurePointer &operator=(const GlobalStructurePointer<Structure> &other) = delete;
|
||||
|
||||
void createIfNull() {
|
||||
if (!_p) {
|
||||
_p = new Structure();
|
||||
internal::registerAbstractStructure(&_p);
|
||||
}
|
||||
}
|
||||
Structure *operator->() {
|
||||
Assert(_p != nullptr);
|
||||
return static_cast<Structure*>(_p);
|
||||
}
|
||||
const Structure *operator->() const {
|
||||
Assert(_p != nullptr);
|
||||
return static_cast<const Structure*>(_p);
|
||||
}
|
||||
explicit operator bool() const {
|
||||
return _p != nullptr;
|
||||
}
|
||||
|
||||
private:
|
||||
AbstractStructure *_p;
|
||||
|
||||
};
|
||||
|
||||
// This method should be called at the end of the app launch.
|
||||
// It will destroy all data structures created by Data::GlobalStructurePointer.
|
||||
void clearGlobalStructures();
|
||||
|
||||
} // namespace Data
|
||||
103
Telegram/SourceFiles/data/data_audio_msg_id.cpp
Normal file
103
Telegram/SourceFiles/data/data_audio_msg_id.cpp
Normal file
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
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/data_audio_msg_id.h"
|
||||
|
||||
#include "data/data_document.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto kMinLengthForChangeablePlaybackSpeed = 20 * TimeId(60); // 20 minutes.
|
||||
|
||||
} // namespace
|
||||
|
||||
AudioMsgId::AudioMsgId() {
|
||||
}
|
||||
|
||||
AudioMsgId::AudioMsgId(
|
||||
not_null<DocumentData*> audio,
|
||||
FullMsgId msgId,
|
||||
uint32 externalPlayId)
|
||||
: _audio(audio)
|
||||
, _contextId(msgId)
|
||||
, _externalPlayId(externalPlayId)
|
||||
, _changeablePlaybackSpeed(_audio->isVoiceMessage()
|
||||
|| _audio->isVideoMessage()
|
||||
|| (_audio->duration() >= kMinLengthForChangeablePlaybackSpeed)) {
|
||||
setTypeFromAudio();
|
||||
}
|
||||
|
||||
uint32 AudioMsgId::CreateExternalPlayId() {
|
||||
static auto Result = uint32(0);
|
||||
return ++Result ? Result : ++Result;
|
||||
}
|
||||
|
||||
AudioMsgId AudioMsgId::ForVideo() {
|
||||
auto result = AudioMsgId();
|
||||
result._externalPlayId = CreateExternalPlayId();
|
||||
result._type = Type::Video;
|
||||
return result;
|
||||
}
|
||||
|
||||
void AudioMsgId::setTypeFromAudio() {
|
||||
if (_audio->isVoiceMessage() || _audio->isVideoMessage()) {
|
||||
_type = Type::Voice;
|
||||
} else if (_audio->isVideoFile()) {
|
||||
_type = Type::Video;
|
||||
} else if (_audio->isAudioFile()) {
|
||||
_type = Type::Song;
|
||||
} else {
|
||||
_type = Type::Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
AudioMsgId::Type AudioMsgId::type() const {
|
||||
return _type;
|
||||
}
|
||||
|
||||
DocumentData *AudioMsgId::audio() const {
|
||||
return _audio;
|
||||
}
|
||||
|
||||
FullMsgId AudioMsgId::contextId() const {
|
||||
return _contextId;
|
||||
}
|
||||
|
||||
uint32 AudioMsgId::externalPlayId() const {
|
||||
return _externalPlayId;
|
||||
}
|
||||
|
||||
bool AudioMsgId::changeablePlaybackSpeed() const {
|
||||
return _changeablePlaybackSpeed;
|
||||
}
|
||||
|
||||
AudioMsgId::operator bool() const {
|
||||
return (_audio != nullptr) || (_externalPlayId != 0);
|
||||
}
|
||||
|
||||
bool AudioMsgId::operator<(const AudioMsgId &other) const {
|
||||
if (quintptr(audio()) < quintptr(other.audio())) {
|
||||
return true;
|
||||
} else if (quintptr(other.audio()) < quintptr(audio())) {
|
||||
return false;
|
||||
} else if (contextId() < other.contextId()) {
|
||||
return true;
|
||||
} else if (other.contextId() < contextId()) {
|
||||
return false;
|
||||
}
|
||||
return (externalPlayId() < other.externalPlayId());
|
||||
}
|
||||
|
||||
bool AudioMsgId::operator==(const AudioMsgId &other) const {
|
||||
return (audio() == other.audio())
|
||||
&& (contextId() == other.contextId())
|
||||
&& (externalPlayId() == other.externalPlayId());
|
||||
}
|
||||
|
||||
bool AudioMsgId::operator!=(const AudioMsgId &other) const {
|
||||
return !(*this == other);
|
||||
}
|
||||
50
Telegram/SourceFiles/data/data_audio_msg_id.h
Normal file
50
Telegram/SourceFiles/data/data_audio_msg_id.h
Normal file
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
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
|
||||
|
||||
class DocumentData;
|
||||
|
||||
class AudioMsgId final {
|
||||
public:
|
||||
enum class Type {
|
||||
Unknown,
|
||||
Voice,
|
||||
Song,
|
||||
Video,
|
||||
};
|
||||
|
||||
AudioMsgId();
|
||||
AudioMsgId(
|
||||
not_null<DocumentData*> audio,
|
||||
FullMsgId msgId,
|
||||
uint32 externalPlayId = 0);
|
||||
|
||||
[[nodiscard]] static uint32 CreateExternalPlayId();
|
||||
[[nodiscard]] static AudioMsgId ForVideo();
|
||||
|
||||
[[nodiscard]] Type type() const;
|
||||
[[nodiscard]] DocumentData *audio() const;
|
||||
[[nodiscard]] FullMsgId contextId() const;
|
||||
[[nodiscard]] uint32 externalPlayId() const;
|
||||
[[nodiscard]] bool changeablePlaybackSpeed() const;
|
||||
[[nodiscard]] explicit operator bool() const;
|
||||
|
||||
bool operator<(const AudioMsgId &other) const;
|
||||
bool operator==(const AudioMsgId &other) const;
|
||||
bool operator!=(const AudioMsgId &other) const;
|
||||
|
||||
private:
|
||||
void setTypeFromAudio();
|
||||
|
||||
DocumentData *_audio = nullptr;
|
||||
Type _type = Type::Unknown;
|
||||
FullMsgId _contextId;
|
||||
uint32 _externalPlayId = 0;
|
||||
bool _changeablePlaybackSpeed = false;
|
||||
|
||||
};
|
||||
20
Telegram/SourceFiles/data/data_authorization.h
Normal file
20
Telegram/SourceFiles/data/data_authorization.h
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
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 {
|
||||
|
||||
struct UnreviewedAuth {
|
||||
uint64 hash = 0;
|
||||
bool unconfirmed = false;
|
||||
TimeId date = 0;
|
||||
QString device;
|
||||
QString location;
|
||||
};
|
||||
|
||||
} // namespace Data
|
||||
338
Telegram/SourceFiles/data/data_auto_download.cpp
Normal file
338
Telegram/SourceFiles/data/data_auto_download.cpp
Normal file
@@ -0,0 +1,338 @@
|
||||
/*
|
||||
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/data_auto_download.h"
|
||||
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_photo.h"
|
||||
#include "data/data_document.h"
|
||||
|
||||
#include <QtCore/QBuffer>
|
||||
|
||||
namespace Data {
|
||||
namespace AutoDownload {
|
||||
namespace {
|
||||
|
||||
constexpr auto kDefaultMaxSize = 8 * int64(1024 * 1024);
|
||||
constexpr auto kDefaultAutoPlaySize = 50 * int64(1024 * 1024);
|
||||
constexpr auto kVersion1 = char(1);
|
||||
constexpr auto kVersion = char(2);
|
||||
|
||||
template <typename Enum>
|
||||
auto enums_view(int from, int till) {
|
||||
using namespace ranges::views;
|
||||
return ints(from, till) | transform([](int index) {
|
||||
return static_cast<Enum>(index);
|
||||
});
|
||||
}
|
||||
|
||||
template <typename Enum>
|
||||
auto enums_view(int till) {
|
||||
return enums_view<Enum>(0, till);
|
||||
}
|
||||
|
||||
void SetDefaultsForSource(Full &data, Source source) {
|
||||
data.setBytesLimit(source, Type::Photo, kDefaultMaxSize);
|
||||
data.setBytesLimit(source, Type::VoiceMessage, kDefaultMaxSize);
|
||||
data.setBytesLimit(
|
||||
source,
|
||||
Type::AutoPlayVideoMessage,
|
||||
kDefaultAutoPlaySize);
|
||||
data.setBytesLimit(source, Type::AutoPlayGIF, kDefaultAutoPlaySize);
|
||||
const auto channelsFileLimit = (source == Source::Channel)
|
||||
? 0
|
||||
: kDefaultMaxSize;
|
||||
data.setBytesLimit(source, Type::File, channelsFileLimit);
|
||||
data.setBytesLimit(source, Type::AutoPlayVideo, kDefaultAutoPlaySize);
|
||||
data.setBytesLimit(source, Type::Music, channelsFileLimit);
|
||||
}
|
||||
|
||||
const Full &Defaults() {
|
||||
static auto Result = [] {
|
||||
auto result = Full::FullDisabled();
|
||||
for (const auto source : enums_view<Source>(kSourcesCount)) {
|
||||
SetDefaultsForSource(result, source);
|
||||
}
|
||||
return result;
|
||||
}();
|
||||
return Result;
|
||||
}
|
||||
|
||||
Source SourceFromPeer(not_null<PeerData*> peer) {
|
||||
if (peer->isUser()) {
|
||||
return Source::User;
|
||||
} else if (peer->isChat() || peer->isMegagroup()) {
|
||||
return Source::Group;
|
||||
} else {
|
||||
return Source::Channel;
|
||||
}
|
||||
}
|
||||
|
||||
Type AutoPlayTypeFromDocument(not_null<DocumentData*> document) {
|
||||
return document->isVideoFile()
|
||||
? Type::AutoPlayVideo
|
||||
: document->isVideoMessage()
|
||||
? Type::AutoPlayVideoMessage
|
||||
: Type::AutoPlayGIF;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void Single::setBytesLimit(int64 bytesLimit) {
|
||||
Expects(bytesLimit >= 0 && bytesLimit <= kMaxBytesLimit);
|
||||
|
||||
_limit = int32(uint32(bytesLimit));
|
||||
|
||||
Ensures(hasValue());
|
||||
}
|
||||
|
||||
bool Single::hasValue() const {
|
||||
return (_limit != -1);
|
||||
}
|
||||
|
||||
bool Single::shouldDownload(int64 fileSize) const {
|
||||
Expects(hasValue());
|
||||
|
||||
const auto realLimit = bytesLimit();
|
||||
return (realLimit > 0) && (fileSize <= realLimit);
|
||||
}
|
||||
|
||||
int64 Single::bytesLimit() const {
|
||||
Expects(hasValue());
|
||||
|
||||
return uint32(_limit);
|
||||
}
|
||||
|
||||
qint32 Single::serialize() const {
|
||||
return _limit;
|
||||
}
|
||||
|
||||
bool Single::setFromSerialized(qint32 serialized) {
|
||||
auto realLimit = quint32(serialized);
|
||||
if (serialized != -1 && int64(realLimit) > kMaxBytesLimit) {
|
||||
return false;
|
||||
}
|
||||
_limit = serialized;
|
||||
return true;
|
||||
}
|
||||
|
||||
const Single &Set::single(Type type) const {
|
||||
Expects(static_cast<int>(type) >= 0
|
||||
&& static_cast<int>(type) < kTypesCount);
|
||||
|
||||
return _data[static_cast<int>(type)];
|
||||
}
|
||||
|
||||
Single &Set::single(Type type) {
|
||||
return const_cast<Single&>(static_cast<const Set*>(this)->single(type));
|
||||
}
|
||||
|
||||
void Set::setBytesLimit(Type type, int64 bytesLimit) {
|
||||
single(type).setBytesLimit(bytesLimit);
|
||||
}
|
||||
|
||||
bool Set::hasValue(Type type) const {
|
||||
return single(type).hasValue();
|
||||
}
|
||||
|
||||
bool Set::shouldDownload(Type type, int64 fileSize) const {
|
||||
return single(type).shouldDownload(fileSize);
|
||||
}
|
||||
|
||||
int64 Set::bytesLimit(Type type) const {
|
||||
return single(type).bytesLimit();
|
||||
}
|
||||
|
||||
qint32 Set::serialize(Type type) const {
|
||||
return single(type).serialize();
|
||||
}
|
||||
|
||||
bool Set::setFromSerialized(Type type, qint32 serialized) {
|
||||
if (static_cast<int>(type) < 0
|
||||
|| static_cast<int>(type) >= kTypesCount) {
|
||||
return false;
|
||||
}
|
||||
return single(type).setFromSerialized(serialized);
|
||||
}
|
||||
|
||||
const Set &Full::set(Source source) const {
|
||||
Expects(static_cast<int>(source) >= 0
|
||||
&& static_cast<int>(source) < kSourcesCount);
|
||||
|
||||
return _data[static_cast<int>(source)];
|
||||
}
|
||||
|
||||
Set &Full::set(Source source) {
|
||||
return const_cast<Set&>(static_cast<const Full*>(this)->set(source));
|
||||
}
|
||||
|
||||
const Set &Full::setOrDefault(Source source, Type type) const {
|
||||
const auto &my = set(source);
|
||||
const auto &result = my.hasValue(type) ? my : Defaults().set(source);
|
||||
|
||||
Ensures(result.hasValue(type));
|
||||
return result;
|
||||
}
|
||||
|
||||
void Full::setBytesLimit(Source source, Type type, int64 bytesLimit) {
|
||||
set(source).setBytesLimit(type, bytesLimit);
|
||||
}
|
||||
|
||||
bool Full::shouldDownload(Source source, Type type, int64 fileSize) const {
|
||||
if (ranges::find(kStreamedTypes, type) != end(kStreamedTypes)) {
|
||||
// With streaming we disable autodownload and hide them in Settings.
|
||||
return false;
|
||||
}
|
||||
return setOrDefault(source, type).shouldDownload(type, fileSize);
|
||||
}
|
||||
|
||||
int64 Full::bytesLimit(Source source, Type type) const {
|
||||
return setOrDefault(source, type).bytesLimit(type);
|
||||
}
|
||||
|
||||
QByteArray Full::serialize() const {
|
||||
auto result = QByteArray();
|
||||
auto size = sizeof(qint8);
|
||||
size += kSourcesCount * kTypesCount * sizeof(qint32);
|
||||
result.reserve(size);
|
||||
{
|
||||
auto buffer = QBuffer(&result);
|
||||
buffer.open(QIODevice::WriteOnly);
|
||||
auto stream = QDataStream(&buffer);
|
||||
stream << qint8(kVersion);
|
||||
for (const auto source : enums_view<Source>(kSourcesCount)) {
|
||||
for (const auto type : enums_view<Type>(kTypesCount)) {
|
||||
stream << set(source).serialize(type);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool Full::setFromSerialized(const QByteArray &serialized) {
|
||||
if (serialized.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto stream = QDataStream(serialized);
|
||||
auto version = qint8();
|
||||
stream >> version;
|
||||
if (stream.status() != QDataStream::Ok) {
|
||||
return false;
|
||||
} else if (version != kVersion && version != kVersion1) {
|
||||
return false;
|
||||
}
|
||||
auto temp = Full();
|
||||
for (const auto source : enums_view<Source>(kSourcesCount)) {
|
||||
for (const auto type : enums_view<Type>(kTypesCount)) {
|
||||
auto value = qint32();
|
||||
stream >> value;
|
||||
if (!temp.set(source).setFromSerialized(type, value)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (version == kVersion1) {
|
||||
for (const auto source : enums_view<Source>(kSourcesCount)) {
|
||||
for (const auto type : kAutoPlayTypes) {
|
||||
temp.setBytesLimit(source, type, std::max(
|
||||
temp.bytesLimit(source, type),
|
||||
kDefaultAutoPlaySize));
|
||||
}
|
||||
}
|
||||
}
|
||||
_data = temp._data;
|
||||
return true;
|
||||
}
|
||||
|
||||
Full Full::FullDisabled() {
|
||||
auto result = Full();
|
||||
for (const auto source : enums_view<Source>(kSourcesCount)) {
|
||||
for (const auto type : enums_view<Type>(kTypesCount)) {
|
||||
result.setBytesLimit(source, type, 0);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool Should(
|
||||
const Full &data,
|
||||
Source source,
|
||||
not_null<DocumentData*> document) {
|
||||
if (document->sticker() || document->isGifv()) {
|
||||
return true;
|
||||
} else if (document->isVoiceMessage()
|
||||
|| document->isVideoMessage()
|
||||
|| document->isSong()
|
||||
|| document->isVideoFile()) {
|
||||
return false;
|
||||
}
|
||||
return data.shouldDownload(source, Type::File, document->size);
|
||||
}
|
||||
|
||||
bool Should(
|
||||
const Full &data,
|
||||
not_null<PeerData*> peer,
|
||||
not_null<DocumentData*> document) {
|
||||
return Should(data, SourceFromPeer(peer), document);
|
||||
}
|
||||
|
||||
bool Should(
|
||||
const Full &data,
|
||||
not_null<DocumentData*> document) {
|
||||
if (document->sticker()) {
|
||||
return true;
|
||||
}
|
||||
return Should(data, Source::User, document)
|
||||
|| Should(data, Source::Group, document)
|
||||
|| Should(data, Source::Channel, document);
|
||||
}
|
||||
|
||||
bool Should(
|
||||
const Full &data,
|
||||
not_null<PeerData*> peer,
|
||||
not_null<PhotoData*> photo) {
|
||||
return data.shouldDownload(
|
||||
SourceFromPeer(peer),
|
||||
Type::Photo,
|
||||
photo->imageByteSize(PhotoSize::Large));
|
||||
}
|
||||
|
||||
bool ShouldAutoPlay(
|
||||
const Full &data,
|
||||
not_null<PeerData*> peer,
|
||||
not_null<DocumentData*> document) {
|
||||
return document->sticker() || data.shouldDownload(
|
||||
SourceFromPeer(peer),
|
||||
AutoPlayTypeFromDocument(document),
|
||||
document->size);
|
||||
}
|
||||
|
||||
bool ShouldAutoPlay(
|
||||
const Full &data,
|
||||
not_null<PeerData*> peer,
|
||||
not_null<PhotoData*> photo) {
|
||||
const auto source = SourceFromPeer(peer);
|
||||
const auto size = photo->videoByteSize(PhotoSize::Large);
|
||||
return photo->hasVideo()
|
||||
&& (data.shouldDownload(source, Type::AutoPlayGIF, size)
|
||||
|| data.shouldDownload(source, Type::AutoPlayVideo, size)
|
||||
|| data.shouldDownload(source, Type::AutoPlayVideoMessage, size));
|
||||
}
|
||||
|
||||
Full WithDisabledAutoPlay(const Full &data) {
|
||||
auto result = data;
|
||||
for (const auto source : enums_view<Source>(kSourcesCount)) {
|
||||
for (const auto type : kAutoPlayTypes) {
|
||||
result.setBytesLimit(source, type, 0);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace AutoDownload
|
||||
} // namespace Data
|
||||
131
Telegram/SourceFiles/data/data_auto_download.h
Normal file
131
Telegram/SourceFiles/data/data_auto_download.h
Normal file
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
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 <array>
|
||||
|
||||
namespace Data {
|
||||
namespace AutoDownload {
|
||||
|
||||
constexpr auto kMaxBytesLimit = 8000 * int64(512 * 1024);
|
||||
|
||||
enum class Source {
|
||||
User = 0x00,
|
||||
Group = 0x01,
|
||||
Channel = 0x02,
|
||||
};
|
||||
|
||||
constexpr auto kSourcesCount = 3;
|
||||
|
||||
enum class Type {
|
||||
Photo = 0x00,
|
||||
AutoPlayVideo = 0x01,
|
||||
VoiceMessage = 0x02,
|
||||
AutoPlayVideoMessage = 0x03,
|
||||
Music = 0x04,
|
||||
AutoPlayGIF = 0x05,
|
||||
File = 0x06,
|
||||
};
|
||||
|
||||
inline constexpr auto kAutoPlayTypes = {
|
||||
Type::AutoPlayVideo,
|
||||
Type::AutoPlayVideoMessage,
|
||||
Type::AutoPlayGIF,
|
||||
};
|
||||
|
||||
inline constexpr auto kStreamedTypes = {
|
||||
Type::VoiceMessage,
|
||||
Type::Music,
|
||||
};
|
||||
|
||||
constexpr auto kTypesCount = 7;
|
||||
|
||||
class Single {
|
||||
public:
|
||||
void setBytesLimit(int64 bytesLimit);
|
||||
|
||||
bool hasValue() const;
|
||||
bool shouldDownload(int64 fileSize) const;
|
||||
int64 bytesLimit() const;
|
||||
|
||||
qint32 serialize() const;
|
||||
bool setFromSerialized(qint32 serialized);
|
||||
|
||||
private:
|
||||
int _limit = -1; // FileSize: Right now any file size fits 32 bit.
|
||||
|
||||
};
|
||||
|
||||
class Set {
|
||||
public:
|
||||
void setBytesLimit(Type type, int64 bytesLimit);
|
||||
|
||||
bool hasValue(Type type) const;
|
||||
bool shouldDownload(Type type, int64 fileSize) const;
|
||||
int64 bytesLimit(Type type) const;
|
||||
|
||||
qint32 serialize(Type type) const;
|
||||
bool setFromSerialized(Type type, qint32 serialized);
|
||||
|
||||
private:
|
||||
const Single &single(Type type) const;
|
||||
Single &single(Type type);
|
||||
|
||||
std::array<Single, kTypesCount> _data;
|
||||
|
||||
};
|
||||
|
||||
class Full {
|
||||
public:
|
||||
void setBytesLimit(Source source, Type type, int64 bytesLimit);
|
||||
|
||||
[[nodiscard]] bool shouldDownload(
|
||||
Source source,
|
||||
Type type,
|
||||
int64 fileSize) const;
|
||||
[[nodiscard]] int64 bytesLimit(Source source, Type type) const;
|
||||
|
||||
[[nodiscard]] QByteArray serialize() const;
|
||||
bool setFromSerialized(const QByteArray &serialized);
|
||||
|
||||
[[nodiscard]] static Full FullDisabled();
|
||||
|
||||
private:
|
||||
[[nodiscard]] const Set &set(Source source) const;
|
||||
[[nodiscard]] Set &set(Source source);
|
||||
[[nodiscard]] const Set &setOrDefault(Source source, Type type) const;
|
||||
|
||||
std::array<Set, kSourcesCount> _data;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] bool Should(
|
||||
const Full &data,
|
||||
not_null<PeerData*> peer,
|
||||
not_null<DocumentData*> document);
|
||||
[[nodiscard]] bool Should(
|
||||
const Full &data,
|
||||
not_null<DocumentData*> document);
|
||||
[[nodiscard]] bool Should(
|
||||
const Full &data,
|
||||
not_null<PeerData*> peer,
|
||||
not_null<PhotoData*> photo);
|
||||
|
||||
[[nodiscard]] bool ShouldAutoPlay(
|
||||
const Full &data,
|
||||
not_null<PeerData*> peer,
|
||||
not_null<DocumentData*> document);
|
||||
[[nodiscard]] bool ShouldAutoPlay(
|
||||
const Full &data,
|
||||
not_null<PeerData*> peer,
|
||||
not_null<PhotoData*> photo);
|
||||
|
||||
[[nodiscard]] Full WithDisabledAutoPlay(const Full &data);
|
||||
|
||||
} // namespace AutoDownload
|
||||
} // namespace Data
|
||||
129
Telegram/SourceFiles/data/data_birthday.cpp
Normal file
129
Telegram/SourceFiles/data/data_birthday.cpp
Normal file
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
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/data_birthday.h"
|
||||
|
||||
#include "base/timer_rpl.h"
|
||||
#include "lang/lang_keys.h"
|
||||
|
||||
#include <QtCore/QDate>
|
||||
|
||||
namespace Data {
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] bool Validate(int day, int month, int year) {
|
||||
if (year != 0
|
||||
&& (year < Birthday::kYearMin || year > Birthday::kYearMax)) {
|
||||
return false;
|
||||
} else if (day < 1) {
|
||||
return false;
|
||||
} else if (month == 2) {
|
||||
if (day == 29) {
|
||||
return !year
|
||||
|| (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0));
|
||||
}
|
||||
return day <= 28;
|
||||
} else if (month == 4 || month == 6 || month == 9 || month == 11) {
|
||||
return day <= 30;
|
||||
} else if (month > 0 && month <= 12) {
|
||||
return day <= 31;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] int Serialize(int day, int month, int year) {
|
||||
return day + month * 100 + year * 10000;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Birthday::Birthday(int day, int month, int year)
|
||||
: _value(Validate(day, month, year) ? Serialize(day, month, year) : 0) {
|
||||
}
|
||||
|
||||
Birthday Birthday::FromSerialized(int value) {
|
||||
return Birthday(value % 100, (value / 100) % 100, value / 10000);
|
||||
}
|
||||
|
||||
int Birthday::serialize() const {
|
||||
return _value;
|
||||
}
|
||||
|
||||
bool Birthday::valid() const {
|
||||
return _value != 0;
|
||||
}
|
||||
|
||||
int Birthday::day() const {
|
||||
return _value % 100;
|
||||
}
|
||||
|
||||
int Birthday::month() const {
|
||||
return (_value / 100) % 100;
|
||||
}
|
||||
|
||||
int Birthday::year() const {
|
||||
return _value / 10000;
|
||||
}
|
||||
|
||||
QString BirthdayText(Birthday date) {
|
||||
if (const auto year = date.year()) {
|
||||
return tr::lng_month_day_year(
|
||||
tr::now,
|
||||
lt_month,
|
||||
Lang::MonthSmall(date.month())(tr::now),
|
||||
lt_day,
|
||||
QString::number(date.day()),
|
||||
lt_year,
|
||||
QString::number(year));
|
||||
} else if (date) {
|
||||
return tr::lng_month_day(
|
||||
tr::now,
|
||||
lt_month,
|
||||
Lang::MonthSmall(date.month())(tr::now),
|
||||
lt_day,
|
||||
QString::number(date.day()));
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString BirthdayCake() {
|
||||
return QString::fromUtf8("\xf0\x9f\x8e\x82");
|
||||
}
|
||||
|
||||
int BirthdayAge(Birthday date) {
|
||||
if (!date.year()) {
|
||||
return 0;
|
||||
}
|
||||
const auto now = QDate::currentDate();
|
||||
const auto day = QDate(date.year(), date.month(), date.day());
|
||||
if (!day.isValid() || day >= now) {
|
||||
return 0;
|
||||
}
|
||||
auto age = now.year() - date.year();
|
||||
if (now < QDate(date.year() + age, date.month(), date.day())) {
|
||||
--age;
|
||||
}
|
||||
return age;
|
||||
}
|
||||
|
||||
bool IsBirthdayToday(Birthday date) {
|
||||
if (!date) {
|
||||
return false;
|
||||
}
|
||||
const auto now = QDate::currentDate();
|
||||
return date.day() == now.day() && date.month() == now.month();
|
||||
}
|
||||
|
||||
rpl::producer<bool> IsBirthdayTodayValue(Birthday date) {
|
||||
return rpl::single() | rpl::then(base::timer_each(
|
||||
60 * crl::time(1000)
|
||||
)) | rpl::map([=] {
|
||||
return IsBirthdayToday(date);
|
||||
}) | rpl::distinct_until_changed();
|
||||
}
|
||||
|
||||
} // namespace Data
|
||||
47
Telegram/SourceFiles/data/data_birthday.h
Normal file
47
Telegram/SourceFiles/data/data_birthday.h
Normal file
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
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 {
|
||||
|
||||
class Birthday final {
|
||||
public:
|
||||
constexpr Birthday() = default;
|
||||
Birthday(int day, int month, int year = 0);
|
||||
|
||||
[[nodiscard]] static Birthday FromSerialized(int value);
|
||||
[[nodiscard]] int serialize() const;
|
||||
|
||||
[[nodiscard]] bool valid() const;
|
||||
|
||||
[[nodiscard]] int day() const;
|
||||
[[nodiscard]] int month() const;
|
||||
[[nodiscard]] int year() const;
|
||||
|
||||
explicit operator bool() const {
|
||||
return valid();
|
||||
}
|
||||
|
||||
friend inline constexpr auto operator<=>(Birthday, Birthday) = default;
|
||||
friend inline constexpr bool operator==(Birthday, Birthday) = default;
|
||||
|
||||
static constexpr auto kYearMin = 1875;
|
||||
static constexpr auto kYearMax = 2100;
|
||||
|
||||
private:
|
||||
int _value = 0;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] QString BirthdayText(Birthday date);
|
||||
[[nodiscard]] QString BirthdayCake();
|
||||
[[nodiscard]] int BirthdayAge(Birthday date);
|
||||
[[nodiscard]] bool IsBirthdayToday(Birthday date);
|
||||
[[nodiscard]] rpl::producer<bool> IsBirthdayTodayValue(Birthday date);
|
||||
|
||||
} // namespace Data
|
||||
73
Telegram/SourceFiles/data/data_boosts.h
Normal file
73
Telegram/SourceFiles/data/data_boosts.h
Normal file
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace Data {
|
||||
|
||||
struct BoostsOverview final {
|
||||
bool group = false;
|
||||
int mine = 0;
|
||||
int level = 0;
|
||||
int boostCount = 0;
|
||||
int currentLevelBoostCount = 0;
|
||||
int nextLevelBoostCount = 0;
|
||||
int premiumMemberCount = 0;
|
||||
float64 premiumMemberPercentage = 0;
|
||||
};
|
||||
|
||||
struct GiftCodeLink final {
|
||||
QString text;
|
||||
QString link;
|
||||
QString slug;
|
||||
};
|
||||
|
||||
struct Boost final {
|
||||
QString id;
|
||||
UserId userId = UserId(0);
|
||||
FullMsgId giveawayMessage;
|
||||
QDateTime date;
|
||||
QDateTime expiresAt;
|
||||
int expiresAfterMonths = 0;
|
||||
GiftCodeLink giftCodeLink;
|
||||
int multiplier = 0;
|
||||
uint64 credits = 0;
|
||||
|
||||
bool isGift = false;
|
||||
bool isGiveaway = false;
|
||||
bool isUnclaimed = false;
|
||||
};
|
||||
|
||||
struct BoostsListSlice final {
|
||||
struct OffsetToken final {
|
||||
QString next;
|
||||
bool gifts = false;
|
||||
};
|
||||
std::vector<Boost> list;
|
||||
int multipliedTotal = 0;
|
||||
bool allLoaded = false;
|
||||
OffsetToken token;
|
||||
};
|
||||
|
||||
struct BoostPrepaidGiveaway final {
|
||||
QDateTime date;
|
||||
uint64 id = 0;
|
||||
uint64 credits = 0;
|
||||
int months = 0;
|
||||
int quantity = 0;
|
||||
int boosts = 0;
|
||||
};
|
||||
|
||||
struct BoostStatus final {
|
||||
BoostsOverview overview;
|
||||
BoostsListSlice firstSliceBoosts;
|
||||
BoostsListSlice firstSliceGifts;
|
||||
std::vector<BoostPrepaidGiveaway> prepaidGiveaway;
|
||||
QString link;
|
||||
};
|
||||
|
||||
} // namespace Data
|
||||
13
Telegram/SourceFiles/data/data_bot_app.cpp
Normal file
13
Telegram/SourceFiles/data/data_bot_app.cpp
Normal file
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
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/data_bot_app.h"
|
||||
|
||||
BotAppData::BotAppData(not_null<Data::Session*> owner, const BotAppId &id)
|
||||
: owner(owner)
|
||||
, id(id) {
|
||||
}
|
||||
27
Telegram/SourceFiles/data/data_bot_app.h
Normal file
27
Telegram/SourceFiles/data/data_bot_app.h
Normal file
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
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_photo.h"
|
||||
#include "data/data_document.h"
|
||||
|
||||
struct BotAppData {
|
||||
BotAppData(not_null<Data::Session*> owner, const BotAppId &id);
|
||||
|
||||
const not_null<Data::Session*> owner;
|
||||
BotAppId id = 0;
|
||||
PeerId botId = 0;
|
||||
QString shortName;
|
||||
QString title;
|
||||
QString description;
|
||||
PhotoData *photo = nullptr;
|
||||
DocumentData *document = nullptr;
|
||||
|
||||
uint64 accessHash = 0;
|
||||
uint64 hash = 0;
|
||||
};
|
||||
383
Telegram/SourceFiles/data/data_changes.cpp
Normal file
383
Telegram/SourceFiles/data/data_changes.cpp
Normal file
@@ -0,0 +1,383 @@
|
||||
/*
|
||||
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/data_changes.h"
|
||||
|
||||
#include "main/main_session.h"
|
||||
|
||||
namespace Data {
|
||||
|
||||
template <typename DataType, typename UpdateType>
|
||||
void Changes::Manager<DataType, UpdateType>::updated(
|
||||
not_null<DataType*> data,
|
||||
Flags flags,
|
||||
bool dropScheduled) {
|
||||
sendRealtimeNotifications(data, flags);
|
||||
if (dropScheduled) {
|
||||
const auto i = _updates.find(data);
|
||||
if (i != _updates.end()) {
|
||||
flags |= i->second;
|
||||
_updates.erase(i);
|
||||
}
|
||||
_stream.fire({ data, flags });
|
||||
} else {
|
||||
_updates[data] |= flags;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename DataType, typename UpdateType>
|
||||
void Changes::Manager<DataType, UpdateType>::sendRealtimeNotifications(
|
||||
not_null<DataType*> data,
|
||||
Flags flags) {
|
||||
for (auto i = 0; i != kCount; ++i) {
|
||||
const auto flag = static_cast<Flag>(1U << i);
|
||||
if (flags & flag) {
|
||||
_realtimeStreams[i].fire({ data, flags });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <typename DataType, typename UpdateType>
|
||||
rpl::producer<UpdateType> Changes::Manager<DataType, UpdateType>::updates(
|
||||
Flags flags) const {
|
||||
return _stream.events(
|
||||
) | rpl::filter([=](const UpdateType &update) {
|
||||
return (update.flags & flags);
|
||||
});
|
||||
}
|
||||
|
||||
template <typename DataType, typename UpdateType>
|
||||
rpl::producer<UpdateType> Changes::Manager<DataType, UpdateType>::updates(
|
||||
not_null<DataType*> data,
|
||||
Flags flags) const {
|
||||
return _stream.events(
|
||||
) | rpl::filter([=](const UpdateType &update) {
|
||||
const auto &[updateData, updateFlags] = update;
|
||||
return (updateData == data) && (updateFlags & flags);
|
||||
});
|
||||
}
|
||||
|
||||
template <typename DataType, typename UpdateType>
|
||||
auto Changes::Manager<DataType, UpdateType>::realtimeUpdates(Flag flag) const
|
||||
-> rpl::producer<UpdateType> {
|
||||
return _realtimeStreams[details::CountBit(flag)].events();
|
||||
}
|
||||
|
||||
template <typename DataType, typename UpdateType>
|
||||
rpl::producer<UpdateType> Changes::Manager<DataType, UpdateType>::flagsValue(
|
||||
not_null<DataType*> data,
|
||||
Flags flags) const {
|
||||
return rpl::single(
|
||||
UpdateType{ data, flags }
|
||||
) | rpl::then(updates(data, flags));
|
||||
}
|
||||
|
||||
template <typename DataType, typename UpdateType>
|
||||
void Changes::Manager<DataType, UpdateType>::drop(not_null<DataType*> data) {
|
||||
_updates.remove(data);
|
||||
}
|
||||
|
||||
template <typename DataType, typename UpdateType>
|
||||
void Changes::Manager<DataType, UpdateType>::sendNotifications() {
|
||||
for (const auto &[data, flags] : base::take(_updates)) {
|
||||
_stream.fire({ data, flags });
|
||||
}
|
||||
}
|
||||
|
||||
Changes::Changes(not_null<Main::Session*> session) : _session(session) {
|
||||
}
|
||||
|
||||
Main::Session &Changes::session() const {
|
||||
return *_session;
|
||||
}
|
||||
|
||||
void Changes::nameUpdated(
|
||||
not_null<PeerData*> peer,
|
||||
base::flat_set<QChar> oldFirstLetters) {
|
||||
_nameStream.fire({ peer, std::move(oldFirstLetters) });
|
||||
}
|
||||
|
||||
rpl::producer<NameUpdate> Changes::realtimeNameUpdates() const {
|
||||
return _nameStream.events();
|
||||
}
|
||||
|
||||
rpl::producer<NameUpdate> Changes::realtimeNameUpdates(
|
||||
not_null<PeerData*> peer) const {
|
||||
return _nameStream.events() | rpl::filter([=](const NameUpdate &update) {
|
||||
return (update.peer == peer);
|
||||
});
|
||||
}
|
||||
|
||||
void Changes::peerUpdated(not_null<PeerData*> peer, PeerUpdate::Flags flags) {
|
||||
_peerChanges.updated(peer, flags);
|
||||
scheduleNotifications();
|
||||
}
|
||||
|
||||
rpl::producer<PeerUpdate> Changes::peerUpdates(
|
||||
PeerUpdate::Flags flags) const {
|
||||
return _peerChanges.updates(flags);
|
||||
}
|
||||
|
||||
rpl::producer<PeerUpdate> Changes::peerUpdates(
|
||||
not_null<PeerData*> peer,
|
||||
PeerUpdate::Flags flags) const {
|
||||
return _peerChanges.updates(peer, flags);
|
||||
}
|
||||
|
||||
rpl::producer<PeerUpdate> Changes::peerFlagsValue(
|
||||
not_null<PeerData*> peer,
|
||||
PeerUpdate::Flags flags) const {
|
||||
return _peerChanges.flagsValue(peer, flags);
|
||||
}
|
||||
|
||||
rpl::producer<PeerUpdate> Changes::realtimePeerUpdates(
|
||||
PeerUpdate::Flag flag) const {
|
||||
return _peerChanges.realtimeUpdates(flag);
|
||||
}
|
||||
|
||||
void Changes::historyUpdated(
|
||||
not_null<History*> history,
|
||||
HistoryUpdate::Flags flags) {
|
||||
_historyChanges.updated(history, flags);
|
||||
scheduleNotifications();
|
||||
}
|
||||
|
||||
rpl::producer<HistoryUpdate> Changes::historyUpdates(
|
||||
HistoryUpdate::Flags flags) const {
|
||||
return _historyChanges.updates(flags);
|
||||
}
|
||||
|
||||
rpl::producer<HistoryUpdate> Changes::historyUpdates(
|
||||
not_null<History*> history,
|
||||
HistoryUpdate::Flags flags) const {
|
||||
return _historyChanges.updates(history, flags);
|
||||
}
|
||||
|
||||
rpl::producer<HistoryUpdate> Changes::historyFlagsValue(
|
||||
not_null<History*> history,
|
||||
HistoryUpdate::Flags flags) const {
|
||||
return _historyChanges.flagsValue(history, flags);
|
||||
}
|
||||
|
||||
rpl::producer<HistoryUpdate> Changes::realtimeHistoryUpdates(
|
||||
HistoryUpdate::Flag flag) const {
|
||||
return _historyChanges.realtimeUpdates(flag);
|
||||
}
|
||||
|
||||
void Changes::topicUpdated(
|
||||
not_null<ForumTopic*> topic,
|
||||
TopicUpdate::Flags flags) {
|
||||
const auto drop = (flags & TopicUpdate::Flag::Destroyed);
|
||||
_topicChanges.updated(topic, flags, drop);
|
||||
if (!drop) {
|
||||
scheduleNotifications();
|
||||
}
|
||||
}
|
||||
|
||||
rpl::producer<TopicUpdate> Changes::topicUpdates(
|
||||
TopicUpdate::Flags flags) const {
|
||||
return _topicChanges.updates(flags);
|
||||
}
|
||||
|
||||
rpl::producer<TopicUpdate> Changes::topicUpdates(
|
||||
not_null<ForumTopic*> topic,
|
||||
TopicUpdate::Flags flags) const {
|
||||
return _topicChanges.updates(topic, flags);
|
||||
}
|
||||
|
||||
rpl::producer<TopicUpdate> Changes::topicFlagsValue(
|
||||
not_null<ForumTopic*> topic,
|
||||
TopicUpdate::Flags flags) const {
|
||||
return _topicChanges.flagsValue(topic, flags);
|
||||
}
|
||||
|
||||
rpl::producer<TopicUpdate> Changes::realtimeTopicUpdates(
|
||||
TopicUpdate::Flag flag) const {
|
||||
return _topicChanges.realtimeUpdates(flag);
|
||||
}
|
||||
|
||||
void Changes::topicRemoved(not_null<ForumTopic*> topic) {
|
||||
_topicChanges.drop(topic);
|
||||
}
|
||||
|
||||
void Changes::sublistUpdated(
|
||||
not_null<SavedSublist*> sublist,
|
||||
SublistUpdate::Flags flags) {
|
||||
const auto drop = (flags & SublistUpdate::Flag::Destroyed);
|
||||
_sublistChanges.updated(sublist, flags, drop);
|
||||
if (!drop) {
|
||||
scheduleNotifications();
|
||||
}
|
||||
}
|
||||
|
||||
rpl::producer<SublistUpdate> Changes::sublistUpdates(
|
||||
SublistUpdate::Flags flags) const {
|
||||
return _sublistChanges.updates(flags);
|
||||
}
|
||||
|
||||
rpl::producer<SublistUpdate> Changes::sublistUpdates(
|
||||
not_null<SavedSublist*> sublist,
|
||||
SublistUpdate::Flags flags) const {
|
||||
return _sublistChanges.updates(sublist, flags);
|
||||
}
|
||||
|
||||
rpl::producer<SublistUpdate> Changes::sublistFlagsValue(
|
||||
not_null<SavedSublist*> sublist,
|
||||
SublistUpdate::Flags flags) const {
|
||||
return _sublistChanges.flagsValue(sublist, flags);
|
||||
}
|
||||
|
||||
rpl::producer<SublistUpdate> Changes::realtimeSublistUpdates(
|
||||
SublistUpdate::Flag flag) const {
|
||||
return _sublistChanges.realtimeUpdates(flag);
|
||||
}
|
||||
|
||||
void Changes::sublistRemoved(not_null<SavedSublist*> sublist) {
|
||||
_sublistChanges.drop(sublist);
|
||||
}
|
||||
|
||||
void Changes::messageUpdated(
|
||||
not_null<HistoryItem*> item,
|
||||
MessageUpdate::Flags flags) {
|
||||
const auto drop = (flags & MessageUpdate::Flag::Destroyed);
|
||||
_messageChanges.updated(item, flags, drop);
|
||||
if (!drop) {
|
||||
scheduleNotifications();
|
||||
}
|
||||
}
|
||||
|
||||
rpl::producer<MessageUpdate> Changes::messageUpdates(
|
||||
MessageUpdate::Flags flags) const {
|
||||
return _messageChanges.updates(flags);
|
||||
}
|
||||
|
||||
rpl::producer<MessageUpdate> Changes::messageUpdates(
|
||||
not_null<HistoryItem*> item,
|
||||
MessageUpdate::Flags flags) const {
|
||||
return _messageChanges.updates(item, flags);
|
||||
}
|
||||
|
||||
rpl::producer<MessageUpdate> Changes::messageFlagsValue(
|
||||
not_null<HistoryItem*> item,
|
||||
MessageUpdate::Flags flags) const {
|
||||
return _messageChanges.flagsValue(item, flags);
|
||||
}
|
||||
|
||||
rpl::producer<MessageUpdate> Changes::realtimeMessageUpdates(
|
||||
MessageUpdate::Flag flag) const {
|
||||
return _messageChanges.realtimeUpdates(flag);
|
||||
}
|
||||
|
||||
void Changes::entryUpdated(
|
||||
not_null<Dialogs::Entry*> entry,
|
||||
EntryUpdate::Flags flags) {
|
||||
const auto drop = (flags & EntryUpdate::Flag::Destroyed);
|
||||
_entryChanges.updated(entry, flags, drop);
|
||||
if (!drop) {
|
||||
scheduleNotifications();
|
||||
}
|
||||
}
|
||||
|
||||
rpl::producer<EntryUpdate> Changes::entryUpdates(
|
||||
EntryUpdate::Flags flags) const {
|
||||
return _entryChanges.updates(flags);
|
||||
}
|
||||
|
||||
rpl::producer<EntryUpdate> Changes::entryUpdates(
|
||||
not_null<Dialogs::Entry*> entry,
|
||||
EntryUpdate::Flags flags) const {
|
||||
return _entryChanges.updates(entry, flags);
|
||||
}
|
||||
|
||||
rpl::producer<EntryUpdate> Changes::entryFlagsValue(
|
||||
not_null<Dialogs::Entry*> entry,
|
||||
EntryUpdate::Flags flags) const {
|
||||
return _entryChanges.flagsValue(entry, flags);
|
||||
}
|
||||
|
||||
rpl::producer<EntryUpdate> Changes::realtimeEntryUpdates(
|
||||
EntryUpdate::Flag flag) const {
|
||||
return _entryChanges.realtimeUpdates(flag);
|
||||
}
|
||||
|
||||
void Changes::entryRemoved(not_null<Dialogs::Entry*> entry) {
|
||||
_entryChanges.drop(entry);
|
||||
}
|
||||
|
||||
void Changes::storyUpdated(
|
||||
not_null<Story*> story,
|
||||
StoryUpdate::Flags flags) {
|
||||
const auto drop = (flags & StoryUpdate::Flag::Destroyed);
|
||||
_storyChanges.updated(story, flags, drop);
|
||||
if (!drop) {
|
||||
scheduleNotifications();
|
||||
}
|
||||
}
|
||||
|
||||
rpl::producer<StoryUpdate> Changes::storyUpdates(
|
||||
StoryUpdate::Flags flags) const {
|
||||
return _storyChanges.updates(flags);
|
||||
}
|
||||
|
||||
rpl::producer<StoryUpdate> Changes::storyUpdates(
|
||||
not_null<Story*> story,
|
||||
StoryUpdate::Flags flags) const {
|
||||
return _storyChanges.updates(story, flags);
|
||||
}
|
||||
|
||||
rpl::producer<StoryUpdate> Changes::storyFlagsValue(
|
||||
not_null<Story*> story,
|
||||
StoryUpdate::Flags flags) const {
|
||||
return _storyChanges.flagsValue(story, flags);
|
||||
}
|
||||
|
||||
rpl::producer<StoryUpdate> Changes::realtimeStoryUpdates(
|
||||
StoryUpdate::Flag flag) const {
|
||||
return _storyChanges.realtimeUpdates(flag);
|
||||
}
|
||||
|
||||
void Changes::chatAdminChanged(
|
||||
not_null<PeerData*> peer,
|
||||
not_null<UserData*> user,
|
||||
ChatAdminRights rights,
|
||||
QString rank) {
|
||||
_chatAdminChanges.fire({
|
||||
.peer = peer,
|
||||
.user = user,
|
||||
.rights = rights,
|
||||
.rank = std::move(rank),
|
||||
});
|
||||
}
|
||||
|
||||
rpl::producer<ChatAdminChange> Changes::chatAdminChanges() const {
|
||||
return _chatAdminChanges.events();
|
||||
}
|
||||
|
||||
void Changes::scheduleNotifications() {
|
||||
if (!_notify) {
|
||||
_notify = true;
|
||||
crl::on_main(&session(), [=] {
|
||||
sendNotifications();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void Changes::sendNotifications() {
|
||||
if (!_notify) {
|
||||
return;
|
||||
}
|
||||
_notify = false;
|
||||
_peerChanges.sendNotifications();
|
||||
_historyChanges.sendNotifications();
|
||||
_messageChanges.sendNotifications();
|
||||
_entryChanges.sendNotifications();
|
||||
_topicChanges.sendNotifications();
|
||||
_sublistChanges.sendNotifications();
|
||||
_storyChanges.sendNotifications();
|
||||
}
|
||||
|
||||
} // namespace Data
|
||||
462
Telegram/SourceFiles/data/data_changes.h
Normal file
462
Telegram/SourceFiles/data/data_changes.h
Normal file
@@ -0,0 +1,462 @@
|
||||
/*
|
||||
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/flags.h"
|
||||
#include "data/data_chat_participant_status.h"
|
||||
|
||||
class History;
|
||||
class PeerData;
|
||||
class HistoryItem;
|
||||
|
||||
namespace Dialogs {
|
||||
class Entry;
|
||||
} // namespace Dialogs
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace Data::details {
|
||||
|
||||
template <typename Flag>
|
||||
inline constexpr int CountBit(Flag Last = Flag::LastUsedBit) {
|
||||
auto i = 0;
|
||||
while ((1ULL << i) != static_cast<uint64>(Last)) {
|
||||
++i;
|
||||
Assert(i != 64);
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
} // namespace Data::details
|
||||
|
||||
namespace Data {
|
||||
|
||||
class ForumTopic;
|
||||
class SavedSublist;
|
||||
class Story;
|
||||
|
||||
struct NameUpdate {
|
||||
NameUpdate(
|
||||
not_null<PeerData*> peer,
|
||||
base::flat_set<QChar> oldFirstLetters)
|
||||
: peer(peer)
|
||||
, oldFirstLetters(std::move(oldFirstLetters)) {
|
||||
}
|
||||
|
||||
not_null<PeerData*> peer;
|
||||
base::flat_set<QChar> oldFirstLetters;
|
||||
};
|
||||
|
||||
struct PeerUpdate {
|
||||
enum class Flag : uint64 {
|
||||
None = 0,
|
||||
|
||||
// Common flags
|
||||
Name = (1ULL << 0),
|
||||
Username = (1ULL << 1),
|
||||
Photo = (1ULL << 2),
|
||||
About = (1ULL << 3),
|
||||
Notifications = (1ULL << 4),
|
||||
Migration = (1ULL << 5),
|
||||
UnavailableReason = (1ULL << 6),
|
||||
ChatThemeToken = (1ULL << 7),
|
||||
ChatWallPaper = (1ULL << 8),
|
||||
IsBlocked = (1ULL << 9),
|
||||
MessagesTTL = (1ULL << 10),
|
||||
FullInfo = (1ULL << 11),
|
||||
Usernames = (1ULL << 12),
|
||||
TranslationDisabled = (1ULL << 13),
|
||||
Color = (1ULL << 14),
|
||||
ColorProfile = (1ULL << 15),
|
||||
BackgroundEmoji = (1ULL << 16),
|
||||
StoriesState = (1ULL << 17),
|
||||
VerifyInfo = (1ULL << 18),
|
||||
StarsPerMessage = (1ULL << 19),
|
||||
|
||||
// For users
|
||||
CanShareContact = (1ULL << 20),
|
||||
IsContact = (1ULL << 21),
|
||||
PhoneNumber = (1ULL << 22),
|
||||
OnlineStatus = (1ULL << 23),
|
||||
BotCommands = (1ULL << 24),
|
||||
BotCanBeInvited = (1ULL << 25),
|
||||
BotStartToken = (1ULL << 26),
|
||||
CommonChats = (1ULL << 27),
|
||||
PeerGifts = (1ULL << 28),
|
||||
HasCalls = (1ULL << 29),
|
||||
SupportInfo = (1ULL << 30),
|
||||
IsBot = (1ULL << 31),
|
||||
EmojiStatus = (1ULL << 32),
|
||||
BusinessDetails = (1ULL << 33),
|
||||
Birthday = (1ULL << 34),
|
||||
PersonalChannel = (1ULL << 35),
|
||||
StarRefProgram = (1ULL << 36),
|
||||
PaysPerMessage = (1ULL << 37),
|
||||
GiftSettings = (1ULL << 38),
|
||||
StarsRating = (1ULL << 39),
|
||||
ContactNote = (1ULL << 40),
|
||||
|
||||
// For chats and channels
|
||||
InviteLinks = (1ULL << 41),
|
||||
Members = (1ULL << 42),
|
||||
Admins = (1ULL << 43),
|
||||
BannedUsers = (1ULL << 44),
|
||||
Rights = (1ULL << 45),
|
||||
PendingRequests = (1ULL << 46),
|
||||
Reactions = (1ULL << 47),
|
||||
|
||||
// For channels
|
||||
ChannelAmIn = (1ULL << 48),
|
||||
StickersSet = (1ULL << 49),
|
||||
EmojiSet = (1ULL << 50),
|
||||
DiscussionLink = (1ULL << 51),
|
||||
MonoforumLink = (1ULL << 52),
|
||||
ChannelLocation = (1ULL << 53),
|
||||
Slowmode = (1ULL << 54),
|
||||
GroupCall = (1ULL << 55),
|
||||
|
||||
// For iteration
|
||||
LastUsedBit = (1ULL << 55),
|
||||
};
|
||||
using Flags = base::flags<Flag>;
|
||||
friend inline constexpr auto is_flag_type(Flag) { return true; }
|
||||
|
||||
not_null<PeerData*> peer;
|
||||
Flags flags = 0;
|
||||
|
||||
};
|
||||
|
||||
struct HistoryUpdate {
|
||||
enum class Flag : uint32 {
|
||||
None = 0,
|
||||
|
||||
IsPinned = (1U << 0),
|
||||
UnreadView = (1U << 1),
|
||||
TopPromoted = (1U << 2),
|
||||
Folder = (1U << 3),
|
||||
UnreadMentions = (1U << 4),
|
||||
UnreadReactions = (1U << 5),
|
||||
ClientSideMessages = (1U << 6),
|
||||
ChatOccupied = (1U << 7),
|
||||
MessageSent = (1U << 8),
|
||||
ScheduledSent = (1U << 9),
|
||||
OutboxRead = (1U << 10),
|
||||
BotKeyboard = (1U << 11),
|
||||
CloudDraft = (1U << 12),
|
||||
TranslateFrom = (1U << 13),
|
||||
TranslatedTo = (1U << 14),
|
||||
|
||||
LastUsedBit = (1U << 14),
|
||||
};
|
||||
using Flags = base::flags<Flag>;
|
||||
friend inline constexpr auto is_flag_type(Flag) { return true; }
|
||||
|
||||
not_null<History*> history;
|
||||
Flags flags = 0;
|
||||
|
||||
};
|
||||
|
||||
struct TopicUpdate {
|
||||
enum class Flag : uint32 {
|
||||
None = 0,
|
||||
|
||||
UnreadView = (1U << 1),
|
||||
UnreadMentions = (1U << 2),
|
||||
UnreadReactions = (1U << 3),
|
||||
Notifications = (1U << 4),
|
||||
Title = (1U << 5),
|
||||
IconId = (1U << 6),
|
||||
ColorId = (1U << 7),
|
||||
CloudDraft = (1U << 8),
|
||||
Closed = (1U << 9),
|
||||
Creator = (1U << 10),
|
||||
Destroyed = (1U << 11),
|
||||
|
||||
LastUsedBit = (1U << 11),
|
||||
};
|
||||
using Flags = base::flags<Flag>;
|
||||
friend inline constexpr auto is_flag_type(Flag) { return true; }
|
||||
|
||||
not_null<ForumTopic*> topic;
|
||||
Flags flags = 0;
|
||||
|
||||
};
|
||||
|
||||
struct SublistUpdate {
|
||||
enum class Flag : uint32 {
|
||||
None = 0,
|
||||
|
||||
UnreadView = (1U << 1),
|
||||
UnreadReactions = (1U << 2),
|
||||
CloudDraft = (1U << 3),
|
||||
Destroyed = (1U << 4),
|
||||
|
||||
LastUsedBit = (1U << 4),
|
||||
};
|
||||
using Flags = base::flags<Flag>;
|
||||
friend inline constexpr auto is_flag_type(Flag) { return true; }
|
||||
|
||||
not_null<SavedSublist*> sublist;
|
||||
Flags flags = 0;
|
||||
|
||||
};
|
||||
|
||||
struct MessageUpdate {
|
||||
enum class Flag : uint32 {
|
||||
None = 0,
|
||||
|
||||
Edited = (1U << 0),
|
||||
Destroyed = (1U << 1),
|
||||
DialogRowRepaint = (1U << 2),
|
||||
DialogRowRefresh = (1U << 3),
|
||||
NewAdded = (1U << 4),
|
||||
ReplyMarkup = (1U << 5),
|
||||
BotCallbackSent = (1U << 6),
|
||||
NewMaybeAdded = (1U << 7),
|
||||
ReplyToTopAdded = (1U << 8),
|
||||
NewUnreadReaction = (1U << 9),
|
||||
|
||||
LastUsedBit = (1U << 9),
|
||||
};
|
||||
using Flags = base::flags<Flag>;
|
||||
friend inline constexpr auto is_flag_type(Flag) { return true; }
|
||||
|
||||
not_null<HistoryItem*> item;
|
||||
Flags flags = 0;
|
||||
|
||||
};
|
||||
|
||||
struct EntryUpdate {
|
||||
enum class Flag : uint32 {
|
||||
None = 0,
|
||||
|
||||
Repaint = (1U << 0),
|
||||
HasPinnedMessages = (1U << 1),
|
||||
ForwardDraft = (1U << 2),
|
||||
LocalDraftSet = (1U << 3),
|
||||
Height = (1U << 4),
|
||||
Destroyed = (1U << 5),
|
||||
|
||||
LastUsedBit = (1U << 5),
|
||||
};
|
||||
using Flags = base::flags<Flag>;
|
||||
friend inline constexpr auto is_flag_type(Flag) { return true; }
|
||||
|
||||
not_null<Dialogs::Entry*> entry;
|
||||
Flags flags = 0;
|
||||
|
||||
};
|
||||
|
||||
struct StoryUpdate {
|
||||
enum class Flag : uint32 {
|
||||
None = 0,
|
||||
|
||||
Edited = (1U << 0),
|
||||
Destroyed = (1U << 1),
|
||||
NewAdded = (1U << 2),
|
||||
ViewsChanged = (1U << 3),
|
||||
MarkRead = (1U << 4),
|
||||
Reaction = (1U << 5),
|
||||
|
||||
LastUsedBit = (1U << 5),
|
||||
};
|
||||
using Flags = base::flags<Flag>;
|
||||
friend inline constexpr auto is_flag_type(Flag) { return true; }
|
||||
|
||||
not_null<Story*> story;
|
||||
Flags flags = 0;
|
||||
|
||||
};
|
||||
|
||||
struct ChatAdminChange {
|
||||
not_null<PeerData*> peer;
|
||||
not_null<UserData*> user;
|
||||
ChatAdminRights rights;
|
||||
QString rank;
|
||||
};
|
||||
|
||||
class Changes final {
|
||||
public:
|
||||
explicit Changes(not_null<Main::Session*> session);
|
||||
|
||||
[[nodiscard]] Main::Session &session() const;
|
||||
|
||||
void nameUpdated(
|
||||
not_null<PeerData*> peer,
|
||||
base::flat_set<QChar> oldFirstLetters);
|
||||
[[nodiscard]] rpl::producer<NameUpdate> realtimeNameUpdates() const;
|
||||
[[nodiscard]] rpl::producer<NameUpdate> realtimeNameUpdates(
|
||||
not_null<PeerData*> peer) const;
|
||||
|
||||
void peerUpdated(not_null<PeerData*> peer, PeerUpdate::Flags flags);
|
||||
[[nodiscard]] rpl::producer<PeerUpdate> peerUpdates(
|
||||
PeerUpdate::Flags flags) const;
|
||||
[[nodiscard]] rpl::producer<PeerUpdate> peerUpdates(
|
||||
not_null<PeerData*> peer,
|
||||
PeerUpdate::Flags flags) const;
|
||||
[[nodiscard]] rpl::producer<PeerUpdate> peerFlagsValue(
|
||||
not_null<PeerData*> peer,
|
||||
PeerUpdate::Flags flags) const;
|
||||
[[nodiscard]] rpl::producer<PeerUpdate> realtimePeerUpdates(
|
||||
PeerUpdate::Flag flag) const;
|
||||
|
||||
void historyUpdated(
|
||||
not_null<History*> history,
|
||||
HistoryUpdate::Flags flags);
|
||||
[[nodiscard]] rpl::producer<HistoryUpdate> historyUpdates(
|
||||
HistoryUpdate::Flags flags) const;
|
||||
[[nodiscard]] rpl::producer<HistoryUpdate> historyUpdates(
|
||||
not_null<History*> history,
|
||||
HistoryUpdate::Flags flags) const;
|
||||
[[nodiscard]] rpl::producer<HistoryUpdate> historyFlagsValue(
|
||||
not_null<History*> history,
|
||||
HistoryUpdate::Flags flags) const;
|
||||
[[nodiscard]] rpl::producer<HistoryUpdate> realtimeHistoryUpdates(
|
||||
HistoryUpdate::Flag flag) const;
|
||||
|
||||
void topicUpdated(
|
||||
not_null<ForumTopic*> topic,
|
||||
TopicUpdate::Flags flags);
|
||||
[[nodiscard]] rpl::producer<TopicUpdate> topicUpdates(
|
||||
TopicUpdate::Flags flags) const;
|
||||
[[nodiscard]] rpl::producer<TopicUpdate> topicUpdates(
|
||||
not_null<ForumTopic*> topic,
|
||||
TopicUpdate::Flags flags) const;
|
||||
[[nodiscard]] rpl::producer<TopicUpdate> topicFlagsValue(
|
||||
not_null<ForumTopic*> topic,
|
||||
TopicUpdate::Flags flags) const;
|
||||
[[nodiscard]] rpl::producer<TopicUpdate> realtimeTopicUpdates(
|
||||
TopicUpdate::Flag flag) const;
|
||||
void topicRemoved(not_null<ForumTopic*> topic);
|
||||
|
||||
void sublistUpdated(
|
||||
not_null<SavedSublist*> sublist,
|
||||
SublistUpdate::Flags flags);
|
||||
[[nodiscard]] rpl::producer<SublistUpdate> sublistUpdates(
|
||||
SublistUpdate::Flags flags) const;
|
||||
[[nodiscard]] rpl::producer<SublistUpdate> sublistUpdates(
|
||||
not_null<SavedSublist*> sublist,
|
||||
SublistUpdate::Flags flags) const;
|
||||
[[nodiscard]] rpl::producer<SublistUpdate> sublistFlagsValue(
|
||||
not_null<SavedSublist*> sublist,
|
||||
SublistUpdate::Flags flags) const;
|
||||
[[nodiscard]] rpl::producer<SublistUpdate> realtimeSublistUpdates(
|
||||
SublistUpdate::Flag flag) const;
|
||||
void sublistRemoved(not_null<SavedSublist*> sublist);
|
||||
|
||||
void messageUpdated(
|
||||
not_null<HistoryItem*> item,
|
||||
MessageUpdate::Flags flags);
|
||||
[[nodiscard]] rpl::producer<MessageUpdate> messageUpdates(
|
||||
MessageUpdate::Flags flags) const;
|
||||
[[nodiscard]] rpl::producer<MessageUpdate> messageUpdates(
|
||||
not_null<HistoryItem*> item,
|
||||
MessageUpdate::Flags flags) const;
|
||||
[[nodiscard]] rpl::producer<MessageUpdate> messageFlagsValue(
|
||||
not_null<HistoryItem*> item,
|
||||
MessageUpdate::Flags flags) const;
|
||||
[[nodiscard]] rpl::producer<MessageUpdate> realtimeMessageUpdates(
|
||||
MessageUpdate::Flag flag) const;
|
||||
|
||||
void entryUpdated(
|
||||
not_null<Dialogs::Entry*> entry,
|
||||
EntryUpdate::Flags flags);
|
||||
[[nodiscard]] rpl::producer<EntryUpdate> entryUpdates(
|
||||
EntryUpdate::Flags flags) const;
|
||||
[[nodiscard]] rpl::producer<EntryUpdate> entryUpdates(
|
||||
not_null<Dialogs::Entry*> entry,
|
||||
EntryUpdate::Flags flags) const;
|
||||
[[nodiscard]] rpl::producer<EntryUpdate> entryFlagsValue(
|
||||
not_null<Dialogs::Entry*> entry,
|
||||
EntryUpdate::Flags flags) const;
|
||||
[[nodiscard]] rpl::producer<EntryUpdate> realtimeEntryUpdates(
|
||||
EntryUpdate::Flag flag) const;
|
||||
void entryRemoved(not_null<Dialogs::Entry*> entry);
|
||||
|
||||
void storyUpdated(
|
||||
not_null<Story*> story,
|
||||
StoryUpdate::Flags flags);
|
||||
[[nodiscard]] rpl::producer<StoryUpdate> storyUpdates(
|
||||
StoryUpdate::Flags flags) const;
|
||||
[[nodiscard]] rpl::producer<StoryUpdate> storyUpdates(
|
||||
not_null<Story*> story,
|
||||
StoryUpdate::Flags flags) const;
|
||||
[[nodiscard]] rpl::producer<StoryUpdate> storyFlagsValue(
|
||||
not_null<Story*> story,
|
||||
StoryUpdate::Flags flags) const;
|
||||
[[nodiscard]] rpl::producer<StoryUpdate> realtimeStoryUpdates(
|
||||
StoryUpdate::Flag flag) const;
|
||||
|
||||
void chatAdminChanged(
|
||||
not_null<PeerData*> peer,
|
||||
not_null<UserData*> user,
|
||||
ChatAdminRights rights,
|
||||
QString rank);
|
||||
[[nodiscard]] rpl::producer<ChatAdminChange> chatAdminChanges() const;
|
||||
|
||||
void sendNotifications();
|
||||
|
||||
private:
|
||||
template <typename DataType, typename UpdateType>
|
||||
class Manager final {
|
||||
public:
|
||||
using Flag = typename UpdateType::Flag;
|
||||
using Flags = typename UpdateType::Flags;
|
||||
|
||||
void updated(
|
||||
not_null<DataType*> data,
|
||||
Flags flags,
|
||||
bool dropScheduled = false);
|
||||
[[nodiscard]] rpl::producer<UpdateType> updates(Flags flags) const;
|
||||
[[nodiscard]] rpl::producer<UpdateType> updates(
|
||||
not_null<DataType*> data,
|
||||
Flags flags) const;
|
||||
[[nodiscard]] rpl::producer<UpdateType> flagsValue(
|
||||
not_null<DataType*> data,
|
||||
Flags flags) const;
|
||||
[[nodiscard]] rpl::producer<UpdateType> realtimeUpdates(
|
||||
Flag flag) const;
|
||||
|
||||
void drop(not_null<DataType*> data);
|
||||
|
||||
void sendNotifications();
|
||||
|
||||
private:
|
||||
static constexpr auto kCount = details::CountBit<Flag>() + 1;
|
||||
|
||||
void sendRealtimeNotifications(
|
||||
not_null<DataType*> data,
|
||||
Flags flags);
|
||||
|
||||
std::array<rpl::event_stream<UpdateType>, kCount> _realtimeStreams;
|
||||
base::flat_map<not_null<DataType*>, Flags> _updates;
|
||||
rpl::event_stream<UpdateType> _stream;
|
||||
|
||||
};
|
||||
|
||||
void scheduleNotifications();
|
||||
|
||||
const not_null<Main::Session*> _session;
|
||||
|
||||
rpl::event_stream<NameUpdate> _nameStream;
|
||||
Manager<PeerData, PeerUpdate> _peerChanges;
|
||||
Manager<History, HistoryUpdate> _historyChanges;
|
||||
Manager<ForumTopic, TopicUpdate> _topicChanges;
|
||||
Manager<SavedSublist, SublistUpdate> _sublistChanges;
|
||||
Manager<HistoryItem, MessageUpdate> _messageChanges;
|
||||
Manager<Dialogs::Entry, EntryUpdate> _entryChanges;
|
||||
Manager<Story, StoryUpdate> _storyChanges;
|
||||
rpl::event_stream<ChatAdminChange> _chatAdminChanges;
|
||||
|
||||
bool _notify = false;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Data
|
||||
1512
Telegram/SourceFiles/data/data_channel.cpp
Normal file
1512
Telegram/SourceFiles/data/data_channel.cpp
Normal file
File diff suppressed because it is too large
Load Diff
641
Telegram/SourceFiles/data/data_channel.h
Normal file
641
Telegram/SourceFiles/data/data_channel.h
Normal file
@@ -0,0 +1,641 @@
|
||||
/*
|
||||
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_peer.h"
|
||||
#include "data/data_pts_waiter.h"
|
||||
#include "data/data_location.h"
|
||||
#include "data/data_chat_participant_status.h"
|
||||
#include "data/data_peer_bot_commands.h"
|
||||
#include "data/data_user_names.h"
|
||||
|
||||
class ChannelData;
|
||||
|
||||
namespace Data {
|
||||
class Forum;
|
||||
class SavedMessages;
|
||||
} // namespace Data
|
||||
|
||||
struct ChannelLocation {
|
||||
QString address;
|
||||
Data::LocationPoint point;
|
||||
|
||||
friend inline bool operator==(
|
||||
const ChannelLocation &a,
|
||||
const ChannelLocation &b) {
|
||||
return a.address.isEmpty()
|
||||
? b.address.isEmpty()
|
||||
: (a.address == b.address && a.point == b.point);
|
||||
}
|
||||
friend inline bool operator!=(
|
||||
const ChannelLocation &a,
|
||||
const ChannelLocation &b) {
|
||||
return !(a == b);
|
||||
}
|
||||
};
|
||||
|
||||
enum class ChannelDataFlag : uint64 {
|
||||
Left = (1ULL << 0),
|
||||
Creator = (1ULL << 1),
|
||||
Forbidden = (1ULL << 2),
|
||||
CallActive = (1ULL << 3),
|
||||
CallNotEmpty = (1ULL << 4),
|
||||
Signatures = (1ULL << 5),
|
||||
Verified = (1ULL << 6),
|
||||
Scam = (1ULL << 7),
|
||||
Fake = (1ULL << 8),
|
||||
Megagroup = (1ULL << 9),
|
||||
Broadcast = (1ULL << 10),
|
||||
Gigagroup = (1ULL << 11),
|
||||
Username = (1ULL << 12),
|
||||
Location = (1ULL << 13),
|
||||
CanSetUsername = (1ULL << 14),
|
||||
CanSetStickers = (1ULL << 15),
|
||||
PreHistoryHidden = (1ULL << 16),
|
||||
CanViewParticipants = (1ULL << 17),
|
||||
HasLink = (1ULL << 18),
|
||||
SlowmodeEnabled = (1ULL << 19),
|
||||
NoForwards = (1ULL << 20),
|
||||
JoinToWrite = (1ULL << 21),
|
||||
RequestToJoin = (1ULL << 22),
|
||||
Forum = (1ULL << 23),
|
||||
AntiSpam = (1ULL << 24),
|
||||
ParticipantsHidden = (1ULL << 25),
|
||||
StoriesHidden = (1ULL << 26),
|
||||
HasActiveStories = (1ULL << 27),
|
||||
HasUnreadStories = (1ULL << 28),
|
||||
CanGetStatistics = (1ULL << 29),
|
||||
ViewAsMessages = (1ULL << 30),
|
||||
SimilarExpanded = (1ULL << 31),
|
||||
CanViewRevenue = (1ULL << 32),
|
||||
PaidMediaAllowed = (1ULL << 33),
|
||||
CanViewCreditsRevenue = (1ULL << 34),
|
||||
SignatureProfiles = (1ULL << 35),
|
||||
StargiftsAvailable = (1ULL << 36),
|
||||
PaidMessagesAvailable = (1ULL << 37),
|
||||
AutoTranslation = (1ULL << 38),
|
||||
Monoforum = (1ULL << 39),
|
||||
MonoforumAdmin = (1ULL << 40),
|
||||
MonoforumDisabled = (1ULL << 41),
|
||||
ForumTabs = (1ULL << 42),
|
||||
HasStarsPerMessage = (1ULL << 43),
|
||||
StarsPerMessageKnown = (1ULL << 44),
|
||||
HasActiveVideoStream = (1ULL << 45),
|
||||
};
|
||||
inline constexpr bool is_flag_type(ChannelDataFlag) { return true; };
|
||||
using ChannelDataFlags = base::flags<ChannelDataFlag>;
|
||||
|
||||
class MegagroupInfo {
|
||||
public:
|
||||
MegagroupInfo();
|
||||
~MegagroupInfo();
|
||||
|
||||
struct Admin {
|
||||
explicit Admin(ChatAdminRightsInfo rights)
|
||||
: rights(rights) {
|
||||
}
|
||||
Admin(ChatAdminRightsInfo rights, bool canEdit)
|
||||
: rights(rights)
|
||||
, canEdit(canEdit) {
|
||||
}
|
||||
ChatAdminRightsInfo rights;
|
||||
bool canEdit = false;
|
||||
};
|
||||
|
||||
struct Restricted {
|
||||
explicit Restricted(ChatRestrictionsInfo rights)
|
||||
: rights(rights) {
|
||||
}
|
||||
ChatRestrictionsInfo rights;
|
||||
};
|
||||
|
||||
ChatData *getMigrateFromChat() const;
|
||||
void setMigrateFromChat(ChatData *chat);
|
||||
|
||||
const ChannelLocation *getLocation() const;
|
||||
void setLocation(const ChannelLocation &location);
|
||||
|
||||
Data::ChatBotCommands::Changed setBotCommands(
|
||||
const std::vector<Data::BotCommands> &commands);
|
||||
[[nodiscard]] const Data::ChatBotCommands &botCommands() const {
|
||||
return _botCommands;
|
||||
}
|
||||
|
||||
void ensureForum(not_null<ChannelData*> that);
|
||||
[[nodiscard]] Data::Forum *forum() const;
|
||||
[[nodiscard]] std::unique_ptr<Data::Forum> takeForumData();
|
||||
|
||||
void ensureMonoforum(not_null<ChannelData*> that);
|
||||
[[nodiscard]] Data::SavedMessages *monoforum() const;
|
||||
[[nodiscard]] std::unique_ptr<Data::SavedMessages> takeMonoforumData();
|
||||
|
||||
std::deque<not_null<UserData*>> lastParticipants;
|
||||
base::flat_map<not_null<UserData*>, Admin> lastAdmins;
|
||||
base::flat_map<not_null<UserData*>, Restricted> lastRestricted;
|
||||
base::flat_set<not_null<PeerData*>> markupSenders;
|
||||
base::flat_set<not_null<UserData*>> bots;
|
||||
rpl::event_stream<bool> unrestrictedByBoostsChanges;
|
||||
|
||||
// For admin badges, full admins list with ranks.
|
||||
base::flat_map<UserId, QString> admins;
|
||||
|
||||
UserData *creator = nullptr; // nullptr means unknown
|
||||
QString creatorRank;
|
||||
int botStatus = 0; // -1 - no bots, 0 - unknown, 1 - one bot, that sees all history, 2 - other
|
||||
bool joinedMessageFound = false;
|
||||
bool adminsLoaded = false;
|
||||
StickerSetIdentifier stickerSet;
|
||||
StickerSetIdentifier emojiSet;
|
||||
|
||||
enum LastParticipantsStatus {
|
||||
LastParticipantsUpToDate = 0x00,
|
||||
LastParticipantsOnceReceived = 0x01,
|
||||
LastParticipantsCountOutdated = 0x02,
|
||||
};
|
||||
mutable int lastParticipantsStatus = LastParticipantsUpToDate;
|
||||
int lastParticipantsCount = 0;
|
||||
int boostsApplied = 0;
|
||||
int boostsUnrestrict = 0;
|
||||
|
||||
int slowmodeSeconds = 0;
|
||||
TimeId slowmodeLastMessage = 0;
|
||||
|
||||
private:
|
||||
ChatData *_migratedFrom = nullptr;
|
||||
ChannelLocation _location;
|
||||
Data::ChatBotCommands _botCommands;
|
||||
std::unique_ptr<Data::Forum> _forum;
|
||||
std::unique_ptr<Data::SavedMessages> _monoforum;
|
||||
|
||||
friend class ChannelData;
|
||||
|
||||
};
|
||||
|
||||
class ChannelData final : public PeerData {
|
||||
public:
|
||||
using Flag = ChannelDataFlag;
|
||||
using Flags = Data::Flags<ChannelDataFlags>;
|
||||
|
||||
using AdminRight = ChatAdminRight;
|
||||
using Restriction = ChatRestriction;
|
||||
using AdminRights = ChatAdminRights;
|
||||
using Restrictions = ChatRestrictions;
|
||||
using AdminRightFlags = Data::Flags<AdminRights>;
|
||||
using RestrictionFlags = Data::Flags<Restrictions>;
|
||||
|
||||
ChannelData(not_null<Data::Session*> owner, PeerId id);
|
||||
|
||||
void setName(const QString &name, const QString &username);
|
||||
void setUsername(const QString &username);
|
||||
void setUsernames(const Data::Usernames &newUsernames);
|
||||
void setPhoto(const MTPChatPhoto &photo);
|
||||
|
||||
[[nodiscard]] uint64 accessHash() const {
|
||||
return _accessHash;
|
||||
}
|
||||
void setAccessHash(uint64 accessHash);
|
||||
|
||||
void setFlags(ChannelDataFlags which);
|
||||
void addFlags(ChannelDataFlags which);
|
||||
void removeFlags(ChannelDataFlags which);
|
||||
[[nodiscard]] auto flags() const {
|
||||
return _flags.current();
|
||||
}
|
||||
[[nodiscard]] auto flagsValue() const {
|
||||
return _flags.value();
|
||||
}
|
||||
|
||||
[[nodiscard]] QString username() const;
|
||||
[[nodiscard]] QString editableUsername() const;
|
||||
[[nodiscard]] const std::vector<QString> &usernames() const;
|
||||
[[nodiscard]] bool isUsernameEditable(QString username) const;
|
||||
|
||||
[[nodiscard]] int membersCount() const {
|
||||
return std::max(_membersCount, 1);
|
||||
}
|
||||
void setMembersCount(int newMembersCount);
|
||||
[[nodiscard]] bool membersCountKnown() const {
|
||||
return (_membersCount >= 0);
|
||||
}
|
||||
|
||||
[[nodiscard]] int adminsCount() const {
|
||||
return _adminsCount;
|
||||
}
|
||||
void setAdminsCount(int newAdminsCount);
|
||||
|
||||
[[nodiscard]] int restrictedCount() const {
|
||||
return _restrictedCount;
|
||||
}
|
||||
void setRestrictedCount(int newRestrictedCount);
|
||||
|
||||
[[nodiscard]] int kickedCount() const {
|
||||
return _kickedCount;
|
||||
}
|
||||
void setKickedCount(int newKickedCount);
|
||||
|
||||
[[nodiscard]] int pendingRequestsCount() const {
|
||||
return _pendingRequestsCount;
|
||||
}
|
||||
[[nodiscard]] const std::vector<UserId> &recentRequesters() const {
|
||||
return _recentRequesters;
|
||||
}
|
||||
void setPendingRequestsCount(
|
||||
int count,
|
||||
const QVector<MTPlong> &recentRequesters);
|
||||
void setPendingRequestsCount(
|
||||
int count,
|
||||
std::vector<UserId> recentRequesters);
|
||||
|
||||
[[nodiscard]] bool haveLeft() const {
|
||||
return flags() & Flag::Left;
|
||||
}
|
||||
[[nodiscard]] bool amIn() const {
|
||||
return !isForbidden() && !haveLeft();
|
||||
}
|
||||
[[nodiscard]] bool addsSignature() const {
|
||||
return flags() & Flag::Signatures;
|
||||
}
|
||||
[[nodiscard]] bool signatureProfiles() const {
|
||||
return flags() & Flag::SignatureProfiles;
|
||||
}
|
||||
[[nodiscard]] bool isForbidden() const {
|
||||
return flags() & Flag::Forbidden;
|
||||
}
|
||||
[[nodiscard]] bool isVerified() const {
|
||||
return flags() & Flag::Verified;
|
||||
}
|
||||
[[nodiscard]] bool isScam() const {
|
||||
return flags() & Flag::Scam;
|
||||
}
|
||||
[[nodiscard]] bool isFake() const {
|
||||
return flags() & Flag::Fake;
|
||||
}
|
||||
[[nodiscard]] bool hasStoriesHidden() const {
|
||||
return flags() & Flag::StoriesHidden;
|
||||
}
|
||||
[[nodiscard]] bool viewForumAsMessages() const {
|
||||
return flags() & Flag::ViewAsMessages;
|
||||
}
|
||||
[[nodiscard]] bool stargiftsAvailable() const {
|
||||
return flags() & Flag::StargiftsAvailable;
|
||||
}
|
||||
[[nodiscard]] bool paidMessagesAvailable() const {
|
||||
return flags() & Flag::PaidMessagesAvailable;
|
||||
}
|
||||
[[nodiscard]] bool hasStarsPerMessage() const {
|
||||
return flags() & Flag::HasStarsPerMessage;
|
||||
}
|
||||
[[nodiscard]] bool starsPerMessageKnown() const {
|
||||
return flags() & Flag::StarsPerMessageKnown;
|
||||
}
|
||||
[[nodiscard]] bool useSubsectionTabs() const;
|
||||
|
||||
[[nodiscard]] static ChatRestrictionsInfo KickedRestrictedRights(
|
||||
not_null<PeerData*> participant);
|
||||
static constexpr auto kRestrictUntilForever = TimeId(INT_MAX);
|
||||
[[nodiscard]] static bool IsRestrictedForever(TimeId until) {
|
||||
return !until || (until == kRestrictUntilForever);
|
||||
}
|
||||
void applyEditAdmin(
|
||||
not_null<UserData*> user,
|
||||
ChatAdminRightsInfo oldRights,
|
||||
ChatAdminRightsInfo newRights,
|
||||
const QString &rank);
|
||||
void applyEditBanned(
|
||||
not_null<PeerData*> participant,
|
||||
ChatRestrictionsInfo oldRights,
|
||||
ChatRestrictionsInfo newRights);
|
||||
void setViewAsMessagesFlag(bool enabled);
|
||||
|
||||
void markForbidden();
|
||||
|
||||
[[nodiscard]] bool isGroupAdmin(not_null<UserData*> user) const;
|
||||
[[nodiscard]] bool lastParticipantsRequestNeeded() const;
|
||||
[[nodiscard]] bool isMegagroup() const {
|
||||
return flags() & Flag::Megagroup;
|
||||
}
|
||||
[[nodiscard]] bool isBroadcast() const {
|
||||
return flags() & Flag::Broadcast;
|
||||
}
|
||||
[[nodiscard]] bool isGigagroup() const {
|
||||
return flags() & Flag::Gigagroup;
|
||||
}
|
||||
[[nodiscard]] bool isForum() const {
|
||||
return flags() & Flag::Forum;
|
||||
}
|
||||
[[nodiscard]] bool isMonoforum() const {
|
||||
return flags() & Flag::Monoforum;
|
||||
}
|
||||
[[nodiscard]] bool hasUsername() const {
|
||||
return flags() & Flag::Username;
|
||||
}
|
||||
[[nodiscard]] bool hasLocation() const {
|
||||
return flags() & Flag::Location;
|
||||
}
|
||||
[[nodiscard]] bool isPublic() const {
|
||||
return hasUsername() || hasLocation();
|
||||
}
|
||||
[[nodiscard]] bool amCreator() const {
|
||||
return flags() & Flag::Creator;
|
||||
}
|
||||
[[nodiscard]] bool joinToWrite() const {
|
||||
return flags() & Flag::JoinToWrite;
|
||||
}
|
||||
[[nodiscard]] bool requestToJoin() const {
|
||||
return flags() & Flag::RequestToJoin;
|
||||
}
|
||||
[[nodiscard]] bool antiSpamMode() const {
|
||||
return flags() & Flag::AntiSpam;
|
||||
}
|
||||
[[nodiscard]] bool autoTranslation() const {
|
||||
return flags() & Flag::AutoTranslation;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto adminRights() const {
|
||||
return _adminRights.current();
|
||||
}
|
||||
[[nodiscard]] auto adminRightsValue() const {
|
||||
return _adminRights.value();
|
||||
}
|
||||
void setAdminRights(ChatAdminRights rights);
|
||||
[[nodiscard]] bool hasAdminRights() const {
|
||||
return (adminRights() != 0);
|
||||
}
|
||||
|
||||
[[nodiscard]] auto restrictions() const {
|
||||
return _restrictions.current();
|
||||
}
|
||||
[[nodiscard]] auto restrictionsValue() const {
|
||||
return _restrictions.value();
|
||||
}
|
||||
[[nodiscard]] TimeId restrictedUntil() const {
|
||||
return _restrictedUntil;
|
||||
}
|
||||
void setRestrictions(ChatRestrictionsInfo rights);
|
||||
[[nodiscard]] bool hasRestrictions() const {
|
||||
return (restrictions() != 0);
|
||||
}
|
||||
[[nodiscard]] bool hasRestrictions(TimeId now) const {
|
||||
return hasRestrictions()
|
||||
&& (restrictedUntil() > now);
|
||||
}
|
||||
|
||||
[[nodiscard]] auto defaultRestrictions() const {
|
||||
return _defaultRestrictions.current();
|
||||
}
|
||||
[[nodiscard]] auto defaultRestrictionsValue() const {
|
||||
return _defaultRestrictions.value();
|
||||
}
|
||||
void setDefaultRestrictions(ChatRestrictions rights);
|
||||
|
||||
// Like in ChatData.
|
||||
[[nodiscard]] bool allowsForwarding() const;
|
||||
[[nodiscard]] bool canEditInformation() const;
|
||||
[[nodiscard]] bool canEditPermissions() const;
|
||||
[[nodiscard]] bool canEditUsername() const;
|
||||
[[nodiscard]] bool canEditPreHistoryHidden() const;
|
||||
[[nodiscard]] bool canAddMembers() const;
|
||||
[[nodiscard]] bool canAddAdmins() const;
|
||||
[[nodiscard]] bool canBanMembers() const;
|
||||
[[nodiscard]] bool anyoneCanAddMembers() const;
|
||||
|
||||
[[nodiscard]] bool canPostMessages() const;
|
||||
[[nodiscard]] bool canEditMessages() const;
|
||||
[[nodiscard]] bool canDeleteMessages() const;
|
||||
[[nodiscard]] bool canPostStories() const;
|
||||
[[nodiscard]] bool canEditStories() const;
|
||||
[[nodiscard]] bool canDeleteStories() const;
|
||||
[[nodiscard]] bool canPostPaidMedia() const;
|
||||
[[nodiscard]] bool canAccessMonoforum() const;
|
||||
[[nodiscard]] bool hiddenPreHistory() const;
|
||||
[[nodiscard]] bool canViewMembers() const;
|
||||
[[nodiscard]] bool canViewAdmins() const;
|
||||
[[nodiscard]] bool canViewBanned() const;
|
||||
[[nodiscard]] bool canEditSignatures() const;
|
||||
[[nodiscard]] bool canEditAutoTranslate() const;
|
||||
[[nodiscard]] bool canEditStickers() const;
|
||||
[[nodiscard]] bool canEditEmoji() const;
|
||||
[[nodiscard]] bool canDelete() const;
|
||||
[[nodiscard]] bool canEditAdmin(not_null<UserData*> user) const;
|
||||
[[nodiscard]] bool canRestrictParticipant(
|
||||
not_null<PeerData*> participant) const;
|
||||
|
||||
void setBotVerifyDetails(Ui::BotVerifyDetails details);
|
||||
void setBotVerifyDetailsIcon(DocumentId iconId);
|
||||
[[nodiscard]] Ui::BotVerifyDetails *botVerifyDetails() const {
|
||||
return _botVerifyDetails.get();
|
||||
}
|
||||
|
||||
void setInviteLink(const QString &newInviteLink);
|
||||
[[nodiscard]] QString inviteLink() const {
|
||||
return _inviteLink;
|
||||
}
|
||||
[[nodiscard]] bool canHaveInviteLink() const;
|
||||
|
||||
void setLocation(const MTPChannelLocation &data);
|
||||
[[nodiscard]] const ChannelLocation *getLocation() const;
|
||||
|
||||
void setDiscussionLink(ChannelData *link);
|
||||
[[nodiscard]] ChannelData *discussionLink() const;
|
||||
[[nodiscard]] bool discussionLinkKnown() const;
|
||||
|
||||
void setMonoforumLink(ChannelData *link);
|
||||
[[nodiscard]] ChannelData *monoforumLink() const;
|
||||
[[nodiscard]] bool monoforumDisabled() const;
|
||||
|
||||
void ptsInit(int32 pts) {
|
||||
_ptsWaiter.init(pts);
|
||||
}
|
||||
void ptsReceived(int32 pts) {
|
||||
_ptsWaiter.updateAndApply(this, pts, 0);
|
||||
}
|
||||
bool ptsUpdateAndApply(int32 pts, int32 count) {
|
||||
return _ptsWaiter.updateAndApply(this, pts, count);
|
||||
}
|
||||
bool ptsUpdateAndApply(
|
||||
int32 pts,
|
||||
int32 count,
|
||||
const MTPUpdate &update) {
|
||||
return _ptsWaiter.updateAndApply(this, pts, count, update);
|
||||
}
|
||||
bool ptsUpdateAndApply(
|
||||
int32 pts,
|
||||
int32 count,
|
||||
const MTPUpdates &updates) {
|
||||
return _ptsWaiter.updateAndApply(this, pts, count, updates);
|
||||
}
|
||||
[[nodiscard]] int32 pts() const {
|
||||
return _ptsWaiter.current();
|
||||
}
|
||||
[[nodiscard]] bool ptsInited() const {
|
||||
return _ptsWaiter.inited();
|
||||
}
|
||||
[[nodiscard]] bool ptsRequesting() const {
|
||||
return _ptsWaiter.requesting();
|
||||
}
|
||||
void ptsSetRequesting(bool isRequesting) {
|
||||
return _ptsWaiter.setRequesting(isRequesting);
|
||||
}
|
||||
// < 0 - not waiting
|
||||
void ptsSetWaitingForShortPoll(int32 ms) {
|
||||
return _ptsWaiter.setWaitingForShortPoll(this, ms);
|
||||
}
|
||||
[[nodiscard]] bool ptsWaitingForSkipped() const {
|
||||
return _ptsWaiter.waitingForSkipped();
|
||||
}
|
||||
[[nodiscard]] bool ptsWaitingForShortPoll() const {
|
||||
return _ptsWaiter.waitingForShortPoll();
|
||||
}
|
||||
|
||||
[[nodiscard]] MsgId availableMinId() const {
|
||||
return _availableMinId;
|
||||
}
|
||||
void setAvailableMinId(MsgId availableMinId);
|
||||
|
||||
[[nodiscard]] ChatData *getMigrateFromChat() const;
|
||||
void setMigrateFromChat(ChatData *chat);
|
||||
|
||||
[[nodiscard]] int slowmodeSeconds() const;
|
||||
void setSlowmodeSeconds(int seconds);
|
||||
[[nodiscard]] TimeId slowmodeLastMessage() const;
|
||||
void growSlowmodeLastMessage(TimeId when);
|
||||
|
||||
void setStarsPerMessage(int stars);
|
||||
[[nodiscard]] int starsPerMessage() const;
|
||||
[[nodiscard]] int commonStarsPerMessage() const;
|
||||
|
||||
[[nodiscard]] int peerGiftsCount() const;
|
||||
void setPeerGiftsCount(int count);
|
||||
|
||||
[[nodiscard]] int boostsApplied() const;
|
||||
[[nodiscard]] int boostsUnrestrict() const;
|
||||
[[nodiscard]] bool unrestrictedByBoosts() const;
|
||||
[[nodiscard]] rpl::producer<bool> unrestrictedByBoostsValue() const;
|
||||
void setBoostsUnrestrict(int applied, int unrestrict);
|
||||
|
||||
void setInvitePeek(const QString &hash, TimeId expires);
|
||||
void clearInvitePeek();
|
||||
[[nodiscard]] TimeId invitePeekExpires() const;
|
||||
[[nodiscard]] QString invitePeekHash() const;
|
||||
void privateErrorReceived();
|
||||
|
||||
[[nodiscard]] Data::GroupCall *groupCall() const {
|
||||
return _call.get();
|
||||
}
|
||||
void migrateCall(std::unique_ptr<Data::GroupCall> call);
|
||||
void setGroupCall(
|
||||
const MTPInputGroupCall &call,
|
||||
TimeId scheduleDate = 0,
|
||||
bool rtmp = false);
|
||||
void clearGroupCall();
|
||||
void setGroupCallDefaultJoinAs(PeerId peerId);
|
||||
[[nodiscard]] PeerId groupCallDefaultJoinAs() const;
|
||||
|
||||
void setAllowedReactions(Data::AllowedReactions value);
|
||||
[[nodiscard]] const Data::AllowedReactions &allowedReactions() const;
|
||||
|
||||
[[nodiscard]] bool hasActiveStories() const;
|
||||
[[nodiscard]] bool hasUnreadStories() const;
|
||||
[[nodiscard]] bool hasActiveVideoStream() const;
|
||||
void setStoriesState(StoriesState state);
|
||||
|
||||
[[nodiscard]] Data::Forum *forum() const {
|
||||
return mgInfo ? mgInfo->forum() : nullptr;
|
||||
}
|
||||
[[nodiscard]] Data::SavedMessages *monoforum() const {
|
||||
return mgInfo ? mgInfo->monoforum() : nullptr;
|
||||
}
|
||||
|
||||
[[nodiscard]] int levelHint() const;
|
||||
void updateLevelHint(int levelHint);
|
||||
|
||||
[[nodiscard]] TimeId subscriptionUntilDate() const;
|
||||
void updateSubscriptionUntilDate(TimeId subscriptionUntilDate);
|
||||
|
||||
[[nodiscard]] MTPInputChannel inputChannel() const;
|
||||
|
||||
// Still public data members.
|
||||
int32 date = 0;
|
||||
std::unique_ptr<MegagroupInfo> mgInfo;
|
||||
|
||||
// > 0 - user who invited me to channel, < 0 - not in channel.
|
||||
UserId inviter = 0;
|
||||
TimeId inviteDate = 0;
|
||||
bool inviteViaRequest = false;
|
||||
|
||||
private:
|
||||
struct InvitePeek {
|
||||
QString hash;
|
||||
TimeId expires = 0;
|
||||
};
|
||||
|
||||
auto unavailableReasons() const
|
||||
-> const std::vector<Data::UnavailableReason> & override;
|
||||
bool canEditLastAdmin(not_null<UserData*> user) const;
|
||||
|
||||
void setUnavailableReasonsList(
|
||||
std::vector<Data::UnavailableReason> &&reasons) override;
|
||||
|
||||
Flags _flags = ChannelDataFlags(Flag::Forbidden);
|
||||
|
||||
PtsWaiter _ptsWaiter;
|
||||
|
||||
Data::UsernamesInfo _username;
|
||||
|
||||
std::vector<UserId> _recentRequesters;
|
||||
MsgId _availableMinId = 0;
|
||||
|
||||
uint64 _accessHash = 0;
|
||||
|
||||
RestrictionFlags _defaultRestrictions;
|
||||
AdminRightFlags _adminRights;
|
||||
RestrictionFlags _restrictions;
|
||||
TimeId _restrictedUntil;
|
||||
TimeId _subscriptionUntilDate;
|
||||
|
||||
std::vector<Data::UnavailableReason> _unavailableReasons;
|
||||
std::unique_ptr<InvitePeek> _invitePeek;
|
||||
QString _inviteLink;
|
||||
|
||||
ChannelData *_discussionLink = nullptr;
|
||||
ChannelData *_monoforumLink = nullptr;
|
||||
bool _discussionLinkKnown = false;
|
||||
|
||||
int _peerGiftsCount = 0;
|
||||
int _membersCount = -1;
|
||||
int _adminsCount = 1;
|
||||
int _restrictedCount = 0;
|
||||
int _kickedCount = 0;
|
||||
int _pendingRequestsCount = 0;
|
||||
int _levelHint = 0;
|
||||
int _starsPerMessage = 0;
|
||||
|
||||
Data::AllowedReactions _allowedReactions;
|
||||
|
||||
std::unique_ptr<Data::GroupCall> _call;
|
||||
PeerId _callDefaultJoinAs = 0;
|
||||
|
||||
std::unique_ptr<Ui::BotVerifyDetails> _botVerifyDetails;
|
||||
|
||||
};
|
||||
|
||||
namespace Data {
|
||||
|
||||
void ApplyMigration(
|
||||
not_null<ChatData*> chat,
|
||||
not_null<ChannelData*> channel);
|
||||
|
||||
void ApplyChannelUpdate(
|
||||
not_null<ChannelData*> channel,
|
||||
const MTPDupdateChatDefaultBannedRights &update);
|
||||
|
||||
void ApplyChannelUpdate(
|
||||
not_null<ChannelData*> channel,
|
||||
const MTPDchannelFull &update);
|
||||
|
||||
} // namespace Data
|
||||
47
Telegram/SourceFiles/data/data_channel_admins.cpp
Normal file
47
Telegram/SourceFiles/data/data_channel_admins.cpp
Normal file
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
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/data_channel_admins.h"
|
||||
|
||||
#include "history/history.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_session.h"
|
||||
#include "main/main_session.h"
|
||||
|
||||
namespace Data {
|
||||
|
||||
ChannelAdminChanges::ChannelAdminChanges(not_null<ChannelData*> channel)
|
||||
: _channel(channel)
|
||||
, _admins(_channel->mgInfo->admins) {
|
||||
}
|
||||
|
||||
void ChannelAdminChanges::add(UserId userId, const QString &rank) {
|
||||
const auto i = _admins.find(userId);
|
||||
if (i == end(_admins) || i->second != rank) {
|
||||
_admins[userId] = rank;
|
||||
_changes.emplace(userId);
|
||||
}
|
||||
}
|
||||
|
||||
void ChannelAdminChanges::remove(UserId userId) {
|
||||
if (_admins.contains(userId)) {
|
||||
_admins.remove(userId);
|
||||
_changes.emplace(userId);
|
||||
}
|
||||
}
|
||||
|
||||
ChannelAdminChanges::~ChannelAdminChanges() {
|
||||
if (_changes.size() > 1
|
||||
|| (!_changes.empty()
|
||||
&& _changes.front() != _channel->session().userId())) {
|
||||
if (const auto history = _channel->owner().historyLoaded(_channel)) {
|
||||
history->applyGroupAdminChanges(_changes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Data
|
||||
28
Telegram/SourceFiles/data/data_channel_admins.h
Normal file
28
Telegram/SourceFiles/data/data_channel_admins.h
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
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 {
|
||||
|
||||
class ChannelAdminChanges {
|
||||
public:
|
||||
ChannelAdminChanges(not_null<ChannelData*> channel);
|
||||
|
||||
void add(UserId userId, const QString &rank);
|
||||
void remove(UserId userId);
|
||||
|
||||
~ChannelAdminChanges();
|
||||
|
||||
private:
|
||||
not_null<ChannelData*> _channel;
|
||||
base::flat_map<UserId, QString> &_admins;
|
||||
base::flat_set<UserId> _changes;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Data
|
||||
42
Telegram/SourceFiles/data/data_channel_earn.h
Normal file
42
Telegram/SourceFiles/data/data_channel_earn.h
Normal file
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
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 <QtCore/QDateTime>
|
||||
|
||||
#include "data/data_credits.h"
|
||||
#include "data/data_statistics_chart.h"
|
||||
|
||||
namespace Data {
|
||||
|
||||
using EarnInt = uint64;
|
||||
|
||||
struct EarnHistorySlice final {
|
||||
using OffsetToken = QString;
|
||||
std::vector<CreditsHistoryEntry> list;
|
||||
int total = 0;
|
||||
bool allLoaded = false;
|
||||
OffsetToken token;
|
||||
};
|
||||
|
||||
struct EarnStatistics final {
|
||||
explicit operator bool() const {
|
||||
return !!usdRate;
|
||||
}
|
||||
Data::StatisticalGraph topHoursGraph;
|
||||
Data::StatisticalGraph revenueGraph;
|
||||
CreditsAmount currentBalance;
|
||||
CreditsAmount availableBalance;
|
||||
CreditsAmount overallRevenue;
|
||||
float64 usdRate = 0.;
|
||||
bool switchedOff = false;
|
||||
|
||||
EarnHistorySlice firstHistorySlice;
|
||||
};
|
||||
|
||||
} // namespace Data
|
||||
591
Telegram/SourceFiles/data/data_chat.cpp
Normal file
591
Telegram/SourceFiles/data/data_chat.cpp
Normal file
@@ -0,0 +1,591 @@
|
||||
/*
|
||||
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/data_chat.h"
|
||||
|
||||
#include "core/application.h"
|
||||
#include "data/data_user.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_changes.h"
|
||||
#include "data/data_group_call.h"
|
||||
#include "data/data_message_reactions.h"
|
||||
#include "data/notify/data_notify_settings.h"
|
||||
#include "history/history.h"
|
||||
#include "main/main_session.h"
|
||||
#include "apiwrap.h"
|
||||
#include "api/api_invite_links.h"
|
||||
|
||||
namespace {
|
||||
|
||||
using UpdateFlag = Data::PeerUpdate::Flag;
|
||||
|
||||
} // namespace
|
||||
|
||||
ChatData::ChatData(not_null<Data::Session*> owner, PeerId id)
|
||||
: PeerData(owner, id) {
|
||||
_flags.changes(
|
||||
) | rpl::on_next([=](const Flags::Change &change) {
|
||||
if (change.diff & Flag::CallNotEmpty) {
|
||||
if (const auto history = this->owner().historyLoaded(this)) {
|
||||
history->updateChatListEntry();
|
||||
}
|
||||
}
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
void ChatData::setPhoto(const MTPChatPhoto &photo) {
|
||||
photo.match([&](const MTPDchatPhoto &data) {
|
||||
updateUserpic(
|
||||
data.vphoto_id().v,
|
||||
data.vdc_id().v,
|
||||
data.is_has_video());
|
||||
}, [&](const MTPDchatPhotoEmpty &) {
|
||||
clearUserpic();
|
||||
});
|
||||
}
|
||||
|
||||
ChatAdminRightsInfo ChatData::defaultAdminRights(not_null<UserData*> user) {
|
||||
const auto isCreator = (creator == peerToUser(user->id))
|
||||
|| (user->isSelf() && amCreator());
|
||||
using Flag = ChatAdminRight;
|
||||
return ChatAdminRightsInfo(Flag::Other
|
||||
| Flag::ChangeInfo
|
||||
| Flag::DeleteMessages
|
||||
| Flag::BanUsers
|
||||
| Flag::InviteByLinkOrAdd
|
||||
| Flag::PinMessages
|
||||
| Flag::ManageCall
|
||||
| (isCreator ? Flag::AddAdmins : Flag(0)));
|
||||
}
|
||||
|
||||
bool ChatData::allowsForwarding() const {
|
||||
return !(flags() & Flag::NoForwards);
|
||||
}
|
||||
|
||||
bool ChatData::canEditInformation() const {
|
||||
return amIn() && !amRestricted(ChatRestriction::ChangeInfo);
|
||||
}
|
||||
|
||||
bool ChatData::canEditPermissions() const {
|
||||
return amIn()
|
||||
&& (amCreator() || (adminRights() & ChatAdminRight::BanUsers));
|
||||
}
|
||||
|
||||
bool ChatData::canEditUsername() const {
|
||||
return amCreator()
|
||||
&& (flags() & Flag::CanSetUsername);
|
||||
}
|
||||
|
||||
bool ChatData::canEditPreHistoryHidden() const {
|
||||
return amCreator();
|
||||
}
|
||||
|
||||
bool ChatData::canDeleteMessages() const {
|
||||
return amCreator()
|
||||
|| (adminRights() & ChatAdminRight::DeleteMessages);
|
||||
}
|
||||
|
||||
bool ChatData::canAddMembers() const {
|
||||
return amIn() && !amRestricted(ChatRestriction::AddParticipants);
|
||||
}
|
||||
|
||||
bool ChatData::canAddAdmins() const {
|
||||
return amIn() && amCreator();
|
||||
}
|
||||
|
||||
bool ChatData::canBanMembers() const {
|
||||
return amCreator()
|
||||
|| (adminRights() & ChatAdminRight::BanUsers);
|
||||
}
|
||||
|
||||
bool ChatData::anyoneCanAddMembers() const {
|
||||
return !(defaultRestrictions() & ChatRestriction::AddParticipants);
|
||||
}
|
||||
|
||||
void ChatData::setName(const QString &newName) {
|
||||
updateNameDelayed(newName.isEmpty() ? name() : newName, {}, {});
|
||||
}
|
||||
|
||||
void ChatData::applyEditAdmin(not_null<UserData*> user, bool isAdmin) {
|
||||
if (isAdmin) {
|
||||
admins.emplace(user);
|
||||
} else {
|
||||
admins.remove(user);
|
||||
}
|
||||
session().changes().peerUpdated(this, UpdateFlag::Admins);
|
||||
}
|
||||
|
||||
void ChatData::invalidateParticipants() {
|
||||
participants.clear();
|
||||
admins.clear();
|
||||
setAdminRights(ChatAdminRights());
|
||||
//setDefaultRestrictions(ChatRestrictions());
|
||||
invitedByMe.clear();
|
||||
botStatus = 0;
|
||||
session().changes().peerUpdated(
|
||||
this,
|
||||
UpdateFlag::Members | UpdateFlag::Admins);
|
||||
}
|
||||
|
||||
void ChatData::setFlags(ChatDataFlags which) {
|
||||
const auto wasIn = amIn();
|
||||
_flags.set(which);
|
||||
if (wasIn && !amIn()) {
|
||||
crl::on_main(&session(), [=] {
|
||||
if (!amIn()) {
|
||||
Core::App().closeChatFromWindows(this);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void ChatData::setInviteLink(const QString &newInviteLink) {
|
||||
_inviteLink = newInviteLink;
|
||||
}
|
||||
|
||||
bool ChatData::canHaveInviteLink() const {
|
||||
return amCreator()
|
||||
|| (adminRights() & ChatAdminRight::InviteByLinkOrAdd);
|
||||
}
|
||||
|
||||
void ChatData::setAdminRights(ChatAdminRights rights) {
|
||||
if (rights == adminRights()) {
|
||||
return;
|
||||
}
|
||||
_adminRights.set(rights);
|
||||
if (!canHaveInviteLink()) {
|
||||
setPendingRequestsCount(0, std::vector<UserId>{});
|
||||
}
|
||||
session().changes().peerUpdated(
|
||||
this,
|
||||
UpdateFlag::Rights | UpdateFlag::Admins | UpdateFlag::BannedUsers);
|
||||
}
|
||||
|
||||
void ChatData::setDefaultRestrictions(ChatRestrictions rights) {
|
||||
if (rights == defaultRestrictions()) {
|
||||
return;
|
||||
}
|
||||
_defaultRestrictions.set(rights);
|
||||
session().changes().peerUpdated(this, UpdateFlag::Rights);
|
||||
}
|
||||
|
||||
void ChatData::refreshBotStatus() {
|
||||
if (participants.empty()) {
|
||||
botStatus = 0;
|
||||
} else {
|
||||
const auto bot = ranges::none_of(participants, &UserData::isBot);
|
||||
botStatus = bot ? -1 : 2;
|
||||
}
|
||||
}
|
||||
|
||||
auto ChatData::applyUpdateVersion(int version) -> UpdateStatus {
|
||||
if (_version > version) {
|
||||
return UpdateStatus::TooOld;
|
||||
} else if (_version + 1 < version) {
|
||||
invalidateParticipants();
|
||||
session().api().requestFullPeer(this);
|
||||
return UpdateStatus::Skipped;
|
||||
}
|
||||
setVersion(version);
|
||||
return UpdateStatus::Good;
|
||||
}
|
||||
|
||||
ChannelData *ChatData::getMigrateToChannel() const {
|
||||
return _migratedTo;
|
||||
}
|
||||
|
||||
void ChatData::setMigrateToChannel(ChannelData *channel) {
|
||||
if (_migratedTo != channel) {
|
||||
_migratedTo = channel;
|
||||
if (channel->amIn()) {
|
||||
session().changes().peerUpdated(this, UpdateFlag::Migration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ChatData::setGroupCall(
|
||||
const MTPInputGroupCall &call,
|
||||
TimeId scheduleDate,
|
||||
bool rtmp) {
|
||||
if (migrateTo()) {
|
||||
return;
|
||||
}
|
||||
call.match([&](const MTPDinputGroupCall &data) {
|
||||
if (_call && _call->id() == data.vid().v) {
|
||||
return;
|
||||
} else if (!_call && !data.vid().v) {
|
||||
return;
|
||||
} else if (!data.vid().v) {
|
||||
clearGroupCall();
|
||||
return;
|
||||
}
|
||||
const auto hasCall = (_call != nullptr);
|
||||
if (hasCall) {
|
||||
owner().unregisterGroupCall(_call.get());
|
||||
}
|
||||
_call = std::make_unique<Data::GroupCall>(
|
||||
this,
|
||||
data.vid().v,
|
||||
data.vaccess_hash().v,
|
||||
scheduleDate,
|
||||
rtmp,
|
||||
Data::GroupCallOrigin::Group);
|
||||
owner().registerGroupCall(_call.get());
|
||||
session().changes().peerUpdated(this, UpdateFlag::GroupCall);
|
||||
addFlags(Flag::CallActive);
|
||||
}, [&](const auto &) {
|
||||
clearGroupCall();
|
||||
});
|
||||
}
|
||||
|
||||
void ChatData::clearGroupCall() {
|
||||
if (!_call) {
|
||||
return;
|
||||
} else if (const auto group = migrateTo(); group && !group->groupCall()) {
|
||||
group->migrateCall(base::take(_call));
|
||||
} else {
|
||||
owner().unregisterGroupCall(_call.get());
|
||||
_call = nullptr;
|
||||
}
|
||||
session().changes().peerUpdated(this, UpdateFlag::GroupCall);
|
||||
removeFlags(Flag::CallActive | Flag::CallNotEmpty);
|
||||
}
|
||||
|
||||
void ChatData::setGroupCallDefaultJoinAs(PeerId peerId) {
|
||||
_callDefaultJoinAs = peerId;
|
||||
}
|
||||
|
||||
PeerId ChatData::groupCallDefaultJoinAs() const {
|
||||
return _callDefaultJoinAs;
|
||||
}
|
||||
|
||||
void ChatData::setBotCommands(const std::vector<Data::BotCommands> &list) {
|
||||
if (_botCommands.update(list)) {
|
||||
owner().botCommandsChanged(this);
|
||||
}
|
||||
}
|
||||
|
||||
void ChatData::setPendingRequestsCount(
|
||||
int count,
|
||||
const QVector<MTPlong> &recentRequesters) {
|
||||
setPendingRequestsCount(count, ranges::views::all(
|
||||
recentRequesters
|
||||
) | ranges::views::transform([&](const MTPlong &value) {
|
||||
return UserId(value);
|
||||
}) | ranges::to_vector);
|
||||
}
|
||||
|
||||
void ChatData::setPendingRequestsCount(
|
||||
int count,
|
||||
std::vector<UserId> recentRequesters) {
|
||||
if (_pendingRequestsCount != count
|
||||
|| _recentRequesters != recentRequesters) {
|
||||
_pendingRequestsCount = count;
|
||||
_recentRequesters = std::move(recentRequesters);
|
||||
session().changes().peerUpdated(this, UpdateFlag::PendingRequests);
|
||||
}
|
||||
}
|
||||
|
||||
void ChatData::setAllowedReactions(Data::AllowedReactions value) {
|
||||
if (_allowedReactions != value) {
|
||||
const auto enabled = [](const Data::AllowedReactions &allowed) {
|
||||
return (allowed.type != Data::AllowedReactionsType::Some)
|
||||
|| !allowed.some.empty()
|
||||
|| allowed.paidEnabled;
|
||||
};
|
||||
const auto was = enabled(_allowedReactions);
|
||||
_allowedReactions = std::move(value);
|
||||
const auto now = enabled(_allowedReactions);
|
||||
if (was != now) {
|
||||
owner().reactions().updateAllInHistory(this, now);
|
||||
}
|
||||
session().changes().peerUpdated(this, UpdateFlag::Reactions);
|
||||
}
|
||||
}
|
||||
|
||||
const Data::AllowedReactions &ChatData::allowedReactions() const {
|
||||
return _allowedReactions;
|
||||
}
|
||||
|
||||
MTPlong ChatData::inputChat() const {
|
||||
return MTP_long(peerToChat(id).bare);
|
||||
}
|
||||
|
||||
namespace Data {
|
||||
|
||||
void ApplyChatUpdate(
|
||||
not_null<ChatData*> chat,
|
||||
const MTPDupdateChatParticipants &update) {
|
||||
ApplyChatUpdate(chat, update.vparticipants());
|
||||
}
|
||||
|
||||
void ApplyChatUpdate(
|
||||
not_null<ChatData*> chat,
|
||||
const MTPDupdateChatParticipantAdd &update) {
|
||||
if (chat->applyUpdateVersion(update.vversion().v)
|
||||
!= ChatData::UpdateStatus::Good) {
|
||||
return;
|
||||
} else if (chat->count < 0) {
|
||||
return;
|
||||
}
|
||||
const auto user = chat->owner().userLoaded(update.vuser_id().v);
|
||||
const auto session = &chat->session();
|
||||
if (!user
|
||||
|| (!chat->participants.empty()
|
||||
&& chat->participants.contains(user))) {
|
||||
chat->invalidateParticipants();
|
||||
++chat->count;
|
||||
return;
|
||||
}
|
||||
if (chat->participants.empty()) {
|
||||
if (chat->count > 0) { // If the count is known.
|
||||
++chat->count;
|
||||
}
|
||||
chat->botStatus = 0;
|
||||
} else {
|
||||
chat->participants.emplace(user);
|
||||
if (UserId(update.vinviter_id()) == session->userId()) {
|
||||
chat->invitedByMe.insert(user);
|
||||
} else {
|
||||
chat->invitedByMe.remove(user);
|
||||
}
|
||||
++chat->count;
|
||||
if (user->isBot()) {
|
||||
chat->botStatus = 2;
|
||||
if (!user->botInfo->inited) {
|
||||
session->api().requestFullPeer(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
session->changes().peerUpdated(chat, UpdateFlag::Members);
|
||||
}
|
||||
|
||||
void ApplyChatUpdate(
|
||||
not_null<ChatData*> chat,
|
||||
const MTPDupdateChatParticipantDelete &update) {
|
||||
if (chat->applyUpdateVersion(update.vversion().v)
|
||||
!= ChatData::UpdateStatus::Good) {
|
||||
return;
|
||||
} else if (chat->count <= 0) {
|
||||
return;
|
||||
}
|
||||
const auto user = chat->owner().userLoaded(update.vuser_id().v);
|
||||
if (!user
|
||||
|| (!chat->participants.empty()
|
||||
&& !chat->participants.contains(user))) {
|
||||
chat->invalidateParticipants();
|
||||
--chat->count;
|
||||
return;
|
||||
}
|
||||
if (chat->participants.empty()) {
|
||||
if (chat->count > 0) {
|
||||
chat->count--;
|
||||
}
|
||||
chat->botStatus = 0;
|
||||
} else {
|
||||
chat->participants.erase(user);
|
||||
chat->count--;
|
||||
chat->invitedByMe.remove(user);
|
||||
chat->admins.remove(user);
|
||||
if (user->isSelf()) {
|
||||
chat->setAdminRights(ChatAdminRights());
|
||||
}
|
||||
if (const auto history = chat->owner().historyLoaded(chat)) {
|
||||
if (history->lastKeyboardFrom == user->id) {
|
||||
history->clearLastKeyboard();
|
||||
}
|
||||
}
|
||||
if (chat->botStatus > 0 && user->isBot()) {
|
||||
chat->refreshBotStatus();
|
||||
}
|
||||
}
|
||||
chat->session().changes().peerUpdated(chat, UpdateFlag::Members);
|
||||
}
|
||||
|
||||
void ApplyChatUpdate(
|
||||
not_null<ChatData*> chat,
|
||||
const MTPDupdateChatParticipantAdmin &update) {
|
||||
if (chat->applyUpdateVersion(update.vversion().v)
|
||||
!= ChatData::UpdateStatus::Good) {
|
||||
return;
|
||||
}
|
||||
const auto session = &chat->session();
|
||||
const auto user = chat->owner().userLoaded(update.vuser_id().v);
|
||||
if (!user) {
|
||||
chat->invalidateParticipants();
|
||||
return;
|
||||
}
|
||||
if (user->isSelf()) {
|
||||
chat->setAdminRights(mtpIsTrue(update.vis_admin())
|
||||
? chat->defaultAdminRights(user).flags
|
||||
: ChatAdminRights());
|
||||
}
|
||||
if (mtpIsTrue(update.vis_admin())) {
|
||||
if (chat->noParticipantInfo()) {
|
||||
session->api().requestFullPeer(chat);
|
||||
} else {
|
||||
chat->admins.emplace(user);
|
||||
}
|
||||
} else {
|
||||
chat->admins.erase(user);
|
||||
}
|
||||
session->changes().peerUpdated(chat, UpdateFlag::Admins);
|
||||
}
|
||||
|
||||
void ApplyChatUpdate(
|
||||
not_null<ChatData*> chat,
|
||||
const MTPDupdateChatDefaultBannedRights &update) {
|
||||
if (chat->applyUpdateVersion(update.vversion().v)
|
||||
!= ChatData::UpdateStatus::Good) {
|
||||
return;
|
||||
}
|
||||
chat->setDefaultRestrictions(ChatRestrictionsInfo(
|
||||
update.vdefault_banned_rights()).flags);
|
||||
}
|
||||
|
||||
void ApplyChatUpdate(not_null<ChatData*> chat, const MTPDchatFull &update) {
|
||||
ApplyChatUpdate(chat, update.vparticipants());
|
||||
|
||||
if (const auto call = update.vcall()) {
|
||||
chat->setGroupCall(*call);
|
||||
} else {
|
||||
chat->clearGroupCall();
|
||||
}
|
||||
if (const auto as = update.vgroupcall_default_join_as()) {
|
||||
chat->setGroupCallDefaultJoinAs(peerFromMTP(*as));
|
||||
} else {
|
||||
chat->setGroupCallDefaultJoinAs(0);
|
||||
}
|
||||
|
||||
chat->setMessagesTTL(update.vttl_period().value_or_empty());
|
||||
if (const auto info = update.vbot_info()) {
|
||||
auto &&commands = ranges::views::all(
|
||||
info->v
|
||||
) | ranges::views::transform(Data::BotCommandsFromTL);
|
||||
chat->setBotCommands(std::move(commands) | ranges::to_vector);
|
||||
} else {
|
||||
chat->setBotCommands({});
|
||||
}
|
||||
using Flag = ChatDataFlag;
|
||||
const auto mask = Flag::CanSetUsername;
|
||||
chat->setFlags((chat->flags() & ~mask)
|
||||
| (update.is_can_set_username() ? Flag::CanSetUsername : Flag()));
|
||||
if (const auto photo = update.vchat_photo()) {
|
||||
chat->setUserpicPhoto(*photo);
|
||||
} else {
|
||||
chat->setUserpicPhoto(MTP_photoEmpty(MTP_long(0)));
|
||||
}
|
||||
if (const auto invite = update.vexported_invite()) {
|
||||
chat->session().api().inviteLinks().setMyPermanent(chat, *invite);
|
||||
} else {
|
||||
chat->session().api().inviteLinks().clearMyPermanent(chat);
|
||||
}
|
||||
if (const auto pinned = update.vpinned_msg_id()) {
|
||||
SetTopPinnedMessageId(chat, pinned->v);
|
||||
}
|
||||
chat->checkFolder(update.vfolder_id().value_or_empty());
|
||||
chat->setThemeToken(qs(update.vtheme_emoticon().value_or_empty()));
|
||||
chat->setTranslationDisabled(update.is_translations_disabled());
|
||||
const auto reactionsLimit = update.vreactions_limit().value_or_empty();
|
||||
if (const auto allowed = update.vavailable_reactions()) {
|
||||
const auto paidEnabled = false;
|
||||
auto parsed = Data::Parse(*allowed, reactionsLimit, paidEnabled);
|
||||
chat->setAllowedReactions(std::move(parsed));
|
||||
} else {
|
||||
chat->setAllowedReactions({ .maxCount = reactionsLimit });
|
||||
}
|
||||
chat->fullUpdated();
|
||||
chat->setAbout(qs(update.vabout()));
|
||||
chat->setPendingRequestsCount(
|
||||
update.vrequests_pending().value_or_empty(),
|
||||
update.vrecent_requesters().value_or_empty());
|
||||
|
||||
chat->owner().notifySettings().apply(chat, update.vnotify_settings());
|
||||
}
|
||||
|
||||
void ApplyChatUpdate(
|
||||
not_null<ChatData*> chat,
|
||||
const MTPChatParticipants &participants) {
|
||||
const auto session = &chat->session();
|
||||
participants.match([&](const MTPDchatParticipantsForbidden &data) {
|
||||
if (const auto self = data.vself_participant()) {
|
||||
// self->
|
||||
}
|
||||
chat->count = -1;
|
||||
chat->invalidateParticipants();
|
||||
}, [&](const MTPDchatParticipants &data) {
|
||||
const auto status = chat->applyUpdateVersion(data.vversion().v);
|
||||
if (status == ChatData::UpdateStatus::TooOld) {
|
||||
return;
|
||||
}
|
||||
// Even if we skipped some updates, we got current participants
|
||||
// and we've requested peer from API to have current rights.
|
||||
chat->setVersion(data.vversion().v);
|
||||
|
||||
const auto &list = data.vparticipants().v;
|
||||
chat->count = list.size();
|
||||
chat->participants.clear();
|
||||
chat->invitedByMe.clear();
|
||||
chat->admins.clear();
|
||||
chat->setAdminRights(ChatAdminRights());
|
||||
const auto selfUserId = session->userId();
|
||||
for (const auto &participant : list) {
|
||||
const auto userId = participant.match([&](const auto &data) {
|
||||
return data.vuser_id().v;
|
||||
});
|
||||
const auto user = chat->owner().userLoaded(userId);
|
||||
if (!user) {
|
||||
chat->invalidateParticipants();
|
||||
break;
|
||||
}
|
||||
|
||||
chat->participants.emplace(user);
|
||||
|
||||
const auto inviterId = participant.match([&](
|
||||
const MTPDchatParticipantCreator &data) {
|
||||
return UserId(0);
|
||||
}, [&](const auto &data) {
|
||||
return UserId(data.vinviter_id());
|
||||
});
|
||||
if (inviterId == selfUserId) {
|
||||
chat->invitedByMe.insert(user);
|
||||
}
|
||||
|
||||
participant.match([&](const MTPDchatParticipantCreator &data) {
|
||||
chat->creator = userId;
|
||||
}, [&](const MTPDchatParticipantAdmin &data) {
|
||||
chat->admins.emplace(user);
|
||||
if (user->isSelf()) {
|
||||
chat->setAdminRights(
|
||||
chat->defaultAdminRights(user).flags);
|
||||
}
|
||||
}, [](const MTPDchatParticipant &) {
|
||||
});
|
||||
}
|
||||
if (chat->participants.empty()) {
|
||||
return;
|
||||
}
|
||||
if (const auto history = chat->owner().historyLoaded(chat)) {
|
||||
if (history->lastKeyboardFrom) {
|
||||
const auto i = ranges::find(
|
||||
chat->participants,
|
||||
history->lastKeyboardFrom,
|
||||
&UserData::id);
|
||||
if (i == end(chat->participants)) {
|
||||
history->clearLastKeyboard();
|
||||
}
|
||||
}
|
||||
}
|
||||
chat->refreshBotStatus();
|
||||
session->changes().peerUpdated(
|
||||
chat,
|
||||
UpdateFlag::Members | UpdateFlag::Admins);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace Data
|
||||
228
Telegram/SourceFiles/data/data_chat.h
Normal file
228
Telegram/SourceFiles/data/data_chat.h
Normal file
@@ -0,0 +1,228 @@
|
||||
/*
|
||||
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_peer.h"
|
||||
#include "data/data_chat_participant_status.h"
|
||||
#include "data/data_peer_bot_commands.h"
|
||||
|
||||
enum class ChatAdminRight;
|
||||
|
||||
enum class ChatDataFlag {
|
||||
Left = (1 << 0),
|
||||
//Kicked = (1 << 1),
|
||||
Creator = (1 << 2),
|
||||
Deactivated = (1 << 3),
|
||||
Forbidden = (1 << 4),
|
||||
CallActive = (1 << 5),
|
||||
CallNotEmpty = (1 << 6),
|
||||
CanSetUsername = (1 << 7),
|
||||
NoForwards = (1 << 8),
|
||||
};
|
||||
inline constexpr bool is_flag_type(ChatDataFlag) { return true; };
|
||||
using ChatDataFlags = base::flags<ChatDataFlag>;
|
||||
|
||||
class ChatData final : public PeerData {
|
||||
public:
|
||||
using Flag = ChatDataFlag;
|
||||
using Flags = Data::Flags<ChatDataFlags>;
|
||||
|
||||
ChatData(not_null<Data::Session*> owner, PeerId id);
|
||||
|
||||
void setName(const QString &newName);
|
||||
void setPhoto(const MTPChatPhoto &photo);
|
||||
|
||||
void invalidateParticipants();
|
||||
[[nodiscard]] bool noParticipantInfo() const {
|
||||
return (count > 0 || amIn()) && participants.empty();
|
||||
}
|
||||
|
||||
void setFlags(ChatDataFlags which);
|
||||
void addFlags(ChatDataFlags which) {
|
||||
_flags.add(which);
|
||||
}
|
||||
void removeFlags(ChatDataFlags which) {
|
||||
_flags.remove(which);
|
||||
}
|
||||
[[nodiscard]] auto flags() const {
|
||||
return _flags.current();
|
||||
}
|
||||
[[nodiscard]] auto flagsValue() const {
|
||||
return _flags.value();
|
||||
}
|
||||
|
||||
[[nodiscard]] auto adminRights() const {
|
||||
return _adminRights.current();
|
||||
}
|
||||
[[nodiscard]] auto adminRightsValue() const {
|
||||
return _adminRights.value();
|
||||
}
|
||||
void setAdminRights(ChatAdminRights rights);
|
||||
[[nodiscard]] bool hasAdminRights() const {
|
||||
return (adminRights() != 0);
|
||||
}
|
||||
|
||||
[[nodiscard]] auto defaultRestrictions() const {
|
||||
return _defaultRestrictions.current();
|
||||
}
|
||||
[[nodiscard]] auto defaultRestrictionsValue() const {
|
||||
return _defaultRestrictions.value();
|
||||
}
|
||||
void setDefaultRestrictions(ChatRestrictions rights);
|
||||
|
||||
[[nodiscard]] bool isForbidden() const {
|
||||
return flags() & Flag::Forbidden;
|
||||
}
|
||||
[[nodiscard]] bool amIn() const {
|
||||
return !isForbidden() && !isDeactivated() && !haveLeft();
|
||||
}
|
||||
[[nodiscard]] bool haveLeft() const {
|
||||
return flags() & ChatDataFlag::Left;
|
||||
}
|
||||
[[nodiscard]] bool amCreator() const {
|
||||
return flags() & ChatDataFlag::Creator;
|
||||
}
|
||||
[[nodiscard]] bool isDeactivated() const {
|
||||
return flags() & ChatDataFlag::Deactivated;
|
||||
}
|
||||
[[nodiscard]] bool isMigrated() const {
|
||||
return (_migratedTo != nullptr);
|
||||
}
|
||||
|
||||
[[nodiscard]] ChatAdminRightsInfo defaultAdminRights(
|
||||
not_null<UserData*> user);
|
||||
|
||||
// Like in ChannelData.
|
||||
[[nodiscard]] bool allowsForwarding() const;
|
||||
[[nodiscard]] bool canEditInformation() const;
|
||||
[[nodiscard]] bool canEditPermissions() const;
|
||||
[[nodiscard]] bool canEditUsername() const;
|
||||
[[nodiscard]] bool canEditPreHistoryHidden() const;
|
||||
[[nodiscard]] bool canDeleteMessages() const;
|
||||
[[nodiscard]] bool canAddMembers() const;
|
||||
[[nodiscard]] bool canAddAdmins() const;
|
||||
[[nodiscard]] bool canBanMembers() const;
|
||||
[[nodiscard]] bool anyoneCanAddMembers() const;
|
||||
|
||||
void applyEditAdmin(not_null<UserData*> user, bool isAdmin);
|
||||
|
||||
void setInviteLink(const QString &newInviteLink);
|
||||
[[nodiscard]] QString inviteLink() const {
|
||||
return _inviteLink;
|
||||
}
|
||||
[[nodiscard]] bool canHaveInviteLink() const;
|
||||
void refreshBotStatus();
|
||||
|
||||
enum class UpdateStatus {
|
||||
Good,
|
||||
TooOld,
|
||||
Skipped,
|
||||
};
|
||||
int version() const {
|
||||
return _version;
|
||||
}
|
||||
void setVersion(int version) {
|
||||
_version = version;
|
||||
}
|
||||
UpdateStatus applyUpdateVersion(int version);
|
||||
|
||||
ChannelData *getMigrateToChannel() const;
|
||||
void setMigrateToChannel(ChannelData *channel);
|
||||
|
||||
[[nodiscard]] Data::GroupCall *groupCall() const {
|
||||
return _call.get();
|
||||
}
|
||||
void setGroupCall(
|
||||
const MTPInputGroupCall &call,
|
||||
TimeId scheduleDate = 0,
|
||||
bool rtmp = false);
|
||||
void clearGroupCall();
|
||||
void setGroupCallDefaultJoinAs(PeerId peerId);
|
||||
[[nodiscard]] PeerId groupCallDefaultJoinAs() const;
|
||||
|
||||
void setBotCommands(const std::vector<Data::BotCommands> &commands);
|
||||
[[nodiscard]] const Data::ChatBotCommands &botCommands() const {
|
||||
return _botCommands;
|
||||
}
|
||||
|
||||
[[nodiscard]] int pendingRequestsCount() const {
|
||||
return _pendingRequestsCount;
|
||||
}
|
||||
[[nodiscard]] const std::vector<UserId> &recentRequesters() const {
|
||||
return _recentRequesters;
|
||||
}
|
||||
void setPendingRequestsCount(
|
||||
int count,
|
||||
const QVector<MTPlong> &recentRequesters);
|
||||
void setPendingRequestsCount(
|
||||
int count,
|
||||
std::vector<UserId> recentRequesters);
|
||||
|
||||
void setAllowedReactions(Data::AllowedReactions value);
|
||||
[[nodiscard]] const Data::AllowedReactions &allowedReactions() const;
|
||||
|
||||
[[nodiscard]] MTPlong inputChat() const;
|
||||
|
||||
// Still public data members.
|
||||
int count = 0;
|
||||
TimeId date = 0;
|
||||
UserId creator = 0;
|
||||
|
||||
base::flat_set<not_null<UserData*>> participants;
|
||||
base::flat_set<not_null<UserData*>> invitedByMe;
|
||||
base::flat_set<not_null<UserData*>> admins;
|
||||
std::deque<not_null<UserData*>> lastAuthors;
|
||||
base::flat_set<not_null<PeerData*>> markupSenders;
|
||||
int botStatus = 0; // -1 - no bots, 0 - unknown, 1 - one bot, that sees all history, 2 - other
|
||||
|
||||
private:
|
||||
Flags _flags;
|
||||
QString _inviteLink;
|
||||
|
||||
Data::Flags<ChatRestrictions> _defaultRestrictions;
|
||||
Data::Flags<ChatAdminRights> _adminRights;
|
||||
int _version = 0;
|
||||
int _pendingRequestsCount = 0;
|
||||
std::vector<UserId> _recentRequesters;
|
||||
|
||||
Data::AllowedReactions _allowedReactions;
|
||||
|
||||
std::unique_ptr<Data::GroupCall> _call;
|
||||
PeerId _callDefaultJoinAs = 0;
|
||||
Data::ChatBotCommands _botCommands;
|
||||
|
||||
ChannelData *_migratedTo = nullptr;
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
namespace Data {
|
||||
|
||||
void ApplyChatUpdate(
|
||||
not_null<ChatData*> chat,
|
||||
const MTPDupdateChatParticipants &update);
|
||||
void ApplyChatUpdate(
|
||||
not_null<ChatData*> chat,
|
||||
const MTPDupdateChatParticipantAdd &update);
|
||||
void ApplyChatUpdate(
|
||||
not_null<ChatData*> chat,
|
||||
const MTPDupdateChatParticipantDelete &update);
|
||||
void ApplyChatUpdate(
|
||||
not_null<ChatData*> chat,
|
||||
const MTPDupdateChatParticipantAdmin &update);
|
||||
void ApplyChatUpdate(
|
||||
not_null<ChatData*> chat,
|
||||
const MTPDupdateChatDefaultBannedRights &update);
|
||||
void ApplyChatUpdate(
|
||||
not_null<ChatData*> chat,
|
||||
const MTPDchatFull &update);
|
||||
void ApplyChatUpdate(
|
||||
not_null<ChatData*> chat,
|
||||
const MTPChatParticipants &update);
|
||||
|
||||
} // namespace Data
|
||||
1157
Telegram/SourceFiles/data/data_chat_filters.cpp
Normal file
1157
Telegram/SourceFiles/data/data_chat_filters.cpp
Normal file
File diff suppressed because it is too large
Load Diff
275
Telegram/SourceFiles/data/data_chat_filters.h
Normal file
275
Telegram/SourceFiles/data/data_chat_filters.h
Normal file
@@ -0,0 +1,275 @@
|
||||
/*
|
||||
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/flags.h"
|
||||
#include "base/timer.h"
|
||||
|
||||
class History;
|
||||
|
||||
namespace Dialogs {
|
||||
class MainList;
|
||||
class Key;
|
||||
} // namespace Dialogs
|
||||
|
||||
namespace Ui {
|
||||
struct MoreChatsBarContent;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Data {
|
||||
|
||||
class Session;
|
||||
|
||||
struct ChatFilterTitle {
|
||||
TextWithEntities text;
|
||||
bool isStatic = false;
|
||||
|
||||
[[nodiscard]] bool empty() const {
|
||||
return text.empty();
|
||||
}
|
||||
};
|
||||
|
||||
[[nodiscard]] TextWithEntities ForceCustomEmojiStatic(TextWithEntities text);
|
||||
|
||||
class ChatFilter final {
|
||||
public:
|
||||
enum class Flag : ushort {
|
||||
Contacts = (1 << 0),
|
||||
NonContacts = (1 << 1),
|
||||
Groups = (1 << 2),
|
||||
Channels = (1 << 3),
|
||||
Bots = (1 << 4),
|
||||
NoMuted = (1 << 5),
|
||||
NoRead = (1 << 6),
|
||||
NoArchived = (1 << 7),
|
||||
RulesMask = ((1 << 8) - 1),
|
||||
|
||||
Chatlist = (1 << 8),
|
||||
HasMyLinks = (1 << 9),
|
||||
StaticTitle = (1 << 10),
|
||||
|
||||
NewChats = (1 << 11), // Telegram Business exceptions.
|
||||
ExistingChats = (1 << 12),
|
||||
};
|
||||
friend constexpr inline bool is_flag_type(Flag) { return true; };
|
||||
using Flags = base::flags<Flag>;
|
||||
|
||||
ChatFilter() = default;
|
||||
ChatFilter(
|
||||
FilterId id,
|
||||
ChatFilterTitle title,
|
||||
QString iconEmoji,
|
||||
std::optional<uint8> colorIndex,
|
||||
Flags flags,
|
||||
base::flat_set<not_null<History*>> always,
|
||||
std::vector<not_null<History*>> pinned,
|
||||
base::flat_set<not_null<History*>> never);
|
||||
|
||||
[[nodiscard]] ChatFilter withId(FilterId id) const;
|
||||
[[nodiscard]] ChatFilter withTitle(ChatFilterTitle title) const;
|
||||
[[nodiscard]] ChatFilter withColorIndex(std::optional<uint8>) const;
|
||||
[[nodiscard]] ChatFilter withChatlist(
|
||||
bool chatlist,
|
||||
bool hasMyLinks) const;
|
||||
[[nodiscard]] ChatFilter withoutAlways(not_null<History*>) const;
|
||||
|
||||
[[nodiscard]] static ChatFilter FromTL(
|
||||
const MTPDialogFilter &data,
|
||||
not_null<Session*> owner);
|
||||
[[nodiscard]] MTPDialogFilter tl(FilterId replaceId = 0) const;
|
||||
|
||||
[[nodiscard]] FilterId id() const;
|
||||
[[nodiscard]] ChatFilterTitle title() const;
|
||||
[[nodiscard]] const TextWithEntities &titleText() const;
|
||||
[[nodiscard]] QString iconEmoji() const;
|
||||
[[nodiscard]] std::optional<uint8> colorIndex() const;
|
||||
[[nodiscard]] Flags flags() const;
|
||||
[[nodiscard]] bool staticTitle() const;
|
||||
[[nodiscard]] bool chatlist() const;
|
||||
[[nodiscard]] bool hasMyLinks() const;
|
||||
[[nodiscard]] const base::flat_set<not_null<History*>> &always() const;
|
||||
[[nodiscard]] const std::vector<not_null<History*>> &pinned() const;
|
||||
[[nodiscard]] const base::flat_set<not_null<History*>> &never() const;
|
||||
|
||||
[[nodiscard]] bool contains(
|
||||
not_null<History*> history,
|
||||
bool ignoreFakeUnread = false) const;
|
||||
|
||||
private:
|
||||
FilterId _id = 0;
|
||||
TextWithEntities _title;
|
||||
QString _iconEmoji;
|
||||
std::optional<uint8> _colorIndex;
|
||||
base::flat_set<not_null<History*>> _always;
|
||||
std::vector<not_null<History*>> _pinned;
|
||||
base::flat_set<not_null<History*>> _never;
|
||||
Flags _flags;
|
||||
|
||||
};
|
||||
|
||||
inline bool operator==(const ChatFilter &a, const ChatFilter &b) {
|
||||
return (a.titleText() == b.titleText())
|
||||
&& (a.iconEmoji() == b.iconEmoji())
|
||||
&& (a.colorIndex() == b.colorIndex())
|
||||
&& (a.flags() == b.flags())
|
||||
&& (a.always() == b.always())
|
||||
&& (a.never() == b.never());
|
||||
}
|
||||
|
||||
inline bool operator!=(const ChatFilter &a, const ChatFilter &b) {
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
struct ChatFilterLink {
|
||||
FilterId id = 0;
|
||||
QString url;
|
||||
QString title;
|
||||
std::vector<not_null<History*>> chats;
|
||||
|
||||
friend inline bool operator==(
|
||||
const ChatFilterLink &a,
|
||||
const ChatFilterLink &b) = default;
|
||||
};
|
||||
|
||||
struct SuggestedFilter {
|
||||
ChatFilter filter;
|
||||
QString description;
|
||||
};
|
||||
|
||||
struct TagColorChanged final {
|
||||
FilterId filterId = 0;
|
||||
bool colorExistenceChanged = false;
|
||||
};
|
||||
|
||||
class ChatFilters final {
|
||||
public:
|
||||
explicit ChatFilters(not_null<Session*> owner);
|
||||
~ChatFilters();
|
||||
|
||||
void setPreloaded(
|
||||
const QVector<MTPDialogFilter> &result,
|
||||
bool tagsEnabled);
|
||||
|
||||
void load();
|
||||
void reload();
|
||||
void apply(const MTPUpdate &update);
|
||||
void set(ChatFilter filter);
|
||||
void remove(FilterId id);
|
||||
void moveAllToFront();
|
||||
[[nodiscard]] const std::vector<ChatFilter> &list() const;
|
||||
[[nodiscard]] rpl::producer<> changed() const;
|
||||
[[nodiscard]] rpl::producer<FilterId> isChatlistChanged() const;
|
||||
[[nodiscard]] rpl::producer<TagColorChanged> tagColorChanged() const;
|
||||
[[nodiscard]] bool loaded() const;
|
||||
[[nodiscard]] bool has() const;
|
||||
|
||||
[[nodiscard]] FilterId defaultId() const;
|
||||
[[nodiscard]] FilterId lookupId(int index) const;
|
||||
|
||||
bool loadNextExceptions(bool chatsListLoaded);
|
||||
|
||||
void refreshHistory(not_null<History*> history);
|
||||
|
||||
[[nodiscard]] not_null<Dialogs::MainList*> chatsList(FilterId filterId);
|
||||
void clear();
|
||||
|
||||
const ChatFilter &applyUpdatedPinned(
|
||||
FilterId id,
|
||||
const std::vector<Dialogs::Key> &dialogs);
|
||||
void saveOrder(
|
||||
const std::vector<FilterId> &order,
|
||||
mtpRequestId after = 0);
|
||||
|
||||
[[nodiscard]] bool archiveNeeded() const;
|
||||
|
||||
void requestSuggested();
|
||||
[[nodiscard]] bool suggestedLoaded() const;
|
||||
[[nodiscard]] auto suggestedFilters() const
|
||||
-> const std::vector<SuggestedFilter> &;
|
||||
[[nodiscard]] rpl::producer<> suggestedUpdated() const;
|
||||
|
||||
ChatFilterLink add(
|
||||
FilterId id,
|
||||
const MTPExportedChatlistInvite &update);
|
||||
void edit(
|
||||
FilterId id,
|
||||
const QString &url,
|
||||
const QString &title);
|
||||
void destroy(FilterId id, const QString &url);
|
||||
rpl::producer<std::vector<ChatFilterLink>> chatlistLinks(
|
||||
FilterId id) const;
|
||||
void reloadChatlistLinks(FilterId id);
|
||||
|
||||
[[nodiscard]] rpl::producer<Ui::MoreChatsBarContent> moreChatsContent(
|
||||
FilterId id);
|
||||
[[nodiscard]] const std::vector<not_null<PeerData*>> &moreChats(
|
||||
FilterId id) const;
|
||||
void moreChatsHide(FilterId id, bool localOnly = false);
|
||||
|
||||
[[nodiscard]] bool tagsEnabled() const;
|
||||
[[nodiscard]] rpl::producer<bool> tagsEnabledValue() const;
|
||||
[[nodiscard]] rpl::producer<bool> tagsEnabledChanges() const;
|
||||
void requestToggleTags(bool value, Fn<void()> fail);
|
||||
|
||||
private:
|
||||
struct MoreChatsData {
|
||||
std::vector<not_null<PeerData*>> missing;
|
||||
crl::time lastUpdate = 0;
|
||||
mtpRequestId requestId = 0;
|
||||
std::weak_ptr<bool> watching;
|
||||
};
|
||||
|
||||
void load(bool force);
|
||||
void received(const QVector<MTPDialogFilter> &list);
|
||||
bool applyOrder(const QVector<MTPint> &order);
|
||||
bool applyChange(ChatFilter &filter, ChatFilter &&updated);
|
||||
void applyInsert(ChatFilter filter, int position);
|
||||
void applyRemove(int position);
|
||||
|
||||
void checkLoadMoreChatsLists();
|
||||
void loadMoreChatsList(FilterId id);
|
||||
|
||||
const not_null<Session*> _owner;
|
||||
|
||||
std::vector<ChatFilter> _list;
|
||||
base::flat_map<FilterId, std::unique_ptr<Dialogs::MainList>> _chatsLists;
|
||||
rpl::event_stream<> _listChanged;
|
||||
rpl::event_stream<FilterId> _isChatlistChanged;
|
||||
rpl::event_stream<TagColorChanged> _tagColorChanged;
|
||||
mtpRequestId _loadRequestId = 0;
|
||||
mtpRequestId _saveOrderRequestId = 0;
|
||||
mtpRequestId _saveOrderAfterId = 0;
|
||||
mtpRequestId _toggleTagsRequestId = 0;
|
||||
bool _loaded = false;
|
||||
bool _reloading = false;
|
||||
|
||||
mtpRequestId _suggestedRequestId = 0;
|
||||
std::vector<SuggestedFilter> _suggested;
|
||||
rpl::event_stream<> _suggestedUpdated;
|
||||
crl::time _suggestedLastReceived = 0;
|
||||
|
||||
rpl::variable<bool> _tagsEnabled = false;
|
||||
|
||||
std::deque<FilterId> _exceptionsToLoad;
|
||||
mtpRequestId _exceptionsLoadRequestId = 0;
|
||||
|
||||
base::flat_map<FilterId, std::vector<ChatFilterLink>> _chatlistLinks;
|
||||
rpl::event_stream<FilterId> _chatlistLinksUpdated;
|
||||
mtpRequestId _linksRequestId = 0;
|
||||
|
||||
base::flat_map<FilterId, MoreChatsData> _moreChatsData;
|
||||
rpl::event_stream<FilterId> _moreChatsUpdated;
|
||||
base::Timer _moreChatsTimer;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] bool CanRemoveFromChatFilter(
|
||||
const ChatFilter &filter,
|
||||
not_null<History*> history);
|
||||
|
||||
} // namespace Data
|
||||
535
Telegram/SourceFiles/data/data_chat_participant_status.cpp
Normal file
535
Telegram/SourceFiles/data/data_chat_participant_status.cpp
Normal file
@@ -0,0 +1,535 @@
|
||||
/*
|
||||
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/data_chat_participant_status.h"
|
||||
|
||||
#include "base/unixtime.h"
|
||||
#include "boxes/peers/edit_peer_permissions_box.h"
|
||||
#include "chat_helpers/compose/compose_show.h"
|
||||
#include "data/data_chat.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_forum_topic.h"
|
||||
#include "data/data_peer_values.h"
|
||||
#include "data/data_user.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "main/main_session.h"
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
#include "ui/chat/attach/attach_prepare.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] ChatAdminRights ChatAdminRightsFlags(
|
||||
const MTPChatAdminRights &rights) {
|
||||
return rights.match([](const MTPDchatAdminRights &data) {
|
||||
using Flag = ChatAdminRight;
|
||||
return (data.is_change_info() ? Flag::ChangeInfo : Flag())
|
||||
| (data.is_post_messages() ? Flag::PostMessages : Flag())
|
||||
| (data.is_edit_messages() ? Flag::EditMessages : Flag())
|
||||
| (data.is_delete_messages() ? Flag::DeleteMessages : Flag())
|
||||
| (data.is_ban_users() ? Flag::BanUsers : Flag())
|
||||
| (data.is_invite_users() ? Flag::InviteByLinkOrAdd : Flag())
|
||||
| (data.is_pin_messages() ? Flag::PinMessages : Flag())
|
||||
| (data.is_add_admins() ? Flag::AddAdmins : Flag())
|
||||
| (data.is_anonymous() ? Flag::Anonymous : Flag())
|
||||
| (data.is_manage_call() ? Flag::ManageCall : Flag())
|
||||
| (data.is_other() ? Flag::Other : Flag())
|
||||
| (data.is_manage_topics() ? Flag::ManageTopics : Flag())
|
||||
| (data.is_post_stories() ? Flag::PostStories : Flag())
|
||||
| (data.is_edit_stories() ? Flag::EditStories : Flag())
|
||||
| (data.is_delete_stories() ? Flag::DeleteStories : Flag())
|
||||
| (data.is_manage_direct_messages()
|
||||
? Flag::ManageDirect
|
||||
: Flag());
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] ChatRestrictions ChatBannedRightsFlags(
|
||||
const MTPChatBannedRights &rights) {
|
||||
return rights.match([](const MTPDchatBannedRights &data) {
|
||||
using Flag = ChatRestriction;
|
||||
return (data.is_view_messages() ? Flag::ViewMessages : Flag())
|
||||
| (data.is_send_stickers() ? Flag::SendStickers : Flag())
|
||||
| (data.is_send_gifs() ? Flag::SendGifs : Flag())
|
||||
| (data.is_send_games() ? Flag::SendGames : Flag())
|
||||
| (data.is_send_inline() ? Flag::SendInline : Flag())
|
||||
| (data.is_send_polls() ? Flag::SendPolls : Flag())
|
||||
| (data.is_send_photos() ? Flag::SendPhotos : Flag())
|
||||
| (data.is_send_videos() ? Flag::SendVideos : Flag())
|
||||
| (data.is_send_roundvideos() ? Flag::SendVideoMessages : Flag())
|
||||
| (data.is_send_audios() ? Flag::SendMusic : Flag())
|
||||
| (data.is_send_voices() ? Flag::SendVoiceMessages : Flag())
|
||||
| (data.is_send_docs() ? Flag::SendFiles : Flag())
|
||||
| (data.is_send_plain() ? Flag::SendOther : Flag())
|
||||
| (data.is_embed_links() ? Flag::EmbedLinks : Flag())
|
||||
| (data.is_change_info() ? Flag::ChangeInfo : Flag())
|
||||
| (data.is_invite_users() ? Flag::AddParticipants : Flag())
|
||||
| (data.is_pin_messages() ? Flag::PinMessages : Flag())
|
||||
| (data.is_manage_topics() ? Flag::CreateTopics : Flag());
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] TimeId ChatBannedRightsUntilDate(
|
||||
const MTPChatBannedRights &rights) {
|
||||
return rights.match([](const MTPDchatBannedRights &data) {
|
||||
return data.vuntil_date().v;
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ChatAdminRightsInfo::ChatAdminRightsInfo(const MTPChatAdminRights &rights)
|
||||
: flags(ChatAdminRightsFlags(rights)) {
|
||||
}
|
||||
|
||||
MTPChatAdminRights AdminRightsToMTP(ChatAdminRightsInfo info) {
|
||||
using Flag = MTPDchatAdminRights::Flag;
|
||||
using R = ChatAdminRight;
|
||||
const auto flags = info.flags;
|
||||
return MTP_chatAdminRights(MTP_flags(Flag()
|
||||
| ((flags & R::ChangeInfo) ? Flag::f_change_info : Flag())
|
||||
| ((flags & R::PostMessages) ? Flag::f_post_messages : Flag())
|
||||
| ((flags & R::EditMessages) ? Flag::f_edit_messages : Flag())
|
||||
| ((flags & R::DeleteMessages) ? Flag::f_delete_messages : Flag())
|
||||
| ((flags & R::BanUsers) ? Flag::f_ban_users : Flag())
|
||||
| ((flags & R::InviteByLinkOrAdd) ? Flag::f_invite_users : Flag())
|
||||
| ((flags & R::PinMessages) ? Flag::f_pin_messages : Flag())
|
||||
| ((flags & R::AddAdmins) ? Flag::f_add_admins : Flag())
|
||||
| ((flags & R::Anonymous) ? Flag::f_anonymous : Flag())
|
||||
| ((flags & R::ManageCall) ? Flag::f_manage_call : Flag())
|
||||
| ((flags & R::Other) ? Flag::f_other : Flag())
|
||||
| ((flags & R::ManageTopics) ? Flag::f_manage_topics : Flag())
|
||||
| ((flags & R::PostStories) ? Flag::f_post_stories : Flag())
|
||||
| ((flags & R::EditStories) ? Flag::f_edit_stories : Flag())
|
||||
| ((flags & R::DeleteStories) ? Flag::f_delete_stories : Flag())
|
||||
| ((flags & R::ManageDirect)
|
||||
? Flag::f_manage_direct_messages
|
||||
: Flag())));
|
||||
}
|
||||
|
||||
ChatRestrictionsInfo::ChatRestrictionsInfo(const MTPChatBannedRights &rights)
|
||||
: flags(ChatBannedRightsFlags(rights))
|
||||
, until(ChatBannedRightsUntilDate(rights)) {
|
||||
}
|
||||
|
||||
MTPChatBannedRights RestrictionsToMTP(ChatRestrictionsInfo info) {
|
||||
using Flag = MTPDchatBannedRights::Flag;
|
||||
using R = ChatRestriction;
|
||||
const auto flags = info.flags;
|
||||
return MTP_chatBannedRights(
|
||||
MTP_flags(Flag()
|
||||
| ((flags & R::ViewMessages) ? Flag::f_view_messages : Flag())
|
||||
| ((flags & R::SendStickers) ? Flag::f_send_stickers : Flag())
|
||||
| ((flags & R::SendGifs) ? Flag::f_send_gifs : Flag())
|
||||
| ((flags & R::SendGames) ? Flag::f_send_games : Flag())
|
||||
| ((flags & R::SendInline) ? Flag::f_send_inline : Flag())
|
||||
| ((flags & R::SendPolls) ? Flag::f_send_polls : Flag())
|
||||
| ((flags & R::SendPhotos) ? Flag::f_send_photos : Flag())
|
||||
| ((flags & R::SendVideos) ? Flag::f_send_videos : Flag())
|
||||
| ((flags & R::SendVideoMessages) ? Flag::f_send_roundvideos : Flag())
|
||||
| ((flags & R::SendMusic) ? Flag::f_send_audios : Flag())
|
||||
| ((flags & R::SendVoiceMessages) ? Flag::f_send_voices : Flag())
|
||||
| ((flags & R::SendFiles) ? Flag::f_send_docs : Flag())
|
||||
| ((flags & R::SendOther) ? Flag::f_send_plain : Flag())
|
||||
| ((flags & R::EmbedLinks) ? Flag::f_embed_links : Flag())
|
||||
| ((flags & R::ChangeInfo) ? Flag::f_change_info : Flag())
|
||||
| ((flags & R::AddParticipants) ? Flag::f_invite_users : Flag())
|
||||
| ((flags & R::PinMessages) ? Flag::f_pin_messages : Flag())
|
||||
| ((flags & R::CreateTopics) ? Flag::f_manage_topics : Flag())),
|
||||
MTP_int(info.until));
|
||||
}
|
||||
|
||||
namespace Data {
|
||||
|
||||
std::vector<ChatRestrictions> ListOfRestrictions(
|
||||
RestrictionsSetOptions options) {
|
||||
auto labels = RestrictionLabels(options);
|
||||
return ranges::views::all(labels)
|
||||
| ranges::views::transform(&RestrictionLabel::flags)
|
||||
| ranges::to_vector;
|
||||
}
|
||||
|
||||
ChatRestrictions AllSendRestrictions() {
|
||||
constexpr auto result = [] {
|
||||
auto result = ChatRestrictions();
|
||||
for (const auto right : AllSendRestrictionsList()) {
|
||||
result |= right;
|
||||
}
|
||||
return result;
|
||||
}();
|
||||
return result;
|
||||
}
|
||||
|
||||
ChatRestrictions FilesSendRestrictions() {
|
||||
constexpr auto result = [] {
|
||||
auto result = ChatRestrictions();
|
||||
for (const auto right : FilesSendRestrictionsList()) {
|
||||
result |= right;
|
||||
}
|
||||
return result;
|
||||
}();
|
||||
return result;
|
||||
}
|
||||
|
||||
ChatRestrictions TabbedPanelSendRestrictions() {
|
||||
constexpr auto result = [] {
|
||||
auto result = ChatRestrictions();
|
||||
for (const auto right : TabbedPanelSendRestrictionsList()) {
|
||||
result |= right;
|
||||
}
|
||||
return result;
|
||||
}();
|
||||
return result;
|
||||
}
|
||||
|
||||
// Duplicated in CanSendAnyOfValue().
|
||||
bool CanSendAnyOf(
|
||||
not_null<const Thread*> thread,
|
||||
ChatRestrictions rights,
|
||||
bool forbidInForums) {
|
||||
const auto peer = thread->peer();
|
||||
const auto topic = thread->asTopic();
|
||||
return CanSendAnyOf(peer, rights, forbidInForums && !topic)
|
||||
&& (!topic || !topic->closed() || topic->canToggleClosed());
|
||||
}
|
||||
|
||||
// Duplicated in CanSendAnyOfValue().
|
||||
bool CanSendAnyOf(
|
||||
not_null<const PeerData*> peer,
|
||||
ChatRestrictions rights,
|
||||
bool forbidInForums) {
|
||||
if (peer->session().frozen()
|
||||
&& !peer->isFreezeAppealChat()) {
|
||||
return false;
|
||||
} else if (const auto user = peer->asUser()) {
|
||||
if (user->isInaccessible()
|
||||
|| user->isRepliesChat()
|
||||
|| user->isVerifyCodes()) {
|
||||
return false;
|
||||
} else if (user->requiresPremiumToWrite()
|
||||
&& !user->session().premium()) {
|
||||
return false;
|
||||
} else if (rights
|
||||
& ~(ChatRestriction::SendVoiceMessages
|
||||
| ChatRestriction::SendVideoMessages
|
||||
| ChatRestriction::SendPolls)) {
|
||||
return true;
|
||||
}
|
||||
for (const auto right : {
|
||||
ChatRestriction::SendVoiceMessages,
|
||||
ChatRestriction::SendVideoMessages,
|
||||
ChatRestriction::SendPolls,
|
||||
}) {
|
||||
if ((rights & right) && !user->amRestricted(right)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else if (const auto chat = peer->asChat()) {
|
||||
if (!chat->amIn()) {
|
||||
return false;
|
||||
}
|
||||
for (const auto right : AllSendRestrictionsList()) {
|
||||
if ((rights & right) && !chat->amRestricted(right)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else if (const auto channel = peer->asChannel()) {
|
||||
if (channel->monoforumDisabled()) {
|
||||
return false;
|
||||
}
|
||||
using Flag = ChannelDataFlag;
|
||||
const auto allowed = channel->amIn()
|
||||
|| ((channel->flags() & Flag::HasLink)
|
||||
&& !(channel->flags() & Flag::JoinToWrite))
|
||||
|| channel->isMonoforum();
|
||||
if (!allowed || (forbidInForums && channel->isForum())) {
|
||||
return false;
|
||||
} else if (channel->canPostMessages()) {
|
||||
return true;
|
||||
} else if (channel->isBroadcast()) {
|
||||
return false;
|
||||
}
|
||||
for (const auto right : AllSendRestrictionsList()) {
|
||||
if ((rights & right) && !channel->amRestricted(right)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
Unexpected("Peer type in CanSendAnyOf.");
|
||||
}
|
||||
|
||||
SendError RestrictionError(
|
||||
not_null<PeerData*> peer,
|
||||
ChatRestriction restriction) {
|
||||
using Flag = ChatRestriction;
|
||||
if (peer->session().frozen()
|
||||
&& !peer->isFreezeAppealChat()) {
|
||||
return SendError({
|
||||
.text = tr::lng_frozen_restrict_title(tr::now),
|
||||
.frozen = true,
|
||||
});
|
||||
} else if (const auto restricted = peer->amRestricted(restriction)) {
|
||||
if (const auto user = peer->asUser()) {
|
||||
if (user->requiresPremiumToWrite()
|
||||
&& !user->session().premium()) {
|
||||
return SendError({
|
||||
.text = tr::lng_restricted_send_non_premium(
|
||||
tr::now,
|
||||
lt_user,
|
||||
user->shortName()),
|
||||
.premiumToLift = true,
|
||||
});
|
||||
}
|
||||
const auto result = (restriction == Flag::SendVoiceMessages)
|
||||
? tr::lng_restricted_send_voice_messages(
|
||||
tr::now,
|
||||
lt_user,
|
||||
user->name())
|
||||
: (restriction == Flag::SendVideoMessages)
|
||||
? tr::lng_restricted_send_video_messages(
|
||||
tr::now,
|
||||
lt_user,
|
||||
user->name())
|
||||
: (restriction == Flag::SendPolls)
|
||||
? u"can't send polls :("_q
|
||||
: (restriction == Flag::PinMessages)
|
||||
? u"can't pin :("_q
|
||||
: SendError();
|
||||
|
||||
Ensures(result.has_value());
|
||||
return result;
|
||||
}
|
||||
const auto all = restricted.isWithEveryone();
|
||||
const auto channel = peer->asChannel();
|
||||
if (channel && channel->monoforumDisabled()) {
|
||||
return tr::lng_action_direct_messages_disabled(tr::now);
|
||||
}
|
||||
if (!all && channel) {
|
||||
auto restrictedUntil = channel->restrictedUntil();
|
||||
if (restrictedUntil > 0
|
||||
&& !ChannelData::IsRestrictedForever(restrictedUntil)) {
|
||||
auto restrictedUntilDateTime = base::unixtime::parse(
|
||||
channel->restrictedUntil());
|
||||
auto date = QLocale().toString(
|
||||
restrictedUntilDateTime.date(),
|
||||
QLocale::ShortFormat);
|
||||
auto time = QLocale().toString(
|
||||
restrictedUntilDateTime.time(),
|
||||
QLocale::ShortFormat);
|
||||
|
||||
switch (restriction) {
|
||||
case Flag::SendPolls:
|
||||
return tr::lng_restricted_send_polls_until(
|
||||
tr::now, lt_date, date, lt_time, time);
|
||||
case Flag::SendOther:
|
||||
return tr::lng_restricted_send_message_until(
|
||||
tr::now, lt_date, date, lt_time, time);
|
||||
case Flag::SendPhotos:
|
||||
return tr::lng_restricted_send_photos_until(
|
||||
tr::now, lt_date, date, lt_time, time);
|
||||
case Flag::SendVideos:
|
||||
return tr::lng_restricted_send_videos_until(
|
||||
tr::now, lt_date, date, lt_time, time);
|
||||
case Flag::SendMusic:
|
||||
return tr::lng_restricted_send_music_until(
|
||||
tr::now, lt_date, date, lt_time, time);
|
||||
case Flag::SendFiles:
|
||||
return tr::lng_restricted_send_files_until(
|
||||
tr::now, lt_date, date, lt_time, time);
|
||||
case Flag::SendVideoMessages:
|
||||
return tr::lng_restricted_send_video_messages_until(
|
||||
tr::now, lt_date, date, lt_time, time);
|
||||
case Flag::SendVoiceMessages:
|
||||
return tr::lng_restricted_send_voice_messages_until(
|
||||
tr::now, lt_date, date, lt_time, time);
|
||||
case Flag::SendStickers:
|
||||
return tr::lng_restricted_send_stickers_until(
|
||||
tr::now, lt_date, date, lt_time, time);
|
||||
case Flag::SendGifs:
|
||||
return tr::lng_restricted_send_gifs_until(
|
||||
tr::now, lt_date, date, lt_time, time);
|
||||
case Flag::SendInline:
|
||||
case Flag::SendGames:
|
||||
return tr::lng_restricted_send_inline_until(
|
||||
tr::now, lt_date, date, lt_time, time);
|
||||
}
|
||||
Unexpected("Restriction in Data::RestrictionErrorKey.");
|
||||
}
|
||||
}
|
||||
if (all
|
||||
&& channel
|
||||
&& channel->boostsUnrestrict()
|
||||
&& !channel->unrestrictedByBoosts()) {
|
||||
return SendError({
|
||||
.text = tr::lng_restricted_boost_group(tr::now),
|
||||
.boostsToLift = (channel->boostsUnrestrict()
|
||||
- channel->boostsApplied()),
|
||||
});
|
||||
}
|
||||
switch (restriction) {
|
||||
case Flag::SendPolls:
|
||||
return all
|
||||
? tr::lng_restricted_send_polls_all(tr::now)
|
||||
: tr::lng_restricted_send_polls(tr::now);
|
||||
case Flag::SendOther:
|
||||
return all
|
||||
? tr::lng_restricted_send_message_all(tr::now)
|
||||
: tr::lng_restricted_send_message(tr::now);
|
||||
case Flag::SendPhotos:
|
||||
return all
|
||||
? tr::lng_restricted_send_photos_all(tr::now)
|
||||
: tr::lng_restricted_send_photos(tr::now);
|
||||
case Flag::SendVideos:
|
||||
return all
|
||||
? tr::lng_restricted_send_videos_all(tr::now)
|
||||
: tr::lng_restricted_send_videos(tr::now);
|
||||
case Flag::SendMusic:
|
||||
return all
|
||||
? tr::lng_restricted_send_music_all(tr::now)
|
||||
: tr::lng_restricted_send_music(tr::now);
|
||||
case Flag::SendFiles:
|
||||
return all
|
||||
? tr::lng_restricted_send_files_all(tr::now)
|
||||
: tr::lng_restricted_send_files(tr::now);
|
||||
case Flag::SendVideoMessages:
|
||||
return all
|
||||
? tr::lng_restricted_send_video_messages_all(tr::now)
|
||||
: tr::lng_restricted_send_video_messages_group(tr::now);
|
||||
case Flag::SendVoiceMessages:
|
||||
return all
|
||||
? tr::lng_restricted_send_voice_messages_all(tr::now)
|
||||
: tr::lng_restricted_send_voice_messages_group(tr::now);
|
||||
case Flag::SendStickers:
|
||||
return all
|
||||
? tr::lng_restricted_send_stickers_all(tr::now)
|
||||
: tr::lng_restricted_send_stickers(tr::now);
|
||||
case Flag::SendGifs:
|
||||
return all
|
||||
? tr::lng_restricted_send_gifs_all(tr::now)
|
||||
: tr::lng_restricted_send_gifs(tr::now);
|
||||
case Flag::SendInline:
|
||||
case Flag::SendGames:
|
||||
return all
|
||||
? tr::lng_restricted_send_inline_all(tr::now)
|
||||
: tr::lng_restricted_send_inline(tr::now);
|
||||
}
|
||||
Unexpected("Restriction in Data::RestrictionErrorKey.");
|
||||
}
|
||||
return SendError();
|
||||
}
|
||||
|
||||
SendError AnyFileRestrictionError(not_null<PeerData*> peer) {
|
||||
using Restriction = ChatRestriction;
|
||||
for (const auto right : FilesSendRestrictionsList()) {
|
||||
if (!RestrictionError(peer, right)) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return RestrictionError(peer, Restriction::SendFiles);
|
||||
}
|
||||
|
||||
SendError FileRestrictionError(
|
||||
not_null<PeerData*> peer,
|
||||
const Ui::PreparedList &list,
|
||||
std::optional<bool> compress) {
|
||||
const auto slowmode = peer->slowmodeApplied();
|
||||
if (slowmode) {
|
||||
if (!list.canBeSentInSlowmode()) {
|
||||
return tr::lng_slowmode_no_many(tr::now);
|
||||
} else if (list.files.size() > 1 && list.hasSticker()) {
|
||||
if (compress == false) {
|
||||
return tr::lng_slowmode_no_many(tr::now);
|
||||
} else {
|
||||
compress = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const auto &file : list.files) {
|
||||
if (const auto error = FileRestrictionError(peer, file, compress)) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
SendError FileRestrictionError(
|
||||
not_null<PeerData*> peer,
|
||||
const Ui::PreparedFile &file,
|
||||
std::optional<bool> compress) {
|
||||
using Type = Ui::PreparedFile::Type;
|
||||
using Restriction = ChatRestriction;
|
||||
const auto stickers = RestrictionError(peer, Restriction::SendStickers);
|
||||
const auto gifs = RestrictionError(peer, Restriction::SendGifs);
|
||||
const auto photos = RestrictionError(peer, Restriction::SendPhotos);
|
||||
const auto videos = RestrictionError(peer, Restriction::SendVideos);
|
||||
const auto music = RestrictionError(peer, Restriction::SendMusic);
|
||||
const auto files = RestrictionError(peer, Restriction::SendFiles);
|
||||
if (!stickers && !gifs && !photos && !videos && !music && !files) {
|
||||
return {};
|
||||
}
|
||||
switch (file.type) {
|
||||
case Type::Photo:
|
||||
if (compress == true && photos) {
|
||||
return photos;
|
||||
} else if (const auto other = file.isSticker() ? stickers : files) {
|
||||
if ((compress == false || photos) && other) {
|
||||
return (file.isSticker() || !photos) ? other : photos;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Type::Video:
|
||||
if (const auto error = file.isGifv() ? gifs : videos) {
|
||||
return error;
|
||||
}
|
||||
break;
|
||||
case Type::Music:
|
||||
if (music) {
|
||||
return music;
|
||||
}
|
||||
break;
|
||||
case Type::File:
|
||||
if (files) {
|
||||
return files;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void ShowSendErrorToast(
|
||||
not_null<Window::SessionNavigation*> navigation,
|
||||
not_null<PeerData*> peer,
|
||||
Data::SendError error) {
|
||||
return ShowSendErrorToast(navigation->uiShow(), peer, error);
|
||||
}
|
||||
|
||||
void ShowSendErrorToast(
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
not_null<PeerData*> peer,
|
||||
Data::SendError error) {
|
||||
if (!error.boostsToLift) {
|
||||
show->showToast(*error);
|
||||
return;
|
||||
}
|
||||
const auto boost = [=] {
|
||||
const auto window = show->resolveWindow();
|
||||
window->resolveBoostState(peer->asChannel(), error.boostsToLift);
|
||||
};
|
||||
show->showToast({
|
||||
.text = tr::link(*error),
|
||||
.filter = [=](const auto &...) { boost(); return false; },
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace Data
|
||||
259
Telegram/SourceFiles/data/data_chat_participant_status.h
Normal file
259
Telegram/SourceFiles/data/data_chat_participant_status.h
Normal file
@@ -0,0 +1,259 @@
|
||||
/*
|
||||
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 ChatHelpers {
|
||||
class Show;
|
||||
} // namespace ChatHelpers
|
||||
|
||||
namespace Ui {
|
||||
struct PreparedList;
|
||||
struct PreparedFile;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Window {
|
||||
class SessionNavigation;
|
||||
} // namespace Window
|
||||
|
||||
enum class ChatAdminRight {
|
||||
ChangeInfo = (1 << 0),
|
||||
PostMessages = (1 << 1),
|
||||
EditMessages = (1 << 2),
|
||||
DeleteMessages = (1 << 3),
|
||||
BanUsers = (1 << 4),
|
||||
InviteByLinkOrAdd = (1 << 5),
|
||||
PinMessages = (1 << 7),
|
||||
AddAdmins = (1 << 9),
|
||||
Anonymous = (1 << 10),
|
||||
ManageCall = (1 << 11),
|
||||
Other = (1 << 12),
|
||||
ManageTopics = (1 << 13),
|
||||
PostStories = (1 << 14),
|
||||
EditStories = (1 << 15),
|
||||
DeleteStories = (1 << 16),
|
||||
ManageDirect = (1 << 17),
|
||||
};
|
||||
inline constexpr bool is_flag_type(ChatAdminRight) { return true; }
|
||||
using ChatAdminRights = base::flags<ChatAdminRight>;
|
||||
|
||||
enum class ChatRestriction {
|
||||
ViewMessages = (1 << 0),
|
||||
|
||||
SendStickers = (1 << 3),
|
||||
SendGifs = (1 << 4),
|
||||
SendGames = (1 << 5),
|
||||
SendInline = (1 << 6),
|
||||
SendPolls = (1 << 8),
|
||||
SendPhotos = (1 << 19),
|
||||
SendVideos = (1 << 20),
|
||||
SendVideoMessages = (1 << 21),
|
||||
SendMusic = (1 << 22),
|
||||
SendVoiceMessages = (1 << 23),
|
||||
SendFiles = (1 << 24),
|
||||
SendOther = (1 << 25),
|
||||
|
||||
EmbedLinks = (1 << 7),
|
||||
|
||||
ChangeInfo = (1 << 10),
|
||||
AddParticipants = (1 << 15),
|
||||
PinMessages = (1 << 17),
|
||||
CreateTopics = (1 << 18),
|
||||
};
|
||||
inline constexpr bool is_flag_type(ChatRestriction) { return true; }
|
||||
using ChatRestrictions = base::flags<ChatRestriction>;
|
||||
|
||||
struct ChatAdminRightsInfo {
|
||||
ChatAdminRightsInfo() = default;
|
||||
explicit ChatAdminRightsInfo(ChatAdminRights flags) : flags(flags) {
|
||||
}
|
||||
explicit ChatAdminRightsInfo(const MTPChatAdminRights &rights);
|
||||
|
||||
ChatAdminRights flags;
|
||||
};
|
||||
|
||||
[[nodiscard]] MTPChatAdminRights AdminRightsToMTP(ChatAdminRightsInfo info);
|
||||
|
||||
struct ChatRestrictionsInfo {
|
||||
ChatRestrictionsInfo() = default;
|
||||
ChatRestrictionsInfo(ChatRestrictions flags, TimeId until)
|
||||
: flags(flags)
|
||||
, until(until) {
|
||||
}
|
||||
explicit ChatRestrictionsInfo(const MTPChatBannedRights &rights);
|
||||
|
||||
ChatRestrictions flags;
|
||||
TimeId until = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] MTPChatBannedRights RestrictionsToMTP(
|
||||
ChatRestrictionsInfo info);
|
||||
|
||||
namespace Data {
|
||||
|
||||
class Thread;
|
||||
|
||||
struct AdminRightsSetOptions {
|
||||
bool isGroup : 1 = false;
|
||||
bool isForum : 1 = false;
|
||||
bool anyoneCanAddMembers : 1 = false;
|
||||
};
|
||||
|
||||
struct RestrictionsSetOptions {
|
||||
bool isForum = false;
|
||||
};
|
||||
|
||||
[[nodiscard]] std::vector<ChatRestrictions> ListOfRestrictions(
|
||||
RestrictionsSetOptions options);
|
||||
|
||||
[[nodiscard]] inline constexpr auto AllSendRestrictionsList() {
|
||||
return std::array{
|
||||
ChatRestriction::SendOther,
|
||||
ChatRestriction::SendStickers,
|
||||
ChatRestriction::SendGifs,
|
||||
ChatRestriction::SendGames,
|
||||
ChatRestriction::SendInline,
|
||||
ChatRestriction::SendPolls,
|
||||
ChatRestriction::SendPhotos,
|
||||
ChatRestriction::SendVideos,
|
||||
ChatRestriction::SendVideoMessages,
|
||||
ChatRestriction::SendMusic,
|
||||
ChatRestriction::SendVoiceMessages,
|
||||
ChatRestriction::SendFiles,
|
||||
};
|
||||
}
|
||||
[[nodiscard]] inline constexpr auto FilesSendRestrictionsList() {
|
||||
return std::array{
|
||||
ChatRestriction::SendStickers,
|
||||
ChatRestriction::SendGifs,
|
||||
ChatRestriction::SendPhotos,
|
||||
ChatRestriction::SendVideos,
|
||||
ChatRestriction::SendMusic,
|
||||
ChatRestriction::SendFiles,
|
||||
};
|
||||
}
|
||||
[[nodiscard]] inline constexpr auto TabbedPanelSendRestrictionsList() {
|
||||
return std::array{
|
||||
ChatRestriction::SendStickers,
|
||||
ChatRestriction::SendGifs,
|
||||
ChatRestriction::SendOther,
|
||||
};
|
||||
}
|
||||
[[nodiscard]] ChatRestrictions AllSendRestrictions();
|
||||
[[nodiscard]] ChatRestrictions FilesSendRestrictions();
|
||||
[[nodiscard]] ChatRestrictions TabbedPanelSendRestrictions();
|
||||
|
||||
[[nodiscard]] bool CanSendAnyOf(
|
||||
not_null<const Thread*> thread,
|
||||
ChatRestrictions rights,
|
||||
bool forbidInForums = true);
|
||||
[[nodiscard]] bool CanSendAnyOf(
|
||||
not_null<const PeerData*> peer,
|
||||
ChatRestrictions rights,
|
||||
bool forbidInForums = true);
|
||||
|
||||
[[nodiscard]] inline bool CanSend(
|
||||
not_null<const Thread*> thread,
|
||||
ChatRestriction right,
|
||||
bool forbidInForums = true) {
|
||||
return CanSendAnyOf(thread, right, forbidInForums);
|
||||
}
|
||||
[[nodiscard]] inline bool CanSend(
|
||||
not_null<const PeerData*> peer,
|
||||
ChatRestriction right,
|
||||
bool forbidInForums = true) {
|
||||
return CanSendAnyOf(peer, right, forbidInForums);
|
||||
}
|
||||
[[nodiscard]] inline bool CanSendTexts(
|
||||
not_null<const Thread*> thread,
|
||||
bool forbidInForums = true) {
|
||||
return CanSend(thread, ChatRestriction::SendOther, forbidInForums);
|
||||
}
|
||||
[[nodiscard]] inline bool CanSendTexts(
|
||||
not_null<const PeerData*> peer,
|
||||
bool forbidInForums = true) {
|
||||
return CanSend(peer, ChatRestriction::SendOther, forbidInForums);
|
||||
}
|
||||
[[nodiscard]] inline bool CanSendAnything(
|
||||
not_null<const Thread*> thread,
|
||||
bool forbidInForums = true) {
|
||||
return CanSendAnyOf(thread, AllSendRestrictions(), forbidInForums);
|
||||
}
|
||||
[[nodiscard]] inline bool CanSendAnything(
|
||||
not_null<const PeerData*> peer,
|
||||
bool forbidInForums = true) {
|
||||
return CanSendAnyOf(peer, AllSendRestrictions(), forbidInForums);
|
||||
}
|
||||
|
||||
struct SendError {
|
||||
SendError(QString text = QString()) : text(std::move(text)) {
|
||||
}
|
||||
|
||||
struct Args {
|
||||
QString text;
|
||||
int boostsToLift = 0;
|
||||
bool monoforumAdmin = false;
|
||||
bool premiumToLift = false;
|
||||
bool frozen = false;
|
||||
};
|
||||
SendError(Args &&args)
|
||||
: text(std::move(args.text))
|
||||
, boostsToLift(args.boostsToLift)
|
||||
, monoforumAdmin(args.monoforumAdmin)
|
||||
, premiumToLift(args.premiumToLift)
|
||||
, frozen(args.frozen) {
|
||||
}
|
||||
|
||||
QString text;
|
||||
int boostsToLift = 0;
|
||||
bool monoforumAdmin = false;
|
||||
bool premiumToLift = false;
|
||||
bool frozen = false;
|
||||
|
||||
[[nodiscard]] SendError value_or(SendError other) const {
|
||||
return *this ? *this : other;
|
||||
}
|
||||
|
||||
explicit operator bool() const {
|
||||
return monoforumAdmin || !text.isEmpty();
|
||||
}
|
||||
[[nodiscard]] bool has_value() const {
|
||||
return !text.isEmpty();
|
||||
}
|
||||
[[nodiscard]] const QString &operator*() const {
|
||||
return text;
|
||||
}
|
||||
};
|
||||
|
||||
struct SendErrorWithThread {
|
||||
SendError error;
|
||||
Thread *thread = nullptr;
|
||||
};
|
||||
|
||||
[[nodiscard]] SendError RestrictionError(
|
||||
not_null<PeerData*> peer,
|
||||
ChatRestriction restriction);
|
||||
[[nodiscard]] SendError AnyFileRestrictionError(not_null<PeerData*> peer);
|
||||
[[nodiscard]] SendError FileRestrictionError(
|
||||
not_null<PeerData*> peer,
|
||||
const Ui::PreparedList &list,
|
||||
std::optional<bool> compress);
|
||||
[[nodiscard]] SendError FileRestrictionError(
|
||||
not_null<PeerData*> peer,
|
||||
const Ui::PreparedFile &file,
|
||||
std::optional<bool> compress);
|
||||
|
||||
void ShowSendErrorToast(
|
||||
not_null<Window::SessionNavigation*> navigation,
|
||||
not_null<PeerData*> peer,
|
||||
SendError error);
|
||||
void ShowSendErrorToast(
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
not_null<PeerData*> peer,
|
||||
SendError error);
|
||||
|
||||
} // namespace Data
|
||||
379
Telegram/SourceFiles/data/data_cloud_file.cpp
Normal file
379
Telegram/SourceFiles/data/data_cloud_file.cpp
Normal file
@@ -0,0 +1,379 @@
|
||||
/*
|
||||
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/data_cloud_file.h"
|
||||
|
||||
#include "data/data_file_origin.h"
|
||||
#include "data/data_session.h"
|
||||
#include "storage/cache/storage_cache_database.h"
|
||||
#include "storage/file_download.h"
|
||||
#include "ui/image/image.h"
|
||||
#include "main/main_session.h"
|
||||
|
||||
namespace Data {
|
||||
|
||||
CloudFile::~CloudFile() {
|
||||
// Destroy loader with still alive CloudFile with already zero '.loader'.
|
||||
// Otherwise in ~FileLoader it tries to clear file.loader and crashes.
|
||||
base::take(loader);
|
||||
}
|
||||
|
||||
void CloudFile::clear() {
|
||||
location = {};
|
||||
base::take(loader);
|
||||
byteSize = 0;
|
||||
progressivePartSize = 0;
|
||||
flags = {};
|
||||
}
|
||||
|
||||
CloudImage::CloudImage() = default;
|
||||
|
||||
CloudImage::CloudImage(
|
||||
not_null<Main::Session*> session,
|
||||
const ImageWithLocation &data) {
|
||||
update(session, data);
|
||||
}
|
||||
|
||||
void CloudImage::set(
|
||||
not_null<Main::Session*> session,
|
||||
const ImageWithLocation &data) {
|
||||
const auto &was = _file.location.file().data;
|
||||
const auto &now = data.location.file().data;
|
||||
if (!data.location.valid()) {
|
||||
_file.flags |= CloudFile::Flag::Cancelled;
|
||||
_file.loader = nullptr;
|
||||
_file.location = ImageLocation();
|
||||
_file.byteSize = 0;
|
||||
_file.flags = CloudFile::Flag();
|
||||
_view = std::weak_ptr<QImage>();
|
||||
} else if (was != now
|
||||
&& (!v::is<InMemoryLocation>(was) || v::is<InMemoryLocation>(now))) {
|
||||
_file.location = ImageLocation();
|
||||
_view = std::weak_ptr<QImage>();
|
||||
}
|
||||
UpdateCloudFile(
|
||||
_file,
|
||||
data,
|
||||
session->data().cache(),
|
||||
kImageCacheTag,
|
||||
[=](FileOrigin origin) { load(session, origin); },
|
||||
[=](QImage preloaded, QByteArray) {
|
||||
setToActive(session, std::move(preloaded));
|
||||
});
|
||||
}
|
||||
|
||||
void CloudImage::update(
|
||||
not_null<Main::Session*> session,
|
||||
const ImageWithLocation &data) {
|
||||
UpdateCloudFile(
|
||||
_file,
|
||||
data,
|
||||
session->data().cache(),
|
||||
kImageCacheTag,
|
||||
[=](FileOrigin origin) { load(session, origin); },
|
||||
[=](QImage preloaded, QByteArray) {
|
||||
setToActive(session, std::move(preloaded));
|
||||
});
|
||||
}
|
||||
|
||||
bool CloudImage::empty() const {
|
||||
return !_file.location.valid();
|
||||
}
|
||||
|
||||
bool CloudImage::loading() const {
|
||||
return (_file.loader != nullptr);
|
||||
}
|
||||
|
||||
bool CloudImage::failed() const {
|
||||
return (_file.flags & CloudFile::Flag::Failed);
|
||||
}
|
||||
|
||||
bool CloudImage::loadedOnce() const {
|
||||
return (_file.flags & CloudFile::Flag::Loaded);
|
||||
}
|
||||
|
||||
void CloudImage::load(not_null<Main::Session*> session, FileOrigin origin) {
|
||||
const auto autoLoading = false;
|
||||
const auto finalCheck = [=] {
|
||||
if (const auto active = activeView()) {
|
||||
return active->isNull();
|
||||
} else if (_file.flags & CloudFile::Flag::Loaded) {
|
||||
return false;
|
||||
}
|
||||
return !(_file.flags & CloudFile::Flag::Loaded);
|
||||
};
|
||||
const auto done = [=](QImage result, QByteArray) {
|
||||
setToActive(session, std::move(result));
|
||||
};
|
||||
LoadCloudFile(
|
||||
session,
|
||||
_file,
|
||||
origin,
|
||||
LoadFromCloudOrLocal,
|
||||
autoLoading,
|
||||
kImageCacheTag,
|
||||
finalCheck,
|
||||
done);
|
||||
}
|
||||
|
||||
const ImageLocation &CloudImage::location() const {
|
||||
return _file.location;
|
||||
}
|
||||
|
||||
int CloudImage::byteSize() const {
|
||||
return _file.byteSize;
|
||||
}
|
||||
|
||||
std::shared_ptr<QImage> CloudImage::createView() {
|
||||
if (auto active = activeView()) {
|
||||
return active;
|
||||
}
|
||||
auto view = std::make_shared<QImage>();
|
||||
_view = view;
|
||||
return view;
|
||||
}
|
||||
|
||||
std::shared_ptr<QImage> CloudImage::activeView() const {
|
||||
return _view.lock();
|
||||
}
|
||||
|
||||
bool CloudImage::isCurrentView(const std::shared_ptr<QImage> &view) const {
|
||||
if (!view) {
|
||||
return empty();
|
||||
}
|
||||
return !view.owner_before(_view) && !_view.owner_before(view);
|
||||
}
|
||||
|
||||
void CloudImage::setToActive(
|
||||
not_null<Main::Session*> session,
|
||||
QImage image) {
|
||||
if (const auto view = activeView()) {
|
||||
*view = image.isNull()
|
||||
? Image::Empty()->original()
|
||||
: std::move(image);
|
||||
session->notifyDownloaderTaskFinished();
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateCloudFile(
|
||||
CloudFile &file,
|
||||
const ImageWithLocation &data,
|
||||
Storage::Cache::Database &cache,
|
||||
uint8 cacheTag,
|
||||
Fn<void(FileOrigin)> restartLoader,
|
||||
Fn<void(QImage, QByteArray)> usePreloaded) {
|
||||
if (!data.location.valid()) {
|
||||
if (data.progressivePartSize && !file.location.valid()) {
|
||||
file.progressivePartSize = data.progressivePartSize;
|
||||
}
|
||||
if (data.location.width()
|
||||
&& data.location.height()
|
||||
&& !file.location.valid()
|
||||
&& !file.location.width()) {
|
||||
file.location = data.location;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const auto needStickerThumbnailUpdate = [&] {
|
||||
const auto was = std::get_if<StorageFileLocation>(
|
||||
&file.location.file().data);
|
||||
const auto now = std::get_if<StorageFileLocation>(
|
||||
&data.location.file().data);
|
||||
using Type = StorageFileLocation::Type;
|
||||
if (!was || !now || was->type() != Type::StickerSetThumb) {
|
||||
return false;
|
||||
}
|
||||
return now->valid()
|
||||
&& (now->type() != Type::StickerSetThumb
|
||||
|| now->cacheKey() != was->cacheKey());
|
||||
};
|
||||
const auto update = !file.location.valid()
|
||||
|| (data.location.file().cacheKey()
|
||||
&& (!file.location.file().cacheKey()
|
||||
|| (file.location.width() < data.location.width())
|
||||
|| (file.location.height() < data.location.height())
|
||||
|| needStickerThumbnailUpdate()));
|
||||
if (!update) {
|
||||
return;
|
||||
}
|
||||
auto cacheBytes = !data.bytes.isEmpty()
|
||||
? data.bytes
|
||||
: v::is<InMemoryLocation>(file.location.file().data)
|
||||
? v::get<InMemoryLocation>(file.location.file().data).bytes
|
||||
: QByteArray();
|
||||
if (!cacheBytes.isEmpty()) {
|
||||
if (const auto cacheKey = data.location.file().cacheKey()) {
|
||||
cache.putIfEmpty(
|
||||
cacheKey,
|
||||
Storage::Cache::Database::TaggedValue(
|
||||
std::move(cacheBytes),
|
||||
cacheTag));
|
||||
}
|
||||
}
|
||||
file.location = data.location;
|
||||
file.byteSize = data.bytesCount;
|
||||
if (!data.preloaded.isNull()) {
|
||||
file.loader = nullptr;
|
||||
if (usePreloaded) {
|
||||
usePreloaded(data.preloaded, data.bytes);
|
||||
}
|
||||
} else if (file.loader) {
|
||||
const auto origin = base::take(file.loader)->fileOrigin();
|
||||
restartLoader(origin);
|
||||
} else if (file.flags & CloudFile::Flag::Failed) {
|
||||
file.flags &= ~CloudFile::Flag::Failed;
|
||||
}
|
||||
}
|
||||
|
||||
void LoadCloudFile(
|
||||
not_null<Main::Session*> session,
|
||||
CloudFile &file,
|
||||
FileOrigin origin,
|
||||
LoadFromCloudSetting fromCloud,
|
||||
bool autoLoading,
|
||||
uint8 cacheTag,
|
||||
Fn<bool()> finalCheck,
|
||||
Fn<void(CloudFile&)> done,
|
||||
Fn<void(bool)> fail,
|
||||
Fn<void()> progress,
|
||||
int downloadFrontPartSize = 0) {
|
||||
const auto loadSize = downloadFrontPartSize
|
||||
? std::min(downloadFrontPartSize, file.byteSize)
|
||||
: file.byteSize;
|
||||
if (file.loader) {
|
||||
if (fromCloud == LoadFromCloudOrLocal) {
|
||||
file.loader->permitLoadFromCloud();
|
||||
}
|
||||
if (file.loader->loadSize() < loadSize) {
|
||||
file.loader->increaseLoadSize(loadSize, autoLoading);
|
||||
}
|
||||
return;
|
||||
} else if ((file.flags & CloudFile::Flag::Failed)
|
||||
|| !file.location.valid()
|
||||
|| (finalCheck && !finalCheck())) {
|
||||
return;
|
||||
}
|
||||
file.flags &= ~CloudFile::Flag::Cancelled;
|
||||
file.loader = CreateFileLoader(
|
||||
session,
|
||||
file.location.file(),
|
||||
origin,
|
||||
QString(),
|
||||
loadSize,
|
||||
file.byteSize,
|
||||
UnknownFileLocation,
|
||||
LoadToCacheAsWell,
|
||||
fromCloud,
|
||||
autoLoading,
|
||||
cacheTag);
|
||||
|
||||
const auto finish = [done](CloudFile &file) {
|
||||
if (!file.loader || file.loader->cancelled()) {
|
||||
file.flags |= CloudFile::Flag::Cancelled;
|
||||
} else {
|
||||
file.flags |= CloudFile::Flag::Loaded;
|
||||
done(file);
|
||||
}
|
||||
// NB! file.loader may be in ~FileLoader() already.
|
||||
if (const auto loader = base::take(file.loader)) {
|
||||
if ((file.flags & CloudFile::Flag::Cancelled)
|
||||
&& !loader->cancelled()) {
|
||||
loader->cancel();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
file.loader->updates(
|
||||
) | rpl::on_next_error_done([=] {
|
||||
if (const auto onstack = progress) {
|
||||
onstack();
|
||||
}
|
||||
}, [=, &file](FileLoader::Error error) {
|
||||
finish(file);
|
||||
file.flags |= CloudFile::Flag::Failed;
|
||||
if (const auto onstack = fail) {
|
||||
onstack(error.started);
|
||||
}
|
||||
}, [=, &file] {
|
||||
finish(file);
|
||||
}, file.loader->lifetime());
|
||||
|
||||
file.loader->start();
|
||||
}
|
||||
|
||||
void LoadCloudFile(
|
||||
not_null<Main::Session*> session,
|
||||
CloudFile &file,
|
||||
FileOrigin origin,
|
||||
LoadFromCloudSetting fromCloud,
|
||||
bool autoLoading,
|
||||
uint8 cacheTag,
|
||||
Fn<bool()> finalCheck,
|
||||
Fn<void(QImage, QByteArray)> done,
|
||||
Fn<void(bool)> fail,
|
||||
Fn<void()> progress,
|
||||
int downloadFrontPartSize) {
|
||||
const auto callback = [=](CloudFile &file) {
|
||||
if (auto read = file.loader->imageData(); read.isNull()) {
|
||||
file.flags |= CloudFile::Flag::Failed;
|
||||
if (const auto onstack = fail) {
|
||||
onstack(true);
|
||||
}
|
||||
} else if (const auto onstack = done) {
|
||||
onstack(std::move(read), file.loader->bytes());
|
||||
}
|
||||
};
|
||||
LoadCloudFile(
|
||||
session,
|
||||
file,
|
||||
origin,
|
||||
fromCloud,
|
||||
autoLoading,
|
||||
cacheTag,
|
||||
finalCheck,
|
||||
callback,
|
||||
std::move(fail),
|
||||
std::move(progress),
|
||||
downloadFrontPartSize);
|
||||
}
|
||||
|
||||
void LoadCloudFile(
|
||||
not_null<Main::Session*> session,
|
||||
CloudFile &file,
|
||||
FileOrigin origin,
|
||||
LoadFromCloudSetting fromCloud,
|
||||
bool autoLoading,
|
||||
uint8 cacheTag,
|
||||
Fn<bool()> finalCheck,
|
||||
Fn<void(QByteArray)> done,
|
||||
Fn<void(bool)> fail,
|
||||
Fn<void()> progress) {
|
||||
const auto callback = [=](CloudFile &file) {
|
||||
if (auto bytes = file.loader->bytes(); bytes.isEmpty()) {
|
||||
file.flags |= CloudFile::Flag::Failed;
|
||||
if (const auto onstack = fail) {
|
||||
onstack(true);
|
||||
}
|
||||
} else if (const auto onstack = done) {
|
||||
onstack(std::move(bytes));
|
||||
}
|
||||
};
|
||||
LoadCloudFile(
|
||||
session,
|
||||
file,
|
||||
origin,
|
||||
fromCloud,
|
||||
autoLoading,
|
||||
cacheTag,
|
||||
finalCheck,
|
||||
callback,
|
||||
std::move(fail),
|
||||
std::move(progress));
|
||||
}
|
||||
|
||||
} // namespace Data
|
||||
119
Telegram/SourceFiles/data/data_cloud_file.h
Normal file
119
Telegram/SourceFiles/data/data_cloud_file.h
Normal file
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
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/flags.h"
|
||||
#include "ui/image/image.h"
|
||||
#include "ui/image/image_location.h"
|
||||
|
||||
class FileLoader;
|
||||
|
||||
namespace Storage {
|
||||
namespace Cache {
|
||||
class Database;
|
||||
} // namespace Cache
|
||||
} // namespace Storage
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace Data {
|
||||
|
||||
struct FileOrigin;
|
||||
|
||||
struct CloudFile final {
|
||||
enum class Flag : uchar {
|
||||
Cancelled = 0x01,
|
||||
Failed = 0x02,
|
||||
Loaded = 0x04,
|
||||
};
|
||||
friend inline constexpr bool is_flag_type(Flag) { return true; };
|
||||
|
||||
~CloudFile();
|
||||
|
||||
void clear();
|
||||
|
||||
ImageLocation location;
|
||||
std::unique_ptr<FileLoader> loader;
|
||||
int byteSize = 0;
|
||||
int progressivePartSize = 0;
|
||||
base::flags<Flag> flags;
|
||||
};
|
||||
|
||||
class CloudImage final {
|
||||
public:
|
||||
CloudImage();
|
||||
CloudImage(
|
||||
not_null<Main::Session*> session,
|
||||
const ImageWithLocation &data);
|
||||
|
||||
// This method will replace the location and zero the _view pointer.
|
||||
void set(
|
||||
not_null<Main::Session*> session,
|
||||
const ImageWithLocation &data);
|
||||
|
||||
void update(
|
||||
not_null<Main::Session*> session,
|
||||
const ImageWithLocation &data);
|
||||
|
||||
[[nodiscard]] bool empty() const;
|
||||
[[nodiscard]] bool loading() const;
|
||||
[[nodiscard]] bool failed() const;
|
||||
[[nodiscard]] bool loadedOnce() const;
|
||||
void load(not_null<Main::Session*> session, FileOrigin origin);
|
||||
[[nodiscard]] const ImageLocation &location() const;
|
||||
[[nodiscard]] int byteSize() const;
|
||||
|
||||
[[nodiscard]] std::shared_ptr<QImage> createView();
|
||||
[[nodiscard]] std::shared_ptr<QImage> activeView() const;
|
||||
[[nodiscard]] bool isCurrentView(
|
||||
const std::shared_ptr<QImage> &view) const;
|
||||
|
||||
private:
|
||||
void setToActive(not_null<Main::Session*> session, QImage image);
|
||||
|
||||
CloudFile _file;
|
||||
std::weak_ptr<QImage> _view;
|
||||
|
||||
};
|
||||
|
||||
void UpdateCloudFile(
|
||||
CloudFile &file,
|
||||
const ImageWithLocation &data,
|
||||
Storage::Cache::Database &cache,
|
||||
uint8 cacheTag,
|
||||
Fn<void(FileOrigin)> restartLoader,
|
||||
Fn<void(QImage, QByteArray)> usePreloaded = nullptr);
|
||||
|
||||
void LoadCloudFile(
|
||||
not_null<Main::Session*> session,
|
||||
CloudFile &file,
|
||||
FileOrigin origin,
|
||||
LoadFromCloudSetting fromCloud,
|
||||
bool autoLoading,
|
||||
uint8 cacheTag,
|
||||
Fn<bool()> finalCheck,
|
||||
Fn<void(QImage, QByteArray)> done,
|
||||
Fn<void(bool)> fail = nullptr,
|
||||
Fn<void()> progress = nullptr,
|
||||
int downloadFrontPartSize = 0);
|
||||
|
||||
void LoadCloudFile(
|
||||
not_null<Main::Session*> session,
|
||||
CloudFile &file,
|
||||
FileOrigin origin,
|
||||
LoadFromCloudSetting fromCloud,
|
||||
bool autoLoading,
|
||||
uint8 cacheTag,
|
||||
Fn<bool()> finalCheck,
|
||||
Fn<void(QByteArray)> done,
|
||||
Fn<void(bool)> fail = nullptr,
|
||||
Fn<void()> progress = nullptr);
|
||||
|
||||
} // namespace Data
|
||||
782
Telegram/SourceFiles/data/data_cloud_themes.cpp
Normal file
782
Telegram/SourceFiles/data/data_cloud_themes.cpp
Normal file
@@ -0,0 +1,782 @@
|
||||
/*
|
||||
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/data_cloud_themes.h"
|
||||
|
||||
#include "api/api_premium.h"
|
||||
#include "window/themes/window_theme.h"
|
||||
#include "window/themes/window_theme_preview.h"
|
||||
#include "window/themes/window_theme_editor_box.h"
|
||||
#include "window/window_controller.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_file_origin.h"
|
||||
#include "data/data_document_media.h"
|
||||
#include "main/main_session.h"
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
#include "media/view/media_view_open_common.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "apiwrap.h"
|
||||
|
||||
namespace Data {
|
||||
namespace {
|
||||
|
||||
constexpr auto kFirstReloadTimeout = 10 * crl::time(1000);
|
||||
constexpr auto kReloadTimeout = 3600 * crl::time(1000);
|
||||
constexpr auto kGiftThemesLimit = 24;
|
||||
|
||||
bool IsTestingColors/* = false*/;
|
||||
|
||||
} // namespace
|
||||
|
||||
CloudTheme CloudTheme::Parse(
|
||||
not_null<Main::Session*> session,
|
||||
const MTPDtheme &data,
|
||||
bool parseSettings) {
|
||||
const auto document = data.vdocument();
|
||||
const auto paper = [&](const MTPThemeSettings &settings) {
|
||||
const auto &data = settings.data();
|
||||
return data.vwallpaper()
|
||||
? WallPaper::Create(session, *data.vwallpaper())
|
||||
: std::nullopt;
|
||||
};
|
||||
const auto outgoingMessagesColors = [&](
|
||||
const MTPThemeSettings &settings) {
|
||||
auto result = std::vector<QColor>();
|
||||
const auto &data = settings.data();
|
||||
if (const auto colors = data.vmessage_colors()) {
|
||||
for (const auto &color : colors->v) {
|
||||
result.push_back(Ui::ColorFromSerialized(color));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const auto accentColor = [&](const MTPThemeSettings &settings) {
|
||||
const auto &data = settings.data();
|
||||
return Ui::ColorFromSerialized(data.vaccent_color());
|
||||
};
|
||||
const auto outgoingAccentColor = [&](const MTPThemeSettings &settings) {
|
||||
const auto &data = settings.data();
|
||||
return Ui::MaybeColorFromSerialized(data.voutbox_accent_color());
|
||||
};
|
||||
const auto basedOnDark = [&](const MTPThemeSettings &settings) {
|
||||
const auto &data = settings.data();
|
||||
return data.vbase_theme().match([](const MTPDbaseThemeNight &) {
|
||||
return true;
|
||||
}, [](const MTPDbaseThemeTinted &) {
|
||||
return true;
|
||||
}, [](const auto &) {
|
||||
return false;
|
||||
});
|
||||
};
|
||||
const auto settings = [&] {
|
||||
auto result = base::flat_map<Type, Settings>();
|
||||
const auto settings = data.vsettings();
|
||||
if (!settings) {
|
||||
return result;
|
||||
}
|
||||
for (const auto &fields : settings->v) {
|
||||
const auto type = basedOnDark(fields) ? Type::Dark : Type::Light;
|
||||
result.emplace(type, Settings{
|
||||
.paper = paper(fields),
|
||||
.accentColor = accentColor(fields),
|
||||
.outgoingAccentColor = outgoingAccentColor(fields),
|
||||
.outgoingMessagesColors = outgoingMessagesColors(fields),
|
||||
});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
return {
|
||||
.id = data.vid().v,
|
||||
.accessHash = data.vaccess_hash().v,
|
||||
.slug = qs(data.vslug()),
|
||||
.title = qs(data.vtitle()),
|
||||
.documentId = (document
|
||||
? session->data().processDocument(*document)->id
|
||||
: DocumentId(0)),
|
||||
.createdBy = data.is_creator() ? session->userId() : UserId(0),
|
||||
.usersCount = data.vinstalls_count().value_or_empty(),
|
||||
.emoticon = qs(data.vemoticon().value_or_empty()),
|
||||
.settings = (parseSettings
|
||||
? settings()
|
||||
: base::flat_map<Type, Settings>()),
|
||||
};
|
||||
}
|
||||
|
||||
CloudTheme CloudTheme::Parse(
|
||||
not_null<Main::Session*> session,
|
||||
const MTPDchatThemeUniqueGift &data,
|
||||
bool parseSettings) {
|
||||
const auto gift = Api::FromTL(session, data.vgift());
|
||||
if (!gift || !gift->unique) {
|
||||
return {};
|
||||
}
|
||||
const auto paper = [&](const MTPThemeSettings &settings) {
|
||||
const auto &data = settings.data();
|
||||
return data.vwallpaper()
|
||||
? WallPaper::Create(session, *data.vwallpaper())
|
||||
: std::nullopt;
|
||||
};
|
||||
const auto basedOnDark = [&](const MTPThemeSettings &settings) {
|
||||
const auto &data = settings.data();
|
||||
return data.vbase_theme().match([](const MTPDbaseThemeNight &) {
|
||||
return true;
|
||||
}, [](const MTPDbaseThemeTinted &) {
|
||||
return true;
|
||||
}, [](const auto &) {
|
||||
return false;
|
||||
});
|
||||
};
|
||||
const auto outgoingMessagesColors = [&](
|
||||
const MTPThemeSettings &settings) {
|
||||
auto result = std::vector<QColor>();
|
||||
const auto &data = settings.data();
|
||||
if (const auto colors = data.vmessage_colors()) {
|
||||
for (const auto &color : colors->v) {
|
||||
result.push_back(Ui::ColorFromSerialized(color));
|
||||
}
|
||||
//} else if (basedOnDark(settings)) {
|
||||
// result.push_back(gift->unique->backdrop.edgeColor);
|
||||
//} else {
|
||||
// result.push_back(anim::color(
|
||||
// gift->unique->backdrop.patternColor,
|
||||
// QColor(255, 255, 255, 255),
|
||||
// 0.75));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const auto accentColor = [&](const MTPThemeSettings &settings) {
|
||||
const auto &data = settings.data();
|
||||
return Ui::ColorFromSerialized(data.vaccent_color());
|
||||
};
|
||||
const auto outgoingAccentColor = [&](const MTPThemeSettings &settings) {
|
||||
const auto &data = settings.data();
|
||||
return Ui::MaybeColorFromSerialized(
|
||||
data.voutbox_accent_color()
|
||||
);// .value_or(gift->unique->backdrop.patternColor);
|
||||
};
|
||||
const auto settings = [&] {
|
||||
auto result = base::flat_map<Type, Settings>();
|
||||
for (const auto &fields : data.vtheme_settings().v) {
|
||||
const auto type = basedOnDark(fields) ? Type::Dark : Type::Light;
|
||||
result.emplace(type, Settings{
|
||||
.paper = paper(fields),
|
||||
.accentColor = accentColor(fields),
|
||||
.outgoingAccentColor = outgoingAccentColor(fields),
|
||||
.outgoingMessagesColors = outgoingMessagesColors(fields),
|
||||
});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
return {
|
||||
.id = gift->unique->id,
|
||||
.unique = gift->unique,
|
||||
.settings = (parseSettings
|
||||
? settings()
|
||||
: base::flat_map<Type, Settings>()),
|
||||
};
|
||||
}
|
||||
|
||||
CloudTheme CloudTheme::Parse(
|
||||
not_null<Main::Session*> session,
|
||||
const MTPTheme &data,
|
||||
bool parseSettings) {
|
||||
return data.match([&](const MTPDtheme &data) {
|
||||
return CloudTheme::Parse(session, data, parseSettings);
|
||||
});
|
||||
}
|
||||
|
||||
QString CloudThemes::Format() {
|
||||
static const auto kResult = QString::fromLatin1("tdesktop");
|
||||
return kResult;
|
||||
}
|
||||
|
||||
CloudThemes::CloudThemes(not_null<Main::Session*> session)
|
||||
: _session(session)
|
||||
, _reloadCurrentTimer([=] { reloadCurrent(); }) {
|
||||
setupReload();
|
||||
}
|
||||
|
||||
void CloudThemes::setupReload() {
|
||||
using namespace Window::Theme;
|
||||
|
||||
if (needReload()) {
|
||||
_reloadCurrentTimer.callOnce(kFirstReloadTimeout);
|
||||
}
|
||||
Background()->updates(
|
||||
) | rpl::filter([](const BackgroundUpdate &update) {
|
||||
return (update.type == BackgroundUpdate::Type::ApplyingTheme);
|
||||
}) | rpl::map([=] {
|
||||
return needReload();
|
||||
}) | rpl::on_next([=](bool need) {
|
||||
install();
|
||||
if (need) {
|
||||
scheduleReload();
|
||||
} else {
|
||||
_reloadCurrentTimer.cancel();
|
||||
}
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
bool CloudThemes::needReload() const {
|
||||
const auto &fields = Window::Theme::Background()->themeObject().cloud;
|
||||
return fields.id && fields.documentId;
|
||||
}
|
||||
|
||||
void CloudThemes::install() {
|
||||
using namespace Window::Theme;
|
||||
|
||||
const auto &fields = Background()->themeObject().cloud;
|
||||
auto &themeId = IsNightMode()
|
||||
? _installedNightThemeId
|
||||
: _installedDayThemeId;
|
||||
const auto cloudId = fields.documentId ? fields.id : uint64(0);
|
||||
if (themeId == cloudId) {
|
||||
return;
|
||||
}
|
||||
themeId = cloudId;
|
||||
using Flag = MTPaccount_InstallTheme::Flag;
|
||||
const auto flags = (IsNightMode() ? Flag::f_dark : Flag(0))
|
||||
| Flag::f_format
|
||||
| (themeId ? Flag::f_theme : Flag(0));
|
||||
_session->api().request(MTPaccount_InstallTheme(
|
||||
MTP_flags(flags),
|
||||
MTP_inputTheme(MTP_long(cloudId), MTP_long(fields.accessHash)),
|
||||
MTP_string(Format()),
|
||||
MTPBaseTheme()
|
||||
)).send();
|
||||
}
|
||||
|
||||
void CloudThemes::reloadCurrent() {
|
||||
if (!needReload()) {
|
||||
return;
|
||||
}
|
||||
const auto &fields = Window::Theme::Background()->themeObject().cloud;
|
||||
_session->api().request(MTPaccount_GetTheme(
|
||||
MTP_string(Format()),
|
||||
MTP_inputTheme(MTP_long(fields.id), MTP_long(fields.accessHash))
|
||||
)).done([=](const MTPTheme &result) {
|
||||
applyUpdate(result);
|
||||
}).fail([=] {
|
||||
_reloadCurrentTimer.callOnce(kReloadTimeout);
|
||||
}).send();
|
||||
}
|
||||
|
||||
void CloudThemes::applyUpdate(const MTPTheme &theme) {
|
||||
theme.match([&](const MTPDtheme &data) {
|
||||
const auto cloud = CloudTheme::Parse(_session, data);
|
||||
const auto &object = Window::Theme::Background()->themeObject();
|
||||
if ((cloud.id != object.cloud.id)
|
||||
|| (cloud.documentId == object.cloud.documentId)
|
||||
|| !cloud.documentId) {
|
||||
return;
|
||||
}
|
||||
applyFromDocument(cloud);
|
||||
});
|
||||
scheduleReload();
|
||||
}
|
||||
|
||||
void CloudThemes::resolve(
|
||||
not_null<Window::Controller*> controller,
|
||||
const QString &slug,
|
||||
const FullMsgId &clickFromMessageId) {
|
||||
_session->api().request(_resolveRequestId).cancel();
|
||||
_resolveRequestId = _session->api().request(MTPaccount_GetTheme(
|
||||
MTP_string(Format()),
|
||||
MTP_inputThemeSlug(MTP_string(slug))
|
||||
)).done([=](const MTPTheme &result) {
|
||||
showPreview(controller, result);
|
||||
}).fail([=](const MTP::Error &error) {
|
||||
if (error.type() == u"THEME_FORMAT_INVALID"_q) {
|
||||
controller->show(Ui::MakeInformBox(tr::lng_theme_no_desktop()));
|
||||
}
|
||||
}).send();
|
||||
}
|
||||
|
||||
void CloudThemes::showPreview(
|
||||
not_null<Window::Controller*> controller,
|
||||
const MTPTheme &data) {
|
||||
data.match([&](const MTPDtheme &data) {
|
||||
showPreview(controller, CloudTheme::Parse(_session, data));
|
||||
});
|
||||
}
|
||||
|
||||
void CloudThemes::showPreview(
|
||||
not_null<Window::Controller*> controller,
|
||||
const CloudTheme &cloud) {
|
||||
if (cloud.documentId) {
|
||||
previewFromDocument(controller, cloud);
|
||||
} else if (cloud.createdBy == _session->userId()) {
|
||||
controller->show(Box(
|
||||
Window::Theme::CreateForExistingBox,
|
||||
controller,
|
||||
cloud));
|
||||
} else {
|
||||
controller->show(Ui::MakeInformBox(tr::lng_theme_no_desktop()));
|
||||
}
|
||||
}
|
||||
|
||||
void CloudThemes::applyFromDocument(const CloudTheme &cloud) {
|
||||
const auto document = _session->data().document(cloud.documentId);
|
||||
loadDocumentAndInvoke(_updatingFrom, cloud, document, [=](
|
||||
std::shared_ptr<Data::DocumentMedia> media) {
|
||||
const auto document = media->owner();
|
||||
auto preview = Window::Theme::PreviewFromFile(
|
||||
media->bytes(),
|
||||
document->location().name(),
|
||||
cloud);
|
||||
if (preview) {
|
||||
Window::Theme::Apply(std::move(preview));
|
||||
Window::Theme::KeepApplied();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void CloudThemes::previewFromDocument(
|
||||
not_null<Window::Controller*> controller,
|
||||
const CloudTheme &cloud) {
|
||||
const auto sessionController = controller->sessionController();
|
||||
if (!sessionController) {
|
||||
return;
|
||||
}
|
||||
const auto document = _session->data().document(cloud.documentId);
|
||||
loadDocumentAndInvoke(_previewFrom, cloud, document, [=](
|
||||
std::shared_ptr<Data::DocumentMedia> media) {
|
||||
const auto document = media->owner();
|
||||
using Open = Media::View::OpenRequest;
|
||||
controller->openInMediaView(Open(sessionController, document, cloud));
|
||||
});
|
||||
}
|
||||
|
||||
void CloudThemes::loadDocumentAndInvoke(
|
||||
LoadingDocument &value,
|
||||
const CloudTheme &cloud,
|
||||
not_null<DocumentData*> document,
|
||||
Fn<void(std::shared_ptr<Data::DocumentMedia>)> callback) {
|
||||
const auto alreadyWaiting = (value.document != nullptr);
|
||||
if (alreadyWaiting) {
|
||||
value.document->cancel();
|
||||
}
|
||||
value.document = document;
|
||||
value.documentMedia = document->createMediaView();
|
||||
value.document->save(
|
||||
Data::FileOriginTheme(cloud.id, cloud.accessHash),
|
||||
QString());
|
||||
value.callback = std::move(callback);
|
||||
if (value.documentMedia->loaded()) {
|
||||
invokeForLoaded(value);
|
||||
return;
|
||||
}
|
||||
if (!alreadyWaiting) {
|
||||
_session->downloaderTaskFinished(
|
||||
) | rpl::filter([=, &value] {
|
||||
return value.documentMedia->loaded();
|
||||
}) | rpl::on_next([=, &value] {
|
||||
invokeForLoaded(value);
|
||||
}, value.subscription);
|
||||
}
|
||||
}
|
||||
|
||||
void CloudThemes::invokeForLoaded(LoadingDocument &value) {
|
||||
const auto onstack = std::move(value.callback);
|
||||
auto media = std::move(value.documentMedia);
|
||||
value = LoadingDocument();
|
||||
onstack(std::move(media));
|
||||
}
|
||||
|
||||
void CloudThemes::scheduleReload() {
|
||||
if (needReload()) {
|
||||
_reloadCurrentTimer.callOnce(kReloadTimeout);
|
||||
} else {
|
||||
_reloadCurrentTimer.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
void CloudThemes::refresh() {
|
||||
if (_refreshRequestId) {
|
||||
return;
|
||||
}
|
||||
_refreshRequestId = _session->api().request(MTPaccount_GetThemes(
|
||||
MTP_string(Format()),
|
||||
MTP_long(_hash)
|
||||
)).done([=](const MTPaccount_Themes &result) {
|
||||
_refreshRequestId = 0;
|
||||
result.match([&](const MTPDaccount_themes &data) {
|
||||
_hash = data.vhash().v;
|
||||
parseThemes(data.vthemes().v);
|
||||
_updates.fire({});
|
||||
}, [](const MTPDaccount_themesNotModified &) {
|
||||
});
|
||||
}).fail([=] {
|
||||
_refreshRequestId = 0;
|
||||
}).send();
|
||||
}
|
||||
|
||||
void CloudThemes::parseThemes(const QVector<MTPTheme> &list) {
|
||||
_list.clear();
|
||||
_list.reserve(list.size());
|
||||
for (const auto &theme : list) {
|
||||
_list.push_back(CloudTheme::Parse(_session, theme));
|
||||
}
|
||||
checkCurrentTheme();
|
||||
}
|
||||
|
||||
void CloudThemes::refreshChatThemes() {
|
||||
if (_chatThemesRequestId) {
|
||||
return;
|
||||
}
|
||||
_chatThemesRequestId = _session->api().request(MTPaccount_GetChatThemes(
|
||||
MTP_long(_chatThemesHash)
|
||||
)).done([=](const MTPaccount_Themes &result) {
|
||||
_chatThemesRequestId = 0;
|
||||
result.match([&](const MTPDaccount_themes &data) {
|
||||
_chatThemesHash = data.vhash().v;
|
||||
parseChatThemes(data.vthemes().v);
|
||||
_chatThemesUpdates.fire({});
|
||||
}, [](const MTPDaccount_themesNotModified &) {
|
||||
});
|
||||
}).fail([=] {
|
||||
_chatThemesRequestId = 0;
|
||||
}).send();
|
||||
}
|
||||
|
||||
const std::vector<CloudTheme> &CloudThemes::chatThemes() const {
|
||||
return _chatThemes;
|
||||
}
|
||||
|
||||
rpl::producer<> CloudThemes::chatThemesUpdated() const {
|
||||
return _chatThemesUpdates.events();
|
||||
}
|
||||
|
||||
std::optional<CloudTheme> CloudThemes::themeForToken(
|
||||
const QString &token) const {
|
||||
if (token.startsWith(u"gift:"_q)) {
|
||||
const auto id = QStringView(token).mid(5).toULongLong();
|
||||
const auto i = _giftThemes.find(id);
|
||||
return (i != end(_giftThemes))
|
||||
? i->second
|
||||
: std::optional<CloudTheme>();
|
||||
}
|
||||
const auto emoji = Ui::Emoji::Find(token);
|
||||
if (!emoji) {
|
||||
return {};
|
||||
}
|
||||
const auto i = ranges::find(_chatThemes, emoji, [](const CloudTheme &v) {
|
||||
return Ui::Emoji::Find(v.emoticon);
|
||||
});
|
||||
return (i != end(_chatThemes)) ? std::make_optional(*i) : std::nullopt;
|
||||
}
|
||||
|
||||
rpl::producer<std::optional<CloudTheme>> CloudThemes::themeForTokenValue(
|
||||
const QString &token) {
|
||||
if (token.startsWith(u"gift:"_q)) {
|
||||
return rpl::single(themeForToken(token));
|
||||
}
|
||||
const auto testing = TestingColors();
|
||||
if (!Ui::Emoji::Find(token)) {
|
||||
return rpl::single<std::optional<CloudTheme>>(std::nullopt);
|
||||
} else if (auto result = themeForToken(token)) {
|
||||
if (testing) {
|
||||
return rpl::single(
|
||||
std::move(result)
|
||||
) | rpl::then(chatThemesUpdated(
|
||||
) | rpl::map([=] {
|
||||
return themeForToken(token);
|
||||
}) | rpl::filter([](const std::optional<CloudTheme> &theme) {
|
||||
return theme.has_value();
|
||||
}));
|
||||
}
|
||||
return rpl::single(std::move(result));
|
||||
}
|
||||
refreshChatThemes();
|
||||
const auto limit = testing ? (1 << 20) : 1;
|
||||
return rpl::single<std::optional<CloudTheme>>(
|
||||
std::nullopt
|
||||
) | rpl::then(chatThemesUpdated(
|
||||
) | rpl::map([=] {
|
||||
return themeForToken(token);
|
||||
}) | rpl::filter([](const std::optional<CloudTheme> &theme) {
|
||||
return theme.has_value();
|
||||
}) | rpl::take(limit));
|
||||
}
|
||||
|
||||
void CloudThemes::myGiftThemesLoadMore(bool reload) {
|
||||
if (reload && !_myGiftThemesTokens.empty()) {
|
||||
_session->api().request(base::take(_myGiftThemesRequestId)).cancel();
|
||||
}
|
||||
if (_myGiftThemesRequestId || (!reload && _myGiftThemesLoaded)) {
|
||||
return;
|
||||
}
|
||||
_myGiftThemesRequestId = _session->api().request(
|
||||
MTPaccount_GetUniqueGiftChatThemes(
|
||||
MTP_string(reload ? QString() : _myGiftThemesNextOffset),
|
||||
MTP_int(kGiftThemesLimit),
|
||||
MTP_long(_myGiftThemesHash))
|
||||
).done([=](const MTPaccount_ChatThemes &result) {
|
||||
_myGiftThemesRequestId = 0;
|
||||
result.match([&](const MTPDaccount_chatThemes &data) {
|
||||
if (reload || _myGiftThemesTokens.empty()) {
|
||||
_myGiftThemesHash = data.vhash().v;
|
||||
_myGiftThemesTokens.clear();
|
||||
_myGiftThemesLoaded = false;
|
||||
}
|
||||
_session->data().processUsers(data.vusers());
|
||||
_session->data().processChats(data.vchats());
|
||||
const auto &list = data.vthemes().v;
|
||||
const auto got = int(list.size());
|
||||
_myGiftThemesTokens.reserve(_myGiftThemesTokens.size() + got);
|
||||
for (const auto &theme : list) {
|
||||
theme.match([](const MTPDchatTheme &) {
|
||||
}, [&](const MTPDchatThemeUniqueGift &data) {
|
||||
_myGiftThemesTokens.push_back(
|
||||
processGiftThemeGetToken(data));
|
||||
});
|
||||
}
|
||||
if (const auto next = data.vnext_offset()) {
|
||||
_myGiftThemesNextOffset = qs(*next);
|
||||
} else {
|
||||
_myGiftThemesLoaded = true;
|
||||
}
|
||||
_myGiftThemesUpdates.fire({});
|
||||
}, [&](const MTPDaccount_chatThemesNotModified &) {
|
||||
if (!reload) {
|
||||
_myGiftThemesLoaded = true;
|
||||
_myGiftThemesUpdates.fire({});
|
||||
}
|
||||
});
|
||||
}).fail([=] {
|
||||
_myGiftThemesRequestId = 0;
|
||||
_myGiftThemesLoaded = true;
|
||||
}).send();
|
||||
}
|
||||
|
||||
const std::vector<QString> &CloudThemes::myGiftThemesTokens() const {
|
||||
return _myGiftThemesTokens;
|
||||
}
|
||||
|
||||
bool CloudThemes::myGiftThemesReady() const {
|
||||
return !_myGiftThemesTokens.empty() || _myGiftThemesLoaded;
|
||||
}
|
||||
|
||||
rpl::producer<> CloudThemes::myGiftThemesUpdated() const {
|
||||
return _myGiftThemesUpdates.events();
|
||||
}
|
||||
|
||||
QString CloudThemes::processGiftThemeGetToken(
|
||||
const MTPDchatThemeUniqueGift &data) {
|
||||
auto parsed = CloudTheme::Parse(_session, data, true);
|
||||
if (parsed.unique) {
|
||||
const auto id = parsed.unique->id;
|
||||
_giftThemes[id] = std::move(parsed);
|
||||
return u"gift:%1"_q.arg(id);
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
void CloudThemes::refreshChatThemesFor(const QString &token) {
|
||||
if (token.startsWith(u"gift:"_q)) {
|
||||
return;
|
||||
}
|
||||
refreshChatThemes();
|
||||
}
|
||||
|
||||
bool CloudThemes::TestingColors() {
|
||||
return IsTestingColors;
|
||||
}
|
||||
|
||||
void CloudThemes::SetTestingColors(bool testing) {
|
||||
IsTestingColors = testing;
|
||||
}
|
||||
|
||||
QString CloudThemes::prepareTestingLink(const CloudTheme &theme) const {
|
||||
const auto hex = [](int value) {
|
||||
return QChar((value < 10) ? ('0' + value) : ('a' + (value - 10)));
|
||||
};
|
||||
const auto hex2 = [&](int value) {
|
||||
return QString() + hex(value / 16) + hex(value % 16);
|
||||
};
|
||||
const auto color = [&](const QColor &color) {
|
||||
return hex2(color.red()) + hex2(color.green()) + hex2(color.blue());
|
||||
};
|
||||
const auto colors = [&](const std::vector<QColor> &colors) {
|
||||
auto list = QStringList();
|
||||
for (const auto &c : colors) {
|
||||
list.push_back(color(c));
|
||||
}
|
||||
return list.join(",");
|
||||
};
|
||||
auto arguments = QStringList();
|
||||
for (const auto &[type, settings] : theme.settings) {
|
||||
const auto add = [&, type = type](const QString &value) {
|
||||
const auto prefix = (type == CloudTheme::Type::Dark)
|
||||
? u"dark_"_q
|
||||
: u""_q;
|
||||
arguments.push_back(prefix + value);
|
||||
};
|
||||
add("accent=" + color(settings.accentColor));
|
||||
if (settings.paper && !settings.paper->backgroundColors().empty()) {
|
||||
add("bg=" + colors(settings.paper->backgroundColors()));
|
||||
}
|
||||
if (settings.paper/* && settings.paper->hasShareUrl()*/) {
|
||||
add("intensity="
|
||||
+ QString::number(settings.paper->patternIntensity()));
|
||||
//const auto url = settings.paper->shareUrl(_session);
|
||||
//const auto from = url.indexOf("bg/");
|
||||
//const auto till = url.indexOf("?");
|
||||
//if (from > 0 && till > from) {
|
||||
// add("slug=" + url.mid(from + 3, till - from - 3));
|
||||
//}
|
||||
}
|
||||
if (settings.outgoingAccentColor) {
|
||||
add("out_accent" + color(*settings.outgoingAccentColor));
|
||||
}
|
||||
if (!settings.outgoingMessagesColors.empty()) {
|
||||
add("out_bg=" + colors(settings.outgoingMessagesColors));
|
||||
}
|
||||
}
|
||||
return arguments.isEmpty()
|
||||
? QString()
|
||||
: ("tg://test_chat_theme?" + arguments.join("&"));
|
||||
}
|
||||
|
||||
std::optional<CloudTheme> CloudThemes::updateThemeFromLink(
|
||||
const QString &emoticon,
|
||||
const QMap<QString, QString> ¶ms) {
|
||||
const auto emoji = Ui::Emoji::Find(emoticon);
|
||||
if (!TestingColors() || !emoji) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto i = ranges::find(_chatThemes, emoji, [](const CloudTheme &v) {
|
||||
return Ui::Emoji::Find(v.emoticon);
|
||||
});
|
||||
if (i == end(_chatThemes)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto hex = [](const QString &value) {
|
||||
return (value.size() != 1)
|
||||
? std::nullopt
|
||||
: (value[0] >= 'a' && value[0] <= 'f')
|
||||
? std::make_optional(10 + int(value[0].unicode() - 'a'))
|
||||
: (value[0] >= 'A' && value[0] <= 'F')
|
||||
? std::make_optional(10 + int(value[0].unicode() - 'A'))
|
||||
: (value[0] >= '0' && value[0] <= '9')
|
||||
? std::make_optional(int(value[0].unicode() - '0'))
|
||||
: std::nullopt;
|
||||
};
|
||||
const auto hex2 = [&](const QString &value) {
|
||||
const auto first = hex(value.mid(0, 1));
|
||||
const auto second = hex(value.mid(1, 1));
|
||||
return (first && second)
|
||||
? std::make_optional((*first) * 16 + (*second))
|
||||
: std::nullopt;
|
||||
};
|
||||
const auto color = [&](const QString &value) {
|
||||
const auto red = hex2(value.mid(0, 2));
|
||||
const auto green = hex2(value.mid(2, 2));
|
||||
const auto blue = hex2(value.mid(4, 2));
|
||||
return (red && green && blue)
|
||||
? std::make_optional(QColor(*red, *green, *blue))
|
||||
: std::nullopt;
|
||||
};
|
||||
const auto colors = [&](const QString &value) {
|
||||
auto list = value.split(",");
|
||||
auto result = std::vector<QColor>();
|
||||
for (const auto &single : list) {
|
||||
if (const auto c = color(single)) {
|
||||
result.push_back(*c);
|
||||
} else {
|
||||
return std::vector<QColor>();
|
||||
}
|
||||
}
|
||||
return (result.size() > 4) ? std::vector<QColor>() : result;
|
||||
};
|
||||
|
||||
const auto parse = [&](CloudThemeType type, const QString &prefix = {}) {
|
||||
const auto accent = color(params["accent"]);
|
||||
if (!accent) {
|
||||
return;
|
||||
}
|
||||
auto &settings = i->settings[type];
|
||||
settings.accentColor = *accent;
|
||||
const auto bg = colors(params["bg"]);
|
||||
settings.paper = (settings.paper && !bg.empty())
|
||||
? std::make_optional(settings.paper->withBackgroundColors(bg))
|
||||
: settings.paper;
|
||||
settings.paper = (settings.paper && params["intensity"].toInt())
|
||||
? std::make_optional(
|
||||
settings.paper->withPatternIntensity(
|
||||
params["intensity"].toInt()))
|
||||
: settings.paper;
|
||||
settings.outgoingAccentColor = color(params["out_accent"]);
|
||||
settings.outgoingMessagesColors = colors(params["out_bg"]);
|
||||
};
|
||||
if (params.contains("dark_accent")) {
|
||||
parse(CloudThemeType::Dark, "dark_");
|
||||
}
|
||||
if (params.contains("accent")) {
|
||||
parse(params["dark"].isEmpty()
|
||||
? CloudThemeType::Light
|
||||
: CloudThemeType::Dark);
|
||||
}
|
||||
_chatThemesUpdates.fire({});
|
||||
return *i;
|
||||
}
|
||||
|
||||
void CloudThemes::parseChatThemes(const QVector<MTPTheme> &list) {
|
||||
_chatThemes.clear();
|
||||
_chatThemes.reserve(list.size());
|
||||
for (const auto &theme : list) {
|
||||
_chatThemes.push_back(CloudTheme::Parse(_session, theme, true));
|
||||
}
|
||||
}
|
||||
|
||||
void CloudThemes::checkCurrentTheme() {
|
||||
const auto &object = Window::Theme::Background()->themeObject();
|
||||
if (!object.cloud.id || !object.cloud.documentId) {
|
||||
return;
|
||||
}
|
||||
const auto i = ranges::find(_list, object.cloud.id, &CloudTheme::id);
|
||||
if (i == end(_list)) {
|
||||
install();
|
||||
}
|
||||
}
|
||||
|
||||
rpl::producer<> CloudThemes::updated() const {
|
||||
return _updates.events();
|
||||
}
|
||||
|
||||
const std::vector<CloudTheme> &CloudThemes::list() const {
|
||||
return _list;
|
||||
}
|
||||
|
||||
void CloudThemes::savedFromEditor(const CloudTheme &theme) {
|
||||
const auto i = ranges::find(_list, theme.id, &CloudTheme::id);
|
||||
if (i != end(_list)) {
|
||||
*i = theme;
|
||||
_updates.fire({});
|
||||
} else {
|
||||
_list.insert(begin(_list), theme);
|
||||
_updates.fire({});
|
||||
}
|
||||
}
|
||||
|
||||
void CloudThemes::remove(uint64 cloudThemeId) {
|
||||
const auto i = ranges::find(_list, cloudThemeId, &CloudTheme::id);
|
||||
if (i == end(_list)) {
|
||||
return;
|
||||
}
|
||||
_session->api().request(MTPaccount_SaveTheme(
|
||||
MTP_inputTheme(
|
||||
MTP_long(i->id),
|
||||
MTP_long(i->accessHash)),
|
||||
MTP_bool(true)
|
||||
)).send();
|
||||
_list.erase(i);
|
||||
_updates.fire({});
|
||||
}
|
||||
|
||||
} // namespace Data
|
||||
176
Telegram/SourceFiles/data/data_cloud_themes.h
Normal file
176
Telegram/SourceFiles/data/data_cloud_themes.h
Normal file
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
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_wall_paper.h"
|
||||
|
||||
class DocumentData;
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace Window {
|
||||
class Controller;
|
||||
} // namespace Window
|
||||
|
||||
namespace Data {
|
||||
|
||||
struct UniqueGift;
|
||||
class DocumentMedia;
|
||||
|
||||
enum class CloudThemeType {
|
||||
Dark,
|
||||
Light,
|
||||
};
|
||||
|
||||
struct CloudTheme {
|
||||
uint64 id = 0;
|
||||
uint64 accessHash = 0;
|
||||
QString slug;
|
||||
QString title;
|
||||
DocumentId documentId = 0;
|
||||
UserId createdBy = 0;
|
||||
int usersCount = 0;
|
||||
QString emoticon;
|
||||
std::shared_ptr<UniqueGift> unique;
|
||||
|
||||
using Type = CloudThemeType;
|
||||
struct Settings {
|
||||
std::optional<WallPaper> paper;
|
||||
QColor accentColor;
|
||||
std::optional<QColor> outgoingAccentColor;
|
||||
std::vector<QColor> outgoingMessagesColors;
|
||||
};
|
||||
base::flat_map<Type, Settings> settings;
|
||||
|
||||
static CloudTheme Parse(
|
||||
not_null<Main::Session*> session,
|
||||
const MTPDtheme &data,
|
||||
bool parseSettings = false);
|
||||
static CloudTheme Parse(
|
||||
not_null<Main::Session*> session,
|
||||
const MTPDchatThemeUniqueGift &data,
|
||||
bool parseSettings = false);
|
||||
static CloudTheme Parse(
|
||||
not_null<Main::Session*> session,
|
||||
const MTPTheme &data,
|
||||
bool parseSettings = false);
|
||||
};
|
||||
|
||||
class CloudThemes final {
|
||||
public:
|
||||
explicit CloudThemes(not_null<Main::Session*> session);
|
||||
|
||||
[[nodiscard]] static QString Format();
|
||||
|
||||
void refresh();
|
||||
[[nodiscard]] rpl::producer<> updated() const;
|
||||
[[nodiscard]] const std::vector<CloudTheme> &list() const;
|
||||
void savedFromEditor(const CloudTheme &data);
|
||||
void remove(uint64 cloudThemeId);
|
||||
|
||||
void refreshChatThemes();
|
||||
[[nodiscard]] const std::vector<CloudTheme> &chatThemes() const;
|
||||
[[nodiscard]] rpl::producer<> chatThemesUpdated() const;
|
||||
|
||||
void myGiftThemesLoadMore(bool reload = false);
|
||||
[[nodiscard]] const std::vector<QString> &myGiftThemesTokens() const;
|
||||
[[nodiscard]] rpl::producer<> myGiftThemesUpdated() const;
|
||||
[[nodiscard]] QString processGiftThemeGetToken(
|
||||
const MTPDchatThemeUniqueGift &data);
|
||||
[[nodiscard]] bool myGiftThemesReady() const;
|
||||
|
||||
void refreshChatThemesFor(const QString &token);
|
||||
[[nodiscard]] std::optional<CloudTheme> themeForToken(
|
||||
const QString &token) const;
|
||||
[[nodiscard]] auto themeForTokenValue(const QString &token)
|
||||
-> rpl::producer<std::optional<CloudTheme>>;
|
||||
|
||||
[[nodiscard]] static bool TestingColors();
|
||||
static void SetTestingColors(bool testing);
|
||||
[[nodiscard]] QString prepareTestingLink(const CloudTheme &theme) const;
|
||||
[[nodiscard]] std::optional<CloudTheme> updateThemeFromLink(
|
||||
const QString &emoticon,
|
||||
const QMap<QString, QString> ¶ms);
|
||||
|
||||
void applyUpdate(const MTPTheme &theme);
|
||||
|
||||
void resolve(
|
||||
not_null<Window::Controller*> controller,
|
||||
const QString &slug,
|
||||
const FullMsgId &clickFromMessageId);
|
||||
void showPreview(
|
||||
not_null<Window::Controller*> controller,
|
||||
const MTPTheme &data);
|
||||
void showPreview(
|
||||
not_null<Window::Controller*> controller,
|
||||
const CloudTheme &cloud);
|
||||
void applyFromDocument(const CloudTheme &cloud);
|
||||
|
||||
private:
|
||||
struct LoadingDocument {
|
||||
CloudTheme theme;
|
||||
DocumentData *document = nullptr;
|
||||
std::shared_ptr<Data::DocumentMedia> documentMedia;
|
||||
rpl::lifetime subscription;
|
||||
Fn<void(std::shared_ptr<Data::DocumentMedia>)> callback;
|
||||
};
|
||||
|
||||
void parseThemes(const QVector<MTPTheme> &list);
|
||||
void checkCurrentTheme();
|
||||
|
||||
void install();
|
||||
void setupReload();
|
||||
[[nodiscard]] bool needReload() const;
|
||||
void scheduleReload();
|
||||
void reloadCurrent();
|
||||
void previewFromDocument(
|
||||
not_null<Window::Controller*> controller,
|
||||
const CloudTheme &cloud);
|
||||
void loadDocumentAndInvoke(
|
||||
LoadingDocument &value,
|
||||
const CloudTheme &cloud,
|
||||
not_null<DocumentData*> document,
|
||||
Fn<void(std::shared_ptr<Data::DocumentMedia>)> callback);
|
||||
void invokeForLoaded(LoadingDocument &value);
|
||||
|
||||
void parseChatThemes(const QVector<MTPTheme> &list);
|
||||
|
||||
const not_null<Main::Session*> _session;
|
||||
uint64 _hash = 0;
|
||||
mtpRequestId _refreshRequestId = 0;
|
||||
mtpRequestId _resolveRequestId = 0;
|
||||
std::vector<CloudTheme> _list;
|
||||
rpl::event_stream<> _updates;
|
||||
|
||||
uint64 _chatThemesHash = 0;
|
||||
mtpRequestId _chatThemesRequestId = 0;
|
||||
std::vector<CloudTheme> _chatThemes;
|
||||
rpl::event_stream<> _chatThemesUpdates;
|
||||
|
||||
mtpRequestId _myGiftThemesRequestId = 0;
|
||||
base::flat_map<uint64, CloudTheme> _giftThemes;
|
||||
std::vector<QString> _myGiftThemesTokens;
|
||||
rpl::event_stream<> _myGiftThemesUpdates;
|
||||
uint64 _myGiftThemesHash = 0;
|
||||
QString _myGiftThemesNextOffset;
|
||||
bool _myGiftThemesLoaded = false;
|
||||
|
||||
base::Timer _reloadCurrentTimer;
|
||||
LoadingDocument _updatingFrom;
|
||||
LoadingDocument _previewFrom;
|
||||
uint64 _installedDayThemeId = 0;
|
||||
uint64 _installedNightThemeId = 0;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Data
|
||||
166
Telegram/SourceFiles/data/data_credits.h
Normal file
166
Telegram/SourceFiles/data/data_credits.h
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "data/data_subscriptions.h"
|
||||
|
||||
namespace Data {
|
||||
|
||||
struct UniqueGift;
|
||||
struct UniqueGiftValue;
|
||||
|
||||
struct CreditTopupOption final {
|
||||
uint64 credits = 0;
|
||||
QString product;
|
||||
QString currency;
|
||||
uint64 amount = 0;
|
||||
bool extended = false;
|
||||
uint64 giftBarePeerId = 0;
|
||||
};
|
||||
|
||||
using CreditTopupOptions = std::vector<CreditTopupOption>;
|
||||
|
||||
enum class CreditsHistoryMediaType {
|
||||
Photo,
|
||||
Video,
|
||||
};
|
||||
|
||||
struct CreditsHistoryMedia {
|
||||
CreditsHistoryMediaType type = CreditsHistoryMediaType::Photo;
|
||||
uint64 id = 0;
|
||||
};
|
||||
|
||||
struct CreditsHistoryEntry final {
|
||||
explicit operator bool() const {
|
||||
return !id.isEmpty();
|
||||
}
|
||||
|
||||
[[nodiscard]] bool isLiveStoryReaction() const {
|
||||
return paidMessagesCount && reaction && !bareMsgId;
|
||||
}
|
||||
|
||||
using PhotoId = uint64;
|
||||
enum class PeerType {
|
||||
Peer,
|
||||
AppStore,
|
||||
PlayMarket,
|
||||
Fragment,
|
||||
Unsupported,
|
||||
PremiumBot,
|
||||
Ads,
|
||||
API,
|
||||
};
|
||||
|
||||
QString id;
|
||||
QString title;
|
||||
TextWithEntities description;
|
||||
QDateTime date;
|
||||
QDateTime firstSaleDate;
|
||||
QDateTime lastSaleDate;
|
||||
PhotoId photoId = 0;
|
||||
std::vector<CreditsHistoryMedia> extended;
|
||||
CreditsAmount credits;
|
||||
uint64 bareMsgId = 0;
|
||||
uint64 barePeerId = 0;
|
||||
uint64 bareGiveawayMsgId = 0;
|
||||
uint64 bareGiftStickerId = 0;
|
||||
uint64 bareGiftOwnerId = 0;
|
||||
uint64 bareGiftHostId = 0;
|
||||
uint64 bareGiftReleasedById = 0;
|
||||
uint64 bareGiftResaleRecipientId = 0;
|
||||
uint64 bareActorId = 0;
|
||||
uint64 bareEntryOwnerId = 0;
|
||||
uint64 giftChannelSavedId = 0;
|
||||
uint64 stargiftId = 0;
|
||||
QString giftPrepayUpgradeHash;
|
||||
QString giftTitle;
|
||||
std::shared_ptr<UniqueGift> uniqueGift;
|
||||
Fn<std::vector<CreditsHistoryEntry>()> pinnedSavedGifts;
|
||||
uint64 nextToUpgradeStickerId = 0;
|
||||
Fn<void()> nextToUpgradeShow;
|
||||
CreditsAmount starrefAmount;
|
||||
int starrefCommission = 0;
|
||||
uint64 starrefRecipientId = 0;
|
||||
PeerType peerType;
|
||||
QDateTime subscriptionUntil;
|
||||
|
||||
// Currency properties.
|
||||
QDateTime adsProceedsToDate;
|
||||
QString provider; // Unused.
|
||||
|
||||
QDateTime successDate;
|
||||
QString successLink;
|
||||
int paidMessagesCount = 0;
|
||||
CreditsAmount paidMessagesAmount;
|
||||
int paidMessagesCommission = 0;
|
||||
int limitedCount = 0;
|
||||
int limitedLeft = 0;
|
||||
int starsConverted = 0;
|
||||
int starsToUpgrade = 0;
|
||||
int starsUpgradedBySender = 0;
|
||||
int starsForDetailsRemove = 0;
|
||||
int premiumMonthsForStars = 0;
|
||||
int floodSkip = 0;
|
||||
int giftNumber = 0;
|
||||
bool converted : 1 = false;
|
||||
bool anonymous : 1 = false;
|
||||
bool stargift : 1 = false;
|
||||
bool auction : 1 = false;
|
||||
bool postsSearch : 1 = false;
|
||||
bool giftTransferred : 1 = false;
|
||||
bool giftRefunded : 1 = false;
|
||||
bool giftUpgraded : 1 = false;
|
||||
bool giftUpgradeSeparate : 1 = false;
|
||||
bool giftUpgradeGifted : 1 = false;
|
||||
bool giftResale : 1 = false;
|
||||
bool giftResaleForceTon : 1 = false;
|
||||
bool giftPinned : 1 = false;
|
||||
bool savedToProfile : 1 = false;
|
||||
bool fromGiftsList : 1 = false;
|
||||
bool fromGiftSlug : 1 = false;
|
||||
bool soldOutInfo : 1 = false;
|
||||
bool canUpgradeGift : 1 = false;
|
||||
bool hasGiftComment : 1 = false;
|
||||
bool reaction : 1 = false;
|
||||
bool refunded : 1 = false;
|
||||
bool pending : 1 = false;
|
||||
bool failed : 1 = false;
|
||||
bool in : 1 = false;
|
||||
bool gift : 1 = false;
|
||||
};
|
||||
|
||||
struct CreditsStatusSlice final {
|
||||
using OffsetToken = QString;
|
||||
std::vector<CreditsHistoryEntry> list;
|
||||
std::vector<SubscriptionEntry> subscriptions;
|
||||
CreditsAmount balance;
|
||||
uint64 subscriptionsMissingBalance = 0;
|
||||
bool allLoaded = false;
|
||||
OffsetToken token;
|
||||
OffsetToken tokenSubscriptions;
|
||||
};
|
||||
|
||||
struct CreditsGiveawayOption final {
|
||||
struct Winner final {
|
||||
int users = 0;
|
||||
uint64 perUserStars = 0;
|
||||
bool isDefault = false;
|
||||
};
|
||||
std::vector<Winner> winners;
|
||||
QString storeProduct;
|
||||
QString currency;
|
||||
uint64 amount = 0;
|
||||
uint64 credits = 0;
|
||||
int yearlyBoosts = 0;
|
||||
bool isExtended = false;
|
||||
bool isDefault = false;
|
||||
};
|
||||
|
||||
using CreditsGiveawayOptions = std::vector<CreditsGiveawayOption>;
|
||||
|
||||
} // namespace Data
|
||||
34
Telegram/SourceFiles/data/data_credits_earn.h
Normal file
34
Telegram/SourceFiles/data/data_credits_earn.h
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
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 "core/credits_amount.h"
|
||||
#include "data/data_statistics_chart.h"
|
||||
|
||||
#include <QtCore/QDateTime>
|
||||
|
||||
namespace Data {
|
||||
|
||||
struct CreditsEarnStatistics final {
|
||||
explicit operator bool() const {
|
||||
return usdRate
|
||||
&& currentBalance
|
||||
&& availableBalance
|
||||
&& overallRevenue;
|
||||
}
|
||||
Data::StatisticalGraph revenueGraph;
|
||||
CreditsAmount currentBalance;
|
||||
CreditsAmount availableBalance;
|
||||
CreditsAmount overallRevenue;
|
||||
float64 usdRate = 0.;
|
||||
bool isWithdrawalEnabled = false;
|
||||
QDateTime nextWithdrawalAt;
|
||||
QString buyAdsUrl;
|
||||
};
|
||||
|
||||
} // namespace Data
|
||||
1916
Telegram/SourceFiles/data/data_document.cpp
Normal file
1916
Telegram/SourceFiles/data/data_document.cpp
Normal file
File diff suppressed because it is too large
Load Diff
424
Telegram/SourceFiles/data/data_document.h
Normal file
424
Telegram/SourceFiles/data/data_document.h
Normal file
@@ -0,0 +1,424 @@
|
||||
/*
|
||||
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/flags.h"
|
||||
#include "base/binary_guard.h"
|
||||
#include "data/data_types.h"
|
||||
#include "data/data_cloud_file.h"
|
||||
#include "core/file_location.h"
|
||||
|
||||
class HistoryItem;
|
||||
class PhotoData;
|
||||
enum class ChatRestriction;
|
||||
class mtpFileLoader;
|
||||
|
||||
namespace Images {
|
||||
class Source;
|
||||
} // namespace Images
|
||||
|
||||
namespace Core {
|
||||
enum class NameType : uchar;
|
||||
} // namespace Core
|
||||
|
||||
namespace Storage {
|
||||
namespace Cache {
|
||||
struct Key;
|
||||
} // namespace Cache
|
||||
} // namespace Storage
|
||||
|
||||
namespace Media {
|
||||
struct VideoQuality;
|
||||
} // namespace Media
|
||||
|
||||
namespace Media::Streaming {
|
||||
class Loader;
|
||||
} // namespace Media::Streaming
|
||||
|
||||
namespace Data {
|
||||
class Session;
|
||||
class DocumentMedia;
|
||||
class ReplyPreview;
|
||||
enum class StickersType : uchar;
|
||||
} // namespace Data
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
inline uint64 mediaMix32To64(int32 a, int32 b) {
|
||||
return (uint64(*reinterpret_cast<uint32*>(&a)) << 32)
|
||||
| uint64(*reinterpret_cast<uint32*>(&b));
|
||||
}
|
||||
|
||||
// version field removed from document.
|
||||
inline MediaKey mediaKey(LocationType type, int32 dc, const uint64 &id) {
|
||||
return MediaKey(mediaMix32To64(type, dc), id);
|
||||
}
|
||||
|
||||
struct DocumentAdditionalData {
|
||||
virtual ~DocumentAdditionalData() = default;
|
||||
|
||||
};
|
||||
|
||||
enum class StickerType : uchar {
|
||||
Webp,
|
||||
Tgs,
|
||||
Webm,
|
||||
};
|
||||
|
||||
struct StickerData : public DocumentAdditionalData {
|
||||
[[nodiscard]] Data::FileOrigin setOrigin() const;
|
||||
[[nodiscard]] bool isStatic() const;
|
||||
[[nodiscard]] bool isLottie() const;
|
||||
[[nodiscard]] bool isAnimated() const;
|
||||
[[nodiscard]] bool isWebm() const;
|
||||
|
||||
QString alt;
|
||||
StickerSetIdentifier set;
|
||||
StickerType type = StickerType::Webp;
|
||||
Data::StickersType setType = Data::StickersType();
|
||||
};
|
||||
|
||||
struct SongData : public DocumentAdditionalData {
|
||||
QString title, performer;
|
||||
};
|
||||
|
||||
struct VoiceData : public DocumentAdditionalData {
|
||||
~VoiceData();
|
||||
|
||||
VoiceWaveform waveform;
|
||||
char wavemax = 0;
|
||||
};
|
||||
|
||||
struct VideoData : public DocumentAdditionalData {
|
||||
QString codec;
|
||||
std::vector<not_null<DocumentData*>> qualities;
|
||||
};
|
||||
|
||||
using RoundData = VoiceData;
|
||||
|
||||
namespace Serialize {
|
||||
class Document;
|
||||
} // namespace Serialize;
|
||||
|
||||
class DocumentData final {
|
||||
public:
|
||||
DocumentData(not_null<Data::Session*> owner, DocumentId id);
|
||||
~DocumentData();
|
||||
|
||||
[[nodiscard]] Data::Session &owner() const;
|
||||
[[nodiscard]] Main::Session &session() const;
|
||||
|
||||
void setattributes(
|
||||
const QVector<MTPDocumentAttribute> &attributes);
|
||||
void setVideoQualities(const QVector<MTPDocument> &list);
|
||||
|
||||
void automaticLoadSettingsChanged();
|
||||
void setVideoQualities(std::vector<not_null<DocumentData*>> qualities);
|
||||
[[nodiscard]] int resolveVideoQuality() const;
|
||||
[[nodiscard]] auto resolveQualities(HistoryItem *context) const
|
||||
-> const std::vector<not_null<DocumentData*>> &;
|
||||
[[nodiscard]] not_null<DocumentData*> chooseQuality(
|
||||
HistoryItem *context,
|
||||
Media::VideoQuality request);
|
||||
|
||||
[[nodiscard]] bool loading() const;
|
||||
[[nodiscard]] QString loadingFilePath() const;
|
||||
[[nodiscard]] bool displayLoading() const;
|
||||
void save(
|
||||
Data::FileOrigin origin,
|
||||
const QString &toFile,
|
||||
LoadFromCloudSetting fromCloud = LoadFromCloudOrLocal,
|
||||
bool autoLoading = false);
|
||||
void cancel();
|
||||
[[nodiscard]] bool cancelled() const;
|
||||
void resetCancelled();
|
||||
[[nodiscard]] float64 progress() const;
|
||||
[[nodiscard]] int64 loadOffset() const;
|
||||
[[nodiscard]] bool uploading() const;
|
||||
[[nodiscard]] bool loadedInMediaCache() const;
|
||||
void setLoadedInMediaCache(bool loaded);
|
||||
|
||||
[[nodiscard]] ChatRestriction requiredSendRight() const;
|
||||
|
||||
void setWaitingForAlbum();
|
||||
[[nodiscard]] bool waitingForAlbum() const;
|
||||
|
||||
[[nodiscard]] const Core::FileLocation &location(
|
||||
bool check = false) const;
|
||||
void setLocation(const Core::FileLocation &loc);
|
||||
|
||||
bool saveFromData();
|
||||
bool saveFromDataSilent();
|
||||
[[nodiscard]] QString filepath(bool check = false) const;
|
||||
|
||||
void forceToCache(bool force);
|
||||
[[nodiscard]] bool saveToCache() const;
|
||||
|
||||
[[nodiscard]] Image *getReplyPreview(
|
||||
Data::FileOrigin origin,
|
||||
not_null<PeerData*> context,
|
||||
bool spoiler);
|
||||
[[nodiscard]] Image *getReplyPreview(not_null<HistoryItem*> item);
|
||||
[[nodiscard]] bool replyPreviewLoaded(bool spoiler) const;
|
||||
|
||||
[[nodiscard]] StickerData *sticker() const;
|
||||
[[nodiscard]] Data::FileOrigin stickerSetOrigin() const;
|
||||
[[nodiscard]] Data::FileOrigin stickerOrGifOrigin() const;
|
||||
[[nodiscard]] bool isStickerSetInstalled() const;
|
||||
[[nodiscard]] SongData *song();
|
||||
[[nodiscard]] const SongData *song() const;
|
||||
[[nodiscard]] VoiceData *voice();
|
||||
[[nodiscard]] const VoiceData *voice() const;
|
||||
[[nodiscard]] RoundData *round();
|
||||
[[nodiscard]] const RoundData *round() const;
|
||||
[[nodiscard]] VideoData *video();
|
||||
[[nodiscard]] const VideoData *video() const;
|
||||
|
||||
void forceIsStreamedAnimation();
|
||||
[[nodiscard]] bool isMusicForProfile() const;
|
||||
[[nodiscard]] bool isVoiceMessage() const;
|
||||
[[nodiscard]] bool isVideoMessage() const;
|
||||
[[nodiscard]] bool isSong() const;
|
||||
[[nodiscard]] bool isSongWithCover() const;
|
||||
[[nodiscard]] bool isAudioFile() const;
|
||||
[[nodiscard]] bool isVideoFile() const;
|
||||
[[nodiscard]] bool isSilentVideo() const;
|
||||
[[nodiscard]] bool isAnimation() const;
|
||||
[[nodiscard]] bool isGifv() const;
|
||||
[[nodiscard]] bool isTheme() const;
|
||||
[[nodiscard]] bool isSharedMediaMusic() const;
|
||||
[[nodiscard]] crl::time duration() const;
|
||||
[[nodiscard]] bool hasDuration() const;
|
||||
[[nodiscard]] bool isImage() const;
|
||||
void recountIsImage();
|
||||
[[nodiscard]] bool supportsStreaming() const;
|
||||
void setNotSupportsStreaming();
|
||||
void setDataAndCache(const QByteArray &data);
|
||||
bool checkWallPaperProperties();
|
||||
[[nodiscard]] bool isWallPaper() const;
|
||||
[[nodiscard]] bool isPatternWallPaper() const;
|
||||
[[nodiscard]] bool isPatternWallPaperPNG() const;
|
||||
[[nodiscard]] bool isPatternWallPaperSVG() const;
|
||||
[[nodiscard]] bool isPremiumSticker() const;
|
||||
[[nodiscard]] bool isPremiumEmoji() const;
|
||||
[[nodiscard]] bool emojiUsesTextColor() const;
|
||||
void overrideEmojiUsesTextColor(bool value);
|
||||
|
||||
[[nodiscard]] bool hasThumbnail() const;
|
||||
[[nodiscard]] bool thumbnailLoading() const;
|
||||
[[nodiscard]] bool thumbnailFailed() const;
|
||||
void loadThumbnail(Data::FileOrigin origin);
|
||||
[[nodiscard]] const ImageLocation &thumbnailLocation() const;
|
||||
[[nodiscard]] int thumbnailByteSize() const;
|
||||
|
||||
[[nodiscard]] bool hasVideoThumbnail() const;
|
||||
[[nodiscard]] bool videoThumbnailLoading() const;
|
||||
[[nodiscard]] bool videoThumbnailFailed() const;
|
||||
void loadVideoThumbnail(Data::FileOrigin origin);
|
||||
[[nodiscard]] const ImageLocation &videoThumbnailLocation() const;
|
||||
[[nodiscard]] int videoThumbnailByteSize() const;
|
||||
|
||||
void updateThumbnails(
|
||||
const InlineImageLocation &inlineThumbnail,
|
||||
const ImageWithLocation &thumbnail,
|
||||
const ImageWithLocation &videoThumbnail,
|
||||
bool isPremiumSticker);
|
||||
|
||||
[[nodiscard]] QByteArray inlineThumbnailBytes() const {
|
||||
return _inlineThumbnailBytes;
|
||||
}
|
||||
[[nodiscard]] bool inlineThumbnailIsPath() const {
|
||||
return (_flags & Flag::InlineThumbnailIsPath);
|
||||
}
|
||||
void clearInlineThumbnailBytes() {
|
||||
_inlineThumbnailBytes = QByteArray();
|
||||
}
|
||||
|
||||
[[nodiscard]] Storage::Cache::Key goodThumbnailCacheKey() const;
|
||||
[[nodiscard]] bool goodThumbnailChecked() const;
|
||||
[[nodiscard]] bool goodThumbnailGenerating() const;
|
||||
[[nodiscard]] bool goodThumbnailNoData() const;
|
||||
void setGoodThumbnailGenerating();
|
||||
void setGoodThumbnailDataReady();
|
||||
void setGoodThumbnailChecked(bool hasData);
|
||||
|
||||
[[nodiscard]] std::shared_ptr<Data::DocumentMedia> createMediaView();
|
||||
[[nodiscard]] auto activeMediaView() const
|
||||
-> std::shared_ptr<Data::DocumentMedia>;
|
||||
void setGoodThumbnailPhoto(not_null<PhotoData*> photo);
|
||||
[[nodiscard]] PhotoData *goodThumbnailPhoto() const;
|
||||
|
||||
[[nodiscard]] Storage::Cache::Key bigFileBaseCacheKey() const;
|
||||
|
||||
void setStoryMedia(bool value);
|
||||
[[nodiscard]] bool storyMedia() const;
|
||||
|
||||
void setRemoteLocation(
|
||||
int32 dc,
|
||||
uint64 access,
|
||||
const QByteArray &fileReference);
|
||||
void setContentUrl(const QString &url);
|
||||
void setWebLocation(const WebFileLocation &location);
|
||||
[[nodiscard]] bool hasRemoteLocation() const;
|
||||
[[nodiscard]] bool hasWebLocation() const;
|
||||
[[nodiscard]] bool isNull() const;
|
||||
[[nodiscard]] MTPInputDocument mtpInput() const;
|
||||
[[nodiscard]] QByteArray fileReference() const;
|
||||
void refreshFileReference(const QByteArray &value);
|
||||
|
||||
// When we have some client-side generated document
|
||||
// (for example for displaying an external inline bot result)
|
||||
// and it has downloaded data, we can collect that data from it
|
||||
// to (this) received from the server "same" document.
|
||||
void collectLocalData(not_null<DocumentData*> local);
|
||||
|
||||
[[nodiscard]] QString filename() const;
|
||||
[[nodiscard]] Core::NameType nameType() const;
|
||||
[[nodiscard]] QString mimeString() const;
|
||||
[[nodiscard]] bool hasMimeType(const QString &mime) const;
|
||||
void setMimeString(const QString &mime);
|
||||
|
||||
[[nodiscard]] bool hasAttachedStickers() const;
|
||||
|
||||
[[nodiscard]] MediaKey mediaKey() const;
|
||||
[[nodiscard]] Storage::Cache::Key cacheKey() const;
|
||||
[[nodiscard]] uint8 cacheTag() const;
|
||||
|
||||
[[nodiscard]] bool canBeStreamed(HistoryItem *item) const;
|
||||
[[nodiscard]] auto createStreamingLoader(
|
||||
Data::FileOrigin origin,
|
||||
bool forceRemoteLoader) const
|
||||
-> std::unique_ptr<Media::Streaming::Loader>;
|
||||
[[nodiscard]] bool useStreamingLoader() const;
|
||||
|
||||
void setInappPlaybackFailed();
|
||||
[[nodiscard]] bool inappPlaybackFailed() const;
|
||||
[[nodiscard]] int videoPreloadPrefix() const;
|
||||
[[nodiscard]] StorageFileLocation videoPreloadLocation() const;
|
||||
|
||||
DocumentId id = 0;
|
||||
int64 size = 0;
|
||||
QSize dimensions;
|
||||
int32 date = 0;
|
||||
DocumentType type = FileDocument;
|
||||
FileStatus status = FileReady;
|
||||
|
||||
std::unique_ptr<Data::UploadState> uploadingData;
|
||||
|
||||
private:
|
||||
enum class Flag : ushort {
|
||||
StreamingMaybeYes = 0x0001,
|
||||
StreamingMaybeNo = 0x0002,
|
||||
StreamingPlaybackFailed = 0x0004,
|
||||
ImageType = 0x0008,
|
||||
DownloadCancelled = 0x0010,
|
||||
LoadedInMediaCache = 0x0020,
|
||||
HasAttachedStickers = 0x0040,
|
||||
InlineThumbnailIsPath = 0x0080,
|
||||
ForceToCache = 0x0100,
|
||||
PremiumSticker = 0x0200,
|
||||
PossibleCoverThumbnail = 0x0400,
|
||||
UseTextColor = 0x0800,
|
||||
StoryDocument = 0x1000,
|
||||
SilentVideo = 0x2000,
|
||||
};
|
||||
using Flags = base::flags<Flag>;
|
||||
friend constexpr bool is_flag_type(Flag) { return true; };
|
||||
|
||||
enum class GoodThumbnailFlag : uchar {
|
||||
Checked = 0x01,
|
||||
Generating = 0x02,
|
||||
NoData = 0x03,
|
||||
Mask = 0x03,
|
||||
|
||||
DataReady = 0x04,
|
||||
};
|
||||
using GoodThumbnailState = base::flags<GoodThumbnailFlag>;
|
||||
friend constexpr bool is_flag_type(GoodThumbnailFlag) { return true; };
|
||||
|
||||
static constexpr Flags kStreamingSupportedMask = Flags()
|
||||
| Flag::StreamingMaybeYes
|
||||
| Flag::StreamingMaybeNo;
|
||||
static constexpr Flags kStreamingSupportedUnknown = Flags()
|
||||
| Flag::StreamingMaybeYes
|
||||
| Flag::StreamingMaybeNo;
|
||||
static constexpr Flags kStreamingSupportedMaybeYes = Flags()
|
||||
| Flag::StreamingMaybeYes;
|
||||
static constexpr Flags kStreamingSupportedMaybeNo = Flags()
|
||||
| Flag::StreamingMaybeNo;
|
||||
static constexpr Flags kStreamingSupportedNo = Flags();
|
||||
|
||||
friend class Serialize::Document;
|
||||
|
||||
[[nodiscard]] LocationType locationType() const;
|
||||
void validateLottieSticker();
|
||||
void setMaybeSupportsStreaming(bool supports);
|
||||
void setLoadedInMediaCacheLocation();
|
||||
void setFileName(const QString &remoteFileName);
|
||||
bool enforceNameType(Core::NameType nameType);
|
||||
|
||||
void finishLoad();
|
||||
void handleLoaderUpdates();
|
||||
void destroyLoader();
|
||||
|
||||
bool saveFromDataChecked();
|
||||
|
||||
void refreshPossibleCoverThumbnail();
|
||||
|
||||
const not_null<Data::Session*> _owner;
|
||||
|
||||
int _videoPreloadPrefix = 0;
|
||||
// Two types of location: from MTProto by dc+access or from web by url
|
||||
int32 _dc = 0;
|
||||
uint64 _access = 0;
|
||||
QByteArray _fileReference;
|
||||
QString _url;
|
||||
QString _filename;
|
||||
QString _mimeString;
|
||||
WebFileLocation _urlLocation;
|
||||
|
||||
QByteArray _inlineThumbnailBytes;
|
||||
Data::CloudFile _thumbnail;
|
||||
Data::CloudFile _videoThumbnail;
|
||||
std::unique_ptr<Data::ReplyPreview> _replyPreview;
|
||||
std::weak_ptr<Data::DocumentMedia> _media;
|
||||
PhotoData *_goodThumbnailPhoto = nullptr;
|
||||
crl::time _duration = -1;
|
||||
|
||||
Core::FileLocation _location;
|
||||
std::unique_ptr<DocumentAdditionalData> _additional;
|
||||
mutable Flags _flags = kStreamingSupportedUnknown;
|
||||
GoodThumbnailState _goodThumbnailState = GoodThumbnailState();
|
||||
Core::NameType _nameType = Core::NameType();
|
||||
std::unique_ptr<FileLoader> _loader;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] PhotoData *LookupVideoCover(
|
||||
not_null<DocumentData*> document,
|
||||
HistoryItem *item);
|
||||
|
||||
VoiceWaveform documentWaveformDecode(const QByteArray &encoded5bit);
|
||||
QByteArray documentWaveformEncode5bit(const VoiceWaveform &waveform);
|
||||
|
||||
QString FileNameForSave(
|
||||
not_null<Main::Session*> session,
|
||||
const QString &title,
|
||||
const QString &filter,
|
||||
const QString &prefix,
|
||||
QString name,
|
||||
bool savingAs,
|
||||
const QDir &dir = QDir());
|
||||
|
||||
QString DocumentFileNameForSave(
|
||||
not_null<const DocumentData*> data,
|
||||
bool forceSavingAs = false,
|
||||
const QString &already = QString(),
|
||||
const QDir &dir = QDir());
|
||||
555
Telegram/SourceFiles/data/data_document_media.cpp
Normal file
555
Telegram/SourceFiles/data/data_document_media.cpp
Normal file
@@ -0,0 +1,555 @@
|
||||
/*
|
||||
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/data_document_media.h"
|
||||
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_document_resolver.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_file_origin.h"
|
||||
#include "media/clip/media_clip_reader.h"
|
||||
#include "main/main_session.h"
|
||||
#include "main/main_session_settings.h"
|
||||
#include "lottie/lottie_animation.h"
|
||||
#include "lottie/lottie_frame_generator.h"
|
||||
#include "ffmpeg/ffmpeg_frame_generator.h"
|
||||
#include "history/history_item.h"
|
||||
#include "history/history.h"
|
||||
#include "window/themes/window_theme_preview.h"
|
||||
#include "core/core_settings.h"
|
||||
#include "core/application.h"
|
||||
#include "core/mime_type.h"
|
||||
#include "storage/file_download.h"
|
||||
#include "ui/chat/attach/attach_prepare.h"
|
||||
|
||||
#include <QtCore/QBuffer>
|
||||
#include <QtGui/QImageReader>
|
||||
|
||||
namespace Data {
|
||||
namespace {
|
||||
|
||||
constexpr auto kReadAreaLimit = 12'032 * 9'024;
|
||||
constexpr auto kWallPaperThumbnailLimit = 960;
|
||||
constexpr auto kGoodThumbQuality = 87;
|
||||
|
||||
enum class FileType {
|
||||
Video,
|
||||
VideoSticker,
|
||||
AnimatedSticker,
|
||||
WallPaper,
|
||||
WallPatternPNG,
|
||||
WallPatternSVG,
|
||||
Theme,
|
||||
};
|
||||
|
||||
[[nodiscard]] bool MayHaveGoodThumbnail(not_null<DocumentData*> owner) {
|
||||
return owner->isVideoFile()
|
||||
|| owner->isAnimation()
|
||||
|| owner->isWallPaper()
|
||||
|| owner->isTheme()
|
||||
|| (owner->sticker() && owner->sticker()->isAnimated());
|
||||
}
|
||||
|
||||
[[nodiscard]] QImage PrepareGoodThumbnail(
|
||||
const QString &path,
|
||||
QByteArray data,
|
||||
FileType type) {
|
||||
if (type == FileType::Video || type == FileType::VideoSticker) {
|
||||
auto result = v::get<Ui::PreparedFileInformation::Video>(
|
||||
::Media::Clip::PrepareForSending(path, data).media);
|
||||
if (result.isWebmSticker && type == FileType::Video) {
|
||||
result.thumbnail = Images::Opaque(std::move(result.thumbnail));
|
||||
}
|
||||
return result.thumbnail;
|
||||
} else if (type == FileType::AnimatedSticker) {
|
||||
return Lottie::ReadThumbnail(Lottie::ReadContent(data, path));
|
||||
} else if (type == FileType::Theme) {
|
||||
return Window::Theme::GeneratePreview(data, path);
|
||||
} else if (type == FileType::WallPatternSVG) {
|
||||
return Images::Read({
|
||||
.path = path,
|
||||
.content = std::move(data),
|
||||
.maxSize = QSize(
|
||||
kWallPaperThumbnailLimit,
|
||||
kWallPaperThumbnailLimit),
|
||||
.gzipSvg = true,
|
||||
}).image;
|
||||
}
|
||||
auto buffer = QBuffer(&data);
|
||||
auto file = QFile(path);
|
||||
auto device = data.isEmpty() ? static_cast<QIODevice*>(&file) : &buffer;
|
||||
auto reader = QImageReader(device);
|
||||
const auto size = reader.size();
|
||||
if (!reader.canRead()
|
||||
|| (size.width() * size.height() > kReadAreaLimit)) {
|
||||
return QImage();
|
||||
}
|
||||
auto result = reader.read();
|
||||
if (!result.width() || !result.height()) {
|
||||
return QImage();
|
||||
}
|
||||
return (result.width() > kWallPaperThumbnailLimit
|
||||
|| result.height() > kWallPaperThumbnailLimit)
|
||||
? result.scaled(
|
||||
kWallPaperThumbnailLimit,
|
||||
kWallPaperThumbnailLimit,
|
||||
Qt::KeepAspectRatio,
|
||||
Qt::SmoothTransformation)
|
||||
: result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
VideoPreviewState::VideoPreviewState(DocumentMedia *media)
|
||||
: _media(media)
|
||||
, _usingThumbnail(media ? media->owner()->hasVideoThumbnail() : false) {
|
||||
}
|
||||
|
||||
void VideoPreviewState::automaticLoad(Data::FileOrigin origin) const {
|
||||
Expects(_media != nullptr);
|
||||
|
||||
if (_usingThumbnail) {
|
||||
_media->videoThumbnailWanted(origin);
|
||||
} else {
|
||||
_media->automaticLoad(origin, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
::Media::Clip::ReaderPointer VideoPreviewState::makeAnimation(
|
||||
Fn<void(::Media::Clip::Notification)> callback) const {
|
||||
Expects(_media != nullptr);
|
||||
Expects(loaded());
|
||||
|
||||
return _usingThumbnail
|
||||
? ::Media::Clip::MakeReader(
|
||||
_media->videoThumbnailContent(),
|
||||
std::move(callback))
|
||||
: ::Media::Clip::MakeReader(
|
||||
_media->owner()->location(),
|
||||
_media->bytes(),
|
||||
std::move(callback));
|
||||
}
|
||||
|
||||
bool VideoPreviewState::usingThumbnail() const {
|
||||
return _usingThumbnail;
|
||||
}
|
||||
|
||||
bool VideoPreviewState::loading() const {
|
||||
return _usingThumbnail
|
||||
? _media->owner()->videoThumbnailLoading()
|
||||
: _media
|
||||
? _media->owner()->loading()
|
||||
: false;
|
||||
}
|
||||
|
||||
bool VideoPreviewState::loaded() const {
|
||||
return _usingThumbnail
|
||||
? !_media->videoThumbnailContent().isEmpty()
|
||||
: _media
|
||||
? _media->loaded()
|
||||
: false;
|
||||
}
|
||||
|
||||
DocumentMedia::DocumentMedia(not_null<DocumentData*> owner)
|
||||
: _owner(owner) {
|
||||
}
|
||||
|
||||
// NB! Right now DocumentMedia can outlive Main::Session!
|
||||
// In DocumentData::collectLocalData a shared_ptr is sent on_main.
|
||||
// In case this is a problem the ~Gif code should be rewritten.
|
||||
DocumentMedia::~DocumentMedia() = default;
|
||||
|
||||
not_null<DocumentData*> DocumentMedia::owner() const {
|
||||
return _owner;
|
||||
}
|
||||
|
||||
void DocumentMedia::goodThumbnailWanted() {
|
||||
_flags |= Flag::GoodThumbnailWanted;
|
||||
}
|
||||
|
||||
Image *DocumentMedia::goodThumbnail() const {
|
||||
Expects((_flags & Flag::GoodThumbnailWanted) != 0);
|
||||
|
||||
if (!_goodThumbnail) {
|
||||
ReadOrGenerateThumbnail(_owner);
|
||||
}
|
||||
return _goodThumbnail.get();
|
||||
}
|
||||
|
||||
void DocumentMedia::setGoodThumbnail(QImage thumbnail) {
|
||||
if (!(_flags & Flag::GoodThumbnailWanted)) {
|
||||
return;
|
||||
}
|
||||
_goodThumbnail = std::make_unique<Image>(std::move(thumbnail));
|
||||
_owner->session().notifyDownloaderTaskFinished();
|
||||
}
|
||||
|
||||
Image *DocumentMedia::thumbnailInline() const {
|
||||
if (!_inlineThumbnail && !_owner->inlineThumbnailIsPath()) {
|
||||
const auto bytes = _owner->inlineThumbnailBytes();
|
||||
if (!bytes.isEmpty()) {
|
||||
auto image = Images::FromInlineBytes(bytes);
|
||||
if (image.isNull()) {
|
||||
_owner->clearInlineThumbnailBytes();
|
||||
} else {
|
||||
_inlineThumbnail = std::make_unique<Image>(std::move(image));
|
||||
}
|
||||
}
|
||||
}
|
||||
return _inlineThumbnail.get();
|
||||
}
|
||||
|
||||
const QPainterPath &DocumentMedia::thumbnailPath() const {
|
||||
if (_pathThumbnail.isEmpty() && _owner->inlineThumbnailIsPath()) {
|
||||
const auto bytes = _owner->inlineThumbnailBytes();
|
||||
if (!bytes.isEmpty()) {
|
||||
_pathThumbnail = Images::PathFromInlineBytes(bytes);
|
||||
if (_pathThumbnail.isEmpty()) {
|
||||
_owner->clearInlineThumbnailBytes();
|
||||
}
|
||||
}
|
||||
}
|
||||
return _pathThumbnail;
|
||||
}
|
||||
|
||||
Image *DocumentMedia::thumbnail() const {
|
||||
return _thumbnail.get();
|
||||
}
|
||||
|
||||
void DocumentMedia::thumbnailWanted(Data::FileOrigin origin) {
|
||||
if (!_thumbnail) {
|
||||
_owner->loadThumbnail(origin);
|
||||
}
|
||||
}
|
||||
|
||||
QSize DocumentMedia::thumbnailSize() const {
|
||||
if (const auto image = _thumbnail.get()) {
|
||||
return image->size();
|
||||
}
|
||||
const auto &location = _owner->thumbnailLocation();
|
||||
return { location.width(), location.height() };
|
||||
}
|
||||
|
||||
void DocumentMedia::setThumbnail(QImage thumbnail) {
|
||||
_thumbnail = std::make_unique<Image>(std::move(thumbnail));
|
||||
_owner->session().notifyDownloaderTaskFinished();
|
||||
}
|
||||
|
||||
QByteArray DocumentMedia::videoThumbnailContent() const {
|
||||
return _videoThumbnailBytes;
|
||||
}
|
||||
|
||||
QSize DocumentMedia::videoThumbnailSize() const {
|
||||
const auto &location = _owner->videoThumbnailLocation();
|
||||
return { location.width(), location.height() };
|
||||
}
|
||||
|
||||
void DocumentMedia::videoThumbnailWanted(Data::FileOrigin origin) {
|
||||
if (_videoThumbnailBytes.isEmpty()) {
|
||||
_owner->loadVideoThumbnail(origin);
|
||||
}
|
||||
}
|
||||
|
||||
void DocumentMedia::setVideoThumbnail(QByteArray content) {
|
||||
_videoThumbnailBytes = std::move(content);
|
||||
_videoThumbnailBytes.detach();
|
||||
}
|
||||
|
||||
void DocumentMedia::checkStickerLarge() {
|
||||
if (_sticker) {
|
||||
return;
|
||||
}
|
||||
const auto data = _owner->sticker();
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
automaticLoad(_owner->stickerSetOrigin(), nullptr);
|
||||
if (data->isAnimated() || !loaded()) {
|
||||
return;
|
||||
}
|
||||
if (_bytes.isEmpty()) {
|
||||
const auto &loc = _owner->location(true);
|
||||
if (loc.accessEnable()) {
|
||||
_sticker = std::make_unique<Image>(loc.name());
|
||||
loc.accessDisable();
|
||||
}
|
||||
} else {
|
||||
_sticker = std::make_unique<Image>(_bytes);
|
||||
}
|
||||
}
|
||||
|
||||
void DocumentMedia::automaticLoad(
|
||||
Data::FileOrigin origin,
|
||||
const HistoryItem *item) {
|
||||
if (_owner->status != FileReady || loaded() || _owner->cancelled()) {
|
||||
return;
|
||||
} else if (!item && !_owner->sticker() && !_owner->isAnimation()) {
|
||||
return;
|
||||
}
|
||||
const auto toCache = _owner->saveToCache();
|
||||
if (!toCache && !Core::App().canSaveFileWithoutAskingForPath()) {
|
||||
// We need a filename, but we're supposed to ask user for it.
|
||||
// No automatic download in this case.
|
||||
return;
|
||||
}
|
||||
const auto indata = _owner->filename();
|
||||
const auto filename = toCache
|
||||
? QString()
|
||||
: DocumentFileNameForSave(_owner);
|
||||
const auto shouldLoadFromCloud = (indata.isEmpty()
|
||||
|| Core::DetectNameType(indata) != Core::NameType::Executable)
|
||||
&& (item
|
||||
? Data::AutoDownload::Should(
|
||||
_owner->session().settings().autoDownload(),
|
||||
item->history()->peer,
|
||||
_owner)
|
||||
: Data::AutoDownload::Should(
|
||||
_owner->session().settings().autoDownload(),
|
||||
_owner));
|
||||
const auto loadFromCloud = shouldLoadFromCloud
|
||||
? LoadFromCloudOrLocal
|
||||
: LoadFromLocalOnly;
|
||||
_owner->save(
|
||||
origin,
|
||||
filename,
|
||||
loadFromCloud,
|
||||
true);
|
||||
}
|
||||
|
||||
void DocumentMedia::collectLocalData(not_null<DocumentMedia*> local) {
|
||||
if (const auto image = local->_goodThumbnail.get()) {
|
||||
_goodThumbnail = std::make_unique<Image>(image->original());
|
||||
}
|
||||
if (const auto image = local->_inlineThumbnail.get()) {
|
||||
_inlineThumbnail = std::make_unique<Image>(image->original());
|
||||
}
|
||||
if (const auto image = local->_thumbnail.get()) {
|
||||
_thumbnail = std::make_unique<Image>(image->original());
|
||||
}
|
||||
if (const auto image = local->_sticker.get()) {
|
||||
_sticker = std::make_unique<Image>(image->original());
|
||||
}
|
||||
_bytes = local->_bytes;
|
||||
_videoThumbnailBytes = local->_videoThumbnailBytes;
|
||||
_flags = local->_flags;
|
||||
}
|
||||
|
||||
void DocumentMedia::setBytes(const QByteArray &bytes) {
|
||||
if (!bytes.isEmpty()) {
|
||||
_bytes = bytes;
|
||||
}
|
||||
}
|
||||
|
||||
QByteArray DocumentMedia::bytes() const {
|
||||
return _bytes;
|
||||
}
|
||||
|
||||
bool DocumentMedia::loaded(bool check) const {
|
||||
return !_bytes.isEmpty() || !_owner->filepath(check).isEmpty();
|
||||
}
|
||||
|
||||
float64 DocumentMedia::progress() const {
|
||||
return (_owner->uploading() || _owner->loading())
|
||||
? _owner->progress()
|
||||
: (loaded() ? 1. : 0.);
|
||||
}
|
||||
|
||||
bool DocumentMedia::canBePlayed(HistoryItem *item) const {
|
||||
return !_owner->inappPlaybackFailed()
|
||||
&& _owner->useStreamingLoader()
|
||||
&& (loaded() || _owner->canBeStreamed(item));
|
||||
}
|
||||
|
||||
bool DocumentMedia::thumbnailEnoughForSticker() const {
|
||||
const auto &location = owner()->thumbnailLocation();
|
||||
const auto size = _thumbnail
|
||||
? QSize(_thumbnail->width(), _thumbnail->height())
|
||||
: location.valid()
|
||||
? QSize(location.width(), location.height())
|
||||
: QSize();
|
||||
return (size.width() >= 128) || (size.height() >= 128);
|
||||
}
|
||||
|
||||
void DocumentMedia::checkStickerSmall() {
|
||||
const auto data = _owner->sticker();
|
||||
if ((data && data->isAnimated()) || thumbnailEnoughForSticker()) {
|
||||
_owner->loadThumbnail(_owner->stickerSetOrigin());
|
||||
if (data && data->isAnimated()) {
|
||||
automaticLoad(_owner->stickerSetOrigin(), nullptr);
|
||||
}
|
||||
} else {
|
||||
checkStickerLarge();
|
||||
}
|
||||
}
|
||||
|
||||
Image *DocumentMedia::getStickerLarge() {
|
||||
checkStickerLarge();
|
||||
return _sticker.get();
|
||||
}
|
||||
|
||||
Image *DocumentMedia::getStickerSmall() {
|
||||
const auto data = _owner->sticker();
|
||||
if ((data && data->isAnimated()) || thumbnailEnoughForSticker()) {
|
||||
return thumbnail();
|
||||
}
|
||||
return _sticker.get();
|
||||
}
|
||||
|
||||
void DocumentMedia::checkStickerLarge(not_null<FileLoader*> loader) {
|
||||
if (_sticker || !_owner->sticker()) {
|
||||
return;
|
||||
}
|
||||
if (auto image = loader->imageData(); !image.isNull()) {
|
||||
_sticker = std::make_unique<Image>(std::move(image));
|
||||
}
|
||||
}
|
||||
|
||||
void DocumentMedia::GenerateGoodThumbnail(
|
||||
not_null<DocumentData*> document,
|
||||
QByteArray data) {
|
||||
const auto type = document->isPatternWallPaperSVG()
|
||||
? FileType::WallPatternSVG
|
||||
: document->isPatternWallPaperPNG()
|
||||
? FileType::WallPatternPNG
|
||||
: document->isWallPaper()
|
||||
? FileType::WallPaper
|
||||
: document->isTheme()
|
||||
? FileType::Theme
|
||||
: !document->sticker()
|
||||
? FileType::Video
|
||||
: document->sticker()->isLottie()
|
||||
? FileType::AnimatedSticker
|
||||
: FileType::VideoSticker;
|
||||
auto location = document->location().isEmpty()
|
||||
? nullptr
|
||||
: std::make_unique<Core::FileLocation>(document->location());
|
||||
if (data.isEmpty() && !location) {
|
||||
document->setGoodThumbnailChecked(false);
|
||||
return;
|
||||
}
|
||||
const auto guard = base::make_weak(&document->session());
|
||||
crl::async([=, location = std::move(location)] {
|
||||
const auto filepath = (location && location->accessEnable())
|
||||
? location->name()
|
||||
: QString();
|
||||
auto result = PrepareGoodThumbnail(filepath, data, type);
|
||||
auto bytes = QByteArray();
|
||||
if (!result.isNull()) {
|
||||
auto buffer = QBuffer(&bytes);
|
||||
const auto format = (type == FileType::AnimatedSticker
|
||||
|| type == FileType::VideoSticker)
|
||||
? "WEBP"
|
||||
: (type == FileType::WallPatternPNG
|
||||
|| type == FileType::WallPatternSVG)
|
||||
? "PNG"
|
||||
: "JPG";
|
||||
result.save(&buffer, format, kGoodThumbQuality);
|
||||
}
|
||||
if (!filepath.isEmpty()) {
|
||||
location->accessDisable();
|
||||
}
|
||||
const auto cache = bytes.isEmpty() ? QByteArray("(failed)") : bytes;
|
||||
crl::on_main(guard, [=] {
|
||||
document->setGoodThumbnailChecked(true);
|
||||
if (const auto active = document->activeMediaView()) {
|
||||
active->setGoodThumbnail(result);
|
||||
}
|
||||
document->owner().cache().put(
|
||||
document->goodThumbnailCacheKey(),
|
||||
Storage::Cache::Database::TaggedValue{
|
||||
base::duplicate(cache),
|
||||
kImageCacheTag });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void DocumentMedia::CheckGoodThumbnail(not_null<DocumentData*> document) {
|
||||
if (!document->goodThumbnailChecked()) {
|
||||
ReadOrGenerateThumbnail(document);
|
||||
}
|
||||
}
|
||||
|
||||
void DocumentMedia::ReadOrGenerateThumbnail(
|
||||
not_null<DocumentData*> document) {
|
||||
if (document->goodThumbnailGenerating()
|
||||
|| document->goodThumbnailNoData()
|
||||
|| !MayHaveGoodThumbnail(document)) {
|
||||
return;
|
||||
}
|
||||
document->setGoodThumbnailGenerating();
|
||||
|
||||
const auto guard = base::make_weak(&document->session());
|
||||
const auto active = document->activeMediaView();
|
||||
const auto got = [=](QByteArray value) {
|
||||
if (value.isEmpty()) {
|
||||
const auto bytes = active ? active->bytes() : QByteArray();
|
||||
crl::on_main(guard, [=] {
|
||||
GenerateGoodThumbnail(document, bytes);
|
||||
});
|
||||
} else if (active) {
|
||||
crl::async([=] {
|
||||
auto image = Images::Read({ .content = value }).image;
|
||||
crl::on_main(guard, [=, image = std::move(image)]() mutable {
|
||||
document->setGoodThumbnailChecked(true);
|
||||
if (const auto active = document->activeMediaView()) {
|
||||
active->setGoodThumbnail(std::move(image));
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
crl::on_main(guard, [=] {
|
||||
document->setGoodThumbnailChecked(true);
|
||||
});
|
||||
}
|
||||
};
|
||||
document->owner().cache().get(document->goodThumbnailCacheKey(), got);
|
||||
}
|
||||
|
||||
auto DocumentIconFrameGenerator(not_null<DocumentMedia*> media)
|
||||
-> FnMut<std::unique_ptr<Ui::FrameGenerator>()> {
|
||||
if (!media->loaded()) {
|
||||
return nullptr;
|
||||
}
|
||||
using Type = StickerType;
|
||||
const auto document = media->owner();
|
||||
const auto content = media->bytes();
|
||||
const auto fromFile = content.isEmpty();
|
||||
const auto type = document->sticker()
|
||||
? document->sticker()->type
|
||||
: (document->isVideoFile() || document->isAnimation())
|
||||
? Type::Webm
|
||||
: Type::Webp;
|
||||
const auto &location = media->owner()->location(true);
|
||||
if (fromFile && !location.accessEnable()) {
|
||||
return nullptr;
|
||||
}
|
||||
return [=]() -> std::unique_ptr<Ui::FrameGenerator> {
|
||||
const auto bytes = Lottie::ReadContent(content, location.name());
|
||||
if (fromFile) {
|
||||
location.accessDisable();
|
||||
}
|
||||
if (bytes.isEmpty()) {
|
||||
return nullptr;
|
||||
}
|
||||
switch (type) {
|
||||
case Type::Tgs:
|
||||
return std::make_unique<Lottie::FrameGenerator>(bytes);
|
||||
case Type::Webm:
|
||||
return std::make_unique<FFmpeg::FrameGenerator>(bytes);
|
||||
case Type::Webp:
|
||||
return std::make_unique<Ui::ImageFrameGenerator>(bytes);
|
||||
}
|
||||
Unexpected("Document type in DocumentIconFrameGenerator.");
|
||||
};
|
||||
}
|
||||
|
||||
auto DocumentIconFrameGenerator(const std::shared_ptr<DocumentMedia> &media)
|
||||
-> FnMut<std::unique_ptr<Ui::FrameGenerator>()> {
|
||||
return DocumentIconFrameGenerator(media.get());
|
||||
}
|
||||
|
||||
} // namespace Data
|
||||
126
Telegram/SourceFiles/data/data_document_media.h
Normal file
126
Telegram/SourceFiles/data/data_document_media.h
Normal file
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
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/flags.h"
|
||||
|
||||
class Image;
|
||||
class FileLoader;
|
||||
|
||||
namespace Ui {
|
||||
class FrameGenerator;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Media {
|
||||
namespace Clip {
|
||||
enum class Notification;
|
||||
class ReaderPointer;
|
||||
} // namespace Clip
|
||||
} // namespace Media
|
||||
|
||||
namespace Data {
|
||||
|
||||
class DocumentMedia;
|
||||
|
||||
class VideoPreviewState final {
|
||||
public:
|
||||
explicit VideoPreviewState(DocumentMedia *media);
|
||||
|
||||
void automaticLoad(Data::FileOrigin origin) const;
|
||||
[[nodiscard]] ::Media::Clip::ReaderPointer makeAnimation(
|
||||
Fn<void(::Media::Clip::Notification)> callback) const;
|
||||
[[nodiscard]] bool usingThumbnail() const;
|
||||
[[nodiscard]] bool loading() const;
|
||||
[[nodiscard]] bool loaded() const;
|
||||
|
||||
private:
|
||||
DocumentMedia *_media = nullptr;
|
||||
bool _usingThumbnail = false;
|
||||
|
||||
};
|
||||
|
||||
class DocumentMedia final {
|
||||
public:
|
||||
explicit DocumentMedia(not_null<DocumentData*> owner);
|
||||
~DocumentMedia();
|
||||
|
||||
[[nodiscard]] not_null<DocumentData*> owner() const;
|
||||
|
||||
void goodThumbnailWanted();
|
||||
[[nodiscard]] Image *goodThumbnail() const;
|
||||
void setGoodThumbnail(QImage thumbnail);
|
||||
|
||||
[[nodiscard]] Image *thumbnailInline() const;
|
||||
[[nodiscard]] const QPainterPath &thumbnailPath() const;
|
||||
|
||||
[[nodiscard]] Image *thumbnail() const;
|
||||
[[nodiscard]] QSize thumbnailSize() const;
|
||||
void thumbnailWanted(Data::FileOrigin origin);
|
||||
void setThumbnail(QImage thumbnail);
|
||||
|
||||
[[nodiscard]] QByteArray videoThumbnailContent() const;
|
||||
[[nodiscard]] QSize videoThumbnailSize() const;
|
||||
void videoThumbnailWanted(Data::FileOrigin origin);
|
||||
void setVideoThumbnail(QByteArray content);
|
||||
|
||||
void checkStickerLarge();
|
||||
void checkStickerSmall();
|
||||
[[nodiscard]] Image *getStickerSmall();
|
||||
[[nodiscard]] Image *getStickerLarge();
|
||||
void checkStickerLarge(not_null<FileLoader*> loader);
|
||||
|
||||
void setBytes(const QByteArray &bytes);
|
||||
[[nodiscard]] QByteArray bytes() const;
|
||||
[[nodiscard]] bool loaded(bool check = false) const;
|
||||
[[nodiscard]] float64 progress() const;
|
||||
[[nodiscard]] bool canBePlayed(HistoryItem *item) const;
|
||||
|
||||
void automaticLoad(Data::FileOrigin origin, const HistoryItem *item);
|
||||
|
||||
void collectLocalData(not_null<DocumentMedia*> local);
|
||||
|
||||
// For DocumentData.
|
||||
static void CheckGoodThumbnail(not_null<DocumentData*> document);
|
||||
|
||||
private:
|
||||
enum class Flag : uchar {
|
||||
GoodThumbnailWanted = 0x01,
|
||||
};
|
||||
inline constexpr bool is_flag_type(Flag) { return true; };
|
||||
using Flags = base::flags<Flag>;
|
||||
|
||||
static void ReadOrGenerateThumbnail(not_null<DocumentData*> document);
|
||||
static void GenerateGoodThumbnail(
|
||||
not_null<DocumentData*> document,
|
||||
QByteArray data);
|
||||
|
||||
[[nodiscard]] bool thumbnailEnoughForSticker() const;
|
||||
|
||||
// NB! Right now DocumentMedia can outlive Main::Session!
|
||||
// In DocumentData::collectLocalData a shared_ptr is sent on_main.
|
||||
// In case this is a problem the ~Gif code should be rewritten.
|
||||
const not_null<DocumentData*> _owner;
|
||||
std::unique_ptr<Image> _goodThumbnail;
|
||||
mutable std::unique_ptr<Image> _inlineThumbnail;
|
||||
mutable QPainterPath _pathThumbnail;
|
||||
std::unique_ptr<Image> _thumbnail;
|
||||
std::unique_ptr<Image> _sticker;
|
||||
QByteArray _bytes;
|
||||
QByteArray _videoThumbnailBytes;
|
||||
Flags _flags;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] auto DocumentIconFrameGenerator(not_null<DocumentMedia*> media)
|
||||
-> FnMut<std::unique_ptr<Ui::FrameGenerator>()>;
|
||||
|
||||
[[nodiscard]] auto DocumentIconFrameGenerator(
|
||||
const std::shared_ptr<DocumentMedia> &media)
|
||||
-> FnMut<std::unique_ptr<Ui::FrameGenerator>()>;
|
||||
|
||||
} // namespace Data
|
||||
275
Telegram/SourceFiles/data/data_document_resolver.cpp
Normal file
275
Telegram/SourceFiles/data/data_document_resolver.cpp
Normal file
@@ -0,0 +1,275 @@
|
||||
/*
|
||||
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/data_document_resolver.h"
|
||||
|
||||
#include "base/options.h"
|
||||
#include "base/platform/base_platform_info.h"
|
||||
#include "boxes/abstract_box.h" // Ui::show().
|
||||
#include "chat_helpers/ttl_media_layer_widget.h"
|
||||
#include "core/application.h"
|
||||
#include "core/core_settings.h"
|
||||
#include "core/mime_type.h"
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_document_media.h"
|
||||
#include "data/data_file_click_handler.h"
|
||||
#include "data/data_session.h"
|
||||
#include "history/history.h"
|
||||
#include "history/history_item.h"
|
||||
#include "history/view/media/history_view_gif.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "media/player/media_player_instance.h"
|
||||
#include "platform/platform_file_utilities.h"
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
#include "ui/chat/chat_theme.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/wrap/slide_wrap.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "styles/style_layers.h"
|
||||
|
||||
#include <QtCore/QBuffer>
|
||||
#include <QtCore/QMimeType>
|
||||
#include <QtCore/QMimeDatabase>
|
||||
|
||||
namespace Data {
|
||||
namespace {
|
||||
|
||||
base::options::toggle OptionExternalVideoPlayer({
|
||||
.id = kOptionExternalVideoPlayer,
|
||||
.name = "External video player",
|
||||
.description = "Use system video player instead of the internal one. "
|
||||
"This disabes video playback in messages.",
|
||||
});
|
||||
|
||||
void ConfirmDontWarnBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
rpl::producer<TextWithEntities> &&text,
|
||||
rpl::producer<QString> &&check,
|
||||
rpl::producer<QString> &&confirm,
|
||||
Fn<void(bool)> callback) {
|
||||
auto checkbox = object_ptr<Ui::Checkbox>(
|
||||
box.get(),
|
||||
std::move(check),
|
||||
false,
|
||||
st::defaultBoxCheckbox);
|
||||
const auto weak = base::make_weak(checkbox.data());
|
||||
auto confirmed = crl::guard(weak, [=, callback = std::move(callback)] {
|
||||
const auto checked = weak->checked();
|
||||
box->closeBox();
|
||||
callback(checked);
|
||||
});
|
||||
Ui::ConfirmBox(box, {
|
||||
.text = std::move(text),
|
||||
.confirmed = std::move(confirmed),
|
||||
.confirmText = std::move(confirm),
|
||||
});
|
||||
auto padding = st::boxPadding;
|
||||
padding.setTop(padding.bottom());
|
||||
box->addRow(std::move(checkbox), std::move(padding));
|
||||
box->addRow(object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(
|
||||
box,
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box,
|
||||
tr::lng_launch_dont_ask_settings(),
|
||||
st::boxLabel)
|
||||
))->toggleOn(weak->checkedValue());
|
||||
}
|
||||
|
||||
void LaunchWithWarning(
|
||||
// not_null<Window::Controller*> controller,
|
||||
const QString &name,
|
||||
HistoryItem *item) {
|
||||
const auto nameType = Core::DetectNameType(name);
|
||||
const auto isIpReveal = (nameType != Core::NameType::Executable)
|
||||
&& Core::IsIpRevealingPath(name);
|
||||
const auto extension = Core::FileExtension(name).toLower();
|
||||
|
||||
auto &app = Core::App();
|
||||
auto &settings = app.settings();
|
||||
const auto warn = [&] {
|
||||
if (item && item->history()->peer->isVerified()) {
|
||||
return false;
|
||||
}
|
||||
return (isIpReveal && settings.ipRevealWarning())
|
||||
|| ((nameType == Core::NameType::Executable
|
||||
|| nameType == Core::NameType::Unknown)
|
||||
&& !settings.noWarningExtensions().contains(extension));
|
||||
}();
|
||||
if (extension.isEmpty()) {
|
||||
// If you launch a file without extension, like "test", in case
|
||||
// there is an executable file with the same name in this folder,
|
||||
// like "test.bat", the executable file will be launched.
|
||||
//
|
||||
// Now we always force an Open With dialog box for such files.
|
||||
//
|
||||
// Let's force it for all platforms for files without extension.
|
||||
crl::on_main([=] {
|
||||
Platform::File::UnsafeShowOpenWith(name);
|
||||
});
|
||||
return;
|
||||
} else if (!warn) {
|
||||
File::Launch(name);
|
||||
return;
|
||||
}
|
||||
const auto callback = [=, &app, &settings](bool checked) {
|
||||
if (checked) {
|
||||
if (isIpReveal) {
|
||||
settings.setIpRevealWarning(false);
|
||||
} else {
|
||||
auto copy = settings.noWarningExtensions();
|
||||
copy.emplace(extension);
|
||||
settings.setNoWarningExtensions(std::move(copy));
|
||||
}
|
||||
app.saveSettingsDelayed();
|
||||
}
|
||||
File::Launch(name);
|
||||
};
|
||||
auto text = isIpReveal
|
||||
? tr::lng_launch_svg_warning(tr::marked)
|
||||
: ((nameType == Core::NameType::Executable)
|
||||
? tr::lng_launch_exe_warning
|
||||
: tr::lng_launch_other_warning)(
|
||||
lt_extension,
|
||||
rpl::single(tr::bold('.' + extension)),
|
||||
tr::marked);
|
||||
auto check = (isIpReveal
|
||||
? tr::lng_launch_exe_dont_ask
|
||||
: tr::lng_launch_dont_ask)();
|
||||
auto confirm = ((nameType == Core::NameType::Executable)
|
||||
? tr::lng_launch_exe_sure
|
||||
: tr::lng_launch_other_sure)();
|
||||
Ui::show(Box(
|
||||
ConfirmDontWarnBox,
|
||||
std::move(text),
|
||||
std::move(check),
|
||||
std::move(confirm),
|
||||
callback));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const char kOptionExternalVideoPlayer[] = "external-video-player";
|
||||
|
||||
base::binary_guard ReadBackgroundImageAsync(
|
||||
not_null<Data::DocumentMedia*> media,
|
||||
FnMut<QImage(QImage)> postprocess,
|
||||
FnMut<void(QImage&&)> done) {
|
||||
auto result = base::binary_guard();
|
||||
const auto gzipSvg = media->owner()->isPatternWallPaperSVG();
|
||||
crl::async([
|
||||
gzipSvg,
|
||||
bytes = media->bytes(),
|
||||
path = media->owner()->filepath(),
|
||||
postprocess = std::move(postprocess),
|
||||
guard = result.make_guard(),
|
||||
callback = std::move(done)
|
||||
]() mutable {
|
||||
auto image = Ui::ReadBackgroundImage(path, bytes, gzipSvg).image;
|
||||
if (image.isNull()) {
|
||||
image = QImage(1, 1, QImage::Format_ARGB32_Premultiplied);
|
||||
image.fill(Qt::black);
|
||||
}
|
||||
if (postprocess) {
|
||||
image = postprocess(std::move(image));
|
||||
}
|
||||
crl::on_main(std::move(guard), [
|
||||
image = std::move(image),
|
||||
callback = std::move(callback)
|
||||
]() mutable {
|
||||
callback(std::move(image));
|
||||
});
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
void ResolveDocument(
|
||||
Window::SessionController *controller,
|
||||
not_null<DocumentData*> document,
|
||||
HistoryItem *item,
|
||||
MsgId topicRootId,
|
||||
PeerId monoforumPeerId) {
|
||||
if (document->isNull()) {
|
||||
return;
|
||||
}
|
||||
const auto msgId = item ? item->fullId() : FullMsgId();
|
||||
|
||||
const auto showDocument = [&] {
|
||||
if (OptionExternalVideoPlayer.value()
|
||||
&& document->isVideoFile()
|
||||
&& !document->filepath().isEmpty()) {
|
||||
File::Launch(document->location(false).fname);
|
||||
} else if (controller) {
|
||||
controller->openDocument(
|
||||
document,
|
||||
true,
|
||||
{ msgId, topicRootId, monoforumPeerId });
|
||||
}
|
||||
};
|
||||
|
||||
const auto media = document->createMediaView();
|
||||
const auto openImageInApp = [&] {
|
||||
if (document->size >= Images::kReadBytesLimit) {
|
||||
return false;
|
||||
}
|
||||
const auto &location = document->location(true);
|
||||
const auto mime = u"image/"_q;
|
||||
if (!location.isEmpty() && location.accessEnable()) {
|
||||
const auto guard = gsl::finally([&] {
|
||||
location.accessDisable();
|
||||
});
|
||||
const auto path = location.name();
|
||||
if (Core::MimeTypeForFile(QFileInfo(path)).name().startsWith(mime)
|
||||
&& QImageReader(path).canRead()) {
|
||||
showDocument();
|
||||
return true;
|
||||
}
|
||||
} else if (document->mimeString().startsWith(mime)
|
||||
&& !media->bytes().isEmpty()) {
|
||||
auto bytes = media->bytes();
|
||||
auto buffer = QBuffer(&bytes);
|
||||
if (QImageReader(&buffer).canRead()) {
|
||||
showDocument();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const auto &location = document->location(true);
|
||||
if (document->isTheme() && media->loaded(true)) {
|
||||
showDocument();
|
||||
location.accessDisable();
|
||||
} else if (media->canBePlayed(item)) {
|
||||
if (document->isAudioFile()
|
||||
|| document->isVoiceMessage()
|
||||
|| document->isVideoMessage()) {
|
||||
::Media::Player::instance()->playPause({ document, msgId });
|
||||
if (controller
|
||||
&& item
|
||||
&& item->media()
|
||||
&& item->media()->ttlSeconds()) {
|
||||
ChatHelpers::ShowTTLMediaLayerWidget(controller, item);
|
||||
}
|
||||
} else {
|
||||
showDocument();
|
||||
}
|
||||
} else {
|
||||
document->saveFromDataSilent();
|
||||
if (!openImageInApp()) {
|
||||
if (!document->filepath(true).isEmpty()) {
|
||||
LaunchWithWarning(location.name(), item);
|
||||
} else if (document->status == FileReady
|
||||
|| document->status == FileDownloadFailed) {
|
||||
DocumentSaveClickHandler::Save(
|
||||
item ? item->fullId() : Data::FileOrigin(),
|
||||
document);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Data
|
||||
37
Telegram/SourceFiles/data/data_document_resolver.h
Normal file
37
Telegram/SourceFiles/data/data_document_resolver.h
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
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/binary_guard.h"
|
||||
|
||||
class DocumentData;
|
||||
class HistoryItem;
|
||||
|
||||
namespace Window {
|
||||
class SessionController;
|
||||
} // namespace Window
|
||||
|
||||
namespace Data {
|
||||
|
||||
class DocumentMedia;
|
||||
|
||||
extern const char kOptionExternalVideoPlayer[];
|
||||
|
||||
base::binary_guard ReadBackgroundImageAsync(
|
||||
not_null<Data::DocumentMedia*> media,
|
||||
FnMut<QImage(QImage)> postprocess,
|
||||
FnMut<void(QImage&&)> done);
|
||||
|
||||
void ResolveDocument(
|
||||
Window::SessionController *controller,
|
||||
not_null<DocumentData*> document,
|
||||
HistoryItem *item,
|
||||
MsgId topicRootId,
|
||||
PeerId monoforumPeerId);
|
||||
|
||||
} // namespace Data
|
||||
1209
Telegram/SourceFiles/data/data_download_manager.cpp
Normal file
1209
Telegram/SourceFiles/data/data_download_manager.cpp
Normal file
File diff suppressed because it is too large
Load Diff
211
Telegram/SourceFiles/data/data_download_manager.h
Normal file
211
Telegram/SourceFiles/data/data_download_manager.h
Normal file
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
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"
|
||||
|
||||
namespace Ui {
|
||||
struct DownloadBarProgress;
|
||||
struct DownloadBarContent;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace Data {
|
||||
|
||||
// Used in serialization!
|
||||
enum class DownloadType {
|
||||
Document,
|
||||
Photo,
|
||||
};
|
||||
|
||||
// unixtime * 1000.
|
||||
using DownloadDate = int64;
|
||||
|
||||
[[nodiscard]] inline TimeId DateFromDownloadDate(DownloadDate date) {
|
||||
return date / 1000;
|
||||
}
|
||||
|
||||
struct DownloadId {
|
||||
uint64 objectId = 0;
|
||||
DownloadType type = DownloadType::Document;
|
||||
};
|
||||
|
||||
struct DownloadProgress {
|
||||
int64 ready = 0;
|
||||
int64 total = 0;
|
||||
};
|
||||
inline constexpr bool operator==(
|
||||
const DownloadProgress &a,
|
||||
const DownloadProgress &b) {
|
||||
return (a.ready == b.ready) && (a.total == b.total);
|
||||
}
|
||||
|
||||
struct DownloadObject {
|
||||
not_null<HistoryItem*> item;
|
||||
DocumentData *document = nullptr;
|
||||
PhotoData *photo = nullptr;
|
||||
};
|
||||
|
||||
struct DownloadedId {
|
||||
DownloadId download;
|
||||
DownloadDate started = 0;
|
||||
QString path;
|
||||
int64 size = 0;
|
||||
FullMsgId itemId;
|
||||
uint64 peerAccessHash = 0;
|
||||
|
||||
std::unique_ptr<DownloadObject> object;
|
||||
};
|
||||
|
||||
struct DownloadingId {
|
||||
DownloadObject object;
|
||||
DownloadDate started = 0;
|
||||
QString path;
|
||||
int64 ready = 0;
|
||||
int64 total = 0;
|
||||
bool hiddenByView = false;
|
||||
bool done = false;
|
||||
};
|
||||
|
||||
class DownloadManager final {
|
||||
public:
|
||||
DownloadManager();
|
||||
~DownloadManager();
|
||||
|
||||
[[nodiscard]] bool empty() const;
|
||||
|
||||
void trackSession(not_null<Main::Session*> session);
|
||||
void itemVisibilitiesUpdated(not_null<Main::Session*> session);
|
||||
|
||||
[[nodiscard]] DownloadDate computeNextStartDate();
|
||||
|
||||
void addLoading(DownloadObject object);
|
||||
void addLoaded(
|
||||
DownloadObject object,
|
||||
const QString &path,
|
||||
DownloadDate started);
|
||||
|
||||
void clearIfFinished();
|
||||
void deleteFiles(const std::vector<GlobalMsgId> &ids);
|
||||
void deleteAll();
|
||||
[[nodiscard]] bool loadedHasNonCloudFile() const;
|
||||
|
||||
[[nodiscard]] auto loadingList() const
|
||||
-> ranges::any_view<const DownloadingId*, ranges::category::input>;
|
||||
[[nodiscard]] DownloadProgress loadingProgress() const;
|
||||
[[nodiscard]] rpl::producer<> loadingListChanges() const;
|
||||
[[nodiscard]] auto loadingProgressValue() const
|
||||
-> rpl::producer<DownloadProgress>;
|
||||
|
||||
[[nodiscard]] bool loadingInProgress(
|
||||
Main::Session *onlyInSession = nullptr) const;
|
||||
void loadingStopWithConfirmation(
|
||||
Fn<void()> callback,
|
||||
Main::Session *onlyInSession = nullptr);
|
||||
|
||||
[[nodiscard]] auto loadedList()
|
||||
-> ranges::any_view<const DownloadedId*, ranges::category::input>;
|
||||
[[nodiscard]] auto loadedAdded() const
|
||||
-> rpl::producer<not_null<const DownloadedId*>>;
|
||||
[[nodiscard]] auto loadedRemoved() const
|
||||
-> rpl::producer<not_null<const HistoryItem*>>;
|
||||
[[nodiscard]] rpl::producer<> loadedResolveDone() const;
|
||||
|
||||
private:
|
||||
struct DeleteFilesDescriptor;
|
||||
struct SessionData {
|
||||
std::vector<DownloadedId> downloaded;
|
||||
std::vector<DownloadingId> downloading;
|
||||
int resolveNeeded = 0;
|
||||
int resolveSentRequests = 0;
|
||||
int resolveSentTotal = 0;
|
||||
rpl::lifetime lifetime;
|
||||
};
|
||||
|
||||
void check(not_null<const HistoryItem*> item);
|
||||
void check(not_null<DocumentData*> document);
|
||||
void check(
|
||||
SessionData &data,
|
||||
std::vector<DownloadingId>::iterator i);
|
||||
void changed(not_null<const HistoryItem*> item);
|
||||
void removed(not_null<const HistoryItem*> item);
|
||||
void detach(DownloadedId &id);
|
||||
void untrack(not_null<Main::Session*> session);
|
||||
void remove(
|
||||
SessionData &data,
|
||||
std::vector<DownloadingId>::iterator i);
|
||||
void cancel(
|
||||
SessionData &data,
|
||||
std::vector<DownloadingId>::iterator i);
|
||||
void clearLoading();
|
||||
|
||||
[[nodiscard]] SessionData &sessionData(not_null<Main::Session*> session);
|
||||
[[nodiscard]] const SessionData &sessionData(
|
||||
not_null<Main::Session*> session) const;
|
||||
[[nodiscard]] SessionData &sessionData(
|
||||
not_null<const HistoryItem*> item);
|
||||
[[nodiscard]] SessionData &sessionData(not_null<DocumentData*> document);
|
||||
|
||||
void resolve(not_null<Main::Session*> session, SessionData &data);
|
||||
void resolveRequestsFinished(
|
||||
not_null<Main::Session*> session,
|
||||
SessionData &data);
|
||||
void checkFullResolveDone();
|
||||
|
||||
[[nodiscard]] not_null<HistoryItem*> regenerateItem(
|
||||
const DownloadObject &previous);
|
||||
[[nodiscard]] not_null<HistoryItem*> generateFakeItem(
|
||||
not_null<DocumentData*> document);
|
||||
[[nodiscard]] not_null<HistoryItem*> generateItem(
|
||||
HistoryItem *previousItem,
|
||||
DocumentData *document,
|
||||
PhotoData *photo);
|
||||
void generateEntry(not_null<Main::Session*> session, DownloadedId &id);
|
||||
|
||||
[[nodiscard]] HistoryItem *lookupLoadingItem(
|
||||
Main::Session *onlyInSession) const;
|
||||
void loadingStop(Main::Session *onlyInSession);
|
||||
|
||||
void finishFilesDelete(DeleteFilesDescriptor &&descriptor);
|
||||
void writePostponed(not_null<Main::Session*> session);
|
||||
[[nodiscard]] Fn<std::optional<QByteArray>()> serializator(
|
||||
not_null<Main::Session*> session) const;
|
||||
[[nodiscard]] std::vector<DownloadedId> deserialize(
|
||||
not_null<Main::Session*> session) const;
|
||||
|
||||
base::flat_map<not_null<Main::Session*>, SessionData> _sessions;
|
||||
base::flat_set<not_null<const HistoryItem*>> _loading;
|
||||
base::flat_set<not_null<DocumentData*>> _loadingDocuments;
|
||||
base::flat_set<not_null<const HistoryItem*>> _loadingDone;
|
||||
base::flat_set<not_null<const HistoryItem*>> _loaded;
|
||||
base::flat_set<not_null<HistoryItem*>> _generated;
|
||||
base::flat_set<not_null<DocumentData*>> _generatedDocuments;
|
||||
|
||||
TimeId _lastStartedBase = 0;
|
||||
int _lastStartedAdded = 0;
|
||||
|
||||
rpl::event_stream<> _loadingListChanges;
|
||||
rpl::variable<DownloadProgress> _loadingProgress;
|
||||
|
||||
rpl::event_stream<not_null<const DownloadedId*>> _loadedAdded;
|
||||
rpl::event_stream<not_null<const HistoryItem*>> _loadedRemoved;
|
||||
rpl::variable<bool> _loadedResolveDone;
|
||||
|
||||
base::Timer _clearLoadingTimer;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] auto MakeDownloadBarProgress()
|
||||
-> rpl::producer<Ui::DownloadBarProgress>;
|
||||
|
||||
[[nodiscard]] rpl::producer<Ui::DownloadBarContent> MakeDownloadBarContent();
|
||||
|
||||
} // namespace Data
|
||||
185
Telegram/SourceFiles/data/data_drafts.cpp
Normal file
185
Telegram/SourceFiles/data/data_drafts.cpp
Normal file
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
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/data_drafts.h"
|
||||
|
||||
#include "api/api_text_entities.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "chat_helpers/message_field.h"
|
||||
#include "history/history.h"
|
||||
#include "history/history_widget.h"
|
||||
#include "history/history_item_components.h"
|
||||
#include "main/main_session.h"
|
||||
#include "data/data_changes.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_web_page.h"
|
||||
#include "mainwidget.h"
|
||||
#include "storage/localstorage.h"
|
||||
|
||||
namespace Data {
|
||||
|
||||
WebPageDraft WebPageDraft::FromItem(not_null<HistoryItem*> item) {
|
||||
const auto previewMedia = item->media();
|
||||
const auto previewPage = previewMedia
|
||||
? previewMedia->webpage()
|
||||
: nullptr;
|
||||
using PageFlag = MediaWebPageFlag;
|
||||
const auto previewFlags = previewMedia
|
||||
? previewMedia->webpageFlags()
|
||||
: PageFlag();
|
||||
return {
|
||||
.id = previewPage ? previewPage->id : 0,
|
||||
.url = previewPage ? previewPage->url : QString(),
|
||||
.forceLargeMedia = !!(previewFlags & PageFlag::ForceLargeMedia),
|
||||
.forceSmallMedia = !!(previewFlags & PageFlag::ForceSmallMedia),
|
||||
.invert = item->invertMedia(),
|
||||
.manual = !!(previewFlags & PageFlag::Manual),
|
||||
.removed = !previewPage,
|
||||
};
|
||||
}
|
||||
|
||||
Draft::Draft(
|
||||
const TextWithTags &textWithTags,
|
||||
FullReplyTo reply,
|
||||
SuggestOptions suggest,
|
||||
const MessageCursor &cursor,
|
||||
WebPageDraft webpage,
|
||||
mtpRequestId saveRequestId)
|
||||
: textWithTags(textWithTags)
|
||||
, reply(std::move(reply))
|
||||
, suggest(suggest)
|
||||
, cursor(cursor)
|
||||
, webpage(webpage)
|
||||
, saveRequestId(saveRequestId) {
|
||||
}
|
||||
|
||||
Draft::Draft(
|
||||
not_null<const Ui::InputField*> field,
|
||||
FullReplyTo reply,
|
||||
SuggestOptions suggest,
|
||||
WebPageDraft webpage,
|
||||
mtpRequestId saveRequestId)
|
||||
: textWithTags(field->getTextWithTags())
|
||||
, reply(std::move(reply))
|
||||
, suggest(suggest)
|
||||
, cursor(field)
|
||||
, webpage(webpage) {
|
||||
}
|
||||
|
||||
void ApplyPeerCloudDraft(
|
||||
not_null<Main::Session*> session,
|
||||
PeerId peerId,
|
||||
MsgId topicRootId,
|
||||
PeerId monoforumPeerId,
|
||||
const MTPDdraftMessage &draft) {
|
||||
const auto history = session->data().history(peerId);
|
||||
const auto date = draft.vdate().v;
|
||||
if (history->skipCloudDraftUpdate(topicRootId, monoforumPeerId, date)) {
|
||||
return;
|
||||
}
|
||||
const auto textWithTags = TextWithTags{
|
||||
qs(draft.vmessage()),
|
||||
TextUtilities::ConvertEntitiesToTextTags(
|
||||
Api::EntitiesFromMTP(
|
||||
session,
|
||||
draft.ventities().value_or_empty()))
|
||||
};
|
||||
auto replyTo = draft.vreply_to()
|
||||
? ReplyToFromMTP(history, *draft.vreply_to())
|
||||
: FullReplyTo();
|
||||
replyTo.topicRootId = topicRootId;
|
||||
replyTo.monoforumPeerId = monoforumPeerId;
|
||||
auto webpage = WebPageDraft{
|
||||
.invert = draft.is_invert_media(),
|
||||
.removed = draft.is_no_webpage(),
|
||||
};
|
||||
if (const auto media = draft.vmedia()) {
|
||||
media->match([&](const MTPDmessageMediaWebPage &data) {
|
||||
const auto parsed = session->data().processWebpage(
|
||||
data.vwebpage());
|
||||
if (!parsed->failed) {
|
||||
webpage.forceLargeMedia = data.is_force_large_media();
|
||||
webpage.forceSmallMedia = data.is_force_small_media();
|
||||
webpage.manual = data.is_manual();
|
||||
webpage.url = parsed->url;
|
||||
webpage.id = parsed->id;
|
||||
}
|
||||
}, [](const auto &) {});
|
||||
}
|
||||
auto suggest = SuggestOptions();
|
||||
if (!history->suggestDraftAllowed()) {
|
||||
// Don't apply suggest options in unsupported chats.
|
||||
} else if (const auto suggested = draft.vsuggested_post()) {
|
||||
const auto &data = suggested->data();
|
||||
suggest.exists = 1;
|
||||
suggest.date = data.vschedule_date().value_or_empty();
|
||||
const auto price = CreditsAmountFromTL(data.vprice());
|
||||
suggest.priceWhole = price.whole();
|
||||
suggest.priceNano = price.nano();
|
||||
suggest.ton = price.ton() ? 1 : 0;
|
||||
}
|
||||
auto cloudDraft = std::make_unique<Draft>(
|
||||
textWithTags,
|
||||
replyTo,
|
||||
suggest,
|
||||
MessageCursor(Ui::kQFixedMax, Ui::kQFixedMax, Ui::kQFixedMax),
|
||||
std::move(webpage));
|
||||
cloudDraft->date = date;
|
||||
|
||||
history->setCloudDraft(std::move(cloudDraft));
|
||||
history->applyCloudDraft(topicRootId, monoforumPeerId);
|
||||
}
|
||||
|
||||
void ClearPeerCloudDraft(
|
||||
not_null<Main::Session*> session,
|
||||
PeerId peerId,
|
||||
MsgId topicRootId,
|
||||
PeerId monoforumPeerId,
|
||||
TimeId date) {
|
||||
const auto history = session->data().history(peerId);
|
||||
if (history->skipCloudDraftUpdate(topicRootId, monoforumPeerId, date)) {
|
||||
return;
|
||||
}
|
||||
|
||||
history->clearCloudDraft(topicRootId, monoforumPeerId);
|
||||
history->applyCloudDraft(topicRootId, monoforumPeerId);
|
||||
}
|
||||
|
||||
void SetChatLinkDraft(not_null<PeerData*> peer, TextWithEntities draft) {
|
||||
static const auto kInlineStart = QRegularExpression("^@[a-zA-Z0-9_]");
|
||||
if (kInlineStart.match(draft.text).hasMatch()) {
|
||||
draft = TextWithEntities().append(' ').append(std::move(draft));
|
||||
}
|
||||
|
||||
const auto textWithTags = TextWithTags{
|
||||
draft.text,
|
||||
TextUtilities::ConvertEntitiesToTextTags(draft.entities)
|
||||
};
|
||||
const auto cursor = MessageCursor{
|
||||
int(textWithTags.text.size()),
|
||||
int(textWithTags.text.size()),
|
||||
Ui::kQFixedMax
|
||||
};
|
||||
const auto history = peer->owner().history(peer->id);
|
||||
const auto topicRootId = MsgId();
|
||||
const auto monoforumPeerId = PeerId();
|
||||
history->setLocalDraft(std::make_unique<Draft>(
|
||||
textWithTags,
|
||||
FullReplyTo{
|
||||
.topicRootId = topicRootId,
|
||||
.monoforumPeerId = monoforumPeerId,
|
||||
},
|
||||
SuggestOptions(),
|
||||
cursor,
|
||||
WebPageDraft()));
|
||||
history->clearLocalEditDraft(topicRootId, monoforumPeerId);
|
||||
history->session().changes().entryUpdated(
|
||||
history,
|
||||
EntryUpdate::Flag::LocalDraftSet);
|
||||
}
|
||||
|
||||
} // namespace Data
|
||||
266
Telegram/SourceFiles/data/data_drafts.h
Normal file
266
Telegram/SourceFiles/data/data_drafts.h
Normal file
@@ -0,0 +1,266 @@
|
||||
/*
|
||||
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_msg_id.h"
|
||||
|
||||
namespace Ui {
|
||||
class InputField;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace Data {
|
||||
|
||||
void ApplyPeerCloudDraft(
|
||||
not_null<Main::Session*> session,
|
||||
PeerId peerId,
|
||||
MsgId topicRootId,
|
||||
PeerId monoforumPeerId,
|
||||
const MTPDdraftMessage &draft);
|
||||
void ClearPeerCloudDraft(
|
||||
not_null<Main::Session*> session,
|
||||
PeerId peerId,
|
||||
MsgId topicRootId,
|
||||
PeerId monoforumPeerId,
|
||||
TimeId date);
|
||||
|
||||
struct WebPageDraft {
|
||||
[[nodiscard]] static WebPageDraft FromItem(not_null<HistoryItem*> item);
|
||||
|
||||
WebPageId id = 0;
|
||||
QString url;
|
||||
bool forceLargeMedia : 1 = false;
|
||||
bool forceSmallMedia : 1 = false;
|
||||
bool invert : 1 = false;
|
||||
bool manual : 1 = false;
|
||||
bool removed : 1 = false;
|
||||
|
||||
friend inline bool operator==(const WebPageDraft&, const WebPageDraft&)
|
||||
= default;
|
||||
};
|
||||
|
||||
struct Draft {
|
||||
Draft() = default;
|
||||
Draft(
|
||||
const TextWithTags &textWithTags,
|
||||
FullReplyTo reply,
|
||||
SuggestOptions suggest,
|
||||
const MessageCursor &cursor,
|
||||
WebPageDraft webpage,
|
||||
mtpRequestId saveRequestId = 0);
|
||||
Draft(
|
||||
not_null<const Ui::InputField*> field,
|
||||
FullReplyTo reply,
|
||||
SuggestOptions suggest,
|
||||
WebPageDraft webpage,
|
||||
mtpRequestId saveRequestId = 0);
|
||||
|
||||
TimeId date = 0;
|
||||
TextWithTags textWithTags;
|
||||
FullReplyTo reply; // reply.messageId.msg is editMsgId for edit draft.
|
||||
SuggestOptions suggest;
|
||||
MessageCursor cursor;
|
||||
WebPageDraft webpage;
|
||||
mtpRequestId saveRequestId = 0;
|
||||
};
|
||||
|
||||
class DraftKey {
|
||||
public:
|
||||
[[nodiscard]] static constexpr DraftKey None() {
|
||||
return 0;
|
||||
}
|
||||
[[nodiscard]] static constexpr DraftKey Local(
|
||||
MsgId topicRootId,
|
||||
PeerId monoforumPeerId) {
|
||||
return Invalid(topicRootId, monoforumPeerId)
|
||||
? None()
|
||||
: (topicRootId
|
||||
? topicRootId.bare
|
||||
: monoforumPeerId
|
||||
? (monoforumPeerId.value + kMonoforumDraftBit)
|
||||
: kLocalDraftIndex);
|
||||
}
|
||||
[[nodiscard]] static constexpr DraftKey LocalEdit(
|
||||
MsgId topicRootId,
|
||||
PeerId monoforumPeerId) {
|
||||
return Invalid(topicRootId, monoforumPeerId)
|
||||
? None()
|
||||
: (kEditDraftShift
|
||||
+ (topicRootId
|
||||
? topicRootId.bare
|
||||
: monoforumPeerId
|
||||
? (monoforumPeerId.value + kMonoforumDraftBit)
|
||||
: kLocalDraftIndex));
|
||||
}
|
||||
[[nodiscard]] static constexpr DraftKey Cloud(
|
||||
MsgId topicRootId,
|
||||
PeerId monoforumPeerId) {
|
||||
return Invalid(topicRootId, monoforumPeerId)
|
||||
? None()
|
||||
: topicRootId
|
||||
? (kCloudDraftShift + topicRootId.bare)
|
||||
: monoforumPeerId
|
||||
? (kCloudDraftShift + monoforumPeerId.value + kMonoforumDraftBit)
|
||||
: kCloudDraftIndex;
|
||||
}
|
||||
[[nodiscard]] static constexpr DraftKey Scheduled() {
|
||||
return kScheduledDraftIndex;
|
||||
}
|
||||
[[nodiscard]] static constexpr DraftKey ScheduledEdit() {
|
||||
return kScheduledDraftIndex + kEditDraftShift;
|
||||
}
|
||||
[[nodiscard]] static constexpr DraftKey Shortcut(
|
||||
BusinessShortcutId shortcutId) {
|
||||
return (shortcutId < 0 || shortcutId >= ServerMaxMsgId)
|
||||
? None()
|
||||
: (kShortcutDraftShift + shortcutId);
|
||||
}
|
||||
[[nodiscard]] static constexpr DraftKey ShortcutEdit(
|
||||
BusinessShortcutId shortcutId) {
|
||||
return (shortcutId < 0 || shortcutId >= ServerMaxMsgId)
|
||||
? None()
|
||||
: (kShortcutDraftShift + kEditDraftShift + shortcutId);
|
||||
}
|
||||
|
||||
[[nodiscard]] static constexpr DraftKey FromSerialized(qint64 value) {
|
||||
return value;
|
||||
}
|
||||
[[nodiscard]] constexpr qint64 serialize() const {
|
||||
return _value;
|
||||
}
|
||||
|
||||
[[nodiscard]] static constexpr DraftKey FromSerializedOld(int32 value) {
|
||||
return !value
|
||||
? None()
|
||||
: (value == kLocalDraftIndex + kEditDraftShiftOld)
|
||||
? LocalEdit(MsgId(), PeerId())
|
||||
: (value == kScheduledDraftIndex + kEditDraftShiftOld)
|
||||
? ScheduledEdit()
|
||||
: (value > 0 && value < 0x4000'0000)
|
||||
? Local(MsgId(value), PeerId())
|
||||
: (value > kEditDraftShiftOld
|
||||
&& value < kEditDraftShiftOld + 0x4000'000)
|
||||
? LocalEdit(MsgId(int64(value - kEditDraftShiftOld)), PeerId())
|
||||
: None();
|
||||
}
|
||||
[[nodiscard]] constexpr bool isLocal() const {
|
||||
return (_value == kLocalDraftIndex)
|
||||
|| (_value > 0
|
||||
&& (_value & kMonoforumDraftMask) < ServerMaxMsgId.bare);
|
||||
}
|
||||
[[nodiscard]] constexpr bool isCloud() const {
|
||||
return (_value == kCloudDraftIndex)
|
||||
|| ((_value & kMonoforumDraftMask) > kCloudDraftShift
|
||||
&& ((_value & kMonoforumDraftMask)
|
||||
< kCloudDraftShift + ServerMaxMsgId.bare));
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr MsgId topicRootId() const {
|
||||
const auto max = ServerMaxMsgId.bare;
|
||||
if (_value & kMonoforumDraftBit) {
|
||||
return 0;
|
||||
} else if ((_value > kCloudDraftShift)
|
||||
&& (_value < kCloudDraftShift + max)) {
|
||||
return (_value - kCloudDraftShift);
|
||||
} else if ((_value > kEditDraftShift)
|
||||
&& (_value < kEditDraftShift + max)) {
|
||||
return (_value - kEditDraftShift);
|
||||
} else if (_value > 0 && _value < max) {
|
||||
return _value;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
[[nodiscard]] constexpr PeerId monoforumPeerId() const {
|
||||
const auto max = ServerMaxMsgId.bare;
|
||||
const auto value = _value & kMonoforumDraftMask;
|
||||
if (!(_value & kMonoforumDraftBit)) {
|
||||
return 0;
|
||||
} else if ((value > kCloudDraftShift)
|
||||
&& (value < kCloudDraftShift + max)) {
|
||||
return PeerId(UserId(value - kCloudDraftShift));
|
||||
} else if ((value > kEditDraftShift)
|
||||
&& (value < kEditDraftShift + max)) {
|
||||
return PeerId(UserId(value - kEditDraftShift));
|
||||
} else if (value > 0 && value < max) {
|
||||
return PeerId(UserId(value));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
friend inline constexpr auto operator<=>(DraftKey, DraftKey) = default;
|
||||
friend inline constexpr bool operator==(DraftKey, DraftKey) = default;
|
||||
|
||||
inline explicit operator bool() const {
|
||||
return _value != 0;
|
||||
}
|
||||
|
||||
private:
|
||||
constexpr DraftKey(int64 value) : _value(value) {
|
||||
}
|
||||
|
||||
[[nodiscard]] static constexpr bool Invalid(
|
||||
MsgId topicRootId,
|
||||
PeerId monoforumPeerId) {
|
||||
return (topicRootId < 0)
|
||||
|| (topicRootId >= ServerMaxMsgId)
|
||||
|| !peerIsUser(monoforumPeerId)
|
||||
|| (monoforumPeerId.value >= ServerMaxMsgId);
|
||||
}
|
||||
|
||||
static constexpr auto kLocalDraftIndex = -1;
|
||||
static constexpr auto kCloudDraftIndex = -2;
|
||||
static constexpr auto kScheduledDraftIndex = -3;
|
||||
static constexpr auto kMonoforumDraftBit = (int64(1) << 60);
|
||||
static constexpr auto kMonoforumDraftMask = (kMonoforumDraftBit - 1);
|
||||
static constexpr auto kEditDraftShift = ServerMaxMsgId.bare;
|
||||
static constexpr auto kCloudDraftShift = 2 * ServerMaxMsgId.bare;
|
||||
static constexpr auto kShortcutDraftShift = 3 * ServerMaxMsgId.bare;
|
||||
static constexpr auto kEditDraftShiftOld = 0x3FFF'FFFF;
|
||||
|
||||
int64 _value = 0;
|
||||
|
||||
};
|
||||
|
||||
using HistoryDrafts = base::flat_map<DraftKey, std::unique_ptr<Draft>>;
|
||||
|
||||
[[nodiscard]] inline bool DraftStringIsEmpty(const QString &text) {
|
||||
for (const auto &ch : text) {
|
||||
if (!ch.isSpace()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline bool DraftIsNull(const Draft *draft) {
|
||||
return !draft
|
||||
|| (!draft->reply.messageId
|
||||
&& !draft->suggest.exists
|
||||
&& DraftStringIsEmpty(draft->textWithTags.text));
|
||||
}
|
||||
|
||||
[[nodiscard]] inline bool DraftsAreEqual(const Draft *a, const Draft *b) {
|
||||
const auto aIsNull = DraftIsNull(a);
|
||||
const auto bIsNull = DraftIsNull(b);
|
||||
if (aIsNull) {
|
||||
return bIsNull;
|
||||
} else if (bIsNull) {
|
||||
return false;
|
||||
}
|
||||
return (a->textWithTags == b->textWithTags)
|
||||
&& (a->reply == b->reply)
|
||||
&& (a->suggest == b->suggest)
|
||||
&& (a->webpage == b->webpage);
|
||||
}
|
||||
|
||||
void SetChatLinkDraft(not_null<PeerData*> peer, TextWithEntities draft);
|
||||
|
||||
} // namespace Data
|
||||
567
Telegram/SourceFiles/data/data_emoji_statuses.cpp
Normal file
567
Telegram/SourceFiles/data/data_emoji_statuses.cpp
Normal file
@@ -0,0 +1,567 @@
|
||||
/*
|
||||
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/data_emoji_statuses.h"
|
||||
|
||||
#include "main/main_session.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_user.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_star_gift.h"
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_wall_paper.h"
|
||||
#include "data/stickers/data_stickers.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "base/timer_rpl.h"
|
||||
#include "base/call_delayed.h"
|
||||
#include "apiwrap.h"
|
||||
#include "ui/controls/tabbed_search.h"
|
||||
|
||||
namespace Data {
|
||||
namespace {
|
||||
|
||||
constexpr auto kRefreshDefaultListEach = 60 * 60 * crl::time(1000);
|
||||
constexpr auto kRecentRequestTimeout = 10 * crl::time(1000);
|
||||
constexpr auto kMaxTimeout = 6 * 60 * 60 * crl::time(1000);
|
||||
|
||||
[[nodiscard]] EmojiStatusCollectible ParseEmojiStatusCollectible(
|
||||
const MTPDemojiStatusCollectible &data) {
|
||||
return EmojiStatusCollectible{
|
||||
.id = data.vcollectible_id().v,
|
||||
.documentId = data.vdocument_id().v,
|
||||
.title = qs(data.vtitle()),
|
||||
.slug = qs(data.vslug()),
|
||||
.patternDocumentId = data.vpattern_document_id().v,
|
||||
.centerColor = Ui::ColorFromSerialized(data.vcenter_color()),
|
||||
.edgeColor = Ui::ColorFromSerialized(data.vedge_color()),
|
||||
.patternColor = Ui::ColorFromSerialized(data.vpattern_color()),
|
||||
.textColor = Ui::ColorFromSerialized(data.vtext_color()),
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
EmojiStatuses::EmojiStatuses(not_null<Session*> owner)
|
||||
: _owner(owner)
|
||||
, _clearingTimer([=] { processClearing(); }) {
|
||||
refreshDefault();
|
||||
refreshColored();
|
||||
|
||||
base::timer_each(
|
||||
kRefreshDefaultListEach
|
||||
) | rpl::on_next([=] {
|
||||
refreshDefault();
|
||||
refreshChannelDefault();
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
EmojiStatuses::~EmojiStatuses() = default;
|
||||
|
||||
Main::Session &EmojiStatuses::session() const {
|
||||
return _owner->session();
|
||||
}
|
||||
|
||||
void EmojiStatuses::refreshRecent() {
|
||||
requestRecent();
|
||||
}
|
||||
|
||||
void EmojiStatuses::refreshDefault() {
|
||||
requestDefault();
|
||||
}
|
||||
|
||||
void EmojiStatuses::refreshColored() {
|
||||
requestColored();
|
||||
}
|
||||
|
||||
void EmojiStatuses::refreshChannelDefault() {
|
||||
requestChannelDefault();
|
||||
}
|
||||
|
||||
void EmojiStatuses::refreshChannelColored() {
|
||||
requestChannelColored();
|
||||
}
|
||||
|
||||
void EmojiStatuses::refreshCollectibles() {
|
||||
requestCollectibles();
|
||||
}
|
||||
|
||||
void EmojiStatuses::refreshRecentDelayed() {
|
||||
if (_recentRequestId || _recentRequestScheduled) {
|
||||
return;
|
||||
}
|
||||
_recentRequestScheduled = true;
|
||||
base::call_delayed(kRecentRequestTimeout, &_owner->session(), [=] {
|
||||
if (_recentRequestScheduled) {
|
||||
requestRecent();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const std::vector<EmojiStatusId> &EmojiStatuses::list(Type type) const {
|
||||
switch (type) {
|
||||
case Type::Recent: return _recent;
|
||||
case Type::Default: return _default;
|
||||
case Type::Colored: return _colored;
|
||||
case Type::ChannelDefault: return _channelDefault;
|
||||
case Type::ChannelColored: return _channelColored;
|
||||
case Type::Collectibles: return _collectibles;
|
||||
}
|
||||
Unexpected("Type in EmojiStatuses::list.");
|
||||
}
|
||||
|
||||
EmojiStatusData EmojiStatuses::parse(const MTPEmojiStatus &status) {
|
||||
return status.match([](const MTPDemojiStatus &data) {
|
||||
return EmojiStatusData{
|
||||
.id = { .documentId = data.vdocument_id().v },
|
||||
.until = data.vuntil().value_or_empty(),
|
||||
};
|
||||
}, [&](const MTPDemojiStatusCollectible &data) {
|
||||
const auto collectibleId = data.vcollectible_id().v;
|
||||
auto &collectible = _collectibleData[collectibleId];
|
||||
if (!collectible) {
|
||||
collectible = std::make_shared<EmojiStatusCollectible>(
|
||||
ParseEmojiStatusCollectible(data));
|
||||
}
|
||||
return EmojiStatusData{
|
||||
.id = { .collectible = collectible },
|
||||
.until = data.vuntil().value_or_empty(),
|
||||
};
|
||||
}, [](const MTPDinputEmojiStatusCollectible &) {
|
||||
return EmojiStatusData();
|
||||
}, [](const MTPDemojiStatusEmpty &) {
|
||||
return EmojiStatusData();
|
||||
});
|
||||
}
|
||||
|
||||
rpl::producer<> EmojiStatuses::recentUpdates() const {
|
||||
return _recentUpdated.events();
|
||||
}
|
||||
|
||||
rpl::producer<> EmojiStatuses::defaultUpdates() const {
|
||||
return _defaultUpdated.events();
|
||||
}
|
||||
|
||||
rpl::producer<> EmojiStatuses::channelDefaultUpdates() const {
|
||||
return _channelDefaultUpdated.events();
|
||||
}
|
||||
|
||||
rpl::producer<> EmojiStatuses::collectiblesUpdates() const {
|
||||
return _collectiblesUpdated.events();
|
||||
}
|
||||
|
||||
void EmojiStatuses::registerAutomaticClear(
|
||||
not_null<PeerData*> peer,
|
||||
TimeId until) {
|
||||
if (!until) {
|
||||
_clearing.remove(peer);
|
||||
if (_clearing.empty()) {
|
||||
_clearingTimer.cancel();
|
||||
}
|
||||
} else if (auto &already = _clearing[peer]; already != until) {
|
||||
already = until;
|
||||
const auto i = ranges::min_element(_clearing, {}, [](auto &&pair) {
|
||||
return pair.second;
|
||||
});
|
||||
if (i->first == peer) {
|
||||
const auto now = base::unixtime::now();
|
||||
if (now < until) {
|
||||
processClearingIn(until - now);
|
||||
} else {
|
||||
processClearing();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto EmojiStatuses::emojiGroupsValue() const -> rpl::producer<Groups> {
|
||||
const_cast<EmojiStatuses*>(this)->requestEmojiGroups();
|
||||
return _emojiGroups.data.value();
|
||||
}
|
||||
|
||||
auto EmojiStatuses::statusGroupsValue() const -> rpl::producer<Groups> {
|
||||
const_cast<EmojiStatuses*>(this)->requestStatusGroups();
|
||||
return _statusGroups.data.value();
|
||||
}
|
||||
|
||||
auto EmojiStatuses::stickerGroupsValue() const -> rpl::producer<Groups> {
|
||||
const_cast<EmojiStatuses*>(this)->requestStickerGroups();
|
||||
return _stickerGroups.data.value();
|
||||
}
|
||||
|
||||
auto EmojiStatuses::profilePhotoGroupsValue() const
|
||||
-> rpl::producer<Groups> {
|
||||
const_cast<EmojiStatuses*>(this)->requestProfilePhotoGroups();
|
||||
return _profilePhotoGroups.data.value();
|
||||
}
|
||||
|
||||
void EmojiStatuses::requestEmojiGroups() {
|
||||
requestGroups(
|
||||
&_emojiGroups,
|
||||
MTPmessages_GetEmojiGroups(MTP_int(_emojiGroups.hash)));
|
||||
|
||||
}
|
||||
|
||||
void EmojiStatuses::requestStatusGroups() {
|
||||
requestGroups(
|
||||
&_statusGroups,
|
||||
MTPmessages_GetEmojiStatusGroups(MTP_int(_statusGroups.hash)));
|
||||
}
|
||||
|
||||
void EmojiStatuses::requestStickerGroups() {
|
||||
requestGroups(
|
||||
&_stickerGroups,
|
||||
MTPmessages_GetEmojiStickerGroups(MTP_int(_stickerGroups.hash)));
|
||||
}
|
||||
|
||||
void EmojiStatuses::requestProfilePhotoGroups() {
|
||||
requestGroups(
|
||||
&_profilePhotoGroups,
|
||||
MTPmessages_GetEmojiProfilePhotoGroups(
|
||||
MTP_int(_profilePhotoGroups.hash)));
|
||||
}
|
||||
|
||||
[[nodiscard]] std::vector<Ui::EmojiGroup> GroupsFromTL(
|
||||
const MTPDmessages_emojiGroups &data) {
|
||||
const auto &list = data.vgroups().v;
|
||||
auto result = std::vector<Ui::EmojiGroup>();
|
||||
result.reserve(list.size());
|
||||
for (const auto &group : list) {
|
||||
group.match([&](const MTPDemojiGroupPremium &data) {
|
||||
result.push_back({
|
||||
.iconId = QString::number(data.vicon_emoji_id().v),
|
||||
.type = Ui::EmojiGroupType::Premium,
|
||||
});
|
||||
}, [&](const auto &data) {
|
||||
auto emoticons = ranges::views::all(
|
||||
data.vemoticons().v
|
||||
) | ranges::views::transform([](const MTPstring &emoticon) {
|
||||
return qs(emoticon);
|
||||
}) | ranges::to_vector;
|
||||
result.push_back({
|
||||
.iconId = QString::number(data.vicon_emoji_id().v),
|
||||
.emoticons = std::move(emoticons),
|
||||
.type = (MTPDemojiGroupGreeting::Is<decltype(data)>()
|
||||
? Ui::EmojiGroupType::Greeting
|
||||
: Ui::EmojiGroupType::Normal),
|
||||
});
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
template <typename Request>
|
||||
void EmojiStatuses::requestGroups(
|
||||
not_null<GroupsType*> type,
|
||||
Request &&request) {
|
||||
if (type->requestId) {
|
||||
return;
|
||||
}
|
||||
type->requestId = _owner->session().api().request(
|
||||
std::forward<Request>(request)
|
||||
).done([=](const MTPmessages_EmojiGroups &result) {
|
||||
type->requestId = 0;
|
||||
result.match([&](const MTPDmessages_emojiGroups &data) {
|
||||
type->hash = data.vhash().v;
|
||||
type->data = GroupsFromTL(data);
|
||||
}, [](const MTPDmessages_emojiGroupsNotModified&) {
|
||||
});
|
||||
}).fail([=] {
|
||||
type->requestId = 0;
|
||||
}).send();
|
||||
}
|
||||
|
||||
void EmojiStatuses::processClearing() {
|
||||
auto minWait = TimeId(0);
|
||||
const auto now = base::unixtime::now();
|
||||
auto clearing = base::take(_clearing);
|
||||
for (auto i = begin(clearing); i != end(clearing);) {
|
||||
const auto until = i->second;
|
||||
if (now < until) {
|
||||
const auto wait = (until - now);
|
||||
if (!minWait || minWait > wait) {
|
||||
minWait = wait;
|
||||
}
|
||||
++i;
|
||||
} else {
|
||||
i->first->setEmojiStatus(EmojiStatusId());
|
||||
i = clearing.erase(i);
|
||||
}
|
||||
}
|
||||
if (_clearing.empty()) {
|
||||
_clearing = std::move(clearing);
|
||||
} else {
|
||||
for (const auto &[user, until] : clearing) {
|
||||
_clearing.emplace(user, until);
|
||||
}
|
||||
}
|
||||
if (minWait) {
|
||||
processClearingIn(minWait);
|
||||
} else {
|
||||
_clearingTimer.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<EmojiStatusId> EmojiStatuses::parse(
|
||||
const MTPDaccount_emojiStatuses &data) {
|
||||
const auto &list = data.vstatuses().v;
|
||||
auto result = std::vector<EmojiStatusId>();
|
||||
result.reserve(list.size());
|
||||
for (const auto &status : list) {
|
||||
const auto parsed = parse(status);
|
||||
if (!parsed.id) {
|
||||
LOG(("API Error: empty status in account.emojiStatuses."));
|
||||
} else {
|
||||
result.push_back(parsed.id);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void EmojiStatuses::processClearingIn(TimeId wait) {
|
||||
const auto waitms = wait * crl::time(1000);
|
||||
_clearingTimer.callOnce(std::min(waitms, kMaxTimeout));
|
||||
}
|
||||
|
||||
void EmojiStatuses::requestRecent() {
|
||||
if (_recentRequestId) {
|
||||
return;
|
||||
}
|
||||
auto &api = _owner->session().api();
|
||||
_recentRequestScheduled = false;
|
||||
_recentRequestId = api.request(MTPaccount_GetRecentEmojiStatuses(
|
||||
MTP_long(_recentHash)
|
||||
)).done([=](const MTPaccount_EmojiStatuses &result) {
|
||||
_recentRequestId = 0;
|
||||
result.match([&](const MTPDaccount_emojiStatuses &data) {
|
||||
updateRecent(data);
|
||||
}, [](const MTPDaccount_emojiStatusesNotModified&) {
|
||||
});
|
||||
}).fail([=] {
|
||||
_recentRequestId = 0;
|
||||
_recentHash = 0;
|
||||
}).send();
|
||||
}
|
||||
|
||||
void EmojiStatuses::requestDefault() {
|
||||
if (_defaultRequestId) {
|
||||
return;
|
||||
}
|
||||
auto &api = _owner->session().api();
|
||||
_defaultRequestId = api.request(MTPaccount_GetDefaultEmojiStatuses(
|
||||
MTP_long(_defaultHash)
|
||||
)).done([=](const MTPaccount_EmojiStatuses &result) {
|
||||
_defaultRequestId = 0;
|
||||
result.match([&](const MTPDaccount_emojiStatuses &data) {
|
||||
updateDefault(data);
|
||||
}, [&](const MTPDaccount_emojiStatusesNotModified &) {
|
||||
});
|
||||
}).fail([=] {
|
||||
_defaultRequestId = 0;
|
||||
_defaultHash = 0;
|
||||
}).send();
|
||||
}
|
||||
|
||||
void EmojiStatuses::requestColored() {
|
||||
if (_coloredRequestId) {
|
||||
return;
|
||||
}
|
||||
auto &api = _owner->session().api();
|
||||
_coloredRequestId = api.request(MTPmessages_GetStickerSet(
|
||||
MTP_inputStickerSetEmojiDefaultStatuses(),
|
||||
MTP_int(0) // hash
|
||||
)).done([=](const MTPmessages_StickerSet &result) {
|
||||
_coloredRequestId = 0;
|
||||
result.match([&](const MTPDmessages_stickerSet &data) {
|
||||
updateColored(data);
|
||||
refreshCollectibles();
|
||||
}, [](const MTPDmessages_stickerSetNotModified &) {
|
||||
LOG(("API Error: Unexpected messages.stickerSetNotModified."));
|
||||
});
|
||||
}).fail([=] {
|
||||
_coloredRequestId = 0;
|
||||
}).send();
|
||||
}
|
||||
|
||||
void EmojiStatuses::requestChannelDefault() {
|
||||
if (_channelDefaultRequestId) {
|
||||
return;
|
||||
}
|
||||
auto &api = _owner->session().api();
|
||||
_channelDefaultRequestId = api.request(MTPaccount_GetDefaultEmojiStatuses(
|
||||
MTP_long(_channelDefaultHash)
|
||||
)).done([=](const MTPaccount_EmojiStatuses &result) {
|
||||
_channelDefaultRequestId = 0;
|
||||
result.match([&](const MTPDaccount_emojiStatuses &data) {
|
||||
updateChannelDefault(data);
|
||||
}, [&](const MTPDaccount_emojiStatusesNotModified &) {
|
||||
});
|
||||
}).fail([=] {
|
||||
_channelDefaultRequestId = 0;
|
||||
_channelDefaultHash = 0;
|
||||
}).send();
|
||||
}
|
||||
|
||||
void EmojiStatuses::requestChannelColored() {
|
||||
if (_channelColoredRequestId) {
|
||||
return;
|
||||
}
|
||||
auto &api = _owner->session().api();
|
||||
_channelColoredRequestId = api.request(MTPmessages_GetStickerSet(
|
||||
MTP_inputStickerSetEmojiChannelDefaultStatuses(),
|
||||
MTP_int(0) // hash
|
||||
)).done([=](const MTPmessages_StickerSet &result) {
|
||||
_channelColoredRequestId = 0;
|
||||
result.match([&](const MTPDmessages_stickerSet &data) {
|
||||
updateChannelColored(data);
|
||||
}, [](const MTPDmessages_stickerSetNotModified &) {
|
||||
LOG(("API Error: Unexpected messages.stickerSetNotModified."));
|
||||
});
|
||||
}).fail([=] {
|
||||
_channelColoredRequestId = 0;
|
||||
}).send();
|
||||
}
|
||||
|
||||
void EmojiStatuses::requestCollectibles() {
|
||||
if (_collectiblesRequestId) {
|
||||
return;
|
||||
}
|
||||
auto &api = _owner->session().api();
|
||||
_collectiblesRequestId = api.request(
|
||||
MTPaccount_GetCollectibleEmojiStatuses(MTP_long(_collectiblesHash))
|
||||
).done([=](const MTPaccount_EmojiStatuses &result) {
|
||||
_collectiblesRequestId = 0;
|
||||
result.match([&](const MTPDaccount_emojiStatuses &data) {
|
||||
updateCollectibles(data);
|
||||
}, [&](const MTPDaccount_emojiStatusesNotModified &) {
|
||||
});
|
||||
}).fail([=] {
|
||||
_collectiblesRequestId = 0;
|
||||
_collectiblesHash = 0;
|
||||
}).send();
|
||||
}
|
||||
|
||||
void EmojiStatuses::updateRecent(const MTPDaccount_emojiStatuses &data) {
|
||||
_recentHash = data.vhash().v;
|
||||
_recent = parse(data);
|
||||
_recentUpdated.fire({});
|
||||
}
|
||||
|
||||
void EmojiStatuses::updateDefault(const MTPDaccount_emojiStatuses &data) {
|
||||
_defaultHash = data.vhash().v;
|
||||
_default = parse(data);
|
||||
_defaultUpdated.fire({});
|
||||
}
|
||||
|
||||
void EmojiStatuses::updateColored(const MTPDmessages_stickerSet &data) {
|
||||
const auto &list = data.vdocuments().v;
|
||||
_colored.clear();
|
||||
_colored.reserve(list.size());
|
||||
for (const auto &sticker : data.vdocuments().v) {
|
||||
_colored.push_back({
|
||||
.documentId = _owner->processDocument(sticker)->id,
|
||||
});
|
||||
}
|
||||
_coloredUpdated.fire({});
|
||||
}
|
||||
|
||||
void EmojiStatuses::updateChannelDefault(
|
||||
const MTPDaccount_emojiStatuses &data) {
|
||||
_channelDefaultHash = data.vhash().v;
|
||||
_channelDefault = parse(data);
|
||||
_channelDefaultUpdated.fire({});
|
||||
}
|
||||
|
||||
void EmojiStatuses::updateChannelColored(
|
||||
const MTPDmessages_stickerSet &data) {
|
||||
const auto &list = data.vdocuments().v;
|
||||
_channelColored.clear();
|
||||
_channelColored.reserve(list.size());
|
||||
for (const auto &sticker : data.vdocuments().v) {
|
||||
_channelColored.push_back({
|
||||
.documentId = _owner->processDocument(sticker)->id,
|
||||
});
|
||||
}
|
||||
_channelColoredUpdated.fire({});
|
||||
}
|
||||
|
||||
void EmojiStatuses::updateCollectibles(
|
||||
const MTPDaccount_emojiStatuses &data) {
|
||||
_collectiblesHash = data.vhash().v;
|
||||
_collectibles = parse(data);
|
||||
_collectiblesUpdated.fire({});
|
||||
}
|
||||
|
||||
void EmojiStatuses::set(EmojiStatusId id, TimeId until) {
|
||||
set(_owner->session().user(), id, until);
|
||||
}
|
||||
|
||||
void EmojiStatuses::set(
|
||||
not_null<PeerData*> peer,
|
||||
EmojiStatusId id,
|
||||
TimeId until) {
|
||||
auto &api = _owner->session().api();
|
||||
auto &requestId = _sentRequests[peer];
|
||||
if (requestId) {
|
||||
api.request(base::take(requestId)).cancel();
|
||||
}
|
||||
peer->setEmojiStatus(id, until);
|
||||
const auto send = [&](auto &&request) {
|
||||
requestId = api.request(
|
||||
std::move(request)
|
||||
).done([=] {
|
||||
_sentRequests.remove(peer);
|
||||
}).fail([=] {
|
||||
_sentRequests.remove(peer);
|
||||
}).send();
|
||||
};
|
||||
using EFlag = MTPDemojiStatus::Flag;
|
||||
using CFlag = MTPDinputEmojiStatusCollectible::Flag;
|
||||
const auto status = !id
|
||||
? MTP_emojiStatusEmpty()
|
||||
: id.collectible
|
||||
? MTP_inputEmojiStatusCollectible(
|
||||
MTP_flags(until ? CFlag::f_until : CFlag()),
|
||||
MTP_long(id.collectible->id),
|
||||
MTP_int(until))
|
||||
: MTP_emojiStatus(
|
||||
MTP_flags(until ? EFlag::f_until : EFlag()),
|
||||
MTP_long(id.documentId),
|
||||
MTP_int(until));
|
||||
if (peer->isSelf()) {
|
||||
send(MTPaccount_UpdateEmojiStatus(status));
|
||||
} else if (const auto channel = peer->asChannel()) {
|
||||
send(MTPchannels_UpdateEmojiStatus(channel->inputChannel(), status));
|
||||
}
|
||||
}
|
||||
|
||||
EmojiStatusId EmojiStatuses::fromUniqueGift(
|
||||
const Data::UniqueGift &gift) {
|
||||
const auto collectibleId = gift.id;
|
||||
auto &collectible = _collectibleData[collectibleId];
|
||||
if (!collectible) {
|
||||
collectible = std::make_shared<EmojiStatusCollectible>(
|
||||
EmojiStatusCollectible{
|
||||
.id = gift.id,
|
||||
.documentId = gift.model.document->id,
|
||||
.title = Data::UniqueGiftName(gift),
|
||||
.slug = gift.slug,
|
||||
.patternDocumentId = gift.pattern.document->id,
|
||||
.centerColor = gift.backdrop.centerColor,
|
||||
.edgeColor = gift.backdrop.edgeColor,
|
||||
.patternColor = gift.backdrop.patternColor,
|
||||
.textColor = gift.backdrop.textColor,
|
||||
});
|
||||
}
|
||||
return { .collectible = collectible };
|
||||
}
|
||||
|
||||
EmojiStatusCollectible *EmojiStatuses::collectibleInfo(CollectibleId id) {
|
||||
const auto i = _collectibleData.find(id);
|
||||
return (i != end(_collectibleData)) ? i->second.get() : nullptr;
|
||||
}
|
||||
|
||||
} // namespace Data
|
||||
178
Telegram/SourceFiles/data/data_emoji_statuses.h
Normal file
178
Telegram/SourceFiles/data/data_emoji_statuses.h
Normal file
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
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"
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace Ui {
|
||||
struct EmojiGroup;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Data {
|
||||
|
||||
class DocumentMedia;
|
||||
class Session;
|
||||
struct UniqueGift;
|
||||
|
||||
struct EmojiStatusCollectible {
|
||||
CollectibleId id = 0;
|
||||
DocumentId documentId = 0;
|
||||
QString title;
|
||||
QString slug;
|
||||
DocumentId patternDocumentId = 0;
|
||||
QColor centerColor;
|
||||
QColor edgeColor;
|
||||
QColor patternColor;
|
||||
QColor textColor;
|
||||
|
||||
explicit operator bool() const {
|
||||
return id != 0;
|
||||
}
|
||||
};
|
||||
struct EmojiStatusData {
|
||||
EmojiStatusId id;
|
||||
TimeId until = 0;
|
||||
};
|
||||
|
||||
class EmojiStatuses final {
|
||||
public:
|
||||
explicit EmojiStatuses(not_null<Session*> owner);
|
||||
~EmojiStatuses();
|
||||
|
||||
[[nodiscard]] Session &owner() const {
|
||||
return *_owner;
|
||||
}
|
||||
[[nodiscard]] Main::Session &session() const;
|
||||
|
||||
void refreshRecent();
|
||||
void refreshRecentDelayed();
|
||||
void refreshDefault();
|
||||
void refreshColored();
|
||||
void refreshChannelDefault();
|
||||
void refreshChannelColored();
|
||||
void refreshCollectibles();
|
||||
|
||||
enum class Type {
|
||||
Recent,
|
||||
Default,
|
||||
Colored,
|
||||
ChannelDefault,
|
||||
ChannelColored,
|
||||
Collectibles,
|
||||
};
|
||||
[[nodiscard]] const std::vector<EmojiStatusId> &list(Type type) const;
|
||||
|
||||
[[nodiscard]] EmojiStatusData parse(const MTPEmojiStatus &status);
|
||||
|
||||
[[nodiscard]] rpl::producer<> recentUpdates() const;
|
||||
[[nodiscard]] rpl::producer<> defaultUpdates() const;
|
||||
[[nodiscard]] rpl::producer<> channelDefaultUpdates() const;
|
||||
[[nodiscard]] rpl::producer<> collectiblesUpdates() const;
|
||||
|
||||
void set(EmojiStatusId id, TimeId until = 0);
|
||||
void set(not_null<PeerData*> peer, EmojiStatusId id, TimeId until = 0);
|
||||
[[nodiscard]] EmojiStatusId fromUniqueGift(const Data::UniqueGift &gift);
|
||||
[[nodiscard]] EmojiStatusCollectible *collectibleInfo(CollectibleId id);
|
||||
|
||||
void registerAutomaticClear(not_null<PeerData*> peer, TimeId until);
|
||||
|
||||
using Groups = std::vector<Ui::EmojiGroup>;
|
||||
[[nodiscard]] rpl::producer<Groups> emojiGroupsValue() const;
|
||||
[[nodiscard]] rpl::producer<Groups> statusGroupsValue() const;
|
||||
[[nodiscard]] rpl::producer<Groups> stickerGroupsValue() const;
|
||||
[[nodiscard]] rpl::producer<Groups> profilePhotoGroupsValue() const;
|
||||
void requestEmojiGroups();
|
||||
void requestStatusGroups();
|
||||
void requestStickerGroups();
|
||||
void requestProfilePhotoGroups();
|
||||
|
||||
private:
|
||||
struct GroupsType {
|
||||
rpl::variable<Groups> data;
|
||||
mtpRequestId requestId = 0;
|
||||
int32 hash = 0;
|
||||
};
|
||||
|
||||
void requestRecent();
|
||||
void requestDefault();
|
||||
void requestColored();
|
||||
void requestChannelDefault();
|
||||
void requestChannelColored();
|
||||
void requestCollectibles();
|
||||
|
||||
void updateRecent(const MTPDaccount_emojiStatuses &data);
|
||||
void updateDefault(const MTPDaccount_emojiStatuses &data);
|
||||
void updateColored(const MTPDmessages_stickerSet &data);
|
||||
void updateChannelDefault(const MTPDaccount_emojiStatuses &data);
|
||||
void updateChannelColored(const MTPDmessages_stickerSet &data);
|
||||
void updateCollectibles(const MTPDaccount_emojiStatuses &data);
|
||||
|
||||
void processClearingIn(TimeId wait);
|
||||
void processClearing();
|
||||
|
||||
[[nodiscard]] std::vector<EmojiStatusId> parse(
|
||||
const MTPDaccount_emojiStatuses &data);
|
||||
|
||||
template <typename Request>
|
||||
void requestGroups(not_null<GroupsType*> type, Request &&request);
|
||||
|
||||
const not_null<Session*> _owner;
|
||||
|
||||
std::vector<EmojiStatusId> _recent;
|
||||
std::vector<EmojiStatusId> _default;
|
||||
std::vector<EmojiStatusId> _colored;
|
||||
std::vector<EmojiStatusId> _channelDefault;
|
||||
std::vector<EmojiStatusId> _channelColored;
|
||||
std::vector<EmojiStatusId> _collectibles;
|
||||
rpl::event_stream<> _recentUpdated;
|
||||
rpl::event_stream<> _defaultUpdated;
|
||||
rpl::event_stream<> _coloredUpdated;
|
||||
rpl::event_stream<> _channelDefaultUpdated;
|
||||
rpl::event_stream<> _channelColoredUpdated;
|
||||
rpl::event_stream<> _collectiblesUpdated;
|
||||
|
||||
base::flat_map<
|
||||
CollectibleId,
|
||||
std::shared_ptr<EmojiStatusCollectible>> _collectibleData;
|
||||
|
||||
mtpRequestId _recentRequestId = 0;
|
||||
bool _recentRequestScheduled = false;
|
||||
uint64 _recentHash = 0;
|
||||
|
||||
mtpRequestId _defaultRequestId = 0;
|
||||
uint64 _defaultHash = 0;
|
||||
|
||||
mtpRequestId _coloredRequestId = 0;
|
||||
|
||||
mtpRequestId _channelDefaultRequestId = 0;
|
||||
uint64 _channelDefaultHash = 0;
|
||||
|
||||
mtpRequestId _channelColoredRequestId = 0;
|
||||
|
||||
mtpRequestId _collectiblesRequestId = 0;
|
||||
uint64 _collectiblesHash = 0;
|
||||
|
||||
base::flat_map<not_null<PeerData*>, mtpRequestId> _sentRequests;
|
||||
|
||||
base::flat_map<not_null<PeerData*>, TimeId> _clearing;
|
||||
base::Timer _clearingTimer;
|
||||
|
||||
GroupsType _emojiGroups;
|
||||
GroupsType _statusGroups;
|
||||
GroupsType _stickerGroups;
|
||||
GroupsType _profilePhotoGroups;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Data
|
||||
247
Telegram/SourceFiles/data/data_file_click_handler.cpp
Normal file
247
Telegram/SourceFiles/data/data_file_click_handler.cpp
Normal file
@@ -0,0 +1,247 @@
|
||||
/*
|
||||
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/data_file_click_handler.h"
|
||||
|
||||
#include "core/click_handler_types.h"
|
||||
#include "core/file_utilities.h"
|
||||
#include "core/application.h"
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_download_manager.h"
|
||||
#include "data/data_photo.h"
|
||||
#include "main/main_session.h"
|
||||
|
||||
FileClickHandler::FileClickHandler(FullMsgId context)
|
||||
: _context(context) {
|
||||
}
|
||||
|
||||
void FileClickHandler::setMessageId(FullMsgId context) {
|
||||
_context = context;
|
||||
}
|
||||
|
||||
FullMsgId FileClickHandler::context() const {
|
||||
return _context;
|
||||
}
|
||||
|
||||
not_null<DocumentData*> DocumentClickHandler::document() const {
|
||||
return _document;
|
||||
}
|
||||
|
||||
DocumentWrappedClickHandler::DocumentWrappedClickHandler(
|
||||
ClickHandlerPtr wrapped,
|
||||
not_null<DocumentData*> document,
|
||||
FullMsgId context)
|
||||
: DocumentClickHandler(document, context)
|
||||
, _wrapped(wrapped) {
|
||||
}
|
||||
|
||||
void DocumentWrappedClickHandler::onClickImpl() const {
|
||||
_wrapped->onClick({ Qt::LeftButton });
|
||||
}
|
||||
|
||||
DocumentClickHandler::DocumentClickHandler(
|
||||
not_null<DocumentData*> document,
|
||||
FullMsgId context)
|
||||
: FileClickHandler(context)
|
||||
, _document(document) {
|
||||
setProperty(
|
||||
kDocumentLinkMediaProperty,
|
||||
reinterpret_cast<qulonglong>(_document.get()));
|
||||
}
|
||||
|
||||
QString DocumentClickHandler::tooltip() const {
|
||||
return property(kDocumentFilenameTooltipProperty).value<QString>();
|
||||
}
|
||||
|
||||
DocumentOpenClickHandler::DocumentOpenClickHandler(
|
||||
not_null<DocumentData*> document,
|
||||
Fn<void(FullMsgId)> &&callback,
|
||||
FullMsgId context)
|
||||
: DocumentClickHandler(document, context)
|
||||
, _handler(std::move(callback)) {
|
||||
Expects(_handler != nullptr);
|
||||
}
|
||||
|
||||
void DocumentOpenClickHandler::onClickImpl() const {
|
||||
_handler(context());
|
||||
}
|
||||
|
||||
void DocumentSaveClickHandler::Save(
|
||||
Data::FileOrigin origin,
|
||||
not_null<DocumentData*> data,
|
||||
Mode mode,
|
||||
Fn<void()> started) {
|
||||
if (data->isNull()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto savename = QString();
|
||||
if (mode == Mode::ToCacheOrFile && data->saveToCache()) {
|
||||
data->save(origin, savename);
|
||||
return;
|
||||
}
|
||||
InvokeQueued(qApp, crl::guard(&data->session(), [=] {
|
||||
// If we call file dialog synchronously, it will stop
|
||||
// background thread timers from working which would
|
||||
// stop audio playback in voice chats / live streams.
|
||||
if (mode != Mode::ToNewFile && data->saveFromData()) {
|
||||
if (started) {
|
||||
started();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const auto filepath = data->filepath(true);
|
||||
const auto fileinfo = QFileInfo(
|
||||
);
|
||||
const auto filedir = filepath.isEmpty()
|
||||
? QDir()
|
||||
: fileinfo.dir();
|
||||
const auto filename = filepath.isEmpty()
|
||||
? QString()
|
||||
: fileinfo.fileName();
|
||||
const auto savename = DocumentFileNameForSave(
|
||||
data,
|
||||
(mode == Mode::ToNewFile),
|
||||
filename,
|
||||
filedir);
|
||||
if (!savename.isEmpty()) {
|
||||
data->save(origin, savename);
|
||||
if (started) {
|
||||
started();
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
void DocumentSaveClickHandler::SaveAndTrack(
|
||||
FullMsgId itemId,
|
||||
not_null<DocumentData*> document,
|
||||
Mode mode,
|
||||
Fn<void()> started) {
|
||||
Save(itemId ? itemId : Data::FileOrigin(), document, mode, [=] {
|
||||
if (document->loading() && !document->loadingFilePath().isEmpty()) {
|
||||
if (const auto item = document->owner().message(itemId)) {
|
||||
Core::App().downloadManager().addLoading({
|
||||
.item = item,
|
||||
.document = document,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (started) {
|
||||
started();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void DocumentSaveClickHandler::onClickImpl() const {
|
||||
SaveAndTrack(context(), document());
|
||||
}
|
||||
|
||||
DocumentCancelClickHandler::DocumentCancelClickHandler(
|
||||
not_null<DocumentData*> document,
|
||||
Fn<void(FullMsgId)> &&callback,
|
||||
FullMsgId context)
|
||||
: DocumentClickHandler(document, context)
|
||||
, _handler(std::move(callback)) {
|
||||
}
|
||||
|
||||
void DocumentCancelClickHandler::onClickImpl() const {
|
||||
const auto data = document();
|
||||
if (data->isNull()) {
|
||||
return;
|
||||
} else if (data->uploading() && _handler) {
|
||||
_handler(context());
|
||||
} else {
|
||||
data->cancel();
|
||||
}
|
||||
}
|
||||
|
||||
void DocumentOpenWithClickHandler::Open(
|
||||
Data::FileOrigin origin,
|
||||
not_null<DocumentData*> data) {
|
||||
if (data->isNull()) {
|
||||
return;
|
||||
}
|
||||
|
||||
data->saveFromDataSilent();
|
||||
const auto path = data->filepath(true);
|
||||
if (!path.isEmpty()) {
|
||||
File::OpenWith(path);
|
||||
} else {
|
||||
DocumentSaveClickHandler::Save(
|
||||
origin,
|
||||
data,
|
||||
DocumentSaveClickHandler::Mode::ToFile);
|
||||
}
|
||||
}
|
||||
|
||||
void DocumentOpenWithClickHandler::onClickImpl() const {
|
||||
Open(context(), document());
|
||||
}
|
||||
|
||||
PhotoClickHandler::PhotoClickHandler(
|
||||
not_null<PhotoData*> photo,
|
||||
FullMsgId context,
|
||||
PeerData *peer)
|
||||
: FileClickHandler(context)
|
||||
, _photo(photo)
|
||||
, _peer(peer) {
|
||||
setProperty(
|
||||
kPhotoLinkMediaProperty,
|
||||
reinterpret_cast<qulonglong>(_photo.get()));
|
||||
}
|
||||
|
||||
not_null<PhotoData*> PhotoClickHandler::photo() const {
|
||||
return _photo;
|
||||
}
|
||||
|
||||
PeerData *PhotoClickHandler::peer() const {
|
||||
return _peer;
|
||||
}
|
||||
|
||||
PhotoOpenClickHandler::PhotoOpenClickHandler(
|
||||
not_null<PhotoData*> photo,
|
||||
Fn<void(FullMsgId)> &&callback,
|
||||
FullMsgId context)
|
||||
: PhotoClickHandler(photo, context)
|
||||
, _handler(std::move(callback)) {
|
||||
Expects(_handler != nullptr);
|
||||
}
|
||||
|
||||
void PhotoOpenClickHandler::onClickImpl() const {
|
||||
_handler(context());
|
||||
}
|
||||
|
||||
void PhotoSaveClickHandler::onClickImpl() const {
|
||||
const auto data = photo();
|
||||
if (data->isNull()) {
|
||||
return;
|
||||
} else {
|
||||
data->clearFailed(Data::PhotoSize::Large);
|
||||
data->load(context());
|
||||
}
|
||||
}
|
||||
|
||||
PhotoCancelClickHandler::PhotoCancelClickHandler(
|
||||
not_null<PhotoData*> photo,
|
||||
Fn<void(FullMsgId)> &&callback,
|
||||
FullMsgId context)
|
||||
: PhotoClickHandler(photo, context)
|
||||
, _handler(std::move(callback)) {
|
||||
}
|
||||
|
||||
void PhotoCancelClickHandler::onClickImpl() const {
|
||||
const auto data = photo();
|
||||
if (data->isNull()) {
|
||||
return;
|
||||
} else if (data->uploading() && _handler) {
|
||||
_handler(context());
|
||||
} else {
|
||||
data->cancel();
|
||||
}
|
||||
}
|
||||
189
Telegram/SourceFiles/data/data_file_click_handler.h
Normal file
189
Telegram/SourceFiles/data/data_file_click_handler.h
Normal file
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
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_file_origin.h"
|
||||
#include "ui/basic_click_handlers.h"
|
||||
|
||||
class DocumentData;
|
||||
class HistoryItem;
|
||||
class PhotoData;
|
||||
|
||||
class FileClickHandler : public LeftButtonClickHandler {
|
||||
public:
|
||||
FileClickHandler(FullMsgId context);
|
||||
|
||||
void setMessageId(FullMsgId context);
|
||||
|
||||
[[nodiscard]] FullMsgId context() const;
|
||||
|
||||
private:
|
||||
FullMsgId _context;
|
||||
|
||||
};
|
||||
|
||||
class DocumentClickHandler : public FileClickHandler {
|
||||
public:
|
||||
DocumentClickHandler(
|
||||
not_null<DocumentData*> document,
|
||||
FullMsgId context = FullMsgId());
|
||||
|
||||
QString tooltip() const override;
|
||||
|
||||
[[nodiscard]] not_null<DocumentData*> document() const;
|
||||
|
||||
private:
|
||||
const not_null<DocumentData*> _document;
|
||||
|
||||
};
|
||||
|
||||
class DocumentSaveClickHandler : public DocumentClickHandler {
|
||||
public:
|
||||
enum class Mode {
|
||||
ToCacheOrFile,
|
||||
ToFile,
|
||||
ToNewFile,
|
||||
};
|
||||
using DocumentClickHandler::DocumentClickHandler;
|
||||
static void Save(
|
||||
Data::FileOrigin origin,
|
||||
not_null<DocumentData*> document,
|
||||
Mode mode = Mode::ToCacheOrFile,
|
||||
Fn<void()> started = nullptr);
|
||||
static void SaveAndTrack(
|
||||
FullMsgId itemId,
|
||||
not_null<DocumentData*> document,
|
||||
Mode mode = Mode::ToCacheOrFile,
|
||||
Fn<void()> started = nullptr);
|
||||
|
||||
protected:
|
||||
void onClickImpl() const override;
|
||||
|
||||
};
|
||||
|
||||
class DocumentOpenClickHandler : public DocumentClickHandler {
|
||||
public:
|
||||
DocumentOpenClickHandler(
|
||||
not_null<DocumentData*> document,
|
||||
Fn<void(FullMsgId)> &&callback,
|
||||
FullMsgId context = FullMsgId());
|
||||
|
||||
protected:
|
||||
void onClickImpl() const override;
|
||||
|
||||
private:
|
||||
const Fn<void(FullMsgId)> _handler;
|
||||
|
||||
};
|
||||
|
||||
class DocumentCancelClickHandler : public DocumentClickHandler {
|
||||
public:
|
||||
DocumentCancelClickHandler(
|
||||
not_null<DocumentData*> document,
|
||||
Fn<void(FullMsgId)> &&callback,
|
||||
FullMsgId context = FullMsgId());
|
||||
|
||||
protected:
|
||||
void onClickImpl() const override;
|
||||
|
||||
private:
|
||||
const Fn<void(FullMsgId)> _handler;
|
||||
|
||||
};
|
||||
|
||||
class DocumentOpenWithClickHandler : public DocumentClickHandler {
|
||||
public:
|
||||
using DocumentClickHandler::DocumentClickHandler;
|
||||
static void Open(
|
||||
Data::FileOrigin origin,
|
||||
not_null<DocumentData*> document);
|
||||
|
||||
protected:
|
||||
void onClickImpl() const override;
|
||||
|
||||
};
|
||||
|
||||
class VoiceSeekClickHandler : public DocumentOpenClickHandler {
|
||||
public:
|
||||
using DocumentOpenClickHandler::DocumentOpenClickHandler;
|
||||
|
||||
protected:
|
||||
void onClickImpl() const override {
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
class DocumentWrappedClickHandler : public DocumentClickHandler {
|
||||
public:
|
||||
DocumentWrappedClickHandler(
|
||||
ClickHandlerPtr wrapped,
|
||||
not_null<DocumentData*> document,
|
||||
FullMsgId context = FullMsgId());
|
||||
|
||||
protected:
|
||||
void onClickImpl() const override;
|
||||
|
||||
private:
|
||||
ClickHandlerPtr _wrapped;
|
||||
|
||||
};
|
||||
|
||||
class PhotoClickHandler : public FileClickHandler {
|
||||
public:
|
||||
PhotoClickHandler(
|
||||
not_null<PhotoData*> photo,
|
||||
FullMsgId context = FullMsgId(),
|
||||
PeerData *peer = nullptr);
|
||||
|
||||
[[nodiscard]] not_null<PhotoData*> photo() const;
|
||||
[[nodiscard]] PeerData *peer() const;
|
||||
|
||||
private:
|
||||
const not_null<PhotoData*> _photo;
|
||||
PeerData * const _peer = nullptr;
|
||||
|
||||
};
|
||||
|
||||
class PhotoOpenClickHandler : public PhotoClickHandler {
|
||||
public:
|
||||
PhotoOpenClickHandler(
|
||||
not_null<PhotoData*> photo,
|
||||
Fn<void(FullMsgId)> &&callback,
|
||||
FullMsgId context = FullMsgId());
|
||||
|
||||
protected:
|
||||
void onClickImpl() const override;
|
||||
|
||||
private:
|
||||
const Fn<void(FullMsgId)> _handler;
|
||||
|
||||
};
|
||||
|
||||
class PhotoSaveClickHandler : public PhotoClickHandler {
|
||||
public:
|
||||
using PhotoClickHandler::PhotoClickHandler;
|
||||
|
||||
protected:
|
||||
void onClickImpl() const override;
|
||||
|
||||
};
|
||||
|
||||
class PhotoCancelClickHandler : public PhotoClickHandler {
|
||||
public:
|
||||
PhotoCancelClickHandler(
|
||||
not_null<PhotoData*> photo,
|
||||
Fn<void(FullMsgId)> &&callback,
|
||||
FullMsgId context = FullMsgId());
|
||||
|
||||
protected:
|
||||
void onClickImpl() const override;
|
||||
|
||||
private:
|
||||
const Fn<void(FullMsgId)> _handler;
|
||||
|
||||
};
|
||||
292
Telegram/SourceFiles/data/data_file_origin.cpp
Normal file
292
Telegram/SourceFiles/data/data_file_origin.cpp
Normal file
@@ -0,0 +1,292 @@
|
||||
/*
|
||||
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/data_file_origin.h"
|
||||
|
||||
namespace Data {
|
||||
namespace {
|
||||
|
||||
struct FileReferenceAccumulator {
|
||||
template <typename Type>
|
||||
void push(const MTPVector<Type> &data) {
|
||||
for (const auto &item : data.v) {
|
||||
push(item);
|
||||
}
|
||||
}
|
||||
template <typename Type>
|
||||
void push(const tl::conditional<Type> &data) {
|
||||
if (data) {
|
||||
push(*data);
|
||||
}
|
||||
}
|
||||
void push(const MTPPhoto &data) {
|
||||
data.match([&](const MTPDphoto &data) {
|
||||
result.data.emplace(
|
||||
PhotoFileLocationId{ data.vid().v },
|
||||
data.vfile_reference().v);
|
||||
}, [](const MTPDphotoEmpty &data) {
|
||||
});
|
||||
}
|
||||
void push(const MTPDocument &data) {
|
||||
data.match([&](const MTPDdocument &data) {
|
||||
result.data.emplace(
|
||||
DocumentFileLocationId{ data.vid().v },
|
||||
data.vfile_reference().v);
|
||||
}, [](const MTPDdocumentEmpty &data) {
|
||||
});
|
||||
}
|
||||
void push(const MTPPage &data) {
|
||||
push(data.data().vphotos());
|
||||
push(data.data().vdocuments());
|
||||
}
|
||||
void push(const MTPWallPaper &data) {
|
||||
data.match([&](const MTPDwallPaper &data) {
|
||||
push(data.vdocument());
|
||||
}, [&](const MTPDwallPaperNoFile &data) {
|
||||
});
|
||||
}
|
||||
void push(const MTPTheme &data) {
|
||||
push(data.data().vdocument());
|
||||
}
|
||||
void push(const MTPWebPageAttribute &data) {
|
||||
data.match([&](const MTPDwebPageAttributeStory &data) {
|
||||
push(data.vstory());
|
||||
}, [&](const MTPDwebPageAttributeTheme &data) {
|
||||
push(data.vdocuments());
|
||||
}, [&](const MTPDwebPageAttributeStickerSet &data) {
|
||||
push(data.vstickers());
|
||||
}, [&](const MTPDwebPageAttributeUniqueStarGift &data) {
|
||||
push(data.vgift());
|
||||
}, [&](const MTPDwebPageAttributeStarGiftCollection &data) {
|
||||
push(data.vicons());
|
||||
}, [&](const MTPDwebPageAttributeStarGiftAuction &data) {
|
||||
push(data.vgift());
|
||||
});
|
||||
}
|
||||
void push(const MTPStarGift &data) {
|
||||
data.match([&](const MTPDstarGift &data) {
|
||||
push(data.vsticker());
|
||||
}, [&](const MTPDstarGiftUnique &data) {
|
||||
push(data.vattributes());
|
||||
});
|
||||
}
|
||||
void push(const MTPStarGiftAttribute &data) {
|
||||
data.match([&](const MTPDstarGiftAttributeModel &data) {
|
||||
push(data.vdocument());
|
||||
}, [&](const MTPDstarGiftAttributePattern &data) {
|
||||
push(data.vdocument());
|
||||
}, [&](const MTPDstarGiftAttributeBackdrop &data) {
|
||||
}, [&](const MTPDstarGiftAttributeOriginalDetails &data) {
|
||||
});
|
||||
}
|
||||
void push(const MTPWebPage &data) {
|
||||
data.match([&](const MTPDwebPage &data) {
|
||||
push(data.vdocument());
|
||||
push(data.vattributes());
|
||||
push(data.vphoto());
|
||||
push(data.vcached_page());
|
||||
}, [](const auto &data) {
|
||||
});
|
||||
}
|
||||
void push(const MTPGame &data) {
|
||||
data.match([&](const MTPDgame &data) {
|
||||
push(data.vdocument());
|
||||
}, [](const auto &data) {
|
||||
});
|
||||
}
|
||||
void push(const MTPMessageExtendedMedia &data) {
|
||||
data.match([&](const MTPDmessageExtendedMediaPreview &data) {
|
||||
}, [&](const MTPDmessageExtendedMedia &data) {
|
||||
push(data.vmedia());
|
||||
});
|
||||
}
|
||||
void push(const MTPMessageMedia &data) {
|
||||
data.match([&](const MTPDmessageMediaPhoto &data) {
|
||||
push(data.vphoto());
|
||||
}, [&](const MTPDmessageMediaDocument &data) {
|
||||
push(data.vdocument());
|
||||
push(data.vvideo_cover());
|
||||
push(data.valt_documents());
|
||||
}, [&](const MTPDmessageMediaWebPage &data) {
|
||||
push(data.vwebpage());
|
||||
}, [&](const MTPDmessageMediaGame &data) {
|
||||
push(data.vgame());
|
||||
}, [&](const MTPDmessageMediaInvoice &data) {
|
||||
push(data.vextended_media());
|
||||
}, [&](const MTPDmessageMediaPaidMedia &data) {
|
||||
push(data.vextended_media());
|
||||
}, [](const auto &data) {
|
||||
});
|
||||
}
|
||||
void push(const MTPMessageReplyHeader &data) {
|
||||
data.match([&](const MTPDmessageReplyHeader &data) {
|
||||
push(data.vreply_media());
|
||||
}, [](const MTPDmessageReplyStoryHeader &data) {
|
||||
});
|
||||
}
|
||||
void push(const MTPMessage &data) {
|
||||
data.match([&](const MTPDmessage &data) {
|
||||
push(data.vmedia());
|
||||
push(data.vreply_to());
|
||||
}, [&](const MTPDmessageService &data) {
|
||||
data.vaction().match(
|
||||
[&](const MTPDmessageActionChatEditPhoto &data) {
|
||||
push(data.vphoto());
|
||||
}, [&](const MTPDmessageActionSuggestProfilePhoto &data) {
|
||||
push(data.vphoto());
|
||||
}, [&](const MTPDmessageActionSetChatWallPaper &data) {
|
||||
push(data.vwallpaper());
|
||||
}, [](const auto &data) {
|
||||
});
|
||||
push(data.vreply_to());
|
||||
}, [](const MTPDmessageEmpty &data) {
|
||||
});
|
||||
}
|
||||
void push(const MTPStoryItem &data) {
|
||||
data.match([&](const MTPDstoryItem &data) {
|
||||
push(data.vmedia());
|
||||
}, [](const MTPDstoryItemDeleted &) {
|
||||
}, [](const MTPDstoryItemSkipped &) {
|
||||
});
|
||||
}
|
||||
void push(const MTPmessages_Messages &data) {
|
||||
data.match([](const MTPDmessages_messagesNotModified &) {
|
||||
}, [&](const auto &data) {
|
||||
push(data.vmessages());
|
||||
});
|
||||
}
|
||||
void push(const MTPphotos_Photos &data) {
|
||||
data.match([&](const auto &data) {
|
||||
push(data.vphotos());
|
||||
});
|
||||
}
|
||||
void push(const MTPusers_UserFull &data) {
|
||||
push(data.data().vfull_user().data().vpersonal_photo());
|
||||
}
|
||||
void push(const MTPmessages_RecentStickers &data) {
|
||||
data.match([&](const MTPDmessages_recentStickers &data) {
|
||||
push(data.vstickers());
|
||||
}, [](const MTPDmessages_recentStickersNotModified &data) {
|
||||
});
|
||||
}
|
||||
void push(const MTPmessages_FavedStickers &data) {
|
||||
data.match([&](const MTPDmessages_favedStickers &data) {
|
||||
push(data.vstickers());
|
||||
}, [](const MTPDmessages_favedStickersNotModified &data) {
|
||||
});
|
||||
}
|
||||
void push(const MTPmessages_StickerSet &data) {
|
||||
data.match([&](const MTPDmessages_stickerSet &data) {
|
||||
push(data.vdocuments());
|
||||
}, [](const MTPDmessages_stickerSetNotModified &data) {
|
||||
});
|
||||
}
|
||||
void push(const MTPmessages_SavedGifs &data) {
|
||||
data.match([&](const MTPDmessages_savedGifs &data) {
|
||||
push(data.vgifs());
|
||||
}, [](const MTPDmessages_savedGifsNotModified &data) {
|
||||
});
|
||||
}
|
||||
void push(const MTPaccount_SavedRingtones &data) {
|
||||
data.match([&](const MTPDaccount_savedRingtones &data) {
|
||||
push(data.vringtones());
|
||||
}, [](const MTPDaccount_savedRingtonesNotModified &data) {
|
||||
});
|
||||
}
|
||||
void push(const MTPhelp_PremiumPromo &data) {
|
||||
push(data.data().vvideos());
|
||||
}
|
||||
void push(const MTPmessages_WebPage &data) {
|
||||
push(data.data().vwebpage());
|
||||
}
|
||||
void push(const MTPstories_Stories &data) {
|
||||
push(data.data().vstories());
|
||||
}
|
||||
void push(const MTPusers_SavedMusic &data) {
|
||||
data.match([&](const MTPDusers_savedMusic &data) {
|
||||
push(data.vdocuments());
|
||||
}, [](const MTPDusers_savedMusicNotModified &data) {
|
||||
});
|
||||
}
|
||||
|
||||
UpdatedFileReferences result;
|
||||
};
|
||||
|
||||
template <typename Type>
|
||||
UpdatedFileReferences GetFileReferencesHelper(const Type &data) {
|
||||
FileReferenceAccumulator result;
|
||||
result.push(data);
|
||||
return result.result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
UpdatedFileReferences GetFileReferences(const MTPmessages_Messages &data) {
|
||||
return GetFileReferencesHelper(data);
|
||||
}
|
||||
|
||||
UpdatedFileReferences GetFileReferences(const MTPphotos_Photos &data) {
|
||||
return GetFileReferencesHelper(data);
|
||||
}
|
||||
|
||||
UpdatedFileReferences GetFileReferences(const MTPusers_UserFull &data) {
|
||||
return GetFileReferencesHelper(data);
|
||||
}
|
||||
|
||||
UpdatedFileReferences GetFileReferences(
|
||||
const MTPmessages_RecentStickers &data) {
|
||||
return GetFileReferencesHelper(data);
|
||||
}
|
||||
|
||||
UpdatedFileReferences GetFileReferences(
|
||||
const MTPmessages_FavedStickers &data) {
|
||||
return GetFileReferencesHelper(data);
|
||||
}
|
||||
|
||||
UpdatedFileReferences GetFileReferences(
|
||||
const MTPmessages_StickerSet &data) {
|
||||
return GetFileReferencesHelper(data);
|
||||
}
|
||||
|
||||
UpdatedFileReferences GetFileReferences(const MTPmessages_SavedGifs &data) {
|
||||
return GetFileReferencesHelper(data);
|
||||
}
|
||||
|
||||
UpdatedFileReferences GetFileReferences(const MTPWallPaper &data) {
|
||||
return GetFileReferencesHelper(data);
|
||||
}
|
||||
|
||||
UpdatedFileReferences GetFileReferences(const MTPTheme &data) {
|
||||
return GetFileReferencesHelper(data);
|
||||
}
|
||||
|
||||
UpdatedFileReferences GetFileReferences(
|
||||
const MTPaccount_SavedRingtones &data) {
|
||||
return GetFileReferencesHelper(data);
|
||||
}
|
||||
|
||||
UpdatedFileReferences GetFileReferences(const MTPhelp_PremiumPromo &data) {
|
||||
return GetFileReferencesHelper(data);
|
||||
}
|
||||
|
||||
UpdatedFileReferences GetFileReferences(const MTPmessages_WebPage &data) {
|
||||
return GetFileReferencesHelper(data);
|
||||
}
|
||||
|
||||
UpdatedFileReferences GetFileReferences(const MTPstories_Stories &data) {
|
||||
return GetFileReferencesHelper(data);
|
||||
}
|
||||
|
||||
UpdatedFileReferences GetFileReferences(const MTPusers_SavedMusic &data) {
|
||||
return GetFileReferencesHelper(data);
|
||||
}
|
||||
|
||||
UpdatedFileReferences GetFileReferences(const MTPMessageMedia &data) {
|
||||
return GetFileReferencesHelper(data);
|
||||
}
|
||||
|
||||
} // namespace Data
|
||||
229
Telegram/SourceFiles/data/data_file_origin.h
Normal file
229
Telegram/SourceFiles/data/data_file_origin.h
Normal file
@@ -0,0 +1,229 @@
|
||||
/*
|
||||
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/variant.h"
|
||||
#include "data/data_types.h"
|
||||
|
||||
namespace Data {
|
||||
|
||||
using FileOriginMessage = FullMsgId;
|
||||
using FileOriginStory = FullStoryId;
|
||||
|
||||
struct FileOriginUserPhoto {
|
||||
FileOriginUserPhoto(UserId userId, PhotoId photoId)
|
||||
: userId(userId)
|
||||
, photoId(photoId) {
|
||||
}
|
||||
|
||||
UserId userId = 0;
|
||||
PhotoId photoId = 0;
|
||||
|
||||
inline bool operator<(const FileOriginUserPhoto &other) const {
|
||||
return std::tie(userId, photoId)
|
||||
< std::tie(other.userId, other.photoId);
|
||||
}
|
||||
};
|
||||
|
||||
struct FileOriginFullUser {
|
||||
FileOriginFullUser(UserId userId)
|
||||
: userId(userId) {
|
||||
}
|
||||
|
||||
UserId userId = 0;
|
||||
|
||||
inline bool operator<(const FileOriginFullUser &other) const {
|
||||
return userId < other.userId;
|
||||
}
|
||||
};
|
||||
|
||||
struct FileOriginPeerPhoto {
|
||||
explicit FileOriginPeerPhoto(PeerId peerId) : peerId(peerId) {
|
||||
}
|
||||
|
||||
PeerId peerId = 0;
|
||||
|
||||
inline bool operator<(const FileOriginPeerPhoto &other) const {
|
||||
return peerId < other.peerId;
|
||||
}
|
||||
};
|
||||
|
||||
struct FileOriginStickerSet {
|
||||
FileOriginStickerSet(uint64 setId, uint64 accessHash)
|
||||
: setId(setId)
|
||||
, accessHash(accessHash) {
|
||||
}
|
||||
|
||||
uint64 setId = 0;
|
||||
uint64 accessHash = 0;
|
||||
|
||||
inline bool operator<(const FileOriginStickerSet &other) const {
|
||||
return setId < other.setId;
|
||||
}
|
||||
};
|
||||
|
||||
struct FileOriginSavedGifs {
|
||||
inline bool operator<(const FileOriginSavedGifs &) const {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
struct FileOriginWallpaper {
|
||||
FileOriginWallpaper(
|
||||
uint64 paperId,
|
||||
uint64 accessHash,
|
||||
UserId ownerId,
|
||||
const QString &slug)
|
||||
: paperId(paperId)
|
||||
, accessHash(accessHash)
|
||||
, ownerId(ownerId)
|
||||
, slug(slug) {
|
||||
}
|
||||
|
||||
uint64 paperId = 0;
|
||||
uint64 accessHash = 0;
|
||||
UserId ownerId = 0;
|
||||
QString slug;
|
||||
|
||||
inline bool operator<(const FileOriginWallpaper &other) const {
|
||||
return paperId < other.paperId;
|
||||
}
|
||||
};
|
||||
|
||||
struct FileOriginTheme {
|
||||
FileOriginTheme(uint64 themeId, uint64 accessHash)
|
||||
: themeId(themeId)
|
||||
, accessHash(accessHash) {
|
||||
}
|
||||
|
||||
uint64 themeId = 0;
|
||||
uint64 accessHash = 0;
|
||||
|
||||
inline bool operator<(const FileOriginTheme &other) const {
|
||||
return themeId < other.themeId;
|
||||
}
|
||||
};
|
||||
|
||||
struct FileOriginRingtones {
|
||||
inline bool operator<(const FileOriginRingtones &) const {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
struct FileOriginPremiumPreviews {
|
||||
inline bool operator<(const FileOriginPremiumPreviews &) const {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
struct FileOriginWebPage {
|
||||
QString url;
|
||||
|
||||
inline bool operator<(const FileOriginWebPage &other) const {
|
||||
return url < other.url;
|
||||
}
|
||||
};
|
||||
|
||||
struct FileOrigin {
|
||||
using Variant = std::variant<
|
||||
v::null_t,
|
||||
FileOriginMessage,
|
||||
FileOriginUserPhoto,
|
||||
FileOriginFullUser,
|
||||
FileOriginPeerPhoto,
|
||||
FileOriginStickerSet,
|
||||
FileOriginSavedGifs,
|
||||
FileOriginWallpaper,
|
||||
FileOriginTheme,
|
||||
FileOriginRingtones,
|
||||
FileOriginPremiumPreviews,
|
||||
FileOriginWebPage,
|
||||
FileOriginStory>;
|
||||
|
||||
FileOrigin() = default;
|
||||
FileOrigin(FileOriginMessage data) : data(data) {
|
||||
}
|
||||
FileOrigin(FileOriginUserPhoto data) : data(data) {
|
||||
}
|
||||
FileOrigin(FileOriginFullUser data) : data(data) {
|
||||
}
|
||||
FileOrigin(FileOriginPeerPhoto data) : data(data) {
|
||||
}
|
||||
FileOrigin(FileOriginStickerSet data) : data(data) {
|
||||
}
|
||||
FileOrigin(FileOriginSavedGifs data) : data(data) {
|
||||
}
|
||||
FileOrigin(FileOriginWallpaper data) : data(data) {
|
||||
}
|
||||
FileOrigin(FileOriginTheme data) : data(data) {
|
||||
}
|
||||
FileOrigin(FileOriginRingtones data) : data(data) {
|
||||
}
|
||||
FileOrigin(FileOriginPremiumPreviews data) : data(data) {
|
||||
}
|
||||
FileOrigin(FileOriginWebPage data) : data(data) {
|
||||
}
|
||||
FileOrigin(FileOriginStory data) : data(data) {
|
||||
}
|
||||
|
||||
explicit operator bool() const {
|
||||
return !v::is_null(data);
|
||||
}
|
||||
inline bool operator<(const FileOrigin &other) const {
|
||||
return data < other.data;
|
||||
}
|
||||
|
||||
Variant data;
|
||||
};
|
||||
|
||||
struct DocumentFileLocationId {
|
||||
uint64 id = 0;
|
||||
};
|
||||
|
||||
inline bool operator<(DocumentFileLocationId a, DocumentFileLocationId b) {
|
||||
return a.id < b.id;
|
||||
}
|
||||
|
||||
struct PhotoFileLocationId {
|
||||
uint64 id = 0;
|
||||
};
|
||||
|
||||
inline bool operator<(PhotoFileLocationId a, PhotoFileLocationId b) {
|
||||
return a.id < b.id;
|
||||
}
|
||||
|
||||
using FileLocationId = std::variant<
|
||||
DocumentFileLocationId,
|
||||
PhotoFileLocationId>;
|
||||
|
||||
struct UpdatedFileReferences {
|
||||
std::map<FileLocationId, QByteArray> data;
|
||||
};
|
||||
|
||||
UpdatedFileReferences GetFileReferences(const MTPmessages_Messages &data);
|
||||
UpdatedFileReferences GetFileReferences(const MTPphotos_Photos &data);
|
||||
UpdatedFileReferences GetFileReferences(const MTPusers_UserFull &data);
|
||||
UpdatedFileReferences GetFileReferences(
|
||||
const MTPmessages_RecentStickers &data);
|
||||
UpdatedFileReferences GetFileReferences(
|
||||
const MTPmessages_FavedStickers &data);
|
||||
UpdatedFileReferences GetFileReferences(const MTPmessages_StickerSet &data);
|
||||
UpdatedFileReferences GetFileReferences(const MTPmessages_SavedGifs &data);
|
||||
UpdatedFileReferences GetFileReferences(const MTPWallPaper &data);
|
||||
UpdatedFileReferences GetFileReferences(const MTPTheme &data);
|
||||
UpdatedFileReferences GetFileReferences(
|
||||
const MTPaccount_SavedRingtones &data);
|
||||
UpdatedFileReferences GetFileReferences(const MTPhelp_PremiumPromo &data);
|
||||
UpdatedFileReferences GetFileReferences(const MTPmessages_WebPage &data);
|
||||
UpdatedFileReferences GetFileReferences(const MTPstories_Stories &data);
|
||||
UpdatedFileReferences GetFileReferences(const MTPusers_SavedMusic &data);
|
||||
|
||||
// Admin Log Event.
|
||||
UpdatedFileReferences GetFileReferences(const MTPMessageMedia &data);
|
||||
|
||||
} // namespace Data
|
||||
83
Telegram/SourceFiles/data/data_flags.h
Normal file
83
Telegram/SourceFiles/data/data_flags.h
Normal file
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <rpl/event_stream.h>
|
||||
|
||||
namespace Data {
|
||||
|
||||
template <typename FlagsType>
|
||||
using FlagsUnderlying = typename FlagsType::Type;
|
||||
|
||||
template <
|
||||
typename FlagsType,
|
||||
FlagsUnderlying<FlagsType> kEssential = FlagsUnderlying<FlagsType>(-1)>
|
||||
class Flags {
|
||||
public:
|
||||
using Type = FlagsType;
|
||||
using Enum = typename Type::Enum;
|
||||
|
||||
struct Change {
|
||||
using Type = FlagsType;
|
||||
using Enum = typename Type::Enum;
|
||||
|
||||
Change(Type diff, Type value)
|
||||
: diff(diff)
|
||||
, value(value) {
|
||||
}
|
||||
Type diff = 0;
|
||||
Type value = 0;
|
||||
};
|
||||
|
||||
Flags() = default;
|
||||
Flags(Type value) : _value(value) {
|
||||
}
|
||||
|
||||
void set(Type which) {
|
||||
if (auto diff = which ^ _value) {
|
||||
_value = which;
|
||||
updated(diff);
|
||||
}
|
||||
}
|
||||
void add(Type which) {
|
||||
if (auto diff = which & ~_value) {
|
||||
_value |= which;
|
||||
updated(diff);
|
||||
}
|
||||
}
|
||||
void remove(Type which) {
|
||||
if (auto diff = which & _value) {
|
||||
_value &= ~which;
|
||||
updated(diff);
|
||||
}
|
||||
}
|
||||
auto current() const {
|
||||
return _value;
|
||||
}
|
||||
auto changes() const {
|
||||
return _changes.events();
|
||||
}
|
||||
auto value() const {
|
||||
return _changes.events_starting_with({
|
||||
Type::from_raw(kEssential),
|
||||
_value });
|
||||
}
|
||||
|
||||
private:
|
||||
void updated(Type diff) {
|
||||
if ((diff &= Type::from_raw(kEssential))) {
|
||||
_changes.fire({ diff, _value });
|
||||
}
|
||||
}
|
||||
|
||||
Type _value = 0;
|
||||
rpl::event_stream<Change> _changes;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Data
|
||||
422
Telegram/SourceFiles/data/data_folder.cpp
Normal file
422
Telegram/SourceFiles/data/data_folder.cpp
Normal file
@@ -0,0 +1,422 @@
|
||||
/*
|
||||
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/data_folder.h"
|
||||
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_histories.h"
|
||||
#include "data/data_changes.h"
|
||||
#include "dialogs/dialogs_key.h"
|
||||
#include "dialogs/ui/dialogs_layout.h"
|
||||
#include "history/history.h"
|
||||
#include "history/history_item.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/text/text_options.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "storage/storage_facade.h"
|
||||
#include "core/application.h"
|
||||
#include "core/core_settings.h"
|
||||
#include "main/main_account.h"
|
||||
#include "main/main_session.h"
|
||||
#include "mtproto/mtproto_config.h"
|
||||
#include "apiwrap.h"
|
||||
#include "mainwidget.h"
|
||||
#include "styles/style_dialogs.h"
|
||||
|
||||
namespace Data {
|
||||
namespace {
|
||||
|
||||
constexpr auto kLoadedChatsMinCount = 20;
|
||||
constexpr auto kShowChatNamesCount = 8;
|
||||
|
||||
[[nodiscard]] TextWithEntities ComposeFolderListEntryText(
|
||||
not_null<Folder*> folder) {
|
||||
const auto &list = folder->lastHistories();
|
||||
if (list.empty()) {
|
||||
if (const auto storiesUnread = folder->storiesUnreadCount()) {
|
||||
return {
|
||||
tr::lng_contacts_stories_status_new(
|
||||
tr::now,
|
||||
lt_count,
|
||||
storiesUnread),
|
||||
};
|
||||
} else if (const auto storiesCount = folder->storiesCount()) {
|
||||
return {
|
||||
tr::lng_contacts_stories_status(
|
||||
tr::now,
|
||||
lt_count,
|
||||
storiesCount),
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto count = std::max(
|
||||
int(list.size()),
|
||||
folder->chatsList()->fullSize().current());
|
||||
|
||||
const auto throwAwayLastName = (list.size() > 1)
|
||||
&& (count == list.size() + 1);
|
||||
auto &&peers = ranges::views::all(
|
||||
list
|
||||
) | ranges::views::take(
|
||||
list.size() - (throwAwayLastName ? 1 : 0)
|
||||
);
|
||||
const auto wrapName = [](not_null<History*> history) {
|
||||
const auto name = history->peer->name();
|
||||
return st::wrap_rtl(TextWithEntities{
|
||||
.text = name,
|
||||
.entities = (history->chatListBadgesState().unread
|
||||
? EntitiesInText{
|
||||
{ EntityType::Semibold, 0, int(name.size()), QString() },
|
||||
{ EntityType::Colorized, 0, int(name.size()), QString() },
|
||||
}
|
||||
: EntitiesInText{}),
|
||||
});
|
||||
};
|
||||
const auto shown = int(peers.size());
|
||||
const auto accumulated = [&] {
|
||||
Expects(shown > 0);
|
||||
|
||||
auto i = peers.begin();
|
||||
auto result = wrapName(*i);
|
||||
for (++i; i != peers.end(); ++i) {
|
||||
result = tr::lng_archived_last_list(
|
||||
tr::now,
|
||||
lt_accumulated,
|
||||
result,
|
||||
lt_chat,
|
||||
wrapName(*i),
|
||||
tr::marked);
|
||||
}
|
||||
return result;
|
||||
}();
|
||||
return (shown < count)
|
||||
? tr::lng_archived_last(
|
||||
tr::now,
|
||||
lt_count,
|
||||
(count - shown),
|
||||
lt_chats,
|
||||
accumulated,
|
||||
tr::marked)
|
||||
: accumulated;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Folder::Folder(not_null<Session*> owner, FolderId id)
|
||||
: Entry(owner, Type::Folder)
|
||||
, _id(id)
|
||||
, _chatsList(
|
||||
&owner->session(),
|
||||
FilterId(),
|
||||
owner->maxPinnedChatsLimitValue(this))
|
||||
, _name(tr::lng_archived_name(tr::now)) {
|
||||
indexNameParts();
|
||||
|
||||
session().changes().peerUpdates(
|
||||
PeerUpdate::Flag::Name
|
||||
) | rpl::filter([=](const PeerUpdate &update) {
|
||||
return ranges::contains(_lastHistories, update.peer, &History::peer);
|
||||
}) | rpl::on_next([=] {
|
||||
++_chatListViewVersion;
|
||||
updateChatListEntryPostponed();
|
||||
}, _lifetime);
|
||||
|
||||
_chatsList.setAllAreMuted(true);
|
||||
|
||||
_chatsList.unreadStateChanges(
|
||||
) | rpl::filter([=] {
|
||||
return inChatList();
|
||||
}) | rpl::on_next([=](const Dialogs::UnreadState &old) {
|
||||
++_chatListViewVersion;
|
||||
notifyUnreadStateChange(old);
|
||||
}, _lifetime);
|
||||
|
||||
_chatsList.fullSize().changes(
|
||||
) | rpl::on_next([=] {
|
||||
updateChatListEntryPostponed();
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
FolderId Folder::id() const {
|
||||
return _id;
|
||||
}
|
||||
|
||||
void Folder::indexNameParts() {
|
||||
// We don't want archive to be filtered in the chats list.
|
||||
}
|
||||
|
||||
void Folder::registerOne(not_null<History*> history) {
|
||||
if (_chatsList.indexed()->size() == 1) {
|
||||
updateChatListSortPosition();
|
||||
if (!_chatsList.cloudUnreadKnown()) {
|
||||
owner().histories().requestDialogEntry(this);
|
||||
}
|
||||
} else {
|
||||
updateChatListEntry();
|
||||
}
|
||||
reorderLastHistories();
|
||||
}
|
||||
|
||||
void Folder::unregisterOne(not_null<History*> history) {
|
||||
if (_chatsList.empty()) {
|
||||
updateChatListExistence();
|
||||
}
|
||||
reorderLastHistories();
|
||||
}
|
||||
|
||||
int Folder::chatListNameVersion() const {
|
||||
return 1;
|
||||
}
|
||||
|
||||
void Folder::oneListMessageChanged(HistoryItem *from, HistoryItem *to) {
|
||||
if (from || to) {
|
||||
reorderLastHistories();
|
||||
}
|
||||
}
|
||||
|
||||
void Folder::reorderLastHistories() {
|
||||
// We want first kShowChatNamesCount histories, by last message date.
|
||||
const auto pred = [](not_null<History*> a, not_null<History*> b) {
|
||||
const auto aItem = a->chatListMessage();
|
||||
const auto bItem = b->chatListMessage();
|
||||
const auto aDate = aItem ? aItem->date() : TimeId(0);
|
||||
const auto bDate = bItem ? bItem->date() : TimeId(0);
|
||||
return aDate > bDate;
|
||||
};
|
||||
_lastHistories.clear();
|
||||
_lastHistories.reserve(kShowChatNamesCount + 1);
|
||||
auto &&histories = ranges::views::all(
|
||||
*_chatsList.indexed()
|
||||
) | ranges::views::transform([](not_null<Dialogs::Row*> row) {
|
||||
return row->history();
|
||||
}) | ranges::views::filter([](History *history) {
|
||||
return (history != nullptr);
|
||||
});
|
||||
auto nonPinnedChecked = 0;
|
||||
for (const auto history : histories) {
|
||||
const auto i = ranges::upper_bound(
|
||||
_lastHistories,
|
||||
not_null(history),
|
||||
pred);
|
||||
if (size(_lastHistories) < kShowChatNamesCount
|
||||
|| i != end(_lastHistories)) {
|
||||
_lastHistories.insert(i, history);
|
||||
}
|
||||
if (size(_lastHistories) > kShowChatNamesCount) {
|
||||
_lastHistories.pop_back();
|
||||
}
|
||||
if (!history->isPinnedDialog(FilterId())
|
||||
&& ++nonPinnedChecked >= kShowChatNamesCount) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
++_chatListViewVersion;
|
||||
updateChatListEntry();
|
||||
}
|
||||
|
||||
not_null<Dialogs::MainList*> Folder::chatsList() {
|
||||
return &_chatsList;
|
||||
}
|
||||
|
||||
void Folder::clearChatsList() {
|
||||
_chatsList.clear();
|
||||
}
|
||||
|
||||
void Folder::chatListPreloadData() {
|
||||
}
|
||||
|
||||
void Folder::paintUserpic(
|
||||
Painter &p,
|
||||
Ui::PeerUserpicView &view,
|
||||
const Dialogs::Ui::PaintContext &context) const {
|
||||
paintUserpic(
|
||||
p,
|
||||
context.st->padding.left(),
|
||||
context.st->padding.top(),
|
||||
context.st->photoSize);
|
||||
}
|
||||
|
||||
void Folder::paintUserpic(Painter &p, int x, int y, int size) const {
|
||||
paintUserpic(p, x, y, size, nullptr, nullptr);
|
||||
}
|
||||
|
||||
void Folder::paintUserpic(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int size,
|
||||
const style::color &bg,
|
||||
const style::color &fg) const {
|
||||
paintUserpic(p, x, y, size, &bg, &fg);
|
||||
}
|
||||
|
||||
void Folder::paintUserpic(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int size,
|
||||
const style::color *overrideBg,
|
||||
const style::color *overrideFg) const {
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(overrideBg ? *overrideBg : st::historyPeerArchiveUserpicBg);
|
||||
{
|
||||
PainterHighQualityEnabler hq(p);
|
||||
p.drawEllipse(x, y, size, size);
|
||||
}
|
||||
if (size == st::defaultDialogRow.photoSize) {
|
||||
const auto rect = QRect{ x, y, size, size };
|
||||
if (overrideFg) {
|
||||
st::dialogsArchiveUserpic.paintInCenter(
|
||||
p,
|
||||
rect,
|
||||
(*overrideFg)->c);
|
||||
} else {
|
||||
st::dialogsArchiveUserpic.paintInCenter(p, rect);
|
||||
}
|
||||
} else {
|
||||
p.save();
|
||||
const auto ratio = size / float64(st::defaultDialogRow.photoSize);
|
||||
p.translate(x + size / 2., y + size / 2.);
|
||||
p.scale(ratio, ratio);
|
||||
const auto skip = st::defaultDialogRow.photoSize;
|
||||
const auto rect = QRect{ -skip, -skip, 2 * skip, 2 * skip };
|
||||
if (overrideFg) {
|
||||
st::dialogsArchiveUserpic.paintInCenter(
|
||||
p,
|
||||
rect,
|
||||
(*overrideFg)->c);
|
||||
} else {
|
||||
st::dialogsArchiveUserpic.paintInCenter(p, rect);
|
||||
}
|
||||
p.restore();
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<not_null<History*>> &Folder::lastHistories() const {
|
||||
return _lastHistories;
|
||||
}
|
||||
|
||||
void Folder::validateListEntryCache() {
|
||||
if (_listEntryCacheVersion == _chatListViewVersion) {
|
||||
return;
|
||||
}
|
||||
_listEntryCacheVersion = _chatListViewVersion;
|
||||
_listEntryCache.setMarkedText(
|
||||
st::dialogsTextStyle,
|
||||
ComposeFolderListEntryText(this),
|
||||
// Use rich options as long as the entry text does not have user text.
|
||||
Ui::ItemTextDefaultOptions());
|
||||
}
|
||||
|
||||
void Folder::updateStoriesCount(int count, int unread) {
|
||||
if (_storiesCount == count && _storiesUnreadCount == unread) {
|
||||
return;
|
||||
}
|
||||
const auto limit = (1 << 16) - 1;
|
||||
const auto was = (_storiesCount > 0);
|
||||
_storiesCount = std::min(count, limit);
|
||||
_storiesUnreadCount = std::min(unread, limit);
|
||||
const auto now = (_storiesCount > 0);
|
||||
if (was == now) {
|
||||
updateChatListEntryPostponed();
|
||||
} else if (now) {
|
||||
updateChatListSortPosition();
|
||||
} else {
|
||||
updateChatListExistence();
|
||||
}
|
||||
++_chatListViewVersion;
|
||||
}
|
||||
|
||||
int Folder::storiesCount() const {
|
||||
return _storiesCount;
|
||||
}
|
||||
|
||||
int Folder::storiesUnreadCount() const {
|
||||
return _storiesUnreadCount;
|
||||
}
|
||||
|
||||
TimeId Folder::adjustedChatListTimeId() const {
|
||||
return chatListTimeId();
|
||||
}
|
||||
|
||||
void Folder::applyDialog(const MTPDdialogFolder &data) {
|
||||
_chatsList.updateCloudUnread(data);
|
||||
if (const auto peerId = peerFromMTP(data.vpeer())) {
|
||||
const auto history = owner().history(peerId);
|
||||
const auto fullId = FullMsgId(peerId, data.vtop_message().v);
|
||||
history->setFolder(this, owner().message(fullId));
|
||||
} else {
|
||||
_chatsList.clear();
|
||||
updateChatListExistence();
|
||||
}
|
||||
if (_chatsList.indexed()->size() < kLoadedChatsMinCount) {
|
||||
session().api().requestDialogs(this);
|
||||
}
|
||||
}
|
||||
|
||||
void Folder::applyPinnedUpdate(const MTPDupdateDialogPinned &data) {
|
||||
const auto folderId = data.vfolder_id().value_or_empty();
|
||||
if (folderId != 0) {
|
||||
LOG(("API Error: Nested folders detected."));
|
||||
}
|
||||
owner().setChatPinned(this, FilterId(), data.is_pinned());
|
||||
}
|
||||
|
||||
int Folder::fixedOnTopIndex() const {
|
||||
return kArchiveFixOnTopIndex;
|
||||
}
|
||||
|
||||
bool Folder::shouldBeInChatList() const {
|
||||
return !_chatsList.empty() || (_storiesCount > 0);
|
||||
}
|
||||
|
||||
Dialogs::UnreadState Folder::chatListUnreadState() const {
|
||||
return _chatsList.unreadState();
|
||||
}
|
||||
|
||||
Dialogs::BadgesState Folder::chatListBadgesState() const {
|
||||
auto result = Dialogs::BadgesForUnread(
|
||||
chatListUnreadState(),
|
||||
Dialogs::CountInBadge::Chats,
|
||||
Dialogs::IncludeInBadge::All);
|
||||
result.unreadMuted = result.mentionMuted = result.reactionMuted = true;
|
||||
if (result.unread && !result.unreadCounter) {
|
||||
result.unreadCounter = 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
HistoryItem *Folder::chatListMessage() const {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool Folder::chatListMessageKnown() const {
|
||||
return true;
|
||||
}
|
||||
|
||||
const QString &Folder::chatListName() const {
|
||||
return _name;
|
||||
}
|
||||
|
||||
const base::flat_set<QString> &Folder::chatListNameWords() const {
|
||||
return _nameWords;
|
||||
}
|
||||
|
||||
const base::flat_set<QChar> &Folder::chatListFirstLetters() const {
|
||||
return _nameFirstLetters;
|
||||
}
|
||||
|
||||
const QString &Folder::chatListNameSortKey() const {
|
||||
static const auto empty = QString();
|
||||
return empty;
|
||||
}
|
||||
|
||||
} // namespace Data
|
||||
116
Telegram/SourceFiles/data/data_folder.h
Normal file
116
Telegram/SourceFiles/data/data_folder.h
Normal file
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
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 "dialogs/dialogs_entry.h"
|
||||
#include "dialogs/dialogs_main_list.h"
|
||||
#include "data/data_messages.h"
|
||||
#include "base/weak_ptr.h"
|
||||
|
||||
class ChannelData;
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace Data {
|
||||
|
||||
class Session;
|
||||
|
||||
class Folder final : public Dialogs::Entry {
|
||||
public:
|
||||
static constexpr auto kId = 1;
|
||||
|
||||
Folder(not_null<Data::Session*> owner, FolderId id);
|
||||
Folder(const Folder &) = delete;
|
||||
Folder &operator=(const Folder &) = delete;
|
||||
|
||||
[[nodiscard]] FolderId id() const;
|
||||
void registerOne(not_null<History*> history);
|
||||
void unregisterOne(not_null<History*> history);
|
||||
void oneListMessageChanged(HistoryItem *from, HistoryItem *to);
|
||||
|
||||
void clearChatsList();
|
||||
[[nodiscard]] not_null<Dialogs::MainList*> chatsList();
|
||||
|
||||
void applyDialog(const MTPDdialogFolder &data);
|
||||
void applyPinnedUpdate(const MTPDupdateDialogPinned &data);
|
||||
|
||||
TimeId adjustedChatListTimeId() const override;
|
||||
|
||||
int fixedOnTopIndex() const override;
|
||||
bool shouldBeInChatList() const override;
|
||||
Dialogs::UnreadState chatListUnreadState() const override;
|
||||
Dialogs::BadgesState chatListBadgesState() const override;
|
||||
HistoryItem *chatListMessage() const override;
|
||||
bool chatListMessageKnown() const override;
|
||||
const QString &chatListName() const override;
|
||||
const QString &chatListNameSortKey() const override;
|
||||
int chatListNameVersion() const override;
|
||||
const base::flat_set<QString> &chatListNameWords() const override;
|
||||
const base::flat_set<QChar> &chatListFirstLetters() const override;
|
||||
|
||||
void chatListPreloadData() override;
|
||||
void paintUserpic(
|
||||
Painter &p,
|
||||
Ui::PeerUserpicView &view,
|
||||
const Dialogs::Ui::PaintContext &context) const override;
|
||||
void paintUserpic(Painter &p, int x, int y, int size) const;
|
||||
void paintUserpic(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int size,
|
||||
const style::color &overrideBg,
|
||||
const style::color &overrideFg) const;
|
||||
|
||||
const std::vector<not_null<History*>> &lastHistories() const;
|
||||
void validateListEntryCache();
|
||||
[[nodiscard]] const Ui::Text::String &listEntryCache() const {
|
||||
return _listEntryCache;
|
||||
}
|
||||
|
||||
void updateStoriesCount(int count, int unread);
|
||||
[[nodiscard]] int storiesCount() const;
|
||||
[[nodiscard]] int storiesUnreadCount() const;
|
||||
|
||||
private:
|
||||
void indexNameParts();
|
||||
|
||||
void reorderLastHistories();
|
||||
|
||||
void paintUserpic(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int size,
|
||||
const style::color *overrideBg,
|
||||
const style::color *overrideFg) const;
|
||||
|
||||
const FolderId _id = 0;
|
||||
Dialogs::MainList _chatsList;
|
||||
|
||||
QString _name;
|
||||
base::flat_set<QString> _nameWords;
|
||||
base::flat_set<QChar> _nameFirstLetters;
|
||||
|
||||
std::vector<not_null<History*>> _lastHistories;
|
||||
|
||||
Ui::Text::String _listEntryCache;
|
||||
int _listEntryCacheVersion = 0;
|
||||
int _chatListViewVersion = 0;
|
||||
//rpl::variable<MessagePosition> _unreadPosition;
|
||||
|
||||
uint16_t _storiesCount = 0;
|
||||
uint16_t _storiesUnreadCount = 0;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Data
|
||||
604
Telegram/SourceFiles/data/data_forum.cpp
Normal file
604
Telegram/SourceFiles/data/data_forum.cpp
Normal file
@@ -0,0 +1,604 @@
|
||||
/*
|
||||
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/data_forum.h"
|
||||
|
||||
#include "data/components/recent_peers.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_histories.h"
|
||||
#include "data/data_changes.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_forum_icons.h"
|
||||
#include "data/data_forum_topic.h"
|
||||
#include "data/data_replies_list.h"
|
||||
#include "data/data_user.h"
|
||||
#include "data/notify/data_notify_settings.h"
|
||||
#include "history/history.h"
|
||||
#include "history/history_item.h"
|
||||
#include "history/history_unread_things.h"
|
||||
#include "main/main_session.h"
|
||||
#include "base/random.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "apiwrap.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "core/application.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "storage/storage_facade.h"
|
||||
#include "storage/storage_shared_media.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "window/notifications_manager.h"
|
||||
#include "styles/style_boxes.h"
|
||||
|
||||
namespace Data {
|
||||
namespace {
|
||||
|
||||
constexpr auto kTopicsFirstLoad = 20;
|
||||
constexpr auto kLoadedTopicsMinCount = 20;
|
||||
constexpr auto kTopicsPerPage = 500;
|
||||
constexpr auto kStalePerRequest = 100;
|
||||
constexpr auto kShowTopicNamesCount = 8;
|
||||
// constexpr auto kGeneralColorId = 0xA9A9A9;
|
||||
|
||||
} // namespace
|
||||
|
||||
Forum::Forum(not_null<History*> history)
|
||||
: _history(history)
|
||||
, _topicsList(&session(), {}, owner().maxPinnedChatsLimitValue(this)) {
|
||||
Expects(_history->peer->isChannel()
|
||||
|| _history->peer->isBot());
|
||||
|
||||
if (_history->inChatList()) {
|
||||
preloadTopics();
|
||||
}
|
||||
if (peer()->canCreateTopics()) {
|
||||
owner().forumIcons().requestDefaultIfUnknown();
|
||||
}
|
||||
}
|
||||
|
||||
Forum::~Forum() {
|
||||
for (const auto &request : _topicRequests) {
|
||||
if (request.second.id != _staleRequestId) {
|
||||
owner().histories().cancelRequest(request.second.id);
|
||||
}
|
||||
}
|
||||
if (_staleRequestId) {
|
||||
session().api().request(_staleRequestId).cancel();
|
||||
}
|
||||
if (_requestId) {
|
||||
session().api().request(_requestId).cancel();
|
||||
}
|
||||
auto &storage = session().storage();
|
||||
auto &changes = session().changes();
|
||||
const auto peerId = _history->peer->id;
|
||||
for (const auto &[rootId, topic] : _topics) {
|
||||
storage.unload(Storage::SharedMediaUnloadThread(
|
||||
peerId,
|
||||
rootId,
|
||||
PeerId()));
|
||||
_history->setForwardDraft(rootId, PeerId(), {});
|
||||
|
||||
const auto raw = topic.get();
|
||||
changes.topicRemoved(raw);
|
||||
changes.entryRemoved(raw);
|
||||
}
|
||||
}
|
||||
|
||||
Session &Forum::owner() const {
|
||||
return _history->owner();
|
||||
}
|
||||
|
||||
Main::Session &Forum::session() const {
|
||||
return _history->session();
|
||||
}
|
||||
|
||||
not_null<History*> Forum::history() const {
|
||||
return _history;
|
||||
}
|
||||
|
||||
not_null<PeerData*> Forum::peer() const {
|
||||
return _history->peer;
|
||||
}
|
||||
|
||||
UserData *Forum::bot() const {
|
||||
return _history->peer->asBot();
|
||||
}
|
||||
|
||||
ChannelData *Forum::channel() const {
|
||||
return _history->peer->asChannel();
|
||||
}
|
||||
|
||||
not_null<Dialogs::MainList*> Forum::topicsList() {
|
||||
return &_topicsList;
|
||||
}
|
||||
|
||||
rpl::producer<> Forum::destroyed() const {
|
||||
if (const auto bot = this->bot()) {
|
||||
return bot->flagsValue(
|
||||
) | rpl::filter([=](const UserData::Flags::Change &update) {
|
||||
using Flag = UserData::Flag;
|
||||
return (update.diff & Flag::Forum)
|
||||
&& !(update.value & Flag::Forum);
|
||||
}) | rpl::take(1) | rpl::to_empty;
|
||||
}
|
||||
return channel()->flagsValue(
|
||||
) | rpl::filter([=](const ChannelData::Flags::Change &update) {
|
||||
using Flag = ChannelData::Flag;
|
||||
return (update.diff & Flag::Forum) && !(update.value & Flag::Forum);
|
||||
}) | rpl::take(1) | rpl::to_empty;
|
||||
}
|
||||
|
||||
rpl::producer<not_null<ForumTopic*>> Forum::topicDestroyed() const {
|
||||
return _topicDestroyed.events();
|
||||
}
|
||||
|
||||
void Forum::preloadTopics() {
|
||||
if (topicsList()->indexed()->size() < kLoadedTopicsMinCount) {
|
||||
requestTopics();
|
||||
}
|
||||
}
|
||||
|
||||
void Forum::reloadTopics() {
|
||||
_topicsList.setLoaded(false);
|
||||
session().api().request(base::take(_requestId)).cancel();
|
||||
_offset = {};
|
||||
for (const auto &[rootId, topic] : _topics) {
|
||||
if (!topic->creating()) {
|
||||
_staleRootIds.emplace(topic->rootId());
|
||||
}
|
||||
}
|
||||
requestTopics();
|
||||
}
|
||||
|
||||
void Forum::requestTopics() {
|
||||
if (_topicsList.loaded() || _requestId) {
|
||||
return;
|
||||
}
|
||||
const auto firstLoad = !_offset.date;
|
||||
const auto loadCount = firstLoad ? kTopicsFirstLoad : kTopicsPerPage;
|
||||
_requestId = session().api().request(MTPmessages_GetForumTopics(
|
||||
MTP_flags(0),
|
||||
peer()->input(),
|
||||
MTPstring(), // q
|
||||
MTP_int(_offset.date),
|
||||
MTP_int(_offset.id),
|
||||
MTP_int(_offset.topicId),
|
||||
MTP_int(loadCount)
|
||||
)).done([=](const MTPmessages_ForumTopics &result) {
|
||||
const auto previousOffset = _offset;
|
||||
applyReceivedTopics(result, _offset);
|
||||
const auto &list = result.data().vtopics().v;
|
||||
if (list.isEmpty()
|
||||
|| list.size() == result.data().vcount().v
|
||||
|| (_offset == previousOffset)) {
|
||||
_topicsList.setLoaded();
|
||||
}
|
||||
_requestId = 0;
|
||||
_chatsListChanges.fire({});
|
||||
if (_topicsList.loaded()) {
|
||||
_chatsListLoadedEvents.fire({});
|
||||
}
|
||||
reorderLastTopics();
|
||||
requestSomeStale();
|
||||
}).fail([=](const MTP::Error &error) {
|
||||
_requestId = 0;
|
||||
_topicsList.setLoaded();
|
||||
if (error.type() == u"CHANNEL_FORUM_MISSING"_q && channel()) {
|
||||
const auto flags = channel()->flags() & ~ChannelDataFlag::Forum;
|
||||
channel()->setFlags(flags);
|
||||
}
|
||||
}).send();
|
||||
}
|
||||
|
||||
void Forum::applyTopicDeleted(MsgId rootId) {
|
||||
_topicsDeleted.emplace(rootId);
|
||||
|
||||
const auto i = _topics.find(rootId);
|
||||
if (i == end(_topics)) {
|
||||
return;
|
||||
}
|
||||
const auto raw = i->second.get();
|
||||
Core::App().notifications().clearFromTopic(raw);
|
||||
owner().removeChatListEntry(raw);
|
||||
|
||||
if (ranges::contains(_lastTopics, not_null(raw))) {
|
||||
reorderLastTopics();
|
||||
}
|
||||
|
||||
if (_activeSubsectionTopic == raw) {
|
||||
_activeSubsectionTopic = nullptr;
|
||||
}
|
||||
_topicDestroyed.fire(raw);
|
||||
_history->session().recentPeers().chatOpenRemove(raw);
|
||||
session().changes().topicUpdated(
|
||||
raw,
|
||||
Data::TopicUpdate::Flag::Destroyed);
|
||||
session().changes().entryUpdated(
|
||||
raw,
|
||||
Data::EntryUpdate::Flag::Destroyed);
|
||||
_topics.erase(i);
|
||||
|
||||
_history->destroyMessagesByTopic(rootId);
|
||||
session().storage().unload(Storage::SharedMediaUnloadThread(
|
||||
_history->peer->id,
|
||||
rootId,
|
||||
PeerId()));
|
||||
_history->setForwardDraft(rootId, PeerId(), {});
|
||||
}
|
||||
|
||||
void Forum::reorderLastTopics() {
|
||||
// We want first kShowTopicNamesCount histories, by last message date.
|
||||
const auto pred = [](not_null<ForumTopic*> a, not_null<ForumTopic*> b) {
|
||||
const auto aItem = a->chatListMessage();
|
||||
const auto bItem = b->chatListMessage();
|
||||
const auto aDate = aItem ? aItem->date() : TimeId(0);
|
||||
const auto bDate = bItem ? bItem->date() : TimeId(0);
|
||||
return aDate > bDate;
|
||||
};
|
||||
_lastTopics.clear();
|
||||
_lastTopics.reserve(kShowTopicNamesCount + 1);
|
||||
auto &&topics = ranges::views::all(
|
||||
*_topicsList.indexed()
|
||||
) | ranges::views::transform([](not_null<Dialogs::Row*> row) {
|
||||
return row->topic();
|
||||
});
|
||||
auto nonPinnedChecked = 0;
|
||||
for (const auto topic : topics) {
|
||||
const auto i = ranges::upper_bound(
|
||||
_lastTopics,
|
||||
not_null(topic),
|
||||
pred);
|
||||
if (size(_lastTopics) < kShowTopicNamesCount
|
||||
|| i != end(_lastTopics)) {
|
||||
_lastTopics.insert(i, topic);
|
||||
}
|
||||
if (size(_lastTopics) > kShowTopicNamesCount) {
|
||||
_lastTopics.pop_back();
|
||||
}
|
||||
if (!topic->isPinnedDialog(FilterId())
|
||||
&& ++nonPinnedChecked >= kShowTopicNamesCount) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
++_lastTopicsVersion;
|
||||
_history->updateChatListEntry();
|
||||
}
|
||||
|
||||
int Forum::recentTopicsListVersion() const {
|
||||
return _lastTopicsVersion;
|
||||
}
|
||||
|
||||
void Forum::recentTopicsInvalidate(not_null<ForumTopic*> topic) {
|
||||
if (ranges::contains(_lastTopics, topic)) {
|
||||
++_lastTopicsVersion;
|
||||
_history->updateChatListEntry();
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<not_null<ForumTopic*>> &Forum::recentTopics() const {
|
||||
return _lastTopics;
|
||||
}
|
||||
|
||||
void Forum::saveActiveSubsectionThread(not_null<Thread*> thread) {
|
||||
if (const auto topic = thread->asTopic()) {
|
||||
Assert(topic->forum() == this);
|
||||
_activeSubsectionTopic = topic->creating() ? nullptr : topic;
|
||||
} else {
|
||||
Assert(thread == history());
|
||||
_activeSubsectionTopic = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
Thread *Forum::activeSubsectionThread() const {
|
||||
return _activeSubsectionTopic;
|
||||
}
|
||||
|
||||
void Forum::markUnreadCountsUnknown(MsgId readTillId) {
|
||||
if (!peer()->useSubsectionTabs()) {
|
||||
return;
|
||||
}
|
||||
for (const auto &[rootId, topic] : _topics) {
|
||||
const auto replies = topic->replies();
|
||||
if (replies->unreadCountCurrent() > 0) {
|
||||
replies->setInboxReadTill(readTillId, std::nullopt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Forum::updateUnreadCounts(
|
||||
MsgId readTillId,
|
||||
const base::flat_map<not_null<ForumTopic*>, int> &counts) {
|
||||
if (!peer()->useSubsectionTabs()) {
|
||||
return;
|
||||
}
|
||||
for (const auto &[rootId, topic] : _topics) {
|
||||
const auto raw = topic.get();
|
||||
const auto replies = raw->replies();
|
||||
const auto i = counts.find(raw);
|
||||
const auto count = (i != end(counts)) ? i->second : 0;
|
||||
replies->setInboxReadTill(readTillId, count);
|
||||
}
|
||||
}
|
||||
|
||||
void Forum::listMessageChanged(HistoryItem *from, HistoryItem *to) {
|
||||
if (from || to) {
|
||||
reorderLastTopics();
|
||||
}
|
||||
}
|
||||
|
||||
void Forum::applyReceivedTopics(
|
||||
const MTPmessages_ForumTopics &topics,
|
||||
ForumOffsets &updateOffsets) {
|
||||
applyReceivedTopics(topics, [&](not_null<ForumTopic*> topic) {
|
||||
if (const auto last = topic->lastServerMessage()) {
|
||||
updateOffsets.date = last->date();
|
||||
updateOffsets.id = last->id;
|
||||
}
|
||||
updateOffsets.topicId = topic->rootId();
|
||||
});
|
||||
}
|
||||
|
||||
void Forum::applyReceivedTopics(
|
||||
const MTPmessages_ForumTopics &topics,
|
||||
Fn<void(not_null<ForumTopic*>)> callback) {
|
||||
const auto &data = topics.data();
|
||||
owner().processUsers(data.vusers());
|
||||
owner().processChats(data.vchats());
|
||||
owner().processMessages(data.vmessages(), NewMessageType::Existing);
|
||||
if (const auto channel = this->channel()) {
|
||||
channel->ptsReceived(data.vpts().v);
|
||||
}
|
||||
applyReceivedTopics(data.vtopics(), std::move(callback));
|
||||
if (!_staleRootIds.empty()) {
|
||||
requestSomeStale();
|
||||
}
|
||||
}
|
||||
|
||||
void Forum::applyReceivedTopics(
|
||||
const MTPVector<MTPForumTopic> &topics,
|
||||
Fn<void(not_null<ForumTopic*>)> callback) {
|
||||
const auto &list = topics.v;
|
||||
for (const auto &topic : list) {
|
||||
const auto rootId = topic.match([&](const auto &data) {
|
||||
return data.vid().v;
|
||||
});
|
||||
_staleRootIds.remove(rootId);
|
||||
topic.match([&](const MTPDforumTopicDeleted &data) {
|
||||
applyTopicDeleted(rootId);
|
||||
}, [&](const MTPDforumTopic &data) {
|
||||
_topicsDeleted.remove(rootId);
|
||||
const auto i = _topics.find(rootId);
|
||||
const auto creating = (i == end(_topics));
|
||||
const auto raw = creating
|
||||
? _topics.emplace(
|
||||
rootId,
|
||||
std::make_unique<ForumTopic>(this, rootId)
|
||||
).first->second.get()
|
||||
: i->second.get();
|
||||
raw->applyTopic(data);
|
||||
if (creating) {
|
||||
if (const auto last = _history->chatListMessage()
|
||||
; last && last->topicRootId() == rootId) {
|
||||
_history->lastItemDialogsView().itemInvalidated(last);
|
||||
_history->updateChatListEntry();
|
||||
}
|
||||
}
|
||||
if (callback) {
|
||||
callback(raw);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void Forum::requestSomeStale() {
|
||||
if (_staleRequestId
|
||||
|| (!_offset.id && _requestId)
|
||||
|| _staleRootIds.empty()) {
|
||||
return;
|
||||
}
|
||||
const auto type = Histories::RequestType::History;
|
||||
auto rootIds = QVector<MTPint>();
|
||||
rootIds.reserve(std::min(int(_staleRootIds.size()), kStalePerRequest));
|
||||
for (auto i = begin(_staleRootIds); i != end(_staleRootIds);) {
|
||||
const auto rootId = *i;
|
||||
i = _staleRootIds.erase(i);
|
||||
|
||||
rootIds.push_back(MTP_int(rootId));
|
||||
if (rootIds.size() == kStalePerRequest) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (rootIds.empty()) {
|
||||
return;
|
||||
}
|
||||
const auto call = [=] {
|
||||
for (const auto &id : rootIds) {
|
||||
finishTopicRequest(id.v);
|
||||
}
|
||||
};
|
||||
auto &histories = owner().histories();
|
||||
_staleRequestId = histories.sendRequest(_history, type, [=](
|
||||
Fn<void()> finish) {
|
||||
return session().api().request(
|
||||
MTPmessages_GetForumTopicsByID(
|
||||
peer()->input(),
|
||||
MTP_vector<MTPint>(rootIds))
|
||||
).done([=](const MTPmessages_ForumTopics &result) {
|
||||
_staleRequestId = 0;
|
||||
applyReceivedTopics(result);
|
||||
call();
|
||||
finish();
|
||||
}).fail([=] {
|
||||
_staleRequestId = 0;
|
||||
call();
|
||||
finish();
|
||||
}).send();
|
||||
});
|
||||
for (const auto &id : rootIds) {
|
||||
_topicRequests[id.v].id = _staleRequestId;
|
||||
}
|
||||
}
|
||||
|
||||
void Forum::finishTopicRequest(MsgId rootId) {
|
||||
if (const auto request = _topicRequests.take(rootId)) {
|
||||
for (const auto &callback : request->callbacks) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Forum::requestTopic(MsgId rootId, Fn<void()> done) {
|
||||
auto &request = _topicRequests[rootId];
|
||||
if (done) {
|
||||
request.callbacks.push_back(std::move(done));
|
||||
}
|
||||
if (!request.id
|
||||
&& _staleRootIds.emplace(rootId).second
|
||||
&& (_staleRootIds.size() == 1)) {
|
||||
crl::on_main(&session(), [peer = peer()] {
|
||||
if (const auto forum = peer->forum()) {
|
||||
forum->requestSomeStale();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ForumTopic *Forum::applyTopicAdded(
|
||||
MsgId rootId,
|
||||
const QString &title,
|
||||
int32 colorId,
|
||||
DocumentId iconId,
|
||||
PeerId creatorId,
|
||||
TimeId date,
|
||||
bool my) {
|
||||
Expects(rootId != 0);
|
||||
|
||||
const auto i = _topics.find(rootId);
|
||||
const auto raw = (i != end(_topics))
|
||||
? i->second.get()
|
||||
: _topics.emplace(
|
||||
rootId,
|
||||
std::make_unique<ForumTopic>(this, rootId)
|
||||
).first->second.get();
|
||||
raw->applyTitle(title);
|
||||
raw->applyColorId(colorId);
|
||||
raw->applyIconId(iconId);
|
||||
raw->applyCreator(creatorId);
|
||||
raw->applyCreationDate(date);
|
||||
raw->applyIsMy(my);
|
||||
if (!creating(rootId)) {
|
||||
raw->addToChatList(FilterId(), topicsList());
|
||||
_chatsListChanges.fire({});
|
||||
reorderLastTopics();
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
MsgId Forum::reserveCreatingId(
|
||||
const QString &title,
|
||||
int32 colorId,
|
||||
DocumentId iconId) {
|
||||
const auto result = owner().nextLocalMessageId();
|
||||
_creatingRootIds.emplace(result);
|
||||
applyTopicAdded(
|
||||
result,
|
||||
title,
|
||||
colorId,
|
||||
iconId,
|
||||
session().userPeerId(),
|
||||
base::unixtime::now(),
|
||||
true);
|
||||
return result;
|
||||
}
|
||||
|
||||
void Forum::discardCreatingId(MsgId rootId) {
|
||||
Expects(creating(rootId));
|
||||
|
||||
const auto i = _topics.find(rootId);
|
||||
if (i != end(_topics)) {
|
||||
Assert(!i->second->inChatList());
|
||||
_topics.erase(i);
|
||||
}
|
||||
_creatingRootIds.remove(rootId);
|
||||
}
|
||||
|
||||
bool Forum::creating(MsgId rootId) const {
|
||||
return _creatingRootIds.contains(rootId);
|
||||
}
|
||||
|
||||
void Forum::created(MsgId rootId, MsgId realId) {
|
||||
if (rootId == realId) {
|
||||
return;
|
||||
}
|
||||
_creatingRootIds.remove(rootId);
|
||||
const auto i = _topics.find(rootId);
|
||||
Assert(i != end(_topics));
|
||||
auto topic = std::move(i->second);
|
||||
_topics.erase(i);
|
||||
const auto id = FullMsgId(_history->peer->id, realId);
|
||||
if (!_topics.contains(realId)) {
|
||||
_topics.emplace(
|
||||
realId,
|
||||
std::move(topic)
|
||||
).first->second->setRealRootId(realId);
|
||||
|
||||
reorderLastTopics();
|
||||
}
|
||||
owner().notifyItemIdChange({ id, rootId });
|
||||
}
|
||||
|
||||
void Forum::clearAllUnreadMentions() {
|
||||
for (const auto &[rootId, topic] : _topics) {
|
||||
topic->unreadMentions().clear();
|
||||
}
|
||||
}
|
||||
|
||||
void Forum::clearAllUnreadReactions() {
|
||||
for (const auto &[rootId, topic] : _topics) {
|
||||
topic->unreadReactions().clear();
|
||||
}
|
||||
}
|
||||
|
||||
void Forum::enumerateTopics(Fn<void(not_null<ForumTopic*>)> action) const {
|
||||
for (const auto &[rootId, topic] : _topics) {
|
||||
action(topic.get());
|
||||
}
|
||||
}
|
||||
|
||||
ForumTopic *Forum::topicFor(MsgId rootId) {
|
||||
if (!rootId) {
|
||||
return nullptr;
|
||||
}
|
||||
const auto i = _topics.find(rootId);
|
||||
return (i != end(_topics)) ? i->second.get() : nullptr;
|
||||
}
|
||||
|
||||
ForumTopic *Forum::enforceTopicFor(MsgId rootId) {
|
||||
Expects(rootId != 0);
|
||||
|
||||
const auto i = _topics.find(rootId);
|
||||
if (i != end(_topics)) {
|
||||
return i->second.get();
|
||||
}
|
||||
requestTopic(rootId);
|
||||
return applyTopicAdded(rootId, {}, {}, {}, {}, {}, {});
|
||||
}
|
||||
|
||||
bool Forum::topicDeleted(MsgId rootId) const {
|
||||
return _topicsDeleted.contains(rootId)
|
||||
|| (rootId == ForumTopic::kGeneralId && peer()->isBot());
|
||||
}
|
||||
|
||||
rpl::producer<> Forum::chatsListChanges() const {
|
||||
return _chatsListChanges.events();
|
||||
}
|
||||
|
||||
rpl::producer<> Forum::chatsListLoadedEvents() const {
|
||||
return _chatsListLoadedEvents.events();
|
||||
}
|
||||
|
||||
} // namespace Data
|
||||
151
Telegram/SourceFiles/data/data_forum.h
Normal file
151
Telegram/SourceFiles/data/data_forum.h
Normal file
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
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 "dialogs/dialogs_main_list.h"
|
||||
|
||||
class History;
|
||||
class ChannelData;
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace Window {
|
||||
class SessionController;
|
||||
} // namespace Window;
|
||||
|
||||
namespace Data {
|
||||
|
||||
class Session;
|
||||
|
||||
struct ForumOffsets {
|
||||
TimeId date = 0;
|
||||
MsgId id = 0;
|
||||
MsgId topicId = 0;
|
||||
|
||||
friend inline constexpr auto operator<=>(
|
||||
ForumOffsets,
|
||||
ForumOffsets) = default;
|
||||
};
|
||||
|
||||
class Forum final {
|
||||
public:
|
||||
explicit Forum(not_null<History*> history);
|
||||
~Forum();
|
||||
|
||||
[[nodiscard]] Session &owner() const;
|
||||
[[nodiscard]] Main::Session &session() const;
|
||||
[[nodiscard]] not_null<PeerData*> peer() const;
|
||||
[[nodiscard]] not_null<History*> history() const;
|
||||
[[nodiscard]] UserData *bot() const;
|
||||
[[nodiscard]] ChannelData *channel() const;
|
||||
[[nodiscard]] not_null<Dialogs::MainList*> topicsList();
|
||||
[[nodiscard]] rpl::producer<> destroyed() const;
|
||||
[[nodiscard]] auto topicDestroyed() const
|
||||
-> rpl::producer<not_null<ForumTopic*>>;
|
||||
|
||||
void preloadTopics();
|
||||
void reloadTopics();
|
||||
void requestTopics();
|
||||
[[nodiscard]] rpl::producer<> chatsListChanges() const;
|
||||
[[nodiscard]] rpl::producer<> chatsListLoadedEvents() const;
|
||||
|
||||
void requestTopic(MsgId rootId, Fn<void()> done = nullptr);
|
||||
ForumTopic *applyTopicAdded(
|
||||
MsgId rootId,
|
||||
const QString &title,
|
||||
int32 colorId,
|
||||
DocumentId iconId,
|
||||
PeerId creatorId,
|
||||
TimeId date,
|
||||
bool my);
|
||||
void applyTopicDeleted(MsgId rootId);
|
||||
[[nodiscard]] ForumTopic *topicFor(MsgId rootId);
|
||||
[[nodiscard]] ForumTopic *enforceTopicFor(MsgId rootId);
|
||||
[[nodiscard]] bool topicDeleted(MsgId rootId) const;
|
||||
|
||||
void applyReceivedTopics(
|
||||
const MTPmessages_ForumTopics &topics,
|
||||
ForumOffsets &updateOffsets);
|
||||
void applyReceivedTopics(
|
||||
const MTPmessages_ForumTopics &topics,
|
||||
Fn<void(not_null<ForumTopic*>)> callback = nullptr);
|
||||
void applyReceivedTopics(
|
||||
const MTPVector<MTPForumTopic> &topics,
|
||||
Fn<void(not_null<ForumTopic*>)> callback = nullptr);
|
||||
|
||||
[[nodiscard]] MsgId reserveCreatingId(
|
||||
const QString &title,
|
||||
int32 colorId,
|
||||
DocumentId iconId);
|
||||
void discardCreatingId(MsgId rootId);
|
||||
[[nodiscard]] bool creating(MsgId rootId) const;
|
||||
void created(MsgId rootId, MsgId realId);
|
||||
|
||||
void clearAllUnreadMentions();
|
||||
void clearAllUnreadReactions();
|
||||
void enumerateTopics(Fn<void(not_null<ForumTopic*>)> action) const;
|
||||
|
||||
void listMessageChanged(HistoryItem *from, HistoryItem *to);
|
||||
[[nodiscard]] int recentTopicsListVersion() const;
|
||||
void recentTopicsInvalidate(not_null<ForumTopic*> topic);
|
||||
[[nodiscard]] auto recentTopics() const
|
||||
-> const std::vector<not_null<ForumTopic*>> &;
|
||||
|
||||
void saveActiveSubsectionThread(not_null<Thread*> thread);
|
||||
[[nodiscard]] Thread *activeSubsectionThread() const;
|
||||
|
||||
void markUnreadCountsUnknown(MsgId readTillId);
|
||||
void updateUnreadCounts(
|
||||
MsgId readTillId,
|
||||
const base::flat_map<not_null<ForumTopic*>, int> &counts);
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime() {
|
||||
return _lifetime;
|
||||
}
|
||||
|
||||
private:
|
||||
struct TopicRequest {
|
||||
mtpRequestId id = 0;
|
||||
std::vector<Fn<void()>> callbacks;
|
||||
};
|
||||
|
||||
void reorderLastTopics();
|
||||
void requestSomeStale();
|
||||
void finishTopicRequest(MsgId rootId);
|
||||
|
||||
const not_null<History*> _history;
|
||||
|
||||
base::flat_map<MsgId, std::unique_ptr<ForumTopic>> _topics;
|
||||
base::flat_set<MsgId> _topicsDeleted;
|
||||
rpl::event_stream<not_null<ForumTopic*>> _topicDestroyed;
|
||||
Dialogs::MainList _topicsList;
|
||||
|
||||
base::flat_map<MsgId, TopicRequest> _topicRequests;
|
||||
base::flat_set<MsgId> _staleRootIds;
|
||||
mtpRequestId _staleRequestId = 0;
|
||||
|
||||
mtpRequestId _requestId = 0;
|
||||
ForumOffsets _offset;
|
||||
|
||||
base::flat_set<MsgId> _creatingRootIds;
|
||||
|
||||
std::vector<not_null<ForumTopic*>> _lastTopics;
|
||||
int _lastTopicsVersion = 0;
|
||||
|
||||
ForumTopic *_activeSubsectionTopic = nullptr;
|
||||
|
||||
rpl::event_stream<> _chatsListChanges;
|
||||
rpl::event_stream<> _chatsListLoadedEvents;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Data
|
||||
119
Telegram/SourceFiles/data/data_forum_icons.cpp
Normal file
119
Telegram/SourceFiles/data/data_forum_icons.cpp
Normal file
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
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/data_forum_icons.h"
|
||||
|
||||
#include "main/main_session.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_forum.h"
|
||||
#include "data/data_forum_topic.h"
|
||||
#include "apiwrap.h"
|
||||
|
||||
namespace Data {
|
||||
|
||||
ForumIcons::ForumIcons(not_null<Session*> owner)
|
||||
: _owner(owner)
|
||||
, _resetUserpicsTimer([=] { resetUserpics(); }) {
|
||||
}
|
||||
|
||||
ForumIcons::~ForumIcons() = default;
|
||||
|
||||
Main::Session &ForumIcons::session() const {
|
||||
return _owner->session();
|
||||
}
|
||||
|
||||
void ForumIcons::requestDefaultIfUnknown() {
|
||||
if (_default.empty()) {
|
||||
requestDefault();
|
||||
}
|
||||
}
|
||||
|
||||
void ForumIcons::refreshDefault() {
|
||||
requestDefault();
|
||||
}
|
||||
|
||||
const std::vector<DocumentId> &ForumIcons::list() const {
|
||||
return _default;
|
||||
}
|
||||
|
||||
rpl::producer<> ForumIcons::defaultUpdates() const {
|
||||
return _defaultUpdated.events();
|
||||
}
|
||||
|
||||
void ForumIcons::requestDefault() {
|
||||
if (_defaultRequestId) {
|
||||
return;
|
||||
}
|
||||
auto &api = _owner->session().api();
|
||||
_defaultRequestId = api.request(MTPmessages_GetStickerSet(
|
||||
MTP_inputStickerSetEmojiDefaultTopicIcons(),
|
||||
MTP_int(0) // hash
|
||||
)).done([=](const MTPmessages_StickerSet &result) {
|
||||
_defaultRequestId = 0;
|
||||
result.match([&](const MTPDmessages_stickerSet &data) {
|
||||
updateDefault(data);
|
||||
}, [](const MTPDmessages_stickerSetNotModified &) {
|
||||
LOG(("API Error: Unexpected messages.stickerSetNotModified."));
|
||||
});
|
||||
}).fail([=] {
|
||||
_defaultRequestId = 0;
|
||||
}).send();
|
||||
}
|
||||
|
||||
void ForumIcons::updateDefault(const MTPDmessages_stickerSet &data) {
|
||||
const auto &list = data.vdocuments().v;
|
||||
_default.clear();
|
||||
_default.reserve(list.size());
|
||||
for (const auto &sticker : list) {
|
||||
_default.push_back(_owner->processDocument(sticker)->id);
|
||||
}
|
||||
_defaultUpdated.fire({});
|
||||
}
|
||||
|
||||
void ForumIcons::scheduleUserpicsReset(not_null<Forum*> forum) {
|
||||
const auto duration = crl::time(st::slideDuration);
|
||||
_resetUserpicsWhen[forum] = crl::now() + duration;
|
||||
if (!_resetUserpicsTimer.isActive()) {
|
||||
_resetUserpicsTimer.callOnce(duration);
|
||||
}
|
||||
}
|
||||
|
||||
void ForumIcons::clearUserpicsReset(not_null<Forum*> forum) {
|
||||
_resetUserpicsWhen.remove(forum);
|
||||
}
|
||||
|
||||
void ForumIcons::resetUserpics() {
|
||||
auto nearest = crl::time();
|
||||
auto now = crl::now();
|
||||
for (auto i = begin(_resetUserpicsWhen); i != end(_resetUserpicsWhen);) {
|
||||
if (i->second > now) {
|
||||
if (!nearest || nearest > i->second) {
|
||||
nearest = i->second;
|
||||
}
|
||||
++i;
|
||||
} else {
|
||||
const auto forum = i->first;
|
||||
i = _resetUserpicsWhen.erase(i);
|
||||
resetUserpicsFor(forum);
|
||||
}
|
||||
}
|
||||
if (nearest) {
|
||||
_resetUserpicsTimer.callOnce(
|
||||
std::min(nearest - now, 86400 * crl::time(1000)));
|
||||
} else {
|
||||
_resetUserpicsTimer.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
void ForumIcons::resetUserpicsFor(not_null<Forum*> forum) {
|
||||
forum->enumerateTopics([](not_null<ForumTopic*> topic) {
|
||||
topic->clearUserpicLoops();
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace Data
|
||||
63
Telegram/SourceFiles/data/data_forum_icons.h
Normal file
63
Telegram/SourceFiles/data/data_forum_icons.h
Normal file
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
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"
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace Data {
|
||||
|
||||
class DocumentMedia;
|
||||
class Session;
|
||||
class Forum;
|
||||
|
||||
class ForumIcons final {
|
||||
public:
|
||||
explicit ForumIcons(not_null<Session*> owner);
|
||||
~ForumIcons();
|
||||
|
||||
[[nodiscard]] Session &owner() const {
|
||||
return *_owner;
|
||||
}
|
||||
[[nodiscard]] Main::Session &session() const;
|
||||
|
||||
void refreshDefault();
|
||||
void requestDefaultIfUnknown();
|
||||
|
||||
[[nodiscard]] const std::vector<DocumentId> &list() const;
|
||||
|
||||
[[nodiscard]] rpl::producer<> defaultUpdates() const;
|
||||
|
||||
void scheduleUserpicsReset(not_null<Forum*> forum);
|
||||
void clearUserpicsReset(not_null<Forum*> forum);
|
||||
|
||||
private:
|
||||
void requestDefault();
|
||||
void resetUserpics();
|
||||
void resetUserpicsFor(not_null<Forum*> forum);
|
||||
|
||||
void updateDefault(const MTPDmessages_stickerSet &data);
|
||||
|
||||
const not_null<Session*> _owner;
|
||||
|
||||
std::vector<DocumentId> _default;
|
||||
rpl::event_stream<> _defaultUpdated;
|
||||
|
||||
mtpRequestId _defaultRequestId = 0;
|
||||
|
||||
base::flat_map<not_null<Forum*>, crl::time> _resetUserpicsWhen;
|
||||
base::Timer _resetUserpicsTimer;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Data
|
||||
997
Telegram/SourceFiles/data/data_forum_topic.cpp
Normal file
997
Telegram/SourceFiles/data/data_forum_topic.cpp
Normal file
@@ -0,0 +1,997 @@
|
||||
/*
|
||||
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/data_forum_topic.h"
|
||||
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_changes.h"
|
||||
#include "data/data_forum.h"
|
||||
#include "data/data_histories.h"
|
||||
#include "data/data_replies_list.h"
|
||||
#include "data/data_send_action.h"
|
||||
#include "data/notify/data_notify_settings.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/stickers/data_custom_emoji.h"
|
||||
#include "dialogs/dialogs_main_list.h"
|
||||
#include "dialogs/ui/dialogs_layout.h"
|
||||
#include "core/application.h"
|
||||
#include "core/core_settings.h"
|
||||
#include "apiwrap.h"
|
||||
#include "api/api_unread_things.h"
|
||||
#include "history/history.h"
|
||||
#include "history/history_item.h"
|
||||
#include "history/history_unread_things.h"
|
||||
#include "history/view/history_view_item_preview.h"
|
||||
#include "history/view/history_view_chat_section.h"
|
||||
#include "main/main_session.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/color_int_conversion.h"
|
||||
#include "ui/text/text_custom_emoji.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "styles/style_dialogs.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
|
||||
#include <QtSvg/QSvgRenderer>
|
||||
|
||||
namespace Data {
|
||||
namespace {
|
||||
|
||||
using UpdateFlag = TopicUpdate::Flag;
|
||||
|
||||
constexpr auto kUserpicLoopsCount = 1;
|
||||
|
||||
} // namespace
|
||||
|
||||
const base::flat_map<int32, QString> &ForumTopicIcons() {
|
||||
static const auto Result = base::flat_map<int32, QString>{
|
||||
{ 0x6FB9F0, u"blue"_q },
|
||||
{ 0xFFD67E, u"yellow"_q },
|
||||
{ 0xCB86DB, u"violet"_q },
|
||||
{ 0x8EEE98, u"green"_q },
|
||||
{ 0xFF93B2, u"rose"_q },
|
||||
{ 0xFB6F5F, u"red"_q },
|
||||
};
|
||||
return Result;
|
||||
}
|
||||
|
||||
const std::vector<int32> &ForumTopicColorIds() {
|
||||
static const auto Result = ForumTopicIcons(
|
||||
) | ranges::views::transform([](const auto &pair) {
|
||||
return pair.first;
|
||||
}) | ranges::to_vector;
|
||||
return Result;
|
||||
}
|
||||
|
||||
const QString &ForumTopicDefaultIcon() {
|
||||
static const auto Result = u"gray"_q;
|
||||
return Result;
|
||||
}
|
||||
|
||||
const QString &ForumTopicIcon(int32 colorId) {
|
||||
const auto &icons = ForumTopicIcons();
|
||||
const auto i = icons.find(colorId);
|
||||
return (i != end(icons)) ? i->second : ForumTopicDefaultIcon();
|
||||
}
|
||||
|
||||
QString ForumTopicIconPath(const QString &name) {
|
||||
return u":/gui/topic_icons/%1.svg"_q.arg(name);
|
||||
}
|
||||
|
||||
QImage ForumTopicIconBackground(int32 colorId, int size) {
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
auto svg = QSvgRenderer(ForumTopicIconPath(ForumTopicIcon(colorId)));
|
||||
auto result = QImage(
|
||||
QSize(size, size) * ratio,
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
result.setDevicePixelRatio(ratio);
|
||||
result.fill(Qt::transparent);
|
||||
|
||||
auto p = QPainter(&result);
|
||||
svg.render(&p, QRect(0, 0, size, size));
|
||||
p.end();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
QString ExtractNonEmojiLetter(const QString &title) {
|
||||
const auto begin = title.data();
|
||||
const auto end = begin + title.size();
|
||||
for (auto ch = begin; ch != end;) {
|
||||
auto length = 0;
|
||||
if (Ui::Emoji::Find(ch, end, &length)) {
|
||||
ch += length;
|
||||
continue;
|
||||
}
|
||||
uint ucs4 = ch->unicode();
|
||||
length = 1;
|
||||
if (QChar::isHighSurrogate(ucs4) && ch + 1 != end) {
|
||||
ushort low = ch[1].unicode();
|
||||
if (QChar::isLowSurrogate(low)) {
|
||||
ucs4 = QChar::surrogateToUcs4(ucs4, low);
|
||||
length = 2;
|
||||
}
|
||||
}
|
||||
if (!QChar::isLetterOrNumber(ucs4)) {
|
||||
ch += length;
|
||||
continue;
|
||||
}
|
||||
return QString(ch, length);
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
QImage ForumTopicIconFrame(
|
||||
int32 colorId,
|
||||
const QString &title,
|
||||
const style::ForumTopicIcon &st) {
|
||||
auto background = ForumTopicIconBackground(colorId, st.size);
|
||||
|
||||
if (const auto one = ExtractNonEmojiLetter(title); !one.isEmpty()) {
|
||||
auto p = QPainter(&background);
|
||||
p.setPen(Qt::white);
|
||||
p.setFont(st.font);
|
||||
p.drawText(
|
||||
QRect(0, st.textTop, st.size, st.font->height * 2),
|
||||
one,
|
||||
style::al_top);
|
||||
}
|
||||
|
||||
return background;
|
||||
}
|
||||
|
||||
QImage ForumTopicGeneralIconFrame(int size, const QColor &color) {
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
auto svg = QSvgRenderer(ForumTopicIconPath(u"general"_q));
|
||||
auto result = QImage(
|
||||
QSize(size, size) * ratio,
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
result.setDevicePixelRatio(ratio);
|
||||
result.fill(Qt::transparent);
|
||||
|
||||
const auto use = size * 1.;
|
||||
const auto skip = size * 0.;
|
||||
auto p = QPainter(&result);
|
||||
svg.render(&p, QRectF(skip, skip, use, use));
|
||||
p.end();
|
||||
|
||||
return style::colorizeImage(result, color);
|
||||
}
|
||||
|
||||
TextWithEntities ForumTopicIconWithTitle(
|
||||
MsgId rootId,
|
||||
DocumentId iconId,
|
||||
const QString &title) {
|
||||
const auto wrapped = st::wrap_rtl(title);
|
||||
return (rootId == ForumTopic::kGeneralId)
|
||||
? TextWithEntities{ u"# "_q + wrapped }
|
||||
: iconId
|
||||
? Data::SingleCustomEmoji(iconId).append(' ').append(wrapped)
|
||||
: TextWithEntities{ wrapped };
|
||||
}
|
||||
|
||||
QString ForumGeneralIconTitle() {
|
||||
return QChar(0) + u"general"_q;
|
||||
}
|
||||
|
||||
bool IsForumGeneralIconTitle(const QString &title) {
|
||||
return !title.isEmpty() && !title[0].unicode();
|
||||
}
|
||||
|
||||
int32 ForumGeneralIconColor(const QColor &color) {
|
||||
return int32(uint32(color.red()) << 16
|
||||
| uint32(color.green()) << 8
|
||||
| uint32(color.blue())
|
||||
| (uint32(color.alpha() == 255 ? 0 : color.alpha()) << 24));
|
||||
}
|
||||
|
||||
QColor ParseForumGeneralIconColor(int32 value) {
|
||||
const auto alpha = uint32(value) >> 24;
|
||||
return QColor(
|
||||
(value >> 16) & 0xFF,
|
||||
(value >> 8) & 0xFF,
|
||||
value & 0xFF,
|
||||
alpha ? alpha : 255);
|
||||
}
|
||||
|
||||
QString TopicIconEmojiEntity(TopicIconDescriptor descriptor) {
|
||||
return IsForumGeneralIconTitle(descriptor.title)
|
||||
? u"topic_general:"_q + QString::number(uint32(descriptor.colorId))
|
||||
: (u"topic_icon:"_q
|
||||
+ QString::number(uint32(descriptor.colorId))
|
||||
+ ' '
|
||||
+ ExtractNonEmojiLetter(descriptor.title));
|
||||
}
|
||||
|
||||
TopicIconDescriptor ParseTopicIconEmojiEntity(QStringView entity) {
|
||||
if (!entity.startsWith(u"topic_")) {
|
||||
return {};
|
||||
}
|
||||
const auto general = u"topic_general:"_q;
|
||||
const auto normal = u"topic_icon:"_q;
|
||||
if (entity.startsWith(general)) {
|
||||
return {
|
||||
.title = ForumGeneralIconTitle(),
|
||||
.colorId = int32(entity.mid(general.size()).toUInt()),
|
||||
};
|
||||
} else if (entity.startsWith(normal)) {
|
||||
const auto parts = entity.mid(normal.size()).split(' ');
|
||||
if (parts.size() == 2) {
|
||||
return {
|
||||
.title = parts[1].isEmpty() ? u" "_q : parts[1].toString(),
|
||||
.colorId = int32(parts[0].toUInt()),
|
||||
};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
ForumTopic::ForumTopic(not_null<Forum*> forum, MsgId rootId)
|
||||
: Thread(&forum->history()->owner(), Type::ForumTopic)
|
||||
, _forum(forum)
|
||||
, _list(_forum->topicsList())
|
||||
, _replies(std::make_shared<RepliesList>(history(), rootId, this))
|
||||
, _sendActionPainter(owner().sendActionManager().repliesPainter(
|
||||
history(),
|
||||
rootId))
|
||||
, _rootId(rootId)
|
||||
, _lastKnownServerMessageId(rootId)
|
||||
, _creatorId(creating() ? forum->session().userPeerId() : 0)
|
||||
, _creationDate(creating() ? base::unixtime::now() : 0)
|
||||
, _flags(creating() ? Flag::My : Flag()) {
|
||||
Thread::setMuted(owner().notifySettings().isMuted(this));
|
||||
|
||||
_sendActionPainter->setTopic(this);
|
||||
subscribeToUnreadChanges();
|
||||
|
||||
if (isGeneral()) {
|
||||
style::PaletteChanged(
|
||||
) | rpl::on_next([=] {
|
||||
_defaultIcon = QImage();
|
||||
}, _lifetime);
|
||||
}
|
||||
}
|
||||
|
||||
ForumTopic::~ForumTopic() {
|
||||
_sendActionPainter->setTopic(nullptr);
|
||||
session().api().unreadThings().cancelRequests(this);
|
||||
}
|
||||
|
||||
std::shared_ptr<Data::RepliesList> ForumTopic::replies() const {
|
||||
return _replies;
|
||||
}
|
||||
|
||||
not_null<PeerData*> ForumTopic::peer() const {
|
||||
return _forum->peer();
|
||||
}
|
||||
|
||||
UserData *ForumTopic::bot() const {
|
||||
return _forum->bot();
|
||||
}
|
||||
|
||||
ChannelData *ForumTopic::channel() const {
|
||||
return _forum->channel();
|
||||
}
|
||||
|
||||
not_null<History*> ForumTopic::history() const {
|
||||
return _forum->history();
|
||||
}
|
||||
|
||||
not_null<Forum*> ForumTopic::forum() const {
|
||||
return _forum;
|
||||
}
|
||||
|
||||
rpl::producer<> ForumTopic::destroyed() const {
|
||||
using namespace rpl::mappers;
|
||||
return rpl::merge(
|
||||
_forum->destroyed(),
|
||||
_forum->topicDestroyed() | rpl::filter(_1 == this) | rpl::to_empty);
|
||||
}
|
||||
|
||||
MsgId ForumTopic::rootId() const {
|
||||
return _rootId;
|
||||
}
|
||||
|
||||
PeerId ForumTopic::creatorId() const {
|
||||
return _creatorId;
|
||||
}
|
||||
|
||||
TimeId ForumTopic::creationDate() const {
|
||||
return _creationDate;
|
||||
}
|
||||
|
||||
not_null<HistoryView::ListMemento*> ForumTopic::listMemento() {
|
||||
if (!_listMemento) {
|
||||
_listMemento = std::make_unique<HistoryView::ListMemento>();
|
||||
}
|
||||
return _listMemento.get();
|
||||
}
|
||||
|
||||
bool ForumTopic::my() const {
|
||||
return (_flags & Flag::My);
|
||||
}
|
||||
|
||||
bool ForumTopic::canEdit() const {
|
||||
return my() || peer()->canManageTopics();
|
||||
}
|
||||
|
||||
bool ForumTopic::canDelete() const {
|
||||
if (creating() || isGeneral()) {
|
||||
return false;
|
||||
} else if (bot()) {
|
||||
return true;
|
||||
} else if (const auto channel = this->channel()) {
|
||||
if (channel->canDeleteMessages()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return my() && replies()->canDeleteMyTopic();
|
||||
}
|
||||
|
||||
bool ForumTopic::canToggleClosed() const {
|
||||
return !creating() && canEdit() && !_forum->peer()->isBot();
|
||||
}
|
||||
|
||||
bool ForumTopic::canTogglePinned() const {
|
||||
return !creating() && peer()->canManageTopics();
|
||||
}
|
||||
|
||||
bool ForumTopic::creating() const {
|
||||
return _forum->creating(_rootId);
|
||||
}
|
||||
|
||||
void ForumTopic::discard() {
|
||||
Expects(creating());
|
||||
|
||||
_forum->discardCreatingId(_rootId);
|
||||
}
|
||||
|
||||
void ForumTopic::setRealRootId(MsgId realId) {
|
||||
if (_rootId != realId) {
|
||||
_rootId = realId;
|
||||
_lastKnownServerMessageId = realId;
|
||||
_replies = std::make_shared<RepliesList>(history(), _rootId);
|
||||
if (_sendActionPainter) {
|
||||
_sendActionPainter->setTopic(nullptr);
|
||||
}
|
||||
_sendActionPainter = owner().sendActionManager().repliesPainter(
|
||||
history(),
|
||||
_rootId);
|
||||
_sendActionPainter->setTopic(this);
|
||||
subscribeToUnreadChanges();
|
||||
}
|
||||
}
|
||||
|
||||
void ForumTopic::subscribeToUnreadChanges() {
|
||||
_replies->unreadCountValue(
|
||||
) | rpl::map([=](std::optional<int> value) {
|
||||
return value ? _replies->displayedUnreadCount() : value;
|
||||
}) | rpl::distinct_until_changed(
|
||||
) | rpl::combine_previous(
|
||||
) | rpl::filter([=] {
|
||||
return inChatList();
|
||||
}) | rpl::on_next([=](
|
||||
std::optional<int> previous,
|
||||
std::optional<int> now) {
|
||||
if (previous.value_or(0) != now.value_or(0)) {
|
||||
_forum->recentTopicsInvalidate(this);
|
||||
}
|
||||
notifyUnreadStateChange(unreadStateFor(
|
||||
previous.value_or(0),
|
||||
previous.has_value()));
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
void ForumTopic::readTillEnd() {
|
||||
_replies->readTill(_lastKnownServerMessageId);
|
||||
}
|
||||
|
||||
void ForumTopic::applyTopic(const MTPDforumTopic &data) {
|
||||
Expects(_rootId == data.vid().v);
|
||||
|
||||
const auto min = data.is_short();
|
||||
|
||||
applyCreator(peerFromMTP(data.vfrom_id()));
|
||||
applyCreationDate(data.vdate().v);
|
||||
|
||||
applyTitle(qs(data.vtitle()));
|
||||
if (const auto iconId = data.vicon_emoji_id()) {
|
||||
applyIconId(iconId->v);
|
||||
} else {
|
||||
applyIconId(0);
|
||||
}
|
||||
applyColorId(data.vicon_color().v);
|
||||
|
||||
applyIsMy(data.is_my());
|
||||
setClosed(data.is_closed());
|
||||
|
||||
if (!min) {
|
||||
owner().setPinnedFromEntryList(this, data.is_pinned());
|
||||
owner().notifySettings().apply(this, data.vnotify_settings());
|
||||
|
||||
if (const auto draft = data.vdraft()) {
|
||||
draft->match([&](const MTPDdraftMessage &data) {
|
||||
Data::ApplyPeerCloudDraft(
|
||||
&session(),
|
||||
peer()->id,
|
||||
_rootId,
|
||||
PeerId(),
|
||||
data);
|
||||
}, [](const MTPDdraftMessageEmpty&) {});
|
||||
}
|
||||
|
||||
_replies->setInboxReadTill(
|
||||
data.vread_inbox_max_id().v,
|
||||
data.vunread_count().v);
|
||||
_replies->setOutboxReadTill(data.vread_outbox_max_id().v);
|
||||
applyTopicTopMessage(data.vtop_message().v);
|
||||
unreadMentions().setCount(data.vunread_mentions_count().v);
|
||||
unreadReactions().setCount(data.vunread_reactions_count().v);
|
||||
}
|
||||
}
|
||||
|
||||
void ForumTopic::applyCreator(PeerId creatorId) {
|
||||
if (_creatorId != creatorId) {
|
||||
_creatorId = creatorId;
|
||||
session().changes().topicUpdated(this, UpdateFlag::Creator);
|
||||
}
|
||||
}
|
||||
|
||||
void ForumTopic::applyCreationDate(TimeId date) {
|
||||
_creationDate = date;
|
||||
}
|
||||
|
||||
void ForumTopic::applyIsMy(bool my) {
|
||||
if (my != this->my()) {
|
||||
if (my) {
|
||||
_flags |= Flag::My;
|
||||
} else {
|
||||
_flags &= ~Flag::My;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool ForumTopic::closed() const {
|
||||
return _flags & Flag::Closed;
|
||||
}
|
||||
|
||||
void ForumTopic::setClosed(bool closed) {
|
||||
if (this->closed() == closed) {
|
||||
return;
|
||||
} else if (closed) {
|
||||
_flags |= Flag::Closed;
|
||||
} else {
|
||||
_flags &= ~Flag::Closed;
|
||||
}
|
||||
session().changes().topicUpdated(this, UpdateFlag::Closed);
|
||||
}
|
||||
|
||||
void ForumTopic::setClosedAndSave(bool closed) {
|
||||
setClosed(closed);
|
||||
|
||||
const auto api = &session().api();
|
||||
const auto weak = base::make_weak(this);
|
||||
api->request(MTPmessages_EditForumTopic(
|
||||
MTP_flags(MTPmessages_EditForumTopic::Flag::f_closed),
|
||||
peer()->input(),
|
||||
MTP_int(_rootId),
|
||||
MTPstring(), // title
|
||||
MTPlong(), // icon_emoji_id
|
||||
MTP_bool(closed),
|
||||
MTPBool() // hiddenKO
|
||||
)).done([=](const MTPUpdates &result) {
|
||||
api->applyUpdates(result);
|
||||
}).fail([=](const MTP::Error &error) {
|
||||
if (error.type() != u"TOPIC_NOT_MODIFIED") {
|
||||
if (const auto topic = weak.get()) {
|
||||
topic->forum()->requestTopic(topic->rootId());
|
||||
}
|
||||
}
|
||||
}).send();
|
||||
}
|
||||
|
||||
bool ForumTopic::hidden() const {
|
||||
return (_flags & Flag::Hidden);
|
||||
}
|
||||
|
||||
void ForumTopic::setHidden(bool hidden) {
|
||||
if (hidden) {
|
||||
_flags |= Flag::Hidden;
|
||||
} else {
|
||||
_flags &= ~Flag::Hidden;
|
||||
}
|
||||
}
|
||||
|
||||
void ForumTopic::indexTitleParts() {
|
||||
_titleWords.clear();
|
||||
_titleFirstLetters.clear();
|
||||
auto toIndexList = QStringList();
|
||||
auto appendToIndex = [&](const QString &value) {
|
||||
if (!value.isEmpty()) {
|
||||
toIndexList.push_back(TextUtilities::RemoveAccents(value));
|
||||
}
|
||||
};
|
||||
|
||||
appendToIndex(_title);
|
||||
const auto appendTranslit = !toIndexList.isEmpty()
|
||||
&& cRussianLetters().match(toIndexList.front()).hasMatch();
|
||||
if (appendTranslit) {
|
||||
appendToIndex(translitRusEng(toIndexList.front()));
|
||||
}
|
||||
auto toIndex = toIndexList.join(' ');
|
||||
toIndex += ' ' + rusKeyboardLayoutSwitch(toIndex);
|
||||
|
||||
const auto namesList = TextUtilities::PrepareSearchWords(toIndex);
|
||||
for (const auto &name : namesList) {
|
||||
_titleWords.insert(name);
|
||||
_titleFirstLetters.insert(name[0]);
|
||||
}
|
||||
}
|
||||
|
||||
int ForumTopic::chatListNameVersion() const {
|
||||
return _titleVersion;
|
||||
}
|
||||
|
||||
void ForumTopic::applyTopicTopMessage(MsgId topMessageId) {
|
||||
if (topMessageId) {
|
||||
growLastKnownServerMessageId(topMessageId);
|
||||
const auto itemId = FullMsgId(peer()->id, topMessageId);
|
||||
if (const auto item = owner().message(itemId)) {
|
||||
setLastServerMessage(item);
|
||||
resolveChatListMessageGroup();
|
||||
} else {
|
||||
setLastServerMessage(nullptr);
|
||||
}
|
||||
} else {
|
||||
setLastServerMessage(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void ForumTopic::resolveChatListMessageGroup() {
|
||||
if (!(_flags & Flag::ResolveChatListMessage)) {
|
||||
return;
|
||||
}
|
||||
// If we set a single album part, request the full album.
|
||||
const auto item = _lastServerMessage.value_or(nullptr);
|
||||
if (item && item->groupId() != MessageGroupId()) {
|
||||
if (owner().groups().isGroupOfOne(item)
|
||||
&& !item->toPreview({
|
||||
.hideSender = true,
|
||||
.hideCaption = true }).images.empty()
|
||||
&& _requestedGroups.emplace(item->fullId()).second) {
|
||||
owner().histories().requestGroupAround(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ForumTopic::growLastKnownServerMessageId(MsgId id) {
|
||||
_lastKnownServerMessageId = std::max(_lastKnownServerMessageId, id);
|
||||
}
|
||||
|
||||
void ForumTopic::setLastServerMessage(HistoryItem *item) {
|
||||
if (item) {
|
||||
growLastKnownServerMessageId(item->id);
|
||||
}
|
||||
_lastServerMessage = item;
|
||||
if (_lastMessage
|
||||
&& *_lastMessage
|
||||
&& !(*_lastMessage)->isRegular()
|
||||
&& (!item
|
||||
|| (*_lastMessage)->date() > item->date()
|
||||
|| (*_lastMessage)->isSending())) {
|
||||
return;
|
||||
}
|
||||
setLastMessage(item);
|
||||
}
|
||||
|
||||
void ForumTopic::setLastMessage(HistoryItem *item) {
|
||||
if (_lastMessage && *_lastMessage == item) {
|
||||
return;
|
||||
}
|
||||
_lastMessage = item;
|
||||
if (!item || item->isRegular()) {
|
||||
_lastServerMessage = item;
|
||||
if (item) {
|
||||
growLastKnownServerMessageId(item->id);
|
||||
}
|
||||
}
|
||||
setChatListMessage(item);
|
||||
}
|
||||
|
||||
void ForumTopic::setChatListMessage(HistoryItem *item) {
|
||||
if (_chatListMessage && *_chatListMessage == item) {
|
||||
return;
|
||||
}
|
||||
const auto was = _chatListMessage.value_or(nullptr);
|
||||
if (item) {
|
||||
if (item->isSponsored()) {
|
||||
return;
|
||||
}
|
||||
if (_chatListMessage
|
||||
&& *_chatListMessage
|
||||
&& !(*_chatListMessage)->isRegular()
|
||||
&& (*_chatListMessage)->date() > item->date()) {
|
||||
return;
|
||||
}
|
||||
_chatListMessage = item;
|
||||
setChatListTimeId(item->date());
|
||||
} else if (!_chatListMessage || *_chatListMessage) {
|
||||
_chatListMessage = nullptr;
|
||||
updateChatListEntry();
|
||||
}
|
||||
_forum->listMessageChanged(was, item);
|
||||
}
|
||||
|
||||
void ForumTopic::chatListPreloadData() {
|
||||
if (_icon) {
|
||||
[[maybe_unused]] const auto preload = _icon->ready();
|
||||
}
|
||||
allowChatListMessageResolve();
|
||||
}
|
||||
|
||||
void ForumTopic::paintUserpic(
|
||||
Painter &p,
|
||||
Ui::PeerUserpicView &view,
|
||||
const Dialogs::Ui::PaintContext &context) const {
|
||||
const auto &st = context.st;
|
||||
auto position = QPoint(st->padding.left(), st->padding.top());
|
||||
if (_icon) {
|
||||
if (context.narrow) {
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
const auto tag = Data::CustomEmojiManager::SizeTag::Normal;
|
||||
const auto size = Data::FrameSizeFromTag(tag) / ratio;
|
||||
position = QPoint(
|
||||
(context.width - size) / 2,
|
||||
(st->height - size) / 2);
|
||||
}
|
||||
_icon->paint(p, {
|
||||
.textColor = (context.active
|
||||
? st::dialogsNameFgActive
|
||||
: context.selected
|
||||
? st::dialogsNameFgOver
|
||||
: st::dialogsNameFg)->c,
|
||||
.now = context.now,
|
||||
.position = position,
|
||||
.paused = context.paused,
|
||||
});
|
||||
} else {
|
||||
if (isGeneral()) {
|
||||
validateGeneralIcon(context);
|
||||
} else {
|
||||
validateDefaultIcon();
|
||||
}
|
||||
const auto size = st::defaultForumTopicIcon.size;
|
||||
if (context.narrow) {
|
||||
position = QPoint(
|
||||
(context.width - size) / 2,
|
||||
(st->height - size) / 2);
|
||||
} else {
|
||||
const auto esize = st::emojiSize;
|
||||
const auto shift = (esize - size) / 2;
|
||||
position += st::forumTopicIconPosition + QPoint(shift, 0);
|
||||
}
|
||||
p.drawImage(position, _defaultIcon);
|
||||
}
|
||||
}
|
||||
|
||||
void ForumTopic::clearUserpicLoops() {
|
||||
if (_icon) {
|
||||
_icon->unload();
|
||||
}
|
||||
}
|
||||
|
||||
void ForumTopic::validateDefaultIcon() const {
|
||||
if (!_defaultIcon.isNull()) {
|
||||
return;
|
||||
}
|
||||
_defaultIcon = ForumTopicIconFrame(
|
||||
_colorId,
|
||||
_title,
|
||||
st::defaultForumTopicIcon);
|
||||
}
|
||||
|
||||
void ForumTopic::validateGeneralIcon(
|
||||
const Dialogs::Ui::PaintContext &context) const {
|
||||
const auto mask = Flag::GeneralIconActive | Flag::GeneralIconSelected;
|
||||
const auto flags = context.active
|
||||
? Flag::GeneralIconActive
|
||||
: context.selected
|
||||
? Flag::GeneralIconSelected
|
||||
: Flag(0);
|
||||
if (!_defaultIcon.isNull() && ((_flags & mask) == flags)) {
|
||||
return;
|
||||
}
|
||||
const auto size = st::defaultForumTopicIcon.size;
|
||||
const auto &color = context.active
|
||||
? st::dialogsTextFgActive
|
||||
: context.selected
|
||||
? st::dialogsTextFgOver
|
||||
: st::dialogsTextFg;
|
||||
_defaultIcon = ForumTopicGeneralIconFrame(size, color->c);
|
||||
_flags = (_flags & ~mask) | flags;
|
||||
}
|
||||
|
||||
void ForumTopic::requestChatListMessage() {
|
||||
if (!chatListMessageKnown() && !forum()->creating(_rootId)) {
|
||||
forum()->requestTopic(_rootId);
|
||||
}
|
||||
}
|
||||
|
||||
TimeId ForumTopic::adjustedChatListTimeId() const {
|
||||
const auto result = chatListTimeId();
|
||||
if (const auto draft = history()->cloudDraft(_rootId, PeerId())) {
|
||||
if (!Data::DraftIsNull(draft) && !session().supportMode()) {
|
||||
return std::max(result, draft->date);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
int ForumTopic::fixedOnTopIndex() const {
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool ForumTopic::shouldBeInChatList() const {
|
||||
return isPinnedDialog(FilterId())
|
||||
|| !lastMessageKnown()
|
||||
|| (lastMessage() != nullptr);
|
||||
}
|
||||
|
||||
HistoryItem *ForumTopic::lastMessage() const {
|
||||
return _lastMessage.value_or(nullptr);
|
||||
}
|
||||
|
||||
bool ForumTopic::lastMessageKnown() const {
|
||||
return _lastMessage.has_value();
|
||||
}
|
||||
|
||||
HistoryItem *ForumTopic::lastServerMessage() const {
|
||||
return _lastServerMessage.value_or(nullptr);
|
||||
}
|
||||
|
||||
bool ForumTopic::lastServerMessageKnown() const {
|
||||
return _lastServerMessage.has_value();
|
||||
}
|
||||
|
||||
MsgId ForumTopic::lastKnownServerMessageId() const {
|
||||
return _lastKnownServerMessageId;
|
||||
}
|
||||
|
||||
QString ForumTopic::title() const {
|
||||
return _title;
|
||||
}
|
||||
|
||||
TextWithEntities ForumTopic::titleWithIcon() const {
|
||||
return ForumTopicIconWithTitle(_rootId, _iconId, _title);
|
||||
}
|
||||
|
||||
TextWithEntities ForumTopic::titleWithIconOrLogo() const {
|
||||
if (_iconId || isGeneral()) {
|
||||
return titleWithIcon();
|
||||
}
|
||||
return Ui::Text::SingleCustomEmoji(Data::TopicIconEmojiEntity({
|
||||
.title = _title,
|
||||
.colorId = _colorId,
|
||||
})).append(' ').append(_title);
|
||||
}
|
||||
|
||||
int ForumTopic::titleVersion() const {
|
||||
return _titleVersion;
|
||||
}
|
||||
|
||||
void ForumTopic::applyTitle(const QString &title) {
|
||||
if (_title == title) {
|
||||
return;
|
||||
}
|
||||
_title = title;
|
||||
invalidateTitleWithIcon();
|
||||
_defaultIcon = QImage();
|
||||
indexTitleParts();
|
||||
updateChatListEntry();
|
||||
session().changes().topicUpdated(this, UpdateFlag::Title);
|
||||
}
|
||||
|
||||
DocumentId ForumTopic::iconId() const {
|
||||
return _iconId;
|
||||
}
|
||||
|
||||
void ForumTopic::applyIconId(DocumentId iconId) {
|
||||
if (_iconId == iconId) {
|
||||
return;
|
||||
}
|
||||
_iconId = iconId;
|
||||
invalidateTitleWithIcon();
|
||||
_icon = iconId
|
||||
? std::make_unique<Ui::Text::LimitedLoopsEmoji>(
|
||||
owner().customEmojiManager().create(
|
||||
_iconId,
|
||||
[=] { updateChatListEntry(); },
|
||||
Data::CustomEmojiManager::SizeTag::Normal),
|
||||
kUserpicLoopsCount)
|
||||
: nullptr;
|
||||
if (iconId) {
|
||||
_defaultIcon = QImage();
|
||||
}
|
||||
updateChatListEntry();
|
||||
session().changes().topicUpdated(this, UpdateFlag::IconId);
|
||||
}
|
||||
|
||||
void ForumTopic::invalidateTitleWithIcon() {
|
||||
++_titleVersion;
|
||||
_forum->recentTopicsInvalidate(this);
|
||||
}
|
||||
|
||||
int32 ForumTopic::colorId() const {
|
||||
return _colorId;
|
||||
}
|
||||
|
||||
void ForumTopic::applyColorId(int32 colorId) {
|
||||
if (_colorId != colorId) {
|
||||
_colorId = colorId;
|
||||
session().changes().topicUpdated(this, UpdateFlag::ColorId);
|
||||
}
|
||||
}
|
||||
|
||||
void ForumTopic::applyMaybeLast(not_null<HistoryItem*> item) {
|
||||
if (!_lastServerMessage.value_or(nullptr)
|
||||
|| (*_lastServerMessage)->id < item->id) {
|
||||
setLastServerMessage(item);
|
||||
resolveChatListMessageGroup();
|
||||
} else {
|
||||
growLastKnownServerMessageId(item->id);
|
||||
}
|
||||
}
|
||||
|
||||
void ForumTopic::applyItemAdded(not_null<HistoryItem*> item) {
|
||||
if (item->isRegular()) {
|
||||
setLastServerMessage(item);
|
||||
} else {
|
||||
setLastMessage(item);
|
||||
}
|
||||
}
|
||||
|
||||
void ForumTopic::maybeSetLastMessage(not_null<HistoryItem*> item) {
|
||||
Expects(item->topicRootId() == _rootId);
|
||||
|
||||
if (!_lastMessage
|
||||
|| !(*_lastMessage)
|
||||
|| ((*_lastMessage)->date() < item->date())
|
||||
|| ((*_lastMessage)->date() == item->date()
|
||||
&& (*_lastMessage)->id < item->id)) {
|
||||
setLastMessage(item);
|
||||
}
|
||||
}
|
||||
|
||||
void ForumTopic::applyItemRemoved(MsgId id) {
|
||||
if (const auto lastItem = lastMessage()) {
|
||||
if (lastItem->id == id) {
|
||||
_lastMessage = std::nullopt;
|
||||
}
|
||||
}
|
||||
if (const auto lastServerItem = lastServerMessage()) {
|
||||
if (lastServerItem->id == id) {
|
||||
_lastServerMessage = std::nullopt;
|
||||
}
|
||||
}
|
||||
if (const auto chatListItem = _chatListMessage.value_or(nullptr)) {
|
||||
if (chatListItem->id == id) {
|
||||
_chatListMessage = std::nullopt;
|
||||
requestChatListMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool ForumTopic::isServerSideUnread(
|
||||
not_null<const HistoryItem*> item) const {
|
||||
return _replies->isServerSideUnread(item);
|
||||
}
|
||||
|
||||
void ForumTopic::setMuted(bool muted) {
|
||||
if (this->muted() == muted) {
|
||||
return;
|
||||
}
|
||||
const auto state = chatListBadgesState();
|
||||
const auto notify = state.unread || state.reaction;
|
||||
const auto notifier = unreadStateChangeNotifier(notify);
|
||||
Thread::setMuted(muted);
|
||||
session().changes().topicUpdated(this, UpdateFlag::Notifications);
|
||||
}
|
||||
|
||||
HistoryView::SendActionPainter *ForumTopic::sendActionPainter() {
|
||||
return _sendActionPainter.get();
|
||||
}
|
||||
|
||||
Dialogs::UnreadState ForumTopic::chatListUnreadState() const {
|
||||
return unreadStateFor(
|
||||
_replies->displayedUnreadCount(),
|
||||
_replies->unreadCountKnown());
|
||||
}
|
||||
|
||||
Dialogs::BadgesState ForumTopic::chatListBadgesState() const {
|
||||
auto result = Dialogs::BadgesForUnread(
|
||||
chatListUnreadState(),
|
||||
Dialogs::CountInBadge::Messages,
|
||||
Dialogs::IncludeInBadge::All);
|
||||
if (!result.unread && _replies->inboxReadTillId() < 2) {
|
||||
result.unread = (bot() || (channel() && channel()->amIn()))
|
||||
&& (_lastKnownServerMessageId > history()->inboxReadTillId());
|
||||
result.unreadMuted = muted();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Dialogs::UnreadState ForumTopic::unreadStateFor(
|
||||
int count,
|
||||
bool known) const {
|
||||
auto result = Dialogs::UnreadState();
|
||||
const auto muted = this->muted();
|
||||
result.messages = count;
|
||||
result.chats = count ? 1 : 0;
|
||||
result.mentions = unreadMentions().has() ? 1 : 0;
|
||||
result.reactions = unreadReactions().has() ? 1 : 0;
|
||||
result.messagesMuted = muted ? result.messages : 0;
|
||||
result.chatsMuted = muted ? result.chats : 0;
|
||||
result.reactionsMuted = muted ? result.reactions : 0;
|
||||
result.known = known;
|
||||
return result;
|
||||
}
|
||||
|
||||
void ForumTopic::allowChatListMessageResolve() {
|
||||
if (_flags & Flag::ResolveChatListMessage) {
|
||||
return;
|
||||
}
|
||||
_flags |= Flag::ResolveChatListMessage;
|
||||
resolveChatListMessageGroup();
|
||||
}
|
||||
|
||||
HistoryItem *ForumTopic::chatListMessage() const {
|
||||
return _lastMessage.value_or(nullptr);
|
||||
}
|
||||
|
||||
bool ForumTopic::chatListMessageKnown() const {
|
||||
return _lastMessage.has_value();
|
||||
}
|
||||
|
||||
const QString &ForumTopic::chatListName() const {
|
||||
return _title;
|
||||
}
|
||||
|
||||
const base::flat_set<QString> &ForumTopic::chatListNameWords() const {
|
||||
return _titleWords;
|
||||
}
|
||||
|
||||
const base::flat_set<QChar> &ForumTopic::chatListFirstLetters() const {
|
||||
return _titleFirstLetters;
|
||||
}
|
||||
|
||||
void ForumTopic::hasUnreadMentionChanged(bool has) {
|
||||
auto was = chatListUnreadState();
|
||||
if (has) {
|
||||
was.mentions = 0;
|
||||
} else {
|
||||
was.mentions = 1;
|
||||
}
|
||||
notifyUnreadStateChange(was);
|
||||
}
|
||||
|
||||
void ForumTopic::hasUnreadReactionChanged(bool has) {
|
||||
auto was = chatListUnreadState();
|
||||
if (has) {
|
||||
was.reactions = was.reactionsMuted = 0;
|
||||
} else {
|
||||
was.reactions = 1;
|
||||
was.reactionsMuted = muted() ? was.reactions : 0;
|
||||
}
|
||||
notifyUnreadStateChange(was);
|
||||
}
|
||||
|
||||
const QString &ForumTopic::chatListNameSortKey() const {
|
||||
static const auto empty = QString();
|
||||
return empty;
|
||||
}
|
||||
|
||||
} // namespace Data
|
||||
253
Telegram/SourceFiles/data/data_forum_topic.h
Normal file
253
Telegram/SourceFiles/data/data_forum_topic.h
Normal file
@@ -0,0 +1,253 @@
|
||||
/*
|
||||
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_thread.h"
|
||||
#include "data/notify/data_peer_notify_settings.h"
|
||||
#include "base/flags.h"
|
||||
|
||||
class ChannelData;
|
||||
enum class ChatRestriction;
|
||||
|
||||
namespace style {
|
||||
struct ForumTopicIcon;
|
||||
} // namespace style
|
||||
|
||||
namespace Dialogs {
|
||||
class MainList;
|
||||
} // namespace Dialogs
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace HistoryView {
|
||||
class SendActionPainter;
|
||||
class ListMemento;
|
||||
} // namespace HistoryView
|
||||
|
||||
namespace Data {
|
||||
|
||||
class RepliesList;
|
||||
class Session;
|
||||
class Forum;
|
||||
|
||||
[[nodiscard]] const base::flat_map<int32, QString> &ForumTopicIcons();
|
||||
[[nodiscard]] const std::vector<int32> &ForumTopicColorIds();
|
||||
[[nodiscard]] const QString &ForumTopicIcon(int32 colorId);
|
||||
[[nodiscard]] QString ForumTopicIconPath(const QString &name);
|
||||
[[nodiscard]] QImage ForumTopicIconBackground(int32 colorId, int size);
|
||||
[[nodiscard]] QImage ForumTopicIconFrame(
|
||||
int32 colorId,
|
||||
const QString &title,
|
||||
const style::ForumTopicIcon &st);
|
||||
[[nodiscard]] QImage ForumTopicGeneralIconFrame(
|
||||
int size,
|
||||
const QColor &color);
|
||||
[[nodiscard]] TextWithEntities ForumTopicIconWithTitle(
|
||||
MsgId rootId,
|
||||
DocumentId iconId,
|
||||
const QString &title);
|
||||
|
||||
[[nodiscard]] QString ForumGeneralIconTitle();
|
||||
[[nodiscard]] bool IsForumGeneralIconTitle(const QString &title);
|
||||
[[nodiscard]] int32 ForumGeneralIconColor(const QColor &color);
|
||||
[[nodiscard]] QColor ParseForumGeneralIconColor(int32 value);
|
||||
|
||||
struct TopicIconDescriptor {
|
||||
QString title;
|
||||
int32 colorId = 0;
|
||||
|
||||
[[nodiscard]] bool empty() const {
|
||||
return !colorId && title.isEmpty();
|
||||
}
|
||||
explicit operator bool() const {
|
||||
return !empty();
|
||||
}
|
||||
};
|
||||
|
||||
[[nodiscard]] QString TopicIconEmojiEntity(TopicIconDescriptor descriptor);
|
||||
[[nodiscard]] TopicIconDescriptor ParseTopicIconEmojiEntity(
|
||||
QStringView entity);
|
||||
|
||||
class ForumTopic final : public Thread {
|
||||
public:
|
||||
static constexpr auto kGeneralId = 1;
|
||||
|
||||
ForumTopic(not_null<Forum*> forum, MsgId rootId);
|
||||
~ForumTopic();
|
||||
|
||||
not_null<History*> owningHistory() override {
|
||||
return history();
|
||||
}
|
||||
|
||||
[[nodiscard]] bool isGeneral() const {
|
||||
return (_rootId == kGeneralId);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::shared_ptr<RepliesList> replies() const;
|
||||
[[nodiscard]] not_null<PeerData*> peer() const;
|
||||
[[nodiscard]] UserData *bot() const;
|
||||
[[nodiscard]] ChannelData *channel() const;
|
||||
[[nodiscard]] not_null<History*> history() const;
|
||||
[[nodiscard]] not_null<Forum*> forum() const;
|
||||
[[nodiscard]] rpl::producer<> destroyed() const;
|
||||
[[nodiscard]] MsgId rootId() const;
|
||||
[[nodiscard]] PeerId creatorId() const;
|
||||
[[nodiscard]] TimeId creationDate() const;
|
||||
|
||||
[[nodiscard]] not_null<HistoryView::ListMemento*> listMemento();
|
||||
|
||||
[[nodiscard]] bool my() const;
|
||||
[[nodiscard]] bool canEdit() const;
|
||||
[[nodiscard]] bool canToggleClosed() const;
|
||||
[[nodiscard]] bool canTogglePinned() const;
|
||||
[[nodiscard]] bool canDelete() const;
|
||||
|
||||
[[nodiscard]] bool closed() const;
|
||||
void setClosed(bool closed);
|
||||
void setClosedAndSave(bool closed);
|
||||
|
||||
[[nodiscard]] bool hidden() const;
|
||||
void setHidden(bool hidden);
|
||||
|
||||
[[nodiscard]] bool creating() const;
|
||||
void discard();
|
||||
|
||||
void setRealRootId(MsgId realId);
|
||||
void readTillEnd();
|
||||
void requestChatListMessage();
|
||||
|
||||
void applyTopic(const MTPDforumTopic &data);
|
||||
|
||||
TimeId adjustedChatListTimeId() const override;
|
||||
|
||||
int fixedOnTopIndex() const override;
|
||||
bool shouldBeInChatList() const override;
|
||||
Dialogs::UnreadState chatListUnreadState() const override;
|
||||
Dialogs::BadgesState chatListBadgesState() const override;
|
||||
HistoryItem *chatListMessage() const override;
|
||||
bool chatListMessageKnown() const override;
|
||||
const QString &chatListName() const override;
|
||||
const QString &chatListNameSortKey() const override;
|
||||
int chatListNameVersion() const override;
|
||||
const base::flat_set<QString> &chatListNameWords() const override;
|
||||
const base::flat_set<QChar> &chatListFirstLetters() const override;
|
||||
|
||||
void hasUnreadMentionChanged(bool has) override;
|
||||
void hasUnreadReactionChanged(bool has) override;
|
||||
|
||||
[[nodiscard]] HistoryItem *lastMessage() const;
|
||||
[[nodiscard]] HistoryItem *lastServerMessage() const;
|
||||
[[nodiscard]] bool lastMessageKnown() const;
|
||||
[[nodiscard]] bool lastServerMessageKnown() const;
|
||||
[[nodiscard]] MsgId lastKnownServerMessageId() const;
|
||||
|
||||
[[nodiscard]] QString title() const;
|
||||
[[nodiscard]] TextWithEntities titleWithIcon() const;
|
||||
[[nodiscard]] TextWithEntities titleWithIconOrLogo() const;
|
||||
[[nodiscard]] int titleVersion() const;
|
||||
void applyTitle(const QString &title);
|
||||
[[nodiscard]] DocumentId iconId() const;
|
||||
void applyIconId(DocumentId iconId);
|
||||
[[nodiscard]] int32 colorId() const;
|
||||
void applyColorId(int32 colorId);
|
||||
void applyCreator(PeerId creatorId);
|
||||
void applyCreationDate(TimeId date);
|
||||
void applyIsMy(bool my);
|
||||
void applyMaybeLast(not_null<HistoryItem*> item);
|
||||
void applyItemAdded(not_null<HistoryItem*> item);
|
||||
void applyItemRemoved(MsgId id);
|
||||
void maybeSetLastMessage(not_null<HistoryItem*> item);
|
||||
|
||||
[[nodiscard]] PeerNotifySettings ¬ify() {
|
||||
return _notify;
|
||||
}
|
||||
[[nodiscard]] const PeerNotifySettings ¬ify() const {
|
||||
return _notify;
|
||||
}
|
||||
|
||||
void chatListPreloadData() override;
|
||||
void paintUserpic(
|
||||
Painter &p,
|
||||
Ui::PeerUserpicView &view,
|
||||
const Dialogs::Ui::PaintContext &context) const override;
|
||||
void clearUserpicLoops();
|
||||
|
||||
[[nodiscard]] bool isServerSideUnread(
|
||||
not_null<const HistoryItem*> item) const override;
|
||||
|
||||
void setMuted(bool muted) override;
|
||||
|
||||
[[nodiscard]] auto sendActionPainter()
|
||||
-> HistoryView::SendActionPainter* override;
|
||||
|
||||
private:
|
||||
enum class Flag : uchar {
|
||||
Closed = (1 << 0),
|
||||
Hidden = (1 << 1),
|
||||
My = (1 << 2),
|
||||
HasPinnedMessages = (1 << 3),
|
||||
GeneralIconActive = (1 << 4),
|
||||
GeneralIconSelected = (1 << 5),
|
||||
ResolveChatListMessage = (1 << 6),
|
||||
};
|
||||
friend inline constexpr bool is_flag_type(Flag) { return true; }
|
||||
using Flags = base::flags<Flag>;
|
||||
|
||||
void indexTitleParts();
|
||||
void validateDefaultIcon() const;
|
||||
void validateGeneralIcon(const Dialogs::Ui::PaintContext &context) const;
|
||||
void applyTopicTopMessage(MsgId topMessageId);
|
||||
void growLastKnownServerMessageId(MsgId id);
|
||||
void invalidateTitleWithIcon();
|
||||
|
||||
void setLastMessage(HistoryItem *item);
|
||||
void setLastServerMessage(HistoryItem *item);
|
||||
void setChatListMessage(HistoryItem *item);
|
||||
void allowChatListMessageResolve();
|
||||
void resolveChatListMessageGroup();
|
||||
|
||||
void subscribeToUnreadChanges();
|
||||
[[nodiscard]] Dialogs::UnreadState unreadStateFor(
|
||||
int count,
|
||||
bool known) const;
|
||||
|
||||
const not_null<Forum*> _forum;
|
||||
const not_null<Dialogs::MainList*> _list;
|
||||
std::shared_ptr<RepliesList> _replies;
|
||||
std::unique_ptr<HistoryView::ListMemento> _listMemento;
|
||||
std::shared_ptr<HistoryView::SendActionPainter> _sendActionPainter;
|
||||
MsgId _rootId = 0;
|
||||
MsgId _lastKnownServerMessageId = 0;
|
||||
|
||||
PeerNotifySettings _notify;
|
||||
|
||||
QString _title;
|
||||
DocumentId _iconId = 0;
|
||||
base::flat_set<QString> _titleWords;
|
||||
base::flat_set<QChar> _titleFirstLetters;
|
||||
PeerId _creatorId = 0;
|
||||
TimeId _creationDate = 0;
|
||||
int _titleVersion = 0;
|
||||
int32 _colorId = 0;
|
||||
mutable Flags _flags;
|
||||
|
||||
std::unique_ptr<Ui::Text::CustomEmoji> _icon;
|
||||
mutable QImage _defaultIcon; // on-demand
|
||||
|
||||
std::optional<HistoryItem*> _lastMessage;
|
||||
std::optional<HistoryItem*> _lastServerMessage;
|
||||
std::optional<HistoryItem*> _chatListMessage;
|
||||
base::flat_set<FullMsgId> _requestedGroups;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Data
|
||||
13
Telegram/SourceFiles/data/data_game.cpp
Normal file
13
Telegram/SourceFiles/data/data_game.cpp
Normal file
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
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/data_game.h"
|
||||
|
||||
GameData::GameData(not_null<Data::Session*> owner, const GameId &id)
|
||||
: owner(owner)
|
||||
, id(id) {
|
||||
}
|
||||
24
Telegram/SourceFiles/data/data_game.h
Normal file
24
Telegram/SourceFiles/data/data_game.h
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
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_photo.h"
|
||||
#include "data/data_document.h"
|
||||
|
||||
struct GameData {
|
||||
GameData(not_null<Data::Session*> owner, const GameId &id);
|
||||
|
||||
const not_null<Data::Session*> owner;
|
||||
GameId id = 0;
|
||||
uint64 accessHash = 0;
|
||||
QString shortName;
|
||||
QString title;
|
||||
QString description;
|
||||
PhotoData *photo = nullptr;
|
||||
DocumentData *document = nullptr;
|
||||
};
|
||||
1159
Telegram/SourceFiles/data/data_group_call.cpp
Normal file
1159
Telegram/SourceFiles/data/data_group_call.cpp
Normal file
File diff suppressed because it is too large
Load Diff
327
Telegram/SourceFiles/data/data_group_call.h
Normal file
327
Telegram/SourceFiles/data/data_group_call.h
Normal file
@@ -0,0 +1,327 @@
|
||||
/*
|
||||
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 PeerData;
|
||||
|
||||
class ApiWrap;
|
||||
|
||||
namespace Calls {
|
||||
struct ParticipantVideoParams;
|
||||
} // namespace Calls
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace TdE2E {
|
||||
struct ParticipantState;
|
||||
struct UserId;
|
||||
} // namespace TdE2E
|
||||
|
||||
namespace Data {
|
||||
|
||||
[[nodiscard]] const std::string &RtmpEndpointId();
|
||||
|
||||
struct LastSpokeTimes {
|
||||
crl::time anything = 0;
|
||||
crl::time voice = 0;
|
||||
};
|
||||
|
||||
struct GroupCallParticipant {
|
||||
not_null<PeerData*> peer;
|
||||
std::shared_ptr<Calls::ParticipantVideoParams> videoParams;
|
||||
TimeId date = 0;
|
||||
TimeId lastActive = 0;
|
||||
uint64 raisedHandRating = 0;
|
||||
uint32 ssrc = 0;
|
||||
int volume = 0;
|
||||
bool sounding : 1 = false;
|
||||
bool speaking : 1 = false;
|
||||
bool additionalSounding : 1 = false;
|
||||
bool additionalSpeaking : 1 = false;
|
||||
bool muted : 1 = false;
|
||||
bool mutedByMe : 1 = false;
|
||||
bool canSelfUnmute : 1 = false;
|
||||
bool onlyMinLoaded : 1 = false;
|
||||
bool videoJoined = false;
|
||||
bool applyVolumeFromMin = true;
|
||||
|
||||
[[nodiscard]] const std::string &cameraEndpoint() const;
|
||||
[[nodiscard]] const std::string &screenEndpoint() const;
|
||||
[[nodiscard]] bool cameraPaused() const;
|
||||
[[nodiscard]] bool screenPaused() const;
|
||||
};
|
||||
|
||||
enum class GroupCallOrigin : uchar {
|
||||
Group,
|
||||
Conference,
|
||||
VideoStream,
|
||||
};
|
||||
|
||||
class GroupCall final {
|
||||
public:
|
||||
GroupCall(
|
||||
not_null<PeerData*> peer,
|
||||
CallId id,
|
||||
uint64 accessHash,
|
||||
TimeId scheduleDate,
|
||||
bool rtmp,
|
||||
GroupCallOrigin origin);
|
||||
~GroupCall();
|
||||
|
||||
[[nodiscard]] Main::Session &session() const;
|
||||
|
||||
[[nodiscard]] CallId id() const;
|
||||
[[nodiscard]] bool loaded() const;
|
||||
[[nodiscard]] rpl::producer<bool> loadedValue() const;
|
||||
[[nodiscard]] bool rtmp() const;
|
||||
[[nodiscard]] GroupCallOrigin origin() const;
|
||||
[[nodiscard]] bool creator() const;
|
||||
[[nodiscard]] bool canManage() const;
|
||||
[[nodiscard]] bool listenersHidden() const;
|
||||
[[nodiscard]] bool blockchainMayBeEmpty() const;
|
||||
[[nodiscard]] not_null<PeerData*> peer() const;
|
||||
[[nodiscard]] MTPInputGroupCall input() const;
|
||||
[[nodiscard]] QString title() const {
|
||||
return _title.current();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<QString> titleValue() const {
|
||||
return _title.value();
|
||||
}
|
||||
void setTitle(const QString &title) {
|
||||
_title = title;
|
||||
}
|
||||
[[nodiscard]] TimeId recordStartDate() const {
|
||||
return _recordStartDate.current();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<TimeId> recordStartDateValue() const {
|
||||
return _recordStartDate.value();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<TimeId> recordStartDateChanges() const {
|
||||
return _recordStartDate.changes();
|
||||
}
|
||||
[[nodiscard]] TimeId scheduleDate() const {
|
||||
return _scheduleDate.current();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<TimeId> scheduleDateValue() const {
|
||||
return _scheduleDate.value();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<TimeId> scheduleDateChanges() const {
|
||||
return _scheduleDate.changes();
|
||||
}
|
||||
[[nodiscard]] bool scheduleStartSubscribed() const {
|
||||
return _scheduleStartSubscribed.current();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<bool> scheduleStartSubscribedValue() const {
|
||||
return _scheduleStartSubscribed.value();
|
||||
}
|
||||
[[nodiscard]] int unmutedVideoLimit() const {
|
||||
return _unmutedVideoLimit.current();
|
||||
}
|
||||
[[nodiscard]] bool recordVideo() const {
|
||||
return _recordVideo;
|
||||
}
|
||||
|
||||
void setPeer(not_null<PeerData*> peer);
|
||||
|
||||
using Participant = GroupCallParticipant;
|
||||
struct ParticipantUpdate {
|
||||
std::optional<Participant> was;
|
||||
std::optional<Participant> now;
|
||||
};
|
||||
|
||||
static constexpr auto kSoundStatusKeptFor = crl::time(1500);
|
||||
|
||||
[[nodiscard]] auto participants() const
|
||||
-> const std::vector<Participant> &;
|
||||
void requestParticipants();
|
||||
[[nodiscard]] bool participantsLoaded() const;
|
||||
[[nodiscard]] PeerData *participantPeerByAudioSsrc(uint32 ssrc) const;
|
||||
[[nodiscard]] const Participant *participantByPeer(
|
||||
not_null<PeerData*> peer) const;
|
||||
[[nodiscard]] const Participant *participantByEndpoint(
|
||||
const std::string &endpoint) const;
|
||||
|
||||
[[nodiscard]] rpl::producer<> participantsReloaded();
|
||||
[[nodiscard]] auto participantUpdated() const
|
||||
-> rpl::producer<ParticipantUpdate>;
|
||||
[[nodiscard]] auto participantSpeaking() const
|
||||
-> rpl::producer<not_null<Participant*>>;
|
||||
|
||||
void setParticipantsWithAccess(base::flat_set<UserId> list);
|
||||
[[nodiscard]] auto participantsWithAccessCurrent() const
|
||||
-> const base::flat_set<UserId> &;
|
||||
[[nodiscard]] auto participantsWithAccessValue() const
|
||||
-> rpl::producer<base::flat_set<UserId>>;
|
||||
[[nodiscard]] auto staleParticipantIds() const
|
||||
-> rpl::producer<base::flat_set<UserId>>;
|
||||
void setParticipantsLoaded();
|
||||
void checkStaleParticipants();
|
||||
|
||||
void enqueueUpdate(const MTPUpdate &update);
|
||||
void applyLocalUpdate(
|
||||
const MTPDupdateGroupCallParticipants &update);
|
||||
|
||||
void applyLastSpoke(uint32 ssrc, LastSpokeTimes when, crl::time now);
|
||||
void applyActiveUpdate(
|
||||
PeerId participantPeerId,
|
||||
LastSpokeTimes when,
|
||||
PeerData *participantPeerLoaded);
|
||||
|
||||
void resolveParticipants(const base::flat_set<uint32> &ssrcs);
|
||||
[[nodiscard]] rpl::producer<
|
||||
not_null<const base::flat_map<
|
||||
uint32,
|
||||
LastSpokeTimes>*>> participantsResolved() const {
|
||||
return _participantsResolved.events();
|
||||
}
|
||||
|
||||
[[nodiscard]] int fullCount() const;
|
||||
[[nodiscard]] rpl::producer<int> fullCountValue() const;
|
||||
[[nodiscard]] QString conferenceInviteLink() const;
|
||||
|
||||
void setInCall();
|
||||
void reload();
|
||||
void reloadIfStale();
|
||||
void processFullCall(const MTPphone_GroupCall &call);
|
||||
|
||||
void setJoinMutedLocally(bool muted);
|
||||
[[nodiscard]] bool joinMuted() const;
|
||||
[[nodiscard]] bool canChangeJoinMuted() const;
|
||||
[[nodiscard]] bool joinedToTop() const;
|
||||
|
||||
void setMessagesEnabledLocally(bool enabled);
|
||||
[[nodiscard]] bool canChangeMessagesEnabled() const {
|
||||
return _canChangeMessagesEnabled;
|
||||
}
|
||||
[[nodiscard]] bool messagesEnabled() const {
|
||||
return _messagesEnabled.current();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<bool> messagesEnabledValue() const {
|
||||
return _messagesEnabled.value();
|
||||
}
|
||||
[[nodiscard]] int messagesMinPrice() const {
|
||||
return _messagesMinPrice.current();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<int> messagesMinPriceValue() const {
|
||||
return _messagesMinPrice.value();
|
||||
}
|
||||
|
||||
[[nodiscard]] not_null<PeerData*> resolveSendAs() const {
|
||||
return _savedSendAs.current();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<not_null<PeerData*>> sendAsValue() const {
|
||||
return _savedSendAs.value();
|
||||
}
|
||||
void saveSendAs(not_null<PeerData*> peer);
|
||||
|
||||
private:
|
||||
enum class ApplySliceSource {
|
||||
FullReloaded,
|
||||
SliceLoaded,
|
||||
UnknownLoaded,
|
||||
UpdateReceived,
|
||||
UpdateConstructed,
|
||||
};
|
||||
enum class QueuedType : uint8 {
|
||||
VersionedParticipant,
|
||||
Participant,
|
||||
Call,
|
||||
};
|
||||
[[nodiscard]] ApiWrap &api() const;
|
||||
|
||||
void discard(const MTPDgroupCallDiscarded &data);
|
||||
[[nodiscard]] bool inCall() const;
|
||||
void applyParticipantsSlice(
|
||||
const QVector<MTPGroupCallParticipant> &list,
|
||||
ApplySliceSource sliceSource);
|
||||
void requestUnknownParticipants();
|
||||
void changePeerEmptyCallFlag();
|
||||
void checkFinishSpeakingByActive();
|
||||
void applyCallFields(const MTPDgroupCall &data);
|
||||
void applyEnqueuedUpdate(const MTPUpdate &update);
|
||||
void setServerParticipantsCount(int count);
|
||||
void computeParticipantsCount();
|
||||
void processQueuedUpdates(bool initial = false);
|
||||
void processFullCallUsersChats(const MTPphone_GroupCall &call);
|
||||
void processFullCallFields(const MTPphone_GroupCall &call);
|
||||
[[nodiscard]] bool requestParticipantsAfterReload(
|
||||
const MTPphone_GroupCall &call) const;
|
||||
[[nodiscard]] bool processSavedFullCall();
|
||||
void finishParticipantsSliceRequest();
|
||||
[[nodiscard]] Participant *findParticipant(not_null<PeerData*> peer);
|
||||
|
||||
const CallId _id = 0;
|
||||
const uint64 _accessHash = 0;
|
||||
|
||||
not_null<PeerData*> _peer;
|
||||
int _version = 0;
|
||||
rpl::event_stream<bool> _loadedChanges;
|
||||
mtpRequestId _participantsRequestId = 0;
|
||||
mtpRequestId _reloadRequestId = 0;
|
||||
crl::time _reloadLastFinished = 0;
|
||||
rpl::variable<QString> _title;
|
||||
QString _conferenceInviteLink;
|
||||
|
||||
base::flat_multi_map<
|
||||
std::pair<int, QueuedType>,
|
||||
MTPUpdate> _queuedUpdates;
|
||||
base::Timer _reloadByQueuedUpdatesTimer;
|
||||
std::optional<MTPphone_GroupCall> _savedFull;
|
||||
|
||||
std::vector<Participant> _participants;
|
||||
base::flat_map<uint32, not_null<PeerData*>> _participantPeerByAudioSsrc;
|
||||
base::flat_map<not_null<PeerData*>, crl::time> _speakingByActiveFinishes;
|
||||
base::Timer _speakingByActiveFinishTimer;
|
||||
QString _nextOffset;
|
||||
int _serverParticipantsCount = 0;
|
||||
rpl::variable<int> _fullCount = 0;
|
||||
rpl::variable<int> _unmutedVideoLimit = 0;
|
||||
rpl::variable<bool> _messagesEnabled = false;
|
||||
rpl::variable<int> _messagesMinPrice = 0;
|
||||
rpl::variable<TimeId> _recordStartDate = 0;
|
||||
rpl::variable<TimeId> _scheduleDate = 0;
|
||||
rpl::variable<bool> _scheduleStartSubscribed = false;
|
||||
|
||||
base::flat_map<uint32, LastSpokeTimes> _unknownSpokenSsrcs;
|
||||
base::flat_map<PeerId, LastSpokeTimes> _unknownSpokenPeerIds;
|
||||
rpl::event_stream<
|
||||
not_null<const base::flat_map<
|
||||
uint32,
|
||||
LastSpokeTimes>*>> _participantsResolved;
|
||||
mtpRequestId _unknownParticipantPeersRequestId = 0;
|
||||
|
||||
rpl::event_stream<ParticipantUpdate> _participantUpdates;
|
||||
rpl::event_stream<not_null<Participant*>> _participantSpeaking;
|
||||
rpl::event_stream<> _participantsReloaded;
|
||||
|
||||
rpl::variable<base::flat_set<UserId>> _participantsWithAccess;
|
||||
rpl::event_stream<base::flat_set<UserId>> _staleParticipantIds;
|
||||
rpl::lifetime _checkStaleLifetime;
|
||||
|
||||
rpl::variable<not_null<PeerData*>> _savedSendAs;
|
||||
|
||||
bool _creator : 1 = false;
|
||||
bool _joinMuted : 1 = false;
|
||||
bool _recordVideo : 1 = false;
|
||||
bool _canChangeJoinMuted : 1 = true;
|
||||
bool _canChangeMessagesEnabled : 1 = true;
|
||||
bool _allParticipantsLoaded : 1 = false;
|
||||
bool _joinedToTop : 1 = false;
|
||||
bool _applyingQueuedUpdates : 1 = false;
|
||||
bool _rtmp : 1 = false;
|
||||
bool _conference : 1 = false;
|
||||
bool _videoStream : 1 = false;
|
||||
bool _listenersHidden : 1 = false;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Data
|
||||
177
Telegram/SourceFiles/data/data_groups.cpp
Normal file
177
Telegram/SourceFiles/data/data_groups.cpp
Normal file
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
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/data_groups.h"
|
||||
|
||||
#include "history/history.h"
|
||||
#include "history/history_item.h"
|
||||
#include "dialogs/ui/dialogs_message_view.h"
|
||||
#include "data/data_media_types.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_forum.h"
|
||||
#include "data/data_forum_topic.h"
|
||||
|
||||
namespace Data {
|
||||
namespace {
|
||||
|
||||
constexpr auto kMaxItemsInGroup = 10;
|
||||
|
||||
} // namespace
|
||||
|
||||
Groups::Groups(not_null<Session*> data) : _data(data) {
|
||||
}
|
||||
|
||||
bool Groups::isGrouped(not_null<const HistoryItem*> item) const {
|
||||
if (!item->groupId()) {
|
||||
return false;
|
||||
}
|
||||
const auto media = item->media();
|
||||
return media && media->canBeGrouped();
|
||||
}
|
||||
|
||||
bool Groups::isGroupOfOne(not_null<const HistoryItem*> item) const {
|
||||
if (const auto groupId = item->groupId()) {
|
||||
const auto i = _groups.find(groupId);
|
||||
return (i != _groups.end()) && (i->second.items.size() == 1);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Groups::registerMessage(not_null<HistoryItem*> item) {
|
||||
if (!isGrouped(item)) {
|
||||
return;
|
||||
}
|
||||
const auto i = _groups.emplace(item->groupId(), Group()).first;
|
||||
auto &items = i->second.items;
|
||||
if (items.size() < kMaxItemsInGroup) {
|
||||
items.insert(findPositionForItem(items, item), item);
|
||||
if (items.size() > 1) {
|
||||
refreshViews(items);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Groups::unregisterMessage(not_null<const HistoryItem*> item) {
|
||||
const auto groupId = item->groupId();
|
||||
if (!groupId) {
|
||||
return;
|
||||
}
|
||||
const auto i = _groups.find(groupId);
|
||||
if (i != end(_groups)) {
|
||||
auto &items = i->second.items;
|
||||
const auto removed = ranges::remove(items, item);
|
||||
const auto last = end(items);
|
||||
if (removed != last) {
|
||||
items.erase(removed, last);
|
||||
if (!items.empty()) {
|
||||
refreshViews(items);
|
||||
} else {
|
||||
_groups.erase(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Groups::refreshMessage(
|
||||
not_null<HistoryItem*> item,
|
||||
bool justRefreshViews) {
|
||||
if (!isGrouped(item)) {
|
||||
unregisterMessage(item);
|
||||
_data->requestItemViewRefresh(item);
|
||||
return;
|
||||
}
|
||||
if (!item->isRegular() && !item->isScheduled() && !item->isUploading()) {
|
||||
return;
|
||||
}
|
||||
const auto groupId = item->groupId();
|
||||
const auto i = _groups.find(groupId);
|
||||
if (i == end(_groups)) {
|
||||
registerMessage(item);
|
||||
return;
|
||||
}
|
||||
auto &items = i->second.items;
|
||||
|
||||
if (justRefreshViews) {
|
||||
refreshViews(items);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto position = findPositionForItem(items, item);
|
||||
auto current = ranges::find(items, item);
|
||||
if (current == end(items)) {
|
||||
items.insert(position, item);
|
||||
} else if (position == current + 1) {
|
||||
return;
|
||||
} else if (position > current + 1) {
|
||||
for (++current; current != position; ++current) {
|
||||
std::swap(*(current - 1), *current);
|
||||
}
|
||||
} else if (position < current) {
|
||||
for (; current != position; --current) {
|
||||
std::swap(*(current - 1), *current);
|
||||
}
|
||||
} else {
|
||||
Unexpected("Position of item in Groups::refreshMessage().");
|
||||
}
|
||||
refreshViews(items);
|
||||
}
|
||||
|
||||
HistoryItemsList::const_iterator Groups::findPositionForItem(
|
||||
const HistoryItemsList &group,
|
||||
not_null<HistoryItem*> item) {
|
||||
const auto last = end(group);
|
||||
const auto itemId = item->id;
|
||||
for (auto result = begin(group); result != last; ++result) {
|
||||
if ((*result)->id > itemId) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return last;
|
||||
}
|
||||
|
||||
const Group *Groups::find(not_null<const HistoryItem*> item) const {
|
||||
const auto groupId = item->groupId();
|
||||
if (!groupId) {
|
||||
return nullptr;
|
||||
}
|
||||
const auto i = _groups.find(groupId);
|
||||
if (i != _groups.end()) {
|
||||
const auto &result = i->second;
|
||||
if (result.items.size() > 1) {
|
||||
return &result;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void Groups::refreshViews(const HistoryItemsList &items) {
|
||||
if (items.empty()) {
|
||||
return;
|
||||
}
|
||||
for (const auto &item : items) {
|
||||
_data->requestItemViewRefresh(item);
|
||||
item->invalidateChatListEntry();
|
||||
}
|
||||
}
|
||||
|
||||
not_null<HistoryItem*> Groups::findItemToEdit(
|
||||
not_null<HistoryItem*> item) const {
|
||||
const auto group = find(item);
|
||||
if (!group) {
|
||||
return item;
|
||||
}
|
||||
const auto &list = group->items;
|
||||
const auto it = ranges::find_if(
|
||||
list,
|
||||
ranges::not_fn(&HistoryItem::emptyText));
|
||||
if (it == end(list)) {
|
||||
return list.front();
|
||||
}
|
||||
return (*it);
|
||||
}
|
||||
|
||||
} // namespace Data
|
||||
47
Telegram/SourceFiles/data/data_groups.h
Normal file
47
Telegram/SourceFiles/data/data_groups.h
Normal file
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
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_types.h"
|
||||
|
||||
namespace Data {
|
||||
|
||||
class Session;
|
||||
|
||||
struct Group {
|
||||
HistoryItemsList items;
|
||||
};
|
||||
|
||||
class Groups {
|
||||
public:
|
||||
Groups(not_null<Session*> data);
|
||||
|
||||
[[nodiscard]] bool isGrouped(not_null<const HistoryItem*> item) const;
|
||||
[[nodiscard]] bool isGroupOfOne(not_null<const HistoryItem*> item) const;
|
||||
void registerMessage(not_null<HistoryItem*> item);
|
||||
void unregisterMessage(not_null<const HistoryItem*> item);
|
||||
void refreshMessage(
|
||||
not_null<HistoryItem*> item,
|
||||
bool justRefreshViews = false);
|
||||
|
||||
[[nodiscard]] const Group *find(not_null<const HistoryItem*> item) const;
|
||||
|
||||
not_null<HistoryItem*> findItemToEdit(not_null<HistoryItem*> item) const;
|
||||
|
||||
private:
|
||||
HistoryItemsList::const_iterator findPositionForItem(
|
||||
const HistoryItemsList &group,
|
||||
not_null<HistoryItem*> item);
|
||||
void refreshViews(const HistoryItemsList &items);
|
||||
|
||||
not_null<Session*> _data;
|
||||
std::map<MessageGroupId, Group> _groups;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Data
|
||||
1275
Telegram/SourceFiles/data/data_histories.cpp
Normal file
1275
Telegram/SourceFiles/data/data_histories.cpp
Normal file
File diff suppressed because it is too large
Load Diff
252
Telegram/SourceFiles/data/data_histories.h
Normal file
252
Telegram/SourceFiles/data/data_histories.h
Normal file
@@ -0,0 +1,252 @@
|
||||
/*
|
||||
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;
|
||||
class HistoryItem;
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace MTP {
|
||||
class Error;
|
||||
struct Response;
|
||||
} // namespace MTP
|
||||
|
||||
namespace Data {
|
||||
|
||||
class Session;
|
||||
class Folder;
|
||||
struct WebPageDraft;
|
||||
class SavedSublist;
|
||||
|
||||
[[nodiscard]] MTPInputReplyTo ReplyToForMTP(
|
||||
not_null<History*> history,
|
||||
FullReplyTo replyTo);
|
||||
[[nodiscard]] MTPInputMedia WebPageForMTP(
|
||||
const Data::WebPageDraft &draft,
|
||||
bool required = false);
|
||||
|
||||
class Histories final {
|
||||
public:
|
||||
enum class RequestType : uchar {
|
||||
None,
|
||||
History,
|
||||
ReadInbox,
|
||||
Delete,
|
||||
Send,
|
||||
};
|
||||
|
||||
explicit Histories(not_null<Session*> owner);
|
||||
|
||||
[[nodiscard]] Session &owner() const;
|
||||
[[nodiscard]] Main::Session &session() const;
|
||||
|
||||
[[nodiscard]] History *find(PeerId peerId);
|
||||
[[nodiscard]] not_null<History*> findOrCreate(PeerId peerId);
|
||||
|
||||
void applyPeerDialogs(const MTPmessages_PeerDialogs &dialogs);
|
||||
|
||||
void unloadAll();
|
||||
void clearAll();
|
||||
|
||||
void readInbox(not_null<History*> history);
|
||||
void readInboxTill(not_null<HistoryItem*> item);
|
||||
void readInboxTill(not_null<History*> history, MsgId tillId);
|
||||
void readInboxOnNewMessage(not_null<HistoryItem*> item);
|
||||
void readClientSideMessage(not_null<HistoryItem*> item);
|
||||
void sendPendingReadInbox(not_null<History*> history);
|
||||
void reportDelivery(not_null<HistoryItem*> item);
|
||||
|
||||
void requestDialogEntry(not_null<Data::Folder*> folder);
|
||||
void requestDialogEntry(
|
||||
not_null<History*> history,
|
||||
Fn<void()> callback = nullptr);
|
||||
void dialogEntryApplied(not_null<History*> history);
|
||||
void changeDialogUnreadMark(not_null<History*> history, bool unread);
|
||||
void changeSublistUnreadMark(
|
||||
not_null<Data::SavedSublist*> sublist,
|
||||
bool unread);
|
||||
void requestFakeChatListMessage(not_null<History*> history);
|
||||
|
||||
void requestGroupAround(not_null<HistoryItem*> item);
|
||||
|
||||
void deleteMessages(
|
||||
not_null<History*> history,
|
||||
const QVector<MTPint> &ids,
|
||||
bool revoke);
|
||||
void deleteAllMessages(
|
||||
not_null<History*> history,
|
||||
MsgId deleteTillId,
|
||||
bool justClear,
|
||||
bool revoke);
|
||||
|
||||
void deleteMessagesByDates(
|
||||
not_null<History*> history,
|
||||
QDate firstDayToDelete,
|
||||
QDate lastDayToDelete,
|
||||
bool revoke);
|
||||
void deleteMessagesByDates(
|
||||
not_null<History*> history,
|
||||
TimeId minDate,
|
||||
TimeId maxDate,
|
||||
bool revoke);
|
||||
|
||||
void deleteMessages(const MessageIdsList &ids, bool revoke);
|
||||
|
||||
int sendRequest(
|
||||
not_null<History*> history,
|
||||
RequestType type,
|
||||
Fn<mtpRequestId(Fn<void()> finish)> generator);
|
||||
void cancelRequest(int id);
|
||||
|
||||
using PreparedMessage = std::variant<
|
||||
MTPmessages_SendMessage,
|
||||
MTPmessages_SendMedia,
|
||||
MTPmessages_SendInlineBotResult,
|
||||
MTPmessages_SendMultiMedia>;
|
||||
int sendPreparedMessage(
|
||||
not_null<History*> history,
|
||||
FullReplyTo replyTo,
|
||||
uint64 randomId,
|
||||
Fn<PreparedMessage(not_null<History*>, FullReplyTo)> message,
|
||||
Fn<void(const MTPUpdates&, const MTP::Response&)> done,
|
||||
Fn<void(const MTP::Error&, const MTP::Response&)> fail);
|
||||
|
||||
struct ReplyToPlaceholder {
|
||||
};
|
||||
template <typename RequestType, typename ...Args>
|
||||
static auto PrepareMessage(const Args &...args)
|
||||
-> Fn<Histories::PreparedMessage(not_null<History*>, FullReplyTo)> {
|
||||
return [=](not_null<History*> history, FullReplyTo replyTo)
|
||||
-> RequestType {
|
||||
return { ReplaceReplyIds(history, args, replyTo)... };
|
||||
};
|
||||
}
|
||||
|
||||
void checkTopicCreated(FullMsgId rootId, MsgId realRoot);
|
||||
[[nodiscard]] FullMsgId convertTopicReplyToId(
|
||||
not_null<History*> history,
|
||||
FullMsgId replyToId) const;
|
||||
[[nodiscard]] MsgId convertTopicReplyToId(
|
||||
not_null<History*> history,
|
||||
MsgId replyToId) const;
|
||||
|
||||
private:
|
||||
struct PostponedHistoryRequest {
|
||||
Fn<mtpRequestId(Fn<void()> finish)> generator;
|
||||
};
|
||||
struct SentRequest {
|
||||
Fn<mtpRequestId(Fn<void()> finish)> generator;
|
||||
mtpRequestId id = 0;
|
||||
RequestType type = RequestType::None;
|
||||
};
|
||||
struct State {
|
||||
base::flat_map<int, PostponedHistoryRequest> postponed;
|
||||
base::flat_map<int, SentRequest> sent;
|
||||
MsgId willReadTill = 0;
|
||||
MsgId sentReadTill = 0;
|
||||
crl::time willReadWhen = 0;
|
||||
bool sentReadDone = false;
|
||||
bool postponedRequestEntry = false;
|
||||
};
|
||||
struct ChatListGroupRequest {
|
||||
MsgId aroundId = 0;
|
||||
mtpRequestId requestId = 0;
|
||||
};
|
||||
struct DelayedByTopicMessage {
|
||||
uint64 randomId = 0;
|
||||
FullMsgId replyTo;
|
||||
Fn<PreparedMessage(not_null<History*>, FullReplyTo)> message;
|
||||
Fn<void(const MTPUpdates&, const MTP::Response&)> done;
|
||||
Fn<void(const MTP::Error&, const MTP::Response&)> fail;
|
||||
int requestId = 0;
|
||||
};
|
||||
struct GroupRequestKey {
|
||||
not_null<History*> history;
|
||||
MsgId rootId = 0;
|
||||
|
||||
friend inline auto operator<=>(
|
||||
GroupRequestKey,
|
||||
GroupRequestKey) = default;
|
||||
};
|
||||
|
||||
template <typename Arg>
|
||||
static auto ReplaceReplyIds(
|
||||
not_null<History*> history,
|
||||
Arg arg,
|
||||
FullReplyTo replyTo) {
|
||||
if constexpr (std::is_same_v<Arg, ReplyToPlaceholder>) {
|
||||
return ReplyToForMTP(history, replyTo);
|
||||
} else {
|
||||
return arg;
|
||||
}
|
||||
}
|
||||
|
||||
void readInboxTill(not_null<History*> history, MsgId tillId, bool force);
|
||||
void sendReadRequests();
|
||||
void sendReadRequest(not_null<History*> history, State &state);
|
||||
[[nodiscard]] State *lookup(not_null<History*> history);
|
||||
void checkEmptyState(not_null<History*> history);
|
||||
void checkPostponed(not_null<History*> history, int id);
|
||||
void finishSentRequest(
|
||||
not_null<History*> history,
|
||||
not_null<State*> state,
|
||||
int id);
|
||||
[[nodiscard]] bool postponeHistoryRequest(const State &state) const;
|
||||
[[nodiscard]] bool postponeEntryRequest(const State &state) const;
|
||||
void postponeRequestDialogEntries();
|
||||
|
||||
void sendDialogRequests();
|
||||
void reportPendingDeliveries();
|
||||
|
||||
[[nodiscard]] bool isCreatingTopic(
|
||||
not_null<History*> history,
|
||||
MsgId rootId) const;
|
||||
void sendCreateTopicRequest(not_null<History*> history, MsgId rootId);
|
||||
void cancelDelayedByTopicRequest(int id);
|
||||
|
||||
const not_null<Session*> _owner;
|
||||
|
||||
std::unordered_map<PeerId, std::unique_ptr<History>> _map;
|
||||
base::flat_map<not_null<History*>, State> _states;
|
||||
base::flat_map<int, not_null<History*>> _historyByRequest;
|
||||
int _requestAutoincrement = 0;
|
||||
base::Timer _readRequestsTimer;
|
||||
|
||||
base::flat_set<not_null<Data::Folder*>> _dialogFolderRequests;
|
||||
base::flat_map<
|
||||
not_null<History*>,
|
||||
std::vector<Fn<void()>>> _dialogRequests;
|
||||
base::flat_map<
|
||||
not_null<History*>,
|
||||
std::vector<Fn<void()>>> _dialogRequestsPending;
|
||||
|
||||
base::flat_set<not_null<History*>> _fakeChatListRequests;
|
||||
|
||||
base::flat_map<
|
||||
GroupRequestKey,
|
||||
ChatListGroupRequest> _chatListGroupRequests;
|
||||
|
||||
base::flat_map<
|
||||
FullMsgId,
|
||||
std::vector<DelayedByTopicMessage>> _creatingTopics;
|
||||
base::flat_map<FullMsgId, MsgId> _createdTopicIds;
|
||||
base::flat_set<mtpRequestId> _creatingTopicRequests;
|
||||
|
||||
base::flat_map<
|
||||
not_null<PeerData*>,
|
||||
base::flat_set<MsgId>> _pendingDeliveryReport;
|
||||
base::flat_set<not_null<PeerData*>> _deliveryReportSent;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Data
|
||||
220
Telegram/SourceFiles/data/data_history_messages.cpp
Normal file
220
Telegram/SourceFiles/data/data_history_messages.cpp
Normal file
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
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/data_history_messages.h"
|
||||
|
||||
#include "apiwrap.h"
|
||||
#include "data/data_chat.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_sparse_ids.h"
|
||||
#include "history/history.h"
|
||||
#include "main/main_session.h"
|
||||
|
||||
namespace Data {
|
||||
|
||||
void HistoryMessages::addNew(MsgId messageId) {
|
||||
_chat.addNew(messageId);
|
||||
}
|
||||
|
||||
void HistoryMessages::addExisting(MsgId messageId, MsgRange noSkipRange) {
|
||||
_chat.addExisting(messageId, noSkipRange);
|
||||
}
|
||||
|
||||
void HistoryMessages::addSlice(
|
||||
std::vector<MsgId> &&messageIds,
|
||||
MsgRange noSkipRange,
|
||||
std::optional<int> count) {
|
||||
_chat.addSlice(std::move(messageIds), noSkipRange, count);
|
||||
}
|
||||
|
||||
void HistoryMessages::removeOne(MsgId messageId) {
|
||||
_chat.removeOne(messageId);
|
||||
_oneRemoved.fire_copy(messageId);
|
||||
}
|
||||
|
||||
void HistoryMessages::removeAll() {
|
||||
_chat.removeAll();
|
||||
_allRemoved.fire({});
|
||||
}
|
||||
|
||||
void HistoryMessages::invalidateBottom() {
|
||||
_chat.invalidateBottom();
|
||||
_bottomInvalidated.fire({});
|
||||
}
|
||||
|
||||
Storage::SparseIdsListResult HistoryMessages::snapshot(
|
||||
const Storage::SparseIdsListQuery &query) const {
|
||||
return _chat.snapshot(query);
|
||||
}
|
||||
|
||||
auto HistoryMessages::sliceUpdated() const
|
||||
-> rpl::producer<Storage::SparseIdsSliceUpdate> {
|
||||
return _chat.sliceUpdated();
|
||||
}
|
||||
|
||||
rpl::producer<MsgId> HistoryMessages::oneRemoved() const {
|
||||
return _oneRemoved.events();
|
||||
}
|
||||
|
||||
rpl::producer<> HistoryMessages::allRemoved() const {
|
||||
return _allRemoved.events();
|
||||
}
|
||||
|
||||
rpl::producer<> HistoryMessages::bottomInvalidated() const {
|
||||
return _bottomInvalidated.events();
|
||||
}
|
||||
|
||||
rpl::producer<SparseIdsSlice> HistoryViewer(
|
||||
not_null<History*> history,
|
||||
MsgId aroundId,
|
||||
int limitBefore,
|
||||
int limitAfter) {
|
||||
Expects(IsServerMsgId(aroundId) || (aroundId == 0));
|
||||
Expects((aroundId != 0) || (limitBefore == 0 && limitAfter == 0));
|
||||
|
||||
return [=](auto consumer) {
|
||||
auto lifetime = rpl::lifetime();
|
||||
|
||||
const auto messages = &history->messages();
|
||||
|
||||
auto builder = lifetime.make_state<SparseIdsSliceBuilder>(
|
||||
aroundId,
|
||||
limitBefore,
|
||||
limitAfter);
|
||||
using RequestAroundInfo = SparseIdsSliceBuilder::AroundData;
|
||||
builder->insufficientAround(
|
||||
) | rpl::on_next([=](const RequestAroundInfo &info) {
|
||||
if (!info.aroundId) {
|
||||
// Ignore messages-count-only requests, because we perform
|
||||
// them with non-zero limit of messages and end up adding
|
||||
// a broken slice with several last messages from the chat
|
||||
// with a non-skip range starting at zero.
|
||||
return;
|
||||
}
|
||||
history->session().api().requestHistory(
|
||||
history,
|
||||
info.aroundId,
|
||||
info.direction);
|
||||
}, lifetime);
|
||||
|
||||
auto pushNextSnapshot = [=] {
|
||||
consumer.put_next(builder->snapshot());
|
||||
};
|
||||
|
||||
using SliceUpdate = Storage::SparseIdsSliceUpdate;
|
||||
messages->sliceUpdated(
|
||||
) | rpl::filter([=](const SliceUpdate &update) {
|
||||
return builder->applyUpdate(update);
|
||||
}) | rpl::on_next(pushNextSnapshot, lifetime);
|
||||
|
||||
messages->oneRemoved(
|
||||
) | rpl::filter([=](MsgId messageId) {
|
||||
return builder->removeOne(messageId);
|
||||
}) | rpl::on_next(pushNextSnapshot, lifetime);
|
||||
|
||||
messages->allRemoved(
|
||||
) | rpl::filter([=] {
|
||||
return builder->removeAll();
|
||||
}) | rpl::on_next(pushNextSnapshot, lifetime);
|
||||
|
||||
messages->bottomInvalidated(
|
||||
) | rpl::filter([=] {
|
||||
return builder->invalidateBottom();
|
||||
}) | rpl::on_next(pushNextSnapshot, lifetime);
|
||||
|
||||
const auto snapshot = messages->snapshot({
|
||||
aroundId,
|
||||
limitBefore,
|
||||
limitAfter,
|
||||
});
|
||||
if (snapshot.count || !snapshot.messageIds.empty()) {
|
||||
if (builder->applyInitial(snapshot)) {
|
||||
pushNextSnapshot();
|
||||
}
|
||||
}
|
||||
builder->checkInsufficient();
|
||||
|
||||
return lifetime;
|
||||
};
|
||||
}
|
||||
|
||||
rpl::producer<SparseIdsMergedSlice> HistoryMergedViewer(
|
||||
not_null<History*> history,
|
||||
/*Universal*/MsgId universalAroundId,
|
||||
int limitBefore,
|
||||
int limitAfter) {
|
||||
const auto migrateFrom = history->peer->migrateFrom();
|
||||
auto createSimpleViewer = [=](
|
||||
PeerId peerId,
|
||||
MsgId topicRootId,
|
||||
PeerId monoforumPeerId,
|
||||
SparseIdsSlice::Key simpleKey,
|
||||
int limitBefore,
|
||||
int limitAfter) {
|
||||
const auto chosen = (history->peer->id == peerId)
|
||||
? history
|
||||
: history->owner().history(peerId);
|
||||
return HistoryViewer(chosen, simpleKey, limitBefore, limitAfter);
|
||||
};
|
||||
const auto peerId = history->peer->id;
|
||||
const auto migratedPeerId = migrateFrom ? migrateFrom->id : PeerId(0);
|
||||
using Key = SparseIdsMergedSlice::Key;
|
||||
return SparseIdsMergedSlice::CreateViewer(
|
||||
Key(peerId, MsgId(), PeerId(), migratedPeerId, universalAroundId),
|
||||
limitBefore,
|
||||
limitAfter,
|
||||
std::move(createSimpleViewer));
|
||||
}
|
||||
|
||||
rpl::producer<MessagesSlice> HistoryMessagesViewer(
|
||||
not_null<History*> history,
|
||||
MessagePosition aroundId,
|
||||
int limitBefore,
|
||||
int limitAfter) {
|
||||
const auto computeUnreadAroundId = [&] {
|
||||
if (const auto migrated = history->migrateFrom()) {
|
||||
if (const auto around = migrated->loadAroundId()) {
|
||||
return MsgId(around - ServerMaxMsgId);
|
||||
}
|
||||
}
|
||||
if (const auto around = history->loadAroundId()) {
|
||||
return around;
|
||||
}
|
||||
return MsgId(ServerMaxMsgId - 1);
|
||||
};
|
||||
const auto messageId = (aroundId.fullId.msg == ShowAtUnreadMsgId)
|
||||
? computeUnreadAroundId()
|
||||
: ((aroundId.fullId.msg == ShowAtTheEndMsgId)
|
||||
|| (aroundId == MaxMessagePosition))
|
||||
? (ServerMaxMsgId - 1)
|
||||
: (aroundId.fullId.peer == history->peer->id)
|
||||
? aroundId.fullId.msg
|
||||
: (aroundId.fullId.msg - ServerMaxMsgId);
|
||||
return HistoryMergedViewer(
|
||||
history,
|
||||
messageId,
|
||||
limitBefore,
|
||||
limitAfter
|
||||
) | rpl::map([=](SparseIdsMergedSlice &&slice) {
|
||||
auto result = Data::MessagesSlice();
|
||||
result.fullCount = slice.fullCount();
|
||||
result.skippedAfter = slice.skippedAfter();
|
||||
result.skippedBefore = slice.skippedBefore();
|
||||
const auto count = slice.size();
|
||||
result.ids.reserve(count);
|
||||
if (const auto msgId = slice.nearest(messageId)) {
|
||||
result.nearestToAround = *msgId;
|
||||
}
|
||||
for (auto i = 0; i != count; ++i) {
|
||||
result.ids.push_back(slice[i]);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace Data
|
||||
67
Telegram/SourceFiles/data/data_history_messages.h
Normal file
67
Telegram/SourceFiles/data/data_history_messages.h
Normal file
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
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 "storage/storage_sparse_ids_list.h"
|
||||
|
||||
class History;
|
||||
class SparseIdsSlice;
|
||||
class SparseIdsMergedSlice;
|
||||
|
||||
namespace Data {
|
||||
|
||||
struct MessagesSlice;
|
||||
struct MessagePosition;
|
||||
|
||||
class HistoryMessages final {
|
||||
public:
|
||||
void addNew(MsgId messageId);
|
||||
void addExisting(MsgId messageId, MsgRange noSkipRange);
|
||||
void addSlice(
|
||||
std::vector<MsgId> &&messageIds,
|
||||
MsgRange noSkipRange,
|
||||
std::optional<int> count);
|
||||
void removeOne(MsgId messageId);
|
||||
void removeAll();
|
||||
void invalidateBottom();
|
||||
|
||||
[[nodiscard]] Storage::SparseIdsListResult snapshot(
|
||||
const Storage::SparseIdsListQuery &query) const;
|
||||
[[nodiscard]] auto sliceUpdated() const
|
||||
-> rpl::producer<Storage::SparseIdsSliceUpdate>;
|
||||
[[nodiscard]] rpl::producer<MsgId> oneRemoved() const;
|
||||
[[nodiscard]] rpl::producer<> allRemoved() const;
|
||||
[[nodiscard]] rpl::producer<> bottomInvalidated() const;
|
||||
|
||||
private:
|
||||
Storage::SparseIdsList _chat;
|
||||
rpl::event_stream<MsgId> _oneRemoved;
|
||||
rpl::event_stream<> _allRemoved;
|
||||
rpl::event_stream<> _bottomInvalidated;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] rpl::producer<SparseIdsSlice> HistoryViewer(
|
||||
not_null<History*> history,
|
||||
MsgId aroundId,
|
||||
int limitBefore,
|
||||
int limitAfter);
|
||||
|
||||
[[nodiscard]] rpl::producer<SparseIdsMergedSlice> HistoryMergedViewer(
|
||||
not_null<History*> history,
|
||||
/*Universal*/MsgId universalAroundId,
|
||||
int limitBefore,
|
||||
int limitAfter);
|
||||
|
||||
[[nodiscard]] rpl::producer<MessagesSlice> HistoryMessagesViewer(
|
||||
not_null<History*> history,
|
||||
MessagePosition aroundId,
|
||||
int limitBefore,
|
||||
int limitAfter);
|
||||
|
||||
} // namespace Data
|
||||
132
Telegram/SourceFiles/data/data_lastseen_status.h
Normal file
132
Telegram/SourceFiles/data/data_lastseen_status.h
Normal file
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
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 {
|
||||
|
||||
inline constexpr auto kLifeStartDate = 1375315200; // Let it be 01.08.2013.
|
||||
|
||||
class LastseenStatus final {
|
||||
public:
|
||||
LastseenStatus() = default;
|
||||
|
||||
[[nodiscard]] static LastseenStatus Recently(bool byMe = false) {
|
||||
return LastseenStatus(kRecentlyValue, false, byMe);
|
||||
}
|
||||
[[nodiscard]] static LastseenStatus WithinWeek(bool byMe = false) {
|
||||
return LastseenStatus(kWithinWeekValue, false, byMe);
|
||||
}
|
||||
[[nodiscard]] static LastseenStatus WithinMonth(bool byMe = false) {
|
||||
return LastseenStatus(kWithinMonthValue, false, byMe);
|
||||
}
|
||||
[[nodiscard]] static LastseenStatus LongAgo(bool byMe = false) {
|
||||
return LastseenStatus(kLongAgoValue, false, byMe);
|
||||
}
|
||||
[[nodiscard]] static LastseenStatus OnlineTill(
|
||||
TimeId till,
|
||||
bool local = false,
|
||||
bool hiddenByMe = false) {
|
||||
return (till >= kLifeStartDate + kSpecialValueSkip)
|
||||
? LastseenStatus(till - kLifeStartDate, !local, hiddenByMe)
|
||||
: LongAgo(hiddenByMe);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool isHidden() const {
|
||||
return !_available;
|
||||
}
|
||||
[[nodiscard]] bool isRecently() const {
|
||||
return !_available && (_value == kRecentlyValue);
|
||||
}
|
||||
[[nodiscard]] bool isWithinWeek() const {
|
||||
return !_available && (_value == kWithinWeekValue);
|
||||
}
|
||||
[[nodiscard]] bool isWithinMonth() const {
|
||||
return !_available && (_value == kWithinMonthValue);
|
||||
}
|
||||
[[nodiscard]] bool isLongAgo() const {
|
||||
return !_available && (_value == kLongAgoValue);
|
||||
}
|
||||
[[nodiscard]] bool isHiddenByMe() const {
|
||||
return _hiddenByMe;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool isOnline(TimeId now) const {
|
||||
return (_value >= kSpecialValueSkip)
|
||||
&& (kLifeStartDate + _value > now);
|
||||
}
|
||||
[[nodiscard]] bool isLocalOnlineValue() const {
|
||||
return !_available && (_value >= kSpecialValueSkip);
|
||||
}
|
||||
[[nodiscard]] TimeId onlineTill() const {
|
||||
return (_value >= kSpecialValueSkip)
|
||||
? (kLifeStartDate + _value)
|
||||
: 0;
|
||||
}
|
||||
|
||||
[[nodiscard]] uint32 serialize() const {
|
||||
return (_value & 0x3FFFFFFF)
|
||||
| (_available << 30)
|
||||
| (_hiddenByMe << 31);
|
||||
}
|
||||
[[nodiscard]] static LastseenStatus FromSerialized(uint32 value) {
|
||||
auto result = LastseenStatus();
|
||||
result._value = value & 0x3FFFFFFF;
|
||||
result._available = (value >> 30) & 1;
|
||||
result._hiddenByMe = (value >> 31) & 1;
|
||||
return result.valid() ? result : LastseenStatus();
|
||||
}
|
||||
|
||||
[[nodiscard]] static LastseenStatus FromLegacy(int32 value) {
|
||||
if (value == -2) {
|
||||
return LastseenStatus::Recently();
|
||||
} else if (value == -3) {
|
||||
return LastseenStatus::WithinWeek();
|
||||
} else if (value == -4) {
|
||||
return LastseenStatus::WithinMonth();
|
||||
} else if (value < -30) {
|
||||
return LastseenStatus::OnlineTill(-value, true);
|
||||
} else if (value > 0) {
|
||||
return LastseenStatus::OnlineTill(value);
|
||||
}
|
||||
return LastseenStatus();
|
||||
}
|
||||
|
||||
friend inline constexpr auto operator<=>(
|
||||
LastseenStatus,
|
||||
LastseenStatus) = default;
|
||||
friend inline constexpr bool operator==(
|
||||
LastseenStatus a,
|
||||
LastseenStatus b) = default;
|
||||
|
||||
private:
|
||||
static constexpr auto kLongAgoValue = uint32(0);
|
||||
static constexpr auto kRecentlyValue = uint32(1);
|
||||
static constexpr auto kWithinWeekValue = uint32(2);
|
||||
static constexpr auto kWithinMonthValue = uint32(3);
|
||||
static constexpr auto kSpecialValueSkip = uint32(4);
|
||||
static constexpr auto kValidAfter = kLifeStartDate + kSpecialValueSkip;
|
||||
|
||||
[[nodiscard]] bool valid() const {
|
||||
constexpr auto kMaxSum = uint32(std::numeric_limits<TimeId>::max());
|
||||
return (kMaxSum - _value > uint32(kLifeStartDate))
|
||||
&& (!_available || (_value >= kSpecialValueSkip));
|
||||
}
|
||||
|
||||
LastseenStatus(uint32 value, bool available, bool hiddenByMe)
|
||||
: _value(value)
|
||||
, _available(available ? 1 : 0)
|
||||
, _hiddenByMe(hiddenByMe ? 1 : 0) {
|
||||
}
|
||||
|
||||
uint32 _value : 30 = 0;
|
||||
uint32 _available : 1 = 0;
|
||||
uint32 _hiddenByMe : 1 = 0;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Data
|
||||
86
Telegram/SourceFiles/data/data_location.cpp
Normal file
86
Telegram/SourceFiles/data/data_location.cpp
Normal file
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
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/data_location.h"
|
||||
|
||||
#include "ui/image/image.h"
|
||||
#include "data/data_file_origin.h"
|
||||
|
||||
namespace Data {
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] QString AsString(float64 value) {
|
||||
constexpr auto kPrecision = 6;
|
||||
return QString::number(value, 'f', kPrecision);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
LocationPoint::LocationPoint(const MTPDgeoPoint &point)
|
||||
: _lat(point.vlat().v)
|
||||
, _lon(point.vlong().v)
|
||||
, _access(point.vaccess_hash().v) {
|
||||
}
|
||||
|
||||
LocationPoint::LocationPoint(float64 lat, float64 lon, IgnoreAccessHash)
|
||||
: _lat(lat)
|
||||
, _lon(lon) {
|
||||
}
|
||||
|
||||
QString LocationPoint::latAsString() const {
|
||||
return AsString(_lat);
|
||||
}
|
||||
|
||||
QString LocationPoint::lonAsString() const {
|
||||
return AsString(_lon);
|
||||
}
|
||||
|
||||
MTPGeoPoint LocationPoint::toMTP() const {
|
||||
return MTP_geoPoint(
|
||||
MTP_flags(0),
|
||||
MTP_double(_lon),
|
||||
MTP_double(_lat),
|
||||
MTP_long(_access),
|
||||
MTP_int(0)); // accuracy_radius
|
||||
}
|
||||
|
||||
float64 LocationPoint::lat() const {
|
||||
return _lat;
|
||||
}
|
||||
|
||||
float64 LocationPoint::lon() const {
|
||||
return _lon;
|
||||
}
|
||||
|
||||
uint64 LocationPoint::accessHash() const {
|
||||
return _access;
|
||||
}
|
||||
|
||||
size_t LocationPoint::hash() const {
|
||||
return QtPrivate::QHashCombine().operator()(
|
||||
std::hash<float64>()(_lat),
|
||||
_lon);
|
||||
}
|
||||
|
||||
GeoPointLocation ComputeLocation(const LocationPoint &point) {
|
||||
const auto scale = 1 + (cScale() * style::DevicePixelRatio()) / 200;
|
||||
const auto zoom = 13 + (scale - 1);
|
||||
const auto w = st::locationSize.width() / scale;
|
||||
const auto h = st::locationSize.height() / scale;
|
||||
|
||||
auto result = GeoPointLocation();
|
||||
result.lat = point.lat();
|
||||
result.lon = point.lon();
|
||||
result.access = point.accessHash();
|
||||
result.width = w;
|
||||
result.height = h;
|
||||
result.zoom = zoom;
|
||||
result.scale = scale;
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace Data
|
||||
84
Telegram/SourceFiles/data/data_location.h
Normal file
84
Telegram/SourceFiles/data/data_location.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 Data {
|
||||
|
||||
struct FileOrigin;
|
||||
|
||||
class LocationPoint {
|
||||
public:
|
||||
LocationPoint() = default;
|
||||
explicit LocationPoint(const MTPDgeoPoint &point);
|
||||
|
||||
enum IgnoreAccessHash {
|
||||
NoAccessHash,
|
||||
};
|
||||
LocationPoint(float64 lat, float64 lon, IgnoreAccessHash);
|
||||
|
||||
[[nodiscard]] QString latAsString() const;
|
||||
[[nodiscard]] QString lonAsString() const;
|
||||
[[nodiscard]] MTPGeoPoint toMTP() const;
|
||||
|
||||
[[nodiscard]] float64 lat() const;
|
||||
[[nodiscard]] float64 lon() const;
|
||||
[[nodiscard]] uint64 accessHash() const;
|
||||
|
||||
[[nodiscard]] size_t hash() const;
|
||||
|
||||
friend inline bool operator==(
|
||||
const LocationPoint &a,
|
||||
const LocationPoint &b) {
|
||||
return (a._lat == b._lat) && (a._lon == b._lon);
|
||||
}
|
||||
|
||||
friend inline bool operator<(
|
||||
const LocationPoint &a,
|
||||
const LocationPoint &b) {
|
||||
return (a._lat < b._lat) || ((a._lat == b._lat) && (a._lon < b._lon));
|
||||
}
|
||||
|
||||
private:
|
||||
float64 _lat = 0;
|
||||
float64 _lon = 0;
|
||||
uint64 _access = 0;
|
||||
|
||||
};
|
||||
|
||||
struct InputVenue {
|
||||
float64 lat = 0.;
|
||||
float64 lon = 0.;
|
||||
QString title;
|
||||
QString address;
|
||||
QString provider;
|
||||
QString id;
|
||||
QString venueType;
|
||||
|
||||
[[nodiscard]] bool justLocation() const {
|
||||
return id.isEmpty();
|
||||
}
|
||||
|
||||
friend inline bool operator==(
|
||||
const InputVenue &,
|
||||
const InputVenue &) = default;
|
||||
};
|
||||
|
||||
[[nodiscard]] GeoPointLocation ComputeLocation(const LocationPoint &point);
|
||||
|
||||
} // namespace Data
|
||||
|
||||
namespace std {
|
||||
|
||||
template <>
|
||||
struct hash<Data::LocationPoint> {
|
||||
size_t operator()(const Data::LocationPoint &value) const {
|
||||
return value.hash();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace std
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user