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

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

View File

@@ -0,0 +1,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

View 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 &current() 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

View 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

View 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

View 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

View File

@@ -0,0 +1,64 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#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

View 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

View 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

View File

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

View File

@@ -0,0 +1,84 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
namespace Main {
class Session;
} // namespace Main
namespace Data {
class Credits final {
public:
explicit Credits(not_null<Main::Session*> session);
~Credits();
void load(bool force = false);
[[nodiscard]] bool loaded() const;
[[nodiscard]] rpl::producer<bool> loadedValue() const;
[[nodiscard]] CreditsAmount balance() const;
[[nodiscard]] CreditsAmount balance(PeerId peerId) const;
[[nodiscard]] rpl::producer<CreditsAmount> balanceValue() const;
[[nodiscard]] float64 usdRate() const;
[[nodiscard]] rpl::producer<float64> rateValue(
not_null<PeerData*> ownedBotOrChannel);
[[nodiscard]] rpl::producer<> refreshedByPeerId(PeerId peerId);
void tonLoad(bool force = false);
[[nodiscard]] bool tonLoaded() const;
[[nodiscard]] rpl::producer<bool> tonLoadedValue() const;
[[nodiscard]] CreditsAmount tonBalance() const;
[[nodiscard]] rpl::producer<CreditsAmount> tonBalanceValue() const;
void apply(CreditsAmount balance);
void apply(PeerId peerId, CreditsAmount balance);
[[nodiscard]] bool statsEnabled() const;
void applyCurrency(PeerId peerId, CreditsAmount balance);
[[nodiscard]] CreditsAmount balanceCurrency(PeerId peerId) const;
void lock(CreditsAmount count);
void unlock(CreditsAmount count);
void withdrawLocked(CreditsAmount count);
void invalidate();
void apply(const MTPDupdateStarsBalance &data);
private:
void updateNonLockedValue();
const not_null<Main::Session*> _session;
std::unique_ptr<rpl::lifetime> _loader;
base::flat_map<PeerId, CreditsAmount> _cachedPeerBalances;
base::flat_map<PeerId, CreditsAmount> _cachedPeerCurrencyBalances;
CreditsAmount _balance;
CreditsAmount _locked;
rpl::variable<CreditsAmount> _nonLockedBalance;
rpl::event_stream<> _loadedChanges;
crl::time _lastLoaded = 0;
float64 _rate = 0.;
rpl::variable<CreditsAmount> _tonBalance;
rpl::event_stream<> _tonLoadedChanges;
crl::time _tonLastLoaded = false;
mtpRequestId _tonRequestId = 0;
bool _statsEnabled = false;
rpl::event_stream<PeerId> _refreshedByPeerId;
SingleQueuedInvokation _reload;
};
} // namespace Data

View File

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

View File

@@ -0,0 +1,70 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/timer.h"
class HistoryItem;
struct HistoryMessageFactcheck;
namespace HistoryView {
class Message;
class WebPage;
} // namespace HistoryView
namespace Main {
class Session;
} // namespace Main
namespace Ui {
class Show;
} // namespace Ui
namespace Data {
class Factchecks final {
public:
explicit Factchecks(not_null<Main::Session*> session);
void requestFor(not_null<HistoryItem*> item);
[[nodiscard]] std::unique_ptr<HistoryView::WebPage> makeMedia(
not_null<HistoryView::Message*> view,
not_null<HistoryMessageFactcheck*> factcheck);
[[nodiscard]] bool canEdit(not_null<HistoryItem*> item) const;
[[nodiscard]] int lengthLimit() const;
void save(
FullMsgId itemId,
TextWithEntities text,
Fn<void(QString)> done);
void save(
FullMsgId itemId,
const TextWithEntities &was,
TextWithEntities text,
std::shared_ptr<Ui::Show> show);
private:
[[nodiscard]] bool canEdit() const;
void subscribeIfNotYet();
void request();
const not_null<Main::Session*> _session;
base::Timer _requestTimer;
base::flat_set<not_null<HistoryItem*>> _pending;
std::vector<HistoryItem*> _requested;
mtpRequestId _requestId = 0;
bool _subscribed = false;
rpl::lifetime _lifetime;
};
} // namespace Data

View File

