mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-04-18 07:07:08 +02:00
Initial chat links edition implementation.
This commit is contained in:
parent
6fe0c60204
commit
3d54f8ec49
23 changed files with 1250 additions and 53 deletions
Telegram
CMakeLists.txt
Resources
animations
icons/settings/premium
langs
qrc/telegram
SourceFiles
|
@ -110,6 +110,8 @@ PRIVATE
|
|||
api/api_chat_filters.h
|
||||
api/api_chat_invite.cpp
|
||||
api/api_chat_invite.h
|
||||
api/api_chat_links.cpp
|
||||
api/api_chat_links.h
|
||||
api/api_chat_participants.cpp
|
||||
api/api_chat_participants.h
|
||||
api/api_cloud_password.cpp
|
||||
|
@ -1297,6 +1299,8 @@ PRIVATE
|
|||
settings/business/settings_shortcut_messages.h
|
||||
settings/business/settings_chat_intro.cpp
|
||||
settings/business/settings_chat_intro.h
|
||||
settings/business/settings_chat_links.cpp
|
||||
settings/business/settings_chat_links.h
|
||||
settings/business/settings_chatbots.cpp
|
||||
settings/business/settings_chatbots.h
|
||||
settings/business/settings_greeting.cpp
|
||||
|
|
BIN
Telegram/Resources/animations/chat_link.tgs
Normal file
BIN
Telegram/Resources/animations/chat_link.tgs
Normal file
Binary file not shown.
BIN
Telegram/Resources/icons/settings/premium/links.png
Normal file
BIN
Telegram/Resources/icons/settings/premium/links.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 610 B |
BIN
Telegram/Resources/icons/settings/premium/links@2x.png
Normal file
BIN
Telegram/Resources/icons/settings/premium/links@2x.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 1.2 KiB |
BIN
Telegram/Resources/icons/settings/premium/links@3x.png
Normal file
BIN
Telegram/Resources/icons/settings/premium/links@3x.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 2.1 KiB |
|
@ -2211,8 +2211,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
"lng_business_about_away_messages" = "Define messages that are automatically sent when you are off.";
|
||||
"lng_business_subtitle_chatbots" = "Chatbots";
|
||||
"lng_business_about_chatbots" = "Add any third party chatbots that will process customer interactions.";
|
||||
"lng_business_subtitle_chat_intro" = "Intro";
|
||||
"lng_business_subtitle_chat_intro" = "Custom Intro";
|
||||
"lng_business_about_chat_intro" = "Customize the message people see before they start a chat with you.";
|
||||
"lng_business_subtitle_chat_links" = "Links to Chat";
|
||||
"lng_business_about_chat_links" = "Create links that start a chat with you, suggesting the first message.";
|
||||
|
||||
"lng_location_title" = "Location";
|
||||
"lng_location_about" = "Display the location of your business on your account.";
|
||||
|
@ -2325,7 +2327,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
"lng_chatbot_menu_remove" = "Remove bot from this chat";
|
||||
"lng_chatbot_menu_revoke" = "Revoke access to this chat";
|
||||
|
||||
"lng_chat_intro_title" = "Intro";
|
||||
"lng_chat_intro_title" = "Custom Intro";
|
||||
"lng_chat_intro_subtitle" = "Customize your intro";
|
||||
"lng_chat_intro_default_title" = "No messages here yet...";
|
||||
"lng_chat_intro_default_message" = "Send a message or click on the greeting below";
|
||||
|
@ -2336,6 +2338,30 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
"lng_chat_intro_about" = "You can customize the message people see before they start a chat with you.";
|
||||
"lng_chat_intro_reset" = "Reset to Default";
|
||||
|
||||
"lng_chat_links_title" = "Links to Chat";
|
||||
"lng_chat_links_about" = "Give your customers short links that start a chat with you – and suggest the first message from them to you.";
|
||||
"lng_chat_links_create_link" = "Create a Link to Chat";
|
||||
"lng_chat_links_footer" = "You can also use a simple link for a chat with you – {links}";
|
||||
"lng_chat_links_footer_both" = "{username} or {link}";
|
||||
"lng_chat_links_no_clicks" = "no clicks";
|
||||
"lng_chat_links_clicks#one" = "{count} click";
|
||||
"lng_chat_links_clicks#other" = "{count} clicks";
|
||||
"lng_chat_link_new_title" = "New Link";
|
||||
"lng_chat_link_edit_title" = "Edit Link";
|
||||
"lng_chat_link_description" = "Add a message that will be entered in the message field for anyone who starts a chat with you using this link.";
|
||||
"lng_chat_link_placeholder" = "Add Preset Message";
|
||||
"lng_chat_link_saved" = "Chat link saved.";
|
||||
"lng_chat_link_copy" = "Copy";
|
||||
"lng_chat_link_share" = "Share";
|
||||
"lng_chat_link_rename" = "Rename";
|
||||
"lng_chat_link_delete" = "Delete";
|
||||
"lng_chat_link_name" = "Link Name (optional)";
|
||||
"lng_chat_link_name_about" = "Add a name for this link that only you will see.";
|
||||
"lng_chat_link_delete_sure" = "Are you sure you want to delete this chat link?";
|
||||
"lng_chat_link_qr_title" = "Chat Link QR Code";
|
||||
"lng_chat_link_qr_about" = "Everyone on Telegram can scan this code to contact you.";
|
||||
"lng_chat_link_copied" = "Chat link copied to clipboard.";
|
||||
|
||||
"lng_boost_channel_button" = "Boost Channel";
|
||||
"lng_boost_group_button" = "Boost Group";
|
||||
"lng_boost_again_button" = "Boost Again";
|
||||
|
|
|
@ -21,5 +21,6 @@
|
|||
<file alias="writing.tgs">../../animations/writing.tgs</file>
|
||||
<file alias="hours.tgs">../../animations/hours.tgs</file>
|
||||
<file alias="phone.tgs">../../animations/phone.tgs</file>
|
||||
<file alias="chat_link.tgs">../../animations/chat_link.tgs</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
|
171
Telegram/SourceFiles/api/api_chat_links.cpp
Normal file
171
Telegram/SourceFiles/api/api_chat_links.cpp
Normal file
|
@ -0,0 +1,171 @@
|
|||
/*
|
||||
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 "api/api_chat_links.h"
|
||||
|
||||
#include "api/api_text_entities.h"
|
||||
#include "apiwrap.h"
|
||||
#include "data/data_session.h"
|
||||
#include "main/main_session.h"
|
||||
|
||||
namespace Api {
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] ChatLink FromMTP(
|
||||
not_null<Main::Session*> session,
|
||||
const MTPBusinessChatLink &link) {
|
||||
const auto &data = link.data();
|
||||
return {
|
||||
.link = qs(data.vlink()),
|
||||
.title = qs(data.vtitle().value_or_empty()),
|
||||
.message = {
|
||||
qs(data.vmessage()),
|
||||
EntitiesFromMTP(
|
||||
session,
|
||||
data.ventities().value_or_empty())
|
||||
},
|
||||
.clicks = data.vviews().v,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] MTPInputBusinessChatLink ToMTP(
|
||||
not_null<Main::Session*> session,
|
||||
const QString &title,
|
||||
const TextWithEntities &message) {
|
||||
auto entities = EntitiesToMTP(
|
||||
session,
|
||||
message.entities,
|
||||
ConvertOption::SkipLocal);
|
||||
using Flag = MTPDinputBusinessChatLink::Flag;
|
||||
const auto flags = (title.isEmpty() ? Flag() : Flag::f_title)
|
||||
| (entities.v.isEmpty() ? Flag() : Flag::f_entities);
|
||||
return MTP_inputBusinessChatLink(
|
||||
MTP_flags(flags),
|
||||
MTP_string(message.text),
|
||||
std::move(entities),
|
||||
MTP_string(title));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ChatLinks::ChatLinks(not_null<ApiWrap*> api) : _api(api) {
|
||||
}
|
||||
|
||||
|
||||
void ChatLinks::create(
|
||||
const QString &title,
|
||||
const TextWithEntities &message,
|
||||
Fn<void(Link)> done) {
|
||||
const auto session = &_api->session();
|
||||
_api->request(MTPaccount_CreateBusinessChatLink(
|
||||
ToMTP(session, title, message)
|
||||
)).done([=](const MTPBusinessChatLink &result) {
|
||||
const auto link = FromMTP(session, result);
|
||||
_list.push_back(link);
|
||||
_updates.fire({ .was = QString(), .now = link });
|
||||
if (done) done(link);
|
||||
}).fail([=](const MTP::Error &error) {
|
||||
const auto type = error.type();
|
||||
if (done) done(Link());
|
||||
}).send();
|
||||
}
|
||||
|
||||
void ChatLinks::edit(
|
||||
const QString &link,
|
||||
const QString &title,
|
||||
const TextWithEntities &message,
|
||||
Fn<void(Link)> done) {
|
||||
const auto session = &_api->session();
|
||||
_api->request(MTPaccount_EditBusinessChatLink(
|
||||
MTP_string(link),
|
||||
ToMTP(session, title, message)
|
||||
)).done([=](const MTPBusinessChatLink &result) {
|
||||
const auto parsed = FromMTP(session, result);
|
||||
if (parsed.link != link) {
|
||||
LOG(("API Error: EditBusinessChatLink changed the link."));
|
||||
if (done) done(Link());
|
||||
return;
|
||||
}
|
||||
const auto i = ranges::find(_list, link, &Link::link);
|
||||
if (i != end(_list)) {
|
||||
*i = parsed;
|
||||
_updates.fire({ .was = link, .now = parsed });
|
||||
if (done) done(parsed);
|
||||
} else {
|
||||
LOG(("API Error: EditBusinessChatLink link not found."));
|
||||
if (done) done(Link());
|
||||
}
|
||||
}).fail([=](const MTP::Error &error) {
|
||||
const auto type = error.type();
|
||||
if (done) done(Link());
|
||||
}).send();
|
||||
}
|
||||
|
||||
void ChatLinks::destroy(
|
||||
const QString &link,
|
||||
Fn<void()> done) {
|
||||
_api->request(MTPaccount_DeleteBusinessChatLink(
|
||||
MTP_string(link)
|
||||
)).done([=] {
|
||||
const auto i = ranges::find(_list, link, &Link::link);
|
||||
if (i != end(_list)) {
|
||||
_list.erase(i);
|
||||
_updates.fire({ .was = link });
|
||||
if (done) done();
|
||||
} else {
|
||||
LOG(("API Error: DeleteBusinessChatLink link not found."));
|
||||
if (done) done();
|
||||
}
|
||||
}).fail([=](const MTP::Error &error) {
|
||||
const auto type = error.type();
|
||||
if (done) done();
|
||||
}).send();
|
||||
}
|
||||
|
||||
void ChatLinks::preload() {
|
||||
if (_loaded || _requestId) {
|
||||
return;
|
||||
}
|
||||
_requestId = _api->request(MTPaccount_GetBusinessChatLinks(
|
||||
)).done([=](const MTPaccount_BusinessChatLinks &result) {
|
||||
const auto &data = result.data();
|
||||
const auto session = &_api->session();
|
||||
const auto owner = &session->data();
|
||||
owner->processUsers(data.vusers());
|
||||
owner->processChats(data.vchats());
|
||||
auto links = std::vector<Link>();
|
||||
links.reserve(data.vlinks().v.size());
|
||||
for (const auto &link : data.vlinks().v) {
|
||||
links.push_back(FromMTP(session, link));
|
||||
}
|
||||
_list = std::move(links);
|
||||
_loaded = true;
|
||||
_loadedUpdates.fire({});
|
||||
}).fail([=] {
|
||||
_requestId = 0;
|
||||
_loaded = true;
|
||||
_loadedUpdates.fire({});
|
||||
}).send();
|
||||
}
|
||||
|
||||
const std::vector<ChatLink> &ChatLinks::list() const {
|
||||
return _list;
|
||||
}
|
||||
|
||||
bool ChatLinks::loaded() const {
|
||||
return _loaded;
|
||||
}
|
||||
|
||||
rpl::producer<> ChatLinks::loadedUpdates() const {
|
||||
return _loadedUpdates.events();
|
||||
}
|
||||
|
||||
rpl::producer<ChatLinks::Update> ChatLinks::updates() const {
|
||||
return _updates.events();
|
||||
}
|
||||
|
||||
} // namespace Api
|
64
Telegram/SourceFiles/api/api_chat_links.h
Normal file
64
Telegram/SourceFiles/api/api_chat_links.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
|
||||
|
||||
class ApiWrap;
|
||||
|
||||
namespace Api {
|
||||
|
||||
struct ChatLink {
|
||||
QString link;
|
||||
QString title;
|
||||
TextWithEntities message;
|
||||
int clicks = 0;
|
||||
};
|
||||
|
||||
struct ChatLinkUpdate {
|
||||
QString was;
|
||||
std::optional<ChatLink> now;
|
||||
};
|
||||
|
||||
class ChatLinks final {
|
||||
public:
|
||||
explicit ChatLinks(not_null<ApiWrap*> api);
|
||||
|
||||
using Link = ChatLink;
|
||||
using Update = ChatLinkUpdate;
|
||||
|
||||
void create(
|
||||
const QString &title,
|
||||
const TextWithEntities &message,
|
||||
Fn<void(Link)> done = nullptr);
|
||||
void edit(
|
||||
const QString &link,
|
||||
const QString &title,
|
||||
const TextWithEntities &message,
|
||||
Fn<void(Link)> done = nullptr);
|
||||
void destroy(
|
||||
const QString &link,
|
||||
Fn<void()> done = nullptr);
|
||||
|
||||
void preload();
|
||||
[[nodiscard]] const std::vector<ChatLink> &list() const;
|
||||
[[nodiscard]] bool loaded() const;
|
||||
[[nodiscard]] rpl::producer<> loadedUpdates() const;
|
||||
[[nodiscard]] rpl::producer<Update> updates() const;
|
||||
|
||||
private:
|
||||
const not_null<ApiWrap*> _api;
|
||||
|
||||
std::vector<Link> _list;
|
||||
rpl::event_stream<> _loadedUpdates;
|
||||
mtpRequestId _requestId = 0;
|
||||
bool _loaded = false;
|
||||
|
||||
rpl::event_stream<Update> _updates;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Api
|
|
@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "api/api_authorizations.h"
|
||||
#include "api/api_attached_stickers.h"
|
||||
#include "api/api_blocked_peers.h"
|
||||
#include "api/api_chat_links.h"
|
||||
#include "api/api_chat_participants.h"
|
||||
#include "api/api_cloud_password.h"
|
||||
#include "api/api_hash.h"
|
||||
|
@ -163,6 +164,7 @@ ApiWrap::ApiWrap(not_null<Main::Session*> session)
|
|||
, _globalPrivacy(std::make_unique<Api::GlobalPrivacy>(this))
|
||||
, _userPrivacy(std::make_unique<Api::UserPrivacy>(this))
|
||||
, _inviteLinks(std::make_unique<Api::InviteLinks>(this))
|
||||
, _chatLinks(std::make_unique<Api::ChatLinks>(this))
|
||||
, _views(std::make_unique<Api::ViewsManager>(this))
|
||||
, _confirmPhone(std::make_unique<Api::ConfirmPhone>(this))
|
||||
, _peerPhoto(std::make_unique<Api::PeerPhoto>(this))
|
||||
|
@ -4424,6 +4426,10 @@ Api::InviteLinks &ApiWrap::inviteLinks() {
|
|||
return *_inviteLinks;
|
||||
}
|
||||
|
||||
Api::ChatLinks &ApiWrap::chatLinks() {
|
||||
return *_chatLinks;
|
||||
}
|
||||
|
||||
Api::ViewsManager &ApiWrap::views() {
|
||||
return *_views;
|
||||
}
|
||||
|
|
|
@ -69,6 +69,7 @@ class SensitiveContent;
|
|||
class GlobalPrivacy;
|
||||
class UserPrivacy;
|
||||
class InviteLinks;
|
||||
class ChatLinks;
|
||||
class ViewsManager;
|
||||
class ConfirmPhone;
|
||||
class PeerPhoto;
|
||||
|
@ -384,6 +385,7 @@ public:
|
|||
[[nodiscard]] Api::GlobalPrivacy &globalPrivacy();
|
||||
[[nodiscard]] Api::UserPrivacy &userPrivacy();
|
||||
[[nodiscard]] Api::InviteLinks &inviteLinks();
|
||||
[[nodiscard]] Api::ChatLinks &chatLinks();
|
||||
[[nodiscard]] Api::ViewsManager &views();
|
||||
[[nodiscard]] Api::ConfirmPhone &confirmPhone();
|
||||
[[nodiscard]] Api::PeerPhoto &peerPhoto();
|
||||
|
@ -703,6 +705,7 @@ private:
|
|||
const std::unique_ptr<Api::GlobalPrivacy> _globalPrivacy;
|
||||
const std::unique_ptr<Api::UserPrivacy> _userPrivacy;
|
||||
const std::unique_ptr<Api::InviteLinks> _inviteLinks;
|
||||
const std::unique_ptr<Api::ChatLinks> _chatLinks;
|
||||
const std::unique_ptr<Api::ViewsManager> _views;
|
||||
const std::unique_ptr<Api::ConfirmPhone> _confirmPhone;
|
||||
const std::unique_ptr<Api::PeerPhoto> _peerPhoto;
|
||||
|
|
|
@ -580,8 +580,10 @@ void LinkController::addLinkBlock(not_null<Ui::VerticalLayout*> container) {
|
|||
ShareInviteLinkBox(&_window->session(), link));
|
||||
});
|
||||
const auto getLinkQr = crl::guard(weak, [=] {
|
||||
delegate()->peerListUiShow()->showBox(
|
||||
InviteLinkQrBox(link, tr::lng_filters_link_qr_about()));
|
||||
delegate()->peerListUiShow()->showBox(InviteLinkQrBox(
|
||||
link,
|
||||
tr::lng_group_invite_qr_title(),
|
||||
tr::lng_filters_link_qr_about()));
|
||||
});
|
||||
const auto editLink = crl::guard(weak, [=] {
|
||||
delegate()->peerListUiShow()->showBox(
|
||||
|
@ -886,8 +888,10 @@ base::unique_qptr<Ui::PopupMenu> LinksController::createRowContextMenu(
|
|||
ShareInviteLinkBox(&_window->session(), link));
|
||||
};
|
||||
const auto getLinkQr = [=] {
|
||||
delegate()->peerListUiShow()->showBox(
|
||||
InviteLinkQrBox(link, tr::lng_filters_link_qr_about()));
|
||||
delegate()->peerListUiShow()->showBox(InviteLinkQrBox(
|
||||
link,
|
||||
tr::lng_group_invite_qr_title(),
|
||||
tr::lng_filters_link_qr_about()));
|
||||
};
|
||||
const auto editLink = [=] {
|
||||
delegate()->peerListUiShow()->showBox(
|
||||
|
|
|
@ -272,9 +272,10 @@ QImage QrForShare(const QString &text) {
|
|||
void QrBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
const QString &link,
|
||||
rpl::producer<QString> title,
|
||||
rpl::producer<QString> about,
|
||||
Fn<void(QImage, std::shared_ptr<Ui::Show>)> share) {
|
||||
box->setTitle(tr::lng_group_invite_qr_title());
|
||||
box->setTitle(std::move(title));
|
||||
|
||||
box->addButton(tr::lng_about_done(), [=] { box->closeBox(); });
|
||||
|
||||
|
@ -350,8 +351,10 @@ void Controller::addHeaderBlock(not_null<Ui::VerticalLayout*> container) {
|
|||
delegate()->peerListUiShow()->showBox(ShareInviteLinkBox(peer, link));
|
||||
});
|
||||
const auto getLinkQr = crl::guard(weak, [=] {
|
||||
delegate()->peerListUiShow()->showBox(
|
||||
InviteLinkQrBox(link, tr::lng_group_invite_qr_about()));
|
||||
delegate()->peerListUiShow()->showBox(InviteLinkQrBox(
|
||||
link,
|
||||
tr::lng_group_invite_qr_title(),
|
||||
tr::lng_group_invite_qr_about()));
|
||||
});
|
||||
const auto revokeLink = crl::guard(weak, [=] {
|
||||
delegate()->peerListUiShow()->showBox(
|
||||
|
@ -976,6 +979,7 @@ void AddPermanentLinkBlock(
|
|||
if (const auto current = value->current(); !current.link.isEmpty()) {
|
||||
show->showBox(InviteLinkQrBox(
|
||||
current.link,
|
||||
tr::lng_group_invite_qr_title(),
|
||||
tr::lng_group_invite_qr_about()));
|
||||
}
|
||||
});
|
||||
|
@ -1130,13 +1134,15 @@ void CopyInviteLink(std::shared_ptr<Ui::Show> show, const QString &link) {
|
|||
|
||||
object_ptr<Ui::BoxContent> ShareInviteLinkBox(
|
||||
not_null<PeerData*> peer,
|
||||
const QString &link) {
|
||||
return ShareInviteLinkBox(&peer->session(), link);
|
||||
const QString &link,
|
||||
const QString &copied) {
|
||||
return ShareInviteLinkBox(&peer->session(), link, copied);
|
||||
}
|
||||
|
||||
object_ptr<Ui::BoxContent> ShareInviteLinkBox(
|
||||
not_null<Main::Session*> session,
|
||||
const QString &link) {
|
||||
const QString &link,
|
||||
const QString &copied) {
|
||||
const auto sending = std::make_shared<bool>();
|
||||
const auto box = std::make_shared<QPointer<ShareBox>>();
|
||||
|
||||
|
@ -1148,7 +1154,9 @@ object_ptr<Ui::BoxContent> ShareInviteLinkBox(
|
|||
|
||||
auto copyCallback = [=] {
|
||||
QGuiApplication::clipboard()->setText(link);
|
||||
showToast(tr::lng_group_invite_copied(tr::now));
|
||||
showToast(copied.isEmpty()
|
||||
? tr::lng_group_invite_copied(tr::now)
|
||||
: copied);
|
||||
};
|
||||
auto submitCallback = [=](
|
||||
std::vector<not_null<Data::Thread*>> &&result,
|
||||
|
@ -1228,8 +1236,9 @@ object_ptr<Ui::BoxContent> ShareInviteLinkBox(
|
|||
|
||||
object_ptr<Ui::BoxContent> InviteLinkQrBox(
|
||||
const QString &link,
|
||||
rpl::producer<QString> title,
|
||||
rpl::producer<QString> about) {
|
||||
return Box(QrBox, link, std::move(about), [=](
|
||||
return Box(QrBox, link, std::move(title), std::move(about), [=](
|
||||
const QImage &image,
|
||||
std::shared_ptr<Ui::Show> show) {
|
||||
auto mime = std::make_unique<QMimeData>();
|
||||
|
|
|
@ -41,12 +41,15 @@ void AddPermanentLinkBlock(
|
|||
void CopyInviteLink(std::shared_ptr<Ui::Show> show, const QString &link);
|
||||
[[nodiscard]] object_ptr<Ui::BoxContent> ShareInviteLinkBox(
|
||||
not_null<PeerData*> peer,
|
||||
const QString &link);
|
||||
const QString &link,
|
||||
const QString &copied = {});
|
||||
[[nodiscard]] object_ptr<Ui::BoxContent> ShareInviteLinkBox(
|
||||
not_null<Main::Session*> session,
|
||||
const QString &link);
|
||||
const QString &link,
|
||||
const QString &copied = {});
|
||||
[[nodiscard]] object_ptr<Ui::BoxContent> InviteLinkQrBox(
|
||||
const QString &link,
|
||||
rpl::producer<QString> title,
|
||||
rpl::producer<QString> about);
|
||||
[[nodiscard]] object_ptr<Ui::BoxContent> RevokeLinkBox(
|
||||
not_null<PeerData*> peer,
|
||||
|
|
|
@ -214,38 +214,11 @@ object_ptr<Ui::BoxContent> DeleteAllRevokedBox(
|
|||
});
|
||||
}
|
||||
|
||||
not_null<Ui::SettingsButton*> AddCreateLinkButton(
|
||||
[[nodiscard]] not_null<Ui::SettingsButton*> AddCreateLinkButton(
|
||||
not_null<Ui::VerticalLayout*> container) {
|
||||
const auto result = container->add(
|
||||
object_ptr<Ui::SettingsButton>(
|
||||
container,
|
||||
tr::lng_group_invite_add(),
|
||||
st::inviteLinkCreate),
|
||||
return container->add(
|
||||
MakeCreateLinkButton(container, tr::lng_group_invite_add()),
|
||||
style::margins(0, st::inviteLinkCreateSkip, 0, 0));
|
||||
const auto icon = Ui::CreateChild<Ui::RpWidget>(result);
|
||||
icon->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
const auto size = st::inviteLinkCreateIconSize;
|
||||
icon->resize(size, size);
|
||||
result->heightValue(
|
||||
) | rpl::start_with_next([=](int height) {
|
||||
const auto &st = st::inviteLinkList.item;
|
||||
icon->move(
|
||||
st.photoPosition.x() + (st.photoSize - size) / 2,
|
||||
(height - size) / 2);
|
||||
}, icon->lifetime());
|
||||
icon->paintRequest(
|
||||
) | rpl::start_with_next([=] {
|
||||
auto p = QPainter(icon);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(st::windowBgActive);
|
||||
const auto rect = icon->rect();
|
||||
{
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.drawEllipse(rect);
|
||||
}
|
||||
st::inviteLinkCreateIcon.paintInCenter(p, rect);
|
||||
}, icon->lifetime());
|
||||
return result;
|
||||
}
|
||||
|
||||
Row::Row(
|
||||
|
@ -584,8 +557,10 @@ base::unique_qptr<Ui::PopupMenu> LinksController::createRowContextMenu(
|
|||
ShareInviteLinkBox(_peer, link));
|
||||
}, &st::menuIconShare);
|
||||
result->addAction(tr::lng_group_invite_context_qr(tr::now), [=] {
|
||||
delegate()->peerListUiShow()->showBox(
|
||||
InviteLinkQrBox(link, tr::lng_group_invite_qr_about()));
|
||||
delegate()->peerListUiShow()->showBox(InviteLinkQrBox(
|
||||
link,
|
||||
tr::lng_group_invite_qr_title(),
|
||||
tr::lng_group_invite_qr_about()));
|
||||
}, &st::menuIconQrCode);
|
||||
result->addAction(tr::lng_group_invite_context_edit(tr::now), [=] {
|
||||
delegate()->peerListUiShow()->showBox(EditLinkBox(_peer, data));
|
||||
|
@ -1014,3 +989,42 @@ void ManageInviteLinksBox(
|
|||
|
||||
box->addButton(tr::lng_about_done(), [=] { box->closeBox(); });
|
||||
}
|
||||
|
||||
object_ptr<Ui::SettingsButton> MakeCreateLinkButton(
|
||||
not_null<QWidget*> parent,
|
||||
rpl::producer<QString> text) {
|
||||
auto result = object_ptr<Ui::SettingsButton>(
|
||||
parent,
|
||||
std::move(text),
|
||||
st::inviteLinkCreate);
|
||||
const auto raw = result.data();
|
||||
|
||||
const auto icon = Ui::CreateChild<Ui::RpWidget>(raw);
|
||||
icon->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
|
||||
const auto size = st::inviteLinkCreateIconSize;
|
||||
icon->resize(size, size);
|
||||
|
||||
raw->heightValue(
|
||||
) | rpl::start_with_next([=](int height) {
|
||||
const auto &st = st::inviteLinkList.item;
|
||||
icon->move(
|
||||
st.photoPosition.x() + (st.photoSize - size) / 2,
|
||||
(height - size) / 2);
|
||||
}, icon->lifetime());
|
||||
|
||||
icon->paintRequest(
|
||||
) | rpl::start_with_next([=] {
|
||||
auto p = QPainter(icon);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(st::windowBgActive);
|
||||
const auto rect = icon->rect();
|
||||
{
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.drawEllipse(rect);
|
||||
}
|
||||
st::inviteLinkCreateIcon.paintInCenter(p, rect);
|
||||
}, icon->lifetime());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -11,9 +11,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
|
||||
class PeerData;
|
||||
|
||||
namespace Ui {
|
||||
class SettingsButton;
|
||||
} // namespace Ui
|
||||
|
||||
void ManageInviteLinksBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
not_null<PeerData*> peer,
|
||||
not_null<UserData*> admin,
|
||||
int count,
|
||||
int revokedCount);
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::SettingsButton> MakeCreateLinkButton(
|
||||
not_null<QWidget*> parent,
|
||||
rpl::producer<QString> text);
|
||||
|
|
|
@ -146,6 +146,8 @@ void PreloadSticker(const std::shared_ptr<Data::DocumentMedia> &media) {
|
|||
return tr::lng_business_subtitle_chatbots();
|
||||
case PremiumFeature::ChatIntro:
|
||||
return tr::lng_business_subtitle_chat_intro();
|
||||
case PremiumFeature::ChatLinks:
|
||||
return tr::lng_business_subtitle_chat_links();
|
||||
}
|
||||
Unexpected("PremiumFeature in SectionTitle.");
|
||||
}
|
||||
|
@ -205,6 +207,8 @@ void PreloadSticker(const std::shared_ptr<Data::DocumentMedia> &media) {
|
|||
return tr::lng_business_about_chatbots();
|
||||
case PremiumFeature::ChatIntro:
|
||||
return tr::lng_business_about_chat_intro();
|
||||
case PremiumFeature::ChatLinks:
|
||||
return tr::lng_business_about_chat_links();
|
||||
}
|
||||
Unexpected("PremiumFeature in SectionTitle.");
|
||||
}
|
||||
|
@ -533,6 +537,7 @@ struct VideoPreviewDocument {
|
|||
case PremiumFeature::AwayMessage: return "away_message";
|
||||
case PremiumFeature::BusinessBots: return "business_bots";
|
||||
case PremiumFeature::ChatIntro: return "business_intro";
|
||||
case PremiumFeature::ChatLinks: return "business_links";
|
||||
}
|
||||
return "";
|
||||
}();
|
||||
|
|
|
@ -75,6 +75,7 @@ enum class PremiumFeature {
|
|||
AwayMessage,
|
||||
BusinessBots,
|
||||
ChatIntro,
|
||||
ChatLinks,
|
||||
|
||||
kCount,
|
||||
};
|
||||
|
|
813
Telegram/SourceFiles/settings/business/settings_chat_links.cpp
Normal file
813
Telegram/SourceFiles/settings/business/settings_chat_links.cpp
Normal file
|
@ -0,0 +1,813 @@
|
|||
/*
|
||||
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 "settings/business/settings_chat_links.h"
|
||||
|
||||
#include "api/api_chat_links.h"
|
||||
#include "apiwrap.h"
|
||||
#include "base/event_filter.h"
|
||||
#include "boxes/peers/edit_peer_invite_link.h"
|
||||
#include "boxes/peers/edit_peer_invite_links.h"
|
||||
#include "boxes/premium_preview_box.h"
|
||||
#include "boxes/peer_list_box.h"
|
||||
#include "chat_helpers/emoji_suggestions_widget.h"
|
||||
#include "chat_helpers/message_field.h"
|
||||
#include "chat_helpers/tabbed_panel.h"
|
||||
#include "chat_helpers/tabbed_selector.h"
|
||||
#include "core/application.h"
|
||||
#include "core/ui_integration.h"
|
||||
#include "core/core_settings.h"
|
||||
#include "data/stickers/data_custom_emoji.h"
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_user.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "main/main_account.h"
|
||||
#include "main/main_app_config.h"
|
||||
#include "main/main_session.h"
|
||||
#include "settings/business/settings_recipients_helper.h"
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
#include "ui/controls/emoji_button.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/wrap/slide_wrap.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
#include "styles/style_info.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_menu_icons.h"
|
||||
#include "styles/style_settings.h"
|
||||
|
||||
#include <QtGui/QGuiApplication>
|
||||
|
||||
namespace Settings {
|
||||
namespace {
|
||||
|
||||
constexpr auto kChangesDebounceTimeout = crl::time(1000);
|
||||
|
||||
using ChatLinkData = Api::ChatLink;
|
||||
|
||||
class ChatLinks final : public BusinessSection<ChatLinks> {
|
||||
public:
|
||||
ChatLinks(
|
||||
QWidget *parent,
|
||||
not_null<Window::SessionController*> controller);
|
||||
~ChatLinks();
|
||||
|
||||
[[nodiscard]] rpl::producer<QString> title() override;
|
||||
|
||||
const Ui::RoundRect *bottomSkipRounding() const override {
|
||||
return &_bottomSkipRounding;
|
||||
}
|
||||
|
||||
private:
|
||||
void setupContent(not_null<Window::SessionController*> controller);
|
||||
|
||||
Ui::RoundRect _bottomSkipRounding;
|
||||
|
||||
};
|
||||
|
||||
struct ChatLinkAction {
|
||||
enum class Type {
|
||||
Copy,
|
||||
Share,
|
||||
Rename,
|
||||
Delete,
|
||||
};
|
||||
QString link;
|
||||
Type type = Type::Copy;
|
||||
};
|
||||
|
||||
class Row;
|
||||
|
||||
class RowDelegate {
|
||||
public:
|
||||
virtual not_null<Main::Session*> rowSession() = 0;
|
||||
virtual void rowUpdateRow(not_null<Row*> row) = 0;
|
||||
virtual void rowPaintIcon(
|
||||
QPainter &p,
|
||||
int x,
|
||||
int y,
|
||||
int size) = 0;
|
||||
};
|
||||
|
||||
class Row final : public PeerListRow {
|
||||
public:
|
||||
Row(not_null<RowDelegate*> delegate, const ChatLinkData &data);
|
||||
|
||||
void update(const ChatLinkData &data);
|
||||
|
||||
[[nodiscard]] ChatLinkData data() const;
|
||||
|
||||
QString generateName() override;
|
||||
QString generateShortName() override;
|
||||
PaintRoundImageCallback generatePaintUserpicCallback(
|
||||
bool forceRound) override;
|
||||
|
||||
QSize rightActionSize() const override;
|
||||
QMargins rightActionMargins() const override;
|
||||
void rightActionPaint(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
bool selected,
|
||||
bool actionSelected) override;
|
||||
bool rightActionDisabled() const override {
|
||||
return true;
|
||||
}
|
||||
|
||||
void paintStatusText(
|
||||
Painter &p,
|
||||
const style::PeerListItem &st,
|
||||
int x,
|
||||
int y,
|
||||
int availableWidth,
|
||||
int outerWidth,
|
||||
bool selected) override;
|
||||
|
||||
private:
|
||||
void updateStatus(const ChatLinkData &data);
|
||||
|
||||
const not_null<RowDelegate*> _delegate;
|
||||
ChatLinkData _data;
|
||||
Ui::Text::String _status;
|
||||
Ui::Text::String _clicks;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] uint64 ComputeRowId(const ChatLinkData &data) {
|
||||
return UniqueRowIdFromString(data.link);
|
||||
}
|
||||
|
||||
[[nodiscard]] QString ComputeClicks(const ChatLinkData &link) {
|
||||
return link.clicks
|
||||
? tr::lng_chat_links_clicks(tr::now, lt_count, link.clicks)
|
||||
: tr::lng_chat_links_no_clicks(tr::now);
|
||||
}
|
||||
|
||||
Row::Row(not_null<RowDelegate*> delegate, const ChatLinkData &data)
|
||||
: PeerListRow(ComputeRowId(data))
|
||||
, _delegate(delegate)
|
||||
, _data(data) {
|
||||
setCustomStatus(QString());
|
||||
updateStatus(data);
|
||||
}
|
||||
|
||||
void Row::updateStatus(const ChatLinkData &data) {
|
||||
const auto context = Core::MarkedTextContext{
|
||||
.session = _delegate->rowSession(),
|
||||
.customEmojiRepaint = [=] { _delegate->rowUpdateRow(this); },
|
||||
};
|
||||
_status.setMarkedText(
|
||||
st::messageTextStyle,
|
||||
data.message,
|
||||
kMarkupTextOptions,
|
||||
context);
|
||||
_clicks.setText(st::messageTextStyle, ComputeClicks(data));
|
||||
}
|
||||
|
||||
void Row::update(const ChatLinkData &data) {
|
||||
_data = data;
|
||||
updateStatus(data);
|
||||
refreshName(st::inviteLinkList.item);
|
||||
_delegate->rowUpdateRow(this);
|
||||
}
|
||||
|
||||
ChatLinkData Row::data() const {
|
||||
return _data;
|
||||
}
|
||||
|
||||
QString Row::generateName() {
|
||||
if (!_data.title.isEmpty()) {
|
||||
return _data.title;
|
||||
}
|
||||
auto result = _data.link;
|
||||
return result.replace(
|
||||
u"https://"_q,
|
||||
QString()
|
||||
);
|
||||
}
|
||||
|
||||
QString Row::generateShortName() {
|
||||
return generateName();
|
||||
}
|
||||
|
||||
PaintRoundImageCallback Row::generatePaintUserpicCallback(bool forceRound) {
|
||||
return [=](
|
||||
QPainter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
int size) {
|
||||
_delegate->rowPaintIcon(p, x, y, size);
|
||||
};
|
||||
}
|
||||
|
||||
QSize Row::rightActionSize() const {
|
||||
return QSize(
|
||||
_clicks.maxWidth(),
|
||||
st::inviteLinkThreeDotsIcon.height());
|
||||
}
|
||||
|
||||
QMargins Row::rightActionMargins() const {
|
||||
return QMargins(
|
||||
0,
|
||||
(st::inviteLinkList.item.height - rightActionSize().height()) / 2,
|
||||
st::inviteLinkThreeDotsSkip,
|
||||
0);
|
||||
}
|
||||
|
||||
void Row::rightActionPaint(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
bool selected,
|
||||
bool actionSelected) {
|
||||
p.setPen(selected ? st::windowSubTextFgOver : st::windowSubTextFg);
|
||||
_clicks.draw(p, x, y, outerWidth);
|
||||
}
|
||||
|
||||
void Row::paintStatusText(
|
||||
Painter &p,
|
||||
const style::PeerListItem &st,
|
||||
int x,
|
||||
int y,
|
||||
int availableWidth,
|
||||
int outerWidth,
|
||||
bool selected) {
|
||||
p.setPen(selected ? st.statusFgOver : st.statusFg);
|
||||
_status.draw(p, {
|
||||
.position = { x, y },
|
||||
.outerWidth = outerWidth,
|
||||
.availableWidth = availableWidth,
|
||||
.palette = &st::defaultTextPalette,
|
||||
.spoiler = Ui::Text::DefaultSpoilerCache(),
|
||||
.now = crl::now(),
|
||||
.elisionLines = 1,
|
||||
});
|
||||
}
|
||||
|
||||
class LinksController final
|
||||
: public PeerListController
|
||||
, public RowDelegate
|
||||
, public base::has_weak_ptr {
|
||||
public:
|
||||
explicit LinksController(not_null<Window::SessionController*> window);
|
||||
|
||||
[[nodiscard]] rpl::producer<int> fullCountValue() const {
|
||||
return _count.value();
|
||||
}
|
||||
|
||||
void prepare() override;
|
||||
void rowClicked(not_null<PeerListRow*> row) override;
|
||||
void rowRightActionClicked(not_null<PeerListRow*> row) override;
|
||||
base::unique_qptr<Ui::PopupMenu> rowContextMenu(
|
||||
QWidget *parent,
|
||||
not_null<PeerListRow*> row) override;
|
||||
Main::Session &session() const override;
|
||||
|
||||
not_null<Main::Session*> rowSession() override;
|
||||
void rowUpdateRow(not_null<Row*> row) override;
|
||||
void rowPaintIcon(
|
||||
QPainter &p,
|
||||
int x,
|
||||
int y,
|
||||
int size) override;
|
||||
|
||||
private:
|
||||
void appendRow(const ChatLinkData &data);
|
||||
void prependRow(const ChatLinkData &data);
|
||||
void updateRow(const ChatLinkData &data);
|
||||
bool removeRow(const QString &link);
|
||||
|
||||
void showRowMenu(
|
||||
not_null<PeerListRow*> row,
|
||||
bool highlightRow);
|
||||
|
||||
[[nodiscard]] base::unique_qptr<Ui::PopupMenu> createRowContextMenu(
|
||||
QWidget *parent,
|
||||
not_null<PeerListRow*> row);
|
||||
|
||||
const not_null<Window::SessionController*> _window;
|
||||
const not_null<Main::Session*> _session;
|
||||
rpl::variable<int> _count;
|
||||
base::unique_qptr<Ui::PopupMenu> _menu;
|
||||
|
||||
QImage _icon;
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
struct LinksList {
|
||||
not_null<Ui::RpWidget*> widget;
|
||||
not_null<LinksController*> controller;
|
||||
};
|
||||
|
||||
LinksList AddLinksList(
|
||||
not_null<Window::SessionController*> window,
|
||||
not_null<Ui::VerticalLayout*> container) {
|
||||
auto &lifetime = container->lifetime();
|
||||
const auto delegate = lifetime.make_state<PeerListContentDelegateShow>(
|
||||
window->uiShow());
|
||||
const auto controller = lifetime.make_state<LinksController>(window);
|
||||
controller->setStyleOverrides(&st::inviteLinkList);
|
||||
const auto content = container->add(object_ptr<PeerListContent>(
|
||||
container,
|
||||
controller));
|
||||
delegate->setContent(content);
|
||||
controller->setDelegate(delegate);
|
||||
|
||||
return { content, controller };
|
||||
}
|
||||
|
||||
void EditChatLinkBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
not_null<Window::SessionController*> controller,
|
||||
ChatLinkData data,
|
||||
Fn<void(ChatLinkData, Fn<void()> close)> submit) {
|
||||
box->setTitle(data.link.isEmpty()
|
||||
? tr::lng_chat_link_new_title()
|
||||
: tr::lng_chat_link_edit_title());
|
||||
|
||||
box->setWidth(st::boxWideWidth);
|
||||
|
||||
Ui::AddDividerText(
|
||||
box->verticalLayout(),
|
||||
tr::lng_chat_link_description());
|
||||
|
||||
const auto peer = controller->session().user();
|
||||
const auto outer = box->getDelegate()->outerContainer();
|
||||
const auto field = box->addRow(
|
||||
object_ptr<Ui::InputField>(
|
||||
box.get(),
|
||||
st::settingsChatLinkField,
|
||||
Ui::InputField::Mode::MultiLine,
|
||||
tr::lng_chat_link_placeholder()));
|
||||
box->setFocusCallback([=] {
|
||||
field->setFocusFast();
|
||||
});
|
||||
|
||||
Ui::AddDivider(box->verticalLayout());
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
|
||||
const auto title = box->addRow(object_ptr<Ui::InputField>(
|
||||
box.get(),
|
||||
st::defaultInputField,
|
||||
tr::lng_chat_link_name(),
|
||||
data.title));
|
||||
|
||||
const auto emojiToggle = Ui::CreateChild<Ui::EmojiButton>(
|
||||
field->parentWidget(),
|
||||
st::defaultComposeFiles.emoji);
|
||||
|
||||
using Selector = ChatHelpers::TabbedSelector;
|
||||
auto &lifetime = box->lifetime();
|
||||
const auto emojiPanel = lifetime.make_state<ChatHelpers::TabbedPanel>(
|
||||
outer,
|
||||
controller,
|
||||
object_ptr<Selector>(
|
||||
nullptr,
|
||||
controller->uiShow(),
|
||||
Window::GifPauseReason::Layer,
|
||||
Selector::Mode::EmojiOnly));
|
||||
emojiPanel->setDesiredHeightValues(
|
||||
1.,
|
||||
st::emojiPanMinHeight / 2,
|
||||
st::emojiPanMinHeight);
|
||||
emojiPanel->hide();
|
||||
emojiPanel->selector()->setCurrentPeer(peer);
|
||||
emojiPanel->selector()->emojiChosen(
|
||||
) | rpl::start_with_next([=](ChatHelpers::EmojiChosen data) {
|
||||
Ui::InsertEmojiAtCursor(field->textCursor(), data.emoji);
|
||||
}, field->lifetime());
|
||||
emojiPanel->selector()->customEmojiChosen(
|
||||
) | rpl::start_with_next([=](ChatHelpers::FileChosen data) {
|
||||
Data::InsertCustomEmoji(field, data.document);
|
||||
}, field->lifetime());
|
||||
|
||||
emojiToggle->installEventFilter(emojiPanel);
|
||||
emojiToggle->addClickHandler([=] {
|
||||
emojiPanel->toggleAnimated();
|
||||
});
|
||||
|
||||
const auto allow = [](not_null<DocumentData*>) { return true; };
|
||||
InitMessageFieldHandlers(
|
||||
controller,
|
||||
field,
|
||||
Window::GifPauseReason::Layer,
|
||||
allow);
|
||||
Ui::Emoji::SuggestionsController::Init(
|
||||
outer,
|
||||
field,
|
||||
&controller->session(),
|
||||
{ .suggestCustomEmoji = true, .allowCustomWithoutPremium = allow });
|
||||
|
||||
field->setSubmitSettings(Core::App().settings().sendSubmitWay());
|
||||
field->setMaxHeight(st::defaultComposeFiles.caption.heightMax);
|
||||
|
||||
const auto save = [=] {
|
||||
auto copy = data;
|
||||
copy.title = title->getLastText().trimmed();
|
||||
auto textWithTags = field->getTextWithAppliedMarkdown();
|
||||
copy.message = TextWithEntities{
|
||||
textWithTags.text,
|
||||
TextUtilities::ConvertTextTagsToEntities(textWithTags.tags)
|
||||
};
|
||||
submit(copy, crl::guard(box, [=] {
|
||||
box->closeBox();
|
||||
}));
|
||||
};
|
||||
const auto updateEmojiPanelGeometry = [=] {
|
||||
const auto parent = emojiPanel->parentWidget();
|
||||
const auto global = emojiToggle->mapToGlobal({ 0, 0 });
|
||||
const auto local = parent->mapFromGlobal(global);
|
||||
emojiPanel->moveBottomRight(
|
||||
local.y(),
|
||||
local.x() + emojiToggle->width() * 3);
|
||||
};
|
||||
const auto filterCallback = [=](not_null<QEvent*> event) {
|
||||
const auto type = event->type();
|
||||
if (type == QEvent::Move || type == QEvent::Resize) {
|
||||
// updateEmojiPanelGeometry uses not only container geometry, but
|
||||
// also container children geometries that will be updated later.
|
||||
crl::on_main(emojiPanel, updateEmojiPanelGeometry);
|
||||
}
|
||||
return base::EventFilterResult::Continue;
|
||||
};
|
||||
base::install_event_filter(emojiPanel, outer, filterCallback);
|
||||
|
||||
field->submits(
|
||||
) | rpl::start_with_next([=] {
|
||||
title->setFocus();
|
||||
}, field->lifetime());
|
||||
field->cancelled(
|
||||
) | rpl::start_with_next([=] {
|
||||
box->closeBox();
|
||||
}, field->lifetime());
|
||||
|
||||
title->submits(
|
||||
) | rpl::start_with_next(save, title->lifetime());
|
||||
|
||||
rpl::combine(
|
||||
box->sizeValue(),
|
||||
field->geometryValue()
|
||||
) | rpl::start_with_next([=](QSize outer, QRect inner) {
|
||||
emojiToggle->moveToLeft(
|
||||
inner.x() + inner.width() - emojiToggle->width(),
|
||||
inner.y() + st::settingsChatLinkEmojiTop);
|
||||
emojiToggle->update();
|
||||
crl::on_main(emojiPanel, updateEmojiPanelGeometry);
|
||||
}, emojiToggle->lifetime());
|
||||
|
||||
const auto initial = TextWithTags{
|
||||
data.message.text,
|
||||
TextUtilities::ConvertEntitiesToTextTags(data.message.entities)
|
||||
};
|
||||
field->setTextWithTags(initial, Ui::InputField::HistoryAction::Clear);
|
||||
auto cursor = field->textCursor();
|
||||
cursor.movePosition(QTextCursor::End);
|
||||
field->setTextCursor(cursor);
|
||||
|
||||
const auto checkChangedTimer = lifetime.make_state<base::Timer>([=] {
|
||||
if (field->getTextWithAppliedMarkdown() == initial) {
|
||||
box->setCloseByOutsideClick(true);
|
||||
}
|
||||
});
|
||||
field->changes(
|
||||
) | rpl::start_with_next([=] {
|
||||
checkChangedTimer->callOnce(kChangesDebounceTimeout);
|
||||
box->setCloseByOutsideClick(false);
|
||||
}, field->lifetime());
|
||||
|
||||
box->addButton(tr::lng_settings_save(), save);
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
}
|
||||
|
||||
void EditChatLink(
|
||||
not_null<Window::SessionController*> window,
|
||||
not_null<Main::Session*> session,
|
||||
ChatLinkData data) {
|
||||
const auto submitting = std::make_shared<bool>();
|
||||
const auto submit = [=](ChatLinkData data, Fn<void()> close) {
|
||||
if (std::exchange(*submitting, true)) {
|
||||
return;
|
||||
}
|
||||
const auto done = crl::guard(window, [=](const auto&) {
|
||||
window->showToast(tr::lng_chat_link_saved(tr::now));
|
||||
close();
|
||||
});
|
||||
session->api().chatLinks().edit(
|
||||
data.link,
|
||||
data.title,
|
||||
data.message,
|
||||
done);
|
||||
};
|
||||
window->show(Box(
|
||||
EditChatLinkBox,
|
||||
window,
|
||||
data,
|
||||
crl::guard(window, submit)));
|
||||
}
|
||||
|
||||
LinksController::LinksController(
|
||||
not_null<Window::SessionController*> window)
|
||||
: _window(window)
|
||||
, _session(&window->session()) {
|
||||
style::PaletteChanged(
|
||||
) | rpl::start_with_next([=] {
|
||||
_icon = QImage();
|
||||
}, _lifetime);
|
||||
|
||||
_session->api().chatLinks().updates(
|
||||
) | rpl::start_with_next([=](const Api::ChatLinkUpdate &update) {
|
||||
if (!update.now) {
|
||||
if (removeRow(update.was)) {
|
||||
delegate()->peerListRefreshRows();
|
||||
}
|
||||
} else if (update.was.isEmpty()) {
|
||||
prependRow(*update.now);
|
||||
delegate()->peerListRefreshRows();
|
||||
} else {
|
||||
updateRow(*update.now);
|
||||
}
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
void LinksController::prepare() {
|
||||
auto &&list = _session->api().chatLinks().list()
|
||||
| ranges::views::reverse;
|
||||
for (const auto &link : list) {
|
||||
appendRow(link);
|
||||
}
|
||||
delegate()->peerListRefreshRows();
|
||||
}
|
||||
|
||||
void LinksController::rowClicked(not_null<PeerListRow*> row) {
|
||||
showRowMenu(row, true);
|
||||
}
|
||||
|
||||
void LinksController::showRowMenu(
|
||||
not_null<PeerListRow*> row,
|
||||
bool highlightRow) {
|
||||
delegate()->peerListShowRowMenu(row, highlightRow);
|
||||
}
|
||||
|
||||
void LinksController::rowRightActionClicked(not_null<PeerListRow*> row) {
|
||||
delegate()->peerListShowRowMenu(row, true);
|
||||
}
|
||||
|
||||
base::unique_qptr<Ui::PopupMenu> LinksController::rowContextMenu(
|
||||
QWidget *parent,
|
||||
not_null<PeerListRow*> row) {
|
||||
auto result = createRowContextMenu(parent, row);
|
||||
|
||||
if (result) {
|
||||
// First clear _menu value, so that we don't check row positions yet.
|
||||
base::take(_menu);
|
||||
|
||||
// Here unique_qptr is used like a shared pointer, where
|
||||
// not the last destroyed pointer destroys the object, but the first.
|
||||
_menu = base::unique_qptr<Ui::PopupMenu>(result.get());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
base::unique_qptr<Ui::PopupMenu> LinksController::createRowContextMenu(
|
||||
QWidget *parent,
|
||||
not_null<PeerListRow*> row) {
|
||||
const auto real = static_cast<Row*>(row.get());
|
||||
const auto data = real->data();
|
||||
const auto link = data.link;
|
||||
auto result = base::make_unique_q<Ui::PopupMenu>(
|
||||
parent,
|
||||
st::popupMenuWithIcons);
|
||||
result->addAction(tr::lng_group_invite_context_copy(tr::now), [=] {
|
||||
QGuiApplication::clipboard()->setText(link);
|
||||
delegate()->peerListUiShow()->showToast(
|
||||
tr::lng_chat_link_copied(tr::now));
|
||||
}, &st::menuIconCopy);
|
||||
result->addAction(tr::lng_group_invite_context_share(tr::now), [=] {
|
||||
delegate()->peerListUiShow()->showBox(ShareInviteLinkBox(
|
||||
_session,
|
||||
link,
|
||||
tr::lng_chat_link_copied(tr::now)));
|
||||
}, &st::menuIconShare);
|
||||
result->addAction(tr::lng_group_invite_context_qr(tr::now), [=] {
|
||||
delegate()->peerListUiShow()->showBox(InviteLinkQrBox(
|
||||
link,
|
||||
tr::lng_chat_link_qr_title(),
|
||||
tr::lng_chat_link_qr_about()));
|
||||
}, &st::menuIconQrCode);
|
||||
result->addAction(tr::lng_group_invite_context_edit(tr::now), [=] {
|
||||
EditChatLink(_window, _session, data);
|
||||
}, &st::menuIconEdit);
|
||||
result->addAction(tr::lng_group_invite_context_delete(tr::now), [=] {
|
||||
const auto sure = [=](Fn<void()> &&close) {
|
||||
_window->session().api().chatLinks().destroy(link, close);
|
||||
};
|
||||
_window->show(Ui::MakeConfirmBox({
|
||||
.text = tr::lng_chat_link_delete_sure(tr::now),
|
||||
.confirmed = sure,
|
||||
.confirmText = tr::lng_box_delete(tr::now),
|
||||
}));
|
||||
}, &st::menuIconDelete);
|
||||
return result;
|
||||
}
|
||||
|
||||
Main::Session &LinksController::session() const {
|
||||
return *_session;
|
||||
}
|
||||
|
||||
void LinksController::appendRow(const ChatLinkData &data) {
|
||||
delegate()->peerListAppendRow(std::make_unique<Row>(this, data));
|
||||
_count = _count.current() + 1;
|
||||
}
|
||||
|
||||
void LinksController::prependRow(const ChatLinkData &data) {
|
||||
delegate()->peerListPrependRow(std::make_unique<Row>(this, data));
|
||||
_count = _count.current() + 1;
|
||||
}
|
||||
|
||||
void LinksController::updateRow(const ChatLinkData &data) {
|
||||
if (const auto row = delegate()->peerListFindRow(ComputeRowId(data))) {
|
||||
const auto real = static_cast<Row*>(row);
|
||||
real->update(data);
|
||||
delegate()->peerListUpdateRow(row);
|
||||
}
|
||||
}
|
||||
|
||||
bool LinksController::removeRow(const QString &link) {
|
||||
const auto id = UniqueRowIdFromString(link);
|
||||
if (const auto row = delegate()->peerListFindRow(id)) {
|
||||
delegate()->peerListRemoveRow(row);
|
||||
_count = std::max(_count.current() - 1, 0);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
not_null<Main::Session*> LinksController::rowSession() {
|
||||
return _session;
|
||||
}
|
||||
|
||||
void LinksController::rowUpdateRow(not_null<Row*> row) {
|
||||
delegate()->peerListUpdateRow(row);
|
||||
}
|
||||
|
||||
void LinksController::rowPaintIcon(
|
||||
QPainter &p,
|
||||
int x,
|
||||
int y,
|
||||
int size) {
|
||||
const auto skip = st::inviteLinkIconSkip;
|
||||
const auto inner = size - 2 * skip;
|
||||
const auto bg = &st::msgFile1Bg;
|
||||
const auto stroke = st::inviteLinkIconStroke;
|
||||
if (_icon.isNull()) {
|
||||
_icon = QImage(
|
||||
QSize(inner, inner) * style::DevicePixelRatio(),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
_icon.fill(Qt::transparent);
|
||||
_icon.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
|
||||
auto p = QPainter(&_icon);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(*bg);
|
||||
{
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
auto rect = QRect(0, 0, inner, inner);
|
||||
p.drawEllipse(rect);
|
||||
}
|
||||
st::inviteLinkIcon.paintInCenter(p, { 0, 0, inner, inner });
|
||||
}
|
||||
p.drawImage(x + skip, y + skip, _icon);
|
||||
}
|
||||
|
||||
ChatLinks::ChatLinks(
|
||||
QWidget *parent,
|
||||
not_null<Window::SessionController*> controller)
|
||||
: BusinessSection(parent, controller)
|
||||
, _bottomSkipRounding(st::boxRadius, st::boxDividerBg) {
|
||||
setupContent(controller);
|
||||
}
|
||||
|
||||
ChatLinks::~ChatLinks() = default;
|
||||
|
||||
rpl::producer<QString> ChatLinks::title() {
|
||||
return tr::lng_chat_links_title();
|
||||
}
|
||||
|
||||
void ChatLinks::setupContent(
|
||||
not_null<Window::SessionController*> controller) {
|
||||
using namespace rpl::mappers;
|
||||
|
||||
const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
|
||||
|
||||
AddDividerTextWithLottie(content, {
|
||||
.lottie = u"chat_link"_q,
|
||||
.lottieSize = st::settingsCloudPasswordIconSize,
|
||||
.lottieMargins = st::peerAppearanceIconPadding,
|
||||
.showFinished = showFinishes() | rpl::take(1),
|
||||
.about = tr::lng_chat_links_about(Ui::Text::WithEntities),
|
||||
.aboutMargins = st::peerAppearanceCoverLabelMargin,
|
||||
});
|
||||
|
||||
Ui::AddSkip(content);
|
||||
|
||||
const auto limit = controller->session().account().appConfig().get<int>(
|
||||
u"business_chat_links_limit"_q,
|
||||
100);
|
||||
const auto add = content->add(
|
||||
object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(
|
||||
content,
|
||||
MakeCreateLinkButton(
|
||||
content,
|
||||
tr::lng_chat_links_create_link()))
|
||||
)->setDuration(0);
|
||||
|
||||
const auto list = AddLinksList(controller, content);
|
||||
add->toggleOn(list.controller->fullCountValue() | rpl::map(_1 < limit));
|
||||
add->finishAnimating();
|
||||
|
||||
add->entity()->setClickedCallback([=] {
|
||||
if (!controller->session().premium()) {
|
||||
ShowPremiumPreviewToBuy(
|
||||
controller,
|
||||
PremiumFeature::ChatLinks);
|
||||
return;
|
||||
}
|
||||
const auto submitting = std::make_shared<bool>();
|
||||
const auto submit = [=](ChatLinkData data, Fn<void()> close) {
|
||||
if (std::exchange(*submitting, true)) {
|
||||
return;
|
||||
}
|
||||
const auto done = [=](const auto&) {
|
||||
controller->showToast(tr::lng_chat_link_saved(tr::now));
|
||||
close();
|
||||
};
|
||||
controller->session().api().chatLinks().create(
|
||||
data.title,
|
||||
data.message,
|
||||
done);
|
||||
};
|
||||
controller->show(Box(
|
||||
EditChatLinkBox,
|
||||
controller,
|
||||
ChatLinkData(),
|
||||
crl::guard(this, submit)));
|
||||
});
|
||||
|
||||
Ui::AddSkip(content);
|
||||
|
||||
const auto self = controller->session().user();
|
||||
const auto username = self->username();
|
||||
const auto make = [&](std::vector<QString> links) {
|
||||
Expects(!links.empty());
|
||||
|
||||
for (auto &link : links) {
|
||||
link = controller->session().createInternalLink(link);
|
||||
}
|
||||
return (links.size() > 1)
|
||||
? tr::lng_chat_links_footer_both(
|
||||
tr::now,
|
||||
lt_username,
|
||||
Ui::Text::Link(links[0], "https://" + links[0]),
|
||||
lt_link,
|
||||
Ui::Text::Link(links[1], "https://" + links[1]),
|
||||
Ui::Text::WithEntities)
|
||||
: Ui::Text::Link(links[0], "https://" + links[0]);
|
||||
};
|
||||
auto links = !username.isEmpty()
|
||||
? make({ username, '+' + self->phone() })
|
||||
: make({ '+' + self->phone() });
|
||||
Ui::AddDividerText(
|
||||
content,
|
||||
tr::lng_chat_links_footer(
|
||||
lt_links,
|
||||
rpl::single(std::move(links)),
|
||||
Ui::Text::WithEntities),
|
||||
st::settingsChatbotsBottomTextMargin,
|
||||
RectPart::Top);
|
||||
|
||||
Ui::ResizeFitChild(this, content);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Type ChatLinksId() {
|
||||
return ChatLinks::Id();
|
||||
}
|
||||
|
||||
} // namespace Settings
|
16
Telegram/SourceFiles/settings/business/settings_chat_links.h
Normal file
16
Telegram/SourceFiles/settings/business/settings_chat_links.h
Normal file
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
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 "settings/settings_type.h"
|
||||
|
||||
namespace Settings {
|
||||
|
||||
[[nodiscard]] Type ChatLinksId();
|
||||
|
||||
} // namespace Settings
|
|
@ -111,8 +111,10 @@ void QuickReplies::setupContent(
|
|||
showOther(ShortcutMessagesId(id));
|
||||
close();
|
||||
};
|
||||
controller->show(
|
||||
Box(EditShortcutNameBox, QString(), crl::guard(this, submit)));
|
||||
controller->show(Box(
|
||||
EditShortcutNameBox,
|
||||
QString(),
|
||||
crl::guard(this, submit)));
|
||||
});
|
||||
if (count > 0) {
|
||||
AddSkip(addWrap);
|
||||
|
|
|
@ -111,6 +111,7 @@ settingsBusinessIconGreeting: icon {{ "settings/premium/status", settingsIconFg
|
|||
settingsBusinessIconAway: icon {{ "settings/premium/business/business_away", settingsIconFg }};
|
||||
settingsBusinessIconChatbots: icon {{ "settings/premium/business/business_chatbots", settingsIconFg }};
|
||||
settingsBusinessIconChatIntro: icon {{ "settings/premium/intro", settingsIconFg }};
|
||||
settingsBusinessIconChatLinks: icon {{ "settings/premium/links", settingsIconFg }};
|
||||
|
||||
settingsPremiumNewBadge: FlatLabel(defaultFlatLabel) {
|
||||
style: TextStyle(semiboldTextStyle) {
|
||||
|
@ -648,3 +649,23 @@ settingsChatIntroField: InputField(defaultMultiSelectSearchField) {
|
|||
textMargins: margins(2px, 0px, 32px, 0px);
|
||||
}
|
||||
settingsChatIntroFieldMargins: margins(20px, 15px, 20px, 8px);
|
||||
|
||||
settingsChatLinkEmojiTop: 2px;
|
||||
settingsChatLinkField: InputField(defaultInputField) {
|
||||
textBg: transparent;
|
||||
textMargins: margins(2px, 8px, 2px, 8px);
|
||||
|
||||
placeholderFg: placeholderFg;
|
||||
placeholderFgActive: placeholderFgActive;
|
||||
placeholderFgError: placeholderFgActive;
|
||||
placeholderMargins: margins(0px, 0px, 0px, 0px);
|
||||
placeholderScale: 0.;
|
||||
placeholderFont: normalFont;
|
||||
|
||||
border: 0px;
|
||||
borderActive: 0px;
|
||||
|
||||
heightMin: 32px;
|
||||
|
||||
font: normalFont;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
*/
|
||||
#include "settings/settings_business.h"
|
||||
|
||||
#include "api/api_chat_links.h"
|
||||
#include "boxes/premium_preview_box.h"
|
||||
#include "core/click_handler_types.h"
|
||||
#include "data/business/data_business_info.h"
|
||||
|
@ -24,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "main/main_session.h"
|
||||
#include "settings/business/settings_away_message.h"
|
||||
#include "settings/business/settings_chat_intro.h"
|
||||
#include "settings/business/settings_chat_links.h"
|
||||
#include "settings/business/settings_chatbots.h"
|
||||
#include "settings/business/settings_greeting.h"
|
||||
#include "settings/business/settings_location.h"
|
||||
|
@ -41,6 +43,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "ui/wrap/fade_wrap.h"
|
||||
#include "ui/wrap/slide_wrap.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "ui/new_badges.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "apiwrap.h"
|
||||
|
@ -58,6 +61,7 @@ struct Entry {
|
|||
rpl::producer<QString> title;
|
||||
rpl::producer<QString> description;
|
||||
PremiumFeature feature = PremiumFeature::BusinessLocation;
|
||||
bool newBadge = false;
|
||||
};
|
||||
|
||||
using Order = std::vector<QString>;
|
||||
|
@ -70,7 +74,8 @@ using Order = std::vector<QString>;
|
|||
u"business_hours"_q,
|
||||
u"business_location"_q,
|
||||
u"business_bots"_q,
|
||||
u"intro"_q,
|
||||
u"business_intro"_q,
|
||||
u"business_links"_q,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -131,12 +136,23 @@ using Order = std::vector<QString>;
|
|||
},
|
||||
},
|
||||
{
|
||||
u"intro"_q,
|
||||
u"business_intro"_q,
|
||||
Entry{
|
||||
&st::settingsBusinessIconChatIntro,
|
||||
tr::lng_business_subtitle_chat_intro(),
|
||||
tr::lng_business_about_chat_intro(),
|
||||
PremiumFeature::ChatIntro,
|
||||
true
|
||||
},
|
||||
},
|
||||
{
|
||||
u"business_links"_q,
|
||||
Entry{
|
||||
&st::settingsBusinessIconChatLinks,
|
||||
tr::lng_business_subtitle_chat_links(),
|
||||
tr::lng_business_about_chat_links(),
|
||||
PremiumFeature::ChatLinks,
|
||||
true
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -177,6 +193,9 @@ void AddBusinessSummary(
|
|||
descriptionPadding);
|
||||
description->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
|
||||
if (entry.newBadge) {
|
||||
Ui::NewBadge::AddAfterLabel(content, label);
|
||||
}
|
||||
const auto dummy = Ui::CreateChild<Ui::AbstractButton>(content.get());
|
||||
dummy->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
|
||||
|
@ -374,6 +393,7 @@ void Business::setupContent() {
|
|||
owner->chatbots().preload();
|
||||
owner->businessInfo().preload();
|
||||
owner->shortcutMessages().preloadShortcuts();
|
||||
owner->session().api().chatLinks().preload();
|
||||
|
||||
Ui::AddSkip(content, st::settingsFromFileTop);
|
||||
|
||||
|
@ -387,6 +407,7 @@ void Business::setupContent() {
|
|||
case PremiumFeature::QuickReplies: return QuickRepliesId();
|
||||
case PremiumFeature::BusinessBots: return ChatbotsId();
|
||||
case PremiumFeature::ChatIntro: return ChatIntroId();
|
||||
case PremiumFeature::ChatLinks: return ChatLinksId();
|
||||
}
|
||||
Unexpected("Feature in showFeature.");
|
||||
}());
|
||||
|
@ -410,6 +431,8 @@ void Business::setupContent() {
|
|||
return owner->chatbots().loaded();
|
||||
case PremiumFeature::ChatIntro:
|
||||
return owner->session().user()->isFullLoaded();
|
||||
case PremiumFeature::ChatLinks:
|
||||
return owner->session().api().chatLinks().loaded();
|
||||
}
|
||||
Unexpected("Feature in isReady.");
|
||||
};
|
||||
|
@ -429,7 +452,8 @@ void Business::setupContent() {
|
|||
owner->chatbots().changes() | rpl::to_empty,
|
||||
owner->session().changes().peerUpdates(
|
||||
owner->session().user(),
|
||||
Data::PeerUpdate::Flag::FullInfo) | rpl::to_empty
|
||||
Data::PeerUpdate::Flag::FullInfo) | rpl::to_empty,
|
||||
owner->session().api().chatLinks().loadedUpdates()
|
||||
) | rpl::start_with_next(check, content->lifetime());
|
||||
|
||||
AddBusinessSummary(content, _controller, [=](PremiumFeature feature) {
|
||||
|
@ -686,6 +710,8 @@ std::vector<PremiumFeature> BusinessFeaturesOrder(
|
|||
return PremiumFeature::BusinessBots;
|
||||
} else if (s == u"business_intro"_q) {
|
||||
return PremiumFeature::ChatIntro;
|
||||
} else if (s == "business_links"_q) {
|
||||
return PremiumFeature::ChatLinks;
|
||||
}
|
||||
return PremiumFeature::kCount;
|
||||
}) | ranges::views::filter([](PremiumFeature feature) {
|
||||
|
|
Loading…
Add table
Reference in a new issue