tdesktop/Telegram/SourceFiles/settings/business/settings_chat_links.cpp

821 lines
22 KiB
C++

/*
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_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;
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().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() });
auto label = object_ptr<Ui::FlatLabel>(
content,
tr::lng_chat_links_footer(
lt_links,
rpl::single(std::move(links)),
Ui::Text::WithEntities),
st::boxDividerLabel);
label->setClickHandlerFilter([=](ClickHandlerPtr handler, auto) {
QGuiApplication::clipboard()->setText(handler->url());
controller->showToast(tr::lng_chat_link_copied(tr::now));
return false;
});
content->add(object_ptr<Ui::DividerLabel>(
content,
std::move(label),
st::settingsChatbotsBottomTextMargin,
RectPart::Top));
Ui::ResizeFitChild(this, content);
}
} // namespace
Type ChatLinksId() {
return ChatLinks::Id();
}
} // namespace Settings