@@ -0,0 +1,481 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "data/components/gift_auctions.h"
#include "api/api_hash.h"
#include "api/api_premium.h"
#include "api/api_text_entities.h"
#include "apiwrap.h"
#include "data/data_session.h"
#include "main/main_session.h"
namespace Data {
GiftAuctions::GiftAuctions(not_null<Main::Session*> session)
: _session(session)
, _timer([=] { checkSubscriptions(); }) {
crl::on_main(_session, [=] {
rpl::merge(
_session->data().chatsListChanges(),
_session->data().chatsListLoadedEvents()
) | rpl::filter(
!rpl::mappers::_1
) | rpl::take(1) | rpl::on_next([=] {
requestActive();
}, _lifetime);
});
}
GiftAuctions::~GiftAuctions() = default;
rpl::producer<GiftAuctionState> GiftAuctions::state(const QString &slug) {
return [=](auto consumer) {
auto lifetime = rpl::lifetime();
auto &entry = _map[slug];
if (!entry) {
entry = std::make_unique<Entry>();
}
const auto raw = entry.get();
raw->changes.events() | rpl::on_next([=] {
consumer.put_next_copy(raw->state);
}, lifetime);
const auto now = crl::now();
if (raw->state.subscribedTill < 0
|| raw->state.subscribedTill >= now) {
consumer.put_next_copy(raw->state);
} else if (raw->state.subscribedTill >= 0) {
request(slug);
}
return lifetime;
};
}
void GiftAuctions::apply(const MTPDupdateStarGiftAuctionState &data) {
if (const auto entry = find(data.vgift_id().v)) {
const auto was = myStateKey(entry->state);
apply(entry, data.vstate());
entry->changes.fire({});
if (was != myStateKey(entry->state)) {
_activeChanged.fire({});
}
} else {
requestActive();
}
}
void GiftAuctions::apply(const MTPDupdateStarGiftAuctionUserState &data) {
if (const auto entry = find(data.vgift_id().v)) {
const auto was = myStateKey(entry->state);
apply(entry, data.vuser_state());
entry->changes.fire({});
if (was != myStateKey(entry->state)) {
_activeChanged.fire({});
}
} else {
requestActive();
}
}
void GiftAuctions::requestAcquired(
uint64 giftId,
Fn<void(std::vector<Data::GiftAcquired>)> done) {
Expects(done != nullptr);
_session->api().request(MTPpayments_GetStarGiftAuctionAcquiredGifts(
MTP_long(giftId)
)).done([=](const MTPpayments_StarGiftAuctionAcquiredGifts &result) {
const auto &data = result.data();
const auto owner = &_session->data();
owner->processUsers(data.vusers());
owner->processChats(data.vchats());
const auto &list = data.vgifts().v;
auto gifts = std::vector<Data::GiftAcquired>();
gifts.reserve(list.size());
for (const auto &gift : list) {
const auto &data = gift.data();
gifts.push_back({
.to = owner->peer(peerFromMTP(data.vpeer())),
.message = (data.vmessage()
? Api::ParseTextWithEntities(_session, *data.vmessage())
: TextWithEntities()),
.date = data.vdate().v,
.bidAmount = int64(data.vbid_amount().v),
.round = data.vround().v,
.number = data.vgift_num().value_or_empty(),
.position = data.vpos().v,
.nameHidden = data.is_name_hidden(),
});
}
if (const auto entry = find(giftId)) {
const auto count = int(gifts.size());
if (entry->state.my.gotCount != count) {
entry->state.my.gotCount = count;
entry->changes.fire({});
}
}
done(std::move(gifts));
}).fail([=] {
done({});
}).send();
}
std::optional<Data::UniqueGiftAttributes> GiftAuctions::attributes(
uint64 giftId) const {
const auto i = _attributes.find(giftId);
return (i != end(_attributes) && i->second.waiters.empty())
? i->second.lists
: std::optional<Data::UniqueGiftAttributes>();
}
void GiftAuctions::requestAttributes(uint64 giftId, Fn<void()> ready) {
auto &entry = _attributes[giftId];
entry.waiters.push_back(std::move(ready));
if (entry.waiters.size() > 1) {
return;
}
_session->api().request(MTPpayments_GetStarGiftUpgradeAttributes(
MTP_long(giftId)
)).done([=](const MTPpayments_StarGiftUpgradeAttributes &result) {
const auto &attributes = result.data().vattributes().v;
auto &entry = _attributes[giftId];
auto &info = entry.lists;
info.models.reserve(attributes.size());
info.patterns.reserve(attributes.size());
info.backdrops.reserve(attributes.size());
for (const auto &attribute : attributes) {
attribute.match([&](const MTPDstarGiftAttributeModel &data) {
info.models.push_back(Api::FromTL(_session, data));
}, [&](const MTPDstarGiftAttributePattern &data) {
info.patterns.push_back(Api::FromTL(_session, data));
}, [&](const MTPDstarGiftAttributeBackdrop &data) {
info.backdrops.push_back(Api::FromTL(data));
}, [](const MTPDstarGiftAttributeOriginalDetails &data) {
});
}
for (const auto &ready : base::take(entry.waiters)) {
ready();
}
}).fail([=] {
for (const auto &ready : base::take(_attributes[giftId].waiters)) {
ready();
}
}).send();
}
rpl::producer<ActiveAuctions> GiftAuctions::active() const {
return _activeChanged.events_starting_with_copy(
rpl::empty
) | rpl::map([=] {
return collectActive();
});
}
rpl::producer<bool> GiftAuctions::hasActiveChanges() const {
const auto has = hasActive();
return _activeChanged.events(
) | rpl::map([=] {
return hasActive();
}) | rpl::combine_previous(
has
) | rpl::filter([=](bool previous, bool current) {
return previous != current;
}) | rpl::map([=](bool previous, bool current) {
return current;
});
}
bool GiftAuctions::hasActive() const {
for (const auto &[slug, entry] : _map) {
if (myStateKey(entry->state)) {
return true;
}
}
return false;
}
void GiftAuctions::checkSubscriptions() {
const auto now = crl::now();
auto next = crl::time();
for (const auto &[slug, entry] : _map) {
const auto raw = entry.get();
const auto till = raw->state.subscribedTill;
if (till <= 0 || !raw->changes.has_consumers()) {
continue;
} else if (till <= now) {
request(slug);
} else {
const auto timeout = till - now;
if (!next || timeout < next) {
next = timeout;
}
}
}
if (next) {
_timer.callOnce(next);
}
}
auto GiftAuctions::myStateKey(const GiftAuctionState &state) const
-> MyStateKey {
if (!state.my.bid) {
return {};
}
auto min = 0;
for (const auto &level : state.bidLevels) {
if (level.position > state.gift->auctionGiftsPerRound) {
break;
} else if (!min || min > level.amount) {
min = level.amount;
}
}
return {
.bid = int(state.my.bid),
.position = MyAuctionPosition(state),
.version = state.version,
};
}
ActiveAuctions GiftAuctions::collectActive() const {
auto result = ActiveAuctions();
result.list.reserve(_map.size());
for (const auto &[slug, entry] : _map) {
const auto raw = &entry->state;
if (raw->gift && raw->my.date) {
result.list.push_back(raw);
}
}
return result;
}
uint64 GiftAuctions::countActiveHash() const {
auto result = Api::HashInit();
for (const auto &active : collectActive().list) {
Api::HashUpdate(result, active->version);
Api::HashUpdate(result, active->my.date);
}
return Api::HashFinalize(result);
}
void GiftAuctions::requestActive() {
if (_activeRequestId) {
return;
}
_activeRequestId = _session->api().request(
MTPpayments_GetStarGiftActiveAuctions(MTP_long(countActiveHash()))
).done([=](const MTPpayments_StarGiftActiveAuctions &result) {
result.match([=](const MTPDpayments_starGiftActiveAuctions &data) {
const auto owner = &_session->data();
owner->processUsers(data.vusers());
owner->processChats(data.vchats());
auto giftsFound = base::flat_set<QString>();
const auto &list = data.vauctions().v;
giftsFound.reserve(list.size());
for (const auto &auction : list) {
const auto &data = auction.data();
auto gift = Api::FromTL(_session, data.vgift());
const auto slug = gift ? gift->auctionSlug : QString();
if (slug.isEmpty()) {
LOG(("Api Error: Bad auction gift."));
continue;
}
auto &entry = _map[slug];
if (!entry) {
entry = std::make_unique<Entry>();
}
const auto raw = entry.get();
if (!raw->state.gift) {
raw->state.gift = std::move(gift);
}
apply(raw, data.vstate());
apply(raw, data.vuser_state());
giftsFound.emplace(slug);
}
for (const auto &[slug, entry] : _map) {
const auto my = &entry->state.my;
if (my->date && !giftsFound.contains(slug)) {
my->to = nullptr;
my->minBidAmount = 0;
my->bid = 0;
my->date = 0;
my->returned = false;
giftsFound.emplace(slug);
}
}
for (const auto &slug : giftsFound) {
_map[slug]->changes.fire({});
}
_activeChanged.fire({});
}, [](const MTPDpayments_starGiftActiveAuctionsNotModified &) {
});
}).send();
}
void GiftAuctions::request(const QString &slug) {
auto &entry = _map[slug];
Assert(entry != nullptr);
const auto raw = entry.get();
if (raw->requested) {
return;
}
raw->requested = true;
_session->api().request(MTPpayments_GetStarGiftAuctionState(
MTP_inputStarGiftAuctionSlug(MTP_string(slug)),
MTP_int(raw->state.version)
)).done([=](const MTPpayments_StarGiftAuctionState &result) {
raw->requested = false;
const auto &data = result.data();
_session->data().processUsers(data.vusers());
_session->data().processChats(data.vchats());
raw->state.gift = Api::FromTL(_session, data.vgift());
if (!raw->state.gift) {
return;
}
const auto timeout = data.vtimeout().v;
const auto ms = timeout * crl::time(1000);
raw->state.subscribedTill = ms ? (crl::now() + ms) : -1;
const auto was = myStateKey(raw->state);
apply(raw, data.vstate());
apply(raw, data.vuser_state());
if (raw->changes.has_consumers()) {
raw->changes.fire({});
if (ms && (!_timer.isActive() || _timer.remainingTime() > ms)) {
_timer.callOnce(ms);
}
}
if (was != myStateKey(raw->state)) {
_activeChanged.fire({});
}
}).send();
}
GiftAuctions::Entry *GiftAuctions::find(uint64 giftId) const {
for (const auto &[slug, entry] : _map) {
if (entry->state.gift && entry->state.gift->id == giftId) {
return entry.get();
}
}
return nullptr;
}
void GiftAuctions::apply(
not_null<Entry*> entry,
const MTPStarGiftAuctionState &state) {
apply(&entry->state, state);
}
void GiftAuctions::apply(
not_null<GiftAuctionState*> entry,
const MTPStarGiftAuctionState &state) {
Expects(entry->gift.has_value());
state.match([&](const MTPDstarGiftAuctionState &data) {
const auto version = data.vversion().v;
if (entry->version >= version) {
return;
}
const auto owner = &_session->data();
entry->startDate = data.vstart_date().v;
entry->endDate = data.vend_date().v;
entry->minBidAmount = data.vmin_bid_amount().v;
const auto &levels = data.vbid_levels().v;
entry->bidLevels.clear();
entry->bidLevels.reserve(levels.size());
for (const auto &level : levels) {
auto &bid = entry->bidLevels.emplace_back();
const auto &data = level.data();
bid.amount = data.vamount().v;
bid.position = data.vpos().v;
bid.date = data.vdate().v;
}
const auto &top = data.vtop_bidders().v;
entry->topBidders.clear();
entry->topBidders.reserve(top.size());
for (const auto &user : top) {
entry->topBidders.push_back(owner->user(UserId(user.v)));
}
entry->nextRoundAt = data.vnext_round_at().v;
entry->giftsLeft = data.vgifts_left().v;
entry->currentRound = data.vcurrent_round().v;
entry->totalRounds = data.vtotal_rounds().v;
const auto &rounds = data.vrounds().v;
entry->roundParameters.clear();
entry->roundParameters.reserve(rounds.size());
for (const auto &round : rounds) {
round.match([&](const MTPDstarGiftAuctionRound &data) {
entry->roundParameters.push_back({
.number = data.vnum().v,
.duration = data.vduration().v,
});
}, [&](const MTPDstarGiftAuctionRoundExtendable &data) {
entry->roundParameters.push_back({
.number = data.vnum().v,
.duration = data.vduration().v,
.extendTop = data.vextend_top().v,
.extendDuration = data.vextend_window().v,
});
});
}
entry->averagePrice = 0;
}, [&](const MTPDstarGiftAuctionStateFinished &data) {
entry->averagePrice = data.vaverage_price().v;
entry->startDate = data.vstart_date().v;
entry->endDate = data.vend_date().v;
entry->minBidAmount = 0;
entry->nextRoundAt
= entry->currentRound
= entry->totalRounds
= entry->giftsLeft
= entry->version
= 0;
}, [&](const MTPDstarGiftAuctionStateNotModified &data) {
});
}
void GiftAuctions::apply(
not_null<Entry*> entry,
const MTPStarGiftAuctionUserState &state) {
apply(&entry->state.my, state);
}
void GiftAuctions::apply(
not_null<StarGiftAuctionMyState*> entry,
const MTPStarGiftAuctionUserState &state) {
const auto &data = state.data();
entry->to = data.vbid_peer()
? _session->data().peer(peerFromMTP(*data.vbid_peer())).get()
: nullptr;
entry->minBidAmount = data.vmin_bid_amount().value_or(0);
entry->bid = data.vbid_amount().value_or(0);
entry->date = data.vbid_date().value_or(0);
entry->gotCount = data.vacquired_count().v;
entry->returned = data.is_returned();
}
int MyAuctionPosition(const GiftAuctionState &state) {
const auto &levels = state.bidLevels;
for (auto i = begin(levels), e = end(levels); i != e; ++i) {
if (i->amount < state.my.bid
|| (i->amount == state.my.bid && i->date >= state.my.date)) {
return i->position;
}
}
return (levels.empty() ? 0 : levels.back().position) + 1;
}
} // namespace Data

