init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
This commit is contained in:
217
Telegram/SourceFiles/data/business/data_business_chatbots.cpp
Normal file
217
Telegram/SourceFiles/data/business/data_business_chatbots.cpp
Normal file
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "data/business/data_business_chatbots.h"
|
||||
|
||||
#include "apiwrap.h"
|
||||
#include "boxes/peers/edit_peer_permissions_box.h"
|
||||
#include "data/business/data_business_common.h"
|
||||
#include "data/business/data_business_info.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_user.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "main/main_session.h"
|
||||
|
||||
namespace Data {
|
||||
|
||||
Chatbots::Chatbots(not_null<Session*> owner)
|
||||
: _owner(owner) {
|
||||
}
|
||||
|
||||
Chatbots::~Chatbots() = default;
|
||||
|
||||
void Chatbots::preload() {
|
||||
if (_loaded || _requestId) {
|
||||
return;
|
||||
}
|
||||
_requestId = _owner->session().api().request(
|
||||
MTPaccount_GetConnectedBots()
|
||||
).done([=](const MTPaccount_ConnectedBots &result) {
|
||||
_requestId = 0;
|
||||
_loaded = true;
|
||||
|
||||
const auto &data = result.data();
|
||||
_owner->processUsers(data.vusers());
|
||||
const auto &list = data.vconnected_bots().v;
|
||||
if (!list.isEmpty()) {
|
||||
const auto &bot = list.front().data();
|
||||
const auto botId = bot.vbot_id().v;
|
||||
_settings = ChatbotsSettings{
|
||||
.bot = _owner->session().data().user(botId),
|
||||
.recipients = FromMTP(_owner, bot.vrecipients()),
|
||||
.permissions = FromMTP(bot.vrights()),
|
||||
};
|
||||
} else {
|
||||
_settings.force_assign(ChatbotsSettings());
|
||||
}
|
||||
}).fail([=](const MTP::Error &error) {
|
||||
_requestId = 0;
|
||||
LOG(("API Error: Could not get connected bots %1 (%2)"
|
||||
).arg(error.code()
|
||||
).arg(error.type()));
|
||||
}).send();
|
||||
}
|
||||
|
||||
bool Chatbots::loaded() const {
|
||||
return _loaded;
|
||||
}
|
||||
|
||||
const ChatbotsSettings &Chatbots::current() const {
|
||||
return _settings.current();
|
||||
}
|
||||
|
||||
rpl::producer<ChatbotsSettings> Chatbots::changes() const {
|
||||
return _settings.changes();
|
||||
}
|
||||
|
||||
rpl::producer<ChatbotsSettings> Chatbots::value() const {
|
||||
return _settings.value();
|
||||
}
|
||||
|
||||
void Chatbots::save(
|
||||
ChatbotsSettings settings,
|
||||
Fn<void()> done,
|
||||
Fn<void(QString)> fail) {
|
||||
const auto was = _settings.current();
|
||||
if (was == settings) {
|
||||
return;
|
||||
} else if (was.bot || settings.bot) {
|
||||
using Flag = MTPaccount_UpdateConnectedBot::Flag;
|
||||
const auto api = &_owner->session().api();
|
||||
api->request(MTPaccount_UpdateConnectedBot(
|
||||
MTP_flags(!settings.bot ? Flag::f_deleted : Flag::f_rights),
|
||||
ToMTP(settings.permissions),
|
||||
(settings.bot ? settings.bot : was.bot)->inputUser(),
|
||||
ForBotsToMTP(settings.recipients)
|
||||
)).done([=](const MTPUpdates &result) {
|
||||
api->applyUpdates(result);
|
||||
if (done) {
|
||||
done();
|
||||
}
|
||||
}).fail([=](const MTP::Error &error) {
|
||||
_settings = was;
|
||||
if (fail) {
|
||||
fail(error.type());
|
||||
}
|
||||
}).send();
|
||||
}
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
void Chatbots::togglePaused(not_null<PeerData*> peer, bool paused) {
|
||||
const auto type = paused
|
||||
? SentRequestType::Pause
|
||||
: SentRequestType::Unpause;
|
||||
const auto api = &_owner->session().api();
|
||||
const auto i = _sentRequests.find(peer);
|
||||
if (i != end(_sentRequests)) {
|
||||
const auto already = i->second.type;
|
||||
if (already == SentRequestType::Remove || already == type) {
|
||||
return;
|
||||
}
|
||||
api->request(i->second.requestId).cancel();
|
||||
_sentRequests.erase(i);
|
||||
}
|
||||
const auto id = api->request(MTPaccount_ToggleConnectedBotPaused(
|
||||
peer->input(),
|
||||
MTP_bool(paused)
|
||||
)).done([=] {
|
||||
if (_sentRequests[peer].type != type) {
|
||||
return;
|
||||
} else if (const auto settings = peer->barSettings()) {
|
||||
peer->setBarSettings(paused
|
||||
? ((*settings | PeerBarSetting::BusinessBotPaused)
|
||||
& ~PeerBarSetting::BusinessBotCanReply)
|
||||
: ((*settings & ~PeerBarSetting::BusinessBotPaused)
|
||||
| PeerBarSetting::BusinessBotCanReply));
|
||||
} else {
|
||||
api->requestPeerSettings(peer);
|
||||
}
|
||||
_sentRequests.remove(peer);
|
||||
}).fail([=] {
|
||||
if (_sentRequests[peer].type != type) {
|
||||
return;
|
||||
}
|
||||
api->requestPeerSettings(peer);
|
||||
_sentRequests.remove(peer);
|
||||
}).send();
|
||||
_sentRequests[peer] = SentRequest{ type, id };
|
||||
}
|
||||
|
||||
void Chatbots::removeFrom(not_null<PeerData*> peer) {
|
||||
const auto type = SentRequestType::Remove;
|
||||
const auto api = &_owner->session().api();
|
||||
const auto i = _sentRequests.find(peer);
|
||||
if (i != end(_sentRequests)) {
|
||||
const auto already = i->second.type;
|
||||
if (already == type) {
|
||||
return;
|
||||
}
|
||||
api->request(i->second.requestId).cancel();
|
||||
_sentRequests.erase(i);
|
||||
}
|
||||
const auto id = api->request(MTPaccount_DisablePeerConnectedBot(
|
||||
peer->input()
|
||||
)).done([=] {
|
||||
if (_sentRequests[peer].type != type) {
|
||||
return;
|
||||
} else if (const auto settings = peer->barSettings()) {
|
||||
peer->clearBusinessBot();
|
||||
} else {
|
||||
api->requestPeerSettings(peer);
|
||||
}
|
||||
_sentRequests.remove(peer);
|
||||
reload();
|
||||
}).fail([=] {
|
||||
api->requestPeerSettings(peer);
|
||||
_sentRequests.remove(peer);
|
||||
}).send();
|
||||
_sentRequests[peer] = SentRequest{ type, id };
|
||||
}
|
||||
|
||||
void Chatbots::reload() {
|
||||
_loaded = false;
|
||||
_owner->session().api().request(base::take(_requestId)).cancel();
|
||||
preload();
|
||||
}
|
||||
|
||||
EditFlagsDescriptor<ChatbotsPermissions> ChatbotsPermissionsLabels() {
|
||||
using Flag = ChatbotsPermission;
|
||||
|
||||
using PermissionLabel = EditFlagsLabel<ChatbotsPermissions>;
|
||||
auto messages = std::vector<PermissionLabel>{
|
||||
{ Flag::ViewMessages, tr::lng_chatbots_read(tr::now) },
|
||||
{ Flag::ReplyToMessages, tr::lng_chatbots_reply(tr::now) },
|
||||
{ Flag::MarkAsRead, tr::lng_chatbots_mark_as_read(tr::now) },
|
||||
{ Flag::DeleteSent, tr::lng_chatbots_delete_sent(tr::now) },
|
||||
{ Flag::DeleteReceived, tr::lng_chatbots_delete_received(tr::now) },
|
||||
};
|
||||
auto manage = std::vector<PermissionLabel>{
|
||||
{ Flag::EditName, tr::lng_chatbots_edit_name(tr::now) },
|
||||
{ Flag::EditBio, tr::lng_chatbots_edit_bio(tr::now) },
|
||||
{ Flag::EditUserpic, tr::lng_chatbots_edit_userpic(tr::now) },
|
||||
{ Flag::EditUsername, tr::lng_chatbots_edit_username(tr::now) },
|
||||
};
|
||||
auto gifts = std::vector<PermissionLabel>{
|
||||
{ Flag::ViewGifts, tr::lng_chatbots_view_gifts(tr::now) },
|
||||
{ Flag::SellGifts, tr::lng_chatbots_sell_gifts(tr::now) },
|
||||
{ Flag::GiftSettings, tr::lng_chatbots_gift_settings(tr::now) },
|
||||
{ Flag::TransferGifts, tr::lng_chatbots_transfer_gifts(tr::now) },
|
||||
{ Flag::TransferStars, tr::lng_chatbots_transfer_stars(tr::now) },
|
||||
};
|
||||
auto stories = std::vector<PermissionLabel>{
|
||||
{ Flag::ManageStories, tr::lng_chatbots_manage_stories(tr::now) },
|
||||
};
|
||||
return { .labels = {
|
||||
{ tr::lng_chatbots_manage_messages(), std::move(messages) },
|
||||
{ tr::lng_chatbots_manage_profile(), std::move(manage) },
|
||||
{ tr::lng_chatbots_manage_gifts(), std::move(gifts) },
|
||||
{ std::nullopt, std::move(stories) },
|
||||
}, .st = nullptr };
|
||||
}
|
||||
|
||||
} // namespace Data
|
||||
76
Telegram/SourceFiles/data/business/data_business_chatbots.h
Normal file
76
Telegram/SourceFiles/data/business/data_business_chatbots.h
Normal file
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "data/business/data_business_common.h"
|
||||
|
||||
class UserData;
|
||||
|
||||
template <typename Flags>
|
||||
struct EditFlagsDescriptor;
|
||||
|
||||
namespace Data {
|
||||
|
||||
class Session;
|
||||
|
||||
struct ChatbotsSettings {
|
||||
UserData *bot = nullptr;
|
||||
BusinessRecipients recipients;
|
||||
ChatbotsPermissions permissions;
|
||||
|
||||
friend inline bool operator==(
|
||||
const ChatbotsSettings &,
|
||||
const ChatbotsSettings &) = default;
|
||||
};
|
||||
|
||||
class Chatbots final {
|
||||
public:
|
||||
explicit Chatbots(not_null<Session*> owner);
|
||||
~Chatbots();
|
||||
|
||||
void preload();
|
||||
[[nodiscard]] bool loaded() const;
|
||||
[[nodiscard]] const ChatbotsSettings ¤t() const;
|
||||
[[nodiscard]] rpl::producer<ChatbotsSettings> changes() const;
|
||||
[[nodiscard]] rpl::producer<ChatbotsSettings> value() const;
|
||||
|
||||
void save(
|
||||
ChatbotsSettings settings,
|
||||
Fn<void()> done,
|
||||
Fn<void(QString)> fail);
|
||||
|
||||
void togglePaused(not_null<PeerData*> peer, bool paused);
|
||||
void removeFrom(not_null<PeerData*> peer);
|
||||
|
||||
private:
|
||||
enum class SentRequestType {
|
||||
Pause,
|
||||
Unpause,
|
||||
Remove,
|
||||
};
|
||||
struct SentRequest {
|
||||
SentRequestType type = SentRequestType::Pause;
|
||||
mtpRequestId requestId = 0;
|
||||
};
|
||||
|
||||
void reload();
|
||||
|
||||
const not_null<Session*> _owner;
|
||||
|
||||
rpl::variable<ChatbotsSettings> _settings;
|
||||
mtpRequestId _requestId = 0;
|
||||
bool _loaded = false;
|
||||
|
||||
base::flat_map<not_null<PeerData*>, SentRequest> _sentRequests;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] auto ChatbotsPermissionsLabels()
|
||||
-> EditFlagsDescriptor<ChatbotsPermissions>;
|
||||
|
||||
} // namespace Data
|
||||
398
Telegram/SourceFiles/data/business/data_business_common.cpp
Normal file
398
Telegram/SourceFiles/data/business/data_business_common.cpp
Normal file
@@ -0,0 +1,398 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "data/business/data_business_common.h"
|
||||
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_user.h"
|
||||
|
||||
namespace Data {
|
||||
namespace {
|
||||
|
||||
constexpr auto kDay = WorkingInterval::kDay;
|
||||
constexpr auto kWeek = WorkingInterval::kWeek;
|
||||
constexpr auto kInNextDayMax = WorkingInterval::kInNextDayMax;
|
||||
|
||||
[[nodiscard]] WorkingIntervals SortAndMerge(WorkingIntervals intervals) {
|
||||
auto &list = intervals.list;
|
||||
ranges::sort(list, ranges::less(), &WorkingInterval::start);
|
||||
for (auto i = 0, count = int(list.size()); i != count; ++i) {
|
||||
if (i && list[i] && list[i -1] && list[i].start <= list[i - 1].end) {
|
||||
list[i - 1] = list[i - 1].united(list[i]);
|
||||
list[i] = {};
|
||||
}
|
||||
if (!list[i]) {
|
||||
list.erase(list.begin() + i);
|
||||
--i;
|
||||
--count;
|
||||
}
|
||||
}
|
||||
return intervals;
|
||||
}
|
||||
|
||||
[[nodiscard]] WorkingIntervals MoveTailToFront(WorkingIntervals intervals) {
|
||||
auto &list = intervals.list;
|
||||
auto after = WorkingInterval{ kWeek, kWeek + kDay };
|
||||
while (!list.empty()) {
|
||||
if (const auto tail = list.back().intersected(after)) {
|
||||
list.back().end = tail.start;
|
||||
if (!list.back()) {
|
||||
list.pop_back();
|
||||
}
|
||||
list.insert(begin(list), tail.shifted(-kWeek));
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return intervals;
|
||||
}
|
||||
|
||||
template <typename Flag>
|
||||
auto RecipientsFlags(const BusinessRecipients &data) {
|
||||
const auto &chats = data.allButExcluded
|
||||
? data.excluded
|
||||
: data.included;
|
||||
using Type = BusinessChatType;
|
||||
return Flag()
|
||||
| ((chats.types & Type::NewChats) ? Flag::f_new_chats : Flag())
|
||||
| ((chats.types & Type::ExistingChats)
|
||||
? Flag::f_existing_chats
|
||||
: Flag())
|
||||
| ((chats.types & Type::Contacts) ? Flag::f_contacts : Flag())
|
||||
| ((chats.types & Type::NonContacts) ? Flag::f_non_contacts : Flag())
|
||||
| (chats.list.empty() ? Flag() : Flag::f_users)
|
||||
| (data.allButExcluded ? Flag::f_exclude_selected : Flag());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
BusinessRecipients BusinessRecipients::MakeValid(BusinessRecipients value) {
|
||||
if (value.included.empty()) {
|
||||
value.allButExcluded = true;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
MTPInputBusinessRecipients ForMessagesToMTP(const BusinessRecipients &data) {
|
||||
using Flag = MTPDinputBusinessRecipients::Flag;
|
||||
const auto &chats = data.allButExcluded ? data.excluded : data.included;
|
||||
return MTP_inputBusinessRecipients(
|
||||
MTP_flags(RecipientsFlags<Flag>(data)),
|
||||
MTP_vector_from_range(chats.list
|
||||
| ranges::views::transform(&UserData::inputUser)));
|
||||
}
|
||||
|
||||
MTPInputBusinessBotRecipients ForBotsToMTP(const BusinessRecipients &data) {
|
||||
using Flag = MTPDinputBusinessBotRecipients::Flag;
|
||||
const auto &chats = data.allButExcluded ? data.excluded : data.included;
|
||||
return MTP_inputBusinessBotRecipients(
|
||||
MTP_flags(RecipientsFlags<Flag>(data)
|
||||
| ((data.allButExcluded || data.excluded.empty())
|
||||
? Flag()
|
||||
: Flag::f_exclude_users)),
|
||||
MTP_vector_from_range(chats.list
|
||||
| ranges::views::transform(&UserData::inputUser)),
|
||||
MTP_vector_from_range(data.excluded.list
|
||||
| ranges::views::transform(&UserData::inputUser)));
|
||||
}
|
||||
|
||||
BusinessRecipients FromMTP(
|
||||
not_null<Session*> owner,
|
||||
const MTPBusinessRecipients &recipients) {
|
||||
using Type = BusinessChatType;
|
||||
|
||||
const auto &data = recipients.data();
|
||||
auto result = BusinessRecipients{
|
||||
.allButExcluded = data.is_exclude_selected(),
|
||||
};
|
||||
auto &chats = result.allButExcluded
|
||||
? result.excluded
|
||||
: result.included;
|
||||
chats.types = Type()
|
||||
| (data.is_new_chats() ? Type::NewChats : Type())
|
||||
| (data.is_existing_chats() ? Type::ExistingChats : Type())
|
||||
| (data.is_contacts() ? Type::Contacts : Type())
|
||||
| (data.is_non_contacts() ? Type::NonContacts : Type());
|
||||
if (const auto users = data.vusers()) {
|
||||
for (const auto &userId : users->v) {
|
||||
chats.list.push_back(owner->user(UserId(userId.v)));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
BusinessRecipients FromMTP(
|
||||
not_null<Session*> owner,
|
||||
const MTPBusinessBotRecipients &recipients) {
|
||||
using Type = BusinessChatType;
|
||||
|
||||
const auto &data = recipients.data();
|
||||
auto result = BusinessRecipients{
|
||||
.allButExcluded = data.is_exclude_selected(),
|
||||
};
|
||||
auto &chats = result.allButExcluded
|
||||
? result.excluded
|
||||
: result.included;
|
||||
chats.types = Type()
|
||||
| (data.is_new_chats() ? Type::NewChats : Type())
|
||||
| (data.is_existing_chats() ? Type::ExistingChats : Type())
|
||||
| (data.is_contacts() ? Type::Contacts : Type())
|
||||
| (data.is_non_contacts() ? Type::NonContacts : Type());
|
||||
if (const auto users = data.vusers()) {
|
||||
for (const auto &userId : users->v) {
|
||||
chats.list.push_back(owner->user(UserId(userId.v)));
|
||||
}
|
||||
}
|
||||
if (!result.allButExcluded) {
|
||||
if (const auto excluded = data.vexclude_users()) {
|
||||
for (const auto &userId : excluded->v) {
|
||||
result.excluded.list.push_back(
|
||||
owner->user(UserId(userId.v)));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
ChatbotsPermissions FromMTP(const MTPBusinessBotRights &rights) {
|
||||
using Flag = ChatbotsPermission;
|
||||
const auto &data = rights.data();
|
||||
|
||||
return Flag::ViewMessages
|
||||
| (data.is_reply() ? Flag::ReplyToMessages : Flag())
|
||||
| (data.is_read_messages() ? Flag::MarkAsRead : Flag())
|
||||
| (data.is_delete_sent_messages() ? Flag::DeleteSent : Flag())
|
||||
| (data.is_delete_received_messages() ? Flag::DeleteReceived : Flag())
|
||||
| (data.is_edit_name() ? Flag::EditName : Flag())
|
||||
| (data.is_edit_bio() ? Flag::EditBio : Flag())
|
||||
| (data.is_edit_profile_photo() ? Flag::EditUserpic : Flag())
|
||||
| (data.is_edit_username() ? Flag::EditUsername : Flag())
|
||||
| (data.is_view_gifts() ? Flag::ViewGifts : Flag())
|
||||
| (data.is_sell_gifts() ? Flag::SellGifts : Flag())
|
||||
| (data.is_change_gift_settings() ? Flag::GiftSettings : Flag())
|
||||
| (data.is_transfer_and_upgrade_gifts() ? Flag::TransferGifts : Flag())
|
||||
| (data.is_transfer_stars() ? Flag::TransferStars : Flag())
|
||||
| (data.is_manage_stories() ? Flag::ManageStories : Flag());
|
||||
}
|
||||
|
||||
MTPBusinessBotRights ToMTP(ChatbotsPermissions rights) {
|
||||
using Flag = MTPDbusinessBotRights::Flag;
|
||||
using Right = ChatbotsPermission;
|
||||
return MTP_businessBotRights(MTP_flags(Flag()
|
||||
| ((rights & Right::ReplyToMessages) ? Flag::f_reply : Flag())
|
||||
| ((rights & Right::MarkAsRead) ? Flag::f_read_messages : Flag())
|
||||
| ((rights & Right::DeleteSent) ? Flag::f_delete_sent_messages : Flag())
|
||||
| ((rights & Right::DeleteReceived) ? Flag::f_delete_received_messages : Flag())
|
||||
| ((rights & Right::EditName) ? Flag::f_edit_name : Flag())
|
||||
| ((rights & Right::EditBio) ? Flag::f_edit_bio : Flag())
|
||||
| ((rights & Right::EditUserpic) ? Flag::f_edit_profile_photo : Flag())
|
||||
| ((rights & Right::EditUsername) ? Flag::f_edit_username : Flag())
|
||||
| ((rights & Right::ViewGifts) ? Flag::f_view_gifts : Flag())
|
||||
| ((rights & Right::SellGifts) ? Flag::f_sell_gifts : Flag())
|
||||
| ((rights & Right::GiftSettings) ? Flag::f_change_gift_settings : Flag())
|
||||
| ((rights & Right::TransferGifts) ? Flag::f_transfer_and_upgrade_gifts : Flag())
|
||||
| ((rights & Right::TransferStars) ? Flag::f_transfer_stars : Flag())
|
||||
| ((rights & Right::ManageStories) ? Flag::f_manage_stories : Flag())));
|
||||
}
|
||||
|
||||
BusinessDetails FromMTP(
|
||||
not_null<Session*> owner,
|
||||
const tl::conditional<MTPBusinessWorkHours> &hours,
|
||||
const tl::conditional<MTPBusinessLocation> &location,
|
||||
const tl::conditional<MTPBusinessIntro> &intro) {
|
||||
auto result = BusinessDetails();
|
||||
if (hours) {
|
||||
const auto &data = hours->data();
|
||||
result.hours.timezoneId = qs(data.vtimezone_id());
|
||||
result.hours.intervals.list = ranges::views::all(
|
||||
data.vweekly_open().v
|
||||
) | ranges::views::transform([](const MTPBusinessWeeklyOpen &open) {
|
||||
const auto &data = open.data();
|
||||
return WorkingInterval{
|
||||
data.vstart_minute().v * 60,
|
||||
data.vend_minute().v * 60,
|
||||
};
|
||||
}) | ranges::to_vector;
|
||||
}
|
||||
if (location) {
|
||||
const auto &data = location->data();
|
||||
result.location.address = qs(data.vaddress());
|
||||
if (const auto point = data.vgeo_point()) {
|
||||
point->match([&](const MTPDgeoPoint &data) {
|
||||
result.location.point = LocationPoint(data);
|
||||
}, [&](const MTPDgeoPointEmpty &) {
|
||||
});
|
||||
}
|
||||
}
|
||||
if (intro) {
|
||||
const auto &data = intro->data();
|
||||
result.intro.title = qs(data.vtitle());
|
||||
result.intro.description = qs(data.vdescription());
|
||||
if (const auto document = data.vsticker()) {
|
||||
result.intro.sticker = owner->processDocument(*document);
|
||||
if (!result.intro.sticker->sticker()) {
|
||||
result.intro.sticker = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] AwaySettings FromMTP(
|
||||
not_null<Session*> owner,
|
||||
const tl::conditional<MTPBusinessAwayMessage> &message) {
|
||||
if (!message) {
|
||||
return AwaySettings();
|
||||
}
|
||||
const auto &data = message->data();
|
||||
auto result = AwaySettings{
|
||||
.recipients = FromMTP(owner, data.vrecipients()),
|
||||
.shortcutId = data.vshortcut_id().v,
|
||||
.offlineOnly = data.is_offline_only(),
|
||||
};
|
||||
data.vschedule().match([&](
|
||||
const MTPDbusinessAwayMessageScheduleAlways &) {
|
||||
result.schedule.type = AwayScheduleType::Always;
|
||||
}, [&](const MTPDbusinessAwayMessageScheduleOutsideWorkHours &) {
|
||||
result.schedule.type = AwayScheduleType::OutsideWorkingHours;
|
||||
}, [&](const MTPDbusinessAwayMessageScheduleCustom &data) {
|
||||
result.schedule.type = AwayScheduleType::Custom;
|
||||
result.schedule.customInterval = WorkingInterval{
|
||||
data.vstart_date().v,
|
||||
data.vend_date().v,
|
||||
};
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] GreetingSettings FromMTP(
|
||||
not_null<Session*> owner,
|
||||
const tl::conditional<MTPBusinessGreetingMessage> &message) {
|
||||
if (!message) {
|
||||
return GreetingSettings();
|
||||
}
|
||||
const auto &data = message->data();
|
||||
return GreetingSettings{
|
||||
.recipients = FromMTP(owner, data.vrecipients()),
|
||||
.noActivityDays = data.vno_activity_days().v,
|
||||
.shortcutId = data.vshortcut_id().v,
|
||||
};
|
||||
}
|
||||
|
||||
WorkingIntervals WorkingIntervals::normalized() const {
|
||||
return SortAndMerge(MoveTailToFront(SortAndMerge(*this)));
|
||||
}
|
||||
|
||||
WorkingIntervals ExtractDayIntervals(
|
||||
const WorkingIntervals &intervals,
|
||||
int dayIndex) {
|
||||
Expects(dayIndex >= 0 && dayIndex < 7);
|
||||
|
||||
auto result = WorkingIntervals();
|
||||
auto &list = result.list;
|
||||
for (const auto &interval : intervals.list) {
|
||||
const auto now = interval.intersected(
|
||||
{ (dayIndex - 1) * kDay, (dayIndex + 2) * kDay });
|
||||
const auto after = interval.intersected(
|
||||
{ (dayIndex + 6) * kDay, (dayIndex + 9) * kDay });
|
||||
const auto before = interval.intersected(
|
||||
{ (dayIndex - 8) * kDay, (dayIndex - 5) * kDay });
|
||||
if (now) {
|
||||
list.push_back(now.shifted(-dayIndex * kDay));
|
||||
}
|
||||
if (after) {
|
||||
list.push_back(after.shifted(-(dayIndex + 7) * kDay));
|
||||
}
|
||||
if (before) {
|
||||
list.push_back(before.shifted(-(dayIndex - 7) * kDay));
|
||||
}
|
||||
}
|
||||
result = result.normalized();
|
||||
|
||||
const auto outside = [&](WorkingInterval interval) {
|
||||
return (interval.end <= 0) || (interval.start >= kDay);
|
||||
};
|
||||
list.erase(ranges::remove_if(list, outside), end(list));
|
||||
|
||||
if (!list.empty() && list.back().start <= 0 && list.back().end >= kDay) {
|
||||
list.back() = { 0, kDay };
|
||||
} else if (!list.empty() && (list.back().end > kDay + kInNextDayMax)) {
|
||||
list.back() = list.back().intersected({ 0, kDay });
|
||||
}
|
||||
if (!list.empty() && list.front().start <= 0) {
|
||||
if (list.front().start < 0
|
||||
&& list.front().end <= kInNextDayMax
|
||||
&& list.front().start > -kDay) {
|
||||
list.erase(begin(list));
|
||||
} else {
|
||||
list.front() = list.front().intersected({ 0, kDay });
|
||||
if (!list.front()) {
|
||||
list.erase(begin(list));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool IsFullOpen(const WorkingIntervals &extractedDay) {
|
||||
return extractedDay // 00:00-23:59 or 00:00-00:00 (next day)
|
||||
&& (extractedDay.list.front() == WorkingInterval{ 0, kDay - 60 }
|
||||
|| extractedDay.list.front() == WorkingInterval{ 0, kDay });
|
||||
}
|
||||
|
||||
WorkingIntervals RemoveDayIntervals(
|
||||
const WorkingIntervals &intervals,
|
||||
int dayIndex) {
|
||||
auto result = intervals.normalized();
|
||||
auto &list = result.list;
|
||||
const auto day = WorkingInterval{ 0, kDay };
|
||||
const auto shifted = day.shifted(dayIndex * kDay);
|
||||
auto before = WorkingInterval{ 0, shifted.start };
|
||||
auto after = WorkingInterval{ shifted.end, kWeek };
|
||||
for (auto i = 0, count = int(list.size()); i != count; ++i) {
|
||||
if (list[i].end <= shifted.start || list[i].start >= shifted.end) {
|
||||
continue;
|
||||
} else if (list[i].end <= shifted.start + kInNextDayMax
|
||||
&& (list[i].start < shifted.start
|
||||
|| (!dayIndex // This 'Sunday' finishing on next day <= 6:00.
|
||||
&& list[i].start == shifted.start
|
||||
&& list.back().end >= kWeek))) {
|
||||
continue;
|
||||
} else if (const auto first = list[i].intersected(before)) {
|
||||
list[i] = first;
|
||||
if (const auto second = list[i].intersected(after)) {
|
||||
list.push_back(second);
|
||||
}
|
||||
} else if (const auto second = list[i].intersected(after)) {
|
||||
list[i] = second;
|
||||
} else {
|
||||
list.erase(list.begin() + i);
|
||||
--i;
|
||||
--count;
|
||||
}
|
||||
}
|
||||
return result.normalized();
|
||||
}
|
||||
|
||||
WorkingIntervals ReplaceDayIntervals(
|
||||
const WorkingIntervals &intervals,
|
||||
int dayIndex,
|
||||
WorkingIntervals replacement) {
|
||||
auto result = RemoveDayIntervals(intervals, dayIndex);
|
||||
const auto first = result.list.insert(
|
||||
end(result.list),
|
||||
begin(replacement.list),
|
||||
end(replacement.list));
|
||||
for (auto &interval : ranges::make_subrange(first, end(result.list))) {
|
||||
interval = interval.shifted(dayIndex * kDay);
|
||||
}
|
||||
return result.normalized();
|
||||
}
|
||||
|
||||
} // namespace Data
|
||||
302
Telegram/SourceFiles/data/business/data_business_common.h
Normal file
302
Telegram/SourceFiles/data/business/data_business_common.h
Normal file
@@ -0,0 +1,302 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/flags.h"
|
||||
#include "data/data_location.h"
|
||||
|
||||
class UserData;
|
||||
|
||||
namespace Data {
|
||||
|
||||
class Session;
|
||||
|
||||
enum class BusinessChatType {
|
||||
NewChats = (1 << 0),
|
||||
ExistingChats = (1 << 1),
|
||||
Contacts = (1 << 2),
|
||||
NonContacts = (1 << 3),
|
||||
};
|
||||
inline constexpr bool is_flag_type(BusinessChatType) { return true; }
|
||||
|
||||
using BusinessChatTypes = base::flags<BusinessChatType>;
|
||||
|
||||
struct BusinessChats {
|
||||
BusinessChatTypes types;
|
||||
std::vector<not_null<UserData*>> list;
|
||||
|
||||
[[nodiscard]] bool empty() const {
|
||||
return !types && list.empty();
|
||||
}
|
||||
|
||||
friend inline bool operator==(
|
||||
const BusinessChats &a,
|
||||
const BusinessChats &b) = default;
|
||||
};
|
||||
|
||||
struct BusinessRecipients {
|
||||
BusinessChats included;
|
||||
BusinessChats excluded;
|
||||
bool allButExcluded = false;
|
||||
|
||||
[[nodiscard]] static BusinessRecipients MakeValid(
|
||||
BusinessRecipients value);
|
||||
|
||||
friend inline bool operator==(
|
||||
const BusinessRecipients &a,
|
||||
const BusinessRecipients &b) = default;
|
||||
};
|
||||
|
||||
enum class BusinessRecipientsType : uchar {
|
||||
Messages,
|
||||
Bots,
|
||||
};
|
||||
|
||||
enum class ChatbotsPermission {
|
||||
ViewMessages = 0x0001,
|
||||
ReplyToMessages = 0x0002,
|
||||
MarkAsRead = 0x0004,
|
||||
DeleteSent = 0x0008,
|
||||
DeleteReceived = 0x0010,
|
||||
EditName = 0x0020,
|
||||
EditBio = 0x0040,
|
||||
EditUserpic = 0x0080,
|
||||
EditUsername = 0x0100,
|
||||
ViewGifts = 0x0200,
|
||||
SellGifts = 0x0400,
|
||||
GiftSettings = 0x0800,
|
||||
TransferGifts = 0x1000,
|
||||
TransferStars = 0x2000,
|
||||
ManageStories = 0x4000,
|
||||
};
|
||||
inline constexpr bool is_flag_type(ChatbotsPermission) { return true; }
|
||||
using ChatbotsPermissions = base::flags<ChatbotsPermission>;
|
||||
|
||||
[[nodiscard]] MTPInputBusinessRecipients ForMessagesToMTP(
|
||||
const BusinessRecipients &data);
|
||||
[[nodiscard]] MTPInputBusinessBotRecipients ForBotsToMTP(
|
||||
const BusinessRecipients &data);
|
||||
[[nodiscard]] BusinessRecipients FromMTP(
|
||||
not_null<Session*> owner,
|
||||
const MTPBusinessRecipients &recipients);
|
||||
[[nodiscard]] BusinessRecipients FromMTP(
|
||||
not_null<Session*> owner,
|
||||
const MTPBusinessBotRecipients &recipients);
|
||||
[[nodiscard]] ChatbotsPermissions FromMTP(
|
||||
const MTPBusinessBotRights &rights);
|
||||
[[nodiscard]] MTPBusinessBotRights ToMTP(ChatbotsPermissions rights);
|
||||
|
||||
struct Timezone {
|
||||
QString id;
|
||||
QString name;
|
||||
TimeId utcOffset = 0;
|
||||
|
||||
friend inline bool operator==(
|
||||
const Timezone &a,
|
||||
const Timezone &b) = default;
|
||||
};
|
||||
|
||||
struct Timezones {
|
||||
std::vector<Timezone> list;
|
||||
|
||||
friend inline bool operator==(
|
||||
const Timezones &a,
|
||||
const Timezones &b) = default;
|
||||
};;
|
||||
|
||||
struct WorkingInterval {
|
||||
static constexpr auto kDay = 24 * 3600;
|
||||
static constexpr auto kWeek = 7 * kDay;
|
||||
static constexpr auto kInNextDayMax = 6 * 3600;
|
||||
|
||||
TimeId start = 0;
|
||||
TimeId end = 0;
|
||||
|
||||
explicit operator bool() const {
|
||||
return start < end;
|
||||
}
|
||||
|
||||
[[nodiscard]] WorkingInterval shifted(TimeId offset) const {
|
||||
return { start + offset, end + offset };
|
||||
}
|
||||
[[nodiscard]] WorkingInterval united(WorkingInterval other) const {
|
||||
if (!*this) {
|
||||
return other;
|
||||
} else if (!other) {
|
||||
return *this;
|
||||
}
|
||||
return {
|
||||
std::min(start, other.start),
|
||||
std::max(end, other.end),
|
||||
};
|
||||
}
|
||||
[[nodiscard]] WorkingInterval intersected(WorkingInterval other) const {
|
||||
const auto result = WorkingInterval{
|
||||
std::max(start, other.start),
|
||||
std::min(end, other.end),
|
||||
};
|
||||
return result ? result : WorkingInterval();
|
||||
}
|
||||
|
||||
friend inline bool operator==(
|
||||
const WorkingInterval &a,
|
||||
const WorkingInterval &b) = default;
|
||||
};
|
||||
|
||||
struct WorkingIntervals {
|
||||
std::vector<WorkingInterval> list;
|
||||
|
||||
[[nodiscard]] WorkingIntervals normalized() const;
|
||||
|
||||
explicit operator bool() const {
|
||||
for (const auto &interval : list) {
|
||||
if (interval) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
friend inline bool operator==(
|
||||
const WorkingIntervals &a,
|
||||
const WorkingIntervals &b) = default;
|
||||
};
|
||||
|
||||
struct WorkingHours {
|
||||
WorkingIntervals intervals;
|
||||
QString timezoneId;
|
||||
|
||||
[[nodiscard]] WorkingHours normalized() const {
|
||||
return { intervals.normalized(), timezoneId };
|
||||
}
|
||||
|
||||
explicit operator bool() const {
|
||||
return !timezoneId.isEmpty() && !intervals.list.empty();
|
||||
}
|
||||
|
||||
friend inline bool operator==(
|
||||
const WorkingHours &a,
|
||||
const WorkingHours &b) = default;
|
||||
};
|
||||
|
||||
[[nodiscard]] WorkingIntervals ExtractDayIntervals(
|
||||
const WorkingIntervals &intervals,
|
||||
int dayIndex);
|
||||
[[nodiscard]] bool IsFullOpen(const WorkingIntervals &extractedDay);
|
||||
[[nodiscard]] WorkingIntervals RemoveDayIntervals(
|
||||
const WorkingIntervals &intervals,
|
||||
int dayIndex);
|
||||
[[nodiscard]] WorkingIntervals ReplaceDayIntervals(
|
||||
const WorkingIntervals &intervals,
|
||||
int dayIndex,
|
||||
WorkingIntervals replacement);
|
||||
|
||||
struct BusinessLocation {
|
||||
QString address;
|
||||
std::optional<LocationPoint> point;
|
||||
|
||||
explicit operator bool() const {
|
||||
return !address.isEmpty();
|
||||
}
|
||||
|
||||
friend inline bool operator==(
|
||||
const BusinessLocation &a,
|
||||
const BusinessLocation &b) = default;
|
||||
};
|
||||
|
||||
struct ChatIntro {
|
||||
QString title;
|
||||
QString description;
|
||||
DocumentData *sticker = nullptr;
|
||||
|
||||
[[nodiscard]] bool customPhrases() const {
|
||||
return !title.isEmpty() || !description.isEmpty();
|
||||
}
|
||||
|
||||
explicit operator bool() const {
|
||||
return customPhrases() || sticker;
|
||||
}
|
||||
|
||||
friend inline bool operator==(
|
||||
const ChatIntro &a,
|
||||
const ChatIntro &b) = default;
|
||||
};
|
||||
|
||||
struct BusinessDetails {
|
||||
WorkingHours hours;
|
||||
BusinessLocation location;
|
||||
ChatIntro intro;
|
||||
|
||||
explicit operator bool() const {
|
||||
return hours || location || intro;
|
||||
}
|
||||
|
||||
friend inline bool operator==(
|
||||
const BusinessDetails &a,
|
||||
const BusinessDetails &b) = default;
|
||||
};
|
||||
|
||||
[[nodiscard]] BusinessDetails FromMTP(
|
||||
not_null<Session*> owner,
|
||||
const tl::conditional<MTPBusinessWorkHours> &hours,
|
||||
const tl::conditional<MTPBusinessLocation> &location,
|
||||
const tl::conditional<MTPBusinessIntro> &intro);
|
||||
|
||||
enum class AwayScheduleType : uchar {
|
||||
Never = 0,
|
||||
Always = 1,
|
||||
OutsideWorkingHours = 2,
|
||||
Custom = 3,
|
||||
};
|
||||
|
||||
struct AwaySchedule {
|
||||
AwayScheduleType type = AwayScheduleType::Never;
|
||||
WorkingInterval customInterval;
|
||||
|
||||
friend inline bool operator==(
|
||||
const AwaySchedule &a,
|
||||
const AwaySchedule &b) = default;
|
||||
};
|
||||
|
||||
struct AwaySettings {
|
||||
BusinessRecipients recipients;
|
||||
AwaySchedule schedule;
|
||||
BusinessShortcutId shortcutId = 0;
|
||||
bool offlineOnly = false;
|
||||
|
||||
explicit operator bool() const {
|
||||
return schedule.type != AwayScheduleType::Never;
|
||||
}
|
||||
|
||||
friend inline bool operator==(
|
||||
const AwaySettings &a,
|
||||
const AwaySettings &b) = default;
|
||||
};
|
||||
|
||||
[[nodiscard]] AwaySettings FromMTP(
|
||||
not_null<Session*> owner,
|
||||
const tl::conditional<MTPBusinessAwayMessage> &message);
|
||||
|
||||
struct GreetingSettings {
|
||||
BusinessRecipients recipients;
|
||||
int noActivityDays = 0;
|
||||
BusinessShortcutId shortcutId = 0;
|
||||
|
||||
explicit operator bool() const {
|
||||
return noActivityDays > 0;
|
||||
}
|
||||
|
||||
friend inline bool operator==(
|
||||
const GreetingSettings &a,
|
||||
const GreetingSettings &b) = default;
|
||||
};
|
||||
|
||||
[[nodiscard]] GreetingSettings FromMTP(
|
||||
not_null<Session*> owner,
|
||||
const tl::conditional<MTPBusinessGreetingMessage> &message);
|
||||
|
||||
} // namespace Data
|
||||
318
Telegram/SourceFiles/data/business/data_business_info.cpp
Normal file
318
Telegram/SourceFiles/data/business/data_business_info.cpp
Normal file
@@ -0,0 +1,318 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "data/business/data_business_info.h"
|
||||
|
||||
#include "apiwrap.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "data/business/data_business_common.h"
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_user.h"
|
||||
#include "main/main_session.h"
|
||||
|
||||
namespace Data {
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] MTPBusinessWorkHours ToMTP(const WorkingHours &data) {
|
||||
const auto list = data.intervals.normalized().list;
|
||||
const auto proj = [](const WorkingInterval &data) {
|
||||
return MTPBusinessWeeklyOpen(MTP_businessWeeklyOpen(
|
||||
MTP_int(data.start / 60),
|
||||
MTP_int(data.end / 60)));
|
||||
};
|
||||
return MTP_businessWorkHours(
|
||||
MTP_flags(0),
|
||||
MTP_string(data.timezoneId),
|
||||
MTP_vector_from_range(list | ranges::views::transform(proj)));
|
||||
}
|
||||
|
||||
[[nodiscard]] MTPBusinessAwayMessageSchedule ToMTP(
|
||||
const AwaySchedule &data) {
|
||||
Expects(data.type != AwayScheduleType::Never);
|
||||
|
||||
return (data.type == AwayScheduleType::Always)
|
||||
? MTP_businessAwayMessageScheduleAlways()
|
||||
: (data.type == AwayScheduleType::OutsideWorkingHours)
|
||||
? MTP_businessAwayMessageScheduleOutsideWorkHours()
|
||||
: MTP_businessAwayMessageScheduleCustom(
|
||||
MTP_int(data.customInterval.start),
|
||||
MTP_int(data.customInterval.end));
|
||||
}
|
||||
|
||||
[[nodiscard]] MTPInputBusinessAwayMessage ToMTP(const AwaySettings &data) {
|
||||
using Flag = MTPDinputBusinessAwayMessage::Flag;
|
||||
return MTP_inputBusinessAwayMessage(
|
||||
MTP_flags(data.offlineOnly ? Flag::f_offline_only : Flag()),
|
||||
MTP_int(data.shortcutId),
|
||||
ToMTP(data.schedule),
|
||||
ForMessagesToMTP(data.recipients));
|
||||
}
|
||||
|
||||
[[nodiscard]] MTPInputBusinessGreetingMessage ToMTP(
|
||||
const GreetingSettings &data) {
|
||||
return MTP_inputBusinessGreetingMessage(
|
||||
MTP_int(data.shortcutId),
|
||||
ForMessagesToMTP(data.recipients),
|
||||
MTP_int(data.noActivityDays));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
BusinessInfo::BusinessInfo(not_null<Session*> owner)
|
||||
: _owner(owner) {
|
||||
}
|
||||
|
||||
BusinessInfo::~BusinessInfo() = default;
|
||||
|
||||
void BusinessInfo::saveWorkingHours(
|
||||
WorkingHours data,
|
||||
Fn<void(QString)> fail) {
|
||||
const auto session = &_owner->session();
|
||||
auto details = session->user()->businessDetails();
|
||||
const auto &was = details.hours;
|
||||
if (was == data) {
|
||||
return;
|
||||
}
|
||||
|
||||
using Flag = MTPaccount_UpdateBusinessWorkHours::Flag;
|
||||
session->api().request(MTPaccount_UpdateBusinessWorkHours(
|
||||
MTP_flags(data ? Flag::f_business_work_hours : Flag()),
|
||||
ToMTP(data)
|
||||
)).fail([=](const MTP::Error &error) {
|
||||
auto details = session->user()->businessDetails();
|
||||
details.hours = was;
|
||||
session->user()->setBusinessDetails(std::move(details));
|
||||
if (fail) {
|
||||
fail(error.type());
|
||||
}
|
||||
}).send();
|
||||
|
||||
details.hours = std::move(data);
|
||||
session->user()->setBusinessDetails(std::move(details));
|
||||
}
|
||||
|
||||
void BusinessInfo::saveChatIntro(ChatIntro data, Fn<void(QString)> fail) {
|
||||
const auto session = &_owner->session();
|
||||
auto details = session->user()->businessDetails();
|
||||
const auto &was = details.intro;
|
||||
if (was == data) {
|
||||
return;
|
||||
} else {
|
||||
const auto session = &_owner->session();
|
||||
using Flag = MTPaccount_UpdateBusinessIntro::Flag;
|
||||
session->api().request(MTPaccount_UpdateBusinessIntro(
|
||||
MTP_flags(data ? Flag::f_intro : Flag()),
|
||||
MTP_inputBusinessIntro(
|
||||
MTP_flags(data.sticker
|
||||
? MTPDinputBusinessIntro::Flag::f_sticker
|
||||
: MTPDinputBusinessIntro::Flag()),
|
||||
MTP_string(data.title),
|
||||
MTP_string(data.description),
|
||||
(data.sticker
|
||||
? data.sticker->mtpInput()
|
||||
: MTP_inputDocumentEmpty()))
|
||||
)).fail([=](const MTP::Error &error) {
|
||||
auto details = session->user()->businessDetails();
|
||||
details.intro = was;
|
||||
session->user()->setBusinessDetails(std::move(details));
|
||||
if (fail) {
|
||||
fail(error.type());
|
||||
}
|
||||
}).send();
|
||||
}
|
||||
|
||||
details.intro = std::move(data);
|
||||
session->user()->setBusinessDetails(std::move(details));
|
||||
}
|
||||
|
||||
void BusinessInfo::saveLocation(
|
||||
BusinessLocation data,
|
||||
Fn<void(QString)> fail) {
|
||||
const auto session = &_owner->session();
|
||||
auto details = session->user()->businessDetails();
|
||||
const auto &was = details.location;
|
||||
if (was == data) {
|
||||
return;
|
||||
} else {
|
||||
const auto session = &_owner->session();
|
||||
using Flag = MTPaccount_UpdateBusinessLocation::Flag;
|
||||
session->api().request(MTPaccount_UpdateBusinessLocation(
|
||||
MTP_flags((data.point ? Flag::f_geo_point : Flag())
|
||||
| (data.address.isEmpty() ? Flag() : Flag::f_address)),
|
||||
(data.point
|
||||
? MTP_inputGeoPoint(
|
||||
MTP_flags(0),
|
||||
MTP_double(data.point->lat()),
|
||||
MTP_double(data.point->lon()),
|
||||
MTPint()) // accuracy_radius
|
||||
: MTP_inputGeoPointEmpty()),
|
||||
MTP_string(data.address)
|
||||
)).fail([=](const MTP::Error &error) {
|
||||
auto details = session->user()->businessDetails();
|
||||
details.location = was;
|
||||
session->user()->setBusinessDetails(std::move(details));
|
||||
if (fail) {
|
||||
fail(error.type());
|
||||
}
|
||||
}).send();
|
||||
}
|
||||
|
||||
details.location = std::move(data);
|
||||
session->user()->setBusinessDetails(std::move(details));
|
||||
}
|
||||
|
||||
void BusinessInfo::applyAwaySettings(AwaySettings data) {
|
||||
if (_awaySettings == data) {
|
||||
return;
|
||||
}
|
||||
_awaySettings = data;
|
||||
_awaySettingsChanged.fire({});
|
||||
}
|
||||
|
||||
void BusinessInfo::saveAwaySettings(
|
||||
AwaySettings data,
|
||||
Fn<void(QString)> fail) {
|
||||
const auto &was = _awaySettings;
|
||||
if (was == data) {
|
||||
return;
|
||||
} else if (!data || data.shortcutId) {
|
||||
using Flag = MTPaccount_UpdateBusinessAwayMessage::Flag;
|
||||
const auto session = &_owner->session();
|
||||
session->api().request(MTPaccount_UpdateBusinessAwayMessage(
|
||||
MTP_flags(data ? Flag::f_message : Flag()),
|
||||
data ? ToMTP(data) : MTPInputBusinessAwayMessage()
|
||||
)).fail([=](const MTP::Error &error) {
|
||||
_awaySettings = was;
|
||||
_awaySettingsChanged.fire({});
|
||||
if (fail) {
|
||||
fail(error.type());
|
||||
}
|
||||
}).send();
|
||||
}
|
||||
_awaySettings = std::move(data);
|
||||
_awaySettingsChanged.fire({});
|
||||
}
|
||||
|
||||
bool BusinessInfo::awaySettingsLoaded() const {
|
||||
return _awaySettings.has_value();
|
||||
}
|
||||
|
||||
AwaySettings BusinessInfo::awaySettings() const {
|
||||
return _awaySettings.value_or(AwaySettings());
|
||||
}
|
||||
|
||||
rpl::producer<> BusinessInfo::awaySettingsChanged() const {
|
||||
return _awaySettingsChanged.events();
|
||||
}
|
||||
|
||||
void BusinessInfo::applyGreetingSettings(GreetingSettings data) {
|
||||
if (_greetingSettings == data) {
|
||||
return;
|
||||
}
|
||||
_greetingSettings = data;
|
||||
_greetingSettingsChanged.fire({});
|
||||
}
|
||||
|
||||
void BusinessInfo::saveGreetingSettings(
|
||||
GreetingSettings data,
|
||||
Fn<void(QString)> fail) {
|
||||
const auto &was = _greetingSettings;
|
||||
if (was == data) {
|
||||
return;
|
||||
} else if (!data || data.shortcutId) {
|
||||
using Flag = MTPaccount_UpdateBusinessGreetingMessage::Flag;
|
||||
_owner->session().api().request(
|
||||
MTPaccount_UpdateBusinessGreetingMessage(
|
||||
MTP_flags(data ? Flag::f_message : Flag()),
|
||||
data ? ToMTP(data) : MTPInputBusinessGreetingMessage())
|
||||
).fail([=](const MTP::Error &error) {
|
||||
_greetingSettings = was;
|
||||
_greetingSettingsChanged.fire({});
|
||||
if (fail) {
|
||||
fail(error.type());
|
||||
}
|
||||
}).send();
|
||||
}
|
||||
_greetingSettings = std::move(data);
|
||||
_greetingSettingsChanged.fire({});
|
||||
}
|
||||
|
||||
bool BusinessInfo::greetingSettingsLoaded() const {
|
||||
return _greetingSettings.has_value();
|
||||
}
|
||||
|
||||
GreetingSettings BusinessInfo::greetingSettings() const {
|
||||
return _greetingSettings.value_or(GreetingSettings());
|
||||
}
|
||||
|
||||
rpl::producer<> BusinessInfo::greetingSettingsChanged() const {
|
||||
return _greetingSettingsChanged.events();
|
||||
}
|
||||
|
||||
void BusinessInfo::preload() {
|
||||
preloadTimezones();
|
||||
}
|
||||
|
||||
void BusinessInfo::preloadTimezones() {
|
||||
if (!_timezones.current().list.empty() || _timezonesRequestId) {
|
||||
return;
|
||||
}
|
||||
_timezonesRequestId = _owner->session().api().request(
|
||||
MTPhelp_GetTimezonesList(MTP_int(_timezonesHash))
|
||||
).done([=](const MTPhelp_TimezonesList &result) {
|
||||
result.match([&](const MTPDhelp_timezonesList &data) {
|
||||
_timezonesHash = data.vhash().v;
|
||||
const auto proj = [](const MTPtimezone &result) {
|
||||
return Timezone{
|
||||
.id = qs(result.data().vid()),
|
||||
.name = qs(result.data().vname()),
|
||||
.utcOffset = result.data().vutc_offset().v,
|
||||
};
|
||||
};
|
||||
_timezones = Timezones{
|
||||
.list = ranges::views::all(
|
||||
data.vtimezones().v
|
||||
) | ranges::views::transform(
|
||||
proj
|
||||
) | ranges::to_vector,
|
||||
};
|
||||
}, [](const MTPDhelp_timezonesListNotModified &) {
|
||||
});
|
||||
}).send();
|
||||
}
|
||||
|
||||
rpl::producer<Timezones> BusinessInfo::timezonesValue() const {
|
||||
const_cast<BusinessInfo*>(this)->preloadTimezones();
|
||||
return _timezones.value();
|
||||
}
|
||||
|
||||
bool BusinessInfo::timezonesLoaded() const {
|
||||
return !_timezones.current().list.empty();
|
||||
}
|
||||
|
||||
QString FindClosestTimezoneId(const std::vector<Timezone> &list) {
|
||||
const auto local = QDateTime::currentDateTime();
|
||||
const auto utc = QDateTime(local.date(), local.time(), Qt::UTC);
|
||||
const auto shift = base::unixtime::now() - (TimeId)::time(nullptr);
|
||||
const auto delta = int(utc.toSecsSinceEpoch())
|
||||
- int(local.toSecsSinceEpoch())
|
||||
- shift;
|
||||
const auto proj = [&](const Timezone &value) {
|
||||
auto distance = value.utcOffset - delta;
|
||||
while (distance > 12 * 3600) {
|
||||
distance -= 24 * 3600;
|
||||
}
|
||||
while (distance < -12 * 3600) {
|
||||
distance += 24 * 3600;
|
||||
}
|
||||
return std::abs(distance);
|
||||
};
|
||||
return ranges::min_element(list, ranges::less(), proj)->id;
|
||||
}
|
||||
|
||||
} // namespace Data
|
||||
64
Telegram/SourceFiles/data/business/data_business_info.h
Normal file
64
Telegram/SourceFiles/data/business/data_business_info.h
Normal file
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "data/business/data_business_common.h"
|
||||
|
||||
namespace Data {
|
||||
|
||||
class Session;
|
||||
|
||||
class BusinessInfo final {
|
||||
public:
|
||||
explicit BusinessInfo(not_null<Session*> owner);
|
||||
~BusinessInfo();
|
||||
|
||||
void preload();
|
||||
|
||||
void saveWorkingHours(WorkingHours data, Fn<void(QString)> fail);
|
||||
void saveChatIntro(ChatIntro data, Fn<void(QString)> fail);
|
||||
void saveLocation(BusinessLocation data, Fn<void(QString)> fail);
|
||||
|
||||
void saveAwaySettings(AwaySettings data, Fn<void(QString)> fail);
|
||||
void applyAwaySettings(AwaySettings data);
|
||||
[[nodiscard]] AwaySettings awaySettings() const;
|
||||
[[nodiscard]] bool awaySettingsLoaded() const;
|
||||
[[nodiscard]] rpl::producer<> awaySettingsChanged() const;
|
||||
|
||||
void saveGreetingSettings(
|
||||
GreetingSettings data,
|
||||
Fn<void(QString)> fail);
|
||||
void applyGreetingSettings(GreetingSettings data);
|
||||
[[nodiscard]] GreetingSettings greetingSettings() const;
|
||||
[[nodiscard]] bool greetingSettingsLoaded() const;
|
||||
[[nodiscard]] rpl::producer<> greetingSettingsChanged() const;
|
||||
|
||||
void preloadTimezones();
|
||||
[[nodiscard]] bool timezonesLoaded() const;
|
||||
[[nodiscard]] rpl::producer<Timezones> timezonesValue() const;
|
||||
|
||||
private:
|
||||
const not_null<Session*> _owner;
|
||||
|
||||
rpl::variable<Timezones> _timezones;
|
||||
|
||||
std::optional<AwaySettings> _awaySettings;
|
||||
rpl::event_stream<> _awaySettingsChanged;
|
||||
|
||||
std::optional<GreetingSettings> _greetingSettings;
|
||||
rpl::event_stream<> _greetingSettingsChanged;
|
||||
|
||||
mtpRequestId _timezonesRequestId = 0;
|
||||
int32 _timezonesHash = 0;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] QString FindClosestTimezoneId(
|
||||
const std::vector<Timezone> &list);
|
||||
|
||||
} // namespace Data
|
||||
786
Telegram/SourceFiles/data/business/data_shortcut_messages.cpp
Normal file
786
Telegram/SourceFiles/data/business/data_shortcut_messages.cpp
Normal file
@@ -0,0 +1,786 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "data/business/data_shortcut_messages.h"
|
||||
|
||||
#include "api/api_hash.h"
|
||||
#include "apiwrap.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_session.h"
|
||||
#include "api/api_text_entities.h"
|
||||
#include "main/main_session.h"
|
||||
#include "history/history.h"
|
||||
#include "history/history_item_components.h"
|
||||
#include "history/history_item_helpers.h"
|
||||
#include "apiwrap.h"
|
||||
|
||||
namespace Data {
|
||||
namespace {
|
||||
|
||||
constexpr auto kRequestTimeLimit = 60 * crl::time(1000);
|
||||
|
||||
[[nodiscard]] MsgId RemoteToLocalMsgId(MsgId id) {
|
||||
Expects(IsServerMsgId(id));
|
||||
|
||||
return ScheduledMaxMsgId + id + 1;
|
||||
}
|
||||
|
||||
[[nodiscard]] MsgId LocalToRemoteMsgId(MsgId id) {
|
||||
Expects(IsShortcutMsgId(id));
|
||||
|
||||
return (id - ScheduledMaxMsgId - 1);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool TooEarlyForRequest(crl::time received) {
|
||||
return (received > 0) && (received + kRequestTimeLimit > crl::now());
|
||||
}
|
||||
|
||||
[[nodiscard]] MTPMessage PrepareMessage(
|
||||
BusinessShortcutId shortcutId,
|
||||
const MTPMessage &message) {
|
||||
return message.match([&](const MTPDmessageEmpty &data) {
|
||||
return MTP_messageEmpty(
|
||||
data.vflags(),
|
||||
data.vid(),
|
||||
data.vpeer_id() ? *data.vpeer_id() : MTPPeer());
|
||||
}, [&](const MTPDmessageService &data) {
|
||||
return MTP_messageService(
|
||||
data.vflags(),
|
||||
data.vid(),
|
||||
data.vfrom_id() ? *data.vfrom_id() : MTPPeer(),
|
||||
data.vpeer_id(),
|
||||
data.vsaved_peer_id() ? *data.vsaved_peer_id() : MTPPeer(),
|
||||
data.vreply_to() ? *data.vreply_to() : MTPMessageReplyHeader(),
|
||||
data.vdate(),
|
||||
data.vaction(),
|
||||
data.vreactions() ? *data.vreactions() : MTPMessageReactions(),
|
||||
MTP_int(data.vttl_period().value_or_empty()));
|
||||
}, [&](const MTPDmessage &data) {
|
||||
return MTP_message(
|
||||
MTP_flags(data.vflags().v
|
||||
| MTPDmessage::Flag::f_quick_reply_shortcut_id),
|
||||
data.vid(),
|
||||
data.vfrom_id() ? *data.vfrom_id() : MTPPeer(),
|
||||
MTPint(), // from_boosts_applied
|
||||
data.vpeer_id(),
|
||||
data.vsaved_peer_id() ? *data.vsaved_peer_id() : MTPPeer(),
|
||||
data.vfwd_from() ? *data.vfwd_from() : MTPMessageFwdHeader(),
|
||||
MTP_long(data.vvia_bot_id().value_or_empty()),
|
||||
MTP_long(data.vvia_business_bot_id().value_or_empty()),
|
||||
data.vreply_to() ? *data.vreply_to() : MTPMessageReplyHeader(),
|
||||
data.vdate(),
|
||||
data.vmessage(),
|
||||
data.vmedia() ? *data.vmedia() : MTPMessageMedia(),
|
||||
data.vreply_markup() ? *data.vreply_markup() : MTPReplyMarkup(),
|
||||
(data.ventities()
|
||||
? *data.ventities()
|
||||
: MTPVector<MTPMessageEntity>()),
|
||||
MTP_int(data.vviews().value_or_empty()),
|
||||
MTP_int(data.vforwards().value_or_empty()),
|
||||
data.vreplies() ? *data.vreplies() : MTPMessageReplies(),
|
||||
MTP_int(data.vedit_date().value_or_empty()),
|
||||
MTP_bytes(data.vpost_author().value_or_empty()),
|
||||
MTP_long(data.vgrouped_id().value_or_empty()),
|
||||
MTPMessageReactions(),
|
||||
MTPVector<MTPRestrictionReason>(),
|
||||
MTP_int(data.vttl_period().value_or_empty()),
|
||||
MTP_int(shortcutId),
|
||||
MTP_long(data.veffect().value_or_empty()),
|
||||
(data.vfactcheck() ? *data.vfactcheck() : MTPFactCheck()),
|
||||
MTP_int(data.vreport_delivery_until_date().value_or_empty()),
|
||||
MTP_long(data.vpaid_message_stars().value_or_empty()),
|
||||
(data.vsuggested_post()
|
||||
? *data.vsuggested_post()
|
||||
: MTPSuggestedPost()),
|
||||
MTP_int(data.vschedule_repeat_period().value_or_empty()));
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool IsShortcutMsgId(MsgId id) {
|
||||
return (id > ScheduledMaxMsgId) && (id < ShortcutMaxMsgId);
|
||||
}
|
||||
|
||||
ShortcutMessages::ShortcutMessages(not_null<Session*> owner)
|
||||
: _session(&owner->session())
|
||||
, _history(owner->history(_session->userPeerId()))
|
||||
, _clearTimer([=] { clearOldRequests(); }) {
|
||||
owner->itemRemoved(
|
||||
) | rpl::filter([](not_null<const HistoryItem*> item) {
|
||||
return item->isBusinessShortcut();
|
||||
}) | rpl::on_next([=](not_null<const HistoryItem*> item) {
|
||||
remove(item);
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
ShortcutMessages::~ShortcutMessages() {
|
||||
for (const auto &request : _requests) {
|
||||
_session->api().request(request.second.requestId).cancel();
|
||||
}
|
||||
}
|
||||
|
||||
void ShortcutMessages::clearOldRequests() {
|
||||
const auto now = crl::now();
|
||||
while (true) {
|
||||
const auto i = ranges::find_if(_requests, [&](const auto &value) {
|
||||
const auto &request = value.second;
|
||||
return !request.requestId
|
||||
&& (request.lastReceived + kRequestTimeLimit <= now);
|
||||
});
|
||||
if (i == end(_requests)) {
|
||||
break;
|
||||
}
|
||||
_requests.erase(i);
|
||||
}
|
||||
}
|
||||
|
||||
void ShortcutMessages::updateShortcuts(const QVector<MTPQuickReply> &list) {
|
||||
auto shortcuts = parseShortcuts(list);
|
||||
auto changes = std::vector<ShortcutIdChange>();
|
||||
for (auto &[id, shortcut] : _shortcuts.list) {
|
||||
if (shortcuts.list.contains(id)) {
|
||||
continue;
|
||||
}
|
||||
auto foundId = BusinessShortcutId();
|
||||
for (auto &[realId, real] : shortcuts.list) {
|
||||
if (real.name == shortcut.name) {
|
||||
foundId = realId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (foundId) {
|
||||
mergeMessagesFromTo(id, foundId);
|
||||
changes.push_back({ .oldId = id, .newId = foundId });
|
||||
} else {
|
||||
shortcuts.list.emplace(id, shortcut);
|
||||
}
|
||||
}
|
||||
const auto changed = !_shortcutsLoaded
|
||||
|| (shortcuts != _shortcuts);
|
||||
if (changed) {
|
||||
_shortcuts = std::move(shortcuts);
|
||||
_shortcutsLoaded = true;
|
||||
for (const auto &change : changes) {
|
||||
_shortcutIdChanges.fire_copy(change);
|
||||
}
|
||||
_shortcutsChanged.fire({});
|
||||
} else {
|
||||
Assert(changes.empty());
|
||||
}
|
||||
}
|
||||
|
||||
void ShortcutMessages::mergeMessagesFromTo(
|
||||
BusinessShortcutId fromId,
|
||||
BusinessShortcutId toId) {
|
||||
auto &to = _data[toId];
|
||||
const auto i = _data.find(fromId);
|
||||
if (i == end(_data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto &from = i->second;
|
||||
auto destroy = base::flat_set<not_null<HistoryItem*>>();
|
||||
for (auto &item : from.items) {
|
||||
if (item->isSending() || item->hasFailed()) {
|
||||
item->setRealShortcutId(toId);
|
||||
to.items.push_back(std::move(item));
|
||||
} else {
|
||||
destroy.emplace(item.get());
|
||||
}
|
||||
}
|
||||
for (const auto &item : destroy) {
|
||||
item->destroy();
|
||||
}
|
||||
_data.remove(fromId);
|
||||
|
||||
cancelRequest(fromId);
|
||||
|
||||
_updates.fire_copy(toId);
|
||||
if (!destroy.empty()) {
|
||||
cancelRequest(toId);
|
||||
request(toId);
|
||||
}
|
||||
}
|
||||
|
||||
Shortcuts ShortcutMessages::parseShortcuts(
|
||||
const QVector<MTPQuickReply> &list) const {
|
||||
auto result = Shortcuts();
|
||||
for (const auto &reply : list) {
|
||||
const auto shortcut = parseShortcut(reply);
|
||||
result.list.emplace(shortcut.id, shortcut);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Shortcut ShortcutMessages::parseShortcut(const MTPQuickReply &reply) const {
|
||||
const auto &data = reply.data();
|
||||
return Shortcut{
|
||||
.id = BusinessShortcutId(data.vshortcut_id().v),
|
||||
.count = data.vcount().v,
|
||||
.name = qs(data.vshortcut()),
|
||||
.topMessageId = localMessageId(data.vtop_message().v),
|
||||
};
|
||||
}
|
||||
|
||||
MsgId ShortcutMessages::localMessageId(MsgId remoteId) const {
|
||||
return RemoteToLocalMsgId(remoteId);
|
||||
}
|
||||
|
||||
MsgId ShortcutMessages::lookupId(not_null<const HistoryItem*> item) const {
|
||||
Expects(item->isBusinessShortcut());
|
||||
Expects(!item->isSending());
|
||||
Expects(!item->hasFailed());
|
||||
|
||||
return LocalToRemoteMsgId(item->id);
|
||||
}
|
||||
|
||||
int ShortcutMessages::count(BusinessShortcutId shortcutId) const {
|
||||
const auto i = _data.find(shortcutId);
|
||||
return (i != end(_data)) ? i->second.items.size() : 0;
|
||||
}
|
||||
|
||||
void ShortcutMessages::apply(const MTPDupdateQuickReplies &update) {
|
||||
updateShortcuts(update.vquick_replies().v);
|
||||
scheduleShortcutsReload();
|
||||
}
|
||||
|
||||
void ShortcutMessages::scheduleShortcutsReload() {
|
||||
const auto hasUnknownMessages = [&] {
|
||||
const auto selfId = _session->userPeerId();
|
||||
for (const auto &[id, shortcut] : _shortcuts.list) {
|
||||
if (!_session->data().message({ selfId, shortcut.topMessageId })) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
if (hasUnknownMessages()) {
|
||||
_shortcutsLoaded = false;
|
||||
const auto cancelledId = base::take(_shortcutsRequestId);
|
||||
_session->api().request(cancelledId).cancel();
|
||||
crl::on_main(_session, [=] {
|
||||
if (cancelledId || hasUnknownMessages()) {
|
||||
preloadShortcuts();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void ShortcutMessages::apply(const MTPDupdateNewQuickReply &update) {
|
||||
const auto &reply = update.vquick_reply();
|
||||
auto foundId = BusinessShortcutId();
|
||||
const auto shortcut = parseShortcut(reply);
|
||||
for (auto &[id, existing] : _shortcuts.list) {
|
||||
if (id == shortcut.id) {
|
||||
foundId = id;
|
||||
break;
|
||||
} else if (existing.name == shortcut.name) {
|
||||
foundId = id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (foundId == shortcut.id) {
|
||||
auto &already = _shortcuts.list[shortcut.id];
|
||||
if (already != shortcut) {
|
||||
already = shortcut;
|
||||
_shortcutsChanged.fire({});
|
||||
}
|
||||
return;
|
||||
} else if (foundId) {
|
||||
_shortcuts.list.emplace(shortcut.id, shortcut);
|
||||
mergeMessagesFromTo(foundId, shortcut.id);
|
||||
_shortcuts.list.remove(foundId);
|
||||
_shortcutIdChanges.fire({ foundId, shortcut.id });
|
||||
_shortcutsChanged.fire({});
|
||||
}
|
||||
}
|
||||
|
||||
void ShortcutMessages::apply(const MTPDupdateQuickReplyMessage &update) {
|
||||
const auto &message = update.vmessage();
|
||||
const auto shortcutId = BusinessShortcutIdFromMessage(message);
|
||||
if (!shortcutId) {
|
||||
return;
|
||||
}
|
||||
const auto loaded = _data.contains(shortcutId);
|
||||
auto &list = _data[shortcutId];
|
||||
append(shortcutId, list, message);
|
||||
sort(list);
|
||||
_updates.fire_copy(shortcutId);
|
||||
updateCount(shortcutId);
|
||||
if (!loaded) {
|
||||
request(shortcutId);
|
||||
}
|
||||
}
|
||||
|
||||
void ShortcutMessages::updateCount(BusinessShortcutId shortcutId) {
|
||||
const auto i = _data.find(shortcutId);
|
||||
const auto j = _shortcuts.list.find(shortcutId);
|
||||
if (j == end(_shortcuts.list)) {
|
||||
return;
|
||||
}
|
||||
const auto count = (i != end(_data))
|
||||
? int(i->second.itemById.size())
|
||||
: 0;
|
||||
if (j->second.count != count) {
|
||||
_shortcuts.list[shortcutId].count = count;
|
||||
_shortcutsChanged.fire({});
|
||||
}
|
||||
}
|
||||
|
||||
void ShortcutMessages::apply(
|
||||
const MTPDupdateDeleteQuickReplyMessages &update) {
|
||||
const auto shortcutId = update.vshortcut_id().v;
|
||||
if (!shortcutId) {
|
||||
return;
|
||||
}
|
||||
auto i = _data.find(shortcutId);
|
||||
if (i == end(_data)) {
|
||||
return;
|
||||
}
|
||||
for (const auto &id : update.vmessages().v) {
|
||||
const auto &list = i->second;
|
||||
const auto j = list.itemById.find(id.v);
|
||||
if (j != end(list.itemById)) {
|
||||
j->second->destroy();
|
||||
i = _data.find(shortcutId);
|
||||
if (i == end(_data)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
_updates.fire_copy(shortcutId);
|
||||
updateCount(shortcutId);
|
||||
|
||||
cancelRequest(shortcutId);
|
||||
request(shortcutId);
|
||||
}
|
||||
|
||||
void ShortcutMessages::apply(const MTPDupdateDeleteQuickReply &update) {
|
||||
const auto shortcutId = update.vshortcut_id().v;
|
||||
if (!shortcutId) {
|
||||
return;
|
||||
}
|
||||
auto i = _data.find(shortcutId);
|
||||
while (i != end(_data) && !i->second.itemById.empty()) {
|
||||
i->second.itemById.back().second->destroy();
|
||||
i = _data.find(shortcutId);
|
||||
}
|
||||
_updates.fire_copy(shortcutId);
|
||||
if (_data.contains(shortcutId)) {
|
||||
updateCount(shortcutId);
|
||||
} else {
|
||||
_shortcuts.list.remove(shortcutId);
|
||||
_shortcutIdChanges.fire({ shortcutId, 0 });
|
||||
}
|
||||
}
|
||||
|
||||
void ShortcutMessages::apply(
|
||||
const MTPDupdateMessageID &update,
|
||||
not_null<HistoryItem*> local) {
|
||||
const auto id = update.vid().v;
|
||||
const auto i = _data.find(local->shortcutId());
|
||||
Assert(i != end(_data));
|
||||
auto &list = i->second;
|
||||
const auto j = list.itemById.find(id);
|
||||
if (j != end(list.itemById) || !IsServerMsgId(id)) {
|
||||
local->destroy();
|
||||
} else {
|
||||
Assert(!list.itemById.contains(local->id));
|
||||
local->setRealId(localMessageId(id));
|
||||
list.itemById.emplace(id, local);
|
||||
}
|
||||
}
|
||||
|
||||
void ShortcutMessages::appendSending(not_null<HistoryItem*> item) {
|
||||
Expects(item->isSending());
|
||||
Expects(item->isBusinessShortcut());
|
||||
|
||||
const auto shortcutId = item->shortcutId();
|
||||
auto &list = _data[shortcutId];
|
||||
list.items.emplace_back(item);
|
||||
sort(list);
|
||||
_updates.fire_copy(shortcutId);
|
||||
}
|
||||
|
||||
void ShortcutMessages::removeSending(not_null<HistoryItem*> item) {
|
||||
Expects(item->isSending() || item->hasFailed());
|
||||
Expects(item->isBusinessShortcut());
|
||||
|
||||
item->destroy();
|
||||
}
|
||||
|
||||
rpl::producer<> ShortcutMessages::updates(BusinessShortcutId shortcutId) {
|
||||
request(shortcutId);
|
||||
|
||||
return _updates.events(
|
||||
) | rpl::filter([=](BusinessShortcutId value) {
|
||||
return (value == shortcutId);
|
||||
}) | rpl::to_empty;
|
||||
}
|
||||
|
||||
Data::MessagesSlice ShortcutMessages::list(BusinessShortcutId shortcutId) {
|
||||
auto result = Data::MessagesSlice();
|
||||
const auto i = _data.find(shortcutId);
|
||||
if (i == end(_data)) {
|
||||
const auto i = _requests.find(shortcutId);
|
||||
if (i == end(_requests)) {
|
||||
return result;
|
||||
}
|
||||
result.fullCount = result.skippedAfter = result.skippedBefore = 0;
|
||||
return result;
|
||||
}
|
||||
const auto &list = i->second.items;
|
||||
result.skippedAfter = result.skippedBefore = 0;
|
||||
result.fullCount = int(list.size());
|
||||
result.ids = ranges::views::all(
|
||||
list
|
||||
) | ranges::views::transform(
|
||||
&HistoryItem::fullId
|
||||
) | ranges::to_vector;
|
||||
return result;
|
||||
}
|
||||
|
||||
void ShortcutMessages::preloadShortcuts() {
|
||||
if (_shortcutsLoaded || _shortcutsRequestId) {
|
||||
return;
|
||||
}
|
||||
const auto owner = &_session->data();
|
||||
_shortcutsRequestId = owner->session().api().request(
|
||||
MTPmessages_GetQuickReplies(MTP_long(_shortcutsHash))
|
||||
).done([=](const MTPmessages_QuickReplies &result) {
|
||||
result.match([&](const MTPDmessages_quickReplies &data) {
|
||||
owner->processUsers(data.vusers());
|
||||
owner->processChats(data.vchats());
|
||||
updateShortcuts(data.vquick_replies().v);
|
||||
}, [&](const MTPDmessages_quickRepliesNotModified &) {
|
||||
if (!_shortcutsLoaded) {
|
||||
_shortcutsLoaded = true;
|
||||
_shortcutsChanged.fire({});
|
||||
}
|
||||
});
|
||||
}).send();
|
||||
}
|
||||
|
||||
const Shortcuts &ShortcutMessages::shortcuts() const {
|
||||
return _shortcuts;
|
||||
}
|
||||
|
||||
bool ShortcutMessages::shortcutsLoaded() const {
|
||||
return _shortcutsLoaded;
|
||||
}
|
||||
|
||||
rpl::producer<> ShortcutMessages::shortcutsChanged() const {
|
||||
return _shortcutsChanged.events();
|
||||
}
|
||||
|
||||
auto ShortcutMessages::shortcutIdChanged() const
|
||||
-> rpl::producer<ShortcutIdChange> {
|
||||
return _shortcutIdChanges.events();
|
||||
}
|
||||
|
||||
BusinessShortcutId ShortcutMessages::emplaceShortcut(QString name) {
|
||||
Expects(_shortcutsLoaded);
|
||||
|
||||
for (auto &[id, shortcut] : _shortcuts.list) {
|
||||
if (shortcut.name == name) {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
const auto result = --_localShortcutId;
|
||||
_shortcuts.list.emplace(result, Shortcut{ .id = result, .name = name });
|
||||
return result;
|
||||
}
|
||||
|
||||
Shortcut ShortcutMessages::lookupShortcut(BusinessShortcutId id) const {
|
||||
const auto i = _shortcuts.list.find(id);
|
||||
|
||||
Ensures(i != end(_shortcuts.list));
|
||||
return i->second;
|
||||
}
|
||||
|
||||
BusinessShortcutId ShortcutMessages::lookupShortcutId(
|
||||
const QString &name) const {
|
||||
for (const auto &[id, shortcut] : _shortcuts.list) {
|
||||
if (!shortcut.name.compare(name, Qt::CaseInsensitive)) {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void ShortcutMessages::editShortcut(
|
||||
BusinessShortcutId id,
|
||||
QString name,
|
||||
Fn<void()> done,
|
||||
Fn<void(QString)> fail) {
|
||||
name = name.trimmed();
|
||||
if (name.isEmpty()) {
|
||||
fail(QString());
|
||||
return;
|
||||
}
|
||||
const auto finish = [=] {
|
||||
const auto i = _shortcuts.list.find(id);
|
||||
if (i != end(_shortcuts.list)) {
|
||||
i->second.name = name;
|
||||
_shortcutsChanged.fire({});
|
||||
}
|
||||
done();
|
||||
};
|
||||
for (const auto &[existingId, shortcut] : _shortcuts.list) {
|
||||
if (shortcut.name == name) {
|
||||
if (existingId == id) {
|
||||
//done();
|
||||
//return;
|
||||
break;
|
||||
} else if (_data[existingId].items.empty() && !shortcut.count) {
|
||||
removeShortcut(existingId);
|
||||
break;
|
||||
} else {
|
||||
fail(u"SHORTCUT_OCCUPIED"_q);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
_session->api().request(MTPmessages_EditQuickReplyShortcut(
|
||||
MTP_int(id),
|
||||
MTP_string(name)
|
||||
)).done(finish).fail([=](const MTP::Error &error) {
|
||||
const auto type = error.type();
|
||||
if (type == u"SHORTCUT_ID_INVALID"_q) {
|
||||
// Not on the server (yet).
|
||||
finish();
|
||||
} else {
|
||||
fail(type);
|
||||
}
|
||||
}).send();
|
||||
}
|
||||
|
||||
void ShortcutMessages::removeShortcut(BusinessShortcutId shortcutId) {
|
||||
auto i = _data.find(shortcutId);
|
||||
while (i != end(_data)) {
|
||||
if (i->second.items.empty()) {
|
||||
_data.erase(i);
|
||||
} else {
|
||||
i->second.items.front()->destroy();
|
||||
}
|
||||
i = _data.find(shortcutId);
|
||||
}
|
||||
_shortcuts.list.remove(shortcutId);
|
||||
_shortcutIdChanges.fire({ shortcutId, 0 });
|
||||
|
||||
_session->api().request(MTPmessages_DeleteQuickReplyShortcut(
|
||||
MTP_int(shortcutId)
|
||||
)).send();
|
||||
}
|
||||
|
||||
void ShortcutMessages::cancelRequest(BusinessShortcutId shortcutId) {
|
||||
const auto j = _requests.find(shortcutId);
|
||||
if (j != end(_requests)) {
|
||||
_session->api().request(j->second.requestId).cancel();
|
||||
_requests.erase(j);
|
||||
}
|
||||
}
|
||||
|
||||
void ShortcutMessages::request(BusinessShortcutId shortcutId) {
|
||||
auto &request = _requests[shortcutId];
|
||||
if (request.requestId || TooEarlyForRequest(request.lastReceived)) {
|
||||
return;
|
||||
}
|
||||
const auto i = _data.find(shortcutId);
|
||||
const auto hash = (i != end(_data))
|
||||
? countListHash(i->second)
|
||||
: uint64(0);
|
||||
request.requestId = _session->api().request(
|
||||
MTPmessages_GetQuickReplyMessages(
|
||||
MTP_flags(0),
|
||||
MTP_int(shortcutId),
|
||||
MTPVector<MTPint>(),
|
||||
MTP_long(hash))
|
||||
).done([=](const MTPmessages_Messages &result) {
|
||||
parse(shortcutId, result);
|
||||
}).fail([=] {
|
||||
_requests.remove(shortcutId);
|
||||
}).send();
|
||||
}
|
||||
|
||||
void ShortcutMessages::parse(
|
||||
BusinessShortcutId shortcutId,
|
||||
const MTPmessages_Messages &list) {
|
||||
auto &request = _requests[shortcutId];
|
||||
request.lastReceived = crl::now();
|
||||
request.requestId = 0;
|
||||
if (!_clearTimer.isActive()) {
|
||||
_clearTimer.callOnce(kRequestTimeLimit * 2);
|
||||
}
|
||||
|
||||
list.match([&](const MTPDmessages_messagesNotModified &data) {
|
||||
}, [&](const auto &data) {
|
||||
_session->data().processUsers(data.vusers());
|
||||
_session->data().processChats(data.vchats());
|
||||
|
||||
const auto &messages = data.vmessages().v;
|
||||
if (messages.isEmpty()) {
|
||||
clearNotSending(shortcutId);
|
||||
return;
|
||||
}
|
||||
auto received = base::flat_set<not_null<HistoryItem*>>();
|
||||
auto clear = base::flat_set<not_null<HistoryItem*>>();
|
||||
auto &list = _data.emplace(shortcutId, List()).first->second;
|
||||
for (const auto &message : messages) {
|
||||
if (const auto item = append(shortcutId, list, message)) {
|
||||
received.emplace(item);
|
||||
}
|
||||
}
|
||||
for (const auto &owned : list.items) {
|
||||
const auto item = owned.get();
|
||||
if (!item->isSending() && !received.contains(item)) {
|
||||
clear.emplace(item);
|
||||
}
|
||||
}
|
||||
updated(shortcutId, received, clear);
|
||||
});
|
||||
}
|
||||
|
||||
HistoryItem *ShortcutMessages::append(
|
||||
BusinessShortcutId shortcutId,
|
||||
List &list,
|
||||
const MTPMessage &message) {
|
||||
const auto id = message.match([&](const auto &data) {
|
||||
return data.vid().v;
|
||||
});
|
||||
const auto i = list.itemById.find(id);
|
||||
if (i != end(list.itemById)) {
|
||||
const auto existing = i->second;
|
||||
message.match([&](const MTPDmessage &data) {
|
||||
if (data.is_edit_hide()) {
|
||||
existing->applyEdition(HistoryMessageEdition(_session, data));
|
||||
} else {
|
||||
existing->updateSentContent({
|
||||
qs(data.vmessage()),
|
||||
Api::EntitiesFromMTP(
|
||||
_session,
|
||||
data.ventities().value_or_empty())
|
||||
}, data.vmedia());
|
||||
existing->updateReplyMarkup(
|
||||
HistoryMessageMarkupData(data.vreply_markup()));
|
||||
existing->updateForwardedInfo(data.vfwd_from());
|
||||
}
|
||||
existing->updateDate(data.vdate().v);
|
||||
_history->owner().requestItemTextRefresh(existing);
|
||||
}, [&](const auto &data) {});
|
||||
return existing;
|
||||
}
|
||||
|
||||
if (!IsServerMsgId(id)) {
|
||||
LOG(("API Error: Bad id in quick reply messages: %1.").arg(id));
|
||||
return nullptr;
|
||||
}
|
||||
const auto item = _session->data().addNewMessage(
|
||||
localMessageId(id),
|
||||
PrepareMessage(shortcutId, message),
|
||||
MessageFlags(), // localFlags
|
||||
NewMessageType::Existing);
|
||||
if (!item
|
||||
|| item->history() != _history
|
||||
|| item->shortcutId() != shortcutId) {
|
||||
LOG(("API Error: Bad data received in quick reply messages."));
|
||||
return nullptr;
|
||||
}
|
||||
list.items.emplace_back(item);
|
||||
list.itemById.emplace(id, item);
|
||||
return item;
|
||||
}
|
||||
|
||||
void ShortcutMessages::clearNotSending(BusinessShortcutId shortcutId) {
|
||||
const auto i = _data.find(shortcutId);
|
||||
if (i == end(_data)) {
|
||||
return;
|
||||
}
|
||||
auto clear = base::flat_set<not_null<HistoryItem*>>();
|
||||
for (const auto &owned : i->second.items) {
|
||||
if (!owned->isSending() && !owned->hasFailed()) {
|
||||
clear.emplace(owned.get());
|
||||
}
|
||||
}
|
||||
updated(shortcutId, {}, clear);
|
||||
}
|
||||
|
||||
void ShortcutMessages::updated(
|
||||
BusinessShortcutId shortcutId,
|
||||
const base::flat_set<not_null<HistoryItem*>> &added,
|
||||
const base::flat_set<not_null<HistoryItem*>> &clear) {
|
||||
if (!clear.empty()) {
|
||||
for (const auto &item : clear) {
|
||||
item->destroy();
|
||||
}
|
||||
}
|
||||
const auto i = _data.find(shortcutId);
|
||||
if (i != end(_data)) {
|
||||
sort(i->second);
|
||||
}
|
||||
if (!added.empty() || !clear.empty()) {
|
||||
_updates.fire_copy(shortcutId);
|
||||
}
|
||||
}
|
||||
|
||||
void ShortcutMessages::sort(List &list) {
|
||||
ranges::sort(list.items, ranges::less(), &HistoryItem::position);
|
||||
}
|
||||
|
||||
void ShortcutMessages::remove(not_null<const HistoryItem*> item) {
|
||||
const auto shortcutId = item->shortcutId();
|
||||
const auto i = _data.find(shortcutId);
|
||||
Assert(i != end(_data));
|
||||
auto &list = i->second;
|
||||
|
||||
if (!item->isSending() && !item->hasFailed()) {
|
||||
list.itemById.remove(lookupId(item));
|
||||
}
|
||||
const auto k = ranges::find(list.items, item, &OwnedItem::get);
|
||||
Assert(k != list.items.end());
|
||||
k->release();
|
||||
list.items.erase(k);
|
||||
|
||||
if (list.items.empty()) {
|
||||
_data.erase(i);
|
||||
}
|
||||
_updates.fire_copy(shortcutId);
|
||||
updateCount(shortcutId);
|
||||
}
|
||||
|
||||
uint64 ShortcutMessages::countListHash(const List &list) const {
|
||||
using namespace Api;
|
||||
|
||||
auto hash = HashInit();
|
||||
auto &&serverside = ranges::views::all(
|
||||
list.items
|
||||
) | ranges::views::filter([](const OwnedItem &item) {
|
||||
return !item->isSending() && !item->hasFailed();
|
||||
}) | ranges::views::reverse;
|
||||
for (const auto &item : serverside) {
|
||||
HashUpdate(hash, lookupId(item.get()).bare);
|
||||
if (const auto edited = item->Get<HistoryMessageEdited>()) {
|
||||
HashUpdate(hash, edited->date);
|
||||
} else {
|
||||
HashUpdate(hash, TimeId(0));
|
||||
}
|
||||
}
|
||||
return HashFinalize(hash);
|
||||
}
|
||||
|
||||
MTPInputQuickReplyShortcut ShortcutIdToMTP(
|
||||
not_null<Main::Session*> session,
|
||||
BusinessShortcutId id) {
|
||||
return id
|
||||
? MTP_inputQuickReplyShortcut(MTP_string(
|
||||
session->data().shortcutMessages().lookupShortcut(id).name))
|
||||
: MTPInputQuickReplyShortcut();
|
||||
}
|
||||
|
||||
} // namespace Data
|
||||
154
Telegram/SourceFiles/data/business/data_shortcut_messages.h
Normal file
154
Telegram/SourceFiles/data/business/data_shortcut_messages.h
Normal file
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "history/history_item.h"
|
||||
#include "base/timer.h"
|
||||
|
||||
class History;
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace Data {
|
||||
|
||||
class Session;
|
||||
struct MessagesSlice;
|
||||
|
||||
struct Shortcut {
|
||||
BusinessShortcutId id = 0;
|
||||
int count = 0;
|
||||
QString name;
|
||||
MsgId topMessageId = 0;
|
||||
|
||||
friend inline bool operator==(
|
||||
const Shortcut &a,
|
||||
const Shortcut &b) = default;
|
||||
};
|
||||
|
||||
struct ShortcutIdChange {
|
||||
BusinessShortcutId oldId = 0;
|
||||
BusinessShortcutId newId = 0;
|
||||
};
|
||||
|
||||
struct Shortcuts {
|
||||
base::flat_map<BusinessShortcutId, Shortcut> list;
|
||||
|
||||
friend inline bool operator==(
|
||||
const Shortcuts &a,
|
||||
const Shortcuts &b) = default;
|
||||
};
|
||||
|
||||
[[nodiscard]] bool IsShortcutMsgId(MsgId id);
|
||||
|
||||
class ShortcutMessages final {
|
||||
public:
|
||||
explicit ShortcutMessages(not_null<Session*> owner);
|
||||
~ShortcutMessages();
|
||||
|
||||
[[nodiscard]] MsgId lookupId(not_null<const HistoryItem*> item) const;
|
||||
[[nodiscard]] int count(BusinessShortcutId shortcutId) const;
|
||||
[[nodiscard]] MsgId localMessageId(MsgId remoteId) const;
|
||||
|
||||
void apply(const MTPDupdateQuickReplies &update);
|
||||
void apply(const MTPDupdateNewQuickReply &update);
|
||||
void apply(const MTPDupdateQuickReplyMessage &update);
|
||||
void apply(const MTPDupdateDeleteQuickReplyMessages &update);
|
||||
void apply(const MTPDupdateDeleteQuickReply &update);
|
||||
void apply(
|
||||
const MTPDupdateMessageID &update,
|
||||
not_null<HistoryItem*> local);
|
||||
|
||||
void appendSending(not_null<HistoryItem*> item);
|
||||
void removeSending(not_null<HistoryItem*> item);
|
||||
|
||||
[[nodiscard]] rpl::producer<> updates(BusinessShortcutId shortcutId);
|
||||
[[nodiscard]] Data::MessagesSlice list(BusinessShortcutId shortcutId);
|
||||
|
||||
void preloadShortcuts();
|
||||
[[nodiscard]] const Shortcuts &shortcuts() const;
|
||||
[[nodiscard]] bool shortcutsLoaded() const;
|
||||
[[nodiscard]] rpl::producer<> shortcutsChanged() const;
|
||||
[[nodiscard]] rpl::producer<ShortcutIdChange> shortcutIdChanged() const;
|
||||
[[nodiscard]] BusinessShortcutId emplaceShortcut(QString name);
|
||||
[[nodiscard]] Shortcut lookupShortcut(BusinessShortcutId id) const;
|
||||
[[nodiscard]] BusinessShortcutId lookupShortcutId(
|
||||
const QString &name) const;
|
||||
void editShortcut(
|
||||
BusinessShortcutId id,
|
||||
QString name,
|
||||
Fn<void()> done,
|
||||
Fn<void(QString)> fail);
|
||||
void removeShortcut(BusinessShortcutId shortcutId);
|
||||
|
||||
private:
|
||||
using OwnedItem = std::unique_ptr<HistoryItem, HistoryItem::Destroyer>;
|
||||
struct List {
|
||||
std::vector<OwnedItem> items;
|
||||
base::flat_map<MsgId, not_null<HistoryItem*>> itemById;
|
||||
};
|
||||
struct Request {
|
||||
mtpRequestId requestId = 0;
|
||||
crl::time lastReceived = 0;
|
||||
};
|
||||
|
||||
void request(BusinessShortcutId shortcutId);
|
||||
void parse(
|
||||
BusinessShortcutId shortcutId,
|
||||
const MTPmessages_Messages &list);
|
||||
HistoryItem *append(
|
||||
BusinessShortcutId shortcutId,
|
||||
List &list,
|
||||
const MTPMessage &message);
|
||||
void clearNotSending(BusinessShortcutId shortcutId);
|
||||
void updated(
|
||||
BusinessShortcutId shortcutId,
|
||||
const base::flat_set<not_null<HistoryItem*>> &added,
|
||||
const base::flat_set<not_null<HistoryItem*>> &clear);
|
||||
void sort(List &list);
|
||||
void remove(not_null<const HistoryItem*> item);
|
||||
[[nodiscard]] uint64 countListHash(const List &list) const;
|
||||
void clearOldRequests();
|
||||
void cancelRequest(BusinessShortcutId shortcutId);
|
||||
void updateCount(BusinessShortcutId shortcutId);
|
||||
|
||||
void scheduleShortcutsReload();
|
||||
void mergeMessagesFromTo(
|
||||
BusinessShortcutId fromId,
|
||||
BusinessShortcutId toId);
|
||||
void updateShortcuts(const QVector<MTPQuickReply> &list);
|
||||
[[nodiscard]] Shortcut parseShortcut(const MTPQuickReply &reply) const;
|
||||
[[nodiscard]] Shortcuts parseShortcuts(
|
||||
const QVector<MTPQuickReply> &list) const;
|
||||
|
||||
const not_null<Main::Session*> _session;
|
||||
const not_null<History*> _history;
|
||||
|
||||
base::Timer _clearTimer;
|
||||
base::flat_map<BusinessShortcutId, List> _data;
|
||||
base::flat_map<BusinessShortcutId, Request> _requests;
|
||||
rpl::event_stream<BusinessShortcutId> _updates;
|
||||
|
||||
Shortcuts _shortcuts;
|
||||
rpl::event_stream<> _shortcutsChanged;
|
||||
rpl::event_stream<ShortcutIdChange> _shortcutIdChanges;
|
||||
BusinessShortcutId _localShortcutId = 0;
|
||||
uint64 _shortcutsHash = 0;
|
||||
mtpRequestId _shortcutsRequestId = 0;
|
||||
bool _shortcutsLoaded = false;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] MTPInputQuickReplyShortcut ShortcutIdToMTP(
|
||||
not_null<Main::Session*> session,
|
||||
BusinessShortcutId id);
|
||||
|
||||
} // namespace Data
|
||||
Reference in New Issue
Block a user