View File

@@ -0,0 +1,157 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/timer.h"
#include "data/data_star_gift.h"
namespace Main {
class Session;
} // namespace Main
namespace Data {
struct GiftAuctionBidLevel {
int64 amount = 0;
int position = 0;
TimeId date = 0;
};
struct StarGiftAuctionMyState {
PeerData *to = nullptr;
int64 minBidAmount = 0;
int64 bid = 0;
TimeId date = 0;
int gotCount = 0;
bool returned = false;
};
struct GiftAuctionRound {
int number = 0;
TimeId duration = 0;
int extendTop = 0;
TimeId extendDuration = 0;
};
struct GiftAuctionState {
std::optional<StarGift> gift;
StarGiftAuctionMyState my;
std::vector<GiftAuctionBidLevel> bidLevels;
std::vector<not_null<UserData*>> topBidders;
std::vector<GiftAuctionRound> roundParameters;
crl::time subscribedTill = 0;
int64 minBidAmount = 0;
int64 averagePrice = 0;
TimeId startDate = 0;
TimeId endDate = 0;
TimeId nextRoundAt = 0;
int currentRound = 0;
int totalRounds = 0;
int giftsLeft = 0;
int version = 0;
[[nodiscard]] bool finished() const {
return (averagePrice != 0);
}
};
struct GiftAcquired {
not_null<PeerData*> to;
TextWithEntities message;
TimeId date = 0;
int64 bidAmount = 0;
int round = 0;
int number = 0;
int position = 0;
bool nameHidden = false;
};
struct ActiveAuctions {
std::vector<not_null<GiftAuctionState*>> list;
};
class GiftAuctions final {
public:
explicit GiftAuctions(not_null<Main::Session*> session);
~GiftAuctions();
[[nodiscard]] rpl::producer<GiftAuctionState> state(const QString &slug);
void apply(const MTPDupdateStarGiftAuctionState &data);
void apply(const MTPDupdateStarGiftAuctionUserState &data);
void requestAcquired(
uint64 giftId,
Fn<void(std::vector<Data::GiftAcquired>)> done);
[[nodiscard]] std::optional<Data::UniqueGiftAttributes> attributes(
uint64 giftId) const;
void requestAttributes(uint64 giftId, Fn<void()> ready);
[[nodiscard]] rpl::producer<ActiveAuctions> active() const;
[[nodiscard]] rpl::producer<bool> hasActiveChanges() const;
[[nodiscard]] bool hasActive() const;
private:
struct Entry {
GiftAuctionState state;
rpl::event_stream<> changes;
bool requested = false;
};
struct MyStateKey {
int bid = 0;
int position = 0;
int version = 0;
explicit operator bool() const {
return bid != 0;
}
friend inline bool operator==(MyStateKey, MyStateKey) = default;
};
struct Attributes {
Data::UniqueGiftAttributes lists;
std::vector<Fn<void()>> waiters;
};
void request(const QString &slug);
Entry *find(uint64 giftId) const;
void apply(
not_null<Entry*> entry,
const MTPStarGiftAuctionState &state);
void apply(
not_null<GiftAuctionState*> entry,
const MTPStarGiftAuctionState &state);
void apply(
not_null<Entry*> entry,
const MTPStarGiftAuctionUserState &state);
void apply(
not_null<StarGiftAuctionMyState*> entry,
const MTPStarGiftAuctionUserState &state);
void checkSubscriptions();
[[nodiscard]] MyStateKey myStateKey(const GiftAuctionState &state) const;
[[nodiscard]] ActiveAuctions collectActive() const;
[[nodiscard]] uint64 countActiveHash() const;
void requestActive();
const not_null<Main::Session*> _session;
base::Timer _timer;
base::flat_map<QString, std::unique_ptr<Entry>> _map;
base::flat_map<uint64, Attributes> _attributes;
rpl::event_stream<> _activeChanged;
mtpRequestId _activeRequestId = 0;
rpl::lifetime _lifetime;
};
[[nodiscard]] int MyAuctionPosition(const GiftAuctionState &state);
} // namespace Data

View File

@@ -0,0 +1,44 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "data/components/location_pickers.h"
#include "api/api_common.h"
#include "ui/controls/location_picker.h"
namespace Data {
struct LocationPickers::Entry {
Api::SendAction action;
base::weak_ptr<Ui::LocationPicker> picker;
};
LocationPickers::LocationPickers() = default;
LocationPickers::~LocationPickers() = default;
Ui::LocationPicker *LocationPickers::lookup(const Api::SendAction &action) {
for (auto i = begin(_pickers); i != end(_pickers);) {
if (const auto strong = i->picker.get()) {
if (i->action == action) {
return strong;
}
++i;
} else {
i = _pickers.erase(i);
}
}
return nullptr;
}
void LocationPickers::emplace(
const Api::SendAction &action,
not_null<Ui::LocationPicker*> picker) {
_pickers.push_back({ action, picker });
}
} // namespace Data

View File

@@ -0,0 +1,39 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/weak_ptr.h"
namespace Api {
struct SendAction;
} // namespace Api
namespace Ui {
class LocationPicker;
} // namespace Ui
namespace Data {
class LocationPickers final {
public:
LocationPickers();
~LocationPickers();
Ui::LocationPicker *lookup(const Api::SendAction &action);
void emplace(
const Api::SendAction &action,
not_null<Ui::LocationPicker*> picker);
private:
struct Entry;
std::vector<Entry> _pickers;
};
} // namespace Data

View File

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

View File

@@ -0,0 +1,79 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
namespace Data::Passkey {
struct RegisterData;
struct LoginData;
} // namespace Data::Passkey
namespace Platform::WebAuthn {
struct RegisterResult;
struct LoginResult;
} // namespace Platform::WebAuthn
namespace Main {
class Session;
} // namespace Main
namespace MTP {
class Sender;
} // namespace MTP
namespace Data {
struct PasskeyEntry {
QString id;
QString name;
TimeId date = 0;
DocumentId softwareEmojiId = 0;
TimeId lastUsageDate = 0;
};
class Passkeys final {
public:
explicit Passkeys(not_null<Main::Session*> session);
~Passkeys();
void initRegistration(Fn<void(const Data::Passkey::RegisterData&)> done);
void registerPasskey(
const Platform::WebAuthn::RegisterResult &result,
Fn<void()> done);
void deletePasskey(
const QString &id,
Fn<void()> done,
Fn<void(QString)> fail);
[[nodiscard]] rpl::producer<> requestList();
[[nodiscard]] const std::vector<PasskeyEntry> &list() const;
[[nodiscard]] bool listKnown() const;
[[nodiscard]] bool canRegister() const;
[[nodiscard]] bool possible() const;
private:
void loadList();
const not_null<Main::Session*> _session;
std::vector<PasskeyEntry> _passkeys;
rpl::event_stream<> _listUpdated;
crl::time _lastRequestTime = 0;
mtpRequestId _listRequestId = 0;
bool _listKnown = false;
};
void InitPasskeyLogin(
MTP::Sender &api,
Fn<void(const Data::Passkey::LoginData&)> done);
void FinishPasskeyLogin(
MTP::Sender &api,
int initialDc,
const Platform::WebAuthn::LoginResult &result,
Fn<void(const MTPauth_Authorization&)> done,
Fn<void(QString)> fail);
} // namespace Data

View File

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

View File

@@ -0,0 +1,107 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/timer.h"
class History;
namespace Main {
class Session;
} // namespace Main
namespace Data {
enum class SetupEmailState {
None,
Setup,
SetupNoSkip,
SettingUp,
SettingUpNoSkip,
};
struct CustomSuggestion final {
QString suggestion;
TextWithEntities title;
TextWithEntities description;
QString url;
friend inline auto operator<=>(
const CustomSuggestion &,
const CustomSuggestion &) = default;
};
class PromoSuggestions final {
public:
explicit PromoSuggestions(
not_null<Main::Session*> session,
Fn<void()> firstPromoLoaded = nullptr);
~PromoSuggestions();
[[nodiscard]] bool current(const QString &key) const;
[[nodiscard]] std::optional<CustomSuggestion> custom() const;
[[nodiscard]] rpl::producer<> requested(const QString &key) const;
void dismiss(const QString &key);
void dismissSetupEmail(Fn<void()> done);
void refreshTopPromotion();
void invalidate();
rpl::producer<> value() const;
// Create rpl::producer<> refreshed() const; on memand.
void requestContactBirthdays(Fn<void()> done, bool force = false);
[[nodiscard]] auto knownContactBirthdays() const
-> std::optional<std::vector<UserId>>;
[[nodiscard]] auto knownBirthdaysToday() const
-> std::optional<std::vector<UserId>>;
[[nodiscard]] static QString SugValidatePassword();
void setSetupEmailState(SetupEmailState state);
[[nodiscard]] SetupEmailState setupEmailState() const;
[[nodiscard]] rpl::producer<SetupEmailState> setupEmailStateValue() const;
private:
void setTopPromoted(
History *promoted,
const QString &type,
const QString &message);
void topPromotionDelayed(TimeId now, TimeId next);
const not_null<Main::Session*> _session;
base::flat_set<QString> _dismissedSuggestions;
std::vector<QString> _pendingSuggestions;
std::optional<CustomSuggestion> _custom;
History *_topPromoted = nullptr;
mtpRequestId _contactBirthdaysRequestId = 0;
int _contactBirthdaysLastDayRequest = -1;
std::vector<UserId> _contactBirthdays;
std::vector<UserId> _contactBirthdaysToday;
mtpRequestId _topPromotionRequestId = 0;
std::pair<QString, uint32> _topPromotionKey;
TimeId _topPromotionNextRequestTime = TimeId(0);
base::Timer _topPromotionTimer;
SetupEmailState _setupEmailState = SetupEmailState::None;
rpl::event_stream<> _refreshed;
rpl::event_stream<SetupEmailState> _setupEmailStateChanges;
Fn<void()> _firstPromoLoaded;
rpl::lifetime _lifetime;
};
} // namespace Data

View File

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

View File

@@ -0,0 +1,55 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/userpic_view.h"
namespace Main {
class Session;
} // namespace Main
namespace Data {
class Thread;
class RecentPeers final {
public:
explicit RecentPeers(not_null<Main::Session*> session);
~RecentPeers();
[[nodiscard]] const std::vector<not_null<PeerData*>> &list() const;
[[nodiscard]] rpl::producer<> updates() const;
void remove(not_null<PeerData*> peer);
void bump(not_null<PeerData*> peer);
void clear();
[[nodiscard]] QByteArray serialize() const;
void applyLocal(QByteArray serialized);
[[nodiscard]] auto collectChatOpenHistory() const
-> std::vector<not_null<Thread*>>;
void chatOpenPush(not_null<Thread*> thread);
void chatOpenRemove(not_null<Thread*> thread);
void chatOpenKeepUserpics(
base::flat_map<not_null<PeerData*>, Ui::PeerUserpicView> userpics);
private:
const not_null<Main::Session*> _session;
std::vector<not_null<PeerData*>> _list;
std::vector<not_null<Thread*>> _opens;
base::flat_map<
not_null<PeerData*>,
Ui::PeerUserpicView> _chatOpenUserpicsCache;
rpl::event_stream<> _updates;
};
} // namespace Data

View File

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

View File

@@ -0,0 +1,73 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "data/data_star_gift.h"
namespace ChatHelpers {
class Show;
} // namespace ChatHelpers
namespace Main {
class Session;
} // namespace Main
namespace Data {
class RecentSharedMediaGifts final {
public:
explicit RecentSharedMediaGifts(not_null<Main::Session*> session);
~RecentSharedMediaGifts();
void request(
not_null<PeerData*> peer,
Fn<void(std::vector<Data::SavedStarGift>)> done,
bool onlyPinnedToTop = false);
void clearLastRequestTime(not_null<PeerData*> peer);
void togglePinned(
std::shared_ptr<ChatHelpers::Show> show,
not_null<PeerData*> peer,
const Data::SavedStarGiftId &manageId,
bool pinned,
std::shared_ptr<Data::UniqueGift> uniqueData,
std::shared_ptr<Data::UniqueGift> replacingData = nullptr);
void reorderPinned(
std::shared_ptr<ChatHelpers::Show> show,
not_null<PeerData*> peer,
int oldPosition,
int newPosition);
private:
void updatePinnedOrder(
std::shared_ptr<ChatHelpers::Show> show,
not_null<PeerData*> peer,
const std::vector<SavedStarGift> &gifts,
const std::vector<Data::SavedStarGiftId> &manageIds,
Fn<void()> done);
[[nodiscard]] std::vector<Data::SavedStarGift> filterGifts(
const std::deque<SavedStarGift> &gifts,
bool onlyPinnedToTop);
struct Entry {
std::deque<SavedStarGift> gifts;
crl::time lastRequestTime = 0;
mtpRequestId requestId = 0;
std::vector<Fn<void()>> pendingCallbacks;
};
const not_null<Main::Session*> _session;
base::flat_map<PeerId, Entry> _recent;
};
} // namespace Data

View File

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

View File

@@ -0,0 +1,100 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "history/history_item.h"
#include "base/timer.h"
class History;
namespace Main {
class Session;
} // namespace Main
namespace Data {
struct MessagesSlice;
[[nodiscard]] bool IsScheduledMsgId(MsgId id);
class ScheduledMessages final {
public:
explicit ScheduledMessages(not_null<Main::Session*> session);
ScheduledMessages(const ScheduledMessages &other) = delete;
ScheduledMessages &operator=(const ScheduledMessages &other) = delete;
~ScheduledMessages();
[[nodiscard]] MsgId lookupId(not_null<const HistoryItem*> item) const;
[[nodiscard]] HistoryItem *lookupItem(PeerId peer, MsgId msg) const;
[[nodiscard]] HistoryItem *lookupItem(FullMsgId itemId) const;
[[nodiscard]] int count(not_null<History*> history) const;
[[nodiscard]] bool hasFor(not_null<Data::ForumTopic*> topic) const;
[[nodiscard]] MsgId localMessageId(MsgId remoteId) const;
void checkEntitiesAndUpdate(const MTPDmessage &data);
void apply(const MTPDupdateNewScheduledMessage &update);
void apply(const MTPDupdateDeleteScheduledMessages &update);
void apply(
const MTPDupdateMessageID &update,
not_null<HistoryItem*> local);
void appendSending(not_null<HistoryItem*> item);
void removeSending(not_null<HistoryItem*> item);
void sendNowSimpleMessage(
const MTPDupdateShortSentMessage &update,
not_null<HistoryItem*> local);
[[nodiscard]] rpl::producer<> updates(not_null<History*> history);
[[nodiscard]] Data::MessagesSlice list(not_null<History*> history) const;
[[nodiscard]] Data::MessagesSlice list(
not_null<const Data::ForumTopic*> topic) const;
void clear();
private:
using OwnedItem = std::unique_ptr<HistoryItem, HistoryItem::Destroyer>;
struct List {
std::vector<OwnedItem> items;
base::flat_map<MsgId, not_null<HistoryItem*>> itemById;
};
struct Request {
mtpRequestId requestId = 0;
crl::time lastReceived = 0;
};
void request(not_null<History*> history);
void parse(
not_null<History*> history,
const MTPmessages_Messages &list);
HistoryItem *append(
not_null<History*> history,
List &list,
const MTPMessage &message);
void clearNotSending(not_null<History*> history);
void updated(
not_null<History*> history,
const base::flat_set<not_null<HistoryItem*>> &added,
const base::flat_set<not_null<HistoryItem*>> &clear);
void sort(List &list);
void remove(not_null<const HistoryItem*> item);
[[nodiscard]] uint64 countListHash(const List &list) const;
void clearOldRequests();
const not_null<Main::Session*> _session;
base::Timer _clearTimer;
base::flat_map<not_null<History*>, List> _data;
base::flat_map<not_null<History*>, Request> _requests;
rpl::event_stream<not_null<History*>> _updates;
rpl::lifetime _lifetime;
};
} // namespace Data

View File

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

View File

@@ -0,0 +1,245 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/timer.h"
#include "history/history_item.h"
#include "ui/image/image_location.h"
#include "window/window_session_controller_link_info.h"
class History;
namespace Api {
struct SponsoredSearchResult;
} // namespace Api
namespace Main {
class Session;
} // namespace Main
namespace Ui {
class RpWidget;
} // namespace Ui
namespace Data {
class MediaPreload;
struct SponsoredReportResult final {
using Id = QByteArray;
struct Option final {
Id id = 0;
QString text;
};
using Options = std::vector<Option>;
enum class FinalStep {
Hidden,
Reported,
Premium,
Silence,
};
Options options;
QString title;
QString error;
FinalStep result;
};
struct SponsoredFrom {
QString title;
QString link;
QString buttonText;
PhotoId photoId = PhotoId(0);
PhotoId mediaPhotoId = PhotoId(0);
DocumentId mediaDocumentId = DocumentId(0);
uint64 backgroundEmojiId = 0;
uint8 colorIndex : 6 = 0;
bool isLinkInternal = false;
bool isRecommended = false;
bool canReport = false;
};
struct SponsoredMessage {
QByteArray randomId;
SponsoredFrom from;
TextWithEntities textWithEntities;
not_null<History*> history;
QString link;
TextWithEntities sponsorInfo;
TextWithEntities additionalInfo;
crl::time durationMin = 0;
crl::time durationMax = 0;
};
struct SponsoredMessageDetails {
std::vector<TextWithEntities> info;
QString link;
QString buttonText;
PhotoId photoId = PhotoId(0);
PhotoId mediaPhotoId = PhotoId(0);
DocumentId mediaDocumentId = DocumentId(0);
uint64 backgroundEmojiId = 0;
uint8 colorIndex : 6 = 0;
bool isLinkInternal = false;
bool canReport = false;
};
struct SponsoredReportAction {
Fn<void(
Data::SponsoredReportResult::Id,
Fn<void(Data::SponsoredReportResult)>)> callback;
};
struct SponsoredForVideoState {
int itemIndex = 0;
crl::time leftTillShow = 0;
[[nodiscard]] bool initial() const {
return !itemIndex && !leftTillShow;
}
};
struct SponsoredForVideo {
std::vector<SponsoredMessage> list;
crl::time startDelay = 0;
crl::time betweenDelay = 0;
SponsoredForVideoState state;
};
class SponsoredMessages final {
public:
enum class AppendResult {
None,
Appended,
MediaLoading,
};
enum class State {
None,
AppendToEnd,
InjectToMiddle,
AppendToTopBar,
};
using Details = SponsoredMessageDetails;
using RandomId = QByteArray;
explicit SponsoredMessages(not_null<Main::Session*> session);
~SponsoredMessages();
[[nodiscard]] bool canHaveFor(not_null<History*> history) const;
[[nodiscard]] bool canHaveFor(not_null<HistoryItem*> item) const;
[[nodiscard]] bool isTopBarFor(not_null<History*> history) const;
void request(not_null<History*> history, Fn<void()> done);
void requestForVideo(
not_null<HistoryItem*> item,
Fn<void(SponsoredForVideo)> done);
void updateForVideo(
FullMsgId itemId,
SponsoredForVideoState state);
void clearItems(not_null<History*> history);
[[nodiscard]] Details lookupDetails(const FullMsgId &fullId) const;
[[nodiscard]] Details lookupDetails(const SponsoredMessage &data) const;
[[nodiscard]] Details lookupDetails(
const Api::SponsoredSearchResult &data) const;
void clicked(const FullMsgId &fullId, bool isMedia, bool isFullscreen);
void clicked(
const QByteArray &randomId,
bool isMedia,
bool isFullscreen);
[[nodiscard]] FullMsgId fillTopBar(
not_null<History*> history,
not_null<Ui::RpWidget*> widget);
[[nodiscard]] rpl::producer<> itemRemoved(const FullMsgId &);
[[nodiscard]] AppendResult append(not_null<History*> history);
void inject(
not_null<History*> history,
MsgId injectAfterMsgId,
int betweenHeight,
int fallbackWidth);
void view(const FullMsgId &fullId);
void view(const QByteArray &randomId);
[[nodiscard]] State state(not_null<History*> history) const;
[[nodiscard]] SponsoredReportAction createReportCallback(
const FullMsgId &fullId);
[[nodiscard]] SponsoredReportAction createReportCallback(
const QByteArray &randomId,
Fn<void()> erase);
void clear();
private:
using OwnedItem = std::unique_ptr<HistoryItem, HistoryItem::Destroyer>;
struct Entry {
OwnedItem item;
FullMsgId itemFullId;
SponsoredMessage sponsored;
std::unique_ptr<MediaPreload> preload;
std::unique_ptr<rpl::lifetime> optionalDestructionNotifier;
};
struct List {
std::vector<Entry> entries;
// Data between history displays.
size_t injectedCount = 0;
bool showedAll = false;
//
crl::time received = 0;
int postsBetween = 0;
State state = State::None;
};
struct ListForVideo {
std::vector<Entry> entries;
crl::time received = 0;
crl::time startDelay = 0;
crl::time betweenDelay = 0;
SponsoredForVideoState state;
};
struct Request {
mtpRequestId requestId = 0;
crl::time lastReceived = 0;
};
struct RequestForVideo {
std::vector<Fn<void(SponsoredForVideo)>> callbacks;
mtpRequestId requestId = 0;
crl::time lastReceived = 0;
};
void parse(
not_null<History*> history,
const MTPmessages_sponsoredMessages &list);
void parseForVideo(
not_null<PeerData*> peer,
const MTPmessages_sponsoredMessages &list);
void append(
Fn<not_null<std::vector<Entry>*>()> entries,
not_null<History*> history,
const MTPSponsoredMessage &message);
[[nodiscard]] SponsoredForVideo prepareForVideo(
not_null<PeerData*> peer);
void clearOldRequests();
const Entry *find(const FullMsgId &fullId) const;
const not_null<Main::Session*> _session;
base::Timer _clearTimer;
base::flat_map<not_null<History*>, List> _data;
base::flat_map<not_null<History*>, Request> _requests;
base::flat_map<RandomId, Request> _viewRequests;
base::flat_map<not_null<PeerData*>, ListForVideo> _dataForVideo;
base::flat_map<not_null<PeerData*>, RequestForVideo> _requestsForVideo;
rpl::event_stream<FullMsgId> _itemRemoved;
rpl::lifetime _lifetime;
};
} // namespace Data

View File

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

View File

@@ -0,0 +1,64 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
namespace Main {
class Session;
} // namespace Main
namespace Data {
enum class TopPeerType {
Chat,
BotApp,
};
class TopPeers final {
public:
TopPeers(not_null<Main::Session*> session, TopPeerType type);
~TopPeers();
[[nodiscard]] std::vector<not_null<PeerData*>> list() const;
[[nodiscard]] bool disabled() const;
[[nodiscard]] rpl::producer<> updates() const;
void remove(not_null<PeerData*> peer);
void increment(not_null<PeerData*> peer, TimeId date);
void reload();
void toggleDisabled(bool disabled);
[[nodiscard]] QByteArray serialize() const;
void applyLocal(QByteArray serialized);
private:
struct TopPeer {
not_null<PeerData*> peer;
float64 rating = 0.;
};
void loadAfterChats();
void request();
[[nodiscard]] uint64 countHash() const;
void updated();
const not_null<Main::Session*> _session;
const TopPeerType _type = {};
std::vector<TopPeer> _list;
rpl::event_stream<> _updates;
crl::time _lastReceived = 0;
TimeId _lastReceivedDate = 0;
mtpRequestId _requestId = 0;
bool _disabled = false;
bool _received = false;
};
} // namespace Data

View 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;
};

View 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

View File

@@ -0,0 +1,70 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
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

View 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);
}

View 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;
};

View 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

View 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

View 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

View 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

View 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

View File

@@ -0,0 +1,73 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
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

View 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) {
}

View 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;
};

View 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

View 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

File diff suppressed because it is too large Load Diff

View 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

View 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

View 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

View 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

View 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

View 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

File diff suppressed because it is too large Load Diff

View 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

View 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

View 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

View 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

View 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

View 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> &params) {
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

View 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> &params);
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

View File

@@ -0,0 +1,166 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#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

View 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

File diff suppressed because it is too large Load Diff

View 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());

View 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

View 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

View 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

View 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

File diff suppressed because it is too large Load Diff

View 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

View 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

View 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

View 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

View 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

View 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();
}
}

View 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;
};

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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 &notify() {
return _notify;
}
[[nodiscard]] const PeerNotifySettings &notify() 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

View 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) {
}

View 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;
};

File diff suppressed because it is too large Load Diff

View 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

View 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

View 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

File diff suppressed because it is too large Load Diff

View 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

View 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

View 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

View 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

View 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

View File

@@ -0,0 +1,84 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
namespace